ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
ascii.h File Reference

Go to the source code of this file.

Data Structures

struct  ascii_frame_source_t
 Frame source structure for grid layout. More...

Macros

#define ASCII_LUMINANCE_LEVELS   256
 Number of luminance levels supported (256).
#define ASCII_SLEEP_NS   50000L
 Sleep duration in nanoseconds between frames (50000 ns = 50 μs).
#define ANSI_FG_PREFIX   "\033[38;2;"
 ANSI foreground color prefix (truecolor mode).
#define ANSI_BG_PREFIX   "\033[48;2;"
 ANSI background color prefix (truecolor mode).
#define ANSI_COLOR_SUFFIX   "m"
 ANSI color suffix.
#define print(s)
 Print string to stdout.
#define console_clear(fd)
 Clear console and move cursor to home position.
#define cursor_reset(fd)
 Reset cursor to home position.
#define ascii_clear_screen(fd)
 Clear terminal screen.
#define ascii_zzz()
 Sleep for frame rate limiting.

Functions

asciichat_error_t ascii_write_init (int fd, bool reset_terminal)
 Initialize ASCII write subsystem.
char * ascii_convert (image_t *original, const ssize_t width, const ssize_t height, const bool color, const bool aspect_ratio, const bool stretch, const char *palette_chars, const char luminance_palette[ASCII_LUMINANCE_LEVELS])
 Convert image to ASCII art.
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)
 Convert image to ASCII art with terminal capability awareness.
char * ascii_convert_with_capabilities_fit (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)
char * ascii_convert_with_capabilities_workspace (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)
char * ascii_convert_with_buffers (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)
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)
char * image_print_with_capabilities (const image_t *image, const terminal_capabilities_t *caps, const char *palette)
 Convert image to ASCII art with terminal capabilities.
asciichat_error_t ascii_write (const char *frame)
 Write ASCII frame to terminal.
void ascii_write_destroy (int fd, bool reset_terminal)
 Destroy ASCII write subsystem.
char * ascii_pad_frame_width (const char *frame, size_t pad)
 Add leading spaces (left-padding) to each line of a frame.
char * ascii_pad_frame_height (const char *frame, size_t pad_top)
 Add blank lines (vertical padding) to center a frame vertically.
asciichat_error_t ascii_grid_layout (int source_count, int width, int height, int *grid_cols, int *grid_rows)
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.
char * get_lum_palette (void)
 Get luminance palette for character mapping.

Variables

char ascii_palette []
 Non-owning source reference used when laying out an ASCII frame. frame_data points to the source text and frame_size tells the compositor how many bytes are valid; the source must remain alive until layout and copy-out finish.

Function Documentation

◆ ascii_convert_with_buffers()

char * ascii_convert_with_buffers ( 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 )

Borrow output from the caller-owned buffer until its next conversion or release. Both workspaces require a single owner; passing NULL output keeps owned-output semantics.

Definition at line 286 of file ascii.c.

288 {
289 return ascii_convert_with_buffers_stabilized(original, width, height, caps, use_aspect_ratio, stretch, palette_chars,
290 contain, workspace, output, NULL);
291}
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
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23

References ascii_convert_with_buffers_stabilized(), height, and width.

Referenced by ascii_convert_with_capabilities_workspace().

◆ ascii_convert_with_buffers_stabilized()

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 )

Convert with a history owned by this stream; reset it when the source changes.

Definition at line 293 of file ascii.c.

