ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
keyboard_help.c
Go to the documentation of this file.
1
8#include "session/display.h"
9#include <ascii-chat/common.h>
12#include <ascii-chat/log/log.h>
17#include <stdio.h>
18#include <string.h>
19#include <ascii-chat/atomic.h>
20#include <stdlib.h>
21
22/* Color codes for enabled/disabled settings */
23#define ENABLED_COLOR LOG_COLOR_INFO /* Green */
24#define DISABLED_COLOR LOG_COLOR_ERROR /* Red */
25
26/* ============================================================================
27 * Keyboard Help Rendering
28 * ============================================================================ */
29
39static void format_volume_bar(double volume, char *bar_output, size_t bar_output_size) {
40 if (!bar_output || bar_output_size < 25) {
41 return;
42 }
43
44 // Clamp volume to [0.0, 1.0]
45 if (volume < 0.0) {
46 volume = 0.0;
47 }
48 if (volume > 1.0) {
49 volume = 1.0;
50 }
51
52 // Calculate filled blocks (10 blocks total for 10% granularity)
53 int filled = (int)(volume * 10.0);
54 int empty = 10 - filled;
55
56 // Build bar: "[====== ] 80%"
57 // Use simple ASCII characters to avoid UTF-8 encoding issues
58 snprintf(bar_output, bar_output_size, "[%.*s%.*s] %d%%", filled, "==========", empty, " ",
59 (int)(volume * 100.0));
60}
61
65static const char *color_mode_to_string(int mode) {
66 switch (mode) {
67 case -1:
68 return "Auto";
69 case 0:
70 return "Mono";
71 case 1:
72 return "16-color";
73 case 2:
74 return "256-color";
75 case 3:
76 return "Truecolor";
77 default:
78 return "Unknown";
79 }
80}
81
85static const char *render_mode_to_string(int mode) {
86 switch (mode) {
87 case 0:
88 return "Foreground";
89 case 1:
90 return "Background";
91 case 2:
92 return "Half-block";
93 default:
94 return "Unknown";
95 }
96}
97
101static const char *color_filter_to_string(color_filter_t filter) {
102 if (filter == COLOR_FILTER_NONE) {
103 return "None";
104 }
106 return def ? def->name : "Unknown";
107}
108
120static void build_help_line(char *output, size_t output_size, const char *content, int max_width) {
121 if (!output || output_size < 256 || !content || max_width < 6) {
122 return;
123 }
124
125 // Available width: max_width - 3 (left border "║ ") - 1 (right border "║")
126 int content_available = max_width - 4;
127 if (content_available < 1)
128 content_available = 1;
129
130 // Truncate content if needed (ANSI-aware, with ellipsis indicator)
131 char truncated[256];
132 truncate_with_ellipsis(content, truncated, sizeof(truncated), content_available);
133
134 int content_width = display_width(truncated);
135 int padding = content_available - content_width;
136 if (padding < 0)
137 padding = 0;
138
139 // Build the line
140 char *pos = output;
141 int remaining = output_size;
142
143 // Left border and spacing
144 int n = snprintf(pos, remaining, "║ %s", truncated);
145 if (n > 0) {
146 pos += n;
147 remaining -= n;
148 }
149
150 // Padding spaces
151 for (int i = 0; i < padding && remaining > 1; i++) {
152 *pos++ = ' ';
153 remaining--;
154 }
155
156 // Right border
157 if (remaining > 3) {
158 snprintf(pos, remaining, "║");
159 }
160}
161
176static void build_settings_line(char *output, size_t output_size, const char *label, const char *value, int max_width,
177 int label_width) {
178 if (!output || output_size < 256 || !label || !value || max_width < 20) {
179 return;
180 }
181
182 // Align all values to start at the same column by padding labels to fixed width
183 const int MAX_LABEL_WIDTH = label_width;
184
185 int actual_label_width = utf8_display_width(label);
186
187 // Calculate label padding to align all values vertically
188 int label_padding = MAX_LABEL_WIDTH - actual_label_width;
189 if (label_padding < 0) {
190 label_padding = 0;
191 }
192
193 // Fixed prefix: "║ " (3) + label (padded to MAX_LABEL_WIDTH) + " : " (3) = 6 + MAX_LABEL_WIDTH columns
194 int fixed_prefix = 1 + 2 + MAX_LABEL_WIDTH + 3;
195 int right_border = 1;
196
197 // Available space for value + final padding
198 int available = max_width - fixed_prefix - right_border;
199 if (available < 4)
200 available = 4; // Minimum space for truncated value
201
202 // Truncate value if needed (ANSI-aware, with ellipsis indicator)
203 char truncated_value[256];
204 truncate_with_ellipsis(value, truncated_value, sizeof(truncated_value), available);
205
206 int value_width = display_width(truncated_value);
207 int padding = available - value_width;
208 if (padding < 0)
209 padding = 0;
210
211 // Build the line: "║ <label><label_pad>: <value><padding>║"
212 char *pos = output;
213 int remaining = output_size;
214
215 // Left border, spacing, and label
216 int n = snprintf(pos, remaining, "║ %s", label);
217 if (n > 0) {
218 pos += n;
219 remaining -= n;
220 }
221
222 // Add label padding spaces to align values
223 for (int i = 0; i < label_padding && remaining > 1; i++) {
224 *pos++ = ' ';
225 remaining--;
226 }
227
228 // Colon with spacing (one space before and after)
229 n = snprintf(pos, remaining, " : ");
230 if (n > 0) {
231 pos += n;
232 remaining -= n;
233 }
234
235 // Value (already truncated)
236 n = snprintf(pos, remaining, "%s", truncated_value);
237 if (n > 0) {
238 pos += n;
239 remaining -= n;
240 }
241
242 // Final padding spaces
243 for (int i = 0; i < padding && remaining > 1; i++) {
244 *pos++ = ' ';
245 remaining--;
246 }
247
248 // Right border
249 if (remaining > 3) {
250 snprintf(pos, remaining, "║");
251 }
252}
253
259static const char *status_indicator(bool enabled) {
261}
262
266static void append_help_line(char *buffer, size_t *buf_pos, size_t BUFFER_SIZE, int start_row, int *current_row,
267 int start_col, int box_width, const char *content) {
268 if (!buffer || *buf_pos >= BUFFER_SIZE || !content) {
269 return;
270 }
271
272 char line_buf[256];
273 int remaining = BUFFER_SIZE - *buf_pos;
274
275 int written = snprintf(buffer + *buf_pos, remaining, "\033[%d;%dH", start_row + *current_row, start_col + 1);
276 if (written > 0) {
277 *buf_pos += written;
278 }
279
280 build_help_line(line_buf, sizeof(line_buf), content, box_width);
281 written = snprintf(buffer + *buf_pos, BUFFER_SIZE - *buf_pos, "%s", line_buf);
282 if (written > 0) {
283 *buf_pos += written;
284 }
285
286 (*current_row)++;
287}
288
293static void append_settings_line(char *buffer, size_t *buf_pos, size_t BUFFER_SIZE, int start_row, int *current_row,
294 int start_col, int box_width, const char *label, const char *value, int label_width) {
295 if (!buffer || *buf_pos >= BUFFER_SIZE || !label || !value) {
296 return;
297 }
298
299 char line_buf[256];
300 int remaining = BUFFER_SIZE - *buf_pos;
301
302 int written = snprintf(buffer + *buf_pos, remaining, "\033[%d;%dH", start_row + *current_row, start_col + 1);
303 if (written > 0) {
304 *buf_pos += written;
305 }
306
307 build_settings_line(line_buf, sizeof(line_buf), label, value, box_width, label_width);
308 written = snprintf(buffer + *buf_pos, BUFFER_SIZE - *buf_pos, "%s", line_buf);
309 if (written > 0) {
310 *buf_pos += written;
311 }
312
313 (*current_row)++;
314}
315
320 if (!ctx) {
321 log_error("keyboard_help_render: ctx is NULL!");
322 return;
323 }
324
325 log_info("keyboard_help_render: STARTING");
326
327 // Get terminal dimensions
328 int term_width = (int)terminal_get_effective_width();
329 int term_height = (int)terminal_get_effective_height();
330 log_info("keyboard_help_render: term_width=%d, term_height=%d", term_width, term_height);
331
332 // Use available terminal width, capped at preferred width
333 int box_width = term_width;
334 if (box_width > 48)
335 box_width = 48; // Cap at preferred width
336 if (box_width < 30)
337 box_width = 30; // Absolute minimum for readability
338
339 // Calculate centering position (true mathematical centering)
340 // Horizontal centering
341 int start_col = (term_width - box_width) / 2;
342 if (start_col < 0) {
343 start_col = 0;
344 }
345
346 // Calculate box height dynamically based on content
347 // With animations separator line added, the standard help screen is 25 rows
348 int box_height = 25;
349
350 // Vertical centering with dynamic box height
351 int start_row = (term_height - box_height) / 2 - 3; // -3 offset for proper vertical centering
352 if (start_row < 0) {
353 start_row = 0;
354 }
355
356 // Build help screen content
357 const size_t BUFFER_SIZE = 8192; // Increased from 4096 to ensure all content fits
358 char *buffer = SAFE_MALLOC(BUFFER_SIZE, char *);
359 size_t buf_pos = 0;
360
361#define APPEND(fmt, ...) \
362 do { \
363 int written = snprintf(buffer + buf_pos, BUFFER_SIZE - buf_pos, fmt, ##__VA_ARGS__); \
364 if (written > 0) { \
365 buf_pos += written; \
366 } \
367 } while (0)
368
369 // Clear screen and position cursor
370 APPEND("\033[2J"); // Clear screen
371 APPEND("\033[H"); // Cursor to home
372
373 // Build help screen with proper spacing using UTF-8 width-aware padding
374 char line_buf[256];
375 char border_buf[256];
376
377 // Generate top border
378 APPEND("\033[%d;%dH", start_row + 1, start_col + 1);
379 border_buf[0] = '\0';
380 int border_pos = 0;
381 int result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "╔");
382 if (result < 0) {
383 log_error("Failed to write top-left corner: snprintf returned %d", result);
384 } else {
385 border_pos += result;
386 }
387 for (int i = 1; i < box_width - 1; i++) {
388 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "═");
389 if (result < 0) {
390 log_error("Failed to write horizontal line: snprintf returned %d", result);
391 break;
392 }
393 border_pos += result;
394 }
395 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "╗");
396 if (result < 0) {
397 log_error("Failed to write top-right corner: snprintf returned %d", result);
398 }
399 APPEND("%s", border_buf);
400
401 // Title
402 APPEND("\033[%d;%dH", start_row + 2, start_col + 1);
403 build_help_line(line_buf, sizeof(line_buf), "ascii-chat Keyboard Shortcuts", box_width);
404 APPEND("%s", line_buf);
405
406 // Generate separator border
407 APPEND("\033[%d;%dH", start_row + 3, start_col + 1);
408 border_buf[0] = '\0';
409 border_pos = 0;
410 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "╠");
411 if (result < 0) {
412 log_error("Failed to write left T: snprintf returned %d", result);
413 } else {
414 border_pos += result;
415 }
416 for (int i = 1; i < box_width - 1; i++) {
417 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "═");
418 if (result < 0) {
419 log_error("Failed to write horizontal line: snprintf returned %d", result);
420 break;
421 }
422 border_pos += result;
423 }
424 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "╣");
425 if (result < 0) {
426 log_error("Failed to write right T: snprintf returned %d", result);
427 }
428 APPEND("%s", border_buf);
429
430 // Navigation section
431 int current_row = 4;
432 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
433 "Navigation & Control:");
434 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
435 "─────────────────────");
436 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
437 "? Toggle this help screen");
438 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
439 "Esc Close help / Quit app");
440
441 // Check if media is provided (only show Space/Seek keys if media is loaded)
442 const char *media_url = GET_OPTION(media_url);
443 const char *media_file = GET_OPTION(media_file);
444 bool has_media = (media_url && strlen(media_url) > 0) || (media_file && strlen(media_file) > 0);
445
446 if (has_media) {
447 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
448 "Space Play/Pause (files only)");
449 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
450 "← / → Seek backward/forward 30s");
451 }
452
453 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
454 "m / M Mute/Unmute audio");
455 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
456 "↑ / ↓ Volume up/down (10%)");
457 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
458 "c / C Cycle color mode");
459 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
460 "f / F Cycle color filter");
461 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
462 "x / X Flip webcam horizontally");
463 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
464 "y / Y Flip webcam vertically");
465 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
466 "r / R Cycle render mode");
467
468#ifndef NDEBUG
469 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
470 "` Print current sync primitive state");
471#endif
472
473 // Blank line before settings section
474 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "");
475
476 // Current settings section
477 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Current Settings:");
478 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "───────────────");
479
480 // Get current option values
481 double current_volume = GET_OPTION(speakers_volume);
482 int current_color_mode = (int)GET_OPTION(color_mode);
483 int current_render_mode = (int)GET_OPTION(render_mode);
484 color_filter_t current_color_filter = GET_OPTION(color_filter);
485 bool flip_x = (bool)GET_OPTION(flip_x);
486 bool flip_y = (bool)GET_OPTION(flip_y);
487 bool current_audio = (bool)GET_OPTION(audio_enabled);
488
489 // Format volume bar as "[======== ] 80%"
490 char volume_bar[32];
491 format_volume_bar(current_volume, volume_bar, sizeof(volume_bar));
492
493 // Get string values
494 const char *color_str = color_mode_to_string(current_color_mode);
495 const char *filter_str = color_filter_to_string(current_color_filter);
496 const char *render_str = render_mode_to_string(current_render_mode);
497
498 // Create status indicators for flip, audio, and matrix rain
499 // Format flip status as "rows=X/O cols=X/O" (rows=flip_y, cols=flip_x)
500 char flip_status[64];
501 snprintf(flip_status, sizeof(flip_status), "rows=%s cols=%s", status_indicator(flip_y), status_indicator(flip_x));
502
503 const char *audio_text = status_indicator(current_audio);
504 bool matrix_rain_enabled = GET_OPTION(matrix_rain);
505 const char *matrix_text = status_indicator(matrix_rain_enabled);
506
507 // Build settings lines with UTF-8 width-aware padding (ordered to match keybinds: m, ↑/↓, c, f, x/y, r)
508 append_settings_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Audio",
509 audio_text, 6);
510 append_settings_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Volume",
511 volume_bar, 6);
512 append_settings_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Color", color_str,
513 6);
514 append_settings_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Filter",
515 filter_str, 6);
516 append_settings_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Render",
517 render_str, 6);
518 append_settings_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Flip",
519 flip_status, 6);
520
521 // Blank line before animations section
522 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "");
523
524 // Animations section
525 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
526 "Animations (number key toggle):");
527 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
528 "───────────────────────────────");
529
530 // Format: "(0) Matrix \"Digital Rain\" : X/O"
531 char animation_line[256];
532 snprintf(animation_line, sizeof(animation_line), "(0) Matrix \"Digital Rain\" : %s", matrix_text);
533 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, animation_line);
534
535 // FPS Counter toggle
536 char fps_line[256];
537 snprintf(fps_line, sizeof(fps_line), "(-) FPS Counter : %s", status_indicator(GET_OPTION(fps_counter)));
538 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, fps_line);
539
540 // Blank line before footer
541 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "");
542
543 // Footer
544 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Press ? to close");
545
546 // Bottom border
547 int remaining_buf = BUFFER_SIZE - buf_pos;
548 int written = snprintf(buffer + buf_pos, remaining_buf, "\033[%d;%dH", start_row + current_row, start_col + 1);
549 if (written > 0) {
550 buf_pos += written;
551 }
552 border_buf[0] = '\0';
553 border_pos = 0;
554 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "╚");
555 if (result < 0) {
556 log_error("Failed to write bottom-left corner: snprintf returned %d", result);
557 } else {
558 border_pos += result;
559 }
560 for (int i = 1; i < box_width - 1; i++) {
561 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "═");
562 if (result < 0) {
563 log_error("Failed to write horizontal line: snprintf returned %d", result);
564 break;
565 }
566 border_pos += result;
567 }
568 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "╝");
569 if (result < 0) {
570 log_error("Failed to write bottom-right corner: snprintf returned %d", result);
571 }
572 written = snprintf(buffer + buf_pos, BUFFER_SIZE - buf_pos, "%s", border_buf);
573 if (written > 0) {
574 buf_pos += written;
575 }
576
577#undef APPEND
578
579 log_info("keyboard_help_render: buffer prepared, buf_pos=%zu", buf_pos);
580
581 // Write buffer to terminal
582 session_display_write_raw(ctx, buffer, buf_pos);
583 log_info("keyboard_help_render: buffer written to terminal");
584
585 // Flush output
586 if (ctx && session_display_has_tty(ctx)) {
587 int tty_fd = session_display_get_tty_fd(ctx);
588 log_info("keyboard_help_render: tty_fd=%d", tty_fd);
589 if (tty_fd >= 0) {
590 (void)terminal_flush(tty_fd);
591 log_info("keyboard_help_render: terminal flushed");
592 }
593 }
594
595 SAFE_FREE(buffer);
596 log_info("keyboard_help_render: COMPLETE");
597}
598
599/* ============================================================================
600 * Keyboard Help State Management
601 *
602 * Note: keyboard_help_toggle() and keyboard_help_is_active()
603 * are implemented in display.c where they have access to the internal
604 * struct session_display_ctx definition containing keyboard_help_active.
605 * ============================================================================ */
⚛️ Atomic operations abstraction layer with debug tracking
⚙️ Common definitions, error codes, macros, and types shared throughout the application
bool enabled
Is filtering active?
Definition grep.c:84
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
asciichat_error_t terminal_flush(int fd)
Flush terminal output.
void session_display_write_raw(session_display_ctx_t *ctx, const char *data, size_t len)
Render raw bytes to the terminal without frame processing.
bool session_display_has_tty(session_display_ctx_t *ctx)
Check if display has a TTY (terminal) available.
void keyboard_help_render(session_display_ctx_t *ctx)
Render keyboard help centered on terminal.
int session_display_get_tty_fd(session_display_ctx_t *ctx)
Get the TTY file descriptor.
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
int display_width(const char *text)
Calculate the visible display width of text with ANSI escape codes.
int utf8_display_width(const char *str)
Calculate terminal display width of a UTF-8 string.
Definition utf8.c:46
void truncate_with_ellipsis(const char *input, char *output, size_t output_size, int max_width)
Truncate a string to a maximum display width, with ellipsis.
const color_filter_def_t * color_filter_get_metadata(color_filter_t filter)
Get color filter metadata by enum value.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
🖥️ Display Width and Terminal Centering Utilities
#define ENABLED_COLOR
#define DISABLED_COLOR
#define APPEND(fmt,...)
🆘 Interactive keyboard help overlay for session keyboard shortcuts
📝 Logging API with multiple log levels and terminal output control
const char * color_mode_to_string(terminal_color_mode_t mode)
Convert color mode enum to string.
const char * render_mode_to_string(render_mode_t mode)
unsigned short int terminal_get_effective_width(void)
Get effective terminal width with fallback priority.
unsigned short int terminal_get_effective_height(void)
Get effective terminal height with fallback priority.
🖥️ Unified terminal display abstraction for session-based rendering
#define bool
Definition stdbool.h:61
Color filter metadata structure.
const char * name
Human-readable filter name (e.g., "matrix-green")
Internal session display context structure.
🖥️ Cross-platform terminal interface for ascii-chat
color_filter_t
Monochromatic color filter enumeration.
Definition terminal.h:599
@ COLOR_FILTER_NONE
No filtering (default)
Definition terminal.h:601
🔤 UTF-8 Encoding and Decoding Utilities
🔤 String Manipulation and Shell Escaping Utilities