]>
Commit | Line | Data |
---|---|---|
21aeafce JK |
1 | #include "git-compat-util.h" |
2 | #include "compat/terminal.h" | |
3 | #include "sigchain.h" | |
4 | #include "strbuf.h" | |
9ea416cb JS |
5 | #include "run-command.h" |
6 | #include "string-list.h" | |
12acdf57 | 7 | #include "hashmap.h" |
21aeafce | 8 | |
380395d0 | 9 | #if defined(HAVE_DEV_TTY) || defined(GIT_WINDOWS_NATIVE) |
afb43561 | 10 | |
afb43561 EFL |
11 | static void restore_term_on_signal(int sig) |
12 | { | |
13 | restore_term(); | |
14 | sigchain_pop(sig); | |
15 | raise(sig); | |
16 | } | |
17 | ||
21aeafce JK |
18 | #ifdef HAVE_DEV_TTY |
19 | ||
afb43561 EFL |
20 | #define INPUT_PATH "/dev/tty" |
21 | #define OUTPUT_PATH "/dev/tty" | |
22 | ||
21aeafce JK |
23 | static int term_fd = -1; |
24 | static struct termios old_term; | |
25 | ||
e22b245e | 26 | void restore_term(void) |
21aeafce JK |
27 | { |
28 | if (term_fd < 0) | |
29 | return; | |
30 | ||
31 | tcsetattr(term_fd, TCSAFLUSH, &old_term); | |
9df92e63 | 32 | close(term_fd); |
21aeafce JK |
33 | term_fd = -1; |
34 | } | |
35 | ||
e22b245e CMAB |
36 | int save_term(int full_duplex) |
37 | { | |
38 | if (term_fd < 0) | |
39 | term_fd = open("/dev/tty", O_RDWR); | |
40 | ||
41 | return (term_fd < 0) ? -1 : tcgetattr(term_fd, &old_term); | |
42 | } | |
43 | ||
94ac3c31 | 44 | static int disable_bits(tcflag_t bits) |
9df92e63 EFL |
45 | { |
46 | struct termios t; | |
47 | ||
e22b245e | 48 | if (save_term(0) < 0) |
9df92e63 EFL |
49 | goto error; |
50 | ||
e22b245e | 51 | t = old_term; |
9df92e63 EFL |
52 | sigchain_push_common(restore_term_on_signal); |
53 | ||
94ac3c31 | 54 | t.c_lflag &= ~bits; |
9df92e63 EFL |
55 | if (!tcsetattr(term_fd, TCSAFLUSH, &t)) |
56 | return 0; | |
57 | ||
58 | error: | |
59 | close(term_fd); | |
60 | term_fd = -1; | |
61 | return -1; | |
62 | } | |
63 | ||
94ac3c31 JS |
64 | static int disable_echo(void) |
65 | { | |
66 | return disable_bits(ECHO); | |
67 | } | |
68 | ||
a5e46e6b JS |
69 | static int enable_non_canonical(void) |
70 | { | |
71 | return disable_bits(ICANON | ECHO); | |
72 | } | |
73 | ||
380395d0 | 74 | #elif defined(GIT_WINDOWS_NATIVE) |
afb43561 EFL |
75 | |
76 | #define INPUT_PATH "CONIN$" | |
77 | #define OUTPUT_PATH "CONOUT$" | |
78 | #define FORCE_TEXT "t" | |
79 | ||
9ea416cb JS |
80 | static int use_stty = 1; |
81 | static struct string_list stty_restore = STRING_LIST_INIT_DUP; | |
afb43561 | 82 | static HANDLE hconin = INVALID_HANDLE_VALUE; |
e22b245e CMAB |
83 | static HANDLE hconout = INVALID_HANDLE_VALUE; |
84 | static DWORD cmode_in, cmode_out; | |
afb43561 | 85 | |
e22b245e | 86 | void restore_term(void) |
afb43561 | 87 | { |
9ea416cb JS |
88 | if (use_stty) { |
89 | int i; | |
90 | struct child_process cp = CHILD_PROCESS_INIT; | |
91 | ||
92 | if (stty_restore.nr == 0) | |
93 | return; | |
94 | ||
ef8d7ac4 | 95 | strvec_push(&cp.args, "stty"); |
9ea416cb | 96 | for (i = 0; i < stty_restore.nr; i++) |
ef8d7ac4 | 97 | strvec_push(&cp.args, stty_restore.items[i].string); |
9ea416cb JS |
98 | run_command(&cp); |
99 | string_list_clear(&stty_restore, 0); | |
100 | return; | |
101 | } | |
102 | ||
afb43561 EFL |
103 | if (hconin == INVALID_HANDLE_VALUE) |
104 | return; | |
105 | ||
e22b245e CMAB |
106 | SetConsoleMode(hconin, cmode_in); |
107 | CloseHandle(hconin); | |
108 | if (cmode_out) { | |
109 | assert(hconout != INVALID_HANDLE_VALUE); | |
110 | SetConsoleMode(hconout, cmode_out); | |
111 | CloseHandle(hconout); | |
112 | } | |
113 | ||
114 | hconin = hconout = INVALID_HANDLE_VALUE; | |
115 | } | |
116 | ||
117 | int save_term(int full_duplex) | |
118 | { | |
119 | hconin = CreateFileA("CONIN$", GENERIC_READ | GENERIC_WRITE, | |
120 | FILE_SHARE_READ, NULL, OPEN_EXISTING, | |
121 | FILE_ATTRIBUTE_NORMAL, NULL); | |
122 | if (hconin == INVALID_HANDLE_VALUE) | |
123 | return -1; | |
124 | ||
125 | if (full_duplex) { | |
126 | hconout = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE, | |
127 | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, | |
128 | FILE_ATTRIBUTE_NORMAL, NULL); | |
129 | if (hconout == INVALID_HANDLE_VALUE) | |
130 | goto error; | |
131 | ||
132 | GetConsoleMode(hconout, &cmode_out); | |
133 | } | |
134 | ||
135 | GetConsoleMode(hconin, &cmode_in); | |
136 | use_stty = 0; | |
137 | return 0; | |
138 | error: | |
afb43561 EFL |
139 | CloseHandle(hconin); |
140 | hconin = INVALID_HANDLE_VALUE; | |
e22b245e | 141 | return -1; |
afb43561 EFL |
142 | } |
143 | ||
94ac3c31 | 144 | static int disable_bits(DWORD bits) |
afb43561 | 145 | { |
9ea416cb JS |
146 | if (use_stty) { |
147 | struct child_process cp = CHILD_PROCESS_INIT; | |
148 | ||
ef8d7ac4 | 149 | strvec_push(&cp.args, "stty"); |
9ea416cb JS |
150 | |
151 | if (bits & ENABLE_LINE_INPUT) { | |
152 | string_list_append(&stty_restore, "icanon"); | |
ef8d7ac4 | 153 | strvec_push(&cp.args, "-icanon"); |
9ea416cb JS |
154 | } |
155 | ||
156 | if (bits & ENABLE_ECHO_INPUT) { | |
157 | string_list_append(&stty_restore, "echo"); | |
ef8d7ac4 | 158 | strvec_push(&cp.args, "-echo"); |
9ea416cb JS |
159 | } |
160 | ||
161 | if (bits & ENABLE_PROCESSED_INPUT) { | |
162 | string_list_append(&stty_restore, "-ignbrk"); | |
163 | string_list_append(&stty_restore, "intr"); | |
164 | string_list_append(&stty_restore, "^c"); | |
ef8d7ac4 JK |
165 | strvec_push(&cp.args, "ignbrk"); |
166 | strvec_push(&cp.args, "intr"); | |
167 | strvec_push(&cp.args, ""); | |
9ea416cb JS |
168 | } |
169 | ||
170 | if (run_command(&cp) == 0) | |
171 | return 0; | |
172 | ||
173 | /* `stty` could not be executed; access the Console directly */ | |
174 | use_stty = 0; | |
175 | } | |
176 | ||
e22b245e | 177 | if (save_term(0) < 0) |
afb43561 EFL |
178 | return -1; |
179 | ||
afb43561 | 180 | sigchain_push_common(restore_term_on_signal); |
e22b245e | 181 | if (!SetConsoleMode(hconin, cmode_in & ~bits)) { |
afb43561 EFL |
182 | CloseHandle(hconin); |
183 | hconin = INVALID_HANDLE_VALUE; | |
184 | return -1; | |
185 | } | |
186 | ||
187 | return 0; | |
188 | } | |
189 | ||
94ac3c31 JS |
190 | static int disable_echo(void) |
191 | { | |
192 | return disable_bits(ENABLE_ECHO_INPUT); | |
193 | } | |
194 | ||
a5e46e6b JS |
195 | static int enable_non_canonical(void) |
196 | { | |
197 | return disable_bits(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); | |
198 | } | |
94ac3c31 | 199 | |
e118f063 JS |
200 | /* |
201 | * Override `getchar()`, as the default implementation does not use | |
202 | * `ReadFile()`. | |
203 | * | |
204 | * This poses a problem when we want to see whether the standard | |
205 | * input has more characters, as the default of Git for Windows is to start the | |
206 | * Bash in a MinTTY, which uses a named pipe to emulate a pty, in which case | |
207 | * our `poll()` emulation calls `PeekNamedPipe()`, which seems to require | |
208 | * `ReadFile()` to be called first to work properly (it only reports 0 | |
209 | * available bytes, otherwise). | |
210 | * | |
211 | * So let's just override `getchar()` with a version backed by `ReadFile()` and | |
212 | * go our merry ways from here. | |
213 | */ | |
214 | static int mingw_getchar(void) | |
215 | { | |
216 | DWORD read = 0; | |
217 | unsigned char ch; | |
218 | ||
219 | if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), &ch, 1, &read, NULL)) | |
220 | return EOF; | |
221 | ||
222 | if (!read) { | |
223 | error("Unexpected 0 read"); | |
224 | return EOF; | |
225 | } | |
226 | ||
227 | return ch; | |
228 | } | |
229 | #define getchar mingw_getchar | |
230 | ||
afb43561 EFL |
231 | #endif |
232 | ||
233 | #ifndef FORCE_TEXT | |
234 | #define FORCE_TEXT | |
235 | #endif | |
236 | ||
21aeafce JK |
237 | char *git_terminal_prompt(const char *prompt, int echo) |
238 | { | |
239 | static struct strbuf buf = STRBUF_INIT; | |
240 | int r; | |
67fe7356 | 241 | FILE *input_fh, *output_fh; |
21aeafce | 242 | |
afb43561 | 243 | input_fh = fopen(INPUT_PATH, "r" FORCE_TEXT); |
67fe7356 | 244 | if (!input_fh) |
21aeafce JK |
245 | return NULL; |
246 | ||
afb43561 | 247 | output_fh = fopen(OUTPUT_PATH, "w" FORCE_TEXT); |
67fe7356 EFL |
248 | if (!output_fh) { |
249 | fclose(input_fh); | |
250 | return NULL; | |
251 | } | |
252 | ||
9df92e63 | 253 | if (!echo && disable_echo()) { |
67fe7356 EFL |
254 | fclose(input_fh); |
255 | fclose(output_fh); | |
9df92e63 | 256 | return NULL; |
21aeafce JK |
257 | } |
258 | ||
67fe7356 EFL |
259 | fputs(prompt, output_fh); |
260 | fflush(output_fh); | |
21aeafce | 261 | |
8f309aeb | 262 | r = strbuf_getline_lf(&buf, input_fh); |
21aeafce | 263 | if (!echo) { |
67fe7356 EFL |
264 | putc('\n', output_fh); |
265 | fflush(output_fh); | |
21aeafce JK |
266 | } |
267 | ||
268 | restore_term(); | |
67fe7356 EFL |
269 | fclose(input_fh); |
270 | fclose(output_fh); | |
21aeafce JK |
271 | |
272 | if (r == EOF) | |
273 | return NULL; | |
274 | return buf.buf; | |
275 | } | |
276 | ||
12acdf57 JS |
277 | /* |
278 | * The `is_known_escape_sequence()` function returns 1 if the passed string | |
279 | * corresponds to an Escape sequence that the terminal capabilities contains. | |
280 | * | |
281 | * To avoid depending on ncurses or other platform-specific libraries, we rely | |
282 | * on the presence of the `infocmp` executable to do the job for us (failing | |
283 | * silently if the program is not available or refused to run). | |
284 | */ | |
285 | struct escape_sequence_entry { | |
286 | struct hashmap_entry entry; | |
287 | char sequence[FLEX_ARRAY]; | |
288 | }; | |
289 | ||
290 | static int sequence_entry_cmp(const void *hashmap_cmp_fn_data, | |
291 | const struct escape_sequence_entry *e1, | |
292 | const struct escape_sequence_entry *e2, | |
293 | const void *keydata) | |
294 | { | |
295 | return strcmp(e1->sequence, keydata ? keydata : e2->sequence); | |
296 | } | |
297 | ||
298 | static int is_known_escape_sequence(const char *sequence) | |
299 | { | |
300 | static struct hashmap sequences; | |
301 | static int initialized; | |
302 | ||
303 | if (!initialized) { | |
304 | struct child_process cp = CHILD_PROCESS_INIT; | |
305 | struct strbuf buf = STRBUF_INIT; | |
306 | char *p, *eol; | |
307 | ||
308 | hashmap_init(&sequences, (hashmap_cmp_fn)sequence_entry_cmp, | |
309 | NULL, 0); | |
310 | ||
ef8d7ac4 | 311 | strvec_pushl(&cp.args, "infocmp", "-L", "-1", NULL); |
12acdf57 JS |
312 | if (pipe_command(&cp, NULL, 0, &buf, 0, NULL, 0)) |
313 | strbuf_setlen(&buf, 0); | |
314 | ||
315 | for (eol = p = buf.buf; *p; p = eol + 1) { | |
316 | p = strchr(p, '='); | |
317 | if (!p) | |
318 | break; | |
319 | p++; | |
320 | eol = strchrnul(p, '\n'); | |
321 | ||
322 | if (starts_with(p, "\\E")) { | |
323 | char *comma = memchr(p, ',', eol - p); | |
324 | struct escape_sequence_entry *e; | |
325 | ||
326 | p[0] = '^'; | |
327 | p[1] = '['; | |
328 | FLEX_ALLOC_MEM(e, sequence, p, comma - p); | |
329 | hashmap_entry_init(&e->entry, | |
330 | strhash(e->sequence)); | |
331 | hashmap_add(&sequences, &e->entry); | |
332 | } | |
333 | if (!*eol) | |
334 | break; | |
335 | } | |
336 | initialized = 1; | |
337 | } | |
338 | ||
339 | return !!hashmap_get_from_hash(&sequences, strhash(sequence), sequence); | |
340 | } | |
341 | ||
a5e46e6b JS |
342 | int read_key_without_echo(struct strbuf *buf) |
343 | { | |
344 | static int warning_displayed; | |
345 | int ch; | |
346 | ||
347 | if (warning_displayed || enable_non_canonical() < 0) { | |
348 | if (!warning_displayed) { | |
349 | warning("reading single keystrokes not supported on " | |
350 | "this platform; reading line instead"); | |
351 | warning_displayed = 1; | |
352 | } | |
353 | ||
354 | return strbuf_getline(buf, stdin); | |
355 | } | |
356 | ||
357 | strbuf_reset(buf); | |
358 | ch = getchar(); | |
359 | if (ch == EOF) { | |
360 | restore_term(); | |
361 | return EOF; | |
362 | } | |
a5e46e6b | 363 | strbuf_addch(buf, ch); |
e118f063 JS |
364 | |
365 | if (ch == '\033' /* ESC */) { | |
366 | /* | |
367 | * We are most likely looking at an Escape sequence. Let's try | |
368 | * to read more bytes, waiting at most half a second, assuming | |
369 | * that the sequence is complete if we did not receive any byte | |
370 | * within that time. | |
371 | * | |
372 | * Start by replacing the Escape byte with ^[ */ | |
373 | strbuf_splice(buf, buf->len - 1, 1, "^[", 2); | |
374 | ||
12acdf57 JS |
375 | /* |
376 | * Query the terminal capabilities once about all the Escape | |
377 | * sequences it knows about, so that we can avoid waiting for | |
378 | * half a second when we know that the sequence is complete. | |
379 | */ | |
380 | while (!is_known_escape_sequence(buf->buf)) { | |
e118f063 JS |
381 | struct pollfd pfd = { .fd = 0, .events = POLLIN }; |
382 | ||
383 | if (poll(&pfd, 1, 500) < 1) | |
384 | break; | |
385 | ||
386 | ch = getchar(); | |
387 | if (ch == EOF) | |
388 | return 0; | |
389 | strbuf_addch(buf, ch); | |
390 | } | |
391 | } | |
392 | ||
a5e46e6b JS |
393 | restore_term(); |
394 | return 0; | |
395 | } | |
396 | ||
21aeafce JK |
397 | #else |
398 | ||
e22b245e CMAB |
399 | int save_term(int full_duplex) |
400 | { | |
401 | /* full_duplex == 1, but no support available */ | |
402 | return -full_duplex; | |
403 | } | |
404 | ||
405 | void restore_term(void) | |
406 | { | |
407 | } | |
408 | ||
21aeafce JK |
409 | char *git_terminal_prompt(const char *prompt, int echo) |
410 | { | |
411 | return getpass(prompt); | |
412 | } | |
413 | ||
a5e46e6b JS |
414 | int read_key_without_echo(struct strbuf *buf) |
415 | { | |
416 | static int warning_displayed; | |
417 | const char *res; | |
418 | ||
419 | if (!warning_displayed) { | |
420 | warning("reading single keystrokes not supported on this " | |
421 | "platform; reading line instead"); | |
422 | warning_displayed = 1; | |
423 | } | |
424 | ||
425 | res = getpass(""); | |
426 | strbuf_reset(buf); | |
427 | if (!res) | |
428 | return EOF; | |
429 | strbuf_addstr(buf, res); | |
430 | return 0; | |
431 | } | |
432 | ||
21aeafce | 433 | #endif |