ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
update_banner.c
Go to the documentation of this file.
1
12#include "session/display.h"
13#include <ascii-chat/common.h>
17#include <ascii-chat/log/log.h>
22
23#include <stdio.h>
24#include <string.h>
25
26// Thread-safe storage for the update check result
27static update_check_result_t g_update_result;
28static mutex_t g_update_result_mutex;
29static bool g_update_result_set = false;
30static lifecycle_t g_update_result_lifecycle = LIFECYCLE_INIT_MUTEX(&g_update_result_mutex);
31
32// Background update check thread
33static asciichat_thread_t g_update_thread;
34static bool g_update_thread_started = false;
35
37 if (!result || !result->update_available) {
38 return;
39 }
40
41 if (lifecycle_init_once(&g_update_result_lifecycle)) {
42 mutex_init(&g_update_result_mutex, "update_banner_result");
43 lifecycle_init_commit(&g_update_result_lifecycle);
44 }
45
46 mutex_lock(&g_update_result_mutex);
47 memcpy(&g_update_result, result, sizeof(g_update_result));
48 g_update_result_set = true;
49 mutex_unlock(&g_update_result_mutex);
50}
51
53 if (!g_update_result_set) {
54 return false;
55 }
56
57 if (lifecycle_init_once(&g_update_result_lifecycle)) {
58 mutex_init(&g_update_result_mutex, "update_banner_result");
59 lifecycle_init_commit(&g_update_result_lifecycle);
60 }
61
62 mutex_lock(&g_update_result_mutex);
63 bool available = g_update_result_set && g_update_result.update_available;
64 mutex_unlock(&g_update_result_mutex);
65 return available;
66}
67
68// Build a box line: "║ <content><padding>║"
69// Content may contain ANSI escape sequences — truncate_with_ellipsis handles them.
70static void build_box_line(char *output, size_t output_size, const char *content, int box_width) {
71 if (!output || output_size < 256 || !content || box_width < 6) {
72 return;
73 }
74
75 int content_available = box_width - 4; // "║ " + "║"
76 if (content_available < 1) {
77 content_available = 1;
78 }
79
80 char truncated[512];
81 truncate_with_ellipsis(content, truncated, sizeof(truncated), content_available);
82
83 int content_width = display_width(truncated);
84 int padding = content_available - content_width;
85 if (padding < 0) {
86 padding = 0;
87 }
88
89 char *pos = output;
90 int remaining = (int)output_size;
91
92 int n = snprintf(pos, remaining, "║ %s", truncated);
93 if (n > 0) {
94 pos += n;
95 remaining -= n;
96 }
97
98 for (int i = 0; i < padding && remaining > 1; i++) {
99 *pos++ = ' ';
100 remaining--;
101 }
102
103 if (remaining > 3) {
104 snprintf(pos, remaining, "║");
105 }
106}
107
108// Append a positioned box line to the buffer
109static void append_line(char *buffer, size_t *buf_pos, size_t buf_size, int start_row, int *current_row, int start_col,
110 int box_width, const char *content) {
111 if (!buffer || *buf_pos >= buf_size || !content) {
112 return;
113 }
114
115 char line_buf[256];
116 int written =
117 snprintf(buffer + *buf_pos, buf_size - *buf_pos, "\033[%d;%dH", start_row + *current_row, start_col + 1);
118 if (written > 0) {
119 *buf_pos += (size_t)written;
120 }
121
122 build_box_line(line_buf, sizeof(line_buf), content, box_width);
123 written = snprintf(buffer + *buf_pos, buf_size - *buf_pos, "%s", line_buf);
124 if (written > 0) {
125 *buf_pos += (size_t)written;
126 }
127
128 (*current_row)++;
129}
130
131// Build a horizontal border line (top: ╔═╗, middle: ╠═╣, bottom: ╚═╝)
132static void append_border(char *buffer, size_t *buf_pos, size_t buf_size, int start_row, int *current_row,
133 int start_col, int box_width, const char *left, const char *right) {
134 if (!buffer || *buf_pos >= buf_size) {
135 return;
136 }
137
138 int written =
139 snprintf(buffer + *buf_pos, buf_size - *buf_pos, "\033[%d;%dH", start_row + *current_row, start_col + 1);
140 if (written > 0) {
141 *buf_pos += (size_t)written;
142 }
143
144 char border[256];
145 int bpos = 0;
146 int r = snprintf(border + bpos, sizeof(border) - bpos, "%s", left);
147 if (r > 0) {
148 bpos += r;
149 }
150 for (int i = 1; i < box_width - 1; i++) {
151 r = snprintf(border + bpos, sizeof(border) - bpos, "═");
152 if (r > 0) {
153 bpos += r;
154 }
155 }
156 snprintf(border + bpos, sizeof(border) - bpos, "%s", right);
157
158 written = snprintf(buffer + *buf_pos, buf_size - *buf_pos, "%s", border);
159 if (written > 0) {
160 *buf_pos += (size_t)written;
161 }
162
163 (*current_row)++;
164}
165
167 if (!ctx) {
168 return false;
169 }
170
171 // Get a copy of the result under mutex
173 mutex_lock(&g_update_result_mutex);
174 memcpy(&result, &g_update_result, sizeof(result));
175 mutex_unlock(&g_update_result_mutex);
176
177 // Get upgrade suggestion
179 char suggestion[512];
180 update_check_get_upgrade_suggestion(method, result.latest_version, suggestion, sizeof(suggestion));
181
182 // Terminal dimensions
183 int term_width = (int)terminal_get_effective_width();
184 int term_height = (int)terminal_get_effective_height();
185
186 // Box sizing
187 int box_width = 52;
188 if (box_width > term_width - 2) {
189 box_width = term_width - 2;
190 }
191 if (box_width < 30) {
192 box_width = 30;
193 }
194
195 int box_height = 16;
196 int start_col = (term_width - box_width) / 2;
197 if (start_col < 0) {
198 start_col = 0;
199 }
200 int start_row = (term_height - box_height) / 2;
201 if (start_row < 0) {
202 start_row = 0;
203 }
204
205 // Build the screen
206 const size_t BUF_SIZE = 4096;
207 char *buffer = SAFE_MALLOC(BUF_SIZE, char *);
208 size_t buf_pos = 0;
209
210#define APPEND(fmt, ...) \
211 do { \
212 int _w = snprintf(buffer + buf_pos, BUF_SIZE - buf_pos, fmt, ##__VA_ARGS__); \
213 if (_w > 0) { \
214 buf_pos += (size_t)_w; \
215 } \
216 } while (0)
217
218 // Clear screen
219 APPEND("\033[2J\033[H");
220
221 int row = 1;
222
223 // Top border
224 append_border(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "╔", "╗");
225
226 // Title (bold yellow)
227 {
228 char title_content[256];
229 snprintf(title_content, sizeof(title_content), "\033[1;33mUpdate Available\033[0m");
230 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, title_content);
231 }
232
233 // Separator
234 append_border(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "╠", "╣");
235
236 // Blank line
237 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "");
238
239 // Current version
240 {
241 char line[256];
242 snprintf(line, sizeof(line), "Current : %s (%.8s)", result.current_version, result.current_sha);
243 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, line);
244 }
245
246 // Latest version (green)
247 {
248 char line[256];
249 snprintf(line, sizeof(line), "Latest : \033[32m%s\033[0m", result.latest_version);
250 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, line);
251 }
252
253 // Exact release source on GitHub.
254 {
255 char line[512];
256 snprintf(line, sizeof(line), "Release : %s", result.release_url);
257 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, line);
258 }
259
260 // Blank line
261 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "");
262
263 // Separator
264 append_border(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "╠", "╣");
265
266 // Upgrade instructions
267 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "To upgrade:");
268 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "");
269
270 // Upgrade command (bold white)
271 {
272 char line[256];
273 snprintf(line, sizeof(line), " \033[1m%s\033[0m", suggestion);
274 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, line);
275 }
276
277 // Blank line
278 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "");
279
280 // Separator
281 append_border(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "╠", "╣");
282
283 // Prompt (grey)
284 {
285 char line[256];
286 snprintf(line, sizeof(line), "\033[90m[Y/Enter] exit to update [N/Esc] continue\033[0m");
287 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, line);
288 }
289
290 // Bottom border
291 append_border(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "╚", "╝");
292
293#undef APPEND
294
295 // Write to terminal
296 session_display_write_raw(ctx, buffer, buf_pos);
297 terminal_flush(STDOUT_FILENO);
298 SAFE_FREE(buffer);
299
300 // Block for user input
301 while (true) {
302 keyboard_key_t key = keyboard_read_with_timeout(60000); // 60s timeout, re-loop
303 if (key == 'y' || key == 'Y' || key == '\r' || key == '\n') {
304 return true;
305 }
306 if (key == 'n' || key == 'N' || key == KEY_ESCAPE) {
307 return false;
308 }
309 // Ignore other keys (including KEY_NONE on timeout — just re-render/wait)
310 }
311}
312
314 mutex_lock(&g_update_result_mutex);
316 memcpy(&result, &g_update_result, sizeof(result));
317 mutex_unlock(&g_update_result_mutex);
318
320 char suggestion[512];
321 update_check_get_upgrade_suggestion(method, result.latest_version, suggestion, sizeof(suggestion));
322
324
325 fprintf(stdout, "\nUpdate available: %s → %s\n\n", result.current_version, result.latest_version);
326
327 if (method == INSTALL_METHOD_GITHUB || method == INSTALL_METHOD_UNKNOWN) {
328 fprintf(stdout, "Download the latest release:\n\n %s\n\n", suggestion);
329 } else {
330 fprintf(stdout, "To upgrade, run:\n\n %s\n\n", suggestion);
331 }
332
333 if (result.release_url[0] != '\0') {
334 fprintf(stdout, "Release notes: %s\n\n", result.release_url);
335 }
336
337 fflush(stdout);
338}
339
340static void *update_check_thread_func(void *arg) {
341 (void)arg;
344 if (err == ASCIICHAT_OK && result.update_available) {
345 char notification[1024];
346 update_check_format_notification(&result, notification, sizeof(notification));
347 log_info("%s", notification);
348
349 // Set notification for splash screen (thread-safe)
350 splash_set_update_notification(notification);
351
352 // Store full result for the update prompt screen
354 }
355 return NULL;
356}
357
359 if (asciichat_thread_create(&g_update_thread, "update_check", update_check_thread_func, NULL) == 0) {
360 g_update_thread_started = true;
361 }
362}
363
365 if (!g_update_thread_started) {
366 return;
367 }
368 int ret = asciichat_thread_join_timeout(&g_update_thread, NULL, 5LL * NS_PER_SEC_INT);
369 if (ret != 0) {
370 // Timeout — thread is still alive. Do a blocking join to avoid orphaning it.
371 // The thread has bounded runtime (DNS + HTTP both have timeouts) so this won't hang.
372 log_warn("Update check thread did not finish within 5s, waiting for completion");
373 asciichat_thread_join(&g_update_thread, NULL);
374 }
375 g_update_thread_started = false;
376}
⚙️ Common definitions, error codes, macros, and types shared throughout the application
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
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_info(...)
Log an INFO message.
Definition log/log.h:561
#define NS_PER_SEC_INT
Definition time.h:157
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_get_upgrade_suggestion(install_method_t method, const char *latest_version, char *buffer, size_t buffer_size)
Get upgrade suggestion string.
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_UNKNOWN
Unknown installation method.
@ INSTALL_METHOD_GITHUB
Manual install from GitHub releases.
int asciichat_thread_join_timeout(asciichat_thread_t *thread, void **retval, uint64_t timeout_ns)
Wait for a thread to complete with timeout.
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.
keyboard_key_t keyboard_read_with_timeout(uint32_t timeout_ms)
Read keyboard input with timeout.
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)
@ KEY_ESCAPE
Escape key (ESC)
Definition keyboard.h:56
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 session_display_write_raw(session_display_ctx_t *ctx, const char *data, size_t len)
Render raw bytes to the terminal without frame processing.
void update_banner_start_check(void)
Start the background update check thread.
void splash_set_update_notification(const char *notification)
Set update notification to display on splash/status screens.
Definition splash.c:776
bool update_banner_has_update(void)
Check if an update is available (thread-safe)
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.
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.
🧵 Cross-platform thread interface for ascii-chat
🖥️ Display Width and Terminal Centering 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.
unsigned short int terminal_get_effective_height(void)
Get effective terminal height with fallback priority.
Intro and status screen display for ascii-chat with animated rainbow effects.
🖥️ Unified terminal display abstraction for session-based rendering
Mutex type (POSIX: pthread_mutex_t with debug tracking)
Internal session display context structure.
Update check result data.
char latest_version[64]
Latest version tag (e.g., "v0.9.0")
char current_sha[41]
Current commit SHA.
char current_version[64]
Current version string.
bool update_available
True if newer version exists.
char release_url[512]
GitHub release page URL.
🖥️ Cross-platform terminal interface for ascii-chat
⏱️ High-precision timing utilities using sokol_time.h and uthash
#define APPEND(fmt,...)
Update available prompt screen shown after splash.
🔤 String Manipulation and Shell Escaping Utilities