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>
46#define ASCII_LOGO_LINES 7
47#define ASCII_LOGO_WIDTH 36
50static char g_update_notification[1024] = {0};
51static mutex_t g_update_notification_mutex;
53static const rgb_pixel_t g_rainbow_colors[] = {
62#define RAINBOW_COLOR_COUNT 7
68 _Atomic(
bool) is_running;
69 _Atomic(
bool) should_stop;
71 asciichat_thread_t anim_thread;
72} g_splash_state = {.is_running =
false, .should_stop =
false, .frame = 0};
89static rgb_pixel_t interpolate_color(rgb_pixel_t color1, rgb_pixel_t color2,
double t) {
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);
102static rgb_pixel_t get_rainbow_color_rgb(
double position) {
104 double norm_pos = position - (long)position;
111 int color_idx = (int)color_pos;
112 double blend = color_pos - color_idx;
122 return interpolate_color(g_rainbow_colors[color_idx], g_rainbow_colors[next_idx], blend);
135 char update_notification[1024];
143static void build_connection_target(
char *buffer,
size_t buffer_size) {
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);
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...");
166 const char *session = GET_OPTION(session_string);
167 if (session && session[0] !=
'\0') {
168 snprintf(buffer,
buffer_size,
"Connecting to session: %s", session);
173 const char *addr = GET_OPTION(address);
175 if (addr && addr[0] !=
'\0') {
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) {
187 snprintf(buffer,
buffer_size,
"Connecting to localhost...");
190 snprintf(buffer,
buffer_size,
"Connecting to %s", addr);
212static void render_splash_header(terminal_size_t term_size,
void *user_data) {
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;
227 double offset = ctx->
frame * rainbow_speed;
230 printf(
"\033[1;36m━");
231 for (
int i = 1; i < term_size.cols - 1; i++) {
237 for (
int logo_line = 0; logo_line < 4; logo_line++) {
239 char plain_line[512];
240 int horiz_pad = (term_size.cols - logo_width) / 2;
246 for (
int j = 0; j < horiz_pad && pos < (int)
sizeof(plain_line) - 1; j++) {
247 plain_line[pos++] =
' ';
249 snprintf(plain_line + pos,
sizeof(plain_line) - pos,
"%s", ascii_logo[logo_line]);
253 if (visible_width < 0) {
254 visible_width = (int)strlen(plain_line);
256 if (term_size.cols > 0 && visible_width >= term_size.cols) {
257 plain_line[term_size.cols - 1] =
'\0';
262 for (
int i = 0; plain_line[i] !=
'\0'; i++) {
263 char ch = plain_line[i];
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);
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) {
288 for (
int j = 0; j < tagline_pad && tpos < (int)
sizeof(plain_tagline) - 1; j++) {
289 plain_tagline[tpos++] =
' ';
291 snprintf(plain_tagline + tpos,
sizeof(plain_tagline) - tpos,
"%s", tagline);
295 if (tagline_visible_width < 0) {
296 tagline_visible_width = (int)strlen(plain_tagline);
298 if (term_size.cols > 0 && tagline_visible_width >= term_size.cols) {
299 plain_tagline[term_size.cols - 1] =
'\0';
302 printf(
"%s\n", plain_tagline);
306 char plain_update[1024];
308 int update_pad = (term_size.cols - update_len) / 2;
309 if (update_pad < 0) {
314 for (
int j = 0; j < update_pad && upos < (int)
sizeof(plain_update) - 1; j++) {
315 plain_update[upos++] =
' ';
317 snprintf(plain_update + upos,
sizeof(plain_update) - upos,
"%s", ctx->
update_notification);
321 if (update_visible_width < 0) {
322 update_visible_width = (int)strlen(plain_update);
324 if (term_size.cols > 0 && update_visible_width >= term_size.cols) {
325 plain_update[term_size.cols - 1] =
'\0';
333 char connection_target[512];
334 build_connection_target(connection_target,
sizeof(connection_target));
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) {
344 for (
int j = 0; j < connection_pad && cpos < (int)
sizeof(plain_connection) - 1; j++) {
345 plain_connection[cpos++] =
' ';
347 snprintf(plain_connection + cpos,
sizeof(plain_connection) - cpos,
"%s", connection_target);
350 int connection_visible_width =
display_width(plain_connection);
351 if (connection_visible_width < 0) {
352 connection_visible_width = (int)strlen(plain_connection);
354 if (term_size.cols > 0 && connection_visible_width >= term_size.cols) {
355 plain_connection[term_size.cols - 1] =
'\0';
358 printf(
"%s\n", plain_connection);
361 printf(
"\033[1;36m━");
362 for (
int i = 1; i < term_size.cols - 1; i++) {
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);
384 return GET_OPTION(splash) && (!is_snapshot || has_media);
386 return GET_OPTION(status_screen);
394static void *splash_animation_thread(
void *arg) {
401 bool keyboard_enabled =
false;
403 if (keyboard_init() == ASCIICHAT_OK) {
404 keyboard_enabled =
true;
410 const int anim_speed = 100;
414 if (keyboard_enabled) {
415 keyboard_key_t key = keyboard_read_nonblocking();
416 if (key == KEY_ESCAPE) {
421 atomic_store(&g_splash_state.should_stop,
true);
431 .use_colors = use_colors,
435 static bool mutex_initialized =
false;
436 if (!mutex_initialized) {
438 mutex_initialized =
true;
440 mutex_lock(&g_update_notification_mutex);
442 mutex_unlock(&g_update_notification_mutex);
445 int header_lines = 8;
451 terminal_screen_config_t screen_config = {
452 .fixed_header_lines = header_lines,
453 .render_header = render_splash_header,
454 .user_data = &header_ctx,
474 if (keyboard_enabled) {
478 atomic_store(&g_splash_state.is_running,
false);
496 int width = GET_OPTION(width);
497 int height = GET_OPTION(height);
498 if (width < 50 || height < 20) {
504 log_warn(
"Failed to initialize splash log buffer");
509 terminal_clear_screen();
513 atomic_store(&g_splash_state.is_running,
true);
514 atomic_store(&g_splash_state.should_stop,
false);
515 g_splash_state.frame = 0;
519 log_warn(
"Failed to create splash animation thread");
528 atomic_store(&g_splash_state.should_stop,
true);
531 if (atomic_load(&g_splash_state.is_running)) {
535 atomic_store(&g_splash_state.is_running,
false);
553 if (mode != 0 && mode != 3) {
554 log_error(
"Status screen only for server/discovery modes");
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");
567 bool has_utf8 = (GET_OPTION(force_utf8) >= 0);
571 terminal_clear_screen();
573 char buffer[4096] = {0};
574 snprintf(buffer,
sizeof(buffer),
"\n");
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),
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) {
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));
592 static bool mutex_initialized_for_status =
false;
593 if (!mutex_initialized_for_status) {
595 mutex_initialized_for_status =
true;
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",
602 mutex_unlock(&g_update_notification_mutex);
604 snprintf(buffer + strlen(buffer),
sizeof(buffer) - strlen(buffer),
"\n");
606 printf(
"%s", buffer);
613 static bool mutex_initialized =
false;
614 if (!mutex_initialized) {
616 mutex_initialized =
true;
619 mutex_lock(&g_update_notification_mutex);
621 if (!notification || notification[0] ==
'\0') {
622 g_update_notification[0] =
'\0';
623 log_debug(
"Cleared update notification for splash/status screens");
625 SAFE_STRNCPY(g_update_notification, notification,
sizeof(g_update_notification));
626 log_debug(
"Set update notification for splash/status screens: %s", notification);
629 mutex_unlock(&g_update_notification_mutex);
bool shutdown_is_requested(void)
int buffer_size
Size of circular buffer.
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)
int display_width(const char *text)
bool session_log_buffer_init(void)
int splash_intro_done(void)
int splash_intro_start(session_display_ctx_t *ctx)
#define RAINBOW_COLOR_COUNT
int splash_display_status(int mode)
void splash_set_update_notification(const char *notification)
bool splash_should_display(bool is_intro)
Internal session display context structure.
void terminal_screen_render(const terminal_screen_config_t *config)
int mutex_init(mutex_t *mutex)
int asciichat_thread_create(asciichat_thread_t *thread, void *(*start_routine)(void *), void *arg)
int asciichat_thread_join(asciichat_thread_t *thread, void **retval)
const char * colored_string(log_color_t color, const char *text)