ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
lib/tui/session.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define TUI_SESSION_DETAIL_BUFFER_SIZE 256
4#define TUI_SESSION_OFFSET_TEXT_BUFFER_SIZE 32
5
6/* Existing buffer budgets in bytes, including NUL for text storage. */
7#define PRESENTATION_TIME_HISTORY_COUNT 120
8
12#include <ascii-chat/log/log.h>
17#include <ascii-chat/common.h>
20#include <string.h>
21#include <stdio.h>
22
45
47 ui_session_t *s = SAFE_CALLOC(1, sizeof(*s), ui_session_t *);
48 if (!s)
49 return NULL;
50 s->frame = frame_buffer_create(24, 80);
51 s->media = frame_buffer_create(24, 80);
53 s->owns_logs = !s->logs;
54 if (s->owns_logs)
56 if (!s->frame || !s->media || !s->logs) {
58 return NULL;
59 }
60 s->started = time(NULL);
62 s->size = (terminal_size_t){.cols = 80, .rows = 24};
63 s->active = -1;
64 s->saved_volume = 1;
65 s->dirty = true;
66 s->splash.use_colors = true;
67 animation_init(&s->animation, "session_splash",
69 .type = ANIMATION_SPLASH_RAINBOW, .fps = 60, .speed = 1, .hidden_policy = ANIMATION_HIDDEN_PAUSE});
70 return s;
71}
72
85
86void ui_session_resize(ui_session_t *s, int cols, int rows) {
87 if (!s || cols < 1 || rows < 1 || cols > 4096 || rows > 4096) {
88 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid TUI session or terminal dimensions");
89 return;
90 }
91 if (cols != s->size.cols || rows != s->size.rows) {
92 s->size = (terminal_size_t){.cols = cols, .rows = rows};
93 s->dirty = true;
94 }
95}
96
97void ui_session_loading(ui_session_t *s, bool loading, const char *message) {
98 if (!s)
99 return;
100 if (loading != s->loading) {
101 s->loading = loading;
103 s->dirty = true;
104 }
105 if (message && strcmp(message, s->splash.connection_target)) {
107 s->dirty = true;
108 }
109}
110
111void ui_session_log(ui_session_t *s, const char *message) {
112 if (s && message) {
113 session_log_buffer_append(s->logs, message);
114 s->dirty = true;
115 }
116}
117
118asciichat_error_t ui_session_media(ui_session_t *s, const char *data, size_t length) {
119 if (!s || !data || length > UI_CONTROLLER_MAX_FRAME_BYTES)
120 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session media frame");
122 frame_buffer_append(s->media, data, length);
123 s->dirty = true;
124 return ASCIICHAT_OK;
125}
126
127static bool ui_session_logs_key_sized(keyboard_key_t key, terminal_size_t size);
128
130 if (!s)
131 return UI_ACTION_NONE;
132 if (s->show_logs && ui_session_logs_key_sized(key, s->size)) {
133 s->dirty = true;
134 return UI_ACTION_LOGS;
135 }
137 if (action == UI_ACTION_HELP) {
138 s->help = !s->help;
139 s->show_logs = false;
140 } else if (action == UI_ACTION_LOGS) {
141 s->show_logs = !s->show_logs;
142 s->help = false;
143 } else if (action == UI_ACTION_CLOSE && s->show_logs) {
144 s->show_logs = false;
146 } else if (action == UI_ACTION_CLOSE && s->help) {
147 s->help = false;
149 }
150 s->dirty |= action != UI_ACTION_NONE;
151 return action;
152}
153
154int ui_session_select_screen(uint32_t visible_screens) {
155 for (int screen = UI_SCREEN_COUNT - 1; screen >= 0; --screen)
156 if (visible_screens & (1u << screen))
157 return screen;
158 return -1;
159}
160
162 return s && s->covered;
163}
164
166 size_t length = s ? frame_buffer_get_length(s->frame) : 0;
167 return length ? length - 1 : 0;
168}
169
171 return s && s->animation.started ? s->splash.animation.next_frame_ns : UINT64_MAX;
172}
173
174const char *ui_session_render(ui_session_t *s, const ui_session_view_t *view, uint64_t now_ns) {
175 if (!s || !view)
176 return NULL;
177 bool changed = s->active != view->screen;
178 s->active = view->screen;
179 bool covered = view->screen >= 0 && ui_too_small(s->size, view->minimum);
180 bool transition = changed || covered != s->covered || s->size.cols != s->presented_size.cols ||
181 s->size.rows != s->presented_size.rows || view->minimum.cols != s->minimum.cols ||
182 view->minimum.rows != s->minimum.rows;
183 s->covered = covered;
184 bool visible =
185 (view->screen == UI_SCREEN_SPLASH || view->screen == UI_SCREEN_HELP || view->screen == UI_SCREEN_MDNS) &&
186 !covered;
187 if (visible != s->animation.visible) {
188 // Account for the elapsed interval under the old visibility before changing
189 // it, so resuming after an idle modal does not advance a paused animation.
190 if (animation_update(&s->animation, now_ns, &s->splash.animation) != ASCIICHAT_OK)
191 return NULL;
192 animation_set_visible(&s->animation, visible);
193 }
194 if (animation_update(&s->animation, now_ns, &s->splash.animation) != ASCIICHAT_OK)
195 return NULL;
196 if (view->screen < 0 || !view->draw)
197 return NULL;
198 bool animated = visible && s->splash.animation.changed;
199 bool refresh = view->refresh_ns && now_ns - s->last_render_ns >= view->refresh_ns;
200 if (covered ? !(transition || s->dirty) : !(transition || s->dirty || view->dirty || animated || refresh))
201 return NULL;
202 s->dirty = false;
203 s->presented_size = s->size;
204 s->minimum = view->minimum;
205 s->last_render_ns = now_ns;
207 if (transition || view->clear) {
208 frame_buffer_append(s->frame, "\033[0m", 4);
210 }
211 if (covered)
212 ui_too_small_render(s->frame, s->size, view->minimum);
213 else {
214 if (view->screen == UI_SCREEN_HELP) {
215 int top = (s->size.rows - view->minimum.rows) / 2;
216 terminal_color_mode_t mode = GET_OPTION(color_mode);
217 bool colors = mode != TERM_COLOR_NONE && GET_OPTION(color) != COLOR_SETTING_FALSE && !GET_OPTION(strip_ansi) &&
218 (mode != TERM_COLOR_AUTO ||
219 detect_terminal_capabilities(options_terminal_settings()).color_level != TERM_COLOR_NONE);
220 frame_buffer_printf(s->frame, "\033[%d;1H", top + 2);
221 invitation_render_logo(s->frame, s->size.cols, (int)(s->splash.animation.seconds * 60), colors, false);
222 }
223 view->draw(s->frame, s->size, &s->splash.animation, view->context);
224 }
225 frame_buffer_append(s->frame, "\0", 1);
227}
228
229static void draw_help(frame_buffer_t *output, terminal_size_t size, const animation_sample_t *animation,
230 void *context) {
231 (void)size;
232 (void)animation;
233 const char *text = context;
234 frame_buffer_append(output, text, strlen(text));
235}
236
237static void draw_splash(frame_buffer_t *output, terminal_size_t size, const animation_sample_t *animation,
238 void *context) {
239 ui_session_t *s = context;
240 s->splash.animation = *animation;
241 ui_session_splash_render(output, size, &s->splash);
242}
243
244void ui_session_logs_render(frame_buffer_t *output, terminal_size_t size, ui_session_t *s) {
246 size_t offset = terminal_screen_logs_offset();
247 char offset_text[TUI_SESSION_OFFSET_TEXT_BUFFER_SIZE];
248 snprintf(offset_text, sizeof(offset_text), "%zu", offset);
249 const char *colored_offset = colored_string(LOG_COLOR_FATAL, offset_text);
250 snprintf(detail, sizeof(detail), "%s | %s rows from tail", ui_status_log_controls(), colored_offset);
251 ui_status_t status = {.mode_name = "", .start_time = s->started, .detail = detail};
252 terminal_screen_config_t config = {
253 .fixed_header_lines = 4, .render_header = ui_status_render_header, .user_data = &status, .show_logs = true};
254 terminal_screen_compose(output, size, &config, s->logs);
255}
256
257static bool ui_session_logs_key_sized(keyboard_key_t key, terminal_size_t size) {
258 if (key == KEY_ESCAPE && log_search_is_active()) {
260 return true;
261 }
262 if (log_search_should_handle(key)) {
264 return true;
265 }
267 return true;
268 // Client-like log screens own these keys while the renderer is hidden.
269 // Search and log navigation take precedence over rendering shortcuts.
270 switch ((int)key) {
271 case 'c':
272 case 'C':
273 case 'r':
274 case 'R':
275 case 'f':
276 case 'F':
277 case 'x':
278 case 'X':
279 case 'y':
280 case 'Y':
281 case '1':
282 case '2':
283 case '3':
284 case 'w':
285 case 'W':
286 case 'z':
287 case 'Z':
288 case '-':
289 return true;
290 default:
291 break;
292 }
293 return false;
294}
295
297 return ui_session_logs_key_sized(key, ui_controller_size());
298}
299
300static void draw_logs(frame_buffer_t *output, terminal_size_t size, const animation_sample_t *animation,
301 void *context) {
302 (void)animation;
303 ui_session_logs_render(output, size, context);
304}
305
306static void draw_media(frame_buffer_t *output, terminal_size_t size, const animation_sample_t *animation,
307 void *context) {
308 (void)animation;
309 ui_session_t *s = context;
311 if (frame_buffer_get_length(s->media) && GET_OPTION(fps_counter)) {
312 unsigned frames = 0;
313 for (unsigned i = 0; i < 120; ++i)
314 frames += s->presentation_times[i] && s->last_render_ns - s->presentation_times[i] < 1000000000ULL;
315 frame_buffer_printf(output, "\033[1;%dH\033[0mFPS: %3u", size.cols > 9 ? size.cols - 8 : 1, frames);
316 }
317}
318
319const char *ui_session_tick(ui_session_t *s, uint64_t now_ns) {
320 if (!s)
321 return NULL;
322 if (!s->dirty && !s->show_logs && !s->help && !s->loading && s->active == UI_SCREEN_MEDIA)
323 return NULL;
324 uint32_t visible = 1u << UI_SCREEN_MEDIA;
325 if (s->loading)
326 visible |= 1u << UI_SCREEN_SPLASH;
327 if (s->show_logs)
328 visible |= 1u << UI_SCREEN_LOGS;
329 if (s->help)
330 visible |= 1u << UI_SCREEN_HELP;
331 ui_session_view_t view = {
332 .screen = ui_session_select_screen(visible), .minimum = {1, 1}, .clear = !s->help, .context = s};
333 char *help = NULL;
334 if (view.screen == UI_SCREEN_HELP) {
335 help = ui_session_help_render(s->size, true, true, &view.minimum);
336 if (!help)
337 return NULL;
338 view.draw = draw_help;
339 view.context = help;
340 } else if (view.screen == UI_SCREEN_LOGS) {
341 view.minimum = (terminal_size_t){.cols = 40, .rows = 6};
342 view.refresh_ns = 100000000;
343 view.draw = draw_logs;
344 } else if (view.screen == UI_SCREEN_SPLASH) {
345 view.minimum = (terminal_size_t){.cols = 52, .rows = 10};
346 view.draw = draw_splash;
347 } else {
348 view.draw = draw_media;
349 if (s->dirty && frame_buffer_get_length(s->media))
350 s->presentation_times[s->presentation_index++ % 120] = now_ns;
351 }
352 const char *frame = ui_session_render(s, &view, now_ns);
354 return frame;
355}
356
358 if (s) {
360 s->help = false;
361 s->show_logs = false;
362 s->dirty = true;
363 }
364}
365
366terminal_size_t ui_session_size(const ui_session_t *s) {
367 return s ? s->size : (terminal_size_t){.cols = 80, .rows = 24};
368}
369
371 if (s)
372 s->dirty = true;
373}
374
deferred_action_t action
Definition actions.c:77
#define SET_ERRNO(code,...)
⚙️ Common definitions, error codes, macros, and types shared throughout the application
int color_mode
Definition fft.c:25
unsigned char colors[(512 *256 *2) *3]
Definition fft.c:22
void frame_buffer_append(frame_buffer_t *buf, const char *str, size_t len)
Append raw bytes to the buffer.
void frame_buffer_reset(frame_buffer_t *buf)
Clear buffer contents while keeping allocation.
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.
void frame_buffer_clear_screen(frame_buffer_t *buf)
Append clear screen and home escape codes (\033[2J\033[H).
struct frame_buffer frame_buffer_t
Opaque frame buffer handle.
unsigned int uint32_t
Definition common.h:62
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
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
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
struct session_log_buffer session_log_buffer_t
Thread-safe fixed-size history ring for session-screen log lines.
session_log_buffer_t * log_get_session_log_buffer(void)
Get the currently registered session log buffer.
Definition log.c:2071
void log_clear_session_log_buffer(void)
Unregister the session log buffer.
Definition log.c:2057
void log_set_session_log_buffer(session_log_buffer_t *buf)
Register a session log buffer with the logger.
Definition log.c:2048
@ LOG_COLOR_FATAL
Definition log/log.h:153
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
#define options_terminal_settings()
Definition options.h:1296
@ COLOR_SETTING_FALSE
Force colors OFF: disable all colors and logging colors.
Definition options.h:249
keyboard_key_t
Options for interactive line editing.
Definition keyboard.h:53
@ KEY_ESCAPE
Escape key (ESC).
Definition keyboard.h:55
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
Definition string.c:284
struct ui_session ui_session_t
void invitation_render_logo(frame_buffer_t *buf, int cols, int frame, bool colors, bool compact)
Definition invitation.c:34
void ui_session_redraw(ui_session_t *s)
void ui_session_resize(ui_session_t *s, int cols, int rows)
#define TUI_SESSION_DETAIL_BUFFER_SIZE
int ui_session_select_screen(uint32_t visible_screens)
const char * ui_session_render(ui_session_t *s, const ui_session_view_t *view, uint64_t now_ns)
size_t ui_session_frame_length(const ui_session_t *s)
void ui_session_loading(ui_session_t *s, bool loading, const char *message)
void ui_session_destroy(ui_session_t *s)
bool ui_session_logs_key(keyboard_key_t key)
#define PRESENTATION_TIME_HISTORY_COUNT
asciichat_error_t ui_session_media(ui_session_t *s, const char *data, size_t length)
uint64_t ui_session_next_frame_ns(const ui_session_t *s)
ui_session_action_t ui_session_input(ui_session_t *s, keyboard_key_t key)
terminal_size_t ui_session_size(const ui_session_t *s)
void ui_session_clear(ui_session_t *s)
void ui_session_capture_logs(ui_session_t *s)
#define TUI_SESSION_OFFSET_TEXT_BUFFER_SIZE
const char * ui_session_tick(ui_session_t *s, uint64_t now_ns)
bool ui_session_is_covered(const ui_session_t *s)
void ui_session_log(ui_session_t *s, const char *message)
ui_session_t * ui_session_create(void)
void ui_session_logs_render(frame_buffer_t *output, terminal_size_t size, ui_session_t *s)
📝 Logging API with multiple log levels and terminal output control
session_log_buffer_t * log_get_history_buffer(void)
Definition log.c:565
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
asciichat_error_t log_search_init(void)
Initialize interactive grep subsystem.
Definition search.c:144
bool log_search_is_active(void)
Check if filtering is active.
Definition search.c:413
void log_search_exit_mode(bool accept)
Exit search mode.
Definition search.c:303
bool log_search_should_handle(int key)
Check if a key should be handled by grep module.
Definition search.c:422
asciichat_error_t log_search_handle_key(keyboard_key_t key)
Process keyboard input for grep.
Definition search.c:508
Interactive search filtering for terminal screens.
void ui_session_help_free(char *text)
char * ui_session_help_render(terminal_size_t size, bool has_media, bool browser, terminal_size_t *minimum)
ui_session_action_t ui_session_key(keyboard_key_t key, double *saved_volume)
session_log_buffer_t * session_log_buffer_create(void)
Create a new session log buffer instance.
void session_log_buffer_append(session_log_buffer_t *buf, const char *message)
Append a log message to the buffer.
void session_log_buffer_destroy(session_log_buffer_t *buf)
Cleanup session log buffer instance.
Thread-safe circular log buffer for session screens (status, splash).
void ui_session_splash_render(frame_buffer_t *buf, terminal_size_t term_size, void *user_data)
void ui_status_render_header(frame_buffer_t *buf, terminal_size_t term_size, void *user_data)
void terminal_screen_compose(frame_buffer_t *output, terminal_size_t size, const terminal_screen_config_t *config, session_log_buffer_t *logs)
const char * ui_status_log_controls(void)
Definition status.c:34
📊 Status screen display and management for server/discovery service modes
🔤 String Manipulation and Shell Escaping Utilities
For animation config, this is the live record that ties type, fps, speed, hidden_policy together....
Configuration for terminal screen rendering.
This is the handoff point for ui session. Callers fill size, frame, media, logs before passing it on;...
uint64_t last_render_ns
frame_buffer_t * frame
uint64_t presentation_times[120]
unsigned presentation_index
ui_splash_view_t splash
animation_t animation
terminal_size_t minimum
terminal_size_t size
terminal_size_t presented_size
session_log_buffer_t * logs
double saved_volume
frame_buffer_t * media
Status information for server/discovery service modes.
Definition status.h:38
size_t terminal_screen_logs_offset(void)
bool terminal_screen_logs_handle_key_sized(int key, terminal_size_t size)
Reusable "fixed header + scrolling logs" terminal screen abstraction.
void ui_too_small_render(frame_buffer_t *buffer, terminal_size_t actual, terminal_size_t minimum)
Definition too_small.c:38
bool ui_too_small(terminal_size_t actual, terminal_size_t minimum)
Definition too_small.c:10
terminal_size_t ui_controller_size(void)
#define UI_CONTROLLER_MAX_FRAME_BYTES
@ UI_SCREEN_COUNT
@ UI_SCREEN_LOGS
@ UI_SCREEN_MEDIA
@ UI_SCREEN_SPLASH
@ UI_SCREEN_MDNS
@ UI_SCREEN_HELP
void animation_destroy(animation_t *a)
asciichat_error_t animation_update(animation_t *a, uint64_t now, animation_sample_t *out)
void animation_reset(animation_t *a)
asciichat_error_t animation_init(animation_t *a, const char *name, animation_config_t config)
void animation_set_visible(animation_t *a, bool visible)