ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
src/common/session/display.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3
4#include <ascii-chat/otel.h>
19
24#include "session/display.h"
27#include "session/render.h"
29#include <ascii-chat/common.h>
31#include <ascii-chat/log/log.h>
36#include <libplatform/terminal.h>
37#include <libplatform/compiler.h>
38#include <libplatform/system.h>
39#include <libplatform/terminal.h>
58#include <libplatform/cpu.h>
59
60#include <string.h>
61#include <stdio.h>
62#include <libplatform/atomic.h>
63#include <stdlib.h>
64
65/* ============================================================================
66 * Session Display Context Structure
67 * ============================================================================ */
68
142
143// Display conversion and submission have a single owner, so scratch images can be reused.
144static image_t *display_image_workspace(image_t **workspace, int width, int height) {
145 if (*workspace && ((*workspace)->w != width || (*workspace)->h != height)) {
146 image_destroy(*workspace);
147 *workspace = NULL;
148 }
149 if (!*workspace)
150 *workspace = image_new((size_t)width, (size_t)height);
151 return *workspace;
152}
153
154/* ============================================================================
155 * Internal Helper Functions
156 * ============================================================================ */
157
166/* ============================================================================
167 * Session Display Lifecycle Functions
168 * ============================================================================ */
169
171 if (!ctx) {
172 return SET_ERRNO(ERROR_INVALID_PARAM, "Display context is NULL");
173 }
174
175 const char *render_file_opt = GET_OPTION(render_file);
176 if ((!render_file_opt || !render_file_opt[0]) && GET_OPTION(render_kitty))
177 render_file_opt = "--kitty";
178 if (!render_file_opt || render_file_opt[0] == '\0') {
179 return ASCIICHAT_OK;
180 }
181 if (ctx->render_file) {
182 return ASCIICHAT_OK;
183 }
184
185 uint32_t encoder_fps = fps;
186 if (encoder_fps == 0) {
187 encoder_fps = (uint32_t)GET_OPTION(fps);
188 }
189 if (encoder_fps == 0) {
190 encoder_fps = 60;
191 }
192
193 int width = (int)GET_OPTION(width);
194 int height = (int)GET_OPTION(height);
195 ctx->render_fps = encoder_fps;
196
197 log_info("render-file: Initializing encoder after media timing selection: path='%s', grid=%dx%d, fps=%u",
198 render_file_opt, width, height, encoder_fps);
200 render_file_create(render_file_opt, width, height, (int)encoder_fps, GET_OPTION(render_theme), &ctx->render_file);
201 if (err != ASCIICHAT_OK) {
203 if (HAS_ERRNO(&error))
204 log_error("render-file: %s", error.context_message);
205 log_error("render-file: Failed to create encoder %s", asciichat_error_format(NULL, err));
206 return err;
207 }
208
209 log_info("render-file: Encoder initialized successfully at %u FPS", encoder_fps);
210 return ASCIICHAT_OK;
211}
212
213static void display_sync_entries(session_display_ctx_t *ctx, bool add) {
214 if (add)
215 NAMED_REGISTER_ATOMIC(&ctx->first_frame, "display.first_frame", NULL);
216 else
218 if (add)
219 NAMED_REGISTER_ATOMIC(&ctx->output_failed, "display.output_failed", NULL);
220 else
222}
223
225 ASCII_OTEL_SCOPE(SESSION_DISPLAY_CREATE);
226 if (GET_OPTION(zalgo) > 0 && (GET_OPTION(force_utf8) == UTF8_SETTING_FALSE ||
227 (GET_OPTION(force_utf8) == UTF8_SETTING_AUTO && terminal_is_stdout_tty() &&
228 !detect_terminal_capabilities(options_terminal_settings()).utf8_support))) {
229 SET_ERRNO(ERROR_USAGE, "--zalgo requires UTF-8 terminal output (on Windows, run chcp 65001 first)");
230 return NULL;
231 }
232 // Auto-create config from command-line options if NULL
233 session_display_config_t auto_config = {0};
234 if (!config) {
235 auto_config.snapshot_mode = GET_OPTION(snapshot_mode);
236 auto_config.palette_type = GET_OPTION(palette_type);
237 auto_config.custom_palette = GET_OPTION(palette_custom_set) ? GET_OPTION(palette_custom) : NULL;
238 auto_config.color_mode = TERM_COLOR_AUTO;
239 config = &auto_config;
240 }
241
242 // Check if we should exit before starting initialization
243 if (config->should_exit_callback && config->should_exit_callback(config->callback_data)) {
244 return NULL;
245 }
246
247 // Allocate context
249
250 // Store configuration
251 ctx->snapshot_mode = config->snapshot_mode;
252 ctx->palette_type = config->palette_type;
254 ctx->audio_ctx = config->audio_ctx;
255 ctx->render_fps = config->render_fps;
256 atomic_store_bool(&ctx->first_frame, true);
257 atomic_store_bool(&ctx->output_failed, false);
258
259 // Get TTY info for direct terminal access
260 ctx->tty_info = get_current_tty();
261
262 // Presentation follows stdout. Opening a separate controlling terminal can
263 // fail under ConPTY even though stdout is an interactive console.
264 ctx->has_tty = terminal_is_stdout_tty();
265 const char *stdout_output = GET_OPTION(render_file);
266 bool owns_stdout = GET_OPTION(render_kitty) || strcmp(stdout_output, "--kitty") == 0 ||
267 strcmp(stdout_output, "-") == 0 || strcmp(stdout_output, "pipe:") == 0;
268 if (owns_stdout)
269 ctx->has_tty = false;
270 if (ctx->has_tty) {
271 // Retain the terminal before decoder/audio workers redirect process stdio
272 // through LOG_IO. The first media submission must remain interactive too.
273 int output = platform_dup(STDOUT_FILENO);
274 if (output >= 0 && platform_isatty(output)) {
275 if (ctx->tty_info.owns_fd && ctx->tty_info.fd >= 0)
276 platform_close(ctx->tty_info.fd);
277 ctx->tty_info.fd = output;
278 ctx->tty_info.owns_fd = true;
279 } else if (output >= 0) {
280 platform_close(output);
281 }
282 }
283 if (ctx->has_tty && (ctx->tty_info.fd < 0 || !platform_isatty(ctx->tty_info.fd))) {
284 if (ctx->tty_info.owns_fd && ctx->tty_info.fd >= 0)
285 platform_close(ctx->tty_info.fd);
286 ctx->tty_info.fd = STDOUT_FILENO;
287 ctx->tty_info.owns_fd = false;
288 }
289
290 // In piped mode, force all logs to stderr to prevent frame data corruption
291 if (terminal_should_force_stderr()) {
293 // Disable buffering for stdout to ensure frames appear immediately
294 // Each frame ends with newline, so unbuffered output is critical for real-time display
295 (void)setvbuf(stdout, NULL, _IONBF, 0);
296 }
297
298 // Also disable buffering for TTY mode to ensure smooth 40+ FPS animation
299 setvbuf(stdout, NULL, _IONBF, 0);
300
301 // Detect terminal capabilities
302 ctx->caps = detect_terminal_capabilities(options_terminal_settings());
303
304 // Set wants_padding based on terminal output mode
305 // Enable padding for all rendering modes (including snapshot) to center output
306 // The padding adds newlines at the top, which is useful for visual centering
307 bool is_snapshot_mode = config->snapshot_mode;
308 ctx->caps.wants_padding = true;
309
310 // Calculate pad_height: when padding is enabled, add top padding for centering
311 // Halfblock mode uses 2 source rows per output row, so padding is halved
312 if (ctx->caps.wants_padding) {
313 ctx->caps.pad_height = 5; // Halfblock needs half the padding (10 / 2 = 5)
314 } else {
315 ctx->caps.pad_height = 0;
316 }
317
318 log_debug("Padding mode: wants_padding=%d (snapshot=%d, has_tty=%d, stdin_tty=%d, stdout_tty=%d), pad_height=%zu",
319 ctx->caps.wants_padding, is_snapshot_mode, ctx->has_tty, terminal_is_stdin_tty(), terminal_is_stdout_tty(),
320 ctx->caps.pad_height);
321
322 // Apply color mode override if specified
323 if (config->color_mode != TERM_COLOR_AUTO) {
324 ctx->caps.color_level = config->color_mode;
325 }
326
327 // Apply command-line overrides
328 ctx->caps = apply_color_mode_override(ctx->caps, options_terminal_settings());
329
330 // Initialize palette
331 int palette_result = initialize_client_palette(config->palette_type, config->custom_palette, ctx->palette_chars,
332 &ctx->palette_len, ctx->luminance_palette);
333 if (palette_result != ASCIICHAT_OK) {
334 log_warn("Failed to initialize palette, using default");
335 // Fall back to standard palette
337 ctx->luminance_palette);
338 }
339
340 // Pre-warm UTF-8 palette cache during initialization (not during rendering)
341 // This is expensive on first use (creates lookup tables), so warm it up now to avoid frame lag
342 // This function is imported from lib/video/simd/common.h
343 (void)get_utf8_palette_cache((const char *)ctx->palette_chars);
344 log_debug("UTF-8 palette cache pre-warmed during display initialization");
345
346 // Initialize ANSI fast lookup tables based on terminal capabilities
347 if (ctx->caps.color_level == TERM_COLOR_TRUECOLOR) {
349 } else if (ctx->caps.color_level == TERM_COLOR_256) {
351 } else if (ctx->caps.color_level == TERM_COLOR_16) {
353 }
354 // TERM_COLOR_MONO requires no init
355
356 // Initialize ASCII output system if we have a TTY
357 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
358 ascii_write_init(ctx->tty_info.fd, false);
359 }
360
361 // Initialize digital rain effect if enabled
362 if (GET_OPTION(matrix_rain)) {
363 // Get terminal dimensions for grid size
364 unsigned short int width_us = terminal_get_effective_width(GET_OPTION(width), 80);
365 unsigned short int height_us = terminal_get_effective_height(GET_OPTION(height), 24);
366
367 int width = (int)width_us;
368 int height = (int)height_us;
369
371 if (ctx->digital_rain) {
372 // Set color from color filter if active
373 color_filter_t filter = GET_OPTION(color_filter);
375 log_info("Digital rain effect enabled: %dx%d grid", width, height);
376 } else {
377 log_warn("Failed to initialize digital rain effect");
378 }
379 ctx->last_frame_time_ns = time_get_ns();
380 }
381
384 return NULL;
385 }
386
387 display_sync_entries(ctx, true);
388
389 // Initialize render-file unless media setup needs to determine its timing first.
390 const char *render_file_opt = GET_OPTION(render_file);
391 log_debug("render-file opt='%s' (len=%zu), deferred=%d", render_file_opt ? render_file_opt : "(null)",
392 render_file_opt ? strlen(render_file_opt) : 0, config->defer_render_file);
393 if (!config->defer_render_file) {
395 if (render_err != ASCIICHAT_OK) {
396 log_error("render-file: Initialization failed %s", asciichat_error_format(NULL, render_err));
398 bool has_error = HAS_ERRNO(&error);
400 SET_ERRNO(render_err, "%s", has_error ? error.context_message : "Render-file initialization failed");
401 return NULL;
402 }
403 }
404
405 ctx->initialized = true;
406 if (ctx->has_tty && !ctx->snapshot_mode) {
407 const char initial[] = "\033[H";
409 (terminal_size_t){.cols = UI_MEDIA_MIN_COLS, .rows = UI_MEDIA_MIN_ROWS},
410 initial, sizeof(initial) - 1);
411 }
412
413 return ctx;
414}
415
416void session_display_begin_drain(session_display_ctx_t *ctx, uint64_t total, bool total_known) {
417 if (ctx && ctx->render_file) {
419 render_file_begin_drain(ctx->render_file, total, total_known);
420 }
421}
422
424 if (!ctx || !ctx->render_file)
425 return ASCIICHAT_OK;
427 ctx->render_file = NULL;
428 return err;
429}
430
432 if (!ctx) {
433 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
434 return;
435 }
436
438
441
442 // Cleanup ASCII rendering if we had a TTY
443 // Don't reset terminal in snapshot mode to preserve the rendered output
444 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
446 }
447
448 // Close the controlling terminal if we opened it
449 if (ctx->tty_info.owns_fd && ctx->tty_info.fd >= 0) {
450 (void)platform_close(ctx->tty_info.fd);
451 ctx->tty_info.fd = -1;
452 ctx->tty_info.owns_fd = false;
453 }
454
455 // Cleanup digital rain effect
456 if (ctx->digital_rain) {
458 ctx->digital_rain = NULL;
459 }
460
464 ctx->resize_workspace = ctx->flip_workspace = ctx->filter_workspace = NULL;
469 ctx->initialized = false;
470
471 // Clear the cached display context in splash state to prevent use-after-free
472 // when worker threads try to access the display after it's been freed
474
475 display_sync_entries(ctx, false);
476 SAFE_FREE(ctx);
477}
478
480 if (!ctx) {
481 SET_ERRNO(ERROR_INVALID_PARAM, "Display context is NULL");
482 return;
483 }
484
485 ctx->stdin_reader = (terminal_fd_reader_t *)reader;
486 log_debug("stdin_reader set");
487}
488
489/* ============================================================================
490 * Session Display Query Functions
491 * ============================================================================ */
492
494 if (!ctx || !ctx->initialized) {
495 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
496 return false;
497 }
498 return ctx->has_tty;
499}
500
501const terminal_capabilities_t *session_display_get_caps(session_display_ctx_t *ctx) {
502 if (!ctx || !ctx->initialized) {
503 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
504 return NULL;
505 }
506 return &ctx->caps;
507}
508
510 if (!ctx || !ctx->initialized) {
511 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
512 return NULL;
513 }
514 return ctx->palette_chars;
515}
516
518 if (!ctx || !ctx->initialized) {
519 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
520 return 0;
521 }
522 return ctx->palette_len;
523}
524
526 if (!ctx || !ctx->initialized) {
527 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
528 return NULL;
529 }
530 return ctx->luminance_palette;
531}
532
534 if (!ctx || !ctx->initialized) {
535 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
536 return -1;
537 }
538 return ctx->tty_info.fd;
539}
540
542 if (!ctx || !ctx->initialized) {
543 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
544 return NULL;
545 }
546
547 return ctx->stdin_reader;
548}
549
551 if (!ctx)
552 return 0;
553 return ctx->render_fps;
554}
555
557 if (!ctx)
558 return;
559 ctx->render_fps = fps;
560 if (fps > 0) {
561 log_debug("Updated render FPS to %u (for correct video duration)", fps);
562 }
563}
564
566 if (!ctx) {
567 return false;
568 }
569 // Return true if first_frame flag is false (meaning first frame has been rendered)
570 return !atomic_load_bool(&ctx->first_frame);
571}
572
574 if (!ctx) {
575 return;
576 }
577 // Reset first_frame flag to true so splash screen can be shown again on next render
578 atomic_store_bool(&ctx->first_frame, true);
579}
580
582 if (!ctx) {
583 return false;
584 }
585 return ctx->render_file != NULL;
586}
587
588/* ============================================================================
589 * Session Display ASCII Conversion Functions
590 * ============================================================================ */
591
592static char *convert_to_ascii(session_display_ctx_t *ctx, const image_t *image, bool reuse) {
593 uint64_t stats_start = time_get_ns();
594 if (!ctx) {
595 SET_ERRNO(ERROR_INVALID_PARAM, "ctx is NULL");
596 return NULL;
597 }
598
599 if (!ctx->initialized) {
600 SET_ERRNO(ERROR_INVALID_STATE, "ctx not initialized");
601 return NULL;
602 }
603
604 if (!image) {
605 SET_ERRNO(ERROR_INVALID_PARAM, "image is NULL");
606 return NULL;
607 }
608
609 // Read the renderer and palette from the same committed options revision.
610 options_snapshot_t snapshot;
611 if (options_read_snapshot(&snapshot) != ASCIICHAT_OK) {
612 return NULL;
613 }
614 const options_t *values = &snapshot.values;
615 unsigned short int width = values->width;
616 unsigned short int height = values->height;
617 bool stretch = values->stretch;
618 terminal_capabilities_t caps_copy = ctx->caps;
619 caps_copy.render_mode = (terminal_render_mode_t)values->render_mode;
620 caps_copy.text_renderer = values->renderer;
621 caps_copy.stabilize = values->stabilize;
622 caps_copy.palette_explicit = values->palette_explicit;
623 terminal_color_mode_t color_mode_opt = (terminal_color_mode_t)values->color_mode;
624 const palette_def_t *builtin = get_builtin_palette(values->palette_type);
625 const char *selected_palette = !values->palette_explicit ? ctx->palette_chars
626 : values->palette_type == PALETTE_CUSTOM ? values->palette_custom
627 : builtin ? builtin->chars
628 : ctx->palette_chars;
629 char palette_chars[ASCII_PALETTE_BUFFER_SIZE];
630 SAFE_STRNCPY(palette_chars, selected_palette, sizeof(palette_chars));
631 bool preserve_aspect_ratio = !stretch;
632
633 // Determine if we should apply flip_x and flip_y
634 bool flip_x_enabled = GET_OPTION(flip_x);
635 bool flip_y_enabled = GET_OPTION(flip_y);
636
637 color_filter_t color_filter = GET_OPTION(color_filter);
638 if (ctx->stabilizer_filter_initialized && ctx->previous_stabilizer_filter != color_filter) {
641 }
642 ctx->previous_stabilizer_filter = color_filter;
644
645 // Handle dynamic matrix rain effect toggle
646 bool matrix_rain_enabled = GET_OPTION(matrix_rain);
647 if (matrix_rain_enabled && !ctx->digital_rain) {
648 // Initialize digital rain if it's now enabled but wasn't before
649 unsigned short int width_us = terminal_get_effective_width(GET_OPTION(width), 80);
650 unsigned short int height_us = terminal_get_effective_height(GET_OPTION(height), 24);
651 int width_for_rain = (int)width_us;
652 int height_for_rain = (int)height_us;
653 ctx->digital_rain = digital_rain_init(width_for_rain, height_for_rain);
654 if (ctx->digital_rain) {
656 log_info("Matrix rain effect: enabled");
657 }
658 } else if (!matrix_rain_enabled && ctx->digital_rain) {
659 // Disable digital rain if it's now disabled but was enabled before
661 ctx->digital_rain = NULL;
662 log_info("Matrix rain effect: disabled");
663 }
664
665 // Make a mutable copy of terminal capabilities for ascii_convert_with_capabilities
666 if (color_mode_opt != TERM_COLOR_AUTO) {
667 caps_copy.color_level = color_mode_opt;
668 }
669
670 // MEASURE EVERY OPERATION - Debug systematic timing
671 uint64_t t_flip_start = time_get_ns();
672
673 // Apply horizontal and/or vertical flips if requested
674 image_t *flipped_image = NULL;
675 const image_t *display_image = image;
676
677 if ((flip_x_enabled || flip_y_enabled) && image->w > 1 && image->h > 1 && image->pixels) {
678 uint64_t t_flip_alloc_start = time_get_ns();
679 flipped_image = ctx->render_file ? image_new(image->w, image->h)
680 : display_image_workspace(&ctx->flip_workspace, image->w, image->h);
681 uint64_t t_flip_alloc_end = time_get_ns();
682
683 if (flipped_image) {
684 uint64_t t_flip_memcpy_start = time_get_ns();
685 // OPTIMIZATION: Copy entire image first (sequential memory access - cache-friendly)
686 memcpy(flipped_image->pixels, image->pixels, (size_t)image->w * (size_t)image->h * sizeof(rgb_pixel_t));
687 uint64_t t_flip_memcpy_end = time_get_ns();
688
689 uint64_t t_flip_reverse_start = time_get_ns();
690
691 // Apply horizontal flip (X-axis)
692 if (flip_x_enabled) {
693#if SIMD_SUPPORT_NEON
694 if (platform_cpu_features() & CPU_FEATURE_NEON) {
695 image_flip_horizontal_neon(flipped_image);
696 } else
697#endif
698 {
699 for (int y = 0; y < image->h; y++) {
700 rgb_pixel_t *row = &flipped_image->pixels[y * image->w];
701 for (int x = 0; x < image->w / 2; x++) {
702 rgb_pixel_t temp = row[x];
703 row[x] = row[image->w - 1 - x];
704 row[image->w - 1 - x] = temp;
705 }
706 }
707 }
708 }
709
710 // Apply vertical flip (Y-axis)
711 if (flip_y_enabled) {
712 for (int y = 0; y < image->h / 2; y++) {
713 rgb_pixel_t *top_row = &flipped_image->pixels[y * image->w];
714 rgb_pixel_t *bottom_row = &flipped_image->pixels[(image->h - 1 - y) * image->w];
715 for (int x = 0; x < image->w; x++) {
716 rgb_pixel_t temp = top_row[x];
717 top_row[x] = bottom_row[x];
718 bottom_row[x] = temp;
719 }
720 }
721 }
722
723 uint64_t t_flip_reverse_end = time_get_ns();
724 display_image = flipped_image;
725
726 log_dev("TIMING_FLIP: alloc=%llu us, memcpy=%llu us, flip=%llu us (x=%d, y=%d)",
727 (t_flip_alloc_end - t_flip_alloc_start) / 1000, (t_flip_memcpy_end - t_flip_memcpy_start) / 1000,
728 (t_flip_reverse_end - t_flip_reverse_start) / 1000, flip_x_enabled, flip_y_enabled);
729 }
730 }
731 uint64_t t_flip_end = time_get_ns();
732
733 uint64_t t_filter_start = time_get_ns();
734
735 // Apply color filter to RGB pixels before ASCII conversion
736 // Rainbow filter is handled separately via ANSI color replacement
737 image_t *filtered_image = NULL;
738 if (color_filter != COLOR_FILTER_NONE && color_filter != COLOR_FILTER_RAINBOW) {
739 // Create a copy to avoid modifying the original
740 filtered_image = ctx->render_file
741 ? image_new(display_image->w, display_image->h)
742 : display_image_workspace(&ctx->filter_workspace, display_image->w, display_image->h);
743 if (filtered_image && filtered_image->pixels && display_image->pixels) {
744 memcpy(filtered_image->pixels, display_image->pixels,
745 (size_t)display_image->w * (size_t)display_image->h * sizeof(rgb_pixel_t));
746
747 // Apply filter to the copy
748 int stride = (int)display_image->w * 3;
749 float time_seconds = (float)t_filter_start / (float)NS_PER_SEC_INT;
750 apply_color_filter((uint8_t *)filtered_image->pixels, (uint32_t)display_image->w, (uint32_t)display_image->h,
751 (uint32_t)stride, color_filter, time_seconds);
752 }
753 }
754
755 const image_t *ascii_input_image = filtered_image ? filtered_image : display_image;
756 uint64_t t_filter_end = time_get_ns();
757
758 uint64_t t_convert_start = time_get_ns();
759 // Recording converts concurrently on its encoder thread and retains owned output.
760 outbuf_t *output = reuse && !ctx->render_file && color_filter == COLOR_FILTER_NONE && !matrix_rain_enabled
761 ? &ctx->ascii_workspace
762 : NULL;
763 // Recording and live display run on separate threads and own separate histories.
764 bool recording = ctx->render_file && !reuse;
765 ascii_stabilizer_t *history = recording ? &ctx->recording_stabilizer : &ctx->stabilizer;
766 if (recording && ctx->render_fps)
767 history->fixed_step_ns = NS_PER_SEC_INT / ctx->render_fps;
769 ascii_input_image, width, height, &caps_copy, preserve_aspect_ratio, stretch, palette_chars, false,
770 ctx->render_file ? NULL : &ctx->resize_workspace, output, caps_copy.stabilize ? history : NULL);
771 if (!caps_copy.stabilize)
772 ascii_stabilizer_reset(history);
773 uint64_t t_convert_end = time_get_ns();
774
775 // Apply rainbow color filter to ANSI output by replacing RGB values
776 // This preserves character selection while applying the filter colors
777 if (result && color_filter == COLOR_FILTER_RAINBOW) {
778 uint64_t t_color_replace_start = time_get_ns();
779 float time_seconds = (float)t_filter_start / (float)NS_PER_SEC_INT;
780
781 char *rainbow_result = rainbow_replace_ansi_colors(result, time_seconds);
782 if (rainbow_result) {
783 SAFE_FREE(result);
784 result = rainbow_result;
785 }
786
787 uint64_t t_color_replace_end = time_get_ns();
788 char color_replace_str[TIME_DURATION_BUFFER_SIZE];
789 time_pretty(t_color_replace_end - t_color_replace_start, -1, color_replace_str, sizeof(color_replace_str));
790 log_dev("COLOR_REPLACE: %s", color_replace_str);
791 }
792
793 // Apply digital rain effect if enabled
794 if (result && ctx->digital_rain) {
795 uint64_t t_rain_start = time_get_ns();
796 uint64_t current_time_ns = t_rain_start;
797 float delta_time = (float)(current_time_ns - ctx->last_frame_time_ns) / (float)NS_PER_SEC_INT;
798 ctx->last_frame_time_ns = current_time_ns;
799
800 // Update digital rain color from current filter (allows live filter changes)
801 color_filter_t current_filter = GET_OPTION(color_filter);
803
804 char *rain_result = digital_rain_apply(ctx->digital_rain, result, delta_time);
805 if (rain_result) {
806 SAFE_FREE(result);
807 result = rain_result;
808 }
809
810 uint64_t t_rain_end = time_get_ns();
811 char rain_str[TIME_DURATION_BUFFER_SIZE];
812 time_pretty(t_rain_end - t_rain_start, -1, rain_str, sizeof(rain_str));
813 log_dev("DIGITAL_RAIN: Effect applied (%s)", rain_str);
814 }
815
816 uint64_t validation_started_ns = time_get_ns();
817 // Validate the completed local frame using the same policy as received text.
818 // Wide palettes use the existing cell composer before the rectangular check.
819 bool explicit_palette = caps_copy.palette_explicit;
820 bool glyph_match = caps_copy.text_renderer == ASCII_RENDERER_GLYPH &&
821 caps_copy.render_mode != RENDER_MODE_HALF_BLOCK && width > 0 && height > 0 &&
822 width <= IMAGE_MAX_WIDTH / 8 && height <= IMAGE_MAX_HEIGHT / 8 &&
823 (!explicit_palette || ascii_glyph_palette_supported(palette_chars));
824 if (result && !glyph_match && (size_t)utf8_display_width(palette_chars) != utf8_char_count(palette_chars)) {
825 ascii_frame_source_t source = {.frame_data = result, .frame_size = strlen(result)};
826 size_t normalized_size;
827 char *rectangular = ascii_create_grid(&source, 1, width, height, &normalized_size);
829 result = rectangular;
830 }
831 if (result) {
832 acip_render_settings_t rendering = {.caps = caps_copy};
833 const char *contract_palette =
834 glyph_match && !explicit_palette
835 ? " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
836 : palette_chars;
837 SAFE_STRNCPY(rendering.palette, contract_palette, sizeof(rendering.palette));
839 if (session_frame_validate_contract(result, strlen(result), width, height, &rendering, effects) != ASCIICHAT_OK) {
841 result = NULL;
842 }
843 }
844
845 uint64_t validation_finished_ns = time_get_ns();
846 log_debug_every(US_PER_SEC_INT, "DISPLAY_STAGE: conversion_ms=%.2f validation_ms=%.2f",
847 (double)(t_convert_end - t_convert_start) / NS_PER_MS_INT,
848 (double)(validation_finished_ns - validation_started_ns) / NS_PER_MS_INT);
849 uint64_t t_cleanup_start = time_get_ns();
850 // Scratch images remain owned by the display until resize or teardown.
851 if (ctx->render_file) {
852 image_destroy(filtered_image);
853 image_destroy(flipped_image);
854 }
855 uint64_t t_cleanup_end = time_get_ns();
856
857 // Log total breakdown with actual measured times
859 "CONVERT_TIMING: flip=%llu us, filter=%llu us, convert=%llu us, cleanup=%llu us, TOTAL=%llu us",
860 (t_flip_end - t_flip_start) / 1000, (t_filter_end - t_filter_start) / 1000,
861 (t_convert_end - t_convert_start) / 1000, (t_cleanup_end - t_cleanup_start) / 1000,
862 (t_cleanup_end - t_flip_start) / 1000);
863
866 return result;
867}
868
869/* ============================================================================
870 * Session Display Rendering Functions
871 * ============================================================================ */
872
873static char *session_display_create_visualization_frame(session_display_ctx_t *ctx) {
874 unsigned int width = terminal_get_effective_width(GET_OPTION(width), 80);
875 unsigned int height = terminal_get_effective_height(GET_OPTION(height), 24);
876 bool use_color = ctx->caps.color_level == TERM_COLOR_TRUECOLOR && GET_OPTION(color) != COLOR_SETTING_FALSE;
879 audio_source_t selected_audio = GET_OPTION(audio_source);
881 if (selected_audio == AUDIO_SOURCE_MEDIA)
882 visual_source = AUDIO_VISUALIZATION_SOURCE_MEDIA;
883 else if (selected_audio == AUDIO_SOURCE_MIC)
884 visual_source = AUDIO_VISUALIZATION_SOURCE_MIC;
885 else if (selected_audio == AUDIO_SOURCE_CALL)
886 visual_source = AUDIO_VISUALIZATION_SOURCE_REMOTE;
887 char *text = GET_OPTION(fft)
888 ? audio_visualization_render_fft(width, height, visual_source, use_color, color_mode,
889 GET_OPTION(flip_x), GET_OPTION(flip_y))
890 : audio_visualization_render_waveform(width, height, visual_source, use_color, color_mode,
891 GET_OPTION(flip_x), GET_OPTION(flip_y));
892 if (text && session_frame_validate(text, strlen(text), width, height) != ASCIICHAT_OK) {
893 SAFE_FREE(text);
894 return NULL;
895 }
896 return text;
897}
898
899void session_display_render_frame(session_display_ctx_t *ctx, const char *frame_data) {
900 // Legacy function: shim that calls both new split functions for backward compatibility
901 // This maintains API compatibility with existing code while delegating to specialized functions
902
903 if (!ctx || !frame_data) {
904 SET_ERRNO(ERROR_INVALID_PARAM, "Display context or frame data is NULL");
905 return;
906 }
907
908 // Write the selected visualization to the render file as well as the terminal.
909 char *visualization_frame = NULL;
910 const char *render_frame = frame_data;
911 if (GET_OPTION(waveform) || GET_OPTION(fft)) {
912 visualization_frame = session_display_create_visualization_frame(ctx);
913 if (visualization_frame)
914 render_frame = visualization_frame;
915 }
916
917 // Write ASCII to terminal (main thread output path)
918 if (ctx && ctx->render_file) {
920 asciichat_error_t err = render_file_write_frame(ctx->render_file, render_frame, time_get_ns());
921 if (err != ASCIICHAT_OK)
922 log_warn_every(5 * US_PER_SEC_INT, "Recording received frame failed: %s", asciichat_error_format(NULL, err));
923 }
924 SAFE_FREE(visualization_frame);
925 session_display_write_ascii(ctx, frame_data);
926}
927
929 return convert_to_ascii(ctx, image, false);
930}
931
933 ASCII_OTEL_SCOPE(SESSION_DISPLAY_CONVERT_TO_ASCII_REUSE);
934 return convert_to_ascii(ctx, image, true);
935}
936
938 if (ctx)
939 ascii_stabilizer_reset(recording ? &ctx->recording_stabilizer : &ctx->stabilizer);
940}
941
943 if (!ctx || ascii != ctx->ascii_workspace.buf)
944 SAFE_FREE(ascii);
945}
946
947void session_display_write_ascii_frame(session_display_ctx_t *ctx, const char *ascii, bool fresh) {
948 ASCII_OTEL_SCOPE(SESSION_DISPLAY_WRITE_ASCII_FRAME);
949 if (!ctx || !ctx->initialized) {
950 SET_ERRNO(ERROR_INVALID_PARAM, "Display context is NULL or uninitialized");
951 return;
952 }
953
954 // Shutdown is asynchronous; queued frames must not retry a failed output.
955 if (atomic_load_bool(&ctx->output_failed)) {
956 return;
957 }
958
959 if (!ascii) {
960 SET_ERRNO(ERROR_INVALID_PARAM, "ASCII data is NULL");
961 return;
962 }
963
964 // Re-evaluate color_level on every frame to pick up live color_mode changes
965 terminal_color_mode_t color_mode = (terminal_color_mode_t)GET_OPTION(color_mode);
966 if (color_mode != TERM_COLOR_AUTO) {
967 ctx->caps.color_level = color_mode;
968 }
969
970 // Suppress frame rendering when help screen is active
971
972 bool visualization_enabled = GET_OPTION(waveform) || GET_OPTION(fft);
973 char *visualization_frame = visualization_enabled ? session_display_create_visualization_frame(ctx) : NULL;
974 if (visualization_frame)
975 ascii = visualization_frame;
976
977 // Leave room for cursor controls when passing the frame to the presenter.
978 const size_t max_frame_len = UI_CONTROLLER_MAX_FRAME_BYTES - 7;
979 size_t frame_len = strnlen(ascii, max_frame_len);
980 if (frame_len == max_frame_len) {
981 SET_ERRNO(ERROR_BUFFER_FULL, "ASCII frame exceeds the presenter limit");
982 SAFE_FREE(visualization_frame);
983 return;
984 }
985 if (frame_len == 0) {
986 SET_ERRNO(ERROR_INVALID_PARAM, "ASCII data is empty");
987 SAFE_FREE(visualization_frame);
988 return;
989 }
990
991 /* Inspect the payload before any terminal presentation. This is bounded
992 * diagnostics: it records shape/control-byte anomalies and a short tail,
993 * never the full webcam frame. */
994 {
995 size_t lines = 0, current = 0, min_line = SIZE_MAX, max_line = 0, last_line = 0;
996 unsigned control_bytes = 0, escape_bytes = 0;
997 for (size_t i = 0; i < frame_len; ++i) {
998 unsigned char byte = (unsigned char)ascii[i];
999 if (byte == '\n') {
1000 min_line = MIN(min_line, current);
1001 max_line = MAX(max_line, current);
1002 last_line = current;
1003 ++lines;
1004 current = 0;
1005 } else {
1006 current++;
1007 if (byte < 32 && byte != '\r' && byte != '\t')
1008 ++control_bytes;
1009 if (byte == 0x1b)
1010 ++escape_bytes;
1011 }
1012 }
1013 if (current) {
1014 min_line = MIN(min_line, current);
1015 max_line = MAX(max_line, current);
1016 last_line = current;
1017 }
1018 char tail[64] = {0};
1019 size_t tail_start = frame_len > 24 ? frame_len - 24 : 0;
1020 size_t tail_used = 0;
1021 for (size_t i = tail_start; i < frame_len && tail_used + 4 < sizeof(tail); ++i)
1022 tail_used += (size_t)safe_snprintf(tail + tail_used, sizeof(tail) - tail_used, "%02X ", (unsigned char)ascii[i]);
1023
1024 // Also capture the actual glyphs in the final row. Byte/line counts alone
1025 // cannot distinguish a bad terminal write from a bad rendered frame.
1026 size_t row_start = 0;
1027 for (size_t i = 0; i < frame_len; ++i)
1028 if (ascii[i] == '\n')
1029 row_start = i + 1;
1030 char last_row[192] = {0};
1031 size_t row_used = 0;
1032 for (size_t i = row_start; i < frame_len && row_used + 5 < sizeof(last_row); ++i) {
1033 unsigned char byte = (unsigned char)ascii[i];
1034 if (byte == 0x1b) {
1035 ++i;
1036 while (i < frame_len && !((ascii[i] >= '@' && ascii[i] <= 'Z') || (ascii[i] >= 'a' && ascii[i] <= 'z')))
1037 ++i;
1038 continue;
1039 }
1040 if (byte >= 32 && byte < 127)
1041 last_row[row_used++] = (char)byte;
1042 else if (byte == '\r' || byte == '\t')
1043 last_row[row_used++] = ' ';
1044 else
1045 row_used += (size_t)safe_snprintf(last_row + row_used, sizeof(last_row) - row_used, "\\x%02X", byte);
1046 }
1048 "FRAME_DATA: bytes=%zu lines=%zu line_bytes=%zu..%zu last=%zu controls=%u escapes=%u last_row=\"%s\" tail=%s",
1049 frame_len, lines, min_line == SIZE_MAX ? 0 : min_line, max_line, last_line, control_bytes,
1050 escape_bytes, last_row, tail);
1051 }
1052
1053 // Apply digital rain effect if enabled
1054 char *display_frame = (char *)ascii;
1055 char *rain_result = NULL;
1056 if (ctx->digital_rain && !visualization_enabled) {
1057 uint64_t t_rain_start = time_get_ns();
1058 uint64_t current_time_ns = t_rain_start;
1059 float delta_time = (float)(current_time_ns - ctx->last_frame_time_ns) / (float)NS_PER_SEC_INT;
1060 ctx->last_frame_time_ns = current_time_ns;
1061
1062 // Update digital rain color from current filter
1063 color_filter_t current_filter = GET_OPTION(color_filter);
1065
1066 rain_result = digital_rain_apply(ctx->digital_rain, (char *)ascii, delta_time);
1067 if (rain_result) {
1068 display_frame = rain_result;
1069 frame_len = strnlen(rain_result, max_frame_len);
1070 if (frame_len == max_frame_len) {
1071 SET_ERRNO(ERROR_BUFFER_FULL, "Digital rain frame exceeds the presenter limit");
1072 SAFE_FREE(rain_result);
1073 SAFE_FREE(visualization_frame);
1074 return;
1075 }
1076
1077 uint64_t t_rain_end = time_get_ns();
1078 char rain_str[TIME_DURATION_BUFFER_SIZE];
1079 time_pretty(t_rain_end - t_rain_start, -1, rain_str, sizeof(rain_str));
1080 log_debug("DIGITAL_RAIN (write_ascii): Effect applied (%s)", rain_str);
1081 }
1082 }
1083
1084 // Binary video owns stdout in every terminal mode.
1085 const char *output = GET_OPTION(render_file);
1086 bool binary_stdout = ctx->render_file && (strcmp(output, "-") == 0 || strcmp(output, "pipe:") == 0 ||
1087 strcmp(output, "--kitty") == 0 || GET_OPTION(render_kitty));
1088 char *zalgo_frame = NULL;
1089 int zalgo_level = GET_OPTION(zalgo);
1090 if (zalgo_level > 0 && !binary_stdout) {
1091 asciichat_error_t err =
1092 zalgo_apply(display_frame, frame_len, zalgo_level, UI_CONTROLLER_MAX_FRAME_BYTES - 7, &zalgo_frame);
1093 if (err == ASCIICHAT_OK) {
1094 display_frame = zalgo_frame;
1095 frame_len = strlen(zalgo_frame);
1096 } else {
1097 log_warn_every(5 * US_PER_SEC_INT, "Cannot apply Zalgo: %s", asciichat_error_format(NULL, err));
1098 }
1099 }
1100
1101 char *plain_frame = NULL;
1102 if (GET_OPTION(strip_ansi)) {
1103 // Expand REP before stripping escapes so repeated cells are preserved.
1104 char *expanded = ansi_expand_rle(display_frame, frame_len);
1105 if (expanded) {
1106 size_t capacity = strlen(expanded) + 1;
1107 plain_frame = SAFE_MALLOC(capacity, char *);
1108 strip_ansi_codes(expanded, plain_frame, capacity);
1109 SAFE_FREE(expanded);
1110 display_frame = plain_frame;
1111 frame_len = strlen(plain_frame);
1112 }
1113 }
1114
1115 // Handle first frame - perform initial terminal reset and splash cleanup
1116 bool was_first_frame = atomic_exchange_bool(&ctx->first_frame, false);
1117 if (was_first_frame) {
1120 // Stop splash screen when first frame is ready
1123 }
1124
1125 if (binary_stdout) {
1126 if (GET_OPTION(snapshot_mode) && !g_snapshot_first_frame_rendered) {
1128 g_snapshot_first_frame_rendered_ns = time_get_ns();
1129 }
1130 SAFE_FREE(plain_frame);
1131 SAFE_FREE(zalgo_frame);
1132 SAFE_FREE(rain_result);
1133 SAFE_FREE(visualization_frame);
1134 return;
1135 }
1136
1137 // Output routing logic
1138 // A snapshot still needs terminal cursor framing when stdout is a real TTY.
1139 // `terminal_is_interactive()` also depends on stdin, which is commonly false
1140 // for scripted snapshot invocations even though the PTY can display output.
1141 bool use_tty_control = ctx->has_tty ||
1142 (ctx->snapshot_mode && ctx->tty_info.fd >= 0 && platform_isatty(ctx->tty_info.fd));
1143
1144 uint64_t frame_write_started_ns = time_get_ns();
1145
1146 // Increment shared frame counter for both client and host rendering
1147 // This metric is used by debug scripts to measure FPS across all modes
1148 static atomic_t g_display_frames_rendered = {0};
1149 int frame_number = atomic_fetch_add_u64(&g_display_frames_rendered, 1) + 1;
1150 log_dev("[FRAMES_RENDERED_TOTAL] %d", frame_number);
1151
1152 if (use_tty_control) {
1153 // TTY mode: Buffer cursor control + frame data together for atomic frame display
1154 const char *cursor_home_sequence = "\033[H";
1155 const char *erase_below_sequence = "\033[J";
1156 size_t cursor_seq_len = 3;
1157 size_t erase_below_len = 3;
1158 // The frame is shorter than the terminal in some configurations. Erase
1159 // from the final frame cursor to the bottom of the screen in the same
1160 // submission so stale rows cannot survive a resize or a shorter frame.
1161 size_t total_size = cursor_seq_len + frame_len + erase_below_len;
1162
1163 if (ctx->tty_frame_capacity < total_size) {
1164 size_t capacity = ctx->tty_frame_capacity ? ctx->tty_frame_capacity : 4096;
1165 while (capacity < total_size && capacity <= SIZE_MAX / 2)
1166 capacity *= 2;
1167 if (capacity < total_size)
1168 capacity = total_size;
1169 ctx->tty_frame_buffer = SAFE_REALLOC(ctx->tty_frame_buffer, capacity, char *);
1170 ctx->tty_frame_capacity = capacity;
1171 }
1172 char *frame_buffer = ctx->tty_frame_buffer;
1173 if (frame_buffer) {
1174 memcpy(frame_buffer, cursor_home_sequence, cursor_seq_len);
1175 memcpy(frame_buffer + cursor_seq_len, display_frame, frame_len);
1176 memcpy(frame_buffer + cursor_seq_len + frame_len, erase_below_sequence, erase_below_len);
1177
1178 log_debug_every(US_PER_SEC_INT, "FRAME_WRITE_TTY: Writing %zu bytes (cursor=%zu + frame=%zu) to stdout",
1179 total_size, cursor_seq_len, frame_len);
1180 int output_fd = ctx->tty_info.fd >= 0 ? ctx->tty_info.fd : STDOUT_FILENO;
1181 asciichat_error_t presented = ASCIICHAT_OK;
1182 if (ctx->snapshot_mode) {
1183 // Snapshot processes may exit immediately after the first frame. Write
1184 // directly so teardown cannot discard a queued UI-controller update.
1185 if (platform_write_all(output_fd, frame_buffer, total_size) != total_size)
1186 presented = SET_ERRNO(ERROR_FILE_OPERATION, "Cannot write snapshot frame to terminal");
1187 } else {
1189 UI_SCREEN_MEDIA, output_fd, (terminal_size_t){.cols = UI_MEDIA_MIN_COLS, .rows = UI_MEDIA_MIN_ROWS},
1190 frame_buffer, total_size);
1191 }
1192 if (was_first_frame && presented == ASCIICHAT_OK)
1194 ssize_t written = presented == ASCIICHAT_OK ? (ssize_t)total_size : -1;
1195 log_debug_every(US_PER_SEC_INT, "FRAME_WRITE_TTY: Wrote %zd bytes (requested %zu)", written, total_size);
1196
1197 // Start snapshot timer on first ASCII frame rendered
1198 if (GET_OPTION(snapshot_mode) && !g_snapshot_first_frame_rendered) {
1200 g_snapshot_first_frame_rendered_ns = time_get_ns();
1201 log_debug("SNAPSHOT: FIRST ASCII FRAME RENDERED (write_ascii) - Timer started");
1202 }
1203
1204 // The UI controller copies the submission before this workspace is reused.
1205 }
1206
1207 // Live presentation owns terminal writes on its worker. Draining here
1208 // blocks packet reception, including audio, behind a slow terminal reader.
1209 if (ctx->snapshot_mode) {
1210 (void)fflush(stdout);
1211 int flush_fd = (ctx->tty_info.fd >= 0) ? ctx->tty_info.fd : STDOUT_FILENO;
1212 (void)terminal_flush(flush_fd);
1213 }
1214
1215 } else if (terminal_is_interactive()) {
1216 // Piped to interactive terminal: output ASCII frames with newline
1217 char *write_buf = SAFE_MALLOC(frame_len + 1, char *);
1218 if (write_buf) {
1219 memcpy(write_buf, display_frame, frame_len);
1220 write_buf[frame_len] = '\n';
1221 if (ui_controller_write(STDOUT_FILENO, write_buf, frame_len + 1) != ASCIICHAT_OK) {
1222 atomic_store_bool(&ctx->output_failed, true);
1224 SAFE_FREE(write_buf);
1225 goto cleanup_frame;
1226 }
1227 SAFE_FREE(write_buf);
1228 }
1229
1230 if (ctx->snapshot_mode) {
1231 (void)fflush(stdout);
1232 int flush_fd = (ctx->tty_info.fd >= 0) ? ctx->tty_info.fd : STDOUT_FILENO;
1233 (void)terminal_flush(flush_fd);
1234 }
1235 } else {
1236 // Named render files leave stdout available for ASCII output
1237 char *write_buf = SAFE_MALLOC(frame_len + 1, char *);
1238 if (write_buf) {
1239 memcpy(write_buf, display_frame, frame_len);
1240 write_buf[frame_len] = '\n';
1241 if (ui_controller_write(STDOUT_FILENO, write_buf, frame_len + 1) != ASCIICHAT_OK) {
1242 atomic_store_bool(&ctx->output_failed, true);
1244 SAFE_FREE(write_buf);
1245 goto cleanup_frame;
1246 }
1247
1248 // Start snapshot timer on first ASCII frame rendered
1249 if (GET_OPTION(snapshot_mode) && !g_snapshot_first_frame_rendered) {
1251 g_snapshot_first_frame_rendered_ns = time_get_ns();
1252 log_debug("SNAPSHOT: FIRST ASCII FRAME RENDERED (write_ascii piped) - Timer started");
1253 }
1254
1255 SAFE_FREE(write_buf);
1256 }
1257
1258 // Writes already reach the OS synchronously; streaming frames do not need
1259 // a durability barrier or a terminal drain on the receiving thread.
1260 (void)fflush(stdout);
1261 if (ctx->snapshot_mode && ctx->has_tty) {
1262 int flush_fd = (ctx->tty_info.fd >= 0) ? ctx->tty_info.fd : STDOUT_FILENO;
1263 (void)terminal_flush(flush_fd);
1264 }
1265 }
1266
1267cleanup_frame:
1268 log_dev_every(3 * US_PER_SEC_INT, "FRAME_WRITE: Published in %.2f ms",
1269 (double)(time_get_ns() - frame_write_started_ns) / NS_PER_MS_INT);
1270
1271 // Clean up digital rain result if allocated
1272 SAFE_FREE(plain_frame);
1273 SAFE_FREE(zalgo_frame);
1274 if (rain_result) {
1275 SAFE_FREE(rain_result);
1276 }
1277 SAFE_FREE(visualization_frame);
1278}
1279
1284
1286 ASCII_OTEL_SCOPE(SESSION_DISPLAY_ENCODE_FRAME);
1287 static int call_count = 0;
1288 if (call_count++ < 5) {
1289 log_debug("ctx=%p, image=%p, captured_ns=%llu, ctx->render_file=%p", (void *)ctx, (void *)image,
1290 (unsigned long long)captured_ns, ctx ? (void *)ctx->render_file : NULL);
1291 }
1292
1293 if (!ctx || !ctx->initialized) {
1294 return SET_ERRNO(ERROR_INVALID_PARAM, "Display context is NULL or uninitialized");
1295 }
1296
1297 if (!image) {
1298 return SET_ERRNO(ERROR_INVALID_PARAM, "Image is NULL");
1299 }
1300
1301 // Only encode if render_file is active
1302 if (!ctx->render_file) {
1303 log_warn_every(10 * US_PER_SEC_INT, "render_file is NULL, skipping encode");
1304 return ASCIICHAT_OK;
1305 }
1306
1307 // Convert image to ASCII
1308 uint64_t conversion_started_ns = time_get_ns();
1309 char *ascii = session_display_convert_to_ascii(ctx, image);
1310 uint64_t converted_ns = time_get_ns();
1311 if (!ascii) {
1312 log_warn("Failed to convert image to ASCII");
1313 return SET_ERRNO(ERROR_MEDIA_INIT, "Cannot convert recording frame to ASCII");
1314 }
1315
1316 // Write the same selected visualization used by the terminal to the render-file encoder.
1317 char *visualization_frame = NULL;
1318 const char *render_frame = ascii;
1319 if (GET_OPTION(waveform) || GET_OPTION(fft)) {
1320 visualization_frame = session_display_create_visualization_frame(ctx);
1321 if (visualization_frame)
1322 render_frame = visualization_frame;
1323 }
1324
1325 uint64_t stats_record_start = time_get_ns();
1326 asciichat_error_t fe = render_file_write_frame(ctx->render_file, render_frame, captured_ns);
1327 uint64_t recorded_ns = time_get_ns();
1328 log_debug_every(US_PER_SEC_INT, "RECORD_FRAME: convert_ms=%.2f output_ms=%.2f total_ms=%.2f",
1329 (double)(converted_ns - conversion_started_ns) / NS_PER_MS_INT,
1330 (double)(recorded_ns - converted_ns) / NS_PER_MS_INT,
1331 (double)(recorded_ns - conversion_started_ns) / NS_PER_MS_INT);
1332 stats_duration_record(stats_runtime_scope(), STATS_DURATION_RECORDING, time_get_ns() - stats_record_start);
1333 if (fe == ASCIICHAT_OK)
1335 SAFE_FREE(visualization_frame);
1336 if (fe != ASCIICHAT_OK) {
1337 log_warn_every(5 * US_PER_SEC_INT, "render-file: encode failed (%s)", asciichat_error_format(NULL, fe));
1338 }
1339
1340 SAFE_FREE(ascii);
1341 return fe;
1342}
1343
1344void session_display_write_raw(session_display_ctx_t *ctx, const char *data, size_t len) {
1345 ASCII_OTEL_SCOPE(SESSION_DISPLAY_WRITE_RAW);
1346 if (!ctx || !ctx->initialized || !data || len == 0) {
1347 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, data=%p, len=%zu", ctx, data, len);
1348 return;
1349 }
1350
1351 int fd = -1;
1352 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
1353 fd = ctx->tty_info.fd;
1354 } else {
1355 fd = STDOUT_FILENO;
1356 }
1357
1358 // Write all data with automatic retry on transient errors
1359 (void)ui_controller_write(fd, data, len);
1360
1361 // Flush immediately after write to TTY to ensure data is sent
1362 (void)terminal_flush(fd);
1363}
1364
1366 if (ctx && !ctx->snapshot_mode)
1368}
1369
1371 if (ctx && !ctx->snapshot_mode)
1373}
1374
1379
1381 if (!ctx || !ctx->initialized) {
1382 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL or uninitialized");
1383 return false;
1384 }
1385 return ctx->audio_playback_enabled;
1386}
1387
1388asciichat_error_t session_display_write_audio(session_display_ctx_t *ctx, const float *buffer, size_t num_samples) {
1389 ASCII_OTEL_SCOPE(SESSION_DISPLAY_WRITE_AUDIO);
1390 if (!ctx || !ctx->initialized || !buffer || num_samples == 0) {
1391 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, buffer=%p, num_samples=%zu", ctx, buffer,
1392 num_samples);
1393 }
1394
1395 if (!ctx->audio_playback_enabled || !ctx->audio_ctx) {
1396 // Audio not enabled, but not an error - just skip silently
1397 return ASCIICHAT_OK;
1398 }
1399
1400 // For mirror mode with local files: write samples directly without jitter buffering
1401 // The jitter buffer is designed for network scenarios with irregular packet arrivals
1402 // For local playback, we just want raw samples flowing to the speakers
1403 audio_context_t *audio_ctx = (audio_context_t *)ctx->audio_ctx;
1404 if (audio_ctx && audio_ctx->playback_buffer) {
1405 audio_ring_buffer_t *rb = audio_ctx->playback_buffer;
1406
1407 // Simple direct write: append samples to ring buffer without network-oriented complexity
1408 uint32_t write_idx = (uint32_t)atomic_load_u64(&rb->write_index);
1409 uint32_t read_idx = (uint32_t)atomic_load_u64(&rb->read_index);
1410
1411 // Calculate available space in ring buffer
1412 uint32_t available = (read_idx - write_idx - 1) & (AUDIO_RING_BUFFER_SIZE - 1);
1413
1414 if ((uint32_t)num_samples > available) {
1415 // Buffer full - just skip this write to avoid distortion from overwriting old audio
1416 return ASCIICHAT_OK;
1417 }
1418
1419 // Direct memcpy to ring buffer, handling wrap-around
1420 uint32_t space_before_wrap = AUDIO_RING_BUFFER_SIZE - write_idx;
1421 if ((uint32_t)num_samples <= space_before_wrap) {
1422 memcpy(&rb->data[write_idx], buffer, num_samples * sizeof(float));
1423 } else {
1424 // Split write: first part to end of buffer, second part wraps to beginning
1425 uint32_t first_part = space_before_wrap;
1426 uint32_t second_part = num_samples - first_part;
1427 memcpy(&rb->data[write_idx], buffer, first_part * sizeof(float));
1428 memcpy(&rb->data[0], &buffer[first_part], second_part * sizeof(float));
1429 }
1430
1431 // Update write index atomically
1432 atomic_store_u64(&rb->write_index, (write_idx + num_samples) % AUDIO_RING_BUFFER_SIZE);
1433
1434 return ASCIICHAT_OK;
1435 }
1436
1437 return ASCIICHAT_OK;
1438}
1439
1440/* ============================================================================
1441 * Keyboard Help Functions (from keyboard_help module)
1442 * ============================================================================ */
1443
1447// Global reference to current display context (used by signal handlers)
1448// Set during render loop, cleared on exit
1449static atomic_ptr_t g_current_display_ctx = {0};
1450
1451// Signal-safe flag to request help screen closure from Ctrl+C handler
1452static atomic_t g_help_signal_cancel = {0};
1453
1454// Signal handler safe way to access current display context
1455static session_display_ctx_t *get_current_display_ctx(void) {
1456 return (session_display_ctx_t *)atomic_ptr_load(&g_current_display_ctx);
1457}
1458
1459// Called by render loop to register/unregister the display context
1461 atomic_ptr_store(&g_current_display_ctx, (void *)ctx);
1462}
1463
1464// Public getter for global display context
1466 return get_current_display_ctx();
1467}
1468
1469// Public setter for global display context (used by discovery participant cleanup)
1473
1474// Called from Ctrl+C signal handler to request help screen closure
1476 atomic_store_bool(&g_help_signal_cancel, true);
1477}
1478
1479// Called from render loop to check if help should be closed
1481 bool expected = true;
1482 return atomic_cas_bool(&g_help_signal_cancel, &expected, false);
1483}
1484
1486 if (!ctx) {
1487 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
1488 return false;
1489 }
1490
1491 // Disable help in snapshot mode only - allow help state check even if stdin/stdout aren't TTYs
1492 if (GET_OPTION(snapshot_mode)) {
1493 return false;
1494 }
1495
1497}
1498
1499// Global accessor for signal handlers (called from Ctrl+C handler)
1500// Checks raw help state without snapshot mode check (signal handler needs to know
1501// if help is active even in snapshot mode)
1503 session_display_ctx_t *ctx = get_current_display_ctx();
1505}
1506
1510
1512 log_debug("duration=%.3f, ctx=%p, render_file=%p", actual_duration_sec, (void *)ctx,
1513 ctx ? (void *)ctx->render_file : NULL);
1514
1515 if (!ctx) {
1516 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
1517 return;
1518 }
1519
1520 if (!ctx->render_file) {
1521 // Terminal snapshots have no render-file encoder; their duration is
1522 // governed by the capture loop and requires no additional action.
1523 return;
1524 }
1525
1526 log_debug(" -> Passing duration %.3f to render_file encoder", actual_duration_sec);
1527 render_file_set_snapshot_actual_duration(ctx->render_file, actual_duration_sec);
1528}
1529
1531 session_display_write_ascii_frame(ctx, ascii, true);
1532}
ANSI escape sequence utilities and fast color code generation.
Application-level callbacks for library code.
#define APP_CALLBACK_VOID(callback_name)
char * ascii_convert_with_buffers_stabilized(image_t *original, const ssize_t width, const ssize_t height, const terminal_capabilities_t *caps, const bool use_aspect_ratio, const bool stretch, const char *palette_chars, const bool contain, image_t **workspace, outbuf_t *output, ascii_stabilizer_t *stabilizer)
Definition ascii.c:293
const char * asciichat_error_format(const char *message, asciichat_error_t code)
Synchronized per-thread pending error stacks and value snapshots. SET_ERRNO pushes,...
#define SET_ERRNO(code,...)
#define HAS_ERRNO(context)
terminal_background_t colorscheme_current_background(void)
@ TERM_BACKGROUND_DARK
Definition colorscheme.h:67
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
Matrix-style digital rain effect for ASCII frames.
int color_mode
Definition fft.c:25
asciichat_error_t audio_visualization_fft_init(void)
Definition fft.c:61
audio_visualization_source_t source
Definition fft.c:24
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
char * audio_visualization_render_fft(unsigned int width, unsigned int height, audio_visualization_source_t source, bool use_color, int color_mode, bool flip_x, bool flip_y)
Definition fft.c:91
asciichat_error_t session_frame_validate(const char *text, size_t length, uint32_t width, uint32_t height)
asciichat_error_t session_frame_validate_contract(const char *text, size_t length, uint32_t width, uint32_t height, const acip_render_settings_t *settings, uint32_t effects)
void ansi_fast_init_256color(void)
Definition ansi.c:232
void ansi_fast_init(void)
Definition ansi.c:227
void ansi_fast_init_16color(void)
Definition ansi.c:315
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:285
#define MIN(a, b)
Return the minimum of two values.
Definition common.h:170
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_MALLOC(size, cast)
Definition common.h:265
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
unsigned long long uint64_t
Definition common.h:63
#define MAX(a, b)
Return the maximum of two values.
Definition common.h:175
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_INVALID_STATE
@ ERROR_BUFFER_FULL
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_MEDIA_INIT
Definition error_codes.h:65
@ ERROR_FILE_OPERATION
@ ERROR_USAGE
Definition error_codes.h:48
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_dev(...)
Log a DEV message (most verbose, development only).
Definition log/log.h:581
void log_set_force_stderr(bool enabled)
Force all terminal log output to stderr.
Definition log.c:767
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define NS_PER_SEC_INT
Definition time.h:169
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as a compact duration with configurable sub-minute precision.
Definition time.c:374
#define TIME_DURATION_BUFFER_SIZE
Definition time.h:40
#define NS_PER_MS_INT
Definition time.h:168
#define US_PER_SEC_INT
Definition time.h:173
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
struct options_state options_t
Immutable, RCU-published snapshot of the application's runtime configuration.
audio_source_t
Audio source selected for visualization.
Definition options.h:276
#define options_terminal_settings()
Definition options.h:1296
@ UTF8_SETTING_AUTO
Smart detection (default): UTF-8 if terminal supports it.
Definition options.h:264
@ UTF8_SETTING_FALSE
Force UTF-8 OFF: disable UTF-8 support.
Definition options.h:268
@ ASCII_RENDERER_GLYPH
Definition options.h:966
@ AUDIO_SOURCE_MIC
Definition options.h:282
@ AUDIO_SOURCE_CALL
Definition options.h:280
@ AUDIO_SOURCE_MEDIA
Definition options.h:284
@ COLOR_SETTING_FALSE
Force colors OFF: disable all colors and logging colors.
Definition options.h:249
int initialize_client_palette(palette_type_t palette_type, const char *custom_chars, char client_palette_chars[ASCII_PALETTE_BUFFER_SIZE], size_t *client_palette_len, char client_luminance_palette[ASCII_LUMINANCE_LEVELS])
Initialize client palette with full configuration.
Definition palette.c:306
const palette_def_t * get_builtin_palette(palette_type_t type)
Get built-in palette definition.
Definition palette.c:53
palette_type_t
Built-in palette type enumeration.
Definition palette.h:90
const char * chars
Character sequence (ordered from dark to light).
Definition palette.h:128
@ PALETTE_CUSTOM
User-defined via –palette-chars.
Definition palette.h:104
@ PALETTE_STANDARD
Standard ASCII palette: " ...',;:clodxkO0KXNWM".
Definition palette.h:94
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all bytes to a file descriptor, handling partial writes.
int platform_dup(int fd)
struct audio_ring_buffer audio_ring_buffer_t
Audio ring buffer for real-time audio streaming.
#define AUDIO_RING_BUFFER_SIZE
Audio ring buffer size in samples (192000 samples = 4 seconds @ 48kHz).
Definition ringbuffer.h:131
void session_display_destroy(session_display_ctx_t *ctx)
void session_display_set_render_fps(session_display_ctx_t *ctx, uint32_t fps)
Set the render FPS for file output encoding.
void session_display_write_ascii(session_display_ctx_t *ctx, const char *ascii)
Write ASCII frame to terminal only (no encoding).
session_display_ctx_t * session_display_create(const session_display_config_t *config)
Create a new session display context.
asciichat_error_t session_display_finish_render_file(session_display_ctx_t *ctx)
Destroy session display context and free resources.
void session_display_clear(session_display_ctx_t *ctx)
Clear the terminal screen.
void * session_display_get_stdin_reader(session_display_ctx_t *ctx)
Get the stdin frame reader for ASCII-to-video rendering.
size_t session_display_get_palette_len(session_display_ctx_t *ctx)
Get the palette character count.
void session_display_set_stdin_reader(session_display_ctx_t *ctx, void *reader)
Set stdin frame reader for ASCII-to-video rendering.
bool keyboard_help_is_active_global(void)
Check if keyboard help is active (global accessor for signal handlers).
bool keyboard_help_is_active(session_display_ctx_t *ctx)
Check if keyboard help is currently active.
struct session_display_ctx session_display_ctx_t
session display config is intentionally a small, concrete record. Fields such as snapshot_mode,...
int splash_intro_done(void)
Signal that intro splash should end (first frame ready to render).
Definition splash.c:426
void session_display_begin_drain(session_display_ctx_t *ctx, uint64_t total, bool total_known)
void session_display_set_snapshot_actual_duration(session_display_ctx_t *ctx, double actual_duration_sec)
Set the actual wall-clock duration for snapshot mode frame timing.
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.
asciichat_error_t session_display_encode_frame(session_display_ctx_t *ctx, const image_t *image, uint64_t captured_ns)
Encode frame to render-file (FFmpeg only, no terminal output).
asciichat_error_t session_display_write_audio(session_display_ctx_t *ctx, const float *buffer, size_t num_samples)
Write audio samples to playback buffer.
session_display_ctx_t * session_display_get_global_context(void)
Get the global display context (used for signal handlers and special cleanup).
bool session_display_has_tty(session_display_ctx_t *ctx)
Check if display has a TTY (terminal) available.
void session_display_cursor_home(session_display_ctx_t *ctx)
Move cursor to home position (top-left).
void keyboard_help_toggle_global(void)
Toggle keyboard help (global accessor for signal handlers).
const char * session_display_get_luminance_palette(session_display_ctx_t *ctx)
Get the luminance mapping palette.
void keyboard_help_signal_cancel(void)
Request help screen closure from signal handler (Ctrl+C).
void session_display_set_global_context_public(session_display_ctx_t *ctx)
Set the global display context (public setter for discovery cleanup).
void session_display_render_frame(session_display_ctx_t *ctx, const char *frame_data)
Render an ASCII frame to the terminal.
void session_display_reset_first_frame(session_display_ctx_t *ctx)
Reset the first frame flag to allow splash screen to show again on next render.
int session_display_get_tty_fd(session_display_ctx_t *ctx)
Get the TTY file descriptor.
void splash_clear_display_context(void)
Clear the cached display context to prevent use-after-free.
Definition splash.c:441
bool session_display_has_first_frame(session_display_ctx_t *ctx)
Check if the display has rendered its first frame.
void session_display_set_global_context(session_display_ctx_t *ctx)
Set the global display context (for signal handlers like Ctrl+C).
bool session_display_has_render_file(session_display_ctx_t *ctx)
Check if display has render-file configured.
bool keyboard_help_check_signal_cancel(void)
Check if help screen closure was requested by signal handler.
const char * session_display_get_palette_chars(session_display_ctx_t *ctx)
Get the palette characters string.
void splash_wait_for_animation(void)
Wait for splash animation thread to fully exit.
Definition splash.c:451
const terminal_capabilities_t * session_display_get_caps(session_display_ctx_t *ctx)
Get detected terminal capabilities.
bool session_display_has_audio_playback(session_display_ctx_t *ctx)
Check if display has audio playback configured.
char * session_display_convert_to_ascii(session_display_ctx_t *ctx, const image_t *image)
Convert an image to ASCII art using display context and command-line options.
void session_display_reset(session_display_ctx_t *ctx)
Reset terminal to default state.
uint32_t session_display_get_render_fps(session_display_ctx_t *ctx)
Get the render FPS configured for file output.
void strip_ansi_codes(const char *input, char *output, size_t output_size)
Strip ANSI escape codes from a string.
Definition string.c:374
int utf8_display_width(const char *str)
Calculate terminal display width of a UTF-8 string.
Definition utf8.c:98
size_t utf8_char_count(const char *str)
Count UTF-8 characters (not bytes).
Definition utf8.c:108
char * rainbow_replace_ansi_colors(const char *ansi_string, float time_seconds)
Replace all RGB ANSI codes with rainbow color.
utf8_palette_cache_t * get_utf8_palette_cache(const char *ascii_chars)
Get UTF-8 palette cache for a character set.
asciichat_error_t ascii_write_init(int fd, bool reset_terminal)
Initialize ASCII write subsystem.
Definition ascii.c:128
void digital_rain_set_color_from_filter(digital_rain_t *rain, color_filter_t filter)
Set digital rain color from color filter.
void ascii_write_destroy(int fd, bool reset_terminal)
Destroy ASCII write subsystem.
Definition ascii.c:559
#define ASCII_LUMINANCE_LEVELS
Number of luminance levels supported (256).
Definition ascii.h:440
#define ASCII_PALETTE_BUFFER_SIZE
#define IMAGE_MAX_WIDTH
An owned RGB image with dimensions and row-major pixel storage.
#define IMAGE_MAX_HEIGHT
Maximum image height (4K resolution).
void digital_rain_destroy(digital_rain_t *rain)
Destroy digital rain effect context.
char * digital_rain_apply(digital_rain_t *rain, const char *frame, float delta_time)
Apply digital rain effect to ASCII frame.
struct digital_rain digital_rain_t
Digital rain effect context.
digital_rain_t * digital_rain_init(int num_columns, int num_rows)
Initialize digital rain effect context.
char * ascii_create_grid(ascii_frame_source_t *sources, int source_count, int width, int height, size_t *out_size)
Create a grid layout from multiple ASCII frames.
Definition ascii.c:742
void image_destroy(image_t *image)
Destroy an image allocated with image_new().
char * ansi_expand_rle(const char *input, size_t input_len)
Expand RLE escape sequences in a string.
Definition rle.c:14
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
🔊 Audio Capture and Playback Interface for ascii-chat
🆘 Interactive keyboard help overlay for session keyboard shortcuts
void stats_counter_add(stats_scope_t *scope, stats_counter_id_t id, uint64_t amount)
void stats_duration_record(stats_scope_t *scope, stats_duration_id_t id, uint64_t elapsed_ns)
bool ascii_glyph_palette_supported(const char *palette)
void ascii_stabilizer_reset(ascii_stabilizer_t *state)
📝 Logging API with multiple log levels and terminal output control
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:746
void signal_exit(void)
Definition main.c:253
NEON-optimized ASCII rendering functions.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
ASCII Palette Management for Video-to-ASCII Conversion.
bool g_snapshot_first_frame_rendered
asciichat_error_t options_read_snapshot(options_snapshot_t *snapshot)
Definition rcu.c:865
struct terminal_fd_reader_s terminal_fd_reader_t
Read ANSI ASCII frames from file descriptor, chunked by height.
#define FRAME_RENDER_CONTRACT_RAIN
void render_file_set_snapshot_actual_duration(render_file_ctx_t *ctx, double actual_duration_sec)
Definition renderer.c:369
asciichat_error_t render_file_write_frame(render_file_ctx_t *ctx, const char *frame, uint64_t captured_ns)
Definition renderer.c:348
void render_file_set_live_timing(render_file_ctx_t *ctx)
Definition renderer.c:266
asciichat_error_t render_file_create(const char *output_path, int cols, int rows, int fps, int theme, render_file_ctx_t **out)
Definition renderer.c:53
asciichat_error_t render_file_destroy(render_file_ctx_t *ctx)
Definition renderer.c:375
void render_file_begin_drain(render_file_ctx_t *ctx, uint64_t total, bool total_known)
Definition renderer.c:364
Terminal-to-pixel renderer and render-file output (audio + video).
struct render_file_ctx_s render_file_ctx_t
Definition renderer.h:63
stats_scope_t * stats_runtime_scope(void)
Definition runtime.c:80
Intro and status screen display for ascii-chat with animated rainbow effects.
void session_display_release_ascii(session_display_ctx_t *ctx, char *ascii)
void session_display_reset_stabilizer(session_display_ctx_t *ctx, bool recording)
asciichat_error_t session_display_init_render_file(session_display_ctx_t *ctx, uint32_t fps)
Perform complete terminal reset.
char * session_display_convert_to_ascii_reuse(session_display_ctx_t *ctx, const image_t *image)
void session_display_set_render_live_timing(session_display_ctx_t *ctx)
void session_display_write_ascii_frame(session_display_ctx_t *ctx, const char *ascii, bool fresh)
🖥️ Unified terminal display abstraction for session-based rendering
uint64_t g_snapshot_first_frame_rendered_ns
Centralized render loop abstraction for all display modes.
🔤 String Manipulation and Shell Escaping Utilities
acip render settings keeps the values that move through the subsystem: caps, width,...
Frame source structure for grid layout.
Definition ascii.h:352
When code needs to work with audio device info, it keeps index, max_input_channels,...
audio_ring_buffer_t * playback_buffer
OLD: Network → Worker (will become network_playback_rb).
atomic_t read_index
Read index (consumer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:215
atomic_t write_index
Write index (producer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:213
float data[192000]
Audio sample data buffer.
Definition ringbuffer.h:211
Growable byte buffer for assembling one complete terminal frame.
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...
int w
Image width in pixels (must be > 0).
int h
Image height in pixels (must be > 0).
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order).
int height
Terminal height in characters (int for OPTION_TYPE_INT compat).
Definition options.h:997
terminal_render_mode_t render_mode
Render mode (foreground/background/half-block).
Definition options.h:1095
bool stretch
Allow aspect ratio distortion.
Definition options.h:1141
int width
Terminal width in characters (int for OPTION_TYPE_INT compat).
Definition options.h:996
Dynamic output buffer (auto-expanding).
char * buf
Buffer pointer (allocated, owned by caller, must be freed).
Validated luminance palette used to turn brightness into characters.
Definition palette.h:124
Terminal-rendering and snapshot settings used to create a display context.
void * callback_data
Opaque data passed to should_exit_callback.
palette_type_t palette_type
Palette type for ASCII rendering.
void * audio_ctx
Audio context for playback (borrowed, not owned).
bool defer_render_file
Defer render-file encoder creation until media timing is known.
const char * custom_palette
Custom palette characters (required if palette_type == PALETTE_CUSTOM).
bool enable_audio_playback
Enable audio playback (mirror mode).
session_display_should_exit_fn should_exit_callback
Optional: callback to check if initialization should be cancelled (e.g., shutdown signal).
uint32_t render_fps
Video FPS for render-file encoding (0 = use default/option).
bool snapshot_mode
Enable snapshot mode (single frame capture).
terminal_color_mode_t color_mode
Color mode override (TERM_COLOR_AUTO for auto-detection).
Runtime terminal state and reusable workspaces for ASCII rendering.
char luminance_palette[ASCII_LUMINANCE_LEVELS]
Luminance-to-character mapping table (256 entries).
bool has_tty
True if we have a valid TTY for interactive output.
tty_info_t tty_info
TTY information (file descriptor, path, ownership).
digital_rain_t * digital_rain
Digital rain effect context (NULL if disabled).
char palette_chars[ASCII_PALETTE_BUFFER_SIZE]
Palette character string for rendering.
uint64_t last_frame_time_ns
Last frame timestamp for digital rain delta time calculation.
bool initialized
Context is fully initialized.
atomic_t first_frame
First frame flag for logging control.
render_file_ctx_t * render_file
Render-to-file context (NULL if disabled).
void * audio_ctx
Audio context for playback (borrowed, not owned).
atomic_t output_failed
Stop submitting frames after the output consumer disappears.
terminal_capabilities_t caps
Detected terminal capabilities.
bool audio_playback_enabled
Audio playback is enabled.
palette_type_t palette_type
Configured palette type.
bool snapshot_mode
Snapshot mode enabled.
uint32_t render_fps
Video FPS for render-file encoding.
size_t palette_len
Number of characters in palette.
terminal_fd_reader_t * stdin_reader
Stdin frame reader for ASCII-to-video rendering (borrowed ref, not owned).
ascii_stabilizer_t recording_stabilizer
⏱️ High-precision timing utilities using sokol_time.h and uthash
#define UI_MEDIA_MIN_COLS
Definition too_small.h:5
#define UI_MEDIA_MIN_ROWS
Definition too_small.h:6
void ui_controller_remove(ui_screen_t screen)
void ui_controller_redraw(void)
void ui_controller_title_activity(ui_title_activity_t activity)
void ui_controller_title_ready(void)
int ui_controller_interactive_screen(void)
bool ui_controller_is_blocked(void)
asciichat_error_t ui_controller_write(int fd, const char *data, size_t len)
asciichat_error_t ui_controller_present_retained(ui_screen_t screen, int fd, terminal_size_t minimum, const char *data, size_t len)
asciichat_error_t ui_controller_present(ui_screen_t screen, int fd, terminal_size_t minimum, const char *data, size_t len)
@ UI_TITLE_ACTIVITY_IDLE
#define UI_CONTROLLER_MAX_FRAME_BYTES
@ UI_SCREEN_MEDIA
@ UI_SCREEN_SPLASH
@ UI_SCREEN_HELP
🔤 UTF-8 Encoding and Decoding Utilities
SIMD-optimized ASCII conversion interface.
audio_visualization_source_t
@ AUDIO_VISUALIZATION_SOURCE_MIX
@ AUDIO_VISUALIZATION_SOURCE_MEDIA
@ AUDIO_VISUALIZATION_SOURCE_MIC
@ AUDIO_VISUALIZATION_SOURCE_REMOTE
#define AUDIO_VISUALIZATION_COLOR_BRIGHTER
#define AUDIO_VISUALIZATION_COLOR_DARKER
char * audio_visualization_render_waveform(unsigned int width, unsigned int height, audio_visualization_source_t source, bool use_color, int color_mode, bool flip_x, bool flip_y)
Definition waveform.c:39
asciichat_error_t zalgo_apply(const char *input, size_t length, int level, size_t max_output_bytes, char **output)
Definition zalgo.c:69