296 {
297 if (original == NULL || caps == NULL) {
298 log_error("Invalid parameters for ascii_convert_with_capabilities");
299 return NULL;
300 }
301
302 // Validate original image dimensions to detect corruption
303 if (original->w <= 0 || original->w > 10000 || original->h <= 0 || original->h > 10000) {
304 log_error("Invalid original image dimensions detected: w=%d, h=%d (likely corrupted)", original->w, original->h);
305 return NULL;
306 }
307
308 if (original->pixels == NULL) {
309 log_error("Original image pixels pointer is NULL");
310 return NULL;
311 }
312
313 // Start with the target dimensions requested by the user.
314 ssize_t resized_width = width;
315 ssize_t resized_height = height;
316
317 // Both contain and cover produce a complete viewport, including any black bars.
318
319 // Preserve an 8x8 sample grid for each glyph in the experimental renderer.
320 bool explicit_palette = caps->palette_explicit;
321 if (caps->text_renderer == ASCII_RENDERER_GLYPH && explicit_palette &&
322 !ascii_glyph_palette_supported(palette_chars)) {
323 SET_ERRNO(ERROR_INVALID_PARAM, "Glyph palette contains characters without glyph masks");
324 return NULL;
325 }
326 bool glyph_match = caps->text_renderer == ASCII_RENDERER_GLYPH && caps->render_mode != RENDER_MODE_HALF_BLOCK &&
327 resized_width > 0 && resized_height > 0 && resized_width <= IMAGE_MAX_WIDTH / 8 &&
328 resized_height <= IMAGE_MAX_HEIGHT / 8;
329 if (glyph_match) {
330 resized_width *= 8;
331 resized_height *= 8;
332 }
333
334 // Half-block mode doubles height for 2x vertical resolution (AFTER aspect ratio)
335 if (caps->render_mode == RENDER_MODE_HALF_BLOCK) {
336 resized_height = resized_height * 2;
337 }
338
339 // Fitting is performed inside the pixel viewport, so ANSI padding is not needed.
340 size_t pad_width = 0;
341 size_t pad_height = 0;
342
343 // Resize the captured frame to the aspect-correct dimensions
344 if (resized_width <= 0 || resized_height <= 0) {
345 log_error("Invalid dimensions for resize: width=%zd, height=%zd", resized_width, resized_height);
346 return NULL;
347 }
348
349 // Validate dimensions fit in image_t's int fields before casting
350 if (resized_width > INT_MAX || resized_height > INT_MAX) {
351 log_error("Dimensions exceed INT_MAX: width=%zd, height=%zd", resized_width, resized_height);
352 return NULL;
353 }
354
355 // PROFILING: Time image allocation and resize
356 uint64_t prof_alloc_start_ns = time_get_ns();
357
358 if (workspace && *workspace && ((*workspace)->w != resized_width || (*workspace)->h != resized_height)) {
359 image_destroy(*workspace);
360 *workspace = NULL;
361 }
362 image_t *resized = workspace ? *workspace : NULL;
363 if (!resized)
364 resized = image_new((size_t)resized_width, (size_t)resized_height);
365 if (workspace)
366 *workspace = resized;
367 if (!resized) {
368 log_error("Failed to allocate resized image");
369 return NULL;
370 }
371
372 image_clear(resized);
373
374 uint64_t prof_alloc_end_ns = time_get_ns();
375
376 uint64_t prof_resize_start_ns = prof_alloc_end_ns;
377
378 if (use_aspect_ratio && !stretch) {
379 // Fit using terminal cells; half-block pixel height is only the sampling resolution.
380 if (contain)
381 image_resize_contain(original, resized, width, height);
382 else
383 image_resize_cover(original, resized, width, height);
384 } else {
385 image_resize(original, resized);
386 }
387
388 uint64_t prof_resize_end_ns = time_get_ns();
389
390 // Check original image before resize (validate dimensions to prevent integer overflow)
391 int orig_black = 0, orig_total = 0;
392 if (original->w > 0 && original->w <= 10000 && original->h > 0 && original->h <= 10000) {
393 int64_t total_pixels = (int64_t)original->w * original->h;
394 int sample_size = (total_pixels < 1000) ? (int)total_pixels : 1000;
395 for (int i = 0; i < sample_size; i++) {
396 rgb_pixel_t p = original->pixels[i];
397 orig_total++;
398 if (p.r == 0 && p.g == 0 && p.b == 0)
399 orig_black++;
400 }
402 ">>> ORIGINAL image: %d/%d pixels are black (%.1f%%), first pixel=RGB(%d,%d,%d)\n", orig_black,
403 orig_total, orig_total > 0 ? 100.0 * orig_black / orig_total : 0, original->pixels[0].r,
404 original->pixels[0].g, original->pixels[0].b);
405 } else {
406 log_warn("Skipping debug check: corrupted image dimensions (w=%d, h=%d)", original->w, original->h);
407 }
408
409 // PROFILING: Time ASCII print
410 uint64_t prof_print_start_ns = prof_resize_end_ns;
411
412 // DEBUG: Log dimensions going to renderer
413 log_debug_every(10 * US_PER_SEC_INT, "original=%dx%d, requested=%zdx%zd, resized=%dx%d, pad=%zux%zu (mode=%d)",
414 original->w, original->h, width, height, resized->w, resized->h, pad_width, pad_height,
415 caps->render_mode);
416
417 // Check if image is all black
418 int black_pixels = 0, total_pixels = 0;
419 for (int i = 0; i < resized->w * resized->h && i < 1000; i++) {
420 rgb_pixel_t p = resized->pixels[i];
421 total_pixels++;
422 if (p.r == 0 && p.g == 0 && p.b == 0)
423 black_pixels++;
424 }
426 ">>> Resized image check: %d/%d pixels are black (%.1f%%), first pixel=RGB(%d,%d,%d)\n", black_pixels,
427 total_pixels, total_pixels > 0 ? 100.0 * black_pixels / total_pixels : 0, resized->pixels[0].r,
428 resized->pixels[0].g, resized->pixels[0].b);
429
430 // Apply the target client's filter after resizing, so it is scoped to this
431 // client and never mutates the shared source frame.
432 if (caps->color_filter != COLOR_FILTER_NONE) {
433 const float time_seconds = (float)time_get_ns() / (float)NS_PER_SEC_INT;
434 if (apply_color_filter((uint8_t *)resized->pixels, (uint32_t)resized->w, (uint32_t)resized->h,
435 (uint32_t)(resized->w * sizeof(rgb_pixel_t)), caps->color_filter, time_seconds) != 0) {
436 log_warn("Failed to apply client color filter %d", caps->color_filter);
437 }
438 }
439
440 // Use the capability-aware image printing function with client's palette
441 // History belongs to this stream and is scoped to the current thread's conversion.
442 ascii_stabilizer_t *active_stabilizer = NULL;
443 if (stabilizer) {
444 uint64_t configuration = 1469598103934665603ULL;
445 const unsigned values[] = {(unsigned)caps->text_renderer,
446 (unsigned)caps->color_level,
447 (unsigned)caps->render_mode,
448 (unsigned)caps->color_filter,
449 (unsigned)explicit_palette,
450 (unsigned)use_aspect_ratio,
451 (unsigned)stretch,
452 (unsigned)contain,
453 (unsigned)original->w,
454 (unsigned)original->h};
455 for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); ++i) {
456 configuration ^= values[i];
457 // FNV multiplication intentionally wraps modulo 2^64.
458 (void)__builtin_mul_overflow(configuration, 1099511628211ULL, &configuration);
459 }
460 if (palette_chars) {
461 for (const unsigned char *p = (const unsigned char *)palette_chars; *p; ++p) {
462 configuration ^= *p;
463 (void)__builtin_mul_overflow(configuration, 1099511628211ULL, &configuration);
464 }
465 }
466 size_t history_height = (size_t)height * (caps->render_mode == RENDER_MODE_HALF_BLOCK ? 2u : 1u);
467 if (ascii_stabilizer_prepare(stabilizer, (size_t)width, history_height, configuration) == ASCIICHAT_OK) {
468 uint64_t now = time_get_ns();
469 stabilizer->elapsed_ns = stabilizer->fixed_step_ns
470 ? stabilizer->fixed_step_ns
471 : (stabilizer->last_timestamp_ns && now >= stabilizer->last_timestamp_ns
472 ? now - stabilizer->last_timestamp_ns
473 : UINT64_MAX);
474 stabilizer->last_timestamp_ns = now;
475 active_stabilizer = stabilizer;
476 }
477 }
478 ascii_stabilizer_t *previous_stabilizer = ascii_stabilizer_set_target(active_stabilizer);
479 outbuf_t *previous_output = ascii_render_set_output_target(output);
480 ascii_color_t glyph_color = caps->color_level == TERM_COLOR_TRUECOLOR ? ASCII_COLOR_TRUE
481 : caps->color_level == TERM_COLOR_256 ? ASCII_COLOR_256
482 : caps->color_level == TERM_COLOR_16 ? ASCII_COLOR_16
484 char *ascii = glyph_match ? ascii_render_glyph_match_mode(resized, glyph_color, simd_cells_backend(),
485 explicit_palette ? palette_chars : NULL,
486 caps->render_mode == RENDER_MODE_BACKGROUND)
487 : image_print_with_capabilities(resized, caps, palette_chars);
488 ascii_render_set_output_target(previous_output);
489 ascii_stabilizer_set_target(previous_stabilizer);
490
491 uint64_t prof_print_end_ns = time_get_ns();
492
493 uint64_t alloc_time_us = time_ns_to_us(time_elapsed_ns(prof_alloc_start_ns, prof_alloc_end_ns));
494 uint64_t resize_time_us = time_ns_to_us(time_elapsed_ns(prof_resize_start_ns, prof_resize_end_ns));
495 uint64_t print_time_us = time_ns_to_us(time_elapsed_ns(prof_print_start_ns, prof_print_end_ns));
496
497 // PROFILING: Time padding
498 uint64_t prof_pad_start_ns = time_get_ns();
499
500 if (!ascii) {
501 log_error("Failed to convert image to ASCII using terminal capabilities");
502 if (!workspace)
503 image_destroy(resized);
504 return NULL;
505 }
506
507 size_t ascii_len = strlen(ascii);
508 if (ascii_len == 0) {
509 log_error("Capability-aware ASCII conversion returned empty string (resized dimensions: %dx%d)", resized->w,
510 resized->h);
511 if (!output)
512 SAFE_FREE(ascii);
513 if (!workspace)
514 image_destroy(resized);
515 return NULL;
516 }
517
518 // Pixel fitting already produced the complete viewport. Return that buffer
519 // directly instead of allocating two identical copies for zero padding.
520 char *ascii_padded = ascii;
521
522 uint64_t prof_pad_end_ns = time_get_ns();
523
524 uint64_t pad_time_ns = time_elapsed_ns(prof_pad_start_ns, prof_pad_end_ns);
525 log_debug_every(US_PER_SEC_INT, "ASCII_STAGE: alloc_ms=%.3f resize_ms=%.3f glyph_ms=%.3f padding_ms=%.3f bytes=%zu",
526 (double)alloc_time_us / 1000, (double)resize_time_us / 1000, (double)print_time_us / 1000,
527 (double)pad_time_ns / NS_PER_MS_INT, ascii_len);
528
529 if (!workspace)
530 image_destroy(resized);
531
532 return ascii_padded;
533}
#define SET_ERRNO(code,...)
const simd_cells_t * simd_cells_backend(void)
Definition cells.c:70
unsigned int uint32_t
Definition common.h:62
#define SAFE_FREE(ptr)
Definition common.h:377
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define NS_PER_SEC_INT
Definition time.h:169
#define NS_PER_MS_INT
Definition time.h:168
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition time.c:123
#define US_PER_SEC_INT
Definition time.h:173
@ ASCII_RENDERER_GLYPH
Definition options.h:966
char * image_print_with_capabilities(const image_t *image, const terminal_capabilities_t *caps, const char *palette)
Convert image to ASCII art with terminal capabilities.
Definition ascii.c:918
int apply_color_filter(uint8_t *pixels, uint32_t width, uint32_t height, uint32_t stride, color_filter_t filter, float time)
Apply color filter to entire image in-place.
#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 image_clear(image_t *p)
Clear image (set all pixels to black).
void image_resize(const image_t *s, image_t *d)
Resize image using nearest-neighbor interpolation.
void image_destroy(image_t *image)
Destroy an image allocated with image_new().
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
asciichat_error_t ascii_stabilizer_prepare(ascii_stabilizer_t *state, size_t width, size_t height, uint64_t configuration)
ascii_stabilizer_t * ascii_stabilizer_set_target(ascii_stabilizer_t *state)
bool ascii_glyph_palette_supported(const char *palette)
outbuf_t * ascii_render_set_output_target(outbuf_t *target)
char * ascii_render_glyph_match_mode(const image_t *image, ascii_color_t mode, const simd_cells_t *ops, const char *palette, bool background)
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
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).
Dynamic output buffer (auto-expanding).
uint8_t b
Blue color component (0-255).
uint8_t g
Green color component (0-255).
uint8_t r
Red color component (0-255).

