Line data Source code
1 : /**
2 : * POSIX terminal implementation.
3 : * Uses termios(3), ioctl TIOCGWINSZ, wcwidth(3).
4 : */
5 : #include "../terminal.h"
6 : #include <stdio.h>
7 : #include <stdlib.h>
8 : #include <string.h>
9 : #include <unistd.h>
10 : #include <termios.h>
11 : #include <sys/ioctl.h>
12 : #include <wchar.h>
13 :
14 : /** Opaque saved terminal state. Definition lives here (hidden from header). */
15 : struct TermRawState {
16 : struct termios saved;
17 : int active;
18 : };
19 :
20 1540 : int terminal_cols(void) {
21 : struct winsize ws;
22 1540 : if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0)
23 1540 : return (int)ws.ws_col;
24 0 : return 80;
25 : }
26 :
27 1660 : int terminal_rows(void) {
28 : struct winsize ws;
29 1660 : if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_row > 0)
30 1660 : return (int)ws.ws_row;
31 0 : return 0;
32 : }
33 :
34 0 : int terminal_is_tty(int fd) {
35 0 : return isatty(fd);
36 : }
37 :
38 371 : TermRawState *terminal_raw_enter(void) {
39 371 : TermRawState *state = malloc(sizeof(TermRawState));
40 371 : if (!state) return NULL;
41 :
42 371 : if (tcgetattr(STDIN_FILENO, &state->saved) != 0) {
43 0 : free(state);
44 0 : return NULL;
45 : }
46 :
47 371 : struct termios raw = state->saved;
48 371 : raw.c_lflag &= ~(unsigned)(ICANON | ECHO | ISIG);
49 371 : raw.c_cc[VMIN] = 1;
50 371 : raw.c_cc[VTIME] = 0;
51 371 : tcsetattr(STDIN_FILENO, TCSANOW, &raw);
52 371 : state->active = 1;
53 371 : return state;
54 : }
55 :
56 369 : void terminal_raw_exit(TermRawState **state) {
57 369 : if (!state || !*state) return;
58 279 : if ((*state)->active)
59 279 : tcsetattr(STDIN_FILENO, TCSANOW, &(*state)->saved);
60 279 : free(*state);
61 279 : *state = NULL;
62 : }
63 :
64 : /**
65 : * Read one byte from STDIN_FILENO via read(2) — NOT via getchar()/stdio.
66 : *
67 : * Rationale: getchar() uses the C stdio buffer. When getchar() calls
68 : * read(2) with VMIN=0 VTIME=1 and gets a 0-byte return (timeout for bare
69 : * ESC), stdio marks the FILE* EOF flag. All subsequent getchar() calls
70 : * then return EOF immediately without blocking, causing an infinite
71 : * redraw loop in the TUI. Using read(2) directly bypasses the stdio
72 : * layer entirely and avoids the EOF flag problem.
73 : *
74 : * Returns the byte value [0..255], or -1 on error/timeout.
75 : */
76 2142 : static int read_byte(void) {
77 : unsigned char c;
78 2142 : ssize_t n = read(STDIN_FILENO, &c, 1);
79 2084 : return (n == 1) ? (int)c : -1;
80 : }
81 :
82 : static int g_last_printable = 0;
83 : static char g_last_utf8[5] = "";
84 :
85 254 : int terminal_last_printable(void) { return g_last_printable; }
86 18 : const char *terminal_last_utf8(void) { return g_last_utf8; }
87 :
88 1039 : TermKey terminal_read_key(void) {
89 : /* The terminal must already be in raw mode (VMIN=1, VTIME=0). */
90 1039 : g_last_printable = 0;
91 1039 : g_last_utf8[0] = '\0';
92 1039 : int c = read_byte();
93 981 : TermKey result = TERM_KEY_IGNORE; /* unknown input → silent no-op */
94 :
95 981 : if (c == '\033') {
96 : /* Temporarily switch to VMIN=0 VTIME=1 (100 ms timeout) to drain
97 : * the escape sequence without blocking if it is a bare ESC. */
98 566 : struct termios t = {0};
99 566 : tcgetattr(STDIN_FILENO, &t);
100 566 : struct termios drain = t;
101 566 : drain.c_cc[VMIN] = 0;
102 566 : drain.c_cc[VTIME] = 1;
103 566 : tcsetattr(STDIN_FILENO, TCSANOW, &drain);
104 :
105 566 : int c2 = read_byte();
106 566 : if (c2 == '[') {
107 529 : int c3 = read_byte();
108 529 : switch (c3) {
109 6 : case 'A': result = TERM_KEY_PREV_LINE; break; /* ESC[A — Up */
110 429 : case 'B': result = TERM_KEY_NEXT_LINE; break; /* ESC[B — Down */
111 1 : case 'C': result = TERM_KEY_RIGHT; break; /* ESC[C — Right */
112 1 : case 'D': result = TERM_KEY_LEFT; break; /* ESC[D — Left */
113 80 : case 'H': result = TERM_KEY_HOME; break; /* ESC[H — Home */
114 3 : case 'F': result = TERM_KEY_END; break; /* ESC[F — End */
115 1 : case 'Z': result = TERM_KEY_SHIFT_TAB; break; /* ESC[Z — Shift+Tab */
116 0 : case '1': { int nx=read_byte(); result=(nx=='~')?TERM_KEY_HOME:TERM_KEY_IGNORE; break; } /* ESC[1~ Home */
117 1 : case '3': { read_byte(); result = TERM_KEY_DELETE; break; } /* ESC[3~ Delete */
118 0 : case '4': { read_byte(); result = TERM_KEY_END; break; } /* ESC[4~ End */
119 3 : case '5': { read_byte(); result = TERM_KEY_PREV_PAGE; break; } /* ESC[5~ PgUp */
120 4 : case '6': { read_byte(); result = TERM_KEY_NEXT_PAGE; break; } /* ESC[6~ PgDn */
121 0 : case '7': { read_byte(); result = TERM_KEY_HOME; break; } /* ESC[7~ Home */
122 0 : case '8': { read_byte(); result = TERM_KEY_END; break; } /* ESC[8~ End */
123 0 : default:
124 0 : if (c3 != -1) {
125 : int ch;
126 0 : while ((ch = read_byte()) != -1) {
127 0 : if ((ch >= 'A' && ch <= 'Z') ||
128 0 : (ch >= 'a' && ch <= 'z') || ch == '~') break;
129 : }
130 : }
131 0 : result = TERM_KEY_IGNORE;
132 0 : break;
133 : }
134 : } else {
135 37 : result = TERM_KEY_ESC; /* bare ESC — go back */
136 : }
137 :
138 : /* Restore VMIN=1 VTIME=0 raw mode. */
139 566 : t.c_cc[VMIN] = 1;
140 566 : t.c_cc[VTIME] = 0;
141 566 : tcsetattr(STDIN_FILENO, TCSANOW, &t);
142 415 : } else if (c == '\n' || c == '\r') {
143 244 : result = TERM_KEY_ENTER;
144 171 : } else if (c == 3 /* Ctrl-C */) {
145 0 : result = TERM_KEY_QUIT;
146 171 : } else if (c == 127 || c == 8 /* DEL / Backspace */) {
147 34 : result = TERM_KEY_BACK;
148 137 : } else if (c == '\t') {
149 4 : result = TERM_KEY_TAB;
150 133 : } else if (c >= 32 && c <= 126) {
151 132 : g_last_printable = c;
152 132 : g_last_utf8[0] = (char)c;
153 132 : g_last_utf8[1] = '\0';
154 132 : result = TERM_KEY_IGNORE;
155 1 : } else if ((c & 0xE0) == 0xC0) {
156 : /* 2-byte UTF-8 sequence (U+0080..U+07FF, covers all Latin accented chars) */
157 0 : int c2 = read_byte();
158 0 : if ((c2 & 0xC0) == 0x80) {
159 0 : g_last_utf8[0] = (char)c;
160 0 : g_last_utf8[1] = (char)c2;
161 0 : g_last_utf8[2] = '\0';
162 0 : g_last_printable = 1;
163 0 : result = TERM_KEY_IGNORE;
164 : }
165 1 : } else if ((c & 0xF0) == 0xE0) {
166 : /* 3-byte UTF-8 sequence (U+0800..U+FFFF) */
167 0 : int c2 = read_byte(), c3 = read_byte();
168 0 : if ((c2 & 0xC0) == 0x80 && (c3 & 0xC0) == 0x80) {
169 0 : g_last_utf8[0] = (char)c;
170 0 : g_last_utf8[1] = (char)c2;
171 0 : g_last_utf8[2] = (char)c3;
172 0 : g_last_utf8[3] = '\0';
173 0 : g_last_printable = 1;
174 0 : result = TERM_KEY_IGNORE;
175 : }
176 1 : } else if ((c & 0xF8) == 0xF0) {
177 : /* 4-byte UTF-8 sequence (U+10000..U+10FFFF) */
178 0 : int c2 = read_byte(), c3 = read_byte(), c4 = read_byte();
179 0 : if ((c2 & 0xC0) == 0x80 && (c3 & 0xC0) == 0x80 && (c4 & 0xC0) == 0x80) {
180 0 : g_last_utf8[0] = (char)c;
181 0 : g_last_utf8[1] = (char)c2;
182 0 : g_last_utf8[2] = (char)c3;
183 0 : g_last_utf8[3] = (char)c4;
184 0 : g_last_utf8[4] = '\0';
185 0 : g_last_printable = 1;
186 0 : result = TERM_KEY_IGNORE;
187 : }
188 : }
189 : /* c == -1 (read error/timeout) → result stays TERM_KEY_IGNORE */
190 :
191 981 : return result;
192 : }
193 :
194 276559 : int terminal_wcwidth(uint32_t cp) {
195 276559 : int w = wcwidth((wchar_t)cp);
196 276559 : return (w < 0) ? 0 : w;
197 : }
198 :
199 14 : int terminal_read_password(const char *prompt, char *buf, size_t size) {
200 14 : if (!buf || size == 0) return -1;
201 :
202 14 : int fd = fileno(stdin);
203 14 : int is_tty = isatty(fd);
204 :
205 14 : if (is_tty) {
206 0 : printf("%s: ", prompt);
207 0 : fflush(stdout);
208 :
209 : struct termios oldt, newt;
210 0 : tcgetattr(fd, &oldt);
211 0 : newt = oldt;
212 0 : newt.c_lflag &= ~(unsigned)ECHO;
213 0 : tcsetattr(fd, TCSANOW, &newt);
214 :
215 0 : char *line = NULL;
216 0 : size_t len = 0;
217 0 : ssize_t nread = getline(&line, &len, stdin);
218 :
219 0 : tcsetattr(fd, TCSANOW, &oldt);
220 0 : printf("\n");
221 :
222 0 : if (nread == -1 || !line) {
223 0 : free(line);
224 0 : return -1;
225 : }
226 :
227 : /* Strip trailing newline */
228 0 : size_t slen = strlen(line);
229 0 : if (slen > 0 && (line[slen-1] == '\n' || line[slen-1] == '\r'))
230 0 : line[--slen] = '\0';
231 0 : if (slen > 0 && (line[slen-1] == '\r'))
232 0 : line[--slen] = '\0';
233 :
234 0 : if (slen >= size) slen = size - 1;
235 0 : memcpy(buf, line, slen);
236 0 : buf[slen] = '\0';
237 0 : free(line);
238 0 : return (int)slen;
239 : } else {
240 : /* Non-TTY: read from stdin without echo manipulation */
241 14 : char *line = NULL;
242 14 : size_t len = 0;
243 14 : ssize_t nread = getline(&line, &len, stdin);
244 14 : if (nread == -1 || !line) {
245 0 : free(line);
246 0 : return -1;
247 : }
248 14 : size_t slen = strlen(line);
249 14 : if (slen > 0 && (line[slen-1] == '\n' || line[slen-1] == '\r'))
250 14 : line[--slen] = '\0';
251 14 : if (slen > 0 && (line[slen-1] == '\r'))
252 0 : line[--slen] = '\0';
253 14 : if (slen >= size) slen = size - 1;
254 14 : memcpy(buf, line, slen);
255 14 : buf[slen] = '\0';
256 14 : free(line);
257 14 : return (int)slen;
258 : }
259 : }
|