ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
splash.c
Go to the documentation of this file.
1
17#include <ascii-chat/ui/splash.h>
18#include <ascii-chat/ui/terminal_screen.h>
19#include <ascii-chat/log/interactive_grep.h>
20#include <ascii-chat/session/display.h>
21#include <ascii-chat/session/session_log_buffer.h>
22#include <ascii-chat/util/display.h>
23#include <ascii-chat/util/ip.h>
24#include <ascii-chat/util/string.h>
25#include <ascii-chat/platform/terminal.h>
26#include <ascii-chat/platform/keyboard.h>
27#include <ascii-chat/platform/system.h>
28#include <ascii-chat/platform/abstraction.h>
29#include <ascii-chat/video/image.h>
30#include <ascii-chat/video/ansi_fast.h>
31#include <ascii-chat/options/options.h>
32#include <ascii-chat/log/logging.h>
33#include <ascii-chat/common.h>
34
35#include <stdio.h>
36#include <stdlib.h>
37#include <string.h>
38#include <stdint.h>
39#include <stdatomic.h>
40#include <time.h>
41
42// ============================================================================
43// ASCII Art and Constants
44// ============================================================================
45
46#define ASCII_LOGO_LINES 7
47#define ASCII_LOGO_WIDTH 36
48
49// Global update notification (set via splash_set_update_notification)
50static char g_update_notification[1024] = {0};
51static mutex_t g_update_notification_mutex;
52
53static const rgb_pixel_t g_rainbow_colors[] = {
54 {255, 0, 0}, // Red
55 {255, 165, 0}, // Orange
56 {255, 255, 0}, // Yellow
57 {0, 255, 0}, // Green
58 {0, 255, 255}, // Cyan
59 {0, 0, 255}, // Blue
60 {255, 0, 255} // Magenta
61};
62#define RAINBOW_COLOR_COUNT 7
63
67static struct {
68 _Atomic(bool) is_running; // true while animation should continue
69 _Atomic(bool) should_stop; // set to true when first frame ready
70 int frame; // current animation frame
71 asciichat_thread_t anim_thread; // animation thread handle
72} g_splash_state = {.is_running = false, .should_stop = false, .frame = 0};
73
74// ============================================================================
75// Helper Functions - TTY Detection
76// ============================================================================
77
78// ============================================================================
79// Helper Functions - Rainbow Rendering
80// ============================================================================
81
89static rgb_pixel_t interpolate_color(rgb_pixel_t color1, rgb_pixel_t color2, double t) {
90 rgb_pixel_t result;
91 result.r = (uint8_t)(color1.r * (1.0 - t) + color2.r * t);
92 result.g = (uint8_t)(color1.g * (1.0 - t) + color2.g * t);
93 result.b = (uint8_t)(color1.b * (1.0 - t) + color2.b * t);
94 return result;
95}
96
102static rgb_pixel_t get_rainbow_color_rgb(double position) {
103 // Normalize position to 0-1 range
104 double norm_pos = position - (long)position;
105 if (norm_pos < 0) {
106 norm_pos += 1.0;
107 }
108
109 // Scale position to color range
110 double color_pos = norm_pos * (RAINBOW_COLOR_COUNT - 1);
111 int color_idx = (int)color_pos;
112 double blend = color_pos - color_idx;
113
114 // Wrap around at the end
115 if (color_idx >= RAINBOW_COLOR_COUNT - 1) {
116 color_idx = RAINBOW_COLOR_COUNT - 1;
117 blend = 0;
118 }
119
120 int next_idx = (color_idx + 1) % RAINBOW_COLOR_COUNT;
121
122 return interpolate_color(g_rainbow_colors[color_idx], g_rainbow_colors[next_idx], blend);
123}
124
125// ============================================================================
126// Header Rendering (callback for terminal_screen)
127// ============================================================================
128
132typedef struct {
133 int frame; // Current animation frame number
134 bool use_colors; // Whether to use rainbow colors
135 char update_notification[1024]; // Update notification message (empty if no update)
137
143static void build_connection_target(char *buffer, size_t buffer_size) {
144 if (!buffer || buffer_size == 0) {
145 return;
146 }
147 buffer[0] = '\0';
148
149 // Mirror mode - no network connection
150 asciichat_mode_t mode = GET_OPTION(detected_mode);
151 if (mode == MODE_MIRROR) {
152 const char *media_url = GET_OPTION(media_url);
153 const char *media_file = GET_OPTION(media_file);
154
155 if (media_url && media_url[0] != '\0') {
156 snprintf(buffer, buffer_size, "Loading from URL...");
157 } else if (media_file && media_file[0] != '\0') {
158 snprintf(buffer, buffer_size, "Loading from file...");
159 } else {
160 snprintf(buffer, buffer_size, "Initializing...");
161 }
162 return;
163 }
164
165 // Check if we have a session string (discovery mode)
166 const char *session = GET_OPTION(session_string);
167 if (session && session[0] != '\0') {
168 snprintf(buffer, buffer_size, "Connecting to session: %s", session);
169 return;
170 }
171
172 // Check if we have an address (client mode)
173 const char *addr = GET_OPTION(address);
174
175 if (addr && addr[0] != '\0') {
176 // Use IP utility to classify the connection type
177 const char *ip_type = get_ip_type_string(addr);
178
179 if (strcmp(ip_type, "Localhost") == 0) {
180 snprintf(buffer, buffer_size, "Connecting to localhost...");
181 } else if (strcmp(ip_type, "LAN") == 0) {
182 snprintf(buffer, buffer_size, "Connecting to %s (LAN)", addr);
183 } else if (strcmp(ip_type, "Internet") == 0) {
184 snprintf(buffer, buffer_size, "Connecting to %s (Internet)", addr);
185 } else if (strcmp(addr, "localhost") == 0) {
186 // Hostname "localhost" not detected by IP util
187 snprintf(buffer, buffer_size, "Connecting to localhost...");
188 } else {
189 // Unknown or hostname
190 snprintf(buffer, buffer_size, "Connecting to %s", addr);
191 }
192 return;
193 }
194
195 // Fallback if no connection info available
196 snprintf(buffer, buffer_size, "Initializing...");
197}
198
212static void render_splash_header(terminal_size_t term_size, void *user_data) {
213 const splash_header_ctx_t *ctx = (const splash_header_ctx_t *)user_data;
214 if (!ctx) {
215 return;
216 }
217
218 // ASCII logo lines (same as help output)
219 const char *ascii_logo[4] = {
220 " __ _ ___ ___(_|_) ___| |__ __ _| |_ ", " / _` / __|/ __| | |_____ / __| '_ \\ / _` | __| ",
221 "| (_| \\__ \\ (__| | |_____| (__| | | | (_| | |_ ", " \\__,_|___/\\___|_|_| \\___|_| |_|\\__,_|\\__| "};
222 const char *tagline = "Video chat in your terminal";
223 const int logo_width = 52;
224 const double rainbow_speed = 0.01; // Characters per frame of wave speed
225
226 // Calculate rainbow offset for this frame (smooth continuous wave)
227 double offset = ctx->frame * rainbow_speed;
228
229 // Line 1: Top border
230 printf("\033[1;36m━");
231 for (int i = 1; i < term_size.cols - 1; i++) {
232 printf("━");
233 }
234 printf("\033[0m\n");
235
236 // Lines 2-5: ASCII logo (centered, truncated if too long)
237 for (int logo_line = 0; logo_line < 4; logo_line++) {
238 // Build plain text line first (for width calculation)
239 char plain_line[512];
240 int horiz_pad = (term_size.cols - logo_width) / 2;
241 if (horiz_pad < 0) {
242 horiz_pad = 0;
243 }
244
245 int pos = 0;
246 for (int j = 0; j < horiz_pad && pos < (int)sizeof(plain_line) - 1; j++) {
247 plain_line[pos++] = ' ';
248 }
249 snprintf(plain_line + pos, sizeof(plain_line) - pos, "%s", ascii_logo[logo_line]);
250
251 // Check visible width and truncate if needed
252 int visible_width = display_width(plain_line);
253 if (visible_width < 0) {
254 visible_width = (int)strlen(plain_line);
255 }
256 if (term_size.cols > 0 && visible_width >= term_size.cols) {
257 plain_line[term_size.cols - 1] = '\0';
258 }
259
260 // Print with rainbow colors
261 int char_idx = 0;
262 for (int i = 0; plain_line[i] != '\0'; i++) {
263 char ch = plain_line[i];
264 if (ch == ' ') {
265 printf(" ");
266 } else if (ctx->use_colors) {
267 double char_pos = (ctx->frame * 52 + char_idx + offset) / 30.0;
268 rgb_pixel_t color = get_rainbow_color_rgb(char_pos);
269 printf("\x1b[38;2;%u;%u;%um%c\x1b[0m", color.r, color.g, color.b, ch);
270 char_idx++;
271 } else {
272 printf("%c", ch);
273 char_idx++;
274 }
275 }
276 printf("\n");
277 }
278
279 // Line 6: Tagline (centered, truncated if too long)
280 char plain_tagline[512];
281 int tagline_len = (int)strlen(tagline);
282 int tagline_pad = (term_size.cols - tagline_len) / 2;
283 if (tagline_pad < 0) {
284 tagline_pad = 0;
285 }
286
287 int tpos = 0;
288 for (int j = 0; j < tagline_pad && tpos < (int)sizeof(plain_tagline) - 1; j++) {
289 plain_tagline[tpos++] = ' ';
290 }
291 snprintf(plain_tagline + tpos, sizeof(plain_tagline) - tpos, "%s", tagline);
292
293 // Check visible width and truncate if needed
294 int tagline_visible_width = display_width(plain_tagline);
295 if (tagline_visible_width < 0) {
296 tagline_visible_width = (int)strlen(plain_tagline);
297 }
298 if (term_size.cols > 0 && tagline_visible_width >= term_size.cols) {
299 plain_tagline[term_size.cols - 1] = '\0';
300 }
301
302 printf("%s\n", plain_tagline);
303
304 // Line 7: Update notification (if available)
305 if (ctx->update_notification[0] != '\0') {
306 char plain_update[1024];
307 int update_len = (int)strlen(ctx->update_notification);
308 int update_pad = (term_size.cols - update_len) / 2;
309 if (update_pad < 0) {
310 update_pad = 0;
311 }
312
313 int upos = 0;
314 for (int j = 0; j < update_pad && upos < (int)sizeof(plain_update) - 1; j++) {
315 plain_update[upos++] = ' ';
316 }
317 snprintf(plain_update + upos, sizeof(plain_update) - upos, "%s", ctx->update_notification);
318
319 // Check visible width and truncate if needed
320 int update_visible_width = display_width(plain_update);
321 if (update_visible_width < 0) {
322 update_visible_width = (int)strlen(plain_update);
323 }
324 if (term_size.cols > 0 && update_visible_width >= term_size.cols) {
325 plain_update[term_size.cols - 1] = '\0';
326 }
327
328 // Print in yellow/warning color
329 printf("%s\n", colored_string(LOG_COLOR_WARN, plain_update));
330 }
331
332 // Line 8 (or 7 if no update): Connection target (centered, truncated if too long)
333 char connection_target[512];
334 build_connection_target(connection_target, sizeof(connection_target));
335
336 char plain_connection[512];
337 int connection_len = (int)strlen(connection_target);
338 int connection_pad = (term_size.cols - connection_len) / 2;
339 if (connection_pad < 0) {
340 connection_pad = 0;
341 }
342
343 int cpos = 0;
344 for (int j = 0; j < connection_pad && cpos < (int)sizeof(plain_connection) - 1; j++) {
345 plain_connection[cpos++] = ' ';
346 }
347 snprintf(plain_connection + cpos, sizeof(plain_connection) - cpos, "%s", connection_target);
348
349 // Check visible width and truncate if needed
350 int connection_visible_width = display_width(plain_connection);
351 if (connection_visible_width < 0) {
352 connection_visible_width = (int)strlen(plain_connection);
353 }
354 if (term_size.cols > 0 && connection_visible_width >= term_size.cols) {
355 plain_connection[term_size.cols - 1] = '\0';
356 }
357
358 printf("%s\n", plain_connection);
359
360 // Line 9 (or 8 if no update): Bottom border
361 printf("\033[1;36m━");
362 for (int i = 1; i < term_size.cols - 1; i++) {
363 printf("━");
364 }
365 printf("\033[0m\n");
366}
367
368// ============================================================================
369// Public API
370// ============================================================================
371
372bool splash_should_display(bool is_intro) {
373 // Check option flags (display splash if enabled, regardless of TTY for testing)
374 if (is_intro) {
375 // Allow splash in snapshot mode if loading from URL/file (has loading time)
376 // Skip splash only for quick webcam snapshots
377 bool is_snapshot = GET_OPTION(snapshot_mode);
378 bool has_media = (GET_OPTION(media_url) && strlen(GET_OPTION(media_url)) > 0) ||
379 (GET_OPTION(media_file) && strlen(GET_OPTION(media_file)) > 0);
380
381 // Show splash if:
382 // 1. Not in snapshot mode, OR
383 // 2. In snapshot mode but loading from URL/file (needs splash during load)
384 return GET_OPTION(splash) && (!is_snapshot || has_media);
385 } else {
386 return GET_OPTION(status_screen);
387 }
388}
389
394static void *splash_animation_thread(void *arg) {
395 (void)arg;
396
397 // Check if colors should be used (TTY check)
398 bool use_colors = terminal_should_color_output(STDOUT_FILENO);
399
400 // Initialize keyboard for interactive grep (if terminal is interactive)
401 bool keyboard_enabled = false;
403 if (keyboard_init() == ASCIICHAT_OK) {
404 keyboard_enabled = true;
405 }
406 }
407
408 // Animate with rainbow wave effect
409 int frame = 0;
410 const int anim_speed = 100; // milliseconds per frame
411
412 while (!atomic_load(&g_splash_state.should_stop) && !shutdown_is_requested()) {
413 // Poll keyboard for interactive grep and Escape to cancel
414 if (keyboard_enabled) {
415 keyboard_key_t key = keyboard_read_nonblocking();
416 if (key == KEY_ESCAPE) {
417 // Escape key: cancel grep if active, otherwise cancel splash
419 interactive_grep_exit_mode(false); // Cancel grep without applying
420 } else {
421 atomic_store(&g_splash_state.should_stop, true); // Exit splash screen
422 }
423 } else if (key != KEY_NONE && interactive_grep_should_handle(key)) {
425 // Continue to render immediately with grep active
426 }
427 }
428 // Set up splash header context for this frame
429 splash_header_ctx_t header_ctx = {
430 .frame = frame,
431 .use_colors = use_colors,
432 };
433
434 // Copy update notification from global state (thread-safe)
435 static bool mutex_initialized = false;
436 if (!mutex_initialized) {
437 mutex_init(&g_update_notification_mutex);
438 mutex_initialized = true;
439 }
440 mutex_lock(&g_update_notification_mutex);
441 SAFE_STRNCPY(header_ctx.update_notification, g_update_notification, sizeof(header_ctx.update_notification));
442 mutex_unlock(&g_update_notification_mutex);
443
444 // Calculate header lines: 8 base lines + 1 if update notification present
445 int header_lines = 8;
446 if (header_ctx.update_notification[0] != '\0') {
447 header_lines = 9;
448 }
449
450 // Configure terminal screen with splash header callback
451 terminal_screen_config_t screen_config = {
452 .fixed_header_lines = header_lines,
453 .render_header = render_splash_header,
454 .user_data = &header_ctx,
455 .show_logs = true, // Show live log feed below splash
456 };
457
458 // Render the screen (header + logs) only in interactive mode
459 // In non-interactive mode, logs flow to stdout/stderr normally
461 terminal_screen_render(&screen_config);
462 }
463
464 // Move to next frame
465 frame++;
466
467 // Sleep to control animation speed (unless grep needs immediate rerender)
469 platform_sleep_ms(anim_speed);
470 }
471 }
472
473 // Cleanup keyboard
474 if (keyboard_enabled) {
475 keyboard_destroy();
476 }
477
478 atomic_store(&g_splash_state.is_running, false);
479 return NULL;
480}
481
483 (void)ctx; // Parameter not used currently
484
485 // Pre-checks
486 if (!splash_should_display(true)) {
487 return 0; // ASCIICHAT_OK equivalent
488 }
489
490 // Don't initialize log buffer in non-interactive mode - logs go directly to stdout/stderr
492 return 0;
493 }
494
495 // Check terminal size
496 int width = GET_OPTION(width);
497 int height = GET_OPTION(height);
498 if (width < 50 || height < 20) {
499 return 0;
500 }
501
502 // Initialize log buffer for capturing logs during animation
504 log_warn("Failed to initialize splash log buffer");
505 return 0;
506 }
507
508 // Clear screen
509 terminal_clear_screen();
510 fflush(stdout);
511
512 // Set running flag
513 atomic_store(&g_splash_state.is_running, true);
514 atomic_store(&g_splash_state.should_stop, false);
515 g_splash_state.frame = 0;
516
517 // Start animation thread
518 if (asciichat_thread_create(&g_splash_state.anim_thread, splash_animation_thread, NULL) != ASCIICHAT_OK) {
519 log_warn("Failed to create splash animation thread");
520 return 0;
521 }
522
523 return 0; // ASCIICHAT_OK
524}
525
527 // Signal animation thread to stop
528 atomic_store(&g_splash_state.should_stop, true);
529
530 // Wait for animation thread to finish
531 if (atomic_load(&g_splash_state.is_running)) {
532 asciichat_thread_join(&g_splash_state.anim_thread, NULL);
533 }
534
535 atomic_store(&g_splash_state.is_running, false);
536
537 // Don't clear screen here - first frame will do it
538 // Clearing here can cause a brief scroll artifact during transition
539
540 // NOTE: Do NOT cleanup session_log_buffer here - it may be used by status screen
541 // Status screen will call session_log_buffer_destroy() when appropriate
542
543 return 0; // ASCIICHAT_OK
544}
545
547 // Pre-checks
548 if (!splash_should_display(false)) {
549 return 0;
550 }
551
552 // Check mode validity
553 if (mode != 0 && mode != 3) { // MODE_SERVER=0, MODE_DISCOVERY_SERVICE=3
554 log_error("Status screen only for server/discovery modes");
555 return 1; // ERROR
556 }
557
558 // Check terminal size
559 int width = GET_OPTION(width);
560 int height = GET_OPTION(height);
561 if (width < 50 || height < 15) {
562 log_debug("Terminal too small for status screen");
563 return 0;
564 }
565
566 // Get terminal capabilities for UTF-8
567 bool has_utf8 = (GET_OPTION(force_utf8) >= 0);
568 (void)has_utf8; // Suppress unused warning
569
570 // Build and display status box
571 terminal_clear_screen();
572
573 char buffer[4096] = {0};
574 snprintf(buffer, sizeof(buffer), "\n");
575
576 if (mode == 0) {
577 // Server mode
578 snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), " Server Status\n");
579 snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), " Address: %s:%d\n", GET_OPTION(address),
580 GET_OPTION(port));
581 snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), " Max clients: %d\n", GET_OPTION(max_clients));
582 snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), " Encryption: %s\n",
583 GET_OPTION(no_encrypt) ? "Disabled" : "Enabled");
584 } else if (mode == 3) {
585 // Discovery service mode
586 snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), " Discovery Service\n");
587 snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), " Address: %s:%d\n", GET_OPTION(address),
588 GET_OPTION(discovery_port));
589 }
590
591 // Add update notification if available
592 static bool mutex_initialized_for_status = false;
593 if (!mutex_initialized_for_status) {
594 mutex_init(&g_update_notification_mutex);
595 mutex_initialized_for_status = true;
596 }
597 mutex_lock(&g_update_notification_mutex);
598 if (g_update_notification[0] != '\0') {
599 snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), "\n %s\n",
600 colored_string(LOG_COLOR_WARN, g_update_notification));
601 }
602 mutex_unlock(&g_update_notification_mutex);
603
604 snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), "\n");
605
606 printf("%s", buffer);
607 fflush(stdout);
608
609 return 0; // ASCIICHAT_OK
610}
611
612void splash_set_update_notification(const char *notification) {
613 static bool mutex_initialized = false;
614 if (!mutex_initialized) {
615 mutex_init(&g_update_notification_mutex);
616 mutex_initialized = true;
617 }
618
619 mutex_lock(&g_update_notification_mutex);
620
621 if (!notification || notification[0] == '\0') {
622 g_update_notification[0] = '\0';
623 log_debug("Cleared update notification for splash/status screens");
624 } else {
625 SAFE_STRNCPY(g_update_notification, notification, sizeof(g_update_notification));
626 log_debug("Set update notification for splash/status screens: %s", notification);
627 }
628
629 mutex_unlock(&g_update_notification_mutex);
630}
bool shutdown_is_requested(void)
Definition common.c:65
int buffer_size
Size of circular buffer.
Definition grep.c:84
void interactive_grep_exit_mode(bool accept)
bool interactive_grep_is_active(void)
bool interactive_grep_needs_rerender(void)
asciichat_error_t interactive_grep_handle_key(keyboard_key_t key)
bool interactive_grep_should_handle(int key)
const char * get_ip_type_string(const char *ip)
Definition ip.c:1091
int display_width(const char *text)
bool terminal_is_interactive(void)
bool terminal_should_color_output(int fd)
Determine if color output should be used.
void platform_sleep_ms(unsigned int ms)
bool session_log_buffer_init(void)
int splash_intro_done(void)
Definition splash.c:526
int splash_intro_start(session_display_ctx_t *ctx)
Definition splash.c:482
#define RAINBOW_COLOR_COUNT
Definition splash.c:62
int splash_display_status(int mode)
Definition splash.c:546
void splash_set_update_notification(const char *notification)
Definition splash.c:612
bool splash_should_display(bool is_intro)
Definition splash.c:372
Internal session display context structure.
Context data for splash header rendering.
Definition splash.c:132
char update_notification[1024]
Definition splash.c:135
void terminal_screen_render(const terminal_screen_config_t *config)
int mutex_init(mutex_t *mutex)
Definition threading.c:16
int asciichat_thread_create(asciichat_thread_t *thread, void *(*start_routine)(void *), void *arg)
Definition threading.c:42
int asciichat_thread_join(asciichat_thread_t *thread, void **retval)
Definition threading.c:46
const char * colored_string(log_color_t color, const char *text)