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 43 : int terminal_cols(void) {
21 43 : struct winsize ws;
22 43 : if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0)
23 0 : return (int)ws.ws_col;
24 43 : return 80;
25 : }
26 :
27 2 : int terminal_rows(void) {
28 2 : struct winsize ws;
29 2 : if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_row > 0)
30 0 : return (int)ws.ws_row;
31 2 : return 0;
32 : }
33 :
34 25 : int terminal_is_tty(int fd) {
35 25 : return isatty(fd);
36 : }
37 :
38 1 : TermRawState *terminal_raw_enter(void) {
39 1 : TermRawState *state = malloc(sizeof(TermRawState));
40 1 : if (!state) return NULL;
41 :
42 1 : if (tcgetattr(STDIN_FILENO, &state->saved) != 0) {
43 1 : free(state);
44 1 : return NULL;
45 : }
46 :
47 0 : struct termios raw = state->saved;
48 0 : raw.c_lflag &= ~(unsigned)(ICANON | ECHO | ISIG);
49 0 : raw.c_cc[VMIN] = 1;
50 0 : raw.c_cc[VTIME] = 0;
51 0 : tcsetattr(STDIN_FILENO, TCSANOW, &raw);
52 0 : state->active = 1;
53 0 : return state;
54 : }
55 :
56 43 : void terminal_raw_exit(TermRawState **state) {
57 43 : if (!state || !*state) return;
58 0 : if ((*state)->active)
59 0 : tcsetattr(STDIN_FILENO, TCSANOW, &(*state)->saved);
60 0 : free(*state);
61 0 : *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 2 : static int read_byte(void) {
77 2 : unsigned char c;
78 2 : ssize_t n = read(STDIN_FILENO, &c, 1);
79 2 : return (n == 1) ? (int)c : -1;
80 : }
81 :
82 : static int g_last_printable = 0;
83 :
84 0 : int terminal_last_printable(void) { return g_last_printable; }
85 :
86 1 : TermKey terminal_read_key(void) {
87 : /* The terminal must already be in raw mode (VMIN=1, VTIME=0). */
88 1 : g_last_printable = 0;
89 1 : int c = read_byte();
90 1 : TermKey result = TERM_KEY_IGNORE; /* unknown input → silent no-op */
91 :
92 1 : if (c == '\033') {
93 : /* Temporarily switch to VMIN=0 VTIME=1 (100 ms timeout) to drain
94 : * the escape sequence without blocking if it is a bare ESC. */
95 1 : struct termios t = {0};
96 1 : tcgetattr(STDIN_FILENO, &t);
97 1 : struct termios drain = t;
98 1 : drain.c_cc[VMIN] = 0;
99 1 : drain.c_cc[VTIME] = 1;
100 1 : tcsetattr(STDIN_FILENO, TCSANOW, &drain);
101 :
102 1 : int c2 = read_byte();
103 1 : if (c2 == '[') {
104 0 : int c3 = read_byte();
105 0 : switch (c3) {
106 0 : case 'A': result = TERM_KEY_PREV_LINE; break; /* ESC[A — Up */
107 0 : case 'B': result = TERM_KEY_NEXT_LINE; break; /* ESC[B — Down */
108 0 : case 'C': result = TERM_KEY_RIGHT; break; /* ESC[C — Right */
109 0 : case 'D': result = TERM_KEY_LEFT; break; /* ESC[D — Left */
110 0 : case 'H': result = TERM_KEY_HOME; break; /* ESC[H — Home */
111 0 : case 'F': result = TERM_KEY_END; break; /* ESC[F — End */
112 0 : case 'Z': result = TERM_KEY_SHIFT_TAB; break; /* ESC[Z — Shift+Tab */
113 0 : case '1': { int nx=read_byte(); result=(nx=='~')?TERM_KEY_HOME:TERM_KEY_IGNORE; break; } /* ESC[1~ Home */
114 0 : case '3': { read_byte(); result = TERM_KEY_DELETE; break; } /* ESC[3~ Delete */
115 0 : case '4': { read_byte(); result = TERM_KEY_END; break; } /* ESC[4~ End */
116 0 : case '5': { read_byte(); result = TERM_KEY_PREV_PAGE; break; } /* ESC[5~ PgUp */
117 0 : case '6': { read_byte(); result = TERM_KEY_NEXT_PAGE; break; } /* ESC[6~ PgDn */
118 0 : case '7': { read_byte(); result = TERM_KEY_HOME; break; } /* ESC[7~ Home */
119 0 : case '8': { read_byte(); result = TERM_KEY_END; break; } /* ESC[8~ End */
120 0 : default:
121 0 : if (c3 != -1) {
122 : int ch;
123 0 : while ((ch = read_byte()) != -1) {
124 0 : if ((ch >= 'A' && ch <= 'Z') ||
125 0 : (ch >= 'a' && ch <= 'z') || ch == '~') break;
126 : }
127 : }
128 0 : result = TERM_KEY_IGNORE;
129 0 : break;
130 : }
131 : } else {
132 1 : result = TERM_KEY_ESC; /* bare ESC — go back */
133 : }
134 :
135 : /* Restore VMIN=1 VTIME=0 raw mode. */
136 1 : t.c_cc[VMIN] = 1;
137 1 : t.c_cc[VTIME] = 0;
138 1 : tcsetattr(STDIN_FILENO, TCSANOW, &t);
139 0 : } else if (c == '\n' || c == '\r') {
140 0 : result = TERM_KEY_ENTER;
141 0 : } else if (c == 3 /* Ctrl-C */) {
142 0 : result = TERM_KEY_QUIT;
143 0 : } else if (c == 127 || c == 8 /* DEL / Backspace */) {
144 0 : result = TERM_KEY_BACK;
145 0 : } else if (c == '\t') {
146 0 : result = TERM_KEY_TAB;
147 0 : } else if (c >= 32 && c <= 126) {
148 0 : g_last_printable = c;
149 0 : result = TERM_KEY_IGNORE;
150 : }
151 : /* c == -1 (read error/timeout) → result stays TERM_KEY_IGNORE */
152 :
153 1 : return result;
154 : }
155 :
156 5333 : int terminal_wcwidth(uint32_t cp) {
157 5333 : int w = wcwidth((wchar_t)cp);
158 5333 : return (w < 0) ? 0 : w;
159 : }
160 :
161 4 : int terminal_read_password(const char *prompt, char *buf, size_t size) {
162 4 : if (!buf || size == 0) return -1;
163 :
164 2 : int fd = fileno(stdin);
165 2 : int is_tty = isatty(fd);
166 :
167 2 : if (is_tty) {
168 0 : printf("%s: ", prompt);
169 0 : fflush(stdout);
170 :
171 0 : struct termios oldt, newt;
172 0 : tcgetattr(fd, &oldt);
173 0 : newt = oldt;
174 0 : newt.c_lflag &= ~(unsigned)ECHO;
175 0 : tcsetattr(fd, TCSANOW, &newt);
176 :
177 0 : char *line = NULL;
178 0 : size_t len = 0;
179 0 : ssize_t nread = getline(&line, &len, stdin);
180 :
181 0 : tcsetattr(fd, TCSANOW, &oldt);
182 0 : printf("\n");
183 :
184 0 : if (nread == -1 || !line) {
185 0 : free(line);
186 0 : return -1;
187 : }
188 :
189 : /* Strip trailing newline */
190 0 : size_t slen = strlen(line);
191 0 : if (slen > 0 && (line[slen-1] == '\n' || line[slen-1] == '\r'))
192 0 : line[--slen] = '\0';
193 0 : if (slen > 0 && (line[slen-1] == '\r'))
194 0 : line[--slen] = '\0';
195 :
196 0 : if (slen >= size) slen = size - 1;
197 0 : memcpy(buf, line, slen);
198 0 : buf[slen] = '\0';
199 0 : free(line);
200 0 : return (int)slen;
201 : } else {
202 : /* Non-TTY: read from stdin without echo manipulation */
203 2 : char *line = NULL;
204 2 : size_t len = 0;
205 2 : ssize_t nread = getline(&line, &len, stdin);
206 2 : if (nread == -1 || !line) {
207 1 : free(line);
208 1 : return -1;
209 : }
210 1 : size_t slen = strlen(line);
211 1 : if (slen > 0 && (line[slen-1] == '\n' || line[slen-1] == '\r'))
212 1 : line[--slen] = '\0';
213 1 : if (slen > 0 && (line[slen-1] == '\r'))
214 0 : line[--slen] = '\0';
215 1 : if (slen >= size) slen = size - 1;
216 1 : memcpy(buf, line, slen);
217 1 : buf[slen] = '\0';
218 1 : free(line);
219 1 : return (int)slen;
220 : }
221 : }
|