ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
help_screen.c
Go to the documentation of this file.
1
7#include <ascii-chat/ui/help_screen.h>
8#include <ascii-chat/session/display.h>
9#include <ascii-chat/common.h>
10#include <ascii-chat/options/options.h>
11#include <ascii-chat/platform/terminal.h>
12#include <ascii-chat/log/logging.h>
13#include <ascii-chat/util/string.h>
14#include <ascii-chat/util/utf8.h>
15#include <stdio.h>
16#include <string.h>
17#include <stdatomic.h>
18#include <stdlib.h>
19
20/* Color codes for enabled/disabled settings */
21#define ENABLED_COLOR LOG_COLOR_INFO /* Green */
22#define DISABLED_COLOR LOG_COLOR_ERROR /* Red */
23
24/* ============================================================================
25 * Help Screen Rendering
26 * ============================================================================ */
27
37static void format_volume_bar(double volume, char *bar_output, size_t bar_output_size) {
38 if (!bar_output || bar_output_size < 25) {
39 return;
40 }
41
42 // Clamp volume to [0.0, 1.0]
43 if (volume < 0.0) {
44 volume = 0.0;
45 }
46 if (volume > 1.0) {
47 volume = 1.0;
48 }
49
50 // Calculate filled blocks (10 blocks total for 10% granularity)
51 int filled = (int)(volume * 10.0);
52 int empty = 10 - filled;
53
54 // Build bar: "[====== ] 80%"
55 // Use simple ASCII characters to avoid UTF-8 encoding issues
56 snprintf(bar_output, bar_output_size, "[%.*s%.*s] %d%%", filled, "==========", empty, " ",
57 (int)(volume * 100.0));
58}
59
63static const char *color_mode_to_string(int mode) {
64 switch (mode) {
65 case 0:
66 return "Mono";
67 case 1:
68 return "16-color";
69 case 2:
70 return "256-color";
71 case 3:
72 return "Truecolor";
73 default:
74 return "Unknown";
75 }
76}
77
81static const char *render_mode_to_string(int mode) {
82 switch (mode) {
83 case 0:
84 return "Foreground";
85 case 1:
86 return "Background";
87 case 2:
88 return "Half-block";
89 default:
90 return "Unknown";
91 }
92}
93
104static void build_help_line(char *output, size_t output_size, const char *content) {
105 if (!output || output_size < 256 || !content) {
106 return;
107 }
108
109 int content_width = utf8_display_width(content);
110
111 // Line structure: "║ " (3 cols) + content + padding + "║" (1 col) = 48 cols
112 // So: padding = 48 - 3 - content_width - 1 = 44 - content_width
113 int padding = 44 - content_width;
114
115 if (padding < 0) {
116 padding = 0;
117 }
118
119 // Build the line
120 char *pos = output;
121 int remaining = output_size;
122
123 // Left border and spacing
124 int n = snprintf(pos, remaining, "║ %s", content);
125 if (n > 0) {
126 pos += n;
127 remaining -= n;
128 }
129
130 // Padding spaces
131 for (int i = 0; i < padding && remaining > 1; i++) {
132 *pos++ = ' ';
133 remaining--;
134 }
135
136 // Right border
137 if (remaining > 3) {
138 snprintf(pos, remaining, "║");
139 }
140}
141
154static void build_settings_line(char *output, size_t output_size, const char *label, const char *value) {
155 if (!output || output_size < 256 || !label || !value) {
156 return;
157 }
158
159 // Align all values to start at the same column by padding labels to fixed width
160 // Maximum label width is 6 ("Volume", "Render", "Webcam")
161 const int MAX_LABEL_WIDTH = 6;
162
163 int label_width = utf8_display_width(label);
164 int value_width = utf8_display_width(value);
165
166 // Calculate label padding to align all values vertically
167 int label_padding = MAX_LABEL_WIDTH - label_width;
168 if (label_padding < 0) {
169 label_padding = 0;
170 }
171
172 // Line structure: "║ " (3) + label + padding + ": " (3) + value + spacing + "║" (1)
173 // Fixed part: 1 (║) + 2 ( ) + 6 (label max) + 3 (:__) = 12 columns to value start
174 // Total: 12 + value_width + final_padding + 1 = 48
175
176 int fixed_prefix = 1 + 2 + MAX_LABEL_WIDTH + 3; // "║ " + label (padded) + ": "
177 int right_border = 1; // "║" = 1 display col
178
179 // Available space for value + padding: 48 - fixed_prefix - right_border
180 int available = 48 - fixed_prefix - right_border; // 36 columns for value + padding
181 int padding = available - value_width;
182
183 // Ensure non-negative padding
184 if (padding < 0) {
185 padding = 0;
186 }
187
188 // Build the line: "║ <label><label_pad>: <value><padding>║"
189 char *pos = output;
190 int remaining = output_size;
191
192 // Left border, spacing, and label
193 int n = snprintf(pos, remaining, "║ %s", label);
194 if (n > 0) {
195 pos += n;
196 remaining -= n;
197 }
198
199 // Add label padding spaces to align values
200 for (int i = 0; i < label_padding && remaining > 1; i++) {
201 *pos++ = ' ';
202 remaining--;
203 }
204
205 // Colon and spacing before value (two spaces)
206 n = snprintf(pos, remaining, ": ");
207 if (n > 0) {
208 pos += n;
209 remaining -= n;
210 }
211
212 // Value
213 n = snprintf(pos, remaining, "%s", value);
214 if (n > 0) {
215 pos += n;
216 remaining -= n;
217 }
218
219 // Final padding spaces
220 for (int i = 0; i < padding && remaining > 1; i++) {
221 *pos++ = ' ';
222 remaining--;
223 }
224
225 // Right border
226 if (remaining > 3) {
227 snprintf(pos, remaining, "║");
228 }
229}
230
235 if (!ctx) {
236 return;
237 }
238
239 // Get terminal dimensions
240 int term_width = (int)GET_OPTION(width);
241 int term_height = (int)GET_OPTION(height);
242
243 // Minimum viable terminal size
244 if (term_width < 50 || term_height < 20) {
245 // Terminal too small - just show a minimal message
246 const char *msg = "\n[Terminal too small for help screen - try resizing]\n";
247 session_display_write_raw(ctx, msg, strlen(msg));
248 return;
249 }
250
251 // Help screen box dimensions (24 rows total: border + title + nav (7 lines) + separator + settings + blank + footer +
252 // border)
253 const int box_width = 48; // Display columns
254 const int box_height = 24; // Total rows including borders
255
256 // Calculate centering position
257 // Horizontal centering
258 int start_col = (term_width - box_width) / 2;
259 if (start_col < 0) {
260 start_col = 0;
261 }
262
263 // Vertical centering
264 int start_row = (term_height - box_height) / 2;
265 if (start_row < 1) {
266 start_row = 1; // Never put at top of screen (leave room for prompts)
267 }
268
269 // Build help screen content
270 const size_t BUFFER_SIZE = 8192; // Increased from 4096 to ensure all content fits
271 char *buffer = SAFE_MALLOC(BUFFER_SIZE, char *);
272 size_t buf_pos = 0;
273
274#define APPEND(fmt, ...) \
275 do { \
276 int written = snprintf(buffer + buf_pos, BUFFER_SIZE - buf_pos, fmt, ##__VA_ARGS__); \
277 if (written > 0) { \
278 buf_pos += written; \
279 } \
280 } while (0)
281
282 // Clear screen and position cursor
283 APPEND("\033[2J"); // Clear screen
284 APPEND("\033[H"); // Cursor to home
285
286 // Build help screen with proper spacing using UTF-8 width-aware padding
287 char line_buf[256];
288
289 // Top border
290 APPEND("\033[%d;%dH", start_row + 1, start_col + 1);
291 APPEND("╔══════════════════════════════════════════════╗");
292
293 // Title
294 APPEND("\033[%d;%dH", start_row + 2, start_col + 1);
295 build_help_line(line_buf, sizeof(line_buf), "ascii-chat Keyboard Shortcuts");
296 APPEND("%s", line_buf);
297
298 // Separator after title
299 APPEND("\033[%d;%dH", start_row + 3, start_col + 1);
300 APPEND("╠══════════════════════════════════════════════╣");
301
302 // Navigation section
303 APPEND("\033[%d;%dH", start_row + 4, start_col + 1);
304 build_help_line(line_buf, sizeof(line_buf), "Navigation & Control:");
305 APPEND("%s", line_buf);
306
307 APPEND("\033[%d;%dH", start_row + 5, start_col + 1);
308 build_help_line(line_buf, sizeof(line_buf), "─────────────────────");
309 APPEND("%s", line_buf);
310
311 APPEND("\033[%d;%dH", start_row + 6, start_col + 1);
312 build_help_line(line_buf, sizeof(line_buf), "? Toggle this help screen");
313 APPEND("%s", line_buf);
314
315 APPEND("\033[%d;%dH", start_row + 7, start_col + 1);
316 build_help_line(line_buf, sizeof(line_buf), "Space Play/Pause (files only)");
317 APPEND("%s", line_buf);
318
319 APPEND("\033[%d;%dH", start_row + 8, start_col + 1);
320 build_help_line(line_buf, sizeof(line_buf), "← / → Seek backward/forward 30s");
321 APPEND("%s", line_buf);
322
323 APPEND("\033[%d;%dH", start_row + 9, start_col + 1);
324 build_help_line(line_buf, sizeof(line_buf), "m Mute/Unmute audio");
325 APPEND("%s", line_buf);
326
327 APPEND("\033[%d;%dH", start_row + 10, start_col + 1);
328 build_help_line(line_buf, sizeof(line_buf), "↑ / ↓ Volume up/down (10%)");
329 APPEND("%s", line_buf);
330
331 APPEND("\033[%d;%dH", start_row + 11, start_col + 1);
332 build_help_line(line_buf, sizeof(line_buf), "c Cycle color mode");
333 APPEND("%s", line_buf);
334
335 APPEND("\033[%d;%dH", start_row + 12, start_col + 1);
336 build_help_line(line_buf, sizeof(line_buf), "f Flip webcam horizontally");
337 APPEND("%s", line_buf);
338
339 APPEND("\033[%d;%dH", start_row + 13, start_col + 1);
340 build_help_line(line_buf, sizeof(line_buf), "r Cycle render mode");
341 APPEND("%s", line_buf);
342
343#ifndef NDEBUG
344 APPEND("\033[%d;%dH", start_row + 14, start_col + 1);
345 build_help_line(line_buf, sizeof(line_buf), "Ctrl+L Print held lock state");
346 APPEND("%s", line_buf);
347
348 // Blank line before settings section
349 APPEND("\033[%d;%dH", start_row + 15, start_col + 1);
350 build_help_line(line_buf, sizeof(line_buf), "");
351 APPEND("%s", line_buf);
352
353 // Current settings section (adjusted row numbers for Ctrl+L line)
354 APPEND("\033[%d;%dH", start_row + 16, start_col + 1);
355 build_help_line(line_buf, sizeof(line_buf), "Current Settings:");
356 APPEND("%s", line_buf);
357
358 APPEND("\033[%d;%dH", start_row + 17, start_col + 1);
359 build_help_line(line_buf, sizeof(line_buf), "───────────────");
360 APPEND("%s", line_buf);
361#else
362 // Blank line before settings section
363 APPEND("\033[%d;%dH", start_row + 14, start_col + 1);
364 build_help_line(line_buf, sizeof(line_buf), "");
365 APPEND("%s", line_buf);
366
367 // Current settings section
368 APPEND("\033[%d;%dH", start_row + 15, start_col + 1);
369 build_help_line(line_buf, sizeof(line_buf), "Current Settings:");
370 APPEND("%s", line_buf);
371
372 APPEND("\033[%d;%dH", start_row + 16, start_col + 1);
373 build_help_line(line_buf, sizeof(line_buf), "───────────────");
374 APPEND("%s", line_buf);
375#endif
376
377 // Current settings section
378 APPEND("\033[%d;%dH", start_row + 15, start_col + 1);
379 build_help_line(line_buf, sizeof(line_buf), "Current Settings:");
380 APPEND("%s", line_buf);
381
382 APPEND("\033[%d;%dH", start_row + 16, start_col + 1);
383 build_help_line(line_buf, sizeof(line_buf), "───────────────");
384 APPEND("%s", line_buf);
385
386 // Get current option values
387 double current_volume = GET_OPTION(speakers_volume);
388 int current_color_mode = (int)GET_OPTION(color_mode);
389 int current_render_mode = (int)GET_OPTION(render_mode);
390 bool flip_x = (bool)GET_OPTION(flip_x);
391 bool flip_y = (bool)GET_OPTION(flip_y);
392 bool current_audio = (bool)GET_OPTION(audio_enabled);
393
394 // Format volume bar as "[======== ] 80%"
395 char volume_bar[32];
396 format_volume_bar(current_volume, volume_bar, sizeof(volume_bar));
397
398 // Get string values
399 const char *color_str = color_mode_to_string(current_color_mode);
400 const char *render_str = render_mode_to_string(current_render_mode);
401
402 // Create colored strings for flip state
403 const char *flip_text = "None";
404 if (flip_x && flip_y) {
405 flip_text = colored_string(ENABLED_COLOR, "X & Y");
406 } else if (flip_x) {
407 flip_text = colored_string(ENABLED_COLOR, "X");
408 } else if (flip_y) {
409 flip_text = colored_string(ENABLED_COLOR, "Y");
410 } else {
411 flip_text = colored_string(DISABLED_COLOR, "None");
412 }
413
414 // Audio state (Enabled = green, Disabled = red)
415 const char *audio_text =
416 current_audio ? colored_string(ENABLED_COLOR, "Enabled") : colored_string(DISABLED_COLOR, "Disabled");
417
418 // Build settings lines with UTF-8 width-aware padding (ordered to match keybinds: m, ↑/↓, c, r, f)
419#ifndef NDEBUG
420 APPEND("\033[%d;%dH", start_row + 18, start_col + 1);
421 build_settings_line(line_buf, sizeof(line_buf), "Audio", audio_text);
422 APPEND("%s", line_buf);
423
424 APPEND("\033[%d;%dH", start_row + 19, start_col + 1);
425 build_settings_line(line_buf, sizeof(line_buf), "Volume", volume_bar);
426 APPEND("%s", line_buf);
427
428 APPEND("\033[%d;%dH", start_row + 20, start_col + 1);
429 build_settings_line(line_buf, sizeof(line_buf), "Color", color_str);
430 APPEND("%s", line_buf);
431
432 APPEND("\033[%d;%dH", start_row + 21, start_col + 1);
433 build_settings_line(line_buf, sizeof(line_buf), "Render", render_str);
434 APPEND("%s", line_buf);
435
436 APPEND("\033[%d;%dH", start_row + 22, start_col + 1);
437 build_settings_line(line_buf, sizeof(line_buf), "Flip", flip_text);
438 APPEND("%s", line_buf);
439
440 // Blank line before footer
441 APPEND("\033[%d;%dH", start_row + 23, start_col + 1);
442 build_help_line(line_buf, sizeof(line_buf), "");
443 APPEND("%s", line_buf);
444
445 // Footer
446 APPEND("\033[%d;%dH", start_row + 24, start_col + 1);
447 build_help_line(line_buf, sizeof(line_buf), "Press ? to close");
448 APPEND("%s", line_buf);
449
450 // Bottom border
451 APPEND("\033[%d;%dH", start_row + 25, start_col + 1);
452 APPEND("╚══════════════════════════════════════════════╝");
453#else
454 APPEND("\033[%d;%dH", start_row + 17, start_col + 1);
455 build_settings_line(line_buf, sizeof(line_buf), "Audio", audio_text);
456 APPEND("%s", line_buf);
457
458 APPEND("\033[%d;%dH", start_row + 18, start_col + 1);
459 build_settings_line(line_buf, sizeof(line_buf), "Volume", volume_bar);
460 APPEND("%s", line_buf);
461
462 APPEND("\033[%d;%dH", start_row + 19, start_col + 1);
463 build_settings_line(line_buf, sizeof(line_buf), "Color", color_str);
464 APPEND("%s", line_buf);
465
466 APPEND("\033[%d;%dH", start_row + 20, start_col + 1);
467 build_settings_line(line_buf, sizeof(line_buf), "Render", render_str);
468 APPEND("%s", line_buf);
469
470 APPEND("\033[%d;%dH", start_row + 21, start_col + 1);
471 build_settings_line(line_buf, sizeof(line_buf), "Flip", flip_text);
472 APPEND("%s", line_buf);
473
474 // Blank line before footer
475 APPEND("\033[%d;%dH", start_row + 22, start_col + 1);
476 build_help_line(line_buf, sizeof(line_buf), "");
477 APPEND("%s", line_buf);
478
479 // Footer
480 APPEND("\033[%d;%dH", start_row + 23, start_col + 1);
481 build_help_line(line_buf, sizeof(line_buf), "Press ? to close");
482 APPEND("%s", line_buf);
483
484 // Bottom border
485 APPEND("\033[%d;%dH", start_row + 24, start_col + 1);
486 APPEND("╚══════════════════════════════════════════════╝");
487#endif
488
489 // Cursor positioning after rendering
490#ifndef NDEBUG
491 APPEND("\033[%d;%dH", start_row + 26, start_col + 1);
492#else
493 APPEND("\033[%d;%dH", start_row + 25, start_col + 1);
494#endif
495
496#undef APPEND
497
498 // Write buffer to terminal
499 session_display_write_raw(ctx, buffer, buf_pos);
500
501 // Flush output
502 if (ctx && session_display_has_tty(ctx)) {
503 int tty_fd = session_display_get_tty_fd(ctx);
504 if (tty_fd >= 0) {
505 (void)terminal_flush(tty_fd);
506 }
507 }
508
509 SAFE_FREE(buffer);
510}
511
512/* ============================================================================
513 * Help Screen State Management
514 *
515 * Note: session_display_toggle_help() and session_display_is_help_active()
516 * are implemented in display.c where they have access to the internal
517 * struct session_display_ctx definition containing help_screen_active.
518 * ============================================================================ */
#define ENABLED_COLOR
Definition help_screen.c:21
#define DISABLED_COLOR
Definition help_screen.c:22
void session_display_render_help(session_display_ctx_t *ctx)
Render help screen centered on terminal.
#define APPEND(fmt,...)
void session_display_write_raw(session_display_ctx_t *ctx, const char *data, size_t len)
bool session_display_has_tty(session_display_ctx_t *ctx)
int session_display_get_tty_fd(session_display_ctx_t *ctx)
const char * color_mode_to_string(terminal_color_mode_t mode)
const char * render_mode_to_string(render_mode_t mode)
asciichat_error_t terminal_flush(int fd)
#define bool
Definition stdbool.h:22
Internal session display context structure.
int utf8_display_width(const char *str)
Definition utf8.c:46
const char * colored_string(log_color_t color, const char *text)