ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
manpage.c
Go to the documentation of this file.
1
13
14/* Storage capacities in elements; text buffers include the terminating NUL. */
15#define OPTIONS_MANPAGE_MODE_BUFFER_SIZE 256
16#define OPTIONS_MANPAGE_CURRENT_AUTO_SECTION_BUFFER_SIZE 128
17#define OPTIONS_MANPAGE_CURRENT_MERGE_SECTION_BUFFER_SIZE 64
18#define OPTIONS_MANPAGE_QUESTION_BUFFER_SIZE 4096
19
21
33#include <ascii-chat/common.h>
34#include <ascii-chat/log/log.h>
36#include <sys/stat.h>
37#include <stdio.h>
38#include <stdlib.h>
39#include <string.h>
40#include <ctype.h>
41
42// =============================================================================
43// Helper Functions (exported for content generators)
44// =============================================================================
45
46const char *escape_groff_special(const char *str) {
47 return str ? str : "";
48}
49
50const char *format_mode_names(option_mode_bitmask_t mode_bitmask) {
51 if (mode_bitmask == 0) {
52 return NULL;
53 }
54
55 option_mode_bitmask_t user_modes_mask = (1 << MODE_SERVER) | (1 << MODE_CLIENT) | (1 << MODE_MIRROR) |
57 (1 << MODE_MCP_SERVER);
58 if ((mode_bitmask & OPTION_MODE_BINARY) && !(mode_bitmask & user_modes_mask)) {
59 return "global";
60 }
61 if ((mode_bitmask & user_modes_mask) == user_modes_mask) {
62 // If all user modes are set, check if binary-level is also set
63 if (mode_bitmask & OPTION_MODE_BINARY) {
64 return "global, client, server, mirror, discovery-service, mcp-server";
65 }
66 return "all modes";
67 }
68
69 static char mode_str[OPTIONS_MANPAGE_MODE_BUFFER_SIZE];
70 mode_str[0] = '\0';
71 int pos = 0;
72
73 // Add global if binary-level is set
74 if (mode_bitmask & OPTION_MODE_BINARY) {
75 pos += safe_snprintf(mode_str + pos, sizeof(mode_str) - pos, "global");
76 }
77
78 // List modes in order: default, client, server, mirror, discovery-service
79 if (mode_bitmask & (1 << MODE_DISCOVERY)) {
80 pos += safe_snprintf(mode_str + pos, sizeof(mode_str) - pos, "%sdefault", pos > 0 ? ", " : "");
81 }
82 if (mode_bitmask & (1 << MODE_CLIENT)) {
83 pos += safe_snprintf(mode_str + pos, sizeof(mode_str) - pos, "%sclient", pos > 0 ? ", " : "");
84 }
85 if (mode_bitmask & (1 << MODE_SERVER)) {
86 pos += safe_snprintf(mode_str + pos, sizeof(mode_str) - pos, "%sserver", pos > 0 ? ", " : "");
87 }
88 if (mode_bitmask & (1 << MODE_MIRROR)) {
89 pos += safe_snprintf(mode_str + pos, sizeof(mode_str) - pos, "%smirror", pos > 0 ? ", " : "");
90 }
91 if (mode_bitmask & (1 << MODE_DISCOVERY_SERVICE)) {
92 pos += safe_snprintf(mode_str + pos, sizeof(mode_str) - pos, "%sdiscovery-service", pos > 0 ? ", " : "");
93 }
94
95 if (mode_bitmask & (1 << MODE_MCP_SERVER)) {
96 pos += safe_snprintf(mode_str + pos, sizeof(mode_str) - pos, "%smcp-server", pos > 0 ? ", " : "");
97 }
98
99 return pos > 0 ? mode_str : NULL;
100}
101
102// =============================================================================
103// Main Public API Functions
104// =============================================================================
105
107 const char *mode_name, const char *output_path,
108 const char *brief_description) {
109 if (!config || !program_name || !brief_description) {
110 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing required parameters for man page generation");
111 }
112
113 FILE *f = NULL;
114 bool should_close = false;
115
116 if (output_path) {
117 f = platform_fopen("file_stream", output_path, "w");
118 if (!f) {
119 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open output file: %s", output_path);
120 }
121 should_close = true;
122 } else {
123 f = stdout;
124 }
125
126 // Write title/header
127 manpage_fmt_write_title(f, program_name, mode_name, brief_description);
128
129 // Write SYNOPSIS
130 char *synopsis_content = NULL;
131 size_t synopsis_len = 0;
132 asciichat_error_t err = manpage_merger_generate_synopsis(mode_name, &synopsis_content, &synopsis_len);
133 if (err == ASCIICHAT_OK && synopsis_content && synopsis_len > 0) {
134 fprintf(f, "%s", synopsis_content);
135 manpage_merger_free_content(synopsis_content);
136 }
137
138 // Write POSITIONAL ARGUMENTS if present
139 if (config->num_positional_args > 0) {
140 char *pos_content = manpage_content_generate_positional(config);
141 if (pos_content && *pos_content != '\0') {
142 fprintf(f, "%s", pos_content);
143 }
145 }
146
147 // Write DESCRIPTION
148 manpage_fmt_write_section(f, "DESCRIPTION");
149 fprintf(f, ".B ascii-chat\nis a terminal-based video chat application that converts webcam video to ASCII\n");
150 fprintf(f, "art in real-time. It enables video chat directly in your terminal, whether you're\n");
151 fprintf(f, "using a local console, a remote SSH session, or any terminal emulator.\n");
153
154 // Write USAGE
155 char *usage_content = NULL;
156 size_t usage_len = 0;
157 err = manpage_merger_generate_usage(config, &usage_content, &usage_len);
158 if (err == ASCIICHAT_OK && usage_content && usage_len > 0) {
159 fprintf(f, ".SH USAGE\n%s", usage_content);
160 manpage_merger_free_content(usage_content);
161 }
162
163 // Write OPTIONS
164 if (config->num_descriptors > 0) {
165 char *options_content = manpage_content_generate_options(config);
166 if (options_content && *options_content != '\0') {
167 fprintf(f, "%s", options_content);
168 }
169 manpage_content_free_options(options_content);
170 }
171
172 // Write EXAMPLES if present
173 if (config->num_examples > 0) {
174 char *examples_content = manpage_content_generate_examples(config);
175 if (examples_content && *examples_content != '\0') {
176 fprintf(f, "%s", examples_content);
177 }
178 manpage_content_free_examples(examples_content);
179 }
180
181 // Write ENVIRONMENT if present
182 bool has_env_vars = false;
183 for (size_t i = 0; i < config->num_descriptors; i++) {
184 if (config->descriptors[i].env_var_name) {
185 has_env_vars = true;
186 break;
187 }
188 }
189
190 if (has_env_vars) {
191 char *env_content = manpage_content_generate_environment(config);
192 if (env_content && *env_content != '\0') {
193 fprintf(f, "%s", env_content);
194 }
196 }
197
198 // Write placeholder manual sections
199 manpage_fmt_write_section(f, "FILES");
200 fprintf(f, ".I ~/.ascii-chat/config.toml\n");
201 fprintf(f, "User configuration file\n");
203
204 manpage_fmt_write_section(f, "NOTES");
205 fprintf(f, "For more information and examples, visit the project repository.\n");
207
208 manpage_fmt_write_section(f, "BUGS");
209 fprintf(f, "Report bugs at the project issue tracker.\n");
211
212 manpage_fmt_write_section(f, "AUTHOR");
213 fprintf(f, "Contributed by the ascii-chat community.\n");
215
216 manpage_fmt_write_section(f, "SEE ALSO");
217 fprintf(f, ".B man(1),\n");
218 fprintf(f, ".B groff_man(7)\n");
220
221 if (should_close) {
222 fclose(f);
223 }
224
225 log_debug("Generated man page template to %s", output_path ? output_path : "stdout");
226 return ASCIICHAT_OK;
227}
228
230 const char *mode_name, const char *output_path,
231 const char *brief_description) {
232 if (!builder || !program_name || !brief_description) {
233 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing required parameters");
234 }
235
236 const options_config_t *config = options_builder_build(builder);
237 if (!config) {
238 return SET_ERRNO(ERROR_CONFIG, "Failed to build options configuration");
239 }
240
241 return options_config_generate_manpage_template(config, program_name, mode_name, output_path, brief_description);
242}
243
261
262static asciichat_error_t manpage_open_merged_output(const char *output_path, FILE **output, bool *close_output) {
263 FILE *f = NULL;
264 bool should_close = false;
265
266 if (output_path && strlen(output_path) > 0 && strcmp(output_path, "-") != 0) {
267 // Check if file already exists and prompt for confirmation
268 struct stat st;
269 if (stat(output_path, &st) == 0) {
270 // File exists - ask user if they want to overwrite
271 log_plain("Man page file already exists: %s", output_path);
272
274 safe_snprintf(question, sizeof(question), "File already exists: %s\n\nOverwrite", output_path);
275 bool overwrite =
276 ui_notice_confirm_kind(UI_PROMPT_MANPAGE_OVERWRITE, UI_NOTICE_WARNING, question, 30); // Default to No
277 if (!overwrite) {
278 log_plain("Man page generation cancelled.");
279 return SET_ERRNO(ERROR_FILE_OPERATION, "User cancelled overwrite");
280 }
281
282 log_plain("Overwriting existing man page file...");
283 }
284
285 f = platform_fopen("file_stream", output_path, "w");
286 if (!f) {
287 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open output file: %s", output_path);
288 }
289 should_close = true;
290 } else {
291 f = stdout;
292 }
293
294 *output = f;
295 *close_output = should_close;
296 return ASCIICHAT_OK;
297}
298
299static const char *manpage_begin_merge(FILE *f, manpage_merge_state_t *state, const char *p, const char *line_end) {
300 state->in_merge_section = true;
301 state->merge_content_generated = false;
302 state->manual_env_count = 0; // Reset manual variable collection
303 state->manual_env_capacity = 0;
304
305 // Extract section name (e.g., "ENVIRONMENT" from "MERGE-START: ENVIRONMENT")
306 const char *section_start = strstr(p, "MERGE-START:");
307 if (section_start && section_start < line_end) {
308 section_start += strlen("MERGE-START:");
309 while (*section_start && section_start < line_end && isspace(*section_start))
310 section_start++;
311
312 const char *section_name_end = section_start;
313 // Advance until we reach line_end (the newline position)
314 while (section_name_end < line_end) {
315 section_name_end++;
316 }
317
318 // Trim trailing whitespace
319 while (section_name_end > section_start && isspace(*(section_name_end - 1))) {
320 section_name_end--;
321 }
322
323 size_t section_name_len = (size_t)(section_name_end - section_start);
324 if (section_name_len > 0 && section_name_len < sizeof(state->current_merge_section)) {
325 // Use memcpy instead of SAFE_STRNCPY because source is not null-terminated
326 memcpy(state->current_merge_section, section_start, section_name_len);
327 state->current_merge_section[section_name_len] = '\0';
328 }
329 }
330
331 // Do NOT write the MERGE-START marker line - it's an internal control marker
332 // The content will be generated when MERGE-END is encountered
333
334 // For ENVIRONMENT MERGE sections, find and preserve the .SH header
335 // (skipping over any marker comment lines that might be in between)
336 if (strcmp(state->current_merge_section, "ENVIRONMENT") == 0) {
337 const char *search_line = line_end + 1;
338 bool found_header = false;
339 while (*search_line && !found_header) {
340 const char *search_line_end = strchr(search_line, '\n');
341 if (!search_line_end)
342 break;
343
344 // Check if this is the .SH header
345 if (strncmp(search_line, ".SH ", sizeof(".SH ") - 1) == 0) {
346 // Write the .SH header line
347 size_t header_len = (size_t)(search_line_end - search_line);
348 fwrite(search_line, 1, header_len + 1, f);
349 p = search_line_end + 1;
350 found_header = true;
351 break;
352 }
353
354 // Skip marker comment lines (.\" MANUAL-*, .\" MERGE-*, etc)
355 if (strncmp(search_line, ".\\\" ", sizeof(".\\\" ") - 1) == 0) {
356 search_line = search_line_end + 1;
357 continue; // Keep searching
358 }
359
360 // If we hit a non-marker, non-.SH line, stop searching
361 break;
362 }
363 if (found_header) {
364 return p;
365 }
366 }
367
368 // Skip to next line
369 p = line_end + 1;
370 return p;
371}
372
373static void manpage_clear_manual_environment(manpage_merge_state_t *state) {
374 // Free collected manual variables (strings first, then arrays)
375 for (size_t i = 0; i < state->manual_env_count; i++) {
376 if (state->manual_env_vars && state->manual_env_vars[i]) {
377 char *var_name = (char *)state->manual_env_vars[i];
378 SAFE_FREE(var_name);
379 }
380 if (state->manual_env_descs && state->manual_env_descs[i]) {
381 char *var_desc = (char *)state->manual_env_descs[i];
382 SAFE_FREE(var_desc);
383 }
384 }
385 if (state->manual_env_vars) {
386 SAFE_FREE(state->manual_env_vars);
387 state->manual_env_vars = NULL;
388 }
389 if (state->manual_env_descs) {
390 SAFE_FREE(state->manual_env_descs);
391 state->manual_env_descs = NULL;
392 }
393 state->manual_env_count = 0;
394 state->manual_env_capacity = 0;
395}
396
397static void manpage_end_merge(FILE *f, const options_config_t *config, manpage_merge_state_t *state) {
398 // Before writing MERGE-END, generate content if not already done
399 if (!state->merge_content_generated) {
400 if (strcmp(state->current_merge_section, "ENVIRONMENT") == 0) {
401 log_debug("[MANPAGE] Generating ENVIRONMENT with %zu manual + %zu auto variables", state->manual_env_count,
402 config->num_descriptors);
404 config, state->manual_env_vars, state->manual_env_count, state->manual_env_descs);
405 if (env_content && *env_content != '\0') {
406 log_debug("[MANPAGE] Writing ENVIRONMENT content: %zu bytes", strlen(env_content));
407 fprintf(f, "%s", env_content);
408 } else {
409 log_warn("[MANPAGE] ENVIRONMENT content is empty!");
410 }
412 }
413 state->merge_content_generated = true;
414 }
415
416 state->in_merge_section = false;
417 // Do NOT write the MERGE-END marker line - it's an internal control marker
418 memset(state->current_merge_section, 0, sizeof(state->current_merge_section));
419
420 manpage_clear_manual_environment(state);
421}
422
423static void manpage_collect_environment(manpage_merge_state_t *state, const char *p, const char *line_end,
424 size_t line_len) {
425 // Check if line starts with ".TP" or ".B " (groff markers)
426 bool is_tp_marker = (line_len >= 3 && strncmp(p, ".TP", sizeof(".TP") - 1) == 0 && (line_len == 3 || isspace(p[3])));
427 bool is_b_marker = (line_len >= 3 && strncmp(p, ".B ", sizeof(".B ") - 1) == 0);
428
429 if (is_b_marker) {
430 // Extract variable name after ".B "
431 const char *var_start = p + 3;
432 while (*var_start && var_start < line_end && isspace(*var_start))
433 var_start++;
434
435 const char *var_end = var_start;
436 while (var_end < line_end && *var_end && *var_end != '\n') {
437 var_end++;
438 }
439
440 size_t var_len = (size_t)(var_end - var_start);
441 if (var_len > 0) {
442 char *var_name = SAFE_MALLOC(var_len + 1, char *);
443 memcpy(var_name, var_start, var_len);
444 var_name[var_len] = '\0';
445
446 // Trim trailing whitespace
447 while (var_len > 0 && isspace(var_name[var_len - 1])) {
448 var_name[--var_len] = '\0';
449 }
450
451 // Store the variable name
452 if (state->manual_env_count >= state->manual_env_capacity) {
453 state->manual_env_capacity = state->manual_env_capacity == 0 ? 16 : state->manual_env_capacity * 2;
454 state->manual_env_vars =
455 SAFE_REALLOC(state->manual_env_vars, state->manual_env_capacity * sizeof(const char *), const char **);
456 state->manual_env_descs =
457 SAFE_REALLOC(state->manual_env_descs, state->manual_env_capacity * sizeof(const char *), const char **);
458 }
459
460 state->manual_env_vars[state->manual_env_count] = var_name;
461 state->manual_env_descs[state->manual_env_count] = NULL; // Will be set from next line(s)
462 state->manual_env_count++;
463 }
464 } else if (state->manual_env_count > 0 && !is_tp_marker && !is_b_marker) {
465 // This is likely a description line following a ".B var_name" line
466 // Set description for the last collected variable
467 const char *desc_start = p;
468 while (*desc_start && desc_start < line_end && isspace(*desc_start))
469 desc_start++;
470
471 size_t desc_len = (size_t)(line_end - desc_start);
472 if (desc_len > 0 && state->manual_env_descs[state->manual_env_count - 1] == NULL) {
473 char *desc = SAFE_MALLOC(desc_len + 1, char *);
474 memcpy(desc, desc_start, desc_len);
475 desc[desc_len] = '\0';
476
477 // Trim trailing whitespace
478 while (desc_len > 0 && isspace(desc[desc_len - 1])) {
479 desc[--desc_len] = '\0';
480 }
481
482 if (desc_len > 0) {
483 state->manual_env_descs[state->manual_env_count - 1] = desc;
484 } else {
485 SAFE_FREE(desc);
486 }
487 }
488 }
489}
490
491static void manpage_begin_auto(manpage_merge_state_t *state, const char *p, const char *line_end) {
492 state->in_auto_section = true;
493 state->found_section_header = false;
494
495 // Extract section name (e.g., "SYNOPSIS" from "AUTO-START: SYNOPSIS")
496 const char *section_start = strstr(p, "AUTO-START:");
497 if (section_start && section_start < line_end) {
498 section_start += strlen("AUTO-START:");
499 while (*section_start && section_start < line_end && isspace(*section_start))
500 section_start++;
501
502 const char *section_name_end = section_start;
503 // Extract section name: everything from section_start until line_end
504 // Do NOT use a condition based on '\n' - just advance until we reach line_end
505 while (section_name_end < line_end) {
506 section_name_end++;
507 }
508
509 // Now trim trailing whitespace from the section name
510 while (section_name_end > section_start && isspace(*(section_name_end - 1))) {
511 section_name_end--;
512 }
513
514 size_t section_name_len = (size_t)(section_name_end - section_start);
515 if (section_name_len > 0 && section_name_len < sizeof(state->current_auto_section)) {
516 // Use memcpy instead of SAFE_STRNCPY because source is not null-terminated
517 // SAFE_STRNCPY uses strlcpy which only copies size-1 bytes
518 memcpy(state->current_auto_section, section_start, section_name_len);
519 state->current_auto_section[section_name_len] = '\0';
520 log_debug("[MANPAGE] Found AUTO-START section: '%s' (%zu bytes)", state->current_auto_section, section_name_len);
521 }
522 }
523
524 // Don't write the AUTO-START marker line - it's an internal control comment
525}
526
527static void manpage_write_auto_content(FILE *f, const options_config_t *config, const manpage_merge_state_t *state) {
528 // Generate content for this AUTO section
529 if (strcmp(state->current_auto_section, "SYNOPSIS") == 0) {
530 log_debug("[MANPAGE] Generating SYNOPSIS section");
531 char *synopsis_content = NULL;
532 size_t synopsis_len = 0;
533 asciichat_error_t gen_err = manpage_merger_generate_synopsis(NULL, &synopsis_content, &synopsis_len);
534 log_debug("[MANPAGE] SYNOPSIS: err=%d, len=%zu", gen_err, synopsis_len);
535 if (gen_err == ASCIICHAT_OK && synopsis_content && synopsis_len > 0) {
536 fprintf(f, "%s", synopsis_content);
537 manpage_merger_free_content(synopsis_content);
538 }
539 } else if (strcmp(state->current_auto_section, "POSITIONAL ARGUMENTS") == 0) {
540 log_debug("[MANPAGE] Generating POSITIONAL ARGUMENTS (config has %zu args)", config->num_positional_args);
541 char *pos_content = manpage_content_generate_positional(config);
542 if (pos_content) {
543 size_t pos_len = strlen(pos_content);
544 log_debug("[MANPAGE] POSITIONAL ARGUMENTS: %zu bytes", pos_len);
545 if (*pos_content != '\0') {
546 fprintf(f, "%s", pos_content);
547 }
548 }
550 } else if (strcmp(state->current_auto_section, "USAGE") == 0) {
551 log_debug("[MANPAGE] Generating USAGE (config has %zu usage lines)", config->num_usage_lines);
552 char *usage_content = NULL;
553 size_t usage_len = 0;
554 asciichat_error_t usage_err = manpage_merger_generate_usage(config, &usage_content, &usage_len);
555 log_debug("[MANPAGE] USAGE: err=%d, len=%zu", usage_err, usage_len);
556 if (usage_err == ASCIICHAT_OK && usage_content && usage_len > 0) {
557 fprintf(f, "%s", usage_content);
558 manpage_merger_free_content(usage_content);
559 }
560 } else if (strcmp(state->current_auto_section, "EXAMPLES") == 0) {
561 log_debug("[MANPAGE] Generating EXAMPLES (config has %zu examples)", config->num_examples);
562 char *examples_content = manpage_content_generate_examples(config);
563 if (examples_content) {
564 size_t ex_len = strlen(examples_content);
565 log_debug("[MANPAGE] EXAMPLES: %zu bytes", ex_len);
566 if (*examples_content != '\0') {
567 fprintf(f, "%s", examples_content);
568 }
569 }
570 manpage_content_free_examples(examples_content);
571 } else if (strcmp(state->current_auto_section, "OPTIONS") == 0) {
572 log_debug("[MANPAGE] Generating OPTIONS (config has %zu descriptors)", config->num_descriptors);
573 char *options_content = manpage_content_generate_options(config);
574 if (options_content) {
575 size_t opt_len = strlen(options_content);
576 log_debug("[MANPAGE] OPTIONS: %zu bytes", opt_len);
577 if (*options_content != '\0') {
578 fprintf(f, "%s", options_content);
579 }
580 }
581 manpage_content_free_options(options_content);
582 }
583}
584
585static void manpage_write_auto_line(FILE *f, const options_config_t *config, manpage_merge_state_t *state,
586 const char *p, const char *line_end, size_t line_len) {
587 // Preserve comment lines that come before the section header
588 if (!state->found_section_header && strstr(p, ".\\\"") != NULL && strstr(p, ".\\\"") < line_end) {
589 // Skip comment lines in AUTO sections that contain "auto-generated" text
590 if (strstr(p, "auto-generated") == NULL) {
591 // Non-marker comment lines can be preserved
592 fwrite(p, 1, line_len + 1, f);
593 }
594 // Skip comment lines with "auto-generated" marker text
595 } else if (!state->found_section_header && strstr(p, ".SH ") != NULL && strstr(p, ".SH ") < line_end) {
596 // If this is the section header line (.SH ...), write it and generate content
597 fwrite(p, 1, line_len + 1, f);
598 state->found_section_header = true;
599
600 manpage_write_auto_content(f, config, state);
601 }
602 // Otherwise skip old content between section header and AUTO-END
603}
604
605static void manpage_merge_template(FILE *f, const options_config_t *config, const char *p) {
606 manpage_merge_state_t storage = {0};
607 manpage_merge_state_t *state = &storage;
608 while (*p) {
609 // Find next line
610 const char *line_end = strchr(p, '\n');
611 if (!line_end) {
612 // Last line without newline
613 if (!state->in_auto_section && !state->in_merge_section) {
614 fputs(p, f);
615 }
616 break;
617 }
618
619 size_t line_len = (size_t)(line_end - p);
620
621 // Check for MERGE-START marker (only in current line)
622 bool has_merge_start = false;
623 if (strstr(p, "MERGE-START:") != NULL && strstr(p, "MERGE-START:") < line_end) {
624 has_merge_start = true;
625 }
626
627 if (has_merge_start) {
628 p = manpage_begin_merge(f, state, p, line_end);
629 continue;
630 } else if (state->in_merge_section && strstr(p, "MERGE-END:") != NULL && strstr(p, "MERGE-END:") < line_end) {
631 manpage_end_merge(f, config, state);
632 p = line_end + 1;
633 continue;
634 } else if (state->in_merge_section) {
635 if (strcmp(state->current_merge_section, "ENVIRONMENT") == 0) {
636 manpage_collect_environment(state, p, line_end, line_len);
637 } else {
638 fwrite(p, 1, line_len + 1, f);
639 }
640 p = line_end + 1;
641 continue;
642 }
643
644 // Check for AUTO-START marker (only in current line)
645 bool has_auto_start = false;
646 const char *temp = p;
647 while (temp < line_end) {
648 if (strstr(temp, "AUTO-START:") != NULL) {
649 const char *found = strstr(temp, "AUTO-START:");
650 if (found < line_end) {
651 has_auto_start = true;
652 break;
653 }
654 // strstr() found it but it's beyond this line, so keep looking
655 temp = found + 1;
656 } else {
657 break;
658 }
659 }
660
661 if (has_auto_start) {
662 manpage_begin_auto(state, p, line_end);
663 } else if (strstr(p, "AUTO-END:") != NULL && strstr(p, "AUTO-END:") < line_end) {
664 state->in_auto_section = false;
665 // Don't write the AUTO-END marker line - it's an internal control comment
666 memset(state->current_auto_section, 0, sizeof(state->current_auto_section));
667 state->found_section_header = false;
668
669 } else if (state->in_auto_section) {
670 manpage_write_auto_line(f, config, state, p, line_end, line_len);
671 } else {
672 // Write all manual content (not in AUTO section)
673 // Skip marker comment lines: .\" AUTO-*, .\" MANUAL-*, .\" MERGE-*
674 // These are internal build-time control comments
675 bool is_marker_comment = (strstr(p, ".\\\" AUTO-") != NULL && strstr(p, ".\\\" AUTO-") < line_end) ||
676 (strstr(p, ".\\\" MANUAL-") != NULL && strstr(p, ".\\\" MANUAL-") < line_end) ||
677 (strstr(p, ".\\\" MERGE-") != NULL && strstr(p, ".\\\" MERGE-") < line_end);
678 if (!is_marker_comment) {
679 fwrite(p, 1, line_len + 1, f);
680 }
681 }
682
683 // Move to next line
684 p = line_end + 1;
685 }
686}
687
689 const char *mode_name, const char *output_path,
690 const char *brief_description) {
691 (void)mode_name; // Not used
692 (void)program_name; // Not used
693 (void)brief_description; // Not used
694
695 if (!config) {
696 return SET_ERRNO(ERROR_INVALID_PARAM, "config is required for man page generation");
697 }
698
699 FILE *f = NULL;
700 bool should_close = false;
701 asciichat_error_t err = manpage_open_merged_output(output_path, &f, &should_close);
702 if (err != ASCIICHAT_OK) {
703 return err;
704 }
705
706 // Load template resources
707 manpage_resources_t resources;
708 memset(&resources, 0, sizeof(resources));
709 err = manpage_resources_load(&resources);
710 if (err != ASCIICHAT_OK) {
711 if (should_close)
712 fclose(f);
713 return err;
714 }
715
716 if (!manpage_resources_is_valid(&resources)) {
717 if (should_close)
718 fclose(f);
719 manpage_resources_destroy(&resources);
720 return SET_ERRNO(ERROR_CONFIG, "Man page resources are not valid");
721 }
722
723 manpage_merge_template(f, config, resources.template_content);
724
725 manpage_resources_destroy(&resources);
726
727 fflush(f);
728 if (should_close) {
729 fclose(f);
730 }
731
732 log_debug("Generated merged man page to %s", output_path ? output_path : "stdout");
733 return ASCIICHAT_OK;
734}
735
736parsed_section_t *parse_manpage_sections(const char *filepath, size_t *num_sections) {
737 if (!filepath || !num_sections) {
738 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
739 return NULL;
740 }
741
742 FILE *f = platform_fopen("file_stream", filepath, "r");
743 if (!f) {
744 SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open file: %s", filepath);
745 return NULL;
746 }
747
748 parsed_section_t *sections = NULL;
749 asciichat_error_t err = manpage_parser_parse_file(f, &sections, num_sections);
750 fclose(f);
751
752 if (err != ASCIICHAT_OK) {
753 return NULL;
754 }
755
756 return sections;
757}
758
759void free_parsed_sections(parsed_section_t *sections, size_t num_sections) {
760 manpage_parser_free_sections(sections, num_sections);
761}
762
763const parsed_section_t *find_section(const parsed_section_t *sections, size_t num_sections, const char *section_name) {
764 return manpage_parser_find_section(sections, num_sections, section_name);
765}
766
767#ifndef NDEBUG
768asciichat_error_t options_config_generate_final_manpage(const char *template_path, const char *output_path,
769 const char *version_string, const char *content_file_path) {
770 (void)template_path;
771 (void)output_path;
772 (void)version_string;
773 (void)content_file_path;
774 return SET_ERRNO(ERROR_CONFIG, "Not implemented in refactored version");
775}
776#endif
777
#define SET_ERRNO(code,...)
#define SET_ERRNO_SYS(code,...)
Options builder API for flexible command-line option configuration.
options_config_t * options_builder_build(options_builder_t *builder)
Build immutable options config.
⚙️ Common definitions, error codes, macros, and types shared throughout the application
char * manpage_content_generate_environment_with_manual(const options_config_t *config, const char **manual_vars, size_t manual_count, const char **manual_descs)
Generate ENVIRONMENT section with manual and auto variables.
Definition environment.c:75
void manpage_content_free_environment(char *content)
Free generated environment content.
char * manpage_content_generate_environment(const options_config_t *config)
Generate ENVIRONMENT VARIABLES section content.
Definition environment.c:14
ENVIRONMENT section generator for man pages.
char * manpage_content_generate_examples(const options_config_t *config)
Generate EXAMPLES section content.
Definition examples.c:81
void manpage_content_free_examples(char *content)
Free generated examples content.
Definition examples.c:149
EXAMPLES section generator for man pages.
void manpage_fmt_write_title(FILE *f, const char *program_name, const char *mode_name, const char *brief_description)
Write groff title/header (.TH directive).
Definition formatter.c:79
void manpage_fmt_write_section(FILE *f, const char *section_name)
Write a section header directive.
Definition formatter.c:32
void manpage_fmt_write_blank_line(FILE *f)
Write a blank line (for spacing between sections).
Definition formatter.c:39
Groff/troff formatting utilities for man page generation.
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:285
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_CONFIG
Definition error_codes.h:52
@ ERROR_FILE_OPERATION
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
Definition log/log.h:665
const parsed_section_t * find_section(const parsed_section_t *sections, size_t num_sections, const char *section_name)
Find section by name.
Definition manpage.c:763
const char * escape_groff_special(const char *str)
Escape special groff/troff characters in text.
Definition manpage.c:46
asciichat_error_t options_config_generate_final_manpage(const char *template_path, const char *output_path, const char *version_string, const char *content_file_path)
Generate final man page (.1) from template (.1.in) with version substitution and optional content fil...
Definition manpage.c:768
asciichat_error_t options_builder_generate_manpage_template(options_builder_t *builder, const char *program_name, const char *mode_name, const char *output_path, const char *brief_description)
Generate man page template from builder (before build()).
Definition manpage.c:229
asciichat_error_t options_config_generate_manpage_template(const options_config_t *config, const char *program_name, const char *mode_name, const char *output_path, const char *brief_description)
Generate a man page template from options builder configuration.
Definition manpage.c:106
void free_parsed_sections(parsed_section_t *sections, size_t num_sections)
Free parsed sections array.
Definition manpage.c:759
parsed_section_t * parse_manpage_sections(const char *filepath, size_t *num_sections)
Parse existing man page template to extract sections.
Definition manpage.c:736
const char * format_mode_names(option_mode_bitmask_t mode_bitmask)
Format option mode names as human-readable string.
Definition manpage.c:50
option_mode_bitmask_t
Option mode bitmask.
Definition options.h:936
asciichat_error_t options_config_generate_manpage_merged(const options_config_t *config, const char *program_name, const char *mode_name, const char *output_path, const char *brief_description)
Generate merged man page template preserving manual content.
Definition manpage.c:688
@ MODE_CLIENT
Client mode - network client options.
Definition options.h:921
@ MODE_MCP_SERVER
Model Context Protocol rendering server.
Definition options.h:925
@ MODE_DISCOVERY_SERVICE
Discovery server mode - session management and WebRTC signaling.
Definition options.h:923
@ MODE_SERVER
Server mode - network server options.
Definition options.h:920
@ MODE_MIRROR
Mirror mode - local webcam viewing (no network).
Definition options.h:922
@ MODE_DISCOVERY
Discovery mode - participant that can dynamically become host.
Definition options.h:924
@ OPTION_MODE_BINARY
Binary-level options (parsed before mode detection).
Definition options.h:945
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
📝 Logging API with multiple log levels and terminal output control
char * manpage_content_generate_options(const options_config_t *config)
Generate OPTIONS section content.
void manpage_content_free_options(char *content)
Free generated options content.
OPTIONS section generator for man pages.
#define OPTIONS_MANPAGE_CURRENT_AUTO_SECTION_BUFFER_SIZE
Definition manpage.c:16
#define OPTIONS_MANPAGE_MODE_BUFFER_SIZE
Definition manpage.c:15
#define OPTIONS_MANPAGE_QUESTION_BUFFER_SIZE
Definition manpage.c:18
#define OPTIONS_MANPAGE_CURRENT_MERGE_SECTION_BUFFER_SIZE
Definition manpage.c:17
Man page template generation from options builder.
asciichat_error_t manpage_merger_generate_usage(const options_config_t *config, char **out_content, size_t *out_len)
Get auto-generated usage content.
Definition merger.c:82
void manpage_merger_free_content(char *content)
Free merged section content.
Definition merger.c:182
asciichat_error_t manpage_merger_generate_synopsis(const char *mode_name, char **out_content, size_t *out_len)
Get auto-generated synopsis content.
Definition merger.c:140
Man page content merging and orchestration.
bool ui_notice_confirm_kind(ui_prompt_kind_t kind, ui_notice_severity_t severity, const char *text, unsigned timeout_seconds)
Definition notice.c:498
@ UI_NOTICE_WARNING
Definition notice.h:11
asciichat_error_t manpage_parser_parse_file(FILE *f, parsed_section_t **out_sections, size_t *out_count)
Parse man page sections from a FILE handle.
Definition parser.c:345
const parsed_section_t * manpage_parser_find_section(const parsed_section_t *sections, size_t count, const char *section_name)
Find section by name.
Definition parser.c:405
void manpage_parser_free_sections(parsed_section_t *sections, size_t count)
Free parsed sections array.
Definition parser.c:388
Man page template parsing and section extraction.
void manpage_content_free_positional(char *content)
Free generated positional content.
Definition positional.c:74
char * manpage_content_generate_positional(const options_config_t *config)
Generate POSITIONAL ARGUMENTS section content.
Definition positional.c:15
POSITIONAL ARGUMENTS section generator for man pages.
void manpage_resources_destroy(manpage_resources_t *resources)
Cleanup allocated man page resources.
Definition resources.c:147
asciichat_error_t manpage_resources_load(manpage_resources_t *resources)
Load man page resources from embedded or filesystem sources.
Definition resources.c:103
bool manpage_resources_is_valid(const manpage_resources_t *resources)
Check if resources are available.
Definition resources.c:176
Resource management for man page generation.
🔤 String Manipulation and Shell Escaping Utilities
const char ** manual_env_descs
Definition manpage.c:252
char current_auto_section[128]
Definition manpage.c:246
const char ** manual_env_vars
Definition manpage.c:251
char current_merge_section[64]
Definition manpage.c:250
size_t manual_env_capacity
Definition manpage.c:254
Man page resource container.
Definition resources.h:23
const char * template_content
Template file content (.1.in).
Definition resources.h:24
const char * env_var_name
Environment variable fallback (or NULL).
Definition builder.h:268
Immutable parsing tables produced from an options builder.
Definition builder.h:457
Immutable parsing tables produced from an options builder.
Definition builder.h:414
size_t num_usage_lines
Number of usage lines.
Definition builder.h:430
option_descriptor_t * descriptors
Array of option descriptors.
Definition builder.h:415
size_t num_positional_args
Number of positional arguments.
Definition builder.h:422
size_t num_descriptors
Number of descriptors.
Definition builder.h:416
size_t num_examples
Number of examples.
Definition builder.h:433
Owned slice of a parsed roff man-page section.
Definition manpage.h:44
USAGE section generator for man pages.