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