ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
mirror/main.c
Go to the documentation of this file.
1
27#include "main.h"
28#include "video/webcam/webcam.h"
29#include "video/ascii.h"
30#include "video/image.h"
31#include "video/ansi_fast.h"
32#include "video/ansi.h"
33#include "video/rle.h"
34
36#include "platform/terminal.h"
37#include "common.h"
38#include "options/options.h"
39#include "options/rcu.h" // For RCU-based options access
40#include "video/palette.h"
41
42#include <signal.h>
43#include <stdlib.h>
44#include <stdio.h>
45#include <stdatomic.h>
46#include <time.h>
47#include <string.h>
48
49#ifdef _WIN32
50#include <windows.h>
51#endif
52
53/* ============================================================================
54 * Global State Variables
55 * ============================================================================ */
56
58static atomic_bool g_mirror_should_exit = false;
59
65static bool mirror_should_exit(void) {
66 return atomic_load(&g_mirror_should_exit);
67}
68
72static void mirror_signal_exit(void) {
73 atomic_store(&g_mirror_should_exit, true);
74}
75
82static bool mirror_console_ctrl_handler(console_ctrl_event_t event) {
83 if (event != CONSOLE_CTRL_C && event != CONSOLE_CTRL_BREAK) {
84 return false;
85 }
86
87 // Use atomic instead of volatile for signal handler
88 static _Atomic int ctrl_c_count = 0;
89 int count = atomic_fetch_add(&ctrl_c_count, 1) + 1;
90
91 if (count > 1) {
92#ifdef _WIN32
93 TerminateProcess(GetCurrentProcess(), 1);
94#else
95 _exit(1);
96#endif
97 }
98
99 mirror_signal_exit();
100 return true;
101}
102
103/* ============================================================================
104 * Mirror Mode TTY Management
105 * ============================================================================ */
106
108static tty_info_t g_mirror_tty_info = {-1, NULL, false};
109
111static bool g_mirror_has_tty = false;
112
118static int mirror_display_init(void) {
119 g_mirror_tty_info = get_current_tty();
120
121 // Only use TTY output if stdout is also a TTY (respects shell redirection)
122 // This ensures `cmd > file` works by detecting stdout redirection
123 bool stdout_is_tty = platform_isatty(STDOUT_FILENO) != 0;
124 if (g_mirror_tty_info.fd >= 0 && stdout_is_tty) {
125 g_mirror_has_tty = platform_isatty(g_mirror_tty_info.fd) != 0;
126 } else {
127 g_mirror_has_tty = false;
128 }
129
130 // Initialize ASCII output
131 ascii_write_init(g_mirror_tty_info.fd, !(GET_OPTION(snapshot_mode)));
132
133 return 0;
134}
135
139static void mirror_display_cleanup(void) {
140 ascii_write_destroy(g_mirror_tty_info.fd, true);
141
142 if (g_mirror_tty_info.owns_fd && g_mirror_tty_info.fd >= 0) {
143 platform_close(g_mirror_tty_info.fd);
144 g_mirror_tty_info.fd = -1;
145 g_mirror_tty_info.owns_fd = false;
146 }
147
148 g_mirror_has_tty = false;
149}
150
156static void mirror_write_frame(const char *frame_data) {
157 if (!frame_data) {
158 return;
159 }
160
161 size_t frame_len = strnlen(frame_data, 1024 * 1024);
162 if (frame_len == 0) {
163 return;
164 }
165
166 if (g_mirror_has_tty && g_mirror_tty_info.fd >= 0) {
167 cursor_reset(g_mirror_tty_info.fd);
168 platform_write(g_mirror_tty_info.fd, frame_data, frame_len);
169 terminal_flush(g_mirror_tty_info.fd);
170 } else {
171 // Expand RLE for pipe/file output where terminals can't interpret REP sequences
172 char *expanded = ansi_expand_rle(frame_data, frame_len);
173 const char *output_data = expanded ? expanded : frame_data;
174 size_t output_len = expanded ? strlen(expanded) : frame_len;
175
176 // Strip all ANSI escape sequences if --strip-ansi is set
177 char *stripped = NULL;
178 if (GET_OPTION(strip_ansi)) {
179 stripped = ansi_strip_escapes(output_data, output_len);
180 if (stripped) {
181 output_data = stripped;
182 output_len = strlen(stripped);
183 }
184 }
185
186 if (!GET_OPTION(snapshot_mode)) {
187 cursor_reset(STDOUT_FILENO);
188 }
189 platform_write(STDOUT_FILENO, output_data, output_len);
190 platform_write(STDOUT_FILENO, "\n", 1); // Trailing newline after frame
191 (void)fflush(stdout);
192 SAFE_FREE(stripped);
193 SAFE_FREE(expanded);
194 }
195}
196
197/* ============================================================================
198 * Mirror Mode Main Loop
199 * ============================================================================ */
200
209int mirror_main(void) {
210 log_info("Starting mirror mode");
211
212 // Install console control-c handler
213 platform_set_console_ctrl_handler(mirror_console_ctrl_handler);
214
215#ifndef _WIN32
216 platform_signal(SIGPIPE, SIG_IGN);
217#endif
218
219 // Initialize webcam
220 int webcam_result = webcam_init(GET_OPTION(webcam_index));
221 if (webcam_result != 0) {
222 log_fatal("Failed to initialize webcam: %s", asciichat_error_string(webcam_result));
223 webcam_print_init_error_help(webcam_result);
224 return webcam_result;
225 }
226
227 // Initialize display
228 if (mirror_display_init() != 0) {
229 log_fatal("Failed to initialize display");
231 return ERROR_DISPLAY;
232 }
233
234 // Detect terminal capabilities
236 caps = apply_color_mode_override(caps);
237
238 // Initialize ANSI color lookup tables based on terminal capabilities
239 if (caps.color_level == TERM_COLOR_TRUECOLOR) {
241 } else if (caps.color_level == TERM_COLOR_256) {
243 } else if (caps.color_level == TERM_COLOR_16) {
245 }
246
247 // Initialize palette
248 char palette_chars[256] = {0};
249 size_t palette_len = 0;
250 char luminance_palette[256] = {0};
251
252 const char *custom_chars = GET_OPTION(palette_custom_set) ? GET_OPTION(palette_custom) : NULL;
253 palette_type_t palette_type = GET_OPTION(palette_type);
254 if (initialize_client_palette(palette_type, custom_chars, palette_chars, &palette_len, luminance_palette) != 0) {
255 log_fatal("Failed to initialize palette");
256 mirror_display_cleanup();
258 return ERROR_INVALID_STATE;
259 }
260
261 // Frame rate limiting
262 const long frame_interval_ms = 1000 / 60; // 60 FPS target
263 struct timespec last_frame_time = {0, 0};
264
265 // Snapshot mode timing
266 struct timespec snapshot_start_time = {0, 0};
267 bool snapshot_done = false;
268 if (GET_OPTION(snapshot_mode)) {
269 (void)clock_gettime(CLOCK_MONOTONIC, &snapshot_start_time);
270 }
271
272 // FPS tracking
273 uint64_t frame_count = 0;
274 struct timespec fps_report_time;
275 (void)clock_gettime(CLOCK_MONOTONIC, &fps_report_time);
276
277 log_info("Mirror mode running - press Ctrl+C to exit");
279
280 while (!mirror_should_exit()) {
281 // Frame rate limiting
282 struct timespec current_time;
283 (void)clock_gettime(CLOCK_MONOTONIC, &current_time);
284
285 long elapsed_ms = (current_time.tv_sec - last_frame_time.tv_sec) * 1000 +
286 (current_time.tv_nsec - last_frame_time.tv_nsec) / 1000000;
287
288 if (elapsed_ms < frame_interval_ms) {
289 platform_sleep_usec((frame_interval_ms - elapsed_ms) * 1000);
290 continue;
291 }
292
293 // Snapshot mode: check if delay has elapsed (delay 0 = capture first frame immediately)
294 if (GET_OPTION(snapshot_mode) && !snapshot_done) {
295 double elapsed_sec = (double)(current_time.tv_sec - snapshot_start_time.tv_sec) +
296 (double)(current_time.tv_nsec - snapshot_start_time.tv_nsec) / 1e9;
297
298 float snapshot_delay = GET_OPTION(snapshot_delay);
299 if (elapsed_sec >= snapshot_delay) {
300 snapshot_done = true;
301 }
302 }
303
304 // Read frame from webcam
305 image_t *image = webcam_read();
306 if (!image) {
307 platform_sleep_usec(10000); // 10ms delay before retry
308 continue;
309 }
310
311 // Convert image to ASCII
312 // When stretch is 0 (disabled), we preserve aspect ratio (true)
313 // When stretch is 1 (enabled), we allow stretching without aspect ratio preservation (false)
314 bool stretch = GET_OPTION(stretch);
315 unsigned short int width = GET_OPTION(width);
316 unsigned short int height = GET_OPTION(height);
317 bool preserve_aspect_ratio = !stretch;
318 char *ascii_frame = ascii_convert_with_capabilities(image, width, height, &caps, preserve_aspect_ratio, stretch,
319 palette_chars, luminance_palette);
320
321 if (ascii_frame) {
322 // When piping/redirecting in snapshot mode, only output the final frame
323 // When outputting to TTY, show live preview frames
324 bool snapshot_mode = GET_OPTION(snapshot_mode);
325 bool should_write = !snapshot_mode || g_mirror_has_tty || snapshot_done;
326 if (should_write) {
327 mirror_write_frame(ascii_frame);
328 }
329
330 // Snapshot mode: exit after capturing the final frame
331 if (snapshot_mode && snapshot_done) {
332 SAFE_FREE(ascii_frame);
333 image_destroy(image);
334 break;
335 }
336
337 SAFE_FREE(ascii_frame);
338 frame_count++;
339 }
340
341 image_destroy(image);
342 last_frame_time = current_time;
343
344 // FPS reporting every 5 seconds
345 uint64_t fps_elapsed_us = ((uint64_t)current_time.tv_sec * 1000000 + (uint64_t)current_time.tv_nsec / 1000) -
346 ((uint64_t)fps_report_time.tv_sec * 1000000 + (uint64_t)fps_report_time.tv_nsec / 1000);
347
348 if (fps_elapsed_us >= 5000000) {
349 double fps = (double)frame_count / ((double)fps_elapsed_us / 1000000.0);
350 log_debug("Mirror FPS: %.1f", fps);
351 frame_count = 0;
352 fps_report_time = current_time;
353 }
354 }
355
356 // Cleanup
358 log_info("Mirror mode shutting down");
359
360 mirror_display_cleanup();
362
363 return 0;
364}
🔌 Cross-platform abstraction layer umbrella header for ascii-chat
char * ansi_strip_escapes(const char *input, size_t input_len)
Strip all ANSI escape sequences from a string.
Definition ansi.c:13
ANSI escape sequence utilities.
Fast ANSI escape sequence generation.
🖼️ ASCII Art Conversion and Output Interface
#define SAFE_FREE(ptr)
Definition common.h:320
unsigned long long uint64_t
Definition common.h:59
@ ERROR_INVALID_STATE
@ ERROR_DISPLAY
Definition error_codes.h:99
#define log_fatal(...)
Log a FATAL message.
#define log_info(...)
Log an INFO message.
#define log_debug(...)
Log a DEBUG message.
void log_set_terminal_output(bool enabled)
Control stderr output to terminal.
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
Definition options.h:644
int initialize_client_palette(palette_type_t palette_type, const char *custom_chars, char client_palette_chars[256], size_t *client_palette_len, char client_luminance_palette[256])
Initialize client palette with full configuration.
Definition palette.c:326
palette_type_t
Built-in palette type enumeration.
Definition palette.h:84
signal_handler_t platform_signal(int sig, signal_handler_t handler)
Set a signal handler.
terminal_capabilities_t apply_color_mode_override(terminal_capabilities_t caps)
Apply command-line overrides to detected capabilities.
int platform_isatty(int fd)
Check if a file descriptor is a terminal.
tty_info_t get_current_tty(void)
Get current TTY information.
bool platform_set_console_ctrl_handler(console_ctrl_handler_t handler)
Register a console control handler (for Ctrl+C, etc.)
int platform_close(int fd)
Safe file close (close replacement)
void platform_sleep_usec(unsigned int usec)
High-precision sleep function with microsecond precision.
terminal_capabilities_t detect_terminal_capabilities(void)
Detect terminal capabilities.
asciichat_error_t terminal_flush(int fd)
Flush terminal output.
console_ctrl_event_t
Console control event types (cross-platform Ctrl+C handling)
Definition system.h:181
ssize_t platform_write(int fd, const void *buf, size_t count)
Platform-safe write function.
@ TERM_COLOR_16
16-color support (standard ANSI colors)
Definition terminal.h:430
@ TERM_COLOR_256
256-color support (extended ANSI palette)
Definition terminal.h:432
@ TERM_COLOR_TRUECOLOR
24-bit truecolor support (RGB colors)
Definition terminal.h:434
@ CONSOLE_CTRL_BREAK
Definition system.h:183
@ CONSOLE_CTRL_C
Definition system.h:182
void ansi_fast_init_256color(void)
Initialize 256-color mode lookup tables.
Definition ansi_fast.c:179
asciichat_error_t ascii_write_init(int fd, bool reset_terminal)
Initialize ASCII write subsystem.
Definition ascii.c:40
void ascii_write_destroy(int fd, bool reset_terminal)
Destroy ASCII write subsystem.
Definition ascii.c:334
void ansi_fast_init(void)
Initialize the decimal lookup table.
Definition ansi_fast.c:173
#define cursor_reset(fd)
Reset cursor to home position.
Definition ascii.h:535
void ansi_fast_init_16color(void)
Initialize 16-color mode lookup tables.
Definition ansi_fast.c:225
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
Definition video/image.c:85
char * ansi_expand_rle(const char *input, size_t input_len)
Expand RLE escape sequences in a string.
Definition rle.c:13
char * ascii_convert_with_capabilities(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 char luminance_palette[256])
Convert image to ASCII art with terminal capability awareness.
Definition ascii.c:188
asciichat_error_t webcam_init(unsigned short int webcam_index)
Initialize global webcam interface.
Definition webcam.c:18
image_t * webcam_read(void)
Capture a frame from global webcam.
Definition webcam.c:57
void webcam_print_init_error_help(asciichat_error_t error_code)
Print helpful error diagnostics for webcam initialization failures.
Definition webcam.c:229
void webcam_cleanup(void)
Clean up global webcam interface.
Definition webcam.c:204
int mirror_main(void)
Run mirror mode main loop.
⚙️ Command-line options parsing and configuration management for ascii-chat
ASCII Palette Management for Video-to-ASCII Conversion.
ANSI RLE (REP) sequence compression and expansion.
ascii-chat Server Mode Entry Point Header
Image structure.
Complete terminal capabilities structure.
Definition terminal.h:485
terminal_color_mode_t color_level
Detected color support level (terminal_color_mode_t)
Definition terminal.h:487
TTY detection and management structure.
Definition terminal.h:523
bool owns_fd
True if we opened the FD and should close it, false otherwise.
Definition terminal.h:529
int fd
File descriptor for TTY access.
Definition terminal.h:525
🖥️ Cross-platform terminal interface for ascii-chat
⏱️ High-precision timing utilities using sokol_time.h and uthash
Image Data Structures and Operations.