References apply_color_filter(), ASCII_COLOR_16, ASCII_COLOR_256, ASCII_COLOR_NONE, ASCII_COLOR_TRUE, ascii_glyph_palette_supported(), ascii_render_glyph_match_mode(), ascii_render_set_output_target(), ASCII_RENDERER_GLYPH, ascii_stabilizer_prepare(), ascii_stabilizer_set_target(), ASCIICHAT_OK, rgb_pixel_t::b, ascii_stabilizer_t::elapsed_ns, ERROR_INVALID_PARAM, ascii_stabilizer_t::fixed_step_ns, rgb_pixel_t::g, image_t::h, height, image_clear(), image_destroy(), IMAGE_MAX_HEIGHT, IMAGE_MAX_WIDTH, image_new(), image_print_with_capabilities(), image_resize(), ascii_stabilizer_t::last_timestamp_ns, log_debug_every, log_error, log_warn, NS_PER_MS_INT, NS_PER_SEC_INT, image_t::pixels, rgb_pixel_t::r, SAFE_FREE, SET_ERRNO, simd_cells_backend(), time_elapsed_ns(), US_PER_SEC_INT, image_t::w, and width.

Referenced by ascii_convert_with_buffers(), and mirror_convert_frame().

◆ ascii_convert_with_capabilities_fit()

