ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
splash.c
Go to the documentation of this file.
1
21#include "session/display.h"
25#include <ascii-chat/util/ip.h>
37#include <ascii-chat/log/log.h>
38#include <ascii-chat/common.h>
39
40#include <stdio.h>
41#include <stdlib.h>
42#include <string.h>
43#include <stdint.h>
44#include <ascii-chat/atomic.h>
45#include <time.h>
46#include <unistd.h>
47#include <fcntl.h>
48
49// ============================================================================
50// ASCII Art and Constants
51// ============================================================================
52
53#define ASCII_LOGO_LINES 7
54#define ASCII_LOGO_WIDTH 36
55
56// Global update notification (set via splash_set_update_notification)
57static char g_update_notification[1024] = {0};
58static mutex_t g_update_notification_mutex;
59static lifecycle_t g_update_notification_lifecycle = LIFECYCLE_INIT_MUTEX(&g_update_notification_mutex);
60
61static const rgb_pixel_t g_rainbow_colors[] = {
62 {255, 0, 0}, // Red
63 {255, 165, 0}, // Orange
64 {255, 255, 0}, // Yellow
65 {0, 255, 0}, // Green
66 {0, 255, 255}, // Cyan
67 {0, 0, 255}, // Blue
68 {255, 0, 255} // Magenta
69};
70#define RAINBOW_COLOR_COUNT 7
71
72// ============================================================================
73// Log Management
74// ============================================================================
75
76void splash_log_init(void) {
78 if (buf) {
80 }
81}
82
87
91
95static struct {
96 atomic_t is_running; // true while animation should continue
97 atomic_t should_stop; // set to true when splash should stop
98 atomic_t thread_created; // true if animation thread was successfully created
99 int frame; // current animation frame
100 asciichat_thread_t anim_thread; // animation thread handle
101 uint64_t start_time_ns; // when splash was started (for minimum display time)
102 atomic_t intro_done_time_ns; // when splash_intro_done() was called
103} g_splash_state = {.is_running = {0},
104 .should_stop = {0},
105 .thread_created = {0},
106 .frame = 0,
107 .start_time_ns = 0,
108 .intro_done_time_ns = {0}};
109
110// ============================================================================
111// Helper Functions - TTY Detection
112// ============================================================================
113
114// ============================================================================
115// Helper Functions - Rainbow Rendering
116// ============================================================================
117
125static rgb_pixel_t interpolate_color(rgb_pixel_t color1, rgb_pixel_t color2, double t) {
126 rgb_pixel_t result;
127 result.r = (uint8_t)(color1.r * (1.0 - t) + color2.r * t);
128 result.g = (uint8_t)(color1.g * (1.0 - t) + color2.g * t);
129 result.b = (uint8_t)(color1.b * (1.0 - t) + color2.b * t);
130 return result;
131}
132
138static rgb_pixel_t get_rainbow_color_rgb(double position) {
139 // Normalize position to 0-1 range
140 double norm_pos = position - (long)position;
141 if (norm_pos < 0) {
142 norm_pos += 1.0;
143 }
144
145 // Scale position to color range
146 double color_pos = norm_pos * (RAINBOW_COLOR_COUNT - 1);
147 int color_idx = (int)color_pos;
148 double blend = color_pos - color_idx;
149
150 // Wrap around at the end
151 if (color_idx >= RAINBOW_COLOR_COUNT - 1) {
152 color_idx = RAINBOW_COLOR_COUNT - 1;
153 blend = 0;
154 }
155
156 int next_idx = (color_idx + 1) % RAINBOW_COLOR_COUNT;
157
158 return interpolate_color(g_rainbow_colors[color_idx], g_rainbow_colors[next_idx], blend);
159}
160
161// ============================================================================
162// Header Rendering (callback for terminal_screen)
163// ============================================================================
164
168typedef struct {
169 int frame; // Current animation frame number
170 bool use_colors; // Whether to use rainbow colors
171 char update_notification[1024]; // Update notification message (empty if no update)
173
179static void build_connection_target(char *buffer, size_t buffer_size) {
180 if (!buffer || buffer_size == 0) {
181 return;
182 }
183 buffer[0] = '\0';
184
185 // Mirror mode - no network connection
186 asciichat_mode_t mode = GET_OPTION(detected_mode);
187 if (mode == MODE_MIRROR) {
188 const char *media_url = GET_OPTION(media_url);
189 const char *media_file = GET_OPTION(media_file);
190
191 if (media_url && media_url[0] != '\0') {
192 snprintf(buffer, buffer_size, "Loading from URL...");
193 } else if (media_file && media_file[0] != '\0') {
194 snprintf(buffer, buffer_size, "Loading from file...");
195 } else {
196 snprintf(buffer, buffer_size, "Initializing...");
197 }
198 return;
199 }
200
201 // Check if we have a session string (discovery mode)
202 const char *session = GET_OPTION(session_string);
203 if (session && session[0] != '\0') {
204 snprintf(buffer, buffer_size, "Connecting to session: %s", session);
205 return;
206 }
207
208 // Check if we have an address (client mode)
209 const char *addr = GET_OPTION(address);
210
211 if (addr && addr[0] != '\0') {
212 // Use IP utility to classify the connection type
213 const char *ip_type = get_ip_type_string(addr);
214
215 // NOLINTNEXTLINE(bugprone-branch-clone)
216 if (strcmp(ip_type, "Localhost") == 0) {
217 snprintf(buffer, buffer_size, "Connecting to localhost...");
218 } else if (strcmp(ip_type, "LAN") == 0) {
219 snprintf(buffer, buffer_size, "Connecting to %s (LAN)", addr);
220 } else if (strcmp(ip_type, "Internet") == 0) {
221 snprintf(buffer, buffer_size, "Connecting to %s (Internet)", addr);
222 } else if (strcmp(addr, "localhost") == 0) {
223 // Hostname "localhost" not detected by IP util
224 snprintf(buffer, buffer_size, "Connecting to localhost...");
225 } else {
226 // Unknown or hostname
227 snprintf(buffer, buffer_size, "Connecting to %s", addr);
228 }
229 return;
230 }
231
232 // Fallback if no connection info available
233 snprintf(buffer, buffer_size, "Initializing...");
234}
235
250static void render_splash_header(frame_buffer_t *buf, terminal_size_t term_size, void *user_data) {
251 const splash_header_ctx_t *ctx = (const splash_header_ctx_t *)user_data;
252 if (!ctx) {
253 return;
254 }
255
256 // ASCII logo lines (same as help output)
257 const char *ascii_logo[4] = {
258 " __ _ ___ ___(_|_) ___| |__ __ _| |_ ", " / _` / __|/ __| | |_____ / __| '_ \\ / _` | __| ",
259 "| (_| \\__ \\ (__| | |_____| (__| | | | (_| | |_ ", " \\__,_|___/\\___|_|_| \\___|_| |_|\\__,_|\\__| "};
260 const char *tagline = "Video chat in your terminal";
261 const int logo_width = 52;
262
263 // Line 1: Top border
264 frame_buffer_render_border(buf, term_size.cols, "\033[1;36m");
265
266 // Lines 2-5: ASCII logo (centered, truncated if too long)
267 for (int logo_line = 0; logo_line < 4; logo_line++) {
268 // Build plain text line first (for width calculation)
269 char plain_line[512];
270 int horiz_pad = (term_size.cols - logo_width) / 2;
271 if (horiz_pad < 0) {
272 horiz_pad = 0;
273 }
274
275 int pos = 0;
276 for (int j = 0; j < horiz_pad && pos < (int)sizeof(plain_line) - 1; j++) {
277 plain_line[pos++] = ' ';
278 }
279 snprintf(plain_line + pos, sizeof(plain_line) - pos, "%s", ascii_logo[logo_line]);
280
281 // Check visible width and truncate if needed
282 int visible_width = display_width(plain_line);
283 if (visible_width < 0) {
284 visible_width = (int)strlen(plain_line);
285 }
286 if (term_size.cols > 0 && visible_width >= term_size.cols) {
287 plain_line[term_size.cols - 1] = '\0';
288 }
289
290 // Print with rainbow colors
291 int char_idx = 0;
292 for (int i = 0; plain_line[i] != '\0'; i++) {
293 char ch = plain_line[i];
294 if (ch == ' ') {
295 frame_buffer_printf(buf, " ");
296 } else if (ctx->use_colors) {
297 double char_pos = (char_idx + ctx->frame / 5.0) / 30.0;
298 rgb_pixel_t color = get_rainbow_color_rgb(char_pos);
299 frame_buffer_printf(buf, "\x1b[38;2;%u;%u;%um%c\x1b[0m", color.r, color.g, color.b, ch);
300 char_idx++;
301 } else {
302 frame_buffer_printf(buf, "%c", ch);
303 char_idx++;
304 }
305 }
306 frame_buffer_printf(buf, "\n");
307 }
308
309 // Line 6: Tagline (centered, truncated if too long)
310 char plain_tagline[512];
311 int tagline_len = (int)strlen(tagline);
312 int tagline_pad = (term_size.cols - tagline_len) / 2;
313 if (tagline_pad < 0) {
314 tagline_pad = 0;
315 }
316
317 int tpos = 0;
318 for (int j = 0; j < tagline_pad && tpos < (int)sizeof(plain_tagline) - 1; j++) {
319 plain_tagline[tpos++] = ' ';
320 }
321 snprintf(plain_tagline + tpos, sizeof(plain_tagline) - tpos, "%s", tagline);
322
323 // Check visible width and truncate if needed
324 int tagline_visible_width = display_width(plain_tagline);
325 if (tagline_visible_width < 0) {
326 tagline_visible_width = (int)strlen(plain_tagline);
327 }
328 if (term_size.cols > 0 && tagline_visible_width >= term_size.cols) {
329 plain_tagline[term_size.cols - 1] = '\0';
330 }
331
332 frame_buffer_printf(buf, "%s\n", plain_tagline);
333
334 // Line 7: Update notification (if available)
335 if (ctx->update_notification[0] != '\0') {
336 char plain_update[1024];
337 int update_len = (int)strlen(ctx->update_notification);
338 int update_pad = (term_size.cols - update_len) / 2;
339 if (update_pad < 0) {
340 update_pad = 0;
341 }
342
343 int upos = 0;
344 for (int j = 0; j < update_pad && upos < (int)sizeof(plain_update) - 1; j++) {
345 plain_update[upos++] = ' ';
346 }
347 snprintf(plain_update + upos, sizeof(plain_update) - upos, "%s", ctx->update_notification);
348
349 // Check visible width and truncate if needed
350 int update_visible_width = display_width(plain_update);
351 if (update_visible_width < 0) {
352 update_visible_width = (int)strlen(plain_update);
353 }
354 if (term_size.cols > 0 && update_visible_width >= term_size.cols) {
355 plain_update[term_size.cols - 1] = '\0';
356 }
357
358 // Print in yellow/warning color
359 frame_buffer_printf(buf, "%s\n", colored_string(LOG_COLOR_WARN, plain_update));
360 }
361
362 // Line 8 (or 7 if no update): Connection target (centered, truncated if too long)
363 char connection_target[512];
364 build_connection_target(connection_target, sizeof(connection_target));
365
366 char plain_connection[512];
367 int connection_len = (int)strlen(connection_target);
368 int connection_pad = (term_size.cols - connection_len) / 2;
369 if (connection_pad < 0) {
370 connection_pad = 0;
371 }
372
373 int cpos = 0;
374 for (int j = 0; j < connection_pad && cpos < (int)sizeof(plain_connection) - 1; j++) {
375 plain_connection[cpos++] = ' ';
376 }
377 snprintf(plain_connection + cpos, sizeof(plain_connection) - cpos, "%s", connection_target);
378
379 // Check visible width and truncate if needed
380 int connection_visible_width = display_width(plain_connection);
381 if (connection_visible_width < 0) {
382 connection_visible_width = (int)strlen(plain_connection);
383 }
384 if (term_size.cols > 0 && connection_visible_width >= term_size.cols) {
385 plain_connection[term_size.cols - 1] = '\0';
386 }
387
388 frame_buffer_printf(buf, "%s\n", plain_connection);
389
390 // Line 9 (or 8 if no update): Bottom border
391 frame_buffer_render_border(buf, term_size.cols, "\033[1;36m");
392}
393
394// ============================================================================
395// Public API
396// ============================================================================
397
398bool splash_should_display(bool is_intro) {
399 // Check option flags (display splash if enabled, regardless of TTY for testing)
400 if (is_intro) {
401 // Allow splash in snapshot mode if loading from URL/file (has loading time)
402 // Skip splash only for quick webcam snapshots (unless explicitly enabled)
403 bool splash_screen_opt = GET_OPTION(splash_screen);
404 bool splash_explicitly_set = GET_OPTION(splash_screen_explicitly_set);
405 bool is_snapshot = GET_OPTION(snapshot_mode);
406 bool has_media = (GET_OPTION(media_url) && strlen(GET_OPTION(media_url)) > 0) ||
407 (GET_OPTION(media_file) && strlen(GET_OPTION(media_file)) > 0);
408
409 // Show splash if enabled, but in snapshot mode only if:
410 // 1. Has media file/URL, OR
411 // 2. Explicitly enabled with --splash-screen=true
412 bool should_display = splash_screen_opt && (!is_snapshot || has_media || splash_explicitly_set);
413 // Show splash if:
414 // 1. Not in snapshot mode, OR
415 // 2. In snapshot mode but loading from URL/file (needs splash during load)
416 return should_display;
417 } else {
418 return GET_OPTION(status_screen);
419 }
420}
421
426static void *splash_animation_thread(void *arg) {
427 (void)arg;
428
429 // Check if colors should be used (TTY check)
430 bool use_colors = terminal_should_color_output(STDOUT_FILENO);
431
432 // Keyboard is pre-initialized from asciichat_shared_init()
433 // Only enable if terminal is interactive (keyboard won't work in non-TTY)
434 bool keyboard_enabled = terminal_is_interactive();
435
436 // Animate with rainbow wave effect - TIME-BASED, not frame-based
437 // This ensures animation speed is consistent regardless of FPS
438 int fps = GET_OPTION(fps);
439 if (fps <= 0)
440 fps = 60; // Default to 60 FPS if not specified
441 const int anim_speed = 1000 / fps; // milliseconds per frame
442 uint64_t loop_start_ns = time_get_ns();
443 int iteration_count = 0; // Just for logging actual FPS
444
445 log_dev("[SPLASH_ANIM_INIT] fps=%d anim_speed=%dms", fps, anim_speed);
446
447 // Don't log until after first frame to avoid startup flicker
448 bool first_frame = true;
449
450 while (!atomic_load_bool(&g_splash_state.should_stop) && !shutdown_is_requested()) {
451 // Calculate animation frame based on ELAPSED TIME, not iteration count
452 // This ensures the animation plays at the same speed regardless of FPS variation
453 uint64_t now_ns = time_get_ns();
454 uint64_t elapsed_ns = now_ns - loop_start_ns;
455 uint64_t elapsed_ms = elapsed_ns / 1000000;
456
457 // Check if we should stop the splash (from splash_intro_done signal)
458 uint64_t intro_done_ns = atomic_load_u64(&g_splash_state.intro_done_time_ns);
459 if (intro_done_ns > 0) {
460 // splash_intro_done() was called - keep splash running for a minimum display time
461 // The first ASCII frame will be rendered after the splash exits
462 const uint64_t min_display_ns = 2000000000ULL; // 2 seconds in nanoseconds
463 const uint64_t max_display_ns = 30000000000ULL; // 30 seconds maximum (safety limit)
464 uint64_t elapsed_since_start_ns = now_ns - g_splash_state.start_time_ns;
465
466 // Stop when: minimum time reached OR maximum time reached
467 // We don't wait for has_first_frame because the first frame is rendered
468 // by the main thread after splash_wait_for_animation() returns
469 bool min_display_reached = elapsed_since_start_ns >= min_display_ns;
470 bool max_time_reached = elapsed_since_start_ns >= max_display_ns;
471
472 if (min_display_reached || max_time_reached) {
473 atomic_store_bool(&g_splash_state.should_stop, true);
474 }
475 }
476
477 // Convert elapsed time to animation frame (at target FPS)
478 // For 60 FPS target: frame = elapsed_ms / 16.67
479 int frame = (int)(elapsed_ms * fps / 1000);
480
481 if (!first_frame) {
482 log_dev("[SPLASH_ANIM] Iter %d: elapsed=%llums frame=%d should_stop=%d", iteration_count,
483 (unsigned long long)elapsed_ms, frame, atomic_load_bool(&g_splash_state.should_stop));
484 }
485
486 // Poll keyboard for interactive grep and Escape to cancel
487 if (!first_frame) {
488 log_dev("[SPLASH_ANIM] Iter %d: keyboard_enabled=%d", iteration_count, keyboard_enabled);
489 }
490 if (keyboard_enabled) {
492 if (key == KEY_ESCAPE) {
493 // Escape key: cancel grep if active, otherwise cancel splash
494 if (log_search_is_active()) {
495 log_search_exit_mode(false); // Cancel grep without applying
496 } else {
497 atomic_store_bool(&g_splash_state.should_stop, true); // Exit splash screen
498 }
499 } else if (key != KEY_NONE && log_search_should_handle(key)) {
501 // Continue to render immediately with grep active
502 }
503 }
504 if (!first_frame) {
505 log_dev("[SPLASH_ANIM] Iter %d: keyboard check done", iteration_count);
506 }
507
508 // Set up splash header context for this frame (using TIME-BASED frame value)
509 splash_header_ctx_t header_ctx = {
510 .frame = frame,
511 .use_colors = use_colors,
512 };
513
514 if (!first_frame) {
515 log_dev("[SPLASH_ANIM] Frame %d: Setting up header context", frame);
516 }
517
518 // Initialize update notification lifecycle and mutex once (safe to call multiple times)
519 if (lifecycle_init_once(&g_update_notification_lifecycle)) {
520 if (!first_frame) {
521 log_dev("[SPLASH_ANIM] Initializing lifecycle for update_notification");
522 }
523 // Initialize the mutex (already configured in the lifecycle structure)
524 mutex_init(&g_update_notification_mutex, "update_notification");
525 lifecycle_init_commit(&g_update_notification_lifecycle);
526 }
527
528 // Copy update notification from global state (thread-safe)
529 if (!first_frame) {
530 log_dev("[SPLASH_ANIM] Frame %d: About to lock update_notification_mutex", frame);
531 }
532 mutex_lock(&g_update_notification_mutex);
533 if (!first_frame) {
534 log_dev("[SPLASH_ANIM] Frame %d: Locked update_notification_mutex", frame);
535 }
536 SAFE_STRNCPY(header_ctx.update_notification, g_update_notification, sizeof(header_ctx.update_notification));
537 mutex_unlock(&g_update_notification_mutex);
538 if (!first_frame) {
539 log_dev("[SPLASH_ANIM] Frame %d: Unlocked update_notification_mutex", frame);
540 }
541
542 // Calculate header lines: 8 base lines + 1 if update notification present
543 int header_lines = 8;
544 if (header_ctx.update_notification[0] != '\0') {
545 header_lines = 9;
546 }
547
548 // Configure terminal screen with splash header callback
549 terminal_screen_config_t screen_config = {
550 .fixed_header_lines = header_lines,
551 .render_header = render_splash_header,
552 .user_data = &header_ctx,
553 .show_logs = true, // Show live log feed below splash
554 };
555
556 // Render the screen (header + logs) only in interactive mode
557 // OR if splash screen was explicitly requested
558 // In non-interactive mode without explicit flag, logs flow to stdout/stderr normally
559 const options_t *opts_render = options_get();
560 bool should_render = terminal_is_interactive() || (opts_render && opts_render->splash_screen_explicitly_set);
561
562 // Log startup phase timing
563 if (elapsed_ms < 500) {
564 log_dev("[STARTUP_PHASE] elapsed_ms=%llu frame=%d (time-based) header_lines=%d should_render=%d",
565 (unsigned long long)elapsed_ms, frame, header_lines, should_render);
566 }
567
568 if (should_render) {
569 terminal_screen_render(&screen_config);
570 if (!first_frame) {
571 log_dev("[SPLASH_ANIM] Iter %d: terminal_screen_render() completed", iteration_count);
572 }
573 first_frame = false; // Mark first frame as complete
574 } else {
575 if (!first_frame) {
576 log_dev("[SPLASH_ANIM] Iter %d: Skipping render (should_render=false)", iteration_count);
577 }
578 }
579
580 iteration_count++;
581
582 // Log progress every 1 second of actual time (not frame count)
583 if (elapsed_ms > 0 && (int)(elapsed_ms % 1000) < anim_speed) {
584 uint64_t now_ns = time_get_ns();
585 uint64_t total_elapsed_ns = now_ns - loop_start_ns;
586 double elapsed_sec = total_elapsed_ns / 1e9;
587 double actual_fps = iteration_count / elapsed_sec;
588 log_info("[SPLASH_ANIM] %llu ms elapsed, iteration %d, actual FPS: %.1f", (unsigned long long)elapsed_ms,
589 iteration_count, actual_fps);
590 }
591
592 // Sleep to control frame rate (unless grep needs immediate rerender)
594 uint64_t sleep_start_ns = time_get_ns();
595 platform_sleep_ms(anim_speed);
596 uint64_t sleep_end_ns = time_get_ns();
597 uint64_t actual_sleep_ms = (sleep_end_ns - sleep_start_ns) / 1000000;
598 if (iteration_count % 60 == 0) {
599 log_debug("[SPLASH_SLEEP] Iter %d: requested=%dms actual=%llums", iteration_count, anim_speed,
600 (unsigned long long)actual_sleep_ms);
601 }
602 } else {
603 if (iteration_count % 60 == 0) {
604 log_debug("[SPLASH_GREP_RERENDER] Iter %d: skipping sleep for grep rerender", iteration_count);
605 }
606 }
607 }
608
609 // Log final iteration count
610 uint64_t final_ns = time_get_ns();
611 uint64_t total_elapsed_ns = final_ns - loop_start_ns;
612 double total_elapsed_sec = total_elapsed_ns / 1e9;
613 double final_fps = total_elapsed_sec > 0 ? iteration_count / total_elapsed_sec : 0;
614 log_info(
615 "[SPLASH_ANIM] === ANIMATION LOOP EXITED === %d iterations in %.3f seconds (%.1f actual FPS). should_stop=%d, "
616 "shutdown=%d",
617 iteration_count, total_elapsed_sec, final_fps, atomic_load_bool(&g_splash_state.should_stop),
619
620 // If shutdown was requested, clear the screen immediately to prevent splash from
621 // appearing briefly during exit
622 if (shutdown_is_requested()) {
623 log_dev("[SPLASH_ANIM] Shutdown detected, clearing screen before exit");
625 terminal_cursor_home(STDOUT_FILENO);
626 terminal_flush(STDOUT_FILENO);
627 }
628
629 // NOTE: Do NOT call keyboard_destroy() here!
630 // Keyboard was initialized by asciichat_shared_init() and needs to persist
631 // for the render loop. The splash thread is allowed to READ keyboard input,
632 // but should not destroy it. It will be destroyed by the render loop cleanup.
633
634 atomic_store_bool(&g_splash_state.is_running, false);
635 log_dev("[SPLASH_ANIM] Animation thread exiting");
636 return NULL;
637}
638
640 (void)ctx; // Not needed - display context is not actively used
641
642 // Check if splash should display
643 if (!splash_should_display(true)) {
644 return 0;
645 }
646
647 // Check terminal size
648 int width = (int)terminal_get_effective_width();
649 int height = (int)terminal_get_effective_height();
650 if (width < 50 || height < 20) {
651 return 0;
652 }
653
654 // Redirect splash screen to stderr when stdout is piped (not a TTY).
655 // This keeps stdout clean for frame data while splash goes to stderr.
656 // Uses platform_isatty() so non-desktop platforms (iOS, WASM) can override.
657 if (!platform_isatty(STDOUT_FILENO)) {
658 terminal_screen_set_output_fd(STDERR_FILENO);
659 }
660
661 // Initialize log buffer (same pattern as server_status)
663
664 // Clear screen and show cursor
666 (void)terminal_cursor_show();
667
668 // Clear log buffer for clean slate
670
671 // Set running flag
672 atomic_store_bool(&g_splash_state.is_running, true);
673 atomic_store_bool(&g_splash_state.should_stop, false);
674 g_splash_state.frame = 0;
675 g_splash_state.start_time_ns = time_get_ns();
676
677 // Start animation thread (only create once)
678 bool expected_false = false;
679 if (!atomic_cas_bool(&g_splash_state.thread_created, &expected_false, true)) {
680 return 0;
681 }
682
683 int err = asciichat_thread_create(&g_splash_state.anim_thread, "splash_anim", splash_animation_thread, NULL);
684 if (err != ASCIICHAT_OK) {
685 log_warn("Failed to create splash animation thread: error=%d", err);
686 atomic_store_bool(&g_splash_state.thread_created, false);
687 return 0;
688 }
689
690 return 0; // ASCIICHAT_OK
691}
692
694 // Record when intro_done was called - animation thread will use this to decide when to stop
695 // Animation thread needs time to check the flag and perform cleanup, so this must happen
696 // BEFORE we try to access the terminal for frame rendering
697 atomic_store_u64(&g_splash_state.intro_done_time_ns, time_get_ns());
698
699 // NOTE: Do NOT modify thread_created flag here!
700 // splash_wait_for_animation() needs to see thread_created==true to know whether to join the thread.
701 // If we set thread_created=false here, splash_wait_for_animation() won't join, and the render loop
702 // will start while the animation thread is still running, causing frame output to conflict with splash.
703 //
704 // Instead, just signal the animation thread to stop via intro_done_time_ns, and let
705 // splash_wait_for_animation() handle the actual thread join.
706
707 // NOTE: DO NOT set is_running = false here! The animation thread sets it to false
708 // when it actually exits (line 633). If we set it to false here, the render loop
709 // guard will think splash is done while the animation thread is still running for
710 // up to 2 more seconds, allowing ASCII frames to render simultaneously with splash.
711 // The is_running flag MUST only be cleared by the animation thread when it finishes.
712
713 return 0;
714}
715
717 // No-op: stderr suppression is no longer used
718 // Logging works naturally through session_log_buffer
719}
720
722 // No-op: display context is no longer cached
723}
724
726 // Check if splash animation is currently rendering
727 // Used to prevent ASCII art from rendering while splash is animating
728 return atomic_load_bool(&g_splash_state.is_running);
729}
730
732 // Wait for animation thread to fully exit before rendering ASCII art
733 // This prevents the splash and ASCII art from appearing simultaneously
734 //
735 // The animation thread will exit gracefully when:
736 // - Intro done signal received AND 2 seconds have elapsed, OR
737 // - 30 seconds have elapsed (safety timeout)
738 // - OR a shutdown signal is received
739 //
740 // We block here indefinitely to ensure the splash animation completes before ASCII art starts
741
742 // Only join if we successfully created the thread
743 if (atomic_load_bool(&g_splash_state.thread_created)) {
744 log_dev("[SPLASH_WAIT] Waiting for animation thread to exit...");
745
746 // Check if shutdown was requested - handle it specially
747 bool is_shutting_down = shutdown_is_requested();
748 if (is_shutting_down) {
749 log_dev("[SPLASH_WAIT] Shutdown requested, signaling animation thread to stop");
750 atomic_store_bool(&g_splash_state.should_stop, true);
751 // During shutdown, use a short timeout (100ms)
752 uint64_t timeout_ns = 100LL * NS_PER_MS_INT;
753 (void)asciichat_thread_join_timeout(&g_splash_state.anim_thread, NULL, timeout_ns);
754 } else {
755 // Normal operation: wait indefinitely for the animation thread to finish
756 // This ensures splash animation is 100% done before ASCII art rendering starts
757 asciichat_error_t err = asciichat_thread_join(&g_splash_state.anim_thread, NULL);
758 if (err == ASCIICHAT_OK) {
759 log_dev("[SPLASH_WAIT] Animation thread exited cleanly");
760 } else {
761 log_warn("[SPLASH_WAIT] Animation thread join failed: %s", asciichat_error_string(err));
762 // Force stop the animation thread if join failed
763 atomic_store_bool(&g_splash_state.should_stop, true);
764 // Clear any partial splash output left on screen
766 terminal_cursor_home(STDOUT_FILENO);
767 terminal_flush(STDOUT_FILENO);
768 }
769 }
770
771 // Mark that we've joined (safe to call multiple times - only joins once)
772 atomic_store_bool(&g_splash_state.thread_created, false);
773 }
774}
775
776void splash_set_update_notification(const char *notification) {
777 if (lifecycle_init_once(&g_update_notification_lifecycle)) {
778 mutex_init(&g_update_notification_mutex, "update_notification");
779 lifecycle_init_commit(&g_update_notification_lifecycle);
780 }
781 mutex_lock(&g_update_notification_mutex);
782
783 if (!notification || notification[0] == '\0') {
784 g_update_notification[0] = '\0';
785 log_debug("Cleared update notification for splash/status screens");
786 } else {
787 SAFE_STRNCPY(g_update_notification, notification, sizeof(g_update_notification));
788 log_debug("Set update notification for splash/status screens: %s", notification);
789 }
790
791 mutex_unlock(&g_update_notification_mutex);
792}
Platform abstraction layer umbrella header providing unified cross-platform API.
ANSI escape sequence utilities and fast color code generation.
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
bool atomic_cas_bool(atomic_t *a, bool *expected, bool new_value)
Atomically compare-and-swap a boolean.
Definition atomic.c:184
⚛️ Atomic operations abstraction layer with debug tracking
⚙️ Common definitions, error codes, macros, and types shared throughout the application
void frame_buffer_printf(frame_buffer_t *buf, const char *fmt,...)
Printf-style append to the buffer.
void frame_buffer_render_border(frame_buffer_t *buf, int cols, const char *color)
Render a horizontal border line (used by both splash and status screens)
Buffered terminal frame rendering.
int buffer_size
Size of circular buffer.
Definition grep.c:90
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ASCIICHAT_OK
Definition error_codes.h:51
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
void log_clear_session_log_buffer(void)
Unregister the session log buffer.
Definition log/log.c:1847
void log_set_session_log_buffer(session_log_buffer_t *buf)
Register a session log buffer with the logger.
Definition log/log.c:1838
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
@ LOG_COLOR_WARN
Definition log/log.h:134
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
#define NS_PER_MS_INT
Definition time.h:156
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
asciichat_mode_t
Mode type for options parsing.
const options_t * options_get(void)
Get current options (lock-free read)
Definition rcu.c:496
@ MODE_MIRROR
Mirror mode - local webcam viewing (no network)
asciichat_error_t terminal_cursor_home(int fd)
Move cursor to home position (top-left)
int asciichat_thread_join_timeout(asciichat_thread_t *thread, void **retval, uint64_t timeout_ns)
Wait for a thread to complete with timeout.
keyboard_key_t keyboard_read_nonblocking(void)
Read next keyboard input without blocking.
asciichat_error_t terminal_clear_screen(void)
Clear the terminal screen.
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
asciichat_error_t terminal_flush(int fd)
Flush terminal output.
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
keyboard_key_t
Unified keyboard key code enumeration.
Definition keyboard.h:54
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
asciichat_error_t terminal_cursor_show(void)
Show terminal cursor.
@ KEY_ESCAPE
Escape key (ESC)
Definition keyboard.h:56
@ KEY_NONE
No key pressed or no input available.
Definition keyboard.h:55
bool splash_is_running(void)
Check if splash screen animation is currently running.
Definition splash.c:725
int splash_intro_done(void)
Signal that intro splash should end (first frame ready to render)
Definition splash.c:693
void splash_restore_stderr(void)
Restore stderr after splash animation completes.
Definition splash.c:716
int splash_intro_start(session_display_ctx_t *ctx)
Start animated intro screen with rainbow ASCII art (non-blocking)
Definition splash.c:639
void splash_log_init(void)
Initialize log buffer for splash animation.
Definition splash.c:76
void splash_log_clear(void)
Clear splash log buffer.
Definition splash.c:88
void splash_set_update_notification(const char *notification)
Set update notification to display on splash/status screens.
Definition splash.c:776
bool splash_should_display(bool is_intro)
Check if splash screen should display.
Definition splash.c:398
void splash_clear_display_context(void)
Clear the cached display context to prevent use-after-free.
Definition splash.c:721
void splash_log_destroy(void)
Destroy splash log buffer.
Definition splash.c:83
void splash_wait_for_animation(void)
Wait for splash animation thread to fully exit.
Definition splash.c:731
bool shutdown_is_requested(void)
Check if shutdown has been requested.
Definition common.c:73
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
const char * get_ip_type_string(const char *ip)
Get human-readable string describing IP address type.
Definition ip.c:1091
int display_width(const char *text)
Calculate the visible display width of text with ANSI escape codes.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
Platform initialization and static synchronization helpers.
Thread-safe circular log buffer for session screens (status, splash)
🖥️ Display Width and Terminal Centering Utilities
🌍 IP Address Parsing and Formatting Utilities
🎮 Cross-platform keyboard input interface for ascii-chat
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
void * asciichat_thread_t
#define asciichat_thread_join(thread, timeout_ms)
void lifecycle_init_commit(lifecycle_t *lc)
Definition lifecycle.c:90
bool lifecycle_init_once(lifecycle_t *lc)
Definition lifecycle.c:52
Lock-free module lifecycle state machine using pure stdatomic.
#define LIFECYCLE_INIT_MUTEX(m)
Static initializer for lifecycle with mutex.
Definition lifecycle.h:74
📝 Logging API with multiple log levels and terminal output control
unsigned short int terminal_get_effective_width(void)
Get effective terminal width with fallback priority.
bool terminal_is_interactive(void)
Check if the session is fully interactive.
unsigned short int terminal_get_effective_height(void)
Get effective terminal height with fallback priority.
bool terminal_should_color_output(int fd)
Determine if color output should be used.
bool log_search_needs_rerender(void)
Check if screen needs immediate re-render.
Definition search.c:884
bool log_search_is_active(void)
Check if filtering is active.
Definition search.c:390
void log_search_exit_mode(bool accept)
Exit search mode.
Definition search.c:287
bool log_search_should_handle(int key)
Check if a key should be handled by grep module.
Definition search.c:399
asciichat_error_t log_search_handle_key(keyboard_key_t key)
Process keyboard input for grep.
Definition search.c:414
Interactive search filtering for terminal screens.
atomic_t intro_done_time_ns
Definition splash.c:102
atomic_t thread_created
Definition splash.c:98
uint64_t start_time_ns
Definition splash.c:101
atomic_t should_stop
Definition splash.c:97
#define RAINBOW_COLOR_COUNT
Definition splash.c:70
atomic_t is_running
Definition splash.c:96
asciichat_thread_t anim_thread
Definition splash.c:100
int frame
Definition splash.c:99
Intro and status screen display for ascii-chat with animated rainbow effects.
🖥️ Unified terminal display abstraction for session-based rendering
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
Frame buffer structure.
Mutex type (POSIX: pthread_mutex_t with debug tracking)
Consolidated options structure.
bool splash_screen_explicitly_set
True if splash screen was explicitly set by user.
RGB pixel structure.
uint8_t b
Blue color component (0-255)
uint8_t g
Green color component (0-255)
uint8_t r
Red color component (0-255)
Internal session display context structure.
Internal circular buffer structure.
Context data for splash header rendering.
Definition splash.c:168
char update_notification[1024]
Definition splash.c:171
Configuration for terminal screen rendering.
int fixed_header_lines
How many lines the header takes (e.g., 4 for status, 8 for splash)
Terminal size structure.
Definition terminal.h:163
int cols
Number of columns (width) in terminal.
Definition terminal.h:165
🔒 Synchronization primitive debugging (mutexes, rwlocks, condition variables)
Cross-platform system functions interface for ascii-chat.
🖥️ Cross-platform terminal interface for ascii-chat
void terminal_screen_log_destroy(void)
Standard log cleanup for terminal screens.
void terminal_screen_render(const terminal_screen_config_t *config)
Render a terminal screen with fixed header and scrolling logs.
void terminal_screen_log_clear(void)
Clear buffered logs for terminal screens.
session_log_buffer_t * terminal_screen_log_init(void)
Initialize session log buffer for terminal screens.
void terminal_screen_set_output_fd(int fd)
Set output file descriptor for terminal screens (splash/status)
Reusable "fixed header + scrolling logs" terminal screen abstraction.
⏱️ High-precision timing utilities using sokol_time.h and uthash
🔤 String Manipulation and Shell Escaping Utilities
int platform_isatty(int fd)
Check if a file descriptor is a terminal.
Definition util.c:63