ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
update_banner.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define TUI_UPDATE_BANNER_CLIPPED_BUFFER_SIZE 1024
4#define TUI_UPDATE_BANNER_HEADING_BUFFER_SIZE 160
5#define TUI_UPDATE_BANNER_INSTALLED_BUFFER_SIZE 160
6#define TUI_UPDATE_BANNER_LATEST_BUFFER_SIZE 160
7#define TUI_UPDATE_BANNER_INSTALLED_VERSION_BUFFER_SIZE 128
8#define TUI_UPDATE_BANNER_COMMAND_BUFFER_SIZE 512
9#define TUI_UPDATE_BANNER_INSTRUCTIONS_BUFFER_SIZE 128
10#define TUI_UPDATE_BANNER_DOMAIN_LINK_BUFFER_SIZE 128
11#define TUI_UPDATE_BANNER_STYLED_INSTRUCTIONS_BUFFER_SIZE 256
12#define TUI_UPDATE_BANNER_INSTRUCTIONS_EXTENDED_BUFFER_SIZE 256
13#define TUI_UPDATE_BANNER_OUTPUT_BUFFER_SIZE 2048
14#define TUI_UPDATE_BANNER_NOTIFICATION_BUFFER_SIZE 1024
15
16#include <ascii-chat/otel.h>
18#include <ascii-chat/ui/popup.h>
29
38#include "session/display.h"
39#include <ascii-chat/common.h>
40#include <libplatform/terminal.h>
42#include <libplatform/system.h>
43#include <libplatform/filesystem.h>
44#include <libplatform/thread.h>
45#include <ascii-chat/log/log.h>
49#include <libplatform/lifecycle.h>
50
51#include <stdio.h>
52#include <stdatomic.h>
53#include <signal.h>
54#include <string.h>
55#include <uv.h>
56
57// Thread-safe storage for the update check result
58static update_check_result_t g_update_result;
59static mutex_t g_update_result_mutex;
60static bool g_update_result_set = false;
61static lifecycle_t g_update_result_lifecycle = LIFECYCLE_INIT_MUTEX(&g_update_result_mutex);
62
63// Background update check thread
64static platform_thread_t g_update_thread;
65static bool g_update_thread_started = false;
66static bool g_update_checking = false;
67static bool g_update_finished = false;
68static _Atomic bool g_update_overlay = false;
69static _Atomic bool g_update_install_failed = false;
70static _Atomic bool g_update_cancel_requested = false;
71static _Atomic int g_update_selected_action = 0;
72static _Atomic uint64_t g_update_visible_until_ns = 0;
73static void update_banner_open_release(void);
74static void update_banner_present_overlay(void);
75
76static bool update_banner_copy_result(update_check_result_t *result) {
77 if (!result || !lifecycle_is_initialized(&g_update_result_lifecycle)) {
78 return false;
79 }
80 mutex_lock(&g_update_result_mutex);
81 bool set = g_update_result_set;
82 if (set) {
83 *result = g_update_result;
84 }
85 mutex_unlock(&g_update_result_mutex);
86 return set;
87}
88
90 if (!result) {
91 return;
92 }
93
94 // Resolve the command before publishing a result to the presentation thread.
97
98 if (lifecycle_init_once(&g_update_result_lifecycle)) {
99 mutex_init(&g_update_result_mutex, "update_banner_result");
100 lifecycle_init_commit(&g_update_result_lifecycle);
101 }
102
103 ascii_otel_ui_usage("update", result->check_succeeded ? "check_completed" : "check_failed", 0);
104 mutex_lock(&g_update_result_mutex);
105 memcpy(&g_update_result, result, sizeof(g_update_result));
106 g_update_result_set = true;
107 mutex_unlock(&g_update_result_mutex);
108}
109
111 update_check_result_t result = {0};
112 return update_banner_copy_result(&result) && result.update_available;
113}
114
115static void update_center_line(frame_buffer_t *out, int row, int left, int width, const char *text, log_color_t color) {
117 truncate_with_ellipsis(text, clipped, sizeof(clipped), width);
118 int padding = (width - display_width(clipped)) / 2;
119 frame_buffer_printf(out, "\033[%d;%dH%s", row, left + padding, colored_string(color, clipped));
120}
121
122static void update_border(frame_buffer_t *out, int row, int left, int width, const char *first, const char *horizontal,
123 const char *last, log_color_t color) {
124 frame_buffer_printf(out, "\033[%d;%dH%s", row, left, colored_string(color, first));
125 int edges = (first[0] ? 1 : 0) + (last[0] ? 1 : 0);
126 const char *stroke = colored_string(color, horizontal);
127 for (int col = 0; col < width - edges; ++col)
128 frame_buffer_append(out, stroke, strlen(stroke));
129 frame_buffer_printf(out, "%s", colored_string(color, last));
130}
131
132static int update_action_count(bool startup, bool checking, bool available) {
133 return startup ? 3 : checking ? 1 : available ? 4 : 2;
134}
135
136static bool update_select_key(int key, int count) {
137 if (key != KEY_LEFT && key != KEY_UP && key != KEY_RIGHT && key != KEY_DOWN && key != '\t')
138 return false;
139 ui_button_t buttons[4] = {{.label = ""}, {.label = ""}, {.label = ""}, {.label = ""}};
140 ui_buttons_t state = {.selected = (size_t)atomic_load(&g_update_selected_action)};
141 (void)ui_buttons_key(&state, buttons, (size_t)count, (keyboard_key_t)key);
142 atomic_store(&g_update_selected_action, (int)state.selected);
144 return true;
145}
146
147static void render_update_dialog(terminal_size_t size, const update_check_result_t *result, bool startup,
148 bool checking) {
149 bool available = !checking && result->update_available;
150 bool unicode = terminal_should_use_utf8(GET_OPTION(force_utf8));
155 const char *horizontal = unicode ? "─" : "-";
156 const char *vertical = unicode ? "│" : "|";
157 const char *labels[4] = {0};
158 int count = update_action_count(startup, checking, available);
159 if (startup) {
160 labels[0] = " Enter Update now ";
161 labels[1] = " O GitHub ";
162 labels[2] = " Esc Continue ";
163 } else if (available) {
164 labels[0] = " Enter Update now ";
165 labels[1] = " O GitHub ";
166 labels[2] = " Esc / F12 Close ";
167 labels[3] = " R Check again ";
168 } else {
169 labels[0] = " Esc / F12 Close ";
170 labels[1] = " R Check again ";
171 }
172 int buttons_width = (count - 1) * 3;
173 for (int i = 0; i < count; ++i)
174 buttons_width += display_width(labels[i]);
175 int width = size.cols - 4;
176 int preferred_width = available ? 78 : checking ? 62 : 58;
177 if (width > preferred_width)
178 width = preferred_width;
179 bool stacked = buttons_width > width - 6;
180 int height = (available ? 18 : checking ? 15 : 13) + (stacked ? count - 1 : 0);
181 bool failed = atomic_load(&g_update_install_failed);
182 if (width < 24 || height > size.rows - 2)
183 return;
184 int left = (size.cols - width) / 2 + 1;
185 int top = (size.rows - height) / 2 + 1;
186 frame_buffer_t *out = frame_buffer_create(size.rows, size.cols);
187 if (!out)
188 return;
189 frame_buffer_printf(out, startup ? "\033[?25l\033[0m\033[2J\033[H" : "\033[?25l\033[0m");
190 ui_popup_frame(out, top, left, width, height, cyan);
191 const char *title = checking ? "Check for updates"
192 : available ? "Update available"
193 : result->check_succeeded ? "You're up to date"
194 : "Update check unavailable";
196 snprintf(heading, sizeof(heading), "\033[1m%s", title);
197 update_center_line(out, top + (checking ? 1 : 2), left + 2, width - 4, heading,
198 checking ? cyan
199 : available ? yellow
200 : result->check_succeeded ? green
201 : yellow);
202 if (checking) {
203 ui_spinner_render(out, top + 3, left + (width - 7) / 2, time_get_ns());
204 update_center_line(out, top + 7, left + 2, width - 4, "Contacting GitHub Releases...", LOG_COLOR_RESET);
205 update_center_line(out, top + 9, left + 2, width - 4, "Video chat in your terminal", muted);
206 } else if (result->check_succeeded) {
209 snprintf(installed_version, sizeof(installed_version), "%s%s", result->current_version[0] == 'v' ? "" : "v",
210 result->current_version);
211 snprintf(installed, sizeof(installed), "Installed %s",
212 colored_string(available ? yellow : green, installed_version));
213 snprintf(latest, sizeof(latest), "Latest %s", colored_string(green, result->latest_version));
214 update_center_line(out, top + 4, left + 2, width - 4, installed, LOG_COLOR_RESET);
215 update_center_line(out, top + 5, left + 2, width - 4, latest, LOG_COLOR_RESET);
216 if (available) {
217 update_center_line(out, top + 6, left + 2, width - 4, "A new version of ascii-chat is available.", muted);
220 sizeof(command));
221 int command_width = display_width(command) + 6;
222 if (command_width > width - 6)
223 command_width = width - 6;
224 int command_left = left + (width - command_width) / 2;
225 update_border(out, top + 7, command_left, command_width, unicode ? "┌" : "+", horizontal, unicode ? "┐" : "+",
226 cyan);
227 frame_buffer_printf(out, "\033[%d;%dH%s%*s%s", top + 8, command_left, colored_string(cyan, vertical),
228 command_width - 2, "", colored_string(cyan, vertical));
229 update_center_line(out, top + 8, command_left + 2, command_width - 4, command, cyan);
230 update_border(out, top + 9, command_left, command_width, unicode ? "└" : "+", horizontal, unicode ? "┘" : "+",
231 cyan);
233 truncate_with_ellipsis("Full instructions on ascii-chat.com", instructions, sizeof(instructions), width - 4);
234 int instructions_left = left + 2 + (width - 4 - display_width(instructions)) / 2;
237 (void)terminal_format_hyperlink(domain_link, sizeof(domain_link), "https://ascii-chat.com", "ascii-chat.com",
238 !GET_OPTION(strip_ansi));
239 char *domain = strstr(instructions, "ascii-chat.com");
240 if (domain) {
241 *domain = '\0';
242 snprintf(styled_instructions, sizeof(styled_instructions), "%s%s", instructions, domain_link);
243 } else {
244 snprintf(styled_instructions, sizeof(styled_instructions), "%s", instructions);
245 }
246 frame_buffer_printf(out, "\033[%d;%dH%s", top + 10, instructions_left,
247 colored_string(LOG_COLOR_WHITE, styled_instructions));
248 update_center_line(out, top + 11, left + 2, width - 4,
249 "Binaries for every OS with release notes on GitHub:", muted);
250 update_center_line(out, top + 12, left + 2, width - 4, result->release_url, cyan);
251 } else {
252 update_center_line(out, top + 7, left + 2, width - 4, "Checked just now", muted);
253 }
254 } else {
255 update_center_line(out, top + 6, left + 2, width - 4, "No network result is available.", muted);
256 update_center_line(out, top + 7, left + 2, width - 4, "Press R to retry.", muted);
257 }
258 int button_row = top + (available ? 16 : checking ? 13 : 11);
259 if (failed) {
260 update_center_line(out, button_row - 3, left + 2, width - 4, "Update command failed. See the logs and retry.",
261 yellow);
262 }
263 update_border(out, button_row - 2, left + 2, width - 4, "", horizontal, "", cyan);
264 int selected = atomic_load(&g_update_selected_action) % count;
265 ui_button_t buttons[4];
266 for (int i = 0; i < count; ++i)
267 buttons[i] = (ui_button_t){.label = labels[i]};
268 ui_buttons_render(out, button_row, left + 3, width - 6, buttons, (size_t)count, (size_t)selected);
271}
272
273static void render_update_prompt(terminal_size_t size, const void *data) {
274 render_update_dialog(size, data, true, false);
275}
276
277static void render_update_overlay(terminal_size_t size, const void *data) {
278 (void)data;
280 mutex_lock(&g_update_result_mutex);
281 result = g_update_result;
282 bool checking = g_update_checking;
283 mutex_unlock(&g_update_result_mutex);
284 render_update_dialog(size, &result, false, checking);
285}
286
288 if (!ctx || !terminal_can_prompt_user(!GET_OPTION(snapshot_mode)))
289 return false;
291 mutex_lock(&g_update_result_mutex);
292 result = g_update_result;
293 mutex_unlock(&g_update_result_mutex);
294 if (update_check_is_dismissed(&result)) {
295 return false;
296 }
297 atomic_store(&g_update_selected_action, 0);
298 if (ui_controller_submit(UI_SCREEN_UPDATE, STDOUT_FILENO, (terminal_size_t){.cols = 32, .rows = 23},
299 render_update_prompt, &result, sizeof(result)) != ASCIICHAT_OK)
300 return false;
301 bool update = false;
302 while (!shutdown_is_requested()) {
303 /* The update prompt is synchronous, but a failed DNS check can put the
304 * error popup above it. Route keys to that modal until it is dismissed. */
307 continue;
308 }
310 if (g_update_overlay)
311 return false;
312 if (update_select_key(key, 3))
313 continue;
314 if (key == 'o' || key == 'O' || ((key == '\r' || key == '\n') && atomic_load(&g_update_selected_action) == 1)) {
315 update_banner_open_release();
316 continue;
317 }
318 if (key == '\r' || key == '\n') {
319 update = atomic_load(&g_update_selected_action) == 0;
320 break;
321 }
322 if (key == 'y' || key == 'Y') {
323 update = true;
324 break;
325 }
326 if (key == 'n' || key == 'N' || key == KEY_ESCAPE)
327 break;
328 }
329 if (!g_update_overlay) {
331 }
332 if (!update && !g_update_overlay) {
335 }
336 return update;
337}
338
340 mutex_lock(&g_update_result_mutex);
342 memcpy(&result, &g_update_result, sizeof(result));
343 mutex_unlock(&g_update_result_mutex);
344
347 (void)terminal_format_hyperlink(domain_link, sizeof(domain_link), "https://ascii-chat.com", "ascii-chat.com",
348 !GET_OPTION(strip_ansi));
349 snprintf(instructions, sizeof(instructions), "Full instructions on %s", domain_link);
351 int len = snprintf(output, sizeof(output),
352 "\033[0m\033[2J\033[H\033[?25h"
353 "\nUpdated ascii-chat: %s -> %s\n\nRun ascii-chat again to start the new version.\n\n"
354 "Binaries for every OS with release notes on GitHub:\n%s\n\n"
355 "%s\n\n",
356 result.current_version, result.latest_version, result.release_url,
357 colored_string(LOG_COLOR_WHITE, instructions));
358 if (len > 0 && len < (int)sizeof(output))
359 ui_controller_finish(STDOUT_FILENO, output, (size_t)len);
360}
361
363 update_check_result_t result = {0};
364 if (!update_banner_copy_result(&result) || !result.update_available) {
365 return false;
366 }
367 g_update_install_failed = false;
368
370 int exit_code = -1;
371#ifndef NDEBUG
372 const char *test_command = SAFE_GETENV("ASCII_CHAT_TEST_UPDATE_COMMAND");
373 if (test_command && test_command[0]) {
374#ifdef _WIN32
375 const char *argv[] = {"cmd", "/d", "/c", test_command, NULL};
376 exit_code = platform_execute_subprocess("cmd", argv, NULL, 0, NULL, NULL);
377#else
378 const char *argv[] = {"/bin/sh", "-c", test_command, NULL};
379 exit_code = platform_execute_subprocess("/bin/sh", argv, NULL, 0, NULL, NULL);
380#endif
381 } else
382#endif
383 if (method == INSTALL_METHOD_HOMEBREW) {
384 const char *argv[] = {"brew", "upgrade", "ascii-chat", NULL};
385 exit_code = platform_execute_subprocess("brew", argv, NULL, 0, NULL, NULL);
386 } else if (method == INSTALL_METHOD_SCOOP) {
387 // Scoop is a PowerShell script/shim, rather than a native executable.
388 const char *argv[] = {"powershell", "-NoProfile", "-Command",
389 "$ErrorActionPreference = 'Stop'; scoop update ascii-chat; if (!$?) { exit 1 }", NULL};
390 exit_code = platform_execute_subprocess("powershell", argv, NULL, 0, NULL, NULL);
391 } else if (method == INSTALL_METHOD_ARCH_AUR) {
392 const char *helper = update_check_get_aur_helper();
393 if (!helper) {
394 log_warn("No supported AUR helper is available");
395 g_update_install_failed = true;
397 return false;
398 }
399 const char *argv[] = {helper, "-S", "ascii-chat", NULL};
400 exit_code = platform_execute_subprocess(helper, argv, NULL, 0, NULL, NULL);
401 } else {
402#ifdef _WIN32
403 const char *argv[] = {
404 "powershell", "-NoProfile", "-ExecutionPolicy",
405 "Bypass", "-Command", "$ErrorActionPreference = 'Stop'; irm https://ascii-chat.com/install.ps1 | iex",
406 NULL};
407 exit_code = platform_execute_subprocess("powershell", argv, NULL, 0, NULL, NULL);
408#else
409 const char *argv[] = {"bash", "-o", "pipefail", "-c", "curl -fsSL https://ascii-chat.com/install.sh | bash", NULL};
410 exit_code = platform_execute_subprocess("bash", argv, NULL, 0, NULL, NULL);
411#endif
412 }
413 if (exit_code != 0) {
414 log_warn("ascii-chat update command exited with status %d", exit_code);
415 g_update_install_failed = true;
417 return false;
418 }
422 return true;
423}
424
425typedef struct {
426 bool exited;
427 int64_t status;
435
436static void update_child_exited(uv_process_t *process, int64_t status, int signal) {
437 update_child_result_t *result = process->data;
438 result->status = status;
439 result->signal = signal;
440 result->exited = true;
441 uv_close((uv_handle_t *)process, NULL);
442}
443
444// A separate process can be terminated on shutdown even while DNS or TLS is blocked.
445static asciichat_error_t update_check_background(update_check_result_t *result) {
446 if (shutdown_is_requested() || atomic_load(&g_update_cancel_requested))
449 return ASCIICHAT_OK;
450 char executable[PLATFORM_MAX_PATH_LENGTH];
451 if (!platform_get_executable_path(executable, sizeof(executable)))
452 return SET_ERRNO(ERROR_PROCESS_FAILED, "Cannot locate executable for background update check");
453 uv_loop_t loop;
454 int rc = uv_loop_init(&loop);
455 if (rc != 0)
456 return SET_ERRNO(ERROR_PROCESS_FAILED, "Cannot initialize update process: %s", uv_strerror(rc));
457 char *args[] = {executable, "--check-update", NULL};
458 uv_stdio_container_t streams[3] = {0};
459 for (size_t i = 0; i < 3; ++i)
460 streams[i].flags = UV_IGNORE;
461 update_child_result_t child_result = {0};
462 uv_process_t child = {.data = &child_result};
463 uv_process_options_t options = {.file = executable,
464 .args = args,
465 .exit_cb = update_child_exited,
466 .stdio_count = 3,
467 .stdio = streams,
468 .flags = UV_PROCESS_WINDOWS_HIDE};
469 rc = uv_spawn(&loop, &child, &options);
470 if (rc != 0) {
471 uv_loop_close(&loop);
472 return SET_ERRNO(ERROR_PROCESS_FAILED, "Cannot start background update check: %s", uv_strerror(rc));
473 }
474 bool killed = false, timed_out = false;
475 uint64_t deadline = time_get_ns() + 15 * NS_PER_SEC_INT;
476 while (!child_result.exited) {
477 uv_run(&loop, UV_RUN_NOWAIT);
478 bool cancelled = shutdown_is_requested() || atomic_load(&g_update_cancel_requested);
479 bool expired = time_get_ns() >= deadline;
480 if (!child_result.exited && (cancelled || expired) && !killed) {
481 int kill_result = uv_process_kill(&child, SIGKILL);
482 if (kill_result != 0 && kill_result != UV_ESRCH) {
483 log_warn("Cannot stop background update check: %s", uv_strerror(kill_result));
484 uv_close((uv_handle_t *)&child, NULL);
485 uv_run(&loop, UV_RUN_DEFAULT);
486 uv_loop_close(&loop);
487 return cancelled ? ERROR_SIGNAL_INTERRUPT : SET_ERRNO(ERROR_PROCESS_FAILED, "Cannot stop update check");
488 }
489 killed = true;
490 timed_out = expired && !cancelled;
491 }
492 if (!child_result.exited)
494 }
495 uv_run(&loop, UV_RUN_DEFAULT);
496 uv_loop_close(&loop);
497 if (timed_out)
498 return SET_ERRNO(ERROR_NETWORK, "Background update check timed out");
499 if (killed)
501 if (child_result.status != 0 || child_result.signal != 0 || !update_check_try_fresh_cache(result))
502 return SET_ERRNO(ERROR_NETWORK, "Background update check failed");
503 return ASCIICHAT_OK;
504}
505
506static void *update_check_thread_func(void *arg) {
507 bool force = arg != NULL;
508 uint64_t visible_until = time_get_ns() + 2 * NS_PER_SEC_INT;
510 bool background = !force && !splash_should_display(true) &&
511 (GET_OPTION(detected_mode) == MODE_MIRROR || GET_OPTION(detected_mode) == MODE_CLIENT ||
512 GET_OPTION(detected_mode) == MODE_DISCOVERY);
514 if (background) {
516 bool previous_notifications = errno_notifications_suppress(true);
517 err = update_check_background(&result);
518 if (err != ASCIICHAT_OK && err != ERROR_SIGNAL_INTERRUPT)
519 log_debug("Automatic update check failed: %s", asciichat_error_string(err));
520 errno_notifications_suppress(previous_notifications);
522 } else {
524 bool previous_notifications = errno_notifications_suppress(true);
525 err = force ? update_check_perform(&result) : update_check_startup(&result);
526 errno_notifications_suppress(previous_notifications);
528 }
529 while (!shutdown_is_requested() && !atomic_load(&g_update_cancel_requested)) {
530 uint64_t overlay_until = atomic_load(&g_update_visible_until_ns);
531 if (overlay_until > visible_until)
532 visible_until = overlay_until;
533 if (time_get_ns() >= visible_until)
534 break;
536 }
537 if (err == ASCIICHAT_OK) {
538 if (result.update_available) {
540 update_check_format_notification(&result, notification, sizeof(notification));
541 log_info("%s", notification);
542 splash_set_update_notification(notification);
543 } else {
544 log_info("Already on latest version: %s", result.current_version);
545 }
546
547 // Store every terminal state for the reusable update overlay.
549 } else {
550 // A failed retry must not advertise an earlier successful result.
551 update_check_result_t unavailable = {0};
552 update_banner_set_result(&unavailable);
553 }
554 mutex_lock(&g_update_result_mutex);
555 g_update_checking = false;
556 g_update_finished = true;
557 mutex_unlock(&g_update_result_mutex);
558 // With no splash, the check runs in the background instead of holding up
559 // the first frame. Notify interactively once its result is available.
560 if (!force && err == ASCIICHAT_OK && result.update_available && !splash_should_display(true) &&
561 (GET_OPTION(detected_mode) == MODE_MIRROR || GET_OPTION(detected_mode) == MODE_CLIENT) &&
562 !atomic_load(&g_update_cancel_requested) && !shutdown_is_requested() &&
563 terminal_can_prompt_user(!GET_OPTION(snapshot_mode)) && !update_check_is_dismissed(&result)) {
564 bool expected = false;
565 if (atomic_compare_exchange_strong(&g_update_overlay, &expected, true)) {
566 atomic_store(&g_update_selected_action, 0);
567 update_banner_present_overlay();
568 }
569 }
571 return NULL;
572}
573
574static void update_banner_start_check_impl(bool force) {
575 if (g_update_thread_started)
576 return;
577 atomic_store(&g_update_cancel_requested, false);
578 if (lifecycle_init_once(&g_update_result_lifecycle)) {
579 mutex_init(&g_update_result_mutex, "update_banner_result");
580 lifecycle_init_commit(&g_update_result_lifecycle);
581 }
582 ascii_otel_ui_usage("update", "check_started", 0);
583 mutex_lock(&g_update_result_mutex);
584 g_update_checking = true;
585 g_update_finished = false;
586 atomic_store(&g_update_selected_action, 0);
587 memset(&g_update_result, 0, sizeof(g_update_result));
588 g_update_result_set = false;
589 mutex_unlock(&g_update_result_mutex);
590 if (platform_thread_create(&g_update_thread, "update_check", update_check_thread_func, force ? (void *)1 : NULL) ==
591 0) {
592 g_update_thread_started = true;
593 } else {
594 ascii_otel_ui_usage("update", "check_failed", 0);
595 mutex_lock(&g_update_result_mutex);
596 g_update_checking = false;
597 g_update_finished = true;
598 mutex_unlock(&g_update_result_mutex);
599 }
601}
602
604 update_banner_start_check_impl(false);
605}
606
608 if (g_update_overlay) {
609 ascii_otel_ui_usage("update", "dismissed", 0);
610 g_update_overlay = false;
612 return;
613 }
614 ascii_otel_ui_usage("update", "opened", 0);
615 g_update_overlay = true;
616 atomic_store(&g_update_visible_until_ns, time_get_ns() + 2 * NS_PER_SEC_INT);
617 atomic_store(&g_update_selected_action, 0);
618 bool finished = false;
619 if (lifecycle_is_initialized(&g_update_result_lifecycle)) {
620 mutex_lock(&g_update_result_mutex);
621 finished = g_update_finished;
622 mutex_unlock(&g_update_result_mutex);
623 }
624 if (g_update_thread_started && finished)
626 if (!g_update_thread_started)
627 update_banner_start_check_impl(true);
628 update_banner_present_overlay();
629}
630
631static void update_banner_present_overlay(void) {
632 const bool snapshot = true;
634 if (ui_controller_submit(UI_SCREEN_UPDATE, STDOUT_FILENO, (terminal_size_t){.cols = 32, .rows = 24},
635 render_update_overlay, &snapshot, sizeof(snapshot)) != ASCIICHAT_OK) {
636 g_update_overlay = false;
637 LOG_ERRNO_IF_SET("Cannot show update checker");
638 }
640}
641
643 if (!lifecycle_is_initialized(&g_update_result_lifecycle))
644 return false;
645 mutex_lock(&g_update_result_mutex);
646 bool checking = g_update_checking;
647 mutex_unlock(&g_update_result_mutex);
648 return checking;
649}
650
652 return g_update_overlay;
653}
654
655static void update_banner_open_release(void) {
657 mutex_lock(&g_update_result_mutex);
658 result = g_update_result;
659 mutex_unlock(&g_update_result_mutex);
660 // The URL can originate in the disk cache. Keep it a single HTTPS argument,
661 // including when Windows passes it through cmd's start builtin.
662 if (!result.check_succeeded || strncmp(result.release_url, "https://", 8) != 0 || !result.release_url[8] ||
663 strpbrk(result.release_url, "\"'&|^<>%!`()\\") != NULL)
664 return;
665 for (const unsigned char *ch = (const unsigned char *)result.release_url; *ch; ++ch) {
666 if (*ch <= ' ' || *ch == 127)
667 return;
668 }
669 ascii_otel_ui_usage("update", "release_open_requested", 0);
670#ifdef _WIN32
671 const char *argv[] = {"cmd", "/c", "start", "", result.release_url, NULL};
672 (void)platform_execute_subprocess("cmd", argv, NULL, 0, NULL, NULL);
673#elif defined(__APPLE__)
674 const char *argv[] = {"open", result.release_url, NULL};
675 (void)platform_execute_subprocess("open", argv, NULL, 0, NULL, NULL);
676#else
677 const char *argv[] = {"xdg-open", result.release_url, NULL};
678 (void)platform_execute_subprocess("xdg-open", argv, NULL, 0, NULL, NULL);
679#endif
680}
681
683 if (key == KEY_F12) {
685 return true;
686 }
687 if (!g_update_overlay)
688 return false;
689 bool checking = update_banner_is_checking();
690 update_check_result_t result = {0};
691 bool available = !checking && update_banner_copy_result(&result) && result.update_available;
692 int count = update_action_count(false, checking, available);
693 if (update_select_key(key, count))
694 return true;
695 if (key == '\r' || key == '\n') {
696 int selected = atomic_load(&g_update_selected_action) % count;
697 if ((available && selected == 2) || (!available && selected == 0)) {
699 return true;
700 }
701 if (checking)
702 return true;
703 if (available && selected == 0) {
705 return true;
706 }
707 if (available && selected == 1) {
708 update_banner_open_release();
709 return true;
710 }
711 key = 'r';
712 }
713 if (key == KEY_ESCAPE) {
715 return true;
716 }
717 if (key == 'o' || key == 'O') {
719 update_banner_open_release();
720 return true;
721 }
722 if (key == 'r' || key == 'R') {
724 return true;
726 update_banner_start_check_impl(true);
727 return true;
728 }
729 return g_update_overlay;
730}
731
733 if (!g_update_thread_started) {
734 return;
735 }
736 // Shutdown must remain responsive after cancellation. The worker checks
737 // g_update_cancel_requested and terminates its child process, so one second
738 // is sufficient for normal teardown while avoiding a multi-second Ctrl-C
739 // stall when a platform/network call is slow to unwind.
740 int ret = thread_join_timeout(&g_update_thread, NULL, 1LL * NS_PER_SEC_INT);
741 if (ret != 0) {
742 // Timeout — thread is still alive. Do a blocking join to avoid orphaning it.
743 // The thread has bounded runtime (DNS + HTTP both have timeouts) so this won't hang.
744 log_warn("Update check thread did not finish within 1s, waiting for completion");
745 thread_join(&g_update_thread, NULL);
746 }
747 g_update_thread_started = false;
748}
749
751 atomic_store(&g_update_cancel_requested, true);
752}
action_args_t args
Definition actions.c:78
Application-level callbacks for library code.
#define APP_CALLBACK_VOID(callback_name)
void errno_scope_end(asciichat_errno_scope_t scope, asciichat_errno_outcome_t outcome)
bool errno_notifications_suppress(bool suppress)
asciichat_errno_scope_t errno_scope_begin(void)
#define SET_ERRNO(code,...)
@ ASCIICHAT_ERRNO_HANDLED
#define LOG_ERRNO_IF_SET(message)
void ui_buttons_render(frame_buffer_t *out, int row, int left, int width, const ui_button_t *b, size_t n, size_t selected)
Definition buttons.c:29
int ui_buttons_key(ui_buttons_t *s, const ui_button_t *b, size_t n, keyboard_key_t key)
Definition buttons.c:8
⚙️ Common definitions, error codes, macros, and types shared throughout the application
bool ui_error_popup_visible(void)
Definition error_popup.c:71
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
void frame_buffer_append(frame_buffer_t *buf, const char *str, size_t len)
Append raw bytes to the buffer.
void frame_buffer_destroy(frame_buffer_t *buf)
Destroy a frame buffer.
size_t frame_buffer_get_length(const frame_buffer_t *buf)
Get the current content length of the frame buffer.
const char * frame_buffer_get_content(const frame_buffer_t *buf)
Get a pointer to the buffer content.
frame_buffer_t * frame_buffer_create(int rows, int cols)
Create a new frame buffer.
void frame_buffer_printf(frame_buffer_t *buf, const char *fmt,...)
Printf-style append to the buffer.
Buffered terminal frame rendering.
struct frame_buffer frame_buffer_t
Opaque frame buffer handle.
#define SAFE_GETENV(name)
Definition common.h:449
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:95
unsigned long long uint64_t
Definition common.h:63
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_SIGNAL_INTERRUPT
@ ERROR_NETWORK
Definition error_codes.h:72
@ ERROR_PROCESS_FAILED
@ ASCIICHAT_OK
Definition error_codes.h:46
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
log_color_t
Color enum for logging - indexes into color arrays.
Definition log/log.h:147
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
@ LOG_COLOR_RESET
Definition log/log.h:155
@ LOG_COLOR_GREY
Definition log/log.h:154
@ LOG_COLOR_DEBUG
Definition log/log.h:149
@ LOG_COLOR_INFO
Definition log/log.h:150
@ LOG_COLOR_WARN
Definition log/log.h:151
@ LOG_COLOR_WHITE
Definition log/log.h:156
#define NS_PER_SEC_INT
Definition time.h:169
asciichat_error_t update_check_startup(update_check_result_t *result)
Perform startup update check with caching.
install_method_t
Installation method for suggesting upgrade command.
void update_check_dismiss_for_a_week(update_check_result_t *result)
void update_check_get_upgrade_suggestion(install_method_t method, const char *latest_version, char *buffer, size_t buffer_size)
Get upgrade suggestion string.
const char * update_check_get_aur_helper(void)
asciichat_error_t update_check_perform(update_check_result_t *result)
Check for updates from GitHub releases API.
bool update_check_try_fresh_cache(update_check_result_t *result)
bool update_check_is_dismissed(const update_check_result_t *result)
install_method_t update_check_detect_install_method(void)
Detect installation method.
void update_check_format_notification(const update_check_result_t *result, char *buffer, size_t buffer_size)
Format update notification message.
@ INSTALL_METHOD_ARCH_AUR
Installed via Arch AUR (paru/yay).
@ INSTALL_METHOD_SCOOP
Installed via Scoop.
@ INSTALL_METHOD_HOMEBREW
Installed via Homebrew.
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
@ MODE_CLIENT
Client mode - network client options.
Definition options.h:921
@ MODE_MIRROR
Mirror mode - local webcam viewing (no network).
Definition options.h:922
@ MODE_DISCOVERY
Discovery mode - participant that can dynamically become host.
Definition options.h:924
int platform_execute_subprocess(const char *executable, const char **argv, char *output_buffer, size_t output_size, bool(*cancel)(void *), void *data)
Execute a subprocess and optionally capture its output.
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
keyboard_key_t
Options for interactive line editing.
Definition keyboard.h:53
@ KEY_UP
Up arrow key.
Definition keyboard.h:57
@ KEY_ESCAPE
Escape key (ESC).
Definition keyboard.h:55
@ KEY_LEFT
Left arrow key.
Definition keyboard.h:59
@ KEY_RIGHT
Right arrow key.
Definition keyboard.h:60
@ KEY_F12
Open the version update checker TUI.
Definition keyboard.h:69
@ KEY_DOWN
Down arrow key.
Definition keyboard.h:58
bool update_banner_install_update(void)
void update_banner_cancel_check(void)
struct session_display_ctx session_display_ctx_t
session display config is intentionally a small, concrete record. Fields such as snapshot_mode,...
void update_banner_wait_for_check(void)
Wait for the background update check thread to finish.
bool update_banner_show_prompt(session_display_ctx_t *ctx)
Show the update prompt screen and wait for user input.
void update_banner_start_check(void)
Start the background update check thread.
bool update_banner_handle_key(int key)
void splash_set_update_notification(const char *notification)
Set update notification to display on splash/status screens.
Definition splash.c:481
bool splash_should_display(bool is_intro)
Render splash header with rainbow animation (callback for terminal_screen).
Definition splash.c:242
bool update_banner_has_update(void)
Check if an update is available (thread-safe).
bool update_banner_is_checking(void)
void update_banner_toggle_overlay(void)
void update_banner_set_result(const update_check_result_t *result)
Store the update check result (thread-safe).
void update_banner_print_instructions(void)
Print upgrade instructions to stdout and prepare for clean exit.
bool update_banner_is_overlay_open(void)
void shutdown_request(void)
Definition common.c:82
bool shutdown_is_requested(void)
Check if shutdown has been requested.
Definition common.c:86
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
Definition string.c:284
int display_width(const char *text)
Calculate the visible display width of text with ANSI escape codes.
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.
Definition string.c:370
🖥️ Display Width and Terminal Centering Utilities
📝 Logging API with multiple log levels and terminal output control
void signal_exit(void)
Definition main.c:253
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
void ascii_otel_ui_usage(const char *screen, const char *event, uint64_t duration_ns)
Definition otel.c:434
asciichat_error_t ui_popup_frame(frame_buffer_t *out, int row, int col, int width, int height, log_color_t color)
Definition popup.c:28
void ui_spinner_render(frame_buffer_t *out, int row, int col, uint64_t elapsed_ns)
Definition progress.c:42
static bool size_t gotel_query_t * out
Definition queries.h:8
Shutdown check system for clean library/application separation.
Intro and status screen display for ascii-chat with animated rainbow effects.
🖥️ Unified terminal display abstraction for session-based rendering
🔤 String Manipulation and Shell Escaping Utilities
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
When code needs to work with ui button, it keeps label, shortcut, disabled, color here instead of sca...
Definition buttons.h:16
update check result keeps the values that move through the subsystem: update_available,...
char latest_version[64]
Latest version tag (e.g., "v0.9.0").
char current_version[64]
Current version string.
bool update_available
True if newer version exists.
bool check_succeeded
True if check completed (even if no update).
char release_url[512]
GitHub release page URL.
⏱️ High-precision timing utilities using sokol_time.h and uthash
void ui_controller_remove(ui_screen_t screen)
asciichat_error_t ui_controller_submit(ui_screen_t screen, int fd, terminal_size_t minimum, ui_render_fn render, const void *snapshot, size_t bytes)
void ui_controller_redraw(void)
void ui_controller_finish(int fd, const char *data, size_t len)
asciichat_error_t ui_controller_write(int fd, const char *data, size_t len)
@ UI_SCREEN_UPDATE
@ UI_SCREEN_ERROR
keyboard_key_t ui_input_wait_key(ui_screen_t screen, unsigned timeout_ms)
Definition tui/input.c:554
#define TUI_UPDATE_BANNER_HEADING_BUFFER_SIZE
#define TUI_UPDATE_BANNER_INSTALLED_VERSION_BUFFER_SIZE
#define TUI_UPDATE_BANNER_COMMAND_BUFFER_SIZE
#define TUI_UPDATE_BANNER_INSTRUCTIONS_BUFFER_SIZE
#define TUI_UPDATE_BANNER_INSTALLED_BUFFER_SIZE
#define TUI_UPDATE_BANNER_LATEST_BUFFER_SIZE
#define TUI_UPDATE_BANNER_NOTIFICATION_BUFFER_SIZE
#define TUI_UPDATE_BANNER_DOMAIN_LINK_BUFFER_SIZE
#define TUI_UPDATE_BANNER_CLIPPED_BUFFER_SIZE
#define TUI_UPDATE_BANNER_STYLED_INSTRUCTIONS_BUFFER_SIZE
#define TUI_UPDATE_BANNER_OUTPUT_BUFFER_SIZE
#define TUI_UPDATE_BANNER_INSTRUCTIONS_EXTENDED_BUFFER_SIZE
Update available prompt screen shown after splash.