char * ascii_convert_with_capabilities_fit ( 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 )

Convert with explicit contain (true) or cover (false) viewport fitting.

Definition at line 269 of file ascii.c.

271 {
272
273 return ascii_convert_with_capabilities_workspace(original, width, height, caps, use_aspect_ratio, stretch,
274 palette_chars, contain, NULL);
275}
char * ascii_convert_with_capabilities_workspace(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)
Definition ascii.c:277

References ascii_convert_with_capabilities_workspace(), height, and width.

Referenced by ascii_convert_with_capabilities().

◆ ascii_convert_with_capabilities_workspace()

char * ascii_convert_with_capabilities_workspace ( 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 )

Caller owns the reusable resize image; calls using it must be serialized.

Definition at line 277 of file ascii.c.

280 {
281
282 return ascii_convert_with_buffers(original, width, height, caps, use_aspect_ratio, stretch, palette_chars, contain,
283 workspace, NULL);
284}
char * ascii_convert_with_buffers(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)
Definition ascii.c:286

References ascii_convert_with_buffers(), height, and width.

Referenced by ascii_convert_with_capabilities_fit().

◆ ascii_grid_layout()

asciichat_error_t ascii_grid_layout ( int source_count,
int width,
int height,
int * grid_cols,
int * grid_rows )

