ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
options/layout.c
Go to the documentation of this file.
1
8
9/* Storage capacities in elements; text buffers include the terminating NUL. */
10#define OPTIONS_LAYOUT_BUF_BUFFER_SIZE 256
11
13#include <ascii-chat/log/log.h>
16#include <ascii-chat/common.h>
17#include <string.h>
18#include <stdio.h>
19
20/* ============================================================================
21 * Colored Segment Printing
22 * ============================================================================ */
23
30static void layout_print_colored_segment(FILE *stream, const char *seg) {
31 if (!seg || !stream)
32 return;
33
34 // Just print the text as-is. All coloring is done in builder.c using colored_string()
35 // before passing to layout functions. Layout functions only handle positioning and wrapping.
36 fprintf(stream, "%s", seg);
37}
38
39/* ============================================================================
40 * Wrapped Description Printing
41 * ============================================================================ */
42
48static int calculate_segment_display_width(const char *text, int len) {
49 if (!text || len <= 0)
50 return 0;
51
52 // Create a stripped version for width calculation
53 char *stripped = ansi_strip_escapes(text, len);
54 if (!stripped)
55 return 0;
56
57 // Calculate display width of stripped text
58 int width = utf8_display_width_n(stripped, strlen(stripped));
59 SAFE_FREE(stripped);
60
61 return width < 0 ? 0 : width;
62}
63
70static bool is_metadata_start(const char *p) {
71 if (!p || *p != '(')
72 return false;
73
74 // Strip ANSI codes to check the actual text
76 const char *src = p;
77 char *dst = buf;
78 int remaining = sizeof(buf) - 1;
79
80 // Copy up to 20 chars, stripping ANSI codes
81 int count = 0;
82 while (*src && count < 20 && remaining > 0) {
83 if (*src == '\x1b') {
84 // Skip ANSI escape sequence
85 src++;
86 while (*src && *src != 'm')
87 src++;
88 if (*src == 'm')
89 src++;
90 } else {
91 *dst++ = *src++;
92 remaining--;
93 count++;
94 }
95 }
96 *dst = '\0';
97
98 return strncmp(buf, "(default:", 9) == 0 || strncmp(buf, "(env:", 5) == 0;
99}
100
101void layout_print_wrapped_description(FILE *stream, const char *text, int indent_width, int term_width,
102 int continuation_indent_extra) {
103 if (!text || !stream)
104 return;
105
106 // Default terminal width if not specified
107 if (term_width <= 0)
108 term_width = 80;
109
110 // Available width for text after indentation, capped at 130 for readability
111 // Account for continuation lines having extra indent, so reduce available width accordingly
112 // This allows the description column itself to be up to 130 chars wide,
113 // starting from indent_width, for a max total line of indent_width + 130 (first line)
114 // or indent_width + continuation_indent_extra + 130 (continuation lines)
115 int available_width = term_width - indent_width - continuation_indent_extra;
116 if (available_width > 130)
117 available_width = 130;
118 if (available_width < 20)
119 available_width = 20;
120
121 const char *line_start = text;
122 const char *last_space = NULL;
123 const char *p = text;
124 bool inside_metadata = false;
125
126 while (*p) {
127 // Check for explicit newline
128 if (*p == '\n') {
129 // Print everything up to the newline
130 int text_len = p - line_start;
131 if (text_len > 0) {
132 char seg[BUFFER_SIZE_MEDIUM];
133 SAFE_STRNCPY(seg, line_start, text_len + 1);
134 layout_print_colored_segment(stream, seg);
135 }
136
137 fprintf(stream, "\n");
138 if (*(p + 1)) {
139 // Continuation lines get extra indent if specified
140 int continuation_indent = indent_width + continuation_indent_extra;
141 for (int i = 0; i < continuation_indent; i++)
142 fprintf(stream, " ");
143 }
144 p++;
145 line_start = p;
146 last_space = NULL;
147 inside_metadata = false;
148 continue;
149 }
150
151 // Check for start of metadata blocks (must check before space tracking)
152 // Use helper that accounts for ANSI codes in the metadata
153 if (!inside_metadata && is_metadata_start(p)) {
154 inside_metadata = true;
155 }
156
157 // Check for end of metadata blocks - always exit on )
158 // We'll re-enter when we see the next metadata marker
159 if (inside_metadata && *p == ')') {
160 inside_metadata = false;
161 }
162
163 // Track spaces for word wrapping, including inside metadata blocks
164 // Prefer breaks before metadata blocks by updating last_space even if we already have one
165 if (*p == ' ') {
166 const char *next = p + 1;
167 while (*next == ' ')
168 next++;
169 // Track space as break point if it's before metadata or a normal space (not before parenthetical)
170 if (is_metadata_start(next) || !last_space || *next != '(') {
171 last_space = p;
172 }
173 }
174
175 // Decode UTF-8 character length
176 int char_bytes = utf8_next_char_bytes(p, strlen(p));
177
178 if (char_bytes <= 0) {
179 // Invalid UTF-8 or end of string, stop
180 break;
181 }
182
183 // Advance to next character first
184 p += char_bytes;
185
186 // Calculate actual display width from line_start to current position (excluding ANSI codes)
187 int actual_width = calculate_segment_display_width(line_start, p - line_start);
188
189 // Check if we need to wrap
190 if (actual_width >= available_width && last_space && last_space > line_start) {
191 // Print text up to break point with colors applied
192 int text_len = last_space - line_start;
193 char seg[BUFFER_SIZE_MEDIUM];
194 SAFE_STRNCPY(seg, line_start, text_len + 1);
195 layout_print_colored_segment(stream, seg);
196
197 fprintf(stream, "\n");
198 // Continuation lines get extra indent if specified
199 int continuation_indent = indent_width + continuation_indent_extra;
200 for (int i = 0; i < continuation_indent; i++)
201 fprintf(stream, " ");
202
203 p = last_space + 1;
204 line_start = p;
205 last_space = NULL;
206 continue;
207 }
208 }
209
210 // Print remaining text
211 if (p > line_start) {
212 char seg[BUFFER_SIZE_MEDIUM];
213 int text_len = p - line_start;
214 SAFE_STRNCPY(seg, line_start, text_len + 1);
215 layout_print_colored_segment(stream, seg);
216 }
217}
218
219/* ============================================================================
220 * Two-Column Row Printing
221 * ============================================================================ */
222
223void layout_print_two_column_row(FILE *stream, const char *first_column, const char *second_column, int first_col_len,
224 int term_width, int continuation_indent_extra) {
225 if (!stream || !first_column || !second_column)
226 return;
227
228 // Strip ANSI escape codes before calculating display width
229 char *stripped = ansi_strip_escapes(first_column, strlen(first_column));
230
231 // Calculate actual display width of first column (accounts for UTF-8, excluding ANSI codes)
232 int display_width = utf8_display_width(stripped ? stripped : first_column);
233 if (display_width < 0)
234 display_width = first_col_len; // Fallback to provided value if available
235
236 SAFE_FREE(stripped);
237
238 // Use calculated first_col_len if provided (accounts for UTF-8 and ANSI codes),
239 // otherwise default to 35. The provided width respects both colored text and UTF-8 characters.
240 // Allow the first column to expand up to 60% of terminal width, but leave room for description.
241 int fixed_first_col_width = (first_col_len > 0) ? first_col_len : 35;
242 int max_first_col_width = (term_width * 6) / 10;
243 if (max_first_col_width < 40)
244 max_first_col_width = 40;
245 if (fixed_first_col_width > max_first_col_width) {
246 fixed_first_col_width = max_first_col_width;
247 }
248
249 // Second column starts after first column (with 2 spaces padding)
250 // This allows descriptions to be quite wide for wrapping
251 int second_col_start = 2 + fixed_first_col_width + 2;
252
253 // At narrow terminal widths, force single-column layout
254 bool force_single_column = (term_width <= 90);
255
256 // Position where first column ends (including leading spaces)
257 int first_col_end = 2 + display_width;
258
259 // Check if first column fits within the fixed width
260 bool fits_in_first_column_width = (display_width <= fixed_first_col_width);
261
262 // If first column fits within its width and we're not forcing single column, try same-line layout
263 if (fits_in_first_column_width && !force_single_column) {
264 // Print first column
265 fprintf(stream, " %s", first_column);
266
267 // Pad to second column start position
268 int padding = second_col_start - first_col_end;
269 if (padding > 0) {
270 fprintf(stream, "%*s", padding, "");
271 } else {
272 fprintf(stream, " ");
273 }
274
275 // Print description with wrapping at second column position
276 layout_print_wrapped_description(stream, second_column, second_col_start, term_width, continuation_indent_extra);
277 fprintf(stream, "\n");
278 } else if (force_single_column) {
279 // Terminal too narrow, put description on next line with indent
280 fprintf(stream, " %s\n", first_column);
281
282 // Only print description if it exists
283 if (second_column && second_column[0] != '\0') {
284 int description_indent = 4;
285 for (int i = 0; i < description_indent; i++)
286 fprintf(stream, " ");
287
288 layout_print_wrapped_description(stream, second_column, description_indent, term_width,
289 continuation_indent_extra);
290 fprintf(stream, "\n");
291 }
292 } else {
293 // First column overflows its fixed width - bump description to next line
294 // Print first column as-is (it will overflow)
295 fprintf(stream, " %s\n", first_column);
296
297 // Only print description if it exists
298 if (second_column && second_column[0] != '\0') {
299 // Print description on next line at second column position, with wide wrapping
300 for (int i = 0; i < second_col_start; i++)
301 fprintf(stream, " ");
302 layout_print_wrapped_description(stream, second_column, second_col_start, term_width, continuation_indent_extra);
303 fprintf(stream, "\n");
304 }
305 }
306}
ANSI escape sequence utilities and fast color code generation.
⚙️ Common definitions, error codes, macros, and types shared throughout the application
unsigned int width
Definition fft.c:23
char * ansi_strip_escapes(const char *input, size_t input_len)
When code needs to work with rgb error, it keeps r, g, b here instead of scattering it across globals...
Definition ansi.c:27
#define BUFFER_SIZE_MEDIUM
Medium buffer size (512 bytes).
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
int utf8_display_width_n(const char *str, size_t max_bytes)
Calculate terminal display width of a UTF-8 string up to byte limit.
Definition utf8.c:102
int display_width(const char *text)
Calculate the visible display width of text with ANSI escape codes.
int utf8_display_width(const char *str)
Calculate terminal display width of a UTF-8 string.
Definition utf8.c:98
int utf8_next_char_bytes(const char *str, size_t max_bytes)
Get byte length of next UTF-8 character.
Definition utf8.c:177
📝 Logging API with multiple log levels and terminal output control
#define OPTIONS_LAYOUT_BUF_BUFFER_SIZE
void layout_print_two_column_row(FILE *stream, const char *first_column, const char *second_column, int first_col_len, int term_width, int continuation_indent_extra)
Print two-column row with automatic wrapping.
void layout_print_wrapped_description(FILE *stream, const char *text, int indent_width, int term_width, int continuation_indent_extra)
Print text with wrapping and proper indentation.
Two-column layout helpers for help text and option formatting.
🔤 String Manipulation and Shell Escaping Utilities
🔤 UTF-8 Encoding and Decoding Utilities