ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
help.c
Go to the documentation of this file.
1
20#include <ascii-chat/log/log.h>
21#include <stdio.h>
22#include <string.h>
23#include <errno.h>
24#include <limits.h>
25
26// ============================================================================
27// Programmatic Section Printers for Help Output
28// ============================================================================
29
43static const char *get_mode_name_from_bitmask(uint32_t mode_bitmask) {
44 // Skip binary-only examples (OPTION_MODE_BINARY = 0x100)
45 if (mode_bitmask & OPTION_MODE_BINARY && !(mode_bitmask & 0x1F)) {
46 return NULL; // Binary-level example, no mode name to prepend
47 }
48
49 // Skip discovery mode (renders like binary app with flags, no mode prefix)
50 if (mode_bitmask == OPTION_MODE_DISCOVERY) {
51 return NULL;
52 }
53
54 // Map individual mode bits to mode names (checked in priority order)
55 if (mode_bitmask & OPTION_MODE_SERVER) {
56 return "server";
57 }
58 if (mode_bitmask & OPTION_MODE_CLIENT) {
59 return "client";
60 }
61 if (mode_bitmask & OPTION_MODE_MIRROR) {
62 return "mirror";
63 }
64 if (mode_bitmask & OPTION_MODE_DISCOVERY_SVC) {
65 return "discovery-service";
66 }
67
68 return NULL; // Unknown or no specific mode
69}
70
78 if (!config)
79 return 0;
80
81 const char *binary_name = PLATFORM_BINARY_NAME;
82
83 int max_col_width = 0;
84 char temp_buf[BUFFER_SIZE_MEDIUM];
85
86 // Check USAGE entries (capped at 45 chars for max first column)
87 for (size_t i = 0; i < config->num_usage_lines; i++) {
88 const usage_descriptor_t *usage = &config->usage_lines[i];
89 int len = 0;
90
91 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, "%s", binary_name);
92
93 if (usage->mode) {
94 const char *colored_mode = colored_string(LOG_COLOR_FATAL, usage->mode);
95 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, " %s", colored_mode);
96 }
97
98 if (usage->positional) {
99 const char *colored_pos = colored_string(LOG_COLOR_INFO, usage->positional);
100 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, " %s", colored_pos);
101 }
102
103 if (usage->show_options) {
104 const char *options_text =
105 (usage->mode && strcmp(usage->mode, "<mode>") == 0) ? "[mode-options...]" : "[options...]";
106 const char *colored_opts = colored_string(LOG_COLOR_WARN, options_text);
107 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, " %s", colored_opts);
108 }
109
110 int w = utf8_display_width(temp_buf);
111 if (w > LAYOUT_COLUMN_WIDTH) {
113 }
114 if (w > max_col_width)
115 max_col_width = w;
116 }
117
118 // Check EXAMPLES entries
119 for (size_t i = 0; i < config->num_examples; i++) {
120 const example_descriptor_t *example = &config->examples[i];
121 int len = 0;
122
123 // Only prepend program name if this is not a utility command
124 if (!example->is_utility_command) {
125 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, "%s", binary_name);
126
127 // Programmatically add mode name based on mode_bitmask
128 const char *mode_name = get_mode_name_from_bitmask(example->mode_bitmask);
129 if (mode_name) {
130 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, " %s", mode_name);
131 }
132 }
133
134 if (example->args) {
135 const char *colored_args = colored_string(LOG_COLOR_INFO, example->args);
136 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, " %s", colored_args);
137 }
138
139 int w = utf8_display_width(temp_buf);
140 if (w > LAYOUT_COLUMN_WIDTH) {
142 }
143 if (w > max_col_width)
144 max_col_width = w;
145 }
146
147 // Check MODES entries (capped at 45 chars)
148 for (size_t i = 0; i < config->num_modes; i++) {
149 const char *colored_name = colored_string(LOG_COLOR_FATAL, config->modes[i].name);
150 int w = utf8_display_width(colored_name);
151 if (w > LAYOUT_COLUMN_WIDTH) {
153 }
154 if (w > max_col_width)
155 max_col_width = w;
156 }
157
158 // Check OPTIONS entries (from descriptors)
159 for (size_t i = 0; i < config->num_descriptors; i++) {
160 const option_descriptor_t *desc = &config->descriptors[i];
161 if (desc->hide_from_mode_help || desc->hide_from_binary_help || !desc->group)
162 continue;
163
164 // Build option display string with separate coloring for short and long flags
165 char opts_buf[BUFFER_SIZE_SMALL];
166 if (desc->short_name && desc->short_name != '\0') {
167 char short_flag[16];
168 safe_snprintf(short_flag, sizeof(short_flag), "-%c", desc->short_name);
169 char long_flag[BUFFER_SIZE_SMALL];
170 safe_snprintf(long_flag, sizeof(long_flag), "--%s", desc->long_name);
171 // Color short flag, add comma, color long flag
172 safe_snprintf(opts_buf, sizeof(opts_buf), "%s, %s", colored_string(LOG_COLOR_WARN, short_flag),
173 colored_string(LOG_COLOR_WARN, long_flag));
174 } else {
175 char long_flag[BUFFER_SIZE_SMALL];
176 safe_snprintf(long_flag, sizeof(long_flag), "--%s", desc->long_name);
177 safe_snprintf(opts_buf, sizeof(opts_buf), "%s", colored_string(LOG_COLOR_WARN, long_flag));
178 }
179 const char *colored_opts = opts_buf;
180
181 int w = utf8_display_width(colored_opts);
182 if (w > LAYOUT_COLUMN_WIDTH) {
184 }
185 if (w > max_col_width)
186 max_col_width = w;
187 }
188
189 // Enforce maximum column width of 45 characters
190 if (max_col_width > 45) {
191 max_col_width = 45;
192 }
193
194 return max_col_width;
195}
196
197/* ============================================================================
198 * Per-Section Column Width Calculation (Abstract)
199 * ============================================================================ */
200
214static int calculate_section_max_col_width(const options_config_t *config, const char *section_type,
215 asciichat_mode_t mode, bool for_binary_help) {
216 if (!config || !section_type) {
217 return 20; // Minimum width
218 }
219
220 int max_width = 0;
221 const char *binary_name = PLATFORM_BINARY_NAME;
222 char temp_buf[BUFFER_SIZE_MEDIUM];
223
224 if (strcmp(section_type, "usage") == 0) {
225 // Calculate max width for USAGE section (use plain text, no ANSI codes)
226 if (config->num_usage_lines == 0)
227 return 20;
228
229 // Get mode name for filtering usage lines (same logic as options_print_help_for_mode)
230 const char *mode_name = NULL;
231 if (!for_binary_help) {
232 switch (mode) {
233 case MODE_SERVER:
234 mode_name = "server";
235 break;
236 case MODE_CLIENT:
237 mode_name = "client";
238 break;
239 case MODE_MIRROR:
240 mode_name = "mirror";
241 break;
243 mode_name = "discovery-service";
244 break;
245 case MODE_DISCOVERY:
246 default:
247 mode_name = NULL; // Binary help uses MODE_DISCOVERY but shows all usage lines
248 break;
249 }
250 }
251
252 for (size_t i = 0; i < config->num_usage_lines; i++) {
253 const usage_descriptor_t *usage = &config->usage_lines[i];
254
255 // Filter usage lines by mode (same logic as options_print_help_for_mode)
256 if (!for_binary_help) {
257 // For mode-specific help, show ONLY the current mode's usage line
258 // Don't show generic binary-level or placeholder lines
259 if (!usage->mode || strcmp(usage->mode, mode_name) != 0) {
260 continue;
261 }
262 }
263
264 int len = 0;
265
266 // Build plain text version for width calculation (no ANSI codes)
267 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, "%s", binary_name);
268
269 if (usage->mode) {
270 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, " %s", usage->mode);
271 }
272
273 if (usage->positional) {
274 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, " %s", usage->positional);
275 }
276
277 if (usage->show_options) {
278 const char *options_text =
279 (usage->mode && strcmp(usage->mode, "<mode>") == 0) ? "[mode-options...]" : "[options...]";
280 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, " %s", options_text);
281 }
282
283 int w = utf8_display_width_n(temp_buf, len);
284 if (w > max_width)
285 max_width = w;
286 }
287
288 // Cap USAGE section at 50 chars max (usage lines are inherently shorter)
289 if (max_width > 50)
290 max_width = 50;
291 } else if (strcmp(section_type, "examples") == 0) {
292 // Calculate max width for EXAMPLES section
293 if (config->num_examples == 0)
294 return 20;
295
296 for (size_t i = 0; i < config->num_examples; i++) {
297 const example_descriptor_t *example = &config->examples[i];
298
299 // Filter examples by mode using bitmask
300 if (for_binary_help) {
301 if (!(example->mode_bitmask & OPTION_MODE_BINARY))
302 continue;
303 } else {
304 uint32_t mode_bitmask = (1 << mode);
305 if (!(example->mode_bitmask & mode_bitmask))
306 continue;
307 }
308
309 int len = 0;
310 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, "%s", binary_name);
311
312 // Include mode name in width calculation (using plain text, not colored)
313 if (!example->is_utility_command) {
314 const char *mode_name = get_mode_name_from_bitmask(example->mode_bitmask);
315 if (mode_name) {
316 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, " %s", mode_name);
317 }
318 }
319
320 if (example->args) {
321 len += safe_snprintf(temp_buf + len, sizeof(temp_buf) - len, " %s", example->args);
322 }
323
324 int w = utf8_display_width_n(temp_buf, len);
325 if (w > max_width)
326 max_width = w;
327 }
328
329 // Allow examples to expand wider (no hard cap) - layout function will handle based on terminal width
330 // Previously capped at 75, but long URLs and other content may exceed this
331 } else if (strcmp(section_type, "modes") == 0) {
332 // Calculate max width for MODES section
333 if (config->num_modes == 0)
334 return 20;
335
336 for (size_t i = 0; i < config->num_modes; i++) {
337 // Use plain text for width calculation (no ANSI codes)
338 int w = utf8_display_width(config->modes[i].name);
339 if (w > max_width)
340 max_width = w;
341 }
342
343 // Cap MODES section at 30 chars max (mode names are short)
344 if (max_width > 30)
345 max_width = 30;
346 } else if (strcmp(section_type, "options") == 0) {
347 // Calculate max width for OPTIONS section
348 if (config->num_descriptors == 0)
349 return 20;
350
351 char option_str[BUFFER_SIZE_MEDIUM];
352
353 for (size_t i = 0; i < config->num_descriptors; i++) {
354 const option_descriptor_t *desc = &config->descriptors[i];
355
356 // Filter by mode and visibility
357 if (!option_applies_to_mode(desc, mode, for_binary_help) || !desc->group || desc->hide_from_mode_help ||
358 desc->hide_from_binary_help) {
359 continue;
360 }
361
362 int option_len = 0;
363
364 if (desc->short_name) {
365 char short_flag[16];
366 safe_snprintf(short_flag, sizeof(short_flag), "-%c", desc->short_name);
367 char long_flag[BUFFER_SIZE_SMALL];
368 safe_snprintf(long_flag, sizeof(long_flag), "--%s", desc->long_name);
369 option_len +=
370 safe_snprintf(option_str + option_len, sizeof(option_str) - option_len, "%s, %s",
372 } else {
373 char long_flag[BUFFER_SIZE_SMALL];
374 safe_snprintf(long_flag, sizeof(long_flag), "--%s", desc->long_name);
375 option_len += safe_snprintf(option_str + option_len, sizeof(option_str) - option_len, "%s",
376 colored_string(LOG_COLOR_WARN, long_flag));
377 }
378
379 if (desc->type != OPTION_TYPE_BOOL && desc->type != OPTION_TYPE_ACTION) {
380 option_len += safe_snprintf(option_str + option_len, sizeof(option_str) - option_len, " ");
381 const char *placeholder = get_option_help_placeholder_str(desc);
382 if (placeholder[0] != '\0') {
383 option_len += safe_snprintf(option_str + option_len, sizeof(option_str) - option_len, "%s",
384 colored_string(LOG_COLOR_INFO, placeholder));
385 }
386 }
387
388 int w = utf8_display_width(option_str);
389 if (w > max_width)
390 max_width = w;
391 }
392 } else if (strcmp(section_type, "positional") == 0) {
393 // Calculate max width for POSITIONAL ARGUMENTS section
394 if (config->num_positional_args == 0)
395 return 20;
396
397 option_mode_bitmask_t current_mode_bitmask = 1U << mode;
398
399 for (size_t pa_idx = 0; pa_idx < config->num_positional_args; pa_idx++) {
400 const positional_arg_descriptor_t *pos_arg = &config->positional_args[pa_idx];
401
402 // Filter by mode_bitmask
403 if (pos_arg->mode_bitmask != 0 && !(pos_arg->mode_bitmask & current_mode_bitmask)) {
404 continue;
405 }
406
407 if (pos_arg->examples) {
408 for (size_t i = 0; i < pos_arg->num_examples; i++) {
409 const char *example = pos_arg->examples[i];
410 const char *p = example;
411
412 // Skip leading spaces
413 while (*p == ' ')
414 p++;
415 const char *first_part = p;
416
417 // Find double-space delimiter
418 while (*p && !(*p == ' ' && *(p + 1) == ' '))
419 p++;
420 int first_len_bytes = (int)(p - first_part);
421
422 int w = utf8_display_width_n(first_part, first_len_bytes);
423 if (w > max_width)
424 max_width = w;
425 }
426 }
427 }
428 }
429
430 // Cap at 75 characters
431 if (max_width > 75)
432 max_width = 75;
433
434 return max_width > 20 ? max_width : 20;
435}
436
445static void print_usage_section(const options_config_t *config, FILE *stream, int term_width, int max_col_width) {
446 if (!config || !stream || config->num_usage_lines == 0) {
447 return;
448 }
449
450 const char *binary_name = PLATFORM_BINARY_NAME;
451
452 fprintf(stream, "%s\n", colored_string(LOG_COLOR_DEBUG, "USAGE"));
453
454 // Build colored syntax strings using colored_string() for all components
455 for (size_t i = 0; i < config->num_usage_lines; i++) {
456 const usage_descriptor_t *usage = &config->usage_lines[i];
457 char usage_buf[BUFFER_SIZE_MEDIUM];
458 int len = 0;
459
460 // Start with binary name
461 len += safe_snprintf(usage_buf + len, sizeof(usage_buf) - len, "%s", binary_name);
462
463 // Add mode if present (magenta color)
464 if (usage->mode) {
465 len +=
466 safe_snprintf(usage_buf + len, sizeof(usage_buf) - len, " %s", colored_string(LOG_COLOR_FATAL, usage->mode));
467 }
468
469 // Add positional args if present (green color)
470 if (usage->positional) {
471 len += safe_snprintf(usage_buf + len, sizeof(usage_buf) - len, " %s",
472 colored_string(LOG_COLOR_INFO, usage->positional));
473 }
474
475 // Add options suffix if requested (yellow color)
476 if (usage->show_options) {
477 const char *options_text =
478 (usage->mode && strcmp(usage->mode, "<mode>") == 0) ? "[mode-options...]" : "[options...]";
479 len +=
480 safe_snprintf(usage_buf + len, sizeof(usage_buf) - len, " %s", colored_string(LOG_COLOR_WARN, options_text));
481 }
482
483 // Print with layout function using global column width
484 layout_print_two_column_row(stream, usage_buf, usage->description, max_col_width, term_width, 0);
485 }
486 fprintf(stream, "\n");
487}
488
496static void print_examples_section(const options_config_t *config, FILE *stream, int term_width, int max_col_width,
497 asciichat_mode_t mode, bool for_binary_help) {
498 if (!config || !stream || config->num_examples == 0) {
499 return;
500 }
501
502 const char *binary_name = PLATFORM_BINARY_NAME;
503
504 fprintf(stream, "%s\n", colored_string(LOG_COLOR_DEBUG, "EXAMPLES"));
505
506 // Build colored command strings using colored_string() for all components
507 for (size_t i = 0; i < config->num_examples; i++) {
508 const example_descriptor_t *example = &config->examples[i];
509
510 // Filter examples based on mode bitmask
511 if (for_binary_help) {
512 // Binary help shows only examples with OPTION_MODE_BINARY flag
513 if (!(example->mode_bitmask & OPTION_MODE_BINARY)) {
514 continue;
515 }
516 } else {
517 // For mode-specific help, show examples with matching mode
518 // Convert mode enum to bitmask
519 uint32_t mode_bitmask = (1 << mode);
520 if (!(example->mode_bitmask & mode_bitmask)) {
521 continue;
522 }
523 }
524
525 char cmd_buf[BUFFER_SIZE_MEDIUM];
526 int len = 0;
527
528 // Only add binary name if this is not a utility command
529 if (!example->is_utility_command) {
530 len += safe_snprintf(cmd_buf + len, sizeof(cmd_buf) - len, "%s", binary_name);
531
532 // Programmatically add mode name based on current mode being displayed
533 // When example applies to multiple modes (via OR'd bitmask), we show the
534 // current mode's name, not the first mode in the bitmask
535 uint32_t current_mode_bitmask = for_binary_help ? OPTION_MODE_BINARY : (1 << mode);
536 const char *mode_name = get_mode_name_from_bitmask(current_mode_bitmask);
537 if (mode_name) {
538 len += safe_snprintf(cmd_buf + len, sizeof(cmd_buf) - len, " %s", colored_string(LOG_COLOR_FATAL, mode_name));
539 }
540 }
541
542 // Add args/flags if present (flags=yellow, arguments=green, utility programs=white)
543 if (example->args) {
544 // Only add space if we've already added something (binary name)
545 if (len > 0) {
546 len += safe_snprintf(cmd_buf + len, sizeof(cmd_buf) - len, " ");
547 }
548
549 // For utility commands, color everything white except flags (yellow)
550 // For regular examples, color flags yellow and arguments green
551 if (example->is_utility_command) {
552 // Utility command: only flags get yellow, everything else is white
553 const char *p = example->args;
554 char current_token[BUFFER_SIZE_SMALL];
555 int token_len = 0;
556
557 while (*p) {
558 if (*p == ' ' || *p == '|' || *p == '>' || *p == '<') {
559 // Flush current token if any
560 if (token_len > 0) {
561 current_token[token_len] = '\0';
562 // Flags (start with -) are yellow, everything else is white
563 if (current_token[0] == '-') {
564 len += safe_snprintf(cmd_buf + len, sizeof(cmd_buf) - len, "%s",
565 colored_string(LOG_COLOR_WARN, current_token));
566 } else {
567 len += safe_snprintf(cmd_buf + len, sizeof(cmd_buf) - len, "%s",
568 colored_string(LOG_COLOR_RESET, current_token));
569 }
570 token_len = 0;
571 }
572 // Add the separator (space, pipe, redirect, etc) in white
573 if (*p != ' ') {
574 len += safe_snprintf(cmd_buf + len, sizeof(cmd_buf) - len, "%s ",
575 colored_string(LOG_COLOR_RESET, (*p == '|') ? "|"
576 : (*p == '>') ? ">"
577 : "<"));
578 } else {
579 len += safe_snprintf(cmd_buf + len, sizeof(cmd_buf) - len, " ");
580 }
581 p++;
582 // Skip multiple spaces
583 while (*p == ' ')
584 p++;
585 } else {
586 current_token[token_len++] = *p;
587 p++;
588 }
589 }
590
591 // Flush last token
592 if (token_len > 0) {
593 current_token[token_len] = '\0';
594 if (current_token[0] == '-') {
595 len += safe_snprintf(cmd_buf + len, sizeof(cmd_buf) - len, "%s",
596 colored_string(LOG_COLOR_WARN, current_token));
597 } else {
598 len += safe_snprintf(cmd_buf + len, sizeof(cmd_buf) - len, "%s",
599 colored_string(LOG_COLOR_RESET, current_token));
600 }
601 }
602 } else {
603 // Regular example: flags=yellow, arguments=green
604 const char *p = example->args;
605 char current_token[BUFFER_SIZE_SMALL];
606 int token_len = 0;
607
608 while (*p) {
609 if (*p == ' ') {
610 // Flush current token if any
611 if (token_len > 0) {
612 current_token[token_len] = '\0';
613 // Color flags (start with -) yellow, arguments green
614 if (current_token[0] == '-') {
615 len += safe_snprintf(cmd_buf + len, sizeof(cmd_buf) - len, "%s ",
616 colored_string(LOG_COLOR_WARN, current_token));
617 } else {
618 len += safe_snprintf(cmd_buf + len, sizeof(cmd_buf) - len, "%s ",
619 colored_string(LOG_COLOR_INFO, current_token));
620 }
621 token_len = 0;
622 }
623 p++;
624 // Skip multiple spaces
625 while (*p == ' ')
626 p++;
627 } else {
628 current_token[token_len++] = *p;
629 p++;
630 }
631 }
632
633 // Flush last token
634 if (token_len > 0) {
635 current_token[token_len] = '\0';
636 if (current_token[0] == '-') {
637 len += safe_snprintf(cmd_buf + len, sizeof(cmd_buf) - len, "%s",
638 colored_string(LOG_COLOR_WARN, current_token));
639 } else {
640 len += safe_snprintf(cmd_buf + len, sizeof(cmd_buf) - len, "%s",
641 colored_string(LOG_COLOR_INFO, current_token));
642 }
643 }
644 }
645
646 // Remove trailing space if added
647 if (len > 0 && cmd_buf[len - 1] == ' ') {
648 len--;
649 }
650 }
651
652 // Print with layout function using global column width
653 layout_print_two_column_row(stream, cmd_buf, example->description, max_col_width, term_width, 0);
654 }
655
656 fprintf(stream, "\n");
657}
658
662static void print_modes_section(const options_config_t *config, FILE *stream, int term_width, int max_col_width) {
663 if (!config || !stream || config->num_modes == 0) {
664 return;
665 }
666
667 fprintf(stream, "%s\n", colored_string(LOG_COLOR_DEBUG, "MODES"));
668
669 // Print each mode with colored name using colored_string() and global column width
670 for (size_t i = 0; i < config->num_modes; i++) {
671 char mode_buf[BUFFER_SIZE_SMALL];
672 safe_snprintf(mode_buf, sizeof(mode_buf), "%s", colored_string(LOG_COLOR_FATAL, config->modes[i].name));
673 layout_print_two_column_row(stream, mode_buf, config->modes[i].description, max_col_width, term_width, 0);
674 }
675
676 fprintf(stream, "\n");
677}
678
682static void print_mode_options_section(FILE *stream, int term_width, int max_col_width) {
683 const char *binary_name = PLATFORM_BINARY_NAME;
684
685 // Print section header with colored "MODE-OPTIONS:" label
686 fprintf(stream, "%s\n", colored_string(LOG_COLOR_DEBUG, "MODE-OPTIONS"));
687
688 // Build colored command with components
689 char usage_buf[512];
690 int len = 0;
691
692 // Binary name (no color)
693 len += safe_snprintf(usage_buf + len, sizeof(usage_buf) - len, "%s ", binary_name);
694
695 // Mode placeholder (magenta)
696 len += safe_snprintf(usage_buf + len, sizeof(usage_buf) - len, "%s", colored_string(LOG_COLOR_FATAL, "<mode>"));
697
698 // Space and help option (yellow)
699 len += safe_snprintf(usage_buf + len, sizeof(usage_buf) - len, " %s", colored_string(LOG_COLOR_WARN, "--help"));
700
701 layout_print_two_column_row(stream, usage_buf, "Show options for a mode", max_col_width, term_width, 0);
702
703 fprintf(stream, "\n");
704}
705
706void options_config_print_usage(const options_config_t *config, FILE *stream) {
707 if (!config || !stream)
708 return;
709
710 // Detect terminal width from COLUMNS env var or use default
711 int term_width = 80;
712 const char *cols_env = SAFE_GETENV("COLUMNS");
713 if (cols_env) {
714 char *endptr;
715 errno = 0;
716 long cols = strtol(cols_env, &endptr, 10);
717 if (*endptr == '\0' && errno == 0 && cols > 40 && cols <= INT_MAX)
718 term_width = (int)cols;
719 else if (*endptr != '\0' || errno != 0 || cols <= 40)
720 log_error("options_config_print_binary_help: Invalid COLUMNS value: %s", cols_env);
721 }
722
723 // Binary-level help uses MODE_DISCOVERY internally
725 bool for_binary_help = true;
726
727 // Calculate per-section column widths (each section is independent, capped at 75)
728 int usage_max_col_width = calculate_section_max_col_width(config, "usage", mode, for_binary_help);
729 int modes_max_col_width = calculate_section_max_col_width(config, "modes", mode, for_binary_help);
730 int examples_max_col_width = calculate_section_max_col_width(config, "examples", mode, for_binary_help);
731 int options_max_col_width = calculate_section_max_col_width(config, "options", mode, for_binary_help);
732
733 // Print programmatically generated sections (USAGE, MODES, MODE-OPTIONS, EXAMPLES)
734 print_usage_section(config, stream, term_width, usage_max_col_width);
735 print_modes_section(config, stream, term_width, modes_max_col_width);
736 print_mode_options_section(stream, term_width, 40); // Keep reasonable width for mode-options
737 print_examples_section(config, stream, term_width, examples_max_col_width, MODE_SERVER, true);
738
739 // Build list of unique groups in order of first appearance
740
741 const char **unique_groups = SAFE_MALLOC(config->num_descriptors * sizeof(const char *), const char **);
742 size_t num_unique_groups = 0;
743
744 for (size_t i = 0; i < config->num_descriptors; i++) {
745 const option_descriptor_t *desc = &config->descriptors[i];
746 // Filter by mode_bitmask - for binary help, show binary options
747 if (!option_applies_to_mode(desc, MODE_SERVER, for_binary_help) || !desc->group) {
748 continue;
749 }
750
751 // Check if this group is already in the list
752 bool group_exists = false;
753 for (size_t j = 0; j < num_unique_groups; j++) {
754 if (unique_groups[j] && strcmp(unique_groups[j], desc->group) == 0) {
755 group_exists = true;
756 break;
757 }
758 }
759
760 // Add new group to list
761 if (!group_exists && num_unique_groups < config->num_descriptors) {
762 unique_groups[num_unique_groups++] = desc->group;
763 }
764 }
765
766 // Print options grouped by group name
767 for (size_t g = 0; g < num_unique_groups; g++) {
768 const char *current_group = unique_groups[g];
769 // Only add leading newline for groups after the first one
770 if (g > 0) {
771 fprintf(stream, "\n");
772 }
773 fprintf(stream, "%s\n", colored_string(LOG_COLOR_DEBUG, current_group));
774
775 // Print all options in this group
776 for (size_t i = 0; i < config->num_descriptors; i++) {
777 const option_descriptor_t *desc = &config->descriptors[i];
778
779 // Skip if not in current group or if doesn't apply to mode
780 if (!option_applies_to_mode(desc, MODE_SERVER, for_binary_help) || !desc->group ||
781 strcmp(desc->group, current_group) != 0) {
782 continue;
783 }
784
785 // Build option string with separate coloring for short and long flags
786 char option_str[BUFFER_SIZE_MEDIUM] = "";
787 int option_len = 0;
788
789 // Short name and long name with separate coloring
790 if (desc->short_name) {
791 char short_flag[16];
792 safe_snprintf(short_flag, sizeof(short_flag), "-%c", desc->short_name);
793 char long_flag[BUFFER_SIZE_SMALL];
794 safe_snprintf(long_flag, sizeof(long_flag), "--%s", desc->long_name);
795 // Color short flag, plain comma-space, color long flag
796 option_len +=
797 safe_snprintf(option_str + option_len, sizeof(option_str) - option_len, "%s, %s",
799 } else {
800 char long_flag[BUFFER_SIZE_SMALL];
801 safe_snprintf(long_flag, sizeof(long_flag), "--%s", desc->long_name);
802 option_len += safe_snprintf(option_str + option_len, sizeof(option_str) - option_len, "%s",
803 colored_string(LOG_COLOR_WARN, long_flag));
804 }
805
806 // Value placeholder (colored green)
807 if (desc->type != OPTION_TYPE_BOOL && desc->type != OPTION_TYPE_ACTION) {
808 option_len += safe_snprintf(option_str + option_len, sizeof(option_str) - option_len, " ");
809 const char *placeholder = get_option_help_placeholder_str(desc);
810 if (placeholder[0] != '\0') {
811 option_len += safe_snprintf(option_str + option_len, sizeof(option_str) - option_len, "%s",
812 colored_string(LOG_COLOR_INFO, placeholder));
813 }
814 }
815
816 // Build description string (plain text, colors applied when printing)
817 char desc_str[BUFFER_SIZE_MEDIUM] = "";
818 int desc_len = 0;
819
820 if (desc->help_text) {
821 desc_len += safe_snprintf(desc_str + desc_len, sizeof(desc_str) - desc_len, "%s", desc->help_text);
822 }
823
824 // Skip adding default if the description already mentions it
825 bool description_has_default =
826 desc->help_text && (strstr(desc->help_text, "(default:") || strstr(desc->help_text, "=default)"));
827
828 if (desc->default_value && !description_has_default) {
829 char default_buf[256];
830 int default_len = format_option_default_value_str(desc, default_buf, sizeof(default_buf));
831 if (default_len > 0) {
832 desc_len +=
833 safe_snprintf(desc_str + desc_len, sizeof(desc_str) - desc_len, " (%s %s)",
835 }
836 }
837
838 if (desc->required) {
839 desc_len += safe_snprintf(desc_str + desc_len, sizeof(desc_str) - desc_len, " [REQUIRED]");
840 }
841
842 // Use layout function with section-specific column width for consistent alignment
843 // option_str already contains colored_string() results, so pass it directly
844 layout_print_two_column_row(stream, option_str, desc_str, options_max_col_width, term_width, 2);
845 }
846 }
847
848 // Cleanup
849 SAFE_FREE(unique_groups);
850
851 fprintf(stream, "\n");
852}
853
860void options_config_print_usage_section(const options_config_t *config, FILE *stream) {
861 if (!config || !stream)
862 return;
863
864 // Detect terminal width from COLUMNS env var or use default
865 int term_width = 80;
866 const char *cols_env = SAFE_GETENV("COLUMNS");
867 if (cols_env) {
868 char *endptr;
869 errno = 0;
870 long cols = strtol(cols_env, &endptr, 10);
871 if (*endptr == '\0' && errno == 0 && cols > 40 && cols <= INT_MAX)
872 term_width = (int)cols;
873 else if (*endptr != '\0' || errno != 0 || cols <= 40)
874 log_error("options_config_print_binary_help: Invalid COLUMNS value: %s", cols_env);
875 }
876
877 // Calculate global max column width across all sections for consistent alignment
878 int max_col_width = options_config_calculate_max_col_width(config);
879
880 // Print only USAGE section
881 print_usage_section(config, stream, term_width, max_col_width);
882}
883
890void options_config_print_options_sections_with_width(const options_config_t *config, FILE *stream, int max_col_width,
891 asciichat_mode_t mode) {
892 if (!config || !stream) {
893 SET_ERRNO(ERROR_INVALID_PARAM, "Config or stream is NULL");
894 return;
895 }
896
897 // Detect terminal width - try actual terminal size first, fallback to COLUMNS env var
898 int term_width = 80;
899 terminal_size_t term_size;
900 if (terminal_get_size(&term_size) == ASCIICHAT_OK && term_size.cols > 40) {
901 term_width = term_size.cols;
902 } else {
903 const char *cols_env = SAFE_GETENV("COLUMNS");
904 if (cols_env) {
905 char *endptr;
906 errno = 0;
907 long cols = strtol(cols_env, &endptr, 10);
908 if (*endptr == '\0' && errno == 0 && cols > 40 && cols <= INT_MAX)
909 term_width = (int)cols;
910 else if (*endptr != '\0' || errno != 0 || cols <= 40)
911 log_error("options_config_print_*: Invalid COLUMNS value: %s", cols_env);
912 }
913 }
914
915 // Calculate column width if not provided
916 if (max_col_width <= 0) {
917 max_col_width = options_config_calculate_max_col_width(config);
918 }
919
920 // CAP max_col_width: 86 if terminal wide, otherwise 45 for narrow first column
921 int max_col_cap = (term_width > 170) ? 86 : 45;
922 if (max_col_width > max_col_cap) {
923 max_col_width = max_col_cap;
924 }
925
926 // Determine if this is binary-level help
927 // Discovery mode IS the binary-level help (shows binary options + discovery options)
928 // Other modes show only mode-specific options
929 bool for_binary_help = (mode == MODE_DISCOVERY);
930 // Build list of unique groups in order of first appearance
931 const char **unique_groups = SAFE_MALLOC(config->num_descriptors * sizeof(const char *), const char **);
932 size_t num_unique_groups = 0;
933
934 // For binary-level help, we now use a two-pass system to order groups,
935 // ensuring binary-level option groups appear before discovery-mode groups.
936 if (for_binary_help) {
937 // Pass 1: Add GENERAL first, then LOGGING if present
938 bool general_added = false;
939 bool logging_added = false;
940 for (size_t i = 0; i < config->num_descriptors; i++) {
941 const option_descriptor_t *desc = &config->descriptors[i];
942 if (desc->group) {
943 if (!general_added && strcmp(desc->group, "GENERAL") == 0) {
944 unique_groups[num_unique_groups++] = "GENERAL";
945 general_added = true;
946 }
947 if (!logging_added && strcmp(desc->group, "LOGGING") == 0) {
948 unique_groups[num_unique_groups++] = "LOGGING";
949 logging_added = true;
950 }
951 if (general_added && logging_added) {
952 break;
953 }
954 }
955 }
956
957 // Pass 2: Collect all other groups that apply for binary help (all modes), if not already added.
958 for (size_t i = 0; i < config->num_descriptors; i++) {
959 const option_descriptor_t *desc = &config->descriptors[i];
960 // An option applies if option_applies_to_mode says so for binary help (which checks OPTION_MODE_ALL)
961 if (option_applies_to_mode(desc, mode, for_binary_help) && desc->group) {
962 // Skip GENERAL and LOGGING groups if we already added them (for binary help)
963 if (strcmp(desc->group, "GENERAL") == 0 || strcmp(desc->group, "LOGGING") == 0) {
964 continue;
965 }
966
967 bool group_exists = false;
968 for (size_t j = 0; j < num_unique_groups; j++) {
969 if (strcmp(unique_groups[j], desc->group) == 0) {
970 group_exists = true;
971 break;
972 }
973 }
974 if (!group_exists) {
975 unique_groups[num_unique_groups++] = desc->group;
976 }
977 } else if (desc->group) {
978 }
979 }
980 } else {
981 // Original logic for other modes
982 for (size_t i = 0; i < config->num_descriptors; i++) {
983 const option_descriptor_t *desc = &config->descriptors[i];
984 if (option_applies_to_mode(desc, mode, for_binary_help) && desc->group) {
985 bool group_exists = false;
986 for (size_t j = 0; j < num_unique_groups; j++) {
987 if (strcmp(unique_groups[j], desc->group) == 0) {
988 group_exists = true;
989 break;
990 }
991 }
992 if (!group_exists) {
993 unique_groups[num_unique_groups++] = desc->group;
994 }
995 }
996 }
997 }
998
999 // Print options grouped by category
1000 for (size_t gi = 0; gi < num_unique_groups; gi++) {
1001 const char *current_group = unique_groups[gi];
1002 // Add newline before each group except the first
1003 if (gi > 0) {
1004 fprintf(stream, "\n");
1005 }
1006 fprintf(stream, "%s\n", colored_string(LOG_COLOR_DEBUG, current_group));
1007
1008 for (size_t i = 0; i < config->num_descriptors; i++) {
1009 const option_descriptor_t *desc = &config->descriptors[i];
1010 if (!option_applies_to_mode(desc, mode, for_binary_help) || !desc->group ||
1011 strcmp(desc->group, current_group) != 0) {
1012 continue;
1013 }
1014
1015 // Build option string (flag part) with separate coloring for short and long flags
1016 char colored_option_str[BUFFER_SIZE_MEDIUM] = "";
1017 int colored_len = 0;
1018
1019 // Short name and long name with separate coloring
1020 if (desc->short_name) {
1021 char short_flag[16];
1022 safe_snprintf(short_flag, sizeof(short_flag), "-%c", desc->short_name);
1023 char long_flag[BUFFER_SIZE_SMALL];
1024 safe_snprintf(long_flag, sizeof(long_flag), "--%s", desc->long_name);
1025 // Color short flag, plain comma-space, color long flag
1026 colored_len +=
1027 safe_snprintf(colored_option_str + colored_len, sizeof(colored_option_str) - colored_len, "%s, %s",
1029 } else {
1030 char long_flag[BUFFER_SIZE_SMALL];
1031 safe_snprintf(long_flag, sizeof(long_flag), "--%s", desc->long_name);
1032 colored_len += safe_snprintf(colored_option_str + colored_len, sizeof(colored_option_str) - colored_len, "%s",
1033 colored_string(LOG_COLOR_WARN, long_flag));
1034 }
1035
1036 // Value placeholder (colored green)
1037 if (desc->type != OPTION_TYPE_BOOL && desc->type != OPTION_TYPE_ACTION) {
1038 colored_len += safe_snprintf(colored_option_str + colored_len, sizeof(colored_option_str) - colored_len, " ");
1039 const char *placeholder = get_option_help_placeholder_str(desc);
1040 if (placeholder[0] != '\0') {
1041 colored_len += safe_snprintf(colored_option_str + colored_len, sizeof(colored_option_str) - colored_len, "%s",
1042 colored_string(LOG_COLOR_INFO, placeholder));
1043 }
1044 }
1045
1046 // Build description with defaults and env vars
1047 char desc_str[1024] = "";
1048 int desc_len = 0;
1049
1050 if (desc->help_text) {
1051 desc_len += safe_snprintf(desc_str + desc_len, sizeof(desc_str) - desc_len, "%s", desc->help_text);
1052 }
1053
1054 // Skip adding default if the description already mentions it
1055 bool description_has_default =
1056 desc->help_text && (strstr(desc->help_text, "(default:") || strstr(desc->help_text, "=default)"));
1057
1058 if (desc->default_value && !description_has_default) {
1059 char default_buf[256];
1060 int default_len = format_option_default_value_str(desc, default_buf, sizeof(default_buf));
1061 if (default_len > 0) {
1062 desc_len +=
1063 safe_snprintf(desc_str + desc_len, sizeof(desc_str) - desc_len, " (%s %s)",
1064 colored_string(LOG_COLOR_FATAL, "default:"), colored_string(LOG_COLOR_FATAL, default_buf));
1065 }
1066 }
1067
1068 if (desc->required) {
1069 desc_len += safe_snprintf(desc_str + desc_len, sizeof(desc_str) - desc_len, " [REQUIRED]");
1070 }
1071
1072 layout_print_two_column_row(stream, colored_option_str, desc_str, max_col_width, term_width, 2);
1073 }
1074 }
1075
1076 // Cleanup
1077 SAFE_FREE(unique_groups);
1078}
1079
1086 options_config_print_options_sections_with_width(config, stream, 0, mode);
1087}
1088
1089// ============================================================================
1090// Unified Help Printing Function
1091// ============================================================================
1092
1105void options_print_help_for_mode(const options_config_t *config, asciichat_mode_t mode, const char *program_name,
1106 const char *description, FILE *desc) {
1107 if (!config || !desc) {
1108 if (desc) {
1109 fprintf(desc, "Error: Failed to create options config\n");
1110 } else {
1111 SET_ERRNO(ERROR_INVALID_PARAM, "Config or desc is NULL");
1112 }
1113 return;
1114 }
1115
1116 // Detect terminal width early so we can decide whether to show ASCII art
1117 int term_width = 80;
1118 terminal_size_t term_size;
1119 if (terminal_get_size(&term_size) == ASCIICHAT_OK && term_size.cols > 40) {
1120 term_width = term_size.cols;
1121 } else {
1122 const char *cols_env = SAFE_GETENV("COLUMNS");
1123 if (cols_env) {
1124 char *endptr;
1125 errno = 0;
1126 long cols = strtol(cols_env, &endptr, 10);
1127 if (*endptr == '\0' && errno == 0 && cols > 40 && cols <= INT_MAX)
1128 term_width = (int)cols;
1129 else if (*endptr != '\0' || errno != 0 || cols <= 40)
1130 log_error("options_config_print_*: Invalid COLUMNS value: %s", cols_env);
1131 }
1132 }
1133
1134 // Print ASCII art logo only if terminal is wide enough (ASCII art is ~52 chars wide)
1135 if (term_width >= 60) {
1136 (void)fprintf(desc, " __ _ ___ ___(_|_) ___| |__ __ _| |_ \n");
1137 (void)fprintf(desc, " / _` / __|/ __| | |_____ / __| '_ \\ / _` | __|\n");
1138 (void)fprintf(desc, "| (_| \\__ \\ (__| | |_____| (__| | | | (_| | |_ \n");
1139 (void)fprintf(desc, " \\__,_|___/\\___|_|_| \\___|_| |_|\\__,_|\\__|\n");
1140 (void)fprintf(desc, "\n");
1141 }
1142
1143 // Print program name and description (color mode name magenta if it's a mode-specific help)
1144 if (program_name) {
1145 const char *space = strchr(program_name, ' ');
1146 if (space && mode >= 0) {
1147 // Mode-specific help: color the mode name
1148 int binary_len = space - program_name;
1149 (void)fprintf(desc, "%.*s %s - %s\n\n", binary_len, program_name, colored_string(LOG_COLOR_FATAL, space + 1),
1150 description);
1151 } else {
1152 // Binary-level help: use colored_string for the program name too
1153 (void)fprintf(desc, "%s - %s\n\n", colored_string(LOG_COLOR_FATAL, program_name), description);
1154 }
1155 }
1156
1157 // Print project links
1158 print_project_links(desc);
1159 (void)fprintf(desc, "\n");
1160
1161 // Determine if this is binary-level help (called for 'ascii-chat --help')
1162 // Binary help uses MODE_DISCOVERY as the mode value
1163 bool for_binary_help = (mode == MODE_DISCOVERY);
1164
1165 // Calculate column widths for USAGE upfront (before printing) for alignment
1166 int usage_max_col_width = calculate_section_max_col_width(config, "usage", mode, for_binary_help);
1167 // Use USAGE width for both USAGE and EXAMPLES sections for consistent alignment
1168 // (don't let long examples push column too far right)
1169 int usage_examples_col_width = usage_max_col_width;
1170
1171 // Print USAGE section (with section-specific column width and mode filtering)
1172 fprintf(desc, "%s\n", colored_string(LOG_COLOR_DEBUG, "USAGE"));
1173 if (config->num_usage_lines > 0) {
1174 // Get mode name for filtering usage lines
1175 const char *mode_name = NULL;
1176 switch (mode) {
1177 case MODE_SERVER:
1178 mode_name = "server";
1179 break;
1180 case MODE_CLIENT:
1181 mode_name = "client";
1182 break;
1183 case MODE_MIRROR:
1184 mode_name = "mirror";
1185 break;
1187 mode_name = "discovery-service";
1188 break;
1189 case MODE_DISCOVERY:
1190 default:
1191 mode_name = NULL; // Binary help shows all usage lines
1192 break;
1193 }
1194
1195 for (size_t i = 0; i < config->num_usage_lines; i++) {
1196 const usage_descriptor_t *usage = &config->usage_lines[i];
1197
1198 // Filter usage lines by mode
1199 if (!for_binary_help) {
1200 // For mode-specific help, show the current mode's usage line and the generic [mode] --help line
1201 bool is_current_mode = (usage->mode && mode_name && strcmp(usage->mode, mode_name) == 0);
1202 bool is_generic_help_line =
1203 (!usage->mode && usage->positional && strcmp(usage->positional, "[mode] --help") == 0);
1204
1205 if (!is_current_mode && !is_generic_help_line) {
1206 continue;
1207 }
1208 }
1209
1210 char usage_buf[BUFFER_SIZE_MEDIUM];
1211 int len = 0;
1212
1213 len += safe_snprintf(usage_buf + len, sizeof(usage_buf) - len, "ascii-chat");
1214
1215 // For mode-specific help, use [mode] placeholder when showing --help usage
1216 if (usage->mode) {
1217 if (!for_binary_help && usage->positional && strcmp(usage->positional, "--help") == 0) {
1218 // Mode-specific help: show [mode] --help as generic placeholder
1219 len +=
1220 safe_snprintf(usage_buf + len, sizeof(usage_buf) - len, " %s", colored_string(LOG_COLOR_FATAL, "[mode]"));
1221 } else {
1222 // Binary help or non-help usage: show actual mode name
1223 len += safe_snprintf(usage_buf + len, sizeof(usage_buf) - len, " %s",
1225 }
1226 }
1227
1228 if (usage->positional) {
1229 len += safe_snprintf(usage_buf + len, sizeof(usage_buf) - len, " %s",
1230 colored_string(LOG_COLOR_INFO, usage->positional));
1231 }
1232
1233 if (usage->show_options) {
1234 const char *options_text =
1235 (usage->mode && strcmp(usage->mode, "<mode>") == 0) ? "[mode-options...]" : "[options...]";
1236 len += safe_snprintf(usage_buf + len, sizeof(usage_buf) - len, " %s",
1237 colored_string(LOG_COLOR_WARN, options_text));
1238 }
1239
1240 layout_print_two_column_row(desc, usage_buf, usage->description, usage_examples_col_width, term_width, 0);
1241 }
1242 }
1243 fprintf(desc, "\n");
1244
1245 // Print MODES section (only for binary-level help)
1246 if (for_binary_help && config->num_modes > 0) {
1247 int modes_max_col_width = calculate_section_max_col_width(config, "modes", mode, for_binary_help);
1248 print_modes_section(config, desc, term_width, modes_max_col_width);
1249 }
1250
1251 // Print positional argument examples (with mode filtering and section-specific column width)
1252 if (config->num_positional_args > 0) {
1253 // First, check if any positional args apply to this mode
1254 option_mode_bitmask_t current_mode_bitmask = 1U << mode;
1255 bool has_applicable_positional_args = false;
1256
1257 for (size_t pa_idx = 0; pa_idx < config->num_positional_args; pa_idx++) {
1258 const positional_arg_descriptor_t *pos_arg = &config->positional_args[pa_idx];
1259
1260 // Filter by mode_bitmask (matching the parsing code logic)
1261 if (pos_arg->mode_bitmask != 0 && !(pos_arg->mode_bitmask & current_mode_bitmask)) {
1262 continue;
1263 }
1264
1265 if (pos_arg->section_heading && pos_arg->examples && pos_arg->num_examples > 0) {
1266 has_applicable_positional_args = true;
1267 break;
1268 }
1269 }
1270
1271 // Only print the section if there are applicable positional args
1272 if (has_applicable_positional_args) {
1273 int positional_max_col_width = calculate_section_max_col_width(config, "positional", mode, false);
1274
1275 for (size_t pa_idx = 0; pa_idx < config->num_positional_args; pa_idx++) {
1276 const positional_arg_descriptor_t *pos_arg = &config->positional_args[pa_idx];
1277
1278 // Filter by mode_bitmask (matching the parsing code logic)
1279 if (pos_arg->mode_bitmask != 0 && !(pos_arg->mode_bitmask & current_mode_bitmask)) {
1280 continue;
1281 }
1282
1283 if (pos_arg->section_heading && pos_arg->examples && pos_arg->num_examples > 0) {
1284 (void)fprintf(desc, "%s\n", colored_string(LOG_COLOR_DEBUG, pos_arg->section_heading));
1285
1286 for (size_t i = 0; i < pos_arg->num_examples; i++) {
1287 const char *example = pos_arg->examples[i];
1288 const char *p = example;
1289 const char *desc_start = NULL;
1290
1291 while (*p == ' ')
1292 p++;
1293 const char *first_part = p;
1294
1295 while (*p && !(*p == ' ' && *(p + 1) == ' '))
1296 p++;
1297 int first_len_bytes = (int)(p - first_part);
1298
1299 while (*p == ' ')
1300 p++;
1301 if (*p) {
1302 desc_start = p;
1303 }
1304
1305 char colored_first_part[256];
1306 safe_snprintf(colored_first_part, sizeof(colored_first_part), "%.*s", first_len_bytes, first_part);
1307 char colored_result[512];
1308 safe_snprintf(colored_result, sizeof(colored_result), "%s",
1309 colored_string(LOG_COLOR_INFO, colored_first_part));
1310
1311 layout_print_two_column_row(desc, colored_result, desc_start ? desc_start : "", positional_max_col_width,
1312 term_width, 0);
1313 }
1314 (void)fprintf(desc, "\n");
1315 }
1316 }
1317 }
1318 }
1319
1320 // Print EXAMPLES section (using USAGE column width for alignment)
1321 print_examples_section(config, desc, term_width, usage_examples_col_width, mode, for_binary_help);
1322
1323 // Print custom sections (after EXAMPLES, before OPTIONS)
1324 if (config->num_custom_sections > 0) {
1325 option_mode_bitmask_t current_mode_bitmask = 1U << mode;
1326 for (size_t i = 0; i < config->num_custom_sections; i++) {
1327 const custom_section_descriptor_t *section = &config->custom_sections[i];
1328
1329 // Filter by mode_bitmask
1330 if (section->mode_bitmask != 0 && !(section->mode_bitmask & current_mode_bitmask)) {
1331 continue;
1332 }
1333
1334 if (section->heading) {
1335 fprintf(desc, "%s\n", colored_string(LOG_COLOR_DEBUG, section->heading));
1336 }
1337
1338 if (section->content) {
1339 // Special handling for KEYBINDINGS section: colorize keybindings and wrap to 70 chars
1340 if (section->heading && strcmp(section->heading, "KEYBINDINGS") == 0) {
1341 // Build colored version with proper keybinding colorization
1342 char colored_output[2048] = "";
1343 const char *src = section->content;
1344 char *dst = colored_output;
1345 size_t remaining = sizeof(colored_output) - 1;
1346
1347 while (*src && remaining > 0) {
1348 // Colorize ? that don't end sentences
1349 if (*src == '?' && *(src + 1) != '\n' && *(src + 1) != '\0') {
1350 const char *colored = colored_string(LOG_COLOR_FATAL, "?");
1351 size_t len = strlen(colored);
1352 if (len <= remaining) {
1353 memcpy(dst, colored, len);
1354 dst += len;
1355 remaining -= len;
1356 src += 1;
1357 } else {
1358 break;
1359 }
1360 } else if (strncmp(src, "Space", 5) == 0 &&
1361 (src > section->content && (*(src - 1) == ',' || *(src - 1) == ' '))) {
1362 const char *colored = colored_string(LOG_COLOR_FATAL, "Space");
1363 size_t len = strlen(colored);
1364 if (len <= remaining) {
1365 memcpy(dst, colored, len);
1366 dst += len;
1367 remaining -= len;
1368 src += 5;
1369 } else {
1370 break;
1371 }
1372 } else if (strncmp(src, "arrows", 6) == 0 &&
1373 (src > section->content && (*(src - 1) == ',' || *(src - 1) == ' '))) {
1374 const char *colored = colored_string(LOG_COLOR_FATAL, "arrows");
1375 size_t len = strlen(colored);
1376 if (len <= remaining) {
1377 memcpy(dst, colored, len);
1378 dst += len;
1379 remaining -= len;
1380 src += 6;
1381 } else {
1382 break;
1383 }
1384 } else if (*src == 'm' && (src > section->content && (*(src - 1) == ',' || *(src - 1) == ' ')) &&
1385 (*(src + 1) == ',' || *(src + 1) == ')')) {
1386 const char *colored = colored_string(LOG_COLOR_FATAL, "m");
1387 size_t len = strlen(colored);
1388 if (len <= remaining) {
1389 memcpy(dst, colored, len);
1390 dst += len;
1391 remaining -= len;
1392 src += 1;
1393 } else {
1394 break;
1395 }
1396 } else if (*src == 'c' && (src > section->content && (*(src - 1) == ',' || *(src - 1) == ' ')) &&
1397 (*(src + 1) == ',' || *(src + 1) == ')')) {
1398 const char *colored = colored_string(LOG_COLOR_FATAL, "c");
1399 size_t len = strlen(colored);
1400 if (len <= remaining) {
1401 memcpy(dst, colored, len);
1402 dst += len;
1403 remaining -= len;
1404 src += 1;
1405 } else {
1406 break;
1407 }
1408 } else if (*src == 'f' && (src > section->content && (*(src - 1) == ',' || *(src - 1) == ' ')) &&
1409 (*(src + 1) == ',' || *(src + 1) == ')')) {
1410 const char *colored = colored_string(LOG_COLOR_FATAL, "f");
1411 size_t len = strlen(colored);
1412 if (len <= remaining) {
1413 memcpy(dst, colored, len);
1414 dst += len;
1415 remaining -= len;
1416 src += 1;
1417 } else {
1418 break;
1419 }
1420 } else if (*src == 'r' && (src > section->content && (*(src - 1) == ',' || *(src - 1) == ' ')) &&
1421 (*(src + 1) == ')')) {
1422 const char *colored = colored_string(LOG_COLOR_FATAL, "r");
1423 size_t len = strlen(colored);
1424 if (len <= remaining) {
1425 memcpy(dst, colored, len);
1426 dst += len;
1427 remaining -= len;
1428 src += 1;
1429 } else {
1430 break;
1431 }
1432 } else {
1433 *dst++ = *src++;
1434 remaining--;
1435 }
1436 }
1437 *dst = '\0';
1438 // Print with 2-space indent, wrapping at min(terminal width, 90)
1439 fprintf(desc, " ");
1440 int keybindings_wrap_width = term_width < 90 ? term_width : 90;
1441 layout_print_wrapped_description(desc, colored_output, 2, keybindings_wrap_width, 0);
1442 fprintf(desc, "\n");
1443 } else {
1444 fprintf(desc, "%s\n", section->content);
1445 }
1446 }
1447
1448 fprintf(desc, "\n");
1449 }
1450 }
1451
1452 // Print options sections (with section-specific column width for options)
1453 int options_max_col_width = calculate_section_max_col_width(config, "options", mode, for_binary_help);
1454 options_config_print_options_sections_with_width(config, desc, options_max_col_width, mode);
1455}
1456
1457void options_struct_destroy(const options_config_t *config, void *options_struct) {
1458 if (!config || !options_struct)
1459 return;
1460
1461 // Free all owned strings
1462 for (size_t i = 0; i < config->num_owned_strings; i++) {
1463 free(config->owned_strings[i]);
1464 }
1465
1466 // Reset owned strings tracking
1467 ((options_config_t *)config)->num_owned_strings = 0;
1468
1469 // NULL out string fields
1470 char *base = (char *)options_struct;
1471 for (size_t i = 0; i < config->num_descriptors; i++) {
1472 const option_descriptor_t *desc = &config->descriptors[i];
1473 if (desc->type == OPTION_TYPE_STRING && desc->owns_memory) {
1474 char **field = (char **)(base + desc->offset);
1475 *field = NULL;
1476 }
1477 }
1478}
const char * get_option_help_placeholder_str(const option_descriptor_t *desc)
Get help placeholder string for an option type.
Definition builder.c:43
int format_option_default_value_str(const option_descriptor_t *desc, char *buf, size_t bufsize)
Format option default value as a string.
Definition builder.c:83
bool option_applies_to_mode(const option_descriptor_t *desc, asciichat_mode_t mode, bool for_binary_help)
Check if an option applies to a specific mode using bitmask.
Definition builder.c:177
Options builder API for flexible command-line option configuration.
@ OPTION_TYPE_STRING
String value (–name foo)
Definition builder.h:164
@ OPTION_TYPE_ACTION
Action that executes and may exit (–list-webcams, etc.)
Definition builder.h:167
@ OPTION_TYPE_BOOL
Boolean flag (–flag, no value)
Definition builder.h:162
Cross-platform error handling utilities.
#define BUFFER_SIZE_SMALL
Small buffer size (256 bytes)
#define BUFFER_SIZE_MEDIUM
Medium buffer size (512 bytes)
unsigned int uint32_t
Definition common.h:58
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_GETENV(name)
Definition common.h:434
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
@ LOG_COLOR_RESET
Definition log/log.h:138
@ LOG_COLOR_FATAL
Definition log/log.h:136
@ LOG_COLOR_DEBUG
Definition log/log.h:132
@ LOG_COLOR_INFO
Definition log/log.h:133
@ LOG_COLOR_WARN
Definition log/log.h:134
asciichat_mode_t
Mode type for options parsing.
option_mode_bitmask_t
Option mode bitmask.
void usage(FILE *desc, asciichat_mode_t mode)
Print usage information for a specific mode.
@ MODE_CLIENT
Client mode - network client options.
@ MODE_DISCOVERY_SERVICE
Discovery server mode - session management and WebRTC signaling.
@ MODE_SERVER
Server mode - network server options.
@ MODE_MIRROR
Mirror mode - local webcam viewing (no network)
@ MODE_DISCOVERY
Discovery mode - participant that can dynamically become host.
@ OPTION_MODE_BINARY
Binary-level options (parsed before mode detection)
@ OPTION_MODE_CLIENT
Client mode (bit 1)
@ OPTION_MODE_DISCOVERY
Discovery mode (bit 4)
@ OPTION_MODE_SERVER
Server mode (bit 0)
@ OPTION_MODE_MIRROR
Mirror mode (bit 2)
@ OPTION_MODE_DISCOVERY_SVC
Discovery server mode (bit 3)
#define PLATFORM_BINARY_NAME
Suppress unused parameter warnings.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
asciichat_error_t terminal_get_size(terminal_size_t *size)
Get terminal size.
int errno
int utf8_display_width_n(const char *str, size_t max_bytes)
Calculate terminal display width of a UTF-8 string up to byte limit.
Definition utf8.c:91
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
int utf8_display_width(const char *str)
Calculate terminal display width of a UTF-8 string.
Definition utf8.c:46
void options_config_print_usage_section(const options_config_t *config, FILE *stream)
Print only the USAGE section.
Definition help.c:860
void options_print_help_for_mode(const options_config_t *config, asciichat_mode_t mode, const char *program_name, const char *description, FILE *desc)
Print help for a specific mode or binary level.
Definition help.c:1105
int options_config_calculate_max_col_width(const options_config_t *config)
Calculate global max column width across all help sections.
Definition help.c:77
void options_config_print_usage(const options_config_t *config, FILE *stream)
Print usage/help text.
Definition help.c:706
void options_config_print_options_sections_with_width(const options_config_t *config, FILE *stream, int max_col_width, asciichat_mode_t mode)
Print everything except the USAGE section.
Definition help.c:890
void options_config_print_options_sections(const options_config_t *config, FILE *stream, asciichat_mode_t mode)
Print everything except the USAGE section (backward compatibility wrapper)
Definition help.c:1085
void options_struct_destroy(const options_config_t *config, void *options_struct)
Clean up memory owned by options struct.
Definition help.c:1457
void layout_print_two_column_row(FILE *stream, const char *first_column, const char *second_column, int first_col_len, int term_width, int continuation_indent_extra)
Print two-column row with automatic wrapping.
Definition layout.c:220
void layout_print_wrapped_description(FILE *stream, const char *text, int indent_width, int term_width, int continuation_indent_extra)
Print text with wrapping and proper indentation.
Definition layout.c:98
Two-column layout helpers for help text and option formatting.
#define LAYOUT_COLUMN_WIDTH
Definition layout.h:19
Application limits and constraints.
📝 Logging API with multiple log levels and terminal output control
Internal declarations for builder implementation.
void print_project_links(FILE *desc)
Print project links with link emoji and colored styling.
Common utilities and helpers for option parsing across all modes.
Custom help section descriptor.
Definition builder.h:389
option_mode_bitmask_t mode_bitmask
Which modes show this section.
Definition builder.h:392
const char * content
Section content.
Definition builder.h:391
const char * heading
Section heading (e.g., "INTERACTIVE CONTROLS")
Definition builder.h:390
Example descriptor for programmatic EXAMPLES generation.
Definition builder.h:310
uint32_t mode_bitmask
Bitmask of modes (OPTION_MODE_SERVER, OPTION_MODE_CLIENT, etc.)
Definition builder.h:311
const char * description
Definition builder.h:313
const char * args
Command-line arguments part of the example.
Definition builder.h:312
bool is_utility_command
True if this is a utility command (don't prepend program name)
Definition builder.h:315
const char * name
Mode name (e.g., "server", "client")
Definition builder.h:322
const char * description
Mode description (e.g., "Run as multi-client video chat server")
Definition builder.h:323
Option descriptor.
Definition builder.h:241
const char * help_text
Description for –help.
Definition builder.h:251
bool hide_from_mode_help
If true, don't show in mode-specific help (binary-level only)
Definition builder.h:254
size_t offset
offsetof(struct, field) - where to store value
Definition builder.h:248
bool hide_from_binary_help
If true, don't show in binary-level help (e.g., in release builds)
Definition builder.h:255
const char * group
Group name for help sections (e.g., "NETWORK OPTIONS")
Definition builder.h:252
const void * default_value
Pointer to default value (or NULL if required)
Definition builder.h:258
bool required
If true, option must be provided.
Definition builder.h:259
bool owns_memory
If true, strings are strdup'd and freed on cleanup.
Definition builder.h:272
const char * long_name
Long option name (e.g., "port")
Definition builder.h:243
char short_name
Short option char (e.g., 'p', or '\0' if none)
Definition builder.h:244
option_type_t type
Value type.
Definition builder.h:247
Options configuration.
Definition builder.h:401
positional_arg_descriptor_t * positional_args
Array of positional argument descriptors.
Definition builder.h:408
size_t num_custom_sections
Number of custom sections.
Definition builder.h:426
size_t num_usage_lines
Number of usage lines.
Definition builder.h:417
option_descriptor_t * descriptors
Array of option descriptors.
Definition builder.h:402
custom_section_descriptor_t * custom_sections
Array of custom help sections.
Definition builder.h:425
size_t num_modes
Number of modes.
Definition builder.h:423
size_t num_positional_args
Number of positional arguments.
Definition builder.h:409
size_t num_descriptors
Number of descriptors.
Definition builder.h:403
char ** owned_strings
Strdup'd strings to free on cleanup.
Definition builder.h:429
size_t num_examples
Number of examples.
Definition builder.h:420
help_mode_descriptor_t * modes
Array of mode descriptors.
Definition builder.h:422
usage_descriptor_t * usage_lines
Array of usage line descriptors.
Definition builder.h:416
example_descriptor_t * examples
Array of example descriptors.
Definition builder.h:419
size_t num_owned_strings
Number of owned strings.
Definition builder.h:430
Positional argument descriptor.
Definition builder.h:357
const char * section_heading
Section heading for examples (e.g., "ADDRESS FORMATS")
Definition builder.h:363
option_mode_bitmask_t mode_bitmask
Which modes this positional arg applies to.
Definition builder.h:380
const char ** examples
Array of example strings with descriptions.
Definition builder.h:364
size_t num_examples
Number of examples.
Definition builder.h:365
Terminal size structure.
Definition terminal.h:163
int cols
Number of columns (width) in terminal.
Definition terminal.h:165
Usage line descriptor for programmatic USAGE generation.
Definition builder.h:296
🖥️ Cross-platform terminal interface for ascii-chat
🔤 UTF-8 Encoding and Decoding Utilities
🔤 String Manipulation and Shell Escaping Utilities