ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
layout.c
Go to the documentation of this file.
1
9#include <ascii-chat/options/layout.h>
10#include <ascii-chat/log/logging.h>
11#include <ascii-chat/util/utf8.h>
12#include <ascii-chat/video/ansi.h>
13#include <ascii-chat/common.h>
14#include <string.h>
15#include <stdio.h>
16
17/* ============================================================================
18 * Colored Segment Printing
19 * ============================================================================ */
20
27static void layout_print_colored_segment(FILE *stream, const char *seg) {
28 if (!seg || !stream)
29 return;
30
31 // Just print the text as-is. All coloring is done in builder.c using colored_string()
32 // before passing to layout functions. Layout functions only handle positioning and wrapping.
33 fprintf(stream, "%s", seg);
34}
35
36/* ============================================================================
37 * Wrapped Description Printing
38 * ============================================================================ */
39
45static int calculate_segment_display_width(const char *text, int len) {
46 if (!text || len <= 0)
47 return 0;
48
49 // Create a stripped version for width calculation
50 char *stripped = ansi_strip_escapes(text, len);
51 if (!stripped)
52 return 0;
53
54 // Calculate display width of stripped text
55 int width = utf8_display_width_n(stripped, strlen(stripped));
56 SAFE_FREE(stripped);
57
58 return width < 0 ? 0 : width;
59}
60
67static bool is_metadata_start(const char *p) {
68 if (!p || *p != '(')
69 return false;
70
71 // Strip ANSI codes to check the actual text
72 char buf[256];
73 const char *src = p;
74 char *dst = buf;
75 int remaining = sizeof(buf) - 1;
76
77 // Copy up to 20 chars, stripping ANSI codes
78 int count = 0;
79 while (*src && count < 20 && remaining > 0) {
80 if (*src == '\x1b') {
81 // Skip ANSI escape sequence
82 src++;
83 while (*src && *src != 'm')
84 src++;
85 if (*src == 'm')
86 src++;
87 } else {
88 *dst++ = *src++;
89 remaining--;
90 count++;
91 }
92 }
93 *dst = '\0';
94
95 return strncmp(buf, "(default:", 9) == 0 || strncmp(buf, "(env:", 5) == 0;
96}
97
98void layout_print_wrapped_description(FILE *stream, const char *text, int indent_width, int term_width) {
99 if (!text || !stream)
100 return;
101
102 // Default terminal width if not specified
103 if (term_width <= 0)
104 term_width = 80;
105
106 // Available width for text after indentation, capped at 130 for readability
107 // This allows the description column itself to be up to 130 chars wide,
108 // starting from indent_width, for a max total line of indent_width + 130
109 int available_width = term_width - indent_width;
110 if (available_width > 130)
111 available_width = 130;
112 if (available_width < 20)
113 available_width = 20;
114
115 const char *line_start = text;
116 const char *last_space = NULL;
117 const char *p = text;
118 bool inside_metadata = false;
119
120 while (*p) {
121 // Check for explicit newline
122 if (*p == '\n') {
123 // Print everything up to the newline
124 int text_len = p - line_start;
125 if (text_len > 0) {
126 char seg[BUFFER_SIZE_MEDIUM];
127 SAFE_STRNCPY(seg, line_start, text_len + 1);
128 layout_print_colored_segment(stream, seg);
129 }
130
131 fprintf(stream, "\n");
132 if (*(p + 1)) {
133 for (int i = 0; i < indent_width; i++)
134 fprintf(stream, " ");
135 }
136 p++;
137 line_start = p;
138 last_space = NULL;
139 inside_metadata = false;
140 continue;
141 }
142
143 // Check for start of metadata blocks (must check before space tracking)
144 // Use helper that accounts for ANSI codes in the metadata
145 if (!inside_metadata && is_metadata_start(p)) {
146 inside_metadata = true;
147 }
148
149 // Check for end of metadata blocks - always exit on )
150 // We'll re-enter when we see the next metadata marker
151 if (inside_metadata && *p == ')') {
152 inside_metadata = false;
153 }
154
155 // Track spaces for word wrapping (but NOT inside metadata blocks)
156 if (*p == ' ' && !inside_metadata) {
157 last_space = p;
158 }
159
160 // Decode UTF-8 character length
161 int char_bytes = utf8_next_char_bytes(p, strlen(p));
162
163 if (char_bytes <= 0) {
164 // Invalid UTF-8 or end of string, stop
165 break;
166 }
167
168 // Advance to next character first
169 p += char_bytes;
170
171 // Calculate actual display width from line_start to current position (excluding ANSI codes)
172 int actual_width = calculate_segment_display_width(line_start, p - line_start);
173
174 // Check if we need to wrap
175 if (actual_width >= available_width && last_space && last_space > line_start) {
176 // Print text up to break point with colors applied
177 int text_len = last_space - line_start;
178 char seg[BUFFER_SIZE_MEDIUM];
179 SAFE_STRNCPY(seg, line_start, text_len + 1);
180 layout_print_colored_segment(stream, seg);
181
182 fprintf(stream, "\n");
183 for (int i = 0; i < indent_width; i++)
184 fprintf(stream, " ");
185
186 p = last_space + 1;
187 line_start = p;
188 last_space = NULL;
189 continue;
190 }
191 }
192
193 // Print remaining text
194 if (p > line_start) {
195 char seg[BUFFER_SIZE_MEDIUM];
196 int text_len = p - line_start;
197 SAFE_STRNCPY(seg, line_start, text_len + 1);
198 layout_print_colored_segment(stream, seg);
199 }
200}
201
202/* ============================================================================
203 * Two-Column Row Printing
204 * ============================================================================ */
205
206void layout_print_two_column_row(FILE *stream, const char *first_column, const char *second_column, int first_col_len,
207 int term_width) {
208 if (!stream || !first_column || !second_column)
209 return;
210
211 // Strip ANSI escape codes before calculating display width
212 char *stripped = ansi_strip_escapes(first_column, strlen(first_column));
213
214 // Calculate actual display width of first column (accounts for UTF-8, excluding ANSI codes)
215 int display_width = utf8_display_width(stripped ? stripped : first_column);
216 if (display_width < 0)
217 display_width = first_col_len; // Fallback to provided value if available
218
219 SAFE_FREE(stripped);
220
221 // Use calculated first_col_len if provided (accounts for UTF-8 and ANSI codes),
222 // otherwise default to 35. The provided width respects both colored text and UTF-8 characters.
223 // Allow the first column to expand up to 60% of terminal width, but leave room for description.
224 int fixed_first_col_width = (first_col_len > 0) ? first_col_len : 35;
225 int max_first_col_width = (term_width * 6) / 10;
226 if (max_first_col_width < 40)
227 max_first_col_width = 40;
228 if (fixed_first_col_width > max_first_col_width) {
229 fixed_first_col_width = max_first_col_width;
230 }
231
232 // Second column starts after first column (with 2 spaces padding)
233 // This allows descriptions to be quite wide for wrapping
234 int second_col_start = 2 + fixed_first_col_width + 2;
235
236 // At narrow terminal widths, force single-column layout
237 bool force_single_column = (term_width <= 90);
238
239 // Position where first column ends (including leading spaces)
240 int first_col_end = 2 + display_width;
241
242 // Check if first column fits within the fixed width
243 bool fits_in_first_column_width = (display_width <= fixed_first_col_width);
244
245 // If first column fits within its width and we're not forcing single column, try same-line layout
246 if (fits_in_first_column_width && !force_single_column) {
247 // Print first column
248 fprintf(stream, " %s", first_column);
249
250 // Pad to second column start position
251 int padding = second_col_start - first_col_end;
252 if (padding > 0) {
253 fprintf(stream, "%*s", padding, "");
254 } else {
255 fprintf(stream, " ");
256 }
257
258 // Print description with wrapping at second column position
259 layout_print_wrapped_description(stream, second_column, second_col_start, term_width);
260 fprintf(stream, "\n");
261 } else if (force_single_column) {
262 // Terminal too narrow, put description on next line with indent
263 fprintf(stream, " %s\n", first_column);
264
265 // Only print description if it exists
266 if (second_column && second_column[0] != '\0') {
267 int description_indent = 4;
268 for (int i = 0; i < description_indent; i++)
269 fprintf(stream, " ");
270
271 layout_print_wrapped_description(stream, second_column, description_indent, term_width);
272 fprintf(stream, "\n");
273 }
274 } else {
275 // First column overflows its fixed width - bump description to next line
276 // Print first column as-is (it will overflow)
277 fprintf(stream, " %s\n", first_column);
278
279 // Only print description if it exists
280 if (second_column && second_column[0] != '\0') {
281 // Print description on next line at second column position, with wide wrapping
282 for (int i = 0; i < second_col_start; i++)
283 fprintf(stream, " ");
284 layout_print_wrapped_description(stream, second_column, second_col_start, term_width);
285 fprintf(stream, "\n");
286 }
287 }
288}
char * ansi_strip_escapes(const char *input, size_t input_len)
Definition ansi.c:13
void layout_print_wrapped_description(FILE *stream, const char *text, int indent_width, int term_width)
Definition layout.c:98
void layout_print_two_column_row(FILE *stream, const char *first_column, const char *second_column, int first_col_len, int term_width)
Definition layout.c:206
int display_width(const char *text)
int utf8_display_width_n(const char *str, size_t max_bytes)
Definition utf8.c:92
int utf8_display_width(const char *str)
Definition utf8.c:46
int utf8_next_char_bytes(const char *str, size_t max_bytes)
Definition utf8.c:207