ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
zsh.c
Go to the documentation of this file.
1
7#include <string.h>
8#include <stdio.h>
9#include <stdlib.h>
14#include <ascii-chat/common.h>
16
20static void zsh_escape_desc(FILE *output, const char *text) {
21 if (!text) {
22 return;
23 }
24
25 for (const char *p = text; *p; p++) {
26 switch (*p) {
27 case '\'':
28 // Escape single quotes: end quote, escaped quote, start quote
29 fprintf(output, "'\\''");
30 break;
31 case '|':
32 case '[':
33 case ']':
34 // Escape special zsh characters that conflict with completion syntax
35 fprintf(output, "\\%c", *p);
36 break;
37 case '\\':
38 // Escape backslashes
39 fprintf(output, "\\\\");
40 break;
41 case '\n':
42 case '\t':
43 fprintf(output, " ");
44 break;
45 default:
46 fputc(*p, output);
47 }
48 }
49}
50
54static const char **zsh_collect_groups(const option_descriptor_t *opts, size_t count, size_t *out_group_count) {
55 if (!opts || count == 0) {
56 *out_group_count = 0;
57 return NULL;
58 }
59
60 // Allocate array for unique groups (worst case: count groups)
61 const char **groups = SAFE_MALLOC(count * sizeof(const char *), const char **);
62 size_t group_count = 0;
63
64 // Collect unique groups
65 for (size_t i = 0; i < count; i++) {
66 const char *group = opts[i].group;
67 if (!group)
68 continue;
69
70 // Check if we already have this group
71 bool found = false;
72 for (size_t j = 0; j < group_count; j++) {
73 if (strcmp(groups[j], group) == 0) {
74 found = true;
75 break;
76 }
77 }
78 if (!found) {
79 groups[group_count++] = group;
80 }
81 }
82
83 // Sort groups alphabetically for consistent ordering
84 for (size_t i = 0; i < group_count; i++) {
85 for (size_t j = i + 1; j < group_count; j++) {
86 if (strcmp(groups[i], groups[j]) > 0) {
87 const char *tmp = groups[i];
88 groups[i] = groups[j];
89 groups[j] = tmp;
90 }
91 }
92 }
93
94 *out_group_count = group_count;
95 return groups;
96}
97
104static void zsh_write_combined_args(FILE *output, const option_descriptor_t *opts, size_t count) {
105 if (!opts || count == 0)
106 return;
107
108 fprintf(output, " local -a args=(\n");
109
110 for (size_t i = 0; i < count; i++) {
111 fprintf(output, " '");
112
113 // Device index options: call helper functions
114 if (strcmp(opts[i].long_name, "webcam-index") == 0) {
115 fprintf(output, "--%s=[", opts[i].long_name);
116 zsh_escape_desc(output, opts[i].help_text);
117 fprintf(output, "]:device index:_ascii_chat_webcam_indices'\n");
118 } else if (strcmp(opts[i].long_name, "microphone-index") == 0) {
119 fprintf(output, "--%s=[", opts[i].long_name);
120 zsh_escape_desc(output, opts[i].help_text);
121 fprintf(output, "]:device index:_ascii_chat_microphone_indices'\n");
122 } else if (strcmp(opts[i].long_name, "speakers-index") == 0) {
123 fprintf(output, "--%s=[", opts[i].long_name);
124 zsh_escape_desc(output, opts[i].help_text);
125 fprintf(output, "]:device index:_ascii_chat_speakers_indices'\n");
126 }
127 // Enum options: full spec with values and descriptions
128 else if (opts[i].metadata.input_type == OPTION_INPUT_ENUM && opts[i].metadata.enum_values) {
129 fprintf(output, "--%s=[", opts[i].long_name);
130 zsh_escape_desc(output, opts[i].help_text);
131 fprintf(output, "]:%s:((\\\n", opts[i].long_name);
132
133 for (size_t j = 0; opts[i].metadata.enum_values[j] != NULL; j++) {
134 fprintf(output, " %s", opts[i].metadata.enum_values[j]);
135 if (opts[i].metadata.enum_descriptions && opts[i].metadata.enum_descriptions[j]) {
136 fprintf(output, "\\:\"");
137 zsh_escape_desc(output, opts[i].metadata.enum_descriptions[j]);
138 fprintf(output, "\"");
139 }
140 fprintf(output, "\\\n");
141 }
142 fprintf(output, " ))'\n");
143 }
144 // Boolean options with values
145 else if (opts[i].type == OPTION_TYPE_BOOL) {
146 fprintf(output, "--%s=[", opts[i].long_name);
147 zsh_escape_desc(output, opts[i].help_text);
148 fprintf(output, "]:value:((true\\:\"enable\" false\\:\"disable\"))'\n");
149 }
150 // Action options (no value)
151 else if (opts[i].type == OPTION_TYPE_ACTION) {
152 fprintf(output, "--%s[", opts[i].long_name);
153 zsh_escape_desc(output, opts[i].help_text);
154 fprintf(output, "]'\n");
155 }
156 // Other options
157 else {
158 fprintf(output, "--%s=[", opts[i].long_name);
159 zsh_escape_desc(output, opts[i].help_text);
160 fprintf(output, "]:value:'\n");
161 }
162 }
163
164 fprintf(output, " )\n\n");
165 fprintf(output, " _arguments -C -s -S : $args && return\n\n");
166}
167
171// NOLINTNEXTLINE(unused-function) - May be used for future completion improvements
172__attribute__((unused)) static void zsh_write_options_grouped(FILE *output, const option_descriptor_t *opts,
173 size_t count, const char *func_prefix) {
174 if (!opts || count == 0)
175 return;
176
177 size_t group_count = 0;
178 const char **groups = zsh_collect_groups(opts, count, &group_count);
179
180 // First pass: declare arrays for each group
181 for (size_t g = 0; g < group_count; g++) {
182 const char *group = groups[g];
183 fprintf(output, " local -a %s_%s_opts=(\n", func_prefix, group);
184
185 // Write all options in this group (long options only to avoid "corrections" duplicates)
186 for (size_t i = 0; i < count; i++) {
187 if (!opts[i].group || strcmp(opts[i].group, group) != 0)
188 continue;
189
190 // Long option only (short options are less discoverable via TAB)
191 fprintf(output, " '--%s:", opts[i].long_name);
192 zsh_escape_desc(output, opts[i].help_text);
193 fprintf(output, "'\n");
194 }
195
196 fprintf(output, " )\n");
197 }
198
199 // Second pass: call _describe for each group (with lowercase display)
200 for (size_t g = 0; g < group_count; g++) {
201 const char *group = groups[g];
202 fprintf(output, " _describe -t %s '", group);
203 utf8_write_lowercase(output, group);
204 fprintf(output, " options' %s_%s_opts\n", func_prefix, group);
205 }
206
207 SAFE_FREE(groups);
208}
209
211 if (!output) {
212 return SET_ERRNO(ERROR_INVALID_PARAM, "Output stream cannot be NULL");
213 }
214
215 /* Load binary-level and discovery options for binary-level completion.
216 * Binary-level completion (options before mode name like --color-mode) uses
217 * _arguments with -n -S flags to establish proper zsh completion context.
218 * Without _arguments, completion functions fail with "can only be called from
219 * completion function" error, causing zsh to display "corrections (errors: N)"
220 * messages. The -n flag disables defaults and -S disables short option
221 * processing, allowing safe context establishment. */
222 size_t binary_count = 0;
223 const option_descriptor_t *binary_opts = options_registry_get_for_display(MODE_DISCOVERY, true, &binary_count);
224
225 size_t discovery_count = 0;
226 const option_descriptor_t *discovery_opts = options_registry_get_for_display(MODE_DISCOVERY, false, &discovery_count);
227
228 fprintf(output, "# Zsh completion script for ascii-chat\n"
229 "# Generated from options registry - DO NOT EDIT MANUALLY\n"
230 "\n"
231 "# Configure completion for ascii-chat\n"
232 "# Use only _complete (no corrections or approximate matching)\n"
233 "zstyle ':completion:*:ascii-chat:*' completer _complete\n"
234 "\n"
235 "# Device index completion helpers\n"
236 "_ascii_chat_webcam_indices() {\n"
237 " local -a indices\n"
238 " local output\n"
239 " output=$(\"${words[1]}\" --list-webcams 2>/dev/null) || return\n"
240 " while IFS= read -r line; do\n"
241 " [[ \"$line\" =~ ^[[:space:]]*([0-9]+)[[:space:]]+(.+)$ ]] && \\\n"
242 " indices+=(\"${match[1]}:${match[2]}\")\n"
243 " done <<< \"$output\"\n"
244 " _describe 'webcam' indices\n"
245 "}\n"
246 "\n"
247 "_ascii_chat_microphone_indices() {\n"
248 " local -a indices\n"
249 " local output\n"
250 " output=$(\"${words[1]}\" --list-microphones 2>/dev/null) || return\n"
251 " while IFS= read -r line; do\n"
252 " [[ \"$line\" =~ ^[[:space:]]*([0-9-]+)[[:space:]]+(.+)$ ]] && \\\n"
253 " indices+=(\"${match[1]}:${match[2]}\")\n"
254 " done <<< \"$output\"\n"
255 " _describe 'microphone' indices\n"
256 "}\n"
257 "\n"
258 "_ascii_chat_speakers_indices() {\n"
259 " local -a indices\n"
260 " local output\n"
261 " output=$(\"${words[1]}\" --list-speakers 2>/dev/null) || return\n"
262 " while IFS= read -r line; do\n"
263 " [[ \"$line\" =~ ^[[:space:]]*([0-9-]+)[[:space:]]+(.+)$ ]] && \\\n"
264 " indices+=(\"${match[1]}:${match[2]}\")\n"
265 " done <<< \"$output\"\n"
266 " _describe 'speakers' indices\n"
267 "}\n"
268 "\n"
269 "_ascii_chat_binary() {\n"
270 " local -a binary_opts=(\n");
271
272 /* Add discovery options to binary-level completion */
273 if (discovery_opts) {
274 for (size_t i = 0; i < discovery_count; i++) {
275 fprintf(output, " '--%s:", discovery_opts[i].long_name);
276 zsh_escape_desc(output, discovery_opts[i].help_text);
277 fprintf(output, "'\n");
278 }
279 }
280
281 fprintf(output, " )\n"
282 " _values 'option' \"${binary_opts[@]}\"\n"
283 "}\n"
284 "\n"
285 "_ascii_chat_binary_grouped() {\n"
286 " local curcontext=$curcontext\n"
287 " local -a context line state state_descr args\n"
288 " local -A opt_args\n\n");
289
290 /* Write binary-level options in grouped format */
291 if (binary_opts) {
292 zsh_write_combined_args(output, binary_opts, binary_count);
293 }
294
295 fprintf(output, "}\n"
296 "\n"
297 "_ascii_chat() {\n"
298 " if [[ ${words[2]} == -* ]]; then\n"
299 " # Binary-level options: show both binary and discovery mode options\n"
300 " # (since discovery is the default mode that runs at binary level)\n"
301 " _ascii_chat_binary_grouped\n"
302 " _ascii_chat_discovery\n"
303 " return\n"
304 " fi\n"
305 "\n"
306 " # Mode routing: check words[2] to determine which mode function to call\n"
307 " case $words[2] in\n"
308 " # Server-like modes\n"
309 " server)\n"
310 " _ascii_chat_server\n"
311 " return\n"
312 " ;;\n"
313 " discovery-service)\n"
314 " _ascii_chat_discovery_service\n"
315 " return\n"
316 " ;;\n"
317 "\n"
318 " # Client-like modes\n"
319 " client)\n"
320 " _ascii_chat_client\n"
321 " return\n"
322 " ;;\n"
323 " mirror)\n"
324 " _ascii_chat_mirror\n"
325 " return\n"
326 " ;;\n"
327 " # Default/unknown input - show mode selection only if truly empty, otherwise discovery\n"
328 " *)\n"
329 " # If words[2] is empty AND we're at position 2, show mode selection\n"
330 " # Otherwise (even for partial input like 'disc'), use discovery mode (the default)\n"
331 " if [[ -z \"${words[2]}\" && $CURRENT -eq 2 ]]; then\n"
332 " # Completing the mode name itself - show available modes\n"
333 " local -a server_modes client_modes\n"
334 " server_modes=(\n");
335
336 /* Generate server-like modes from registry */
337 size_t mode_server_count = 0;
338 const mode_descriptor_t *mode_server_descs = get_modes_by_group("server-like", &mode_server_count);
339 if (mode_server_descs) {
340 for (size_t i = 0; i < mode_server_count; i++) {
341 fprintf(output, " '%s:%s'\n", mode_server_descs[i].name, mode_server_descs[i].description);
342 }
343 SAFE_FREE(mode_server_descs);
344 }
345
346 fprintf(output, " )\n"
347 " client_modes=(\n");
348
349 /* Generate client-like modes from registry (exclude discovery since it's the default mode) */
350 size_t mode_client_count = 0;
351 const mode_descriptor_t *mode_client_descs = get_modes_by_group("client-like", &mode_client_count);
352 if (mode_client_descs) {
353 for (size_t i = 0; i < mode_client_count; i++) {
354 /* Skip discovery mode - it's the default when no mode is specified */
355 if (strcmp(mode_client_descs[i].name, "discovery") == 0) {
356 continue;
357 }
358 fprintf(output, " '%s:%s'\n", mode_client_descs[i].name, mode_client_descs[i].description);
359 }
360 SAFE_FREE(mode_client_descs);
361 }
362
363 fprintf(output, " )\n"
364 " _describe -t server-modes 'server-like modes' server_modes\n"
365 " _describe -t client-modes 'client-like modes' client_modes\n"
366 " else\n"
367 " # Default mode (discovery) - complete its options\n"
368 " _ascii_chat_discovery\n"
369 " fi\n"
370 " ;;\n"
371 " esac\n"
372 "}\n"
373 "\n"
374 "# Server-like modes: handle incoming connections and stream management\n"
375 "_ascii_chat_server() {\n"
376 " local curcontext=$curcontext\n"
377 " local -a context line state state_descr args\n"
378 " local -A opt_args\n\n");
379
380 /* Server options - grouped by category */
381 size_t server_count = 0;
382 const option_descriptor_t *server_opts = options_registry_get_for_display(MODE_SERVER, false, &server_count);
383
384 if (server_opts) {
385 zsh_write_combined_args(output, server_opts, server_count);
386 SAFE_FREE(server_opts);
387 }
388
389 fprintf(output, "}\n"
390 "\n"
391 "_ascii_chat_discovery_service() {\n"
392 " local curcontext=$curcontext\n"
393 " local -a context line state state_descr args\n"
394 " local -A opt_args\n\n");
395
396 /* Discovery-service options - grouped by category */
397 size_t discovery_svc_count = 0;
398 const option_descriptor_t *discovery_svc_opts =
399 options_registry_get_for_display(MODE_DISCOVERY_SERVICE, false, &discovery_svc_count);
400
401 if (discovery_svc_opts) {
402 zsh_write_combined_args(output, discovery_svc_opts, discovery_svc_count);
403 SAFE_FREE(discovery_svc_opts);
404 }
405
406 fprintf(output, "}\n"
407 "\n"
408 "# Client-like modes: connect to servers or render local media\n"
409 "_ascii_chat_client() {\n"
410 " local curcontext=$curcontext\n"
411 " local -a context line state state_descr args\n"
412 " local -A opt_args\n\n");
413
414 /* Client options - grouped by category */
415 size_t client_count = 0;
416 const option_descriptor_t *client_opts = options_registry_get_for_display(MODE_CLIENT, false, &client_count);
417
418 if (client_opts) {
419 zsh_write_combined_args(output, client_opts, client_count);
420 SAFE_FREE(client_opts);
421 }
422
423 fprintf(output, "}\n"
424 "\n"
425 "_ascii_chat_mirror() {\n"
426 " local curcontext=$curcontext\n"
427 " local -a context line state state_descr args\n"
428 " local -A opt_args\n\n");
429
430 /* Mirror options - grouped by category */
431 size_t mirror_count = 0;
432 const option_descriptor_t *mirror_opts = options_registry_get_for_display(MODE_MIRROR, false, &mirror_count);
433
434 if (mirror_opts) {
435 zsh_write_combined_args(output, mirror_opts, mirror_count);
436 SAFE_FREE(mirror_opts);
437 }
438
439 fprintf(output, "}\n"
440 "\n"
441 "# Discovery mode: find sessions via discovery service (default mode when no mode specified)\n"
442 "_ascii_chat_discovery() {\n"
443 " local curcontext=$curcontext\n"
444 " local -a context line state state_descr args\n"
445 " local -A opt_args\n\n");
446
447 /* Discovery options - grouped by category (reuse already-loaded options) */
448 if (discovery_opts) {
449 zsh_write_combined_args(output, discovery_opts, discovery_count);
450 }
451
452 fprintf(output, "}\n"
453 "\n"
454 "# Register completion functions (must come after definitions)\n"
455 "compdef _ascii_chat ascii-chat\n"
456 "compdef _ascii_chat 'build/bin/ascii-chat'\n");
457
458 SAFE_FREE(binary_opts);
459 SAFE_FREE(discovery_opts);
460
461 return ASCIICHAT_OK;
462}
@ OPTION_INPUT_ENUM
Choose from fixed set of enum values.
Definition builder.h:178
@ OPTION_TYPE_ACTION
Action that executes and may exit (–list-webcams, etc.)
Definition builder.h:167
@ OPTION_TYPE_BOOL
Boolean flag (–flag, no value)
Definition builder.h:162
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Option enum value definitions - single source of truth.
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
@ MODE_CLIENT
Client mode - network client options.
@ MODE_DISCOVERY_SERVICE
Discovery server mode - session management and WebRTC signaling.
@ MODE_SERVER
Server mode - network server options.
@ MODE_MIRROR
Mirror mode - local webcam viewing (no network)
@ MODE_DISCOVERY
Discovery mode - participant that can dynamically become host.
void utf8_write_lowercase(FILE *output, const char *text)
Write UTF-8 string to file with ASCII lowercase conversion.
Definition utf8.c:360
Mode-aware default value getters and mode metadata.
const mode_descriptor_t * get_modes_by_group(const char *group, size_t *out_count)
Get all mode descriptors grouped by category.
const option_descriptor_t * options_registry_get_for_display(asciichat_mode_t mode, bool for_binary_help, size_t *num_options)
Get options for help/completions display with unified filtering.
Definition public_api.c:324
Central registry of all command-line options with mode applicability.
Network quality metrics for a single participant.
Definition metrics.h:33
Metadata for a mode.
Definition main.c:366
Option descriptor.
Definition builder.h:241
const char * group
Group name for help sections (e.g., "NETWORK OPTIONS")
Definition builder.h:252
option_metadata_t metadata
Metadata for shell completions (enums, ranges, examples, etc.)
Definition builder.h:281
const char ** enum_values
Enum value strings (null-terminated, e.g., {"auto", "none", "16", "256", "truecolor",...
Definition builder.h:195
🔤 UTF-8 Encoding and Decoding Utilities
asciichat_error_t completions_generate_zsh(FILE *output)
Generate zsh completion script.
Definition zsh.c:210
Zsh shell completion generator.