ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
help.c File Reference

Help text generation and formatting for options. More...

Go to the source code of this file.

Functions

int options_config_calculate_max_col_width (const options_config_t *config)
 Calculate global max column width across all help sections.
 
void options_config_print_usage (const options_config_t *config, FILE *stream)
 
void options_config_print_usage_section (const options_config_t *config, FILE *stream)
 Print only the USAGE section.
 
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.
 
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)
 
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.
 
void options_struct_destroy (const options_config_t *config, void *options_struct)
 

Detailed Description

Help text generation and formatting for options.

Implements all help/usage text generation functions including programmatic section printers and unified help output.

Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

Definition in file help.c.

Function Documentation

◆ options_config_calculate_max_col_width()

int options_config_calculate_max_col_width ( const options_config_t *  config)

Calculate global max column width across all help sections.

Calculates the maximum width needed for proper alignment across USAGE, EXAMPLES, OPTIONS, and MODES sections.

Definition at line 76 of file help.c.

76 {
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}
void usage(FILE *desc, asciichat_mode_t mode)
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(const char *str)
Definition utf8.c:46
const char * colored_string(log_color_t color, const char *text)

References colored_string(), safe_snprintf(), usage(), and utf8_display_width().

Referenced by options_config_print_options_sections_with_width(), and options_config_print_usage_section().

◆ options_config_print_options_sections()

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)

Calls options_config_print_options_sections_with_width with auto-calculation.

Definition at line 1074 of file help.c.

1074 {
1075 options_config_print_options_sections_with_width(config, stream, 0, mode);
1076}
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

References options_config_print_options_sections_with_width().

◆ options_config_print_options_sections_with_width()

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.

Prints MODES, MODE-OPTIONS, EXAMPLES, and OPTIONS sections. Used with options_config_print_usage_section to allow custom content in between.

Definition at line 883 of file help.c.

884 {
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}
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
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 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
asciichat_error_t terminal_get_size(terminal_size_t *size)

References colored_string(), format_option_default_value_str(), get_option_help_placeholder_str(), layout_print_two_column_row(), option_applies_to_mode(), options_config_calculate_max_col_width(), safe_snprintf(), and terminal_get_size().

Referenced by options_config_print_options_sections(), and options_print_help_for_mode().

◆ options_config_print_usage()

void options_config_print_usage ( const options_config_t *  config,
FILE *  stream 
)

Definition at line 707 of file help.c.

707 {
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}

References colored_string(), format_option_default_value_str(), get_option_help_placeholder_str(), layout_print_two_column_row(), option_applies_to_mode(), and safe_snprintf().

◆ options_config_print_usage_section()

void options_config_print_usage_section ( const options_config_t *  config,
FILE *  stream 
)

Print only the USAGE section.

Splits usage printing from other sections to allow custom content (like positional argument examples) to be inserted between USAGE and other sections.

Definition at line 857 of file help.c.

857 {
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}

References options_config_calculate_max_col_width().

◆ options_print_help_for_mode()

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.

This is the single unified function for all help output (binary level and all modes). It handles common layout logic, terminal detection, and section printing.

Parameters
configOptions config with all options (binary + all modes)
modeMode to show help for (use -1 for binary-level help)
program_nameFull program name with mode (e.g., "ascii-chat server")
descriptionBrief description of the mode/binary
descOutput file stream (usually stdout)

Definition at line 1094 of file help.c.

1095 {
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}
void layout_print_wrapped_description(FILE *stream, const char *text, int indent_width, int term_width)
Definition layout.c:98
void print_project_links(FILE *desc)

References colored_string(), layout_print_two_column_row(), layout_print_wrapped_description(), options_config_print_options_sections_with_width(), print_project_links(), safe_snprintf(), terminal_get_size(), and usage().

Referenced by usage().

◆ options_struct_destroy()

void options_struct_destroy ( const options_config_t *  config,
void *  options_struct 
)

Definition at line 1424 of file help.c.

1424 {
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}