ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
prompt.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define TUI_PROMPT_CENTERED_BUFFER_SIZE 2048
4#define TUI_PROMPT_TARGET_BUFFER_SIZE 256
5#define TUI_PROMPT_SHORTCUTS_BUFFER_SIZE 1024
6
11#include <ascii-chat/ui/text.h>
17#include <ascii-chat/common.h>
20#include <string.h>
23
41
42static void media_center_line(frame_buffer_t *buffer, const char *text, int row, int cols) {
43 int width = utf8_display_width(text);
44 int col = (cols - width) / 2 + 1;
45 frame_buffer_printf(buffer, "\033[%d;1H%*s", row, col > 1 ? col - 1 : 0, "");
46 int used = 0;
47 for (const char *p = text; *p;) {
48 int bytes = utf8_next_char_bytes(p, strlen(p));
49 if (bytes <= 0)
50 break;
51 int cells = utf8_display_width_n(p, (size_t)bytes);
52 if (used + cells > cols - 2)
53 break;
54 frame_buffer_append(buffer, p, (size_t)bytes);
55 used += cells;
56 p += bytes;
57 }
58 frame_buffer_append(buffer, "\033[K", 3);
59}
60
61static void media_panel_clear_background(frame_buffer_t *buffer, terminal_size_t size, int top, int logo_rows,
62 int shown) {
63 for (int row = 1; row <= size.rows; ++row) {
64 bool logo = row >= top && row < top + logo_rows;
65 bool heading = row == top + logo_rows + 1 || row == top + logo_rows + 2;
66 bool input = row >= top + logo_rows + 4 && row < top + logo_rows + 4 + shown;
67 if (!logo && !heading && !input)
68 frame_buffer_printf(buffer, "\033[%d;1H\033[K", row);
69 }
70}
71
72void ui_media_panel_render(frame_buffer_t *buffer, terminal_size_t size, const char *title, const char *instruction,
73 const char *text, size_t cursor) {
74 bool compact = size.cols < 52;
75 int logo_rows = compact ? 1 : 4;
76 int lines = 1;
77 if (text) {
78 for (size_t i = 0; text[i]; ++i) {
79 if (text[i] == '\n') {
80 ++lines;
81 }
82 }
83 }
84 int available = size.rows - logo_rows - 5;
85 if (available < 1)
86 available = 1;
87 int shown = text ? (lines < available ? lines : available) : 0;
88 int height = logo_rows + 3 + (text ? 1 + shown : 0);
89 int top = (size.rows - height) / 2 + 1;
90 if (top < 1)
91 top = 1;
92 bool colors = !GET_OPTION(strip_ansi) &&
93 terminal_should_color_output(frame_buffer_get_screen_output_fd(), options_terminal_settings());
94 frame_buffer_append(buffer, "\033[0m\033[H\033[?25l", 13);
95 frame_buffer_printf(buffer, "\033[%d;1H", top);
97 int tick = sample ? (int)(sample->seconds * 60) : (int)((time_get_ns() / NS_PER_MS_INT / 16) % 100000);
98 invitation_render_logo(buffer, size.cols, tick, colors, compact);
99 media_center_line(buffer, title, top + logo_rows + 1, size.cols);
100 media_center_line(buffer, instruction, top + logo_rows + 2, size.cols);
101 if (!text) {
102 media_panel_clear_background(buffer, size, top, logo_rows, 0);
103 return;
104 }
105 int width = size.cols - 6;
106 if (width > 76)
107 width = 76;
108 int longest = 20;
109 const char *line = text;
110 for (const char *p = text;; ++p) {
111 if (!*p || *p == '\n') {
112 int n = utf8_display_width_n(line, (size_t)(p - line));
113 if (n > longest)
114 longest = n;
115 if (!*p)
116 break;
117 line = p + 1;
118 }
119 }
120 if (longest < width)
121 width = longest;
122 int col = (size.cols - (width + 2)) / 2 + 1;
123 for (int i = 0; i < shown; ++i)
124 frame_buffer_printf(buffer, "\033[%d;1H\033[K", top + logo_rows + 4 + i);
125 ui_input_render(buffer, top + logo_rows + 4, col, width, shown, text, cursor, true);
126 media_panel_clear_background(buffer, size, top, logo_rows, shown);
127}
128
129static int password_lines(const prompt_snapshot_t *s) {
130 int lines = 1;
131 for (const char *p = s->text + s->prompt_bytes; *p; ++p)
132 lines += *p == '\n';
133 return lines;
134}
135
136static void password_center_line(frame_buffer_t *buffer, const char *text, int row, int left, int width) {
137 char centered[TUI_PROMPT_CENTERED_BUFFER_SIZE];
138 ui_text_cell(centered, sizeof(centered), text, width - 4, UI_ALIGN_CENTER);
139 frame_buffer_printf(buffer, "\033[%d;%dH%s", row, left + 2, centered);
140}
141
142static void password_panel(frame_buffer_t *buffer, terminal_size_t size, const prompt_snapshot_t *s) {
143 int width = size.cols - 4;
144 if (width > 64)
145 width = 64;
146 int lines = password_lines(s);
147 int height = 13 + lines;
148 int top = (size.rows - height) / 2 + 1;
149 int logo_rows = size.cols >= 52 && top >= 6 ? 4 : 1;
150 int logo_top = top - logo_rows - (top >= 3 ? 1 : 0);
151 frame_buffer_printf(buffer, "\033[0m\033[H\033[2J\033[?25l\033[%d;1H", logo_top);
153 invitation_render_logo(buffer, size.cols, sample ? (int)(sample->seconds * 60) : 0, s->color, logo_rows == 1);
154 if (top >= 3)
155 media_center_line(buffer, "Video chat in your terminal", top - 1, size.cols);
156 int left = (size.cols - width) / 2 + 1;
157 ui_popup_frame_titled(buffer, top, left, width, height, LOG_COLOR_RESET, "Password required");
159 snprintf(target, sizeof(target), "Connect to %.*s", width - 15, s->text);
160 password_center_line(buffer, target, top + 2, left, width);
161 password_center_line(buffer, "This server requires a password.", top + 3, left, width);
162 if (s->incorrect || s->newline_limit)
163 password_center_line(
164 buffer,
165 colored_string(LOG_COLOR_WARN, s->newline_limit ? "Maximum 3 newlines." : "Incorrect password. Try again."),
166 top + 4, left, width);
167 frame_buffer_printf(buffer, "\033[%d;%dHPassword", top + 6, left + 2);
168 ui_popup_frame(buffer, top + 7, left + 2, width - 4, lines + 2, LOG_COLOR_DEBUG);
169 const ui_button_t buttons[] = {{.label = "[ Confirm ]", .disabled = !s->can_confirm, .color = LOG_COLOR_INFO},
170 {.label = "[ Cancel ]", .color = LOG_COLOR_ERROR}};
171 ui_buttons_render(buffer, top + 9 + lines, left + 2, width - 4, buttons, 2, s->selected_button);
172 const char *orange = s->color ? ui_text_color_prefix(LOG_COLOR_DEV) : "";
173 const char *foreground = s->color ? ui_text_color_prefix(LOG_COLOR_RESET) : "";
174 char shortcuts[TUI_PROMPT_SHORTCUTS_BUFFER_SIZE];
175 snprintf(shortcuts, sizeof(shortcuts), "%sTab%s select %sEnter%s confirm %sEsc%s cancel", orange, foreground,
176 orange, foreground, orange, foreground);
177 password_center_line(buffer, shortcuts, top + 10 + lines, left, width);
178 snprintf(shortcuts, sizeof(shortcuts), "%sShift+Enter%s newline (maximum 3)", orange, foreground);
179 password_center_line(buffer, shortcuts, top + 11 + lines, left, width);
180 ui_input_render(buffer, top + 8, left + 3, width - 8, lines, s->text + s->prompt_bytes, s->cursor, false);
181}
182
183const char *ui_prompt_title(const void *data) {
184 const prompt_snapshot_t *snapshot = data;
185 if (strncmp(snapshot->text, "Open media:", 11) == 0)
186 return "load file/url";
187 if (strstr(snapshot->text, "(y/N)") || strstr(snapshot->text, "(Y/n)"))
188 return "confirmation";
189 if (snapshot->secret)
190 return "password required";
191 return "input";
192}
193
194static void render_prompt(terminal_size_t size, const void *data) {
195 const prompt_snapshot_t *snapshot = data;
196 const char *visible = snapshot->text + snapshot->prompt_bytes;
197 frame_buffer_t *buffer = frame_buffer_create(size.rows, size.cols);
198 if (!buffer)
199 return;
200 if (snapshot->password_dialog) {
201 password_panel(buffer, size, snapshot);
203 frame_buffer_destroy(buffer);
204 return;
205 }
206 if (strncmp(snapshot->text, "Open media:", 11) == 0) {
207 ui_media_panel_render(buffer, size, "Open media:", "Drop files / paste paths or URLs; Enter plays; Esc cancels",
208 visible, snapshot->cursor);
210 frame_buffer_destroy(buffer);
211 return;
212 }
213 frame_buffer_append(buffer, "\033[0m\033[H", 7);
214 int row =
215 ui_notice_render(buffer, snapshot->text, snapshot->severity, size.cols, snapshot->unicode, snapshot->color) + 2;
216 frame_buffer_printf(buffer, "\033[%d;1H\033[2K", row);
217 ui_input_render(buffer, row, 1, size.cols - 3, 1, visible, snapshot->cursor, false);
219 frame_buffer_destroy(buffer);
220}
221
222static int prompt_fd(void) {
224 return STDERR_FILENO; // The controller inherits the active screen's retained terminal.
225 return platform_isatty(STDERR_FILENO) ? STDERR_FILENO : STDOUT_FILENO;
226}
227
228const char *ui_prompt_usage_name(const void *data) {
229 const prompt_snapshot_t *snapshot = data;
230 if (!snapshot)
231 return "input_prompt";
232 return ui_prompt_kind_name(snapshot->kind);
233}
234
235const char *ui_prompt_kind_name(ui_prompt_kind_t kind) {
236 switch (kind) {
237 case UI_PROMPT_PASSWORD:
238 return "password_prompt";
239 case UI_PROMPT_MEDIA_INPUT:
240 return "media_input_prompt";
241 case UI_PROMPT_CONFIRMATION:
242 return "confirmation_prompt";
243 case UI_PROMPT_SSH_PASSPHRASE:
244 return "ssh_passphrase_prompt";
245 case UI_PROMPT_GPG_PASSPHRASE:
246 return "gpg_passphrase_prompt";
247 case UI_PROMPT_TRUST_HOST:
248 return "trust_host_prompt";
249 case UI_PROMPT_UNVERIFIED_HOST:
250 return "unverified_host_prompt";
251 case UI_PROMPT_DISCOVERY_KEY_CHANGE:
252 return "discovery_key_change_prompt";
253 case UI_PROMPT_IP_DISCLOSURE:
254 return "ip_disclosure_prompt";
255 case UI_PROMPT_CONFIG_OVERWRITE:
256 return "config_overwrite_prompt";
257 case UI_PROMPT_MANPAGE_OVERWRITE:
258 return "manpage_overwrite_prompt";
259 case UI_PROMPT_COMPLETION_OVERWRITE:
260 return "completion_overwrite_prompt";
261 default:
262 return "input_prompt";
263 }
264}
265
266asciichat_error_t ui_prompt_present(const char *prompt, const char *visible, size_t cursor, bool secret) {
267 return ui_prompt_present_kind(prompt, visible, cursor, secret, secret ? UI_PROMPT_PASSWORD : UI_PROMPT_INPUT);
268}
269
270asciichat_error_t ui_prompt_present_kind(const char *prompt, const char *visible, size_t cursor, bool secret,
271 ui_prompt_kind_t kind) {
272 size_t prompt_bytes = strlen(prompt) + 1;
273 size_t visible_bytes = strlen(visible) + 1;
274 prompt_snapshot_t *snapshot = SAFE_MALLOC(sizeof(*snapshot) + prompt_bytes + visible_bytes, prompt_snapshot_t *);
275 if (!snapshot)
276 return SET_ERRNO(ERROR_MEMORY, "Cannot allocate prompt presentation");
277 snapshot->kind = kind;
278 snapshot->password_dialog = snapshot->incorrect = snapshot->can_confirm = snapshot->newline_limit = false;
279 snapshot->selected_button = 0;
280 snapshot->prompt_bytes = prompt_bytes;
281 snapshot->cursor = cursor;
282 snapshot->secret = secret;
284 bool media = strncmp(prompt, "Open media:", 11) == 0;
285 snapshot->color =
286 !media && !GET_OPTION(strip_ansi) && terminal_should_color_output(prompt_fd(), options_terminal_settings());
287 snapshot->unicode = !media && terminal_should_use_utf8(GET_OPTION(force_utf8));
288 memcpy(snapshot->text, prompt, prompt_bytes);
289 memcpy(snapshot->text + prompt_bytes, visible, visible_bytes);
290 // Height is measured at the minimum width, so every larger terminal fits.
291 terminal_size_t minimum = {.cols = 40,
292 .rows = ui_notice_render(NULL, prompt, snapshot->severity, 40, false, false) + 3};
293 if (media)
294 minimum.rows = 8;
295 asciichat_error_t result = ui_controller_submit(UI_SCREEN_PROMPT, prompt_fd(), minimum, render_prompt, snapshot,
296 sizeof(*snapshot) + prompt_bytes + visible_bytes);
297 SAFE_FREE(snapshot);
298 return result;
299}
300
301void ui_prompt_dismiss(int previous) {
304 ui_controller_write(prompt_fd(), "\r\n", 2);
305}
306
309}
310
311int ui_prompt_required_rows(const void *data, int cols) {
312 const prompt_snapshot_t *snapshot = data;
313 if (snapshot->password_dialog) {
314 int rows = 15 + password_lines(snapshot);
315 return rows < 19 ? 19 : rows;
316 }
317 if (strncmp(snapshot->text, "Open media:", 11) == 0)
318 return cols < 52 ? 8 : 11;
319 return ui_notice_render(NULL, snapshot->text, snapshot->severity, cols, false, false) + 3;
320}
321
322static void render_media_loading(terminal_size_t size, const void *data) {
323 (void)data;
324 frame_buffer_t *buffer = frame_buffer_create(size.rows, size.cols);
325 if (!buffer)
326 return;
327 ui_media_panel_render(buffer, size, "Loading media...", "Esc cancels; current playback continues.", NULL, 0);
329 frame_buffer_destroy(buffer);
330}
331
333 return ui_controller_submit(UI_SCREEN_RENDER_PROGRESS, STDERR_FILENO, (terminal_size_t){.cols = 40, .rows = 8},
334 render_media_loading, "", 1);
335}
336
337asciichat_error_t ui_password_prompt_present(const char *target, const char *masked, size_t cursor,
338 size_t selected_button, bool incorrect, bool can_confirm,
339 bool newline_limit) {
340 if (!target || !masked || selected_button > 1)
341 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid password presentation");
342 size_t target_bytes = strlen(target) + 1, masked_bytes = strlen(masked) + 1;
343 size_t bytes = sizeof(prompt_snapshot_t) + target_bytes + masked_bytes;
345 if (!s)
346 return SET_ERRNO(ERROR_MEMORY, "Cannot allocate password presentation");
347 s->prompt_bytes = target_bytes;
348 s->cursor = cursor;
349 s->selected_button = selected_button;
350 s->kind = UI_PROMPT_PASSWORD;
351 s->password_dialog = true;
352 s->secret = true;
353 s->incorrect = incorrect;
354 s->can_confirm = can_confirm;
355 s->newline_limit = newline_limit;
356 s->color = !GET_OPTION(strip_ansi) && terminal_should_color_output(prompt_fd(), options_terminal_settings());
357 memcpy(s->text, target, target_bytes);
358 memcpy(s->text + target_bytes, masked, masked_bytes);
360 (terminal_size_t){.cols = 40, .rows = 19}, render_prompt, s, bytes);
361 SAFE_FREE(s);
362 return result;
363}
#define SET_ERRNO(code,...)
void ui_buttons_render(frame_buffer_t *out, int row, int left, int width, const ui_button_t *b, size_t n, size_t selected)
Definition buttons.c:29
⚙️ Common definitions, error codes, macros, and types shared throughout the application
char cells[(512 *256 *2)]
Definition fft.c:21
unsigned char colors[(512 *256 *2) *3]
Definition fft.c:22
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
void frame_buffer_append(frame_buffer_t *buf, const char *str, size_t len)
Append raw bytes to the buffer.
void frame_buffer_destroy(frame_buffer_t *buf)
Destroy a frame buffer.
size_t frame_buffer_get_length(const frame_buffer_t *buf)
Get the current content length of the frame buffer.
const char * frame_buffer_get_content(const frame_buffer_t *buf)
Get a pointer to the buffer content.
frame_buffer_t * frame_buffer_create(int rows, int cols)
Create a new frame buffer.
void frame_buffer_printf(frame_buffer_t *buf, const char *fmt,...)
Printf-style append to the buffer.
int frame_buffer_get_screen_output_fd(void)
Get current output file descriptor for terminal screens.
Buffered terminal frame rendering.
struct frame_buffer frame_buffer_t
Opaque frame buffer handle.
#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
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_MEMORY
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
@ LOG_COLOR_RESET
Definition log/log.h:155
@ LOG_COLOR_ERROR
Definition log/log.h:152
@ LOG_COLOR_DEV
Definition log/log.h:148
@ LOG_COLOR_DEBUG
Definition log/log.h:149
@ LOG_COLOR_INFO
Definition log/log.h:150
@ LOG_COLOR_WARN
Definition log/log.h:151
#define NS_PER_MS_INT
Definition time.h:168
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
#define options_terminal_settings()
Definition options.h:1296
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
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
Definition string.c:284
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
void invitation_render_logo(frame_buffer_t *buf, int cols, int frame, bool colors, bool compact)
Definition invitation.c:34
int ui_notice_render(frame_buffer_t *buffer, const char *text, ui_notice_severity_t severity, int cols, bool unicode, bool color)
Definition notice.c:64
ui_notice_severity_t ui_notice_prompt_severity(void)
Definition notice.c:522
ui_notice_severity_t
Definition notice.h:9
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
asciichat_error_t ui_popup_frame(frame_buffer_t *out, int row, int col, int width, int height, log_color_t color)
Definition popup.c:28
asciichat_error_t ui_popup_frame_titled(frame_buffer_t *out, int row, int col, int width, int height, log_color_t color, const char *title)
Definition popup.c:41
asciichat_error_t ui_prompt_present(const char *prompt, const char *visible, size_t cursor, bool secret)
Definition prompt.c:266
const char * ui_prompt_title(const void *data)
Definition prompt.c:183
#define TUI_PROMPT_SHORTCUTS_BUFFER_SIZE
Definition prompt.c:5
const char * ui_prompt_usage_name(const void *data)
Definition prompt.c:228
void ui_media_panel_render(frame_buffer_t *buffer, terminal_size_t size, const char *title, const char *instruction, const char *text, size_t cursor)
Definition prompt.c:72
#define TUI_PROMPT_CENTERED_BUFFER_SIZE
Definition prompt.c:3
int ui_prompt_required_rows(const void *data, int cols)
Definition prompt.c:311
asciichat_error_t ui_password_prompt_present(const char *target, const char *masked, size_t cursor, size_t selected_button, bool incorrect, bool can_confirm, bool newline_limit)
Definition prompt.c:337
#define TUI_PROMPT_TARGET_BUFFER_SIZE
Definition prompt.c:4
void ui_prompt_remove(void)
Definition prompt.c:307
asciichat_error_t ui_prompt_present_kind(const char *prompt, const char *visible, size_t cursor, bool secret, ui_prompt_kind_t kind)
Definition prompt.c:270
asciichat_error_t ui_media_loading_present(void)
Definition prompt.c:332
const char * ui_prompt_kind_name(ui_prompt_kind_t kind)
Definition prompt.c:235
void ui_prompt_dismiss(int previous)
Definition prompt.c:301
🔤 String Manipulation and Shell Escaping Utilities
For animation config, this is the live record that ties type, fps, speed, hidden_policy together....
bool newline_limit
Definition prompt.c:29
ui_notice_severity_t severity
Definition prompt.c:30
size_t cursor
Definition prompt.c:27
size_t prompt_bytes
Definition prompt.c:26
bool password_dialog
Definition prompt.c:29
ui_prompt_kind_t kind
Definition prompt.c:25
size_t selected_button
Definition prompt.c:28
const char * ui_text_color_prefix(log_color_t color)
Definition text.c:18
size_t ui_text_cell(char *out, size_t cap, const char *text, int width, ui_alignment_t align)
Definition text.c:48
@ UI_ALIGN_CENTER
Definition text.h:9
⏱️ High-precision timing utilities using sokol_time.h and uthash
void ui_controller_dismiss(ui_screen_t screen, int previous)
asciichat_error_t ui_controller_submit(ui_screen_t screen, int fd, terminal_size_t minimum, ui_render_fn render, const void *snapshot, size_t bytes)
bool ui_controller_is_presenting(void)
const animation_sample_t * ui_controller_animation_sample(void)
asciichat_error_t ui_controller_write(int fd, const char *data, size_t len)
@ UI_SCREEN_PROMPT
@ UI_SCREEN_RENDER_PROGRESS
asciichat_error_t ui_input_render(frame_buffer_t *buffer, int row, int col, int width, int height, const char *text, size_t cursor, bool preserve_quotes)
Definition ui/input.c:6
🔤 UTF-8 Encoding and Decoding Utilities