Calculate the shared grid layout before sizing source video tiles.

Definition at line 659 of file ascii.c.

659 {
660 if (source_count <= 0 || width <= 0 || height <= 0 || !grid_cols || !grid_rows)
661 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid grid dimensions");
662 // Multiple sources: create grid layout
663 // Calculate grid dimensions that maximize the use of terminal space
664 // Character aspect ratio: terminal chars are typically ~2x taller than wide
665 float char_aspect = 2.0f;
666
667 float best_score = -1.0f;
668 int best_cols = 1;
669 int best_rows = source_count;
670
671 // Try all possible grid configurations
672 for (int test_cols = 1; test_cols <= source_count; test_cols++) {
673 int test_rows = (int)ceil((double)source_count / test_cols);
674
675 // Skip configurations with too many empty cells
676 int empty_cells = (test_cols * test_rows) - source_count;
677 if (empty_cells > source_count / 2)
678 continue; // Don't waste more than 50% space
679
680 // Calculate the size each cell would have
681 int cell_width = (width - (test_cols - 1)) / test_cols; // -1 per separator
682 int cell_height = (height - (test_rows - 1)) / test_rows; // -1 per separator
683
684 // Skip if cells would be too small
685 if (cell_width < 10 || cell_height < 3)
686 continue;
687
688 // Calculate the aspect ratio of each cell (accounting for char aspect)
689 float cell_aspect = ((float)cell_width / (float)cell_height) / char_aspect;
690
691 // Score based on how close to square (1:1) each video cell would be
692 // This naturally adapts to any terminal size
693 float aspect_score = 1.0f - fabsf(logf(cell_aspect)); // log makes it symmetric around 1
694 if (aspect_score < 0)
695 aspect_score = 0;
696
697 // Bonus for better space utilization
698 float utilization = (float)source_count / (float)(test_cols * test_rows);
699
700 // For 2 clients specifically, heavily weight the aspect score
701 // This makes 2 clients naturally go horizontal on wide terminals and vertical on tall ones
702 float total_score;
703 if (source_count == 2) {
704 // For 2 clients, we want the layout that gives the most square-ish cells
705 total_score = aspect_score * 0.9f + utilization * 0.1f;
706 } else {
707 // For 3+ clients, balance aspect ratio with space utilization
708 total_score = aspect_score * 0.7f + utilization * 0.3f;
709 }
710
711 // Small bonus for simpler grids (prefer 2x2 over 3x1, etc.)
712 if (test_cols == test_rows) {
713 total_score += 0.05f; // Slight preference for square grids
714 }
715
716 if (total_score > best_score) {
717 best_score = total_score;
718 best_cols = test_cols;
719 best_rows = test_rows;
720 }
721 }
722
723 *grid_cols = best_cols;
724 *grid_rows = best_rows;
725 return ASCIICHAT_OK;
726}

References ASCIICHAT_OK, ERROR_INVALID_PARAM, height, SET_ERRNO, and width.

Referenced by ascii_create_grid().