ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
bash.c
Go to the documentation of this file.
1
9#include <string.h>
10#include <stdio.h>
11#include <stdbool.h>
12#include <ascii-chat/options/completions/bash.h>
13#include <ascii-chat/options/completions/completions.h>
14#include <ascii-chat/options/registry.h>
15#include <ascii-chat/options/enums.h>
16#include <ascii-chat/common.h>
17
21static void bash_escape_help(FILE *output, const char *text) {
22 if (!text) {
23 return;
24 }
25
26 for (const char *p = text; *p; p++) {
27 switch (*p) {
28 case '\'':
29 fprintf(output, "'\\''");
30 break;
31 case '\n':
32 fprintf(output, "\\n");
33 break;
34 case '\t':
35 fprintf(output, "\\t");
36 break;
37 default:
38 fputc(*p, output);
39 }
40 }
41}
42
46static void bash_write_option(FILE *output, const option_descriptor_t *opt) {
47 if (!opt) {
48 return;
49 }
50
51 if (opt->short_name != '\0') {
52 fprintf(output, " '-%c' '", opt->short_name);
53 bash_escape_help(output, opt->help_text);
54 fprintf(output, "'\n");
55 }
56
57 fprintf(output, " '--%s' '", opt->long_name);
58 bash_escape_help(output, opt->help_text);
59 fprintf(output, "'\n");
60}
61
65static void bash_write_header(FILE *output) {
66 fprintf(output, "# Bash completion script for ascii-chat\n"
67 "# Generated from options registry - DO NOT EDIT MANUALLY\n"
68 "# Usage: eval \"$(ascii-chat --completions bash)\"\n"
69 "\n"
70 "_ascii_chat() {\n"
71 " local cur prev words cword\n"
72 " _init_completion || return\n"
73 "\n");
74}
75
79static void bash_write_footer(FILE *output) {
80 fprintf(output, "}\n"
81 "\n"
82 "complete -F _ascii_chat ascii-chat\n");
83}
84
91static asciichat_error_t bash_write_all_options(FILE *output) {
92 /* Binary-level options - use MODE_DISCOVERY (the default mode) with for_binary_help=true
93 * This matches how options_print_help_for_mode() filters options for '--help' display */
94 size_t binary_count = 0;
95 const option_descriptor_t *binary_opts = options_registry_get_for_display(MODE_DISCOVERY, true, &binary_count);
96
97 fprintf(output, " # Binary-level options (same as 'ascii-chat --help')\n local -a binary_opts=(\n");
98 if (binary_opts) {
99 for (size_t i = 0; i < binary_count; i++) {
100 bash_write_option(output, &binary_opts[i]);
101 }
102 SAFE_FREE(binary_opts);
103 }
104 fprintf(output, " )\n\n");
105
106 /* Server options - mode-specific with for_binary_help=false
107 * This matches how options_print_help_for_mode() filters for 'ascii-chat server --help' */
108 size_t server_count = 0;
109 const option_descriptor_t *server_opts = options_registry_get_for_display(MODE_SERVER, false, &server_count);
110
111 fprintf(output, " # Server-mode options (same as 'ascii-chat server --help')\n local -a server_opts=(\n");
112 if (server_opts) {
113 for (size_t i = 0; i < server_count; i++) {
114 bash_write_option(output, &server_opts[i]);
115 }
116 SAFE_FREE(server_opts);
117 }
118 fprintf(output, " )\n\n");
119
120 /* Client options - mode-specific with for_binary_help=false */
121 size_t client_count = 0;
122 const option_descriptor_t *client_opts = options_registry_get_for_display(MODE_CLIENT, false, &client_count);
123
124 fprintf(output, " # Client-mode options (same as 'ascii-chat client --help')\n local -a client_opts=(\n");
125 if (client_opts) {
126 for (size_t i = 0; i < client_count; i++) {
127 bash_write_option(output, &client_opts[i]);
128 }
129 SAFE_FREE(client_opts);
130 }
131 fprintf(output, " )\n\n");
132
133 /* Mirror options - mode-specific with for_binary_help=false */
134 size_t mirror_count = 0;
135 const option_descriptor_t *mirror_opts = options_registry_get_for_display(MODE_MIRROR, false, &mirror_count);
136
137 fprintf(output, " # Mirror-mode options (same as 'ascii-chat mirror --help')\n local -a mirror_opts=(\n");
138 if (mirror_opts) {
139 for (size_t i = 0; i < mirror_count; i++) {
140 bash_write_option(output, &mirror_opts[i]);
141 }
142 SAFE_FREE(mirror_opts);
143 }
144 fprintf(output, " )\n\n");
145
146 /* Discovery-service options */
147 size_t discovery_svc_count = 0;
148 const option_descriptor_t *discovery_svc_opts =
149 options_registry_get_for_display(MODE_DISCOVERY_SERVICE, false, &discovery_svc_count);
150
151 fprintf(output, " # Discovery-service options (same as 'ascii-chat discovery-service --help')\n local -a "
152 "discovery_svc_opts=(\n");
153 if (discovery_svc_opts) {
154 for (size_t i = 0; i < discovery_svc_count; i++) {
155 bash_write_option(output, &discovery_svc_opts[i]);
156 }
157 SAFE_FREE(discovery_svc_opts);
158 }
159 fprintf(output, " )\n\n");
160
161 return ASCIICHAT_OK;
162}
163
167static void bash_write_enum_cases(FILE *output) {
168 size_t combined_count = 0;
169 option_descriptor_t *combined_opts = completions_collect_all_modes_unique(&combined_count);
170
171 // Also add binary-level options (--log-level, --log-file, etc.)
172 size_t binary_count = 0;
173 const option_descriptor_t *binary_opts = options_registry_get_for_display(MODE_DISCOVERY, true, &binary_count);
174
175 if (binary_opts) {
176 for (size_t i = 0; i < binary_count; i++) {
177 // Check if already in combined_opts
178 bool already_has = false;
179 for (size_t j = 0; j < combined_count; j++) {
180 if (strcmp(combined_opts[j].long_name, binary_opts[i].long_name) == 0) {
181 already_has = true;
182 break;
183 }
184 }
185 if (!already_has) {
186 combined_count++;
187 option_descriptor_t *temp = (option_descriptor_t *)SAFE_REALLOC(
188 combined_opts, combined_count * sizeof(option_descriptor_t), option_descriptor_t *);
189 if (temp) {
190 combined_opts = temp;
191 combined_opts[combined_count - 1] = binary_opts[i];
192 }
193 }
194 }
195 SAFE_FREE(binary_opts);
196 }
197
198 if (!combined_opts) {
199 return;
200 }
201
202 for (size_t i = 0; i < combined_count; i++) {
203 const option_descriptor_t *opt = &combined_opts[i];
204 const option_metadata_t *meta = &opt->metadata;
205
206 // Skip options with no enum or examples
207 bool has_examples = meta->examples && meta->examples[0] != NULL;
208 if (meta->input_type != OPTION_INPUT_ENUM && !has_examples && meta->numeric_range.max == 0) {
209 continue;
210 }
211
212 // Write case statement
213 if (opt->short_name != '\0') {
214 fprintf(output, " -%c | ", opt->short_name);
215 }
216 fprintf(output, "--%-25s)\n", opt->long_name);
217 fprintf(output, " COMPREPLY=($(compgen -W \"");
218
219 // Write enum values
220 if (meta->input_type == OPTION_INPUT_ENUM && meta->enum_values && meta->enum_values[0] != NULL) {
221 for (size_t j = 0; meta->enum_values[j] != NULL; j++) {
222 fprintf(output, "%s", meta->enum_values[j]);
223 if (meta->enum_values[j + 1] != NULL) {
224 fprintf(output, " ");
225 }
226 }
227 }
228 // Check for examples first (more practical than calculated ranges)
229 else if (has_examples) {
230 for (size_t j = 0; meta->examples[j] != NULL; j++) {
231 fprintf(output, "%s", meta->examples[j]);
232 if (meta->examples[j + 1] != NULL) {
233 fprintf(output, " ");
234 }
235 }
236 }
237 // Fallback to numeric range if no examples
238 else if (meta->input_type == OPTION_INPUT_NUMERIC) {
239 if (meta->numeric_range.min == 1 && meta->numeric_range.max == 9) {
240 fprintf(output, "1 2 3 4 5 6 7 8 9");
241 } else if (meta->numeric_range.max > 0) {
242 fprintf(output, "%d %d %d", meta->numeric_range.min, (meta->numeric_range.min + meta->numeric_range.max) / 2,
243 meta->numeric_range.max);
244 }
245 }
246
247 fprintf(output, "\" -- \"$cur\"))\n");
248 fprintf(output, " return\n");
249 fprintf(output, " ;;\n");
250 }
251
252 SAFE_FREE(combined_opts);
253}
254
258static void bash_write_completion_logic(FILE *output) {
259 fprintf(output, " # Modes\n"
260 " local modes=\"server client mirror discovery-service\"\n"
261 "\n"
262 " # Detect which mode we're in\n"
263 " local mode=\"\"\n"
264 " local i\n"
265 " for ((i = 1; i < cword; i++)); do\n"
266 " case \"${words[i]}\" in\n"
267 " server | client | mirror | discovery-service)\n"
268 " mode=\"${words[i]}\"\n"
269 " break\n"
270 " ;;\n"
271 " esac\n"
272 " done\n"
273 "\n"
274 " case \"$prev\" in\n"
275 " # Options that take file paths\n");
276
277 // Generate file path options dynamically from registry (collect from all modes)
278 size_t combined_count = 0;
279 option_descriptor_t *combined_opts = completions_collect_all_modes_unique(&combined_count);
280
281 if (combined_opts) {
282 bool first = true;
283 for (size_t i = 0; i < combined_count; i++) {
284 const option_descriptor_t *opt = &combined_opts[i];
285 const option_metadata_t *meta = options_registry_get_metadata(opt->long_name);
286
287 if (meta && meta->input_type == OPTION_INPUT_FILEPATH) {
288 if (!first) {
289 fprintf(output, " | ");
290 }
291 first = false;
292
293 if (opt->short_name != '\0') {
294 fprintf(output, "-%c | ", opt->short_name);
295 }
296 fprintf(output, "--%s", opt->long_name);
297 }
298 }
299 SAFE_FREE(combined_opts);
300
301 if (!first) {
302 fprintf(output, ")\n");
303 fprintf(output, " _filedir\n");
304 fprintf(output, " return\n");
305 fprintf(output, " ;;\n");
306 }
307 }
308
309 /* Write enum cases */
310 bash_write_enum_cases(output);
311
312 fprintf(output, " esac\n"
313 "\n"
314 " # If current word starts with -, complete options\n"
315 " if [[ \"$cur\" == -* ]]; then\n"
316 " local -a opts_to_complete\n"
317 "\n"
318 " case \"$mode\" in\n"
319 " client)\n"
320 " opts_to_complete=(\"${binary_opts[@]}\" \"${client_opts[@]}\")\n"
321 " ;;\n"
322 " server)\n"
323 " opts_to_complete=(\"${binary_opts[@]}\" \"${server_opts[@]}\")\n"
324 " ;;\n"
325 " mirror)\n"
326 " opts_to_complete=(\"${binary_opts[@]}\" \"${mirror_opts[@]}\")\n"
327 " ;;\n"
328 " *)\n"
329 " opts_to_complete=(\"${binary_opts[@]}\")\n"
330 " ;;\n"
331 " esac\n"
332 "\n"
333 " # Generate completions with help text\n"
334 " local -a completions\n"
335 " for ((i = 0; i < ${#opts_to_complete[@]}; i += 2)); do\n"
336 " if [[ \"${opts_to_complete[i]}\" == \"$cur\"* ]]; then\n"
337 " completions+=(\"${opts_to_complete[i]}\")\n"
338 " fi\n"
339 " done\n"
340 "\n"
341 " if [[ ${#completions[@]} -gt 0 ]]; then\n"
342 " if compopt &>/dev/null 2>&1; then\n"
343 " compopt -o nosort 2>/dev/null || true\n"
344 " COMPREPLY=()\n"
345 " for opt in \"${completions[@]}\"; do\n"
346 " for ((i = 0; i < ${#opts_to_complete[@]}; i += 2)); do\n"
347 " if [[ \"${opts_to_complete[i]}\" == \"$opt\" ]]; then\n"
348 " COMPREPLY+=(\"$opt ${opts_to_complete[i+1]}\")\n"
349 " break\n"
350 " fi\n"
351 " done\n"
352 " done\n"
353 " else\n"
354 " COMPREPLY=($(compgen -W \"${completions[*]}\" -- \"$cur\"))\n"
355 " fi\n"
356 " fi\n"
357 " return\n"
358 " fi\n"
359 "\n"
360 " # If no mode specified yet, suggest modes\n"
361 " if [[ -z \"$mode\" ]]; then\n"
362 " COMPREPLY=($(compgen -W \"$modes\" -- \"$cur\"))\n"
363 " return\n"
364 " fi\n");
365}
366
367asciichat_error_t completions_generate_bash(FILE *output) {
368 if (!output) {
369 return SET_ERRNO(ERROR_INVALID_PARAM, "Output stream cannot be NULL");
370 }
371
372 bash_write_header(output);
373 asciichat_error_t err = bash_write_all_options(output);
374 if (err != ASCIICHAT_OK) {
375 return err;
376 }
377 bash_write_completion_logic(output);
378 bash_write_footer(output);
379
380 return ASCIICHAT_OK;
381}
asciichat_error_t completions_generate_bash(FILE *output)
Definition bash.c:367
option_descriptor_t * completions_collect_all_modes_unique(size_t *count)
Collect options from all modes with deduplication.
Definition completions.c:96
const option_descriptor_t * options_registry_get_for_display(asciichat_mode_t mode, bool for_binary_help, size_t *num_options)
Definition public_api.c:323
const option_metadata_t * options_registry_get_metadata(const char *long_name)
Definition public_api.c:369