terminal: pop signal handler when terminal is restored
[git.git] / compat / terminal.c
blob11288cfe5c96680862c101441ffdcb4fd8755e1b
1 #include "git-compat-util.h"
2 #include "compat/terminal.h"
3 #include "sigchain.h"
4 #include "strbuf.h"
5 #include "run-command.h"
6 #include "string-list.h"
7 #include "hashmap.h"
9 #if defined(HAVE_DEV_TTY) || defined(GIT_WINDOWS_NATIVE)
11 static void restore_term_on_signal(int sig)
13 restore_term();
14 /* restore_term calls sigchain_pop_common */
15 raise(sig);
18 #ifdef HAVE_DEV_TTY
20 #define INPUT_PATH "/dev/tty"
21 #define OUTPUT_PATH "/dev/tty"
23 static int term_fd = -1;
24 static struct termios old_term;
26 void restore_term(void)
28 if (term_fd < 0)
29 return;
31 tcsetattr(term_fd, TCSAFLUSH, &old_term);
32 close(term_fd);
33 term_fd = -1;
34 sigchain_pop_common();
37 int save_term(int full_duplex)
39 if (term_fd < 0)
40 term_fd = open("/dev/tty", O_RDWR);
41 if (term_fd < 0)
42 return -1;
43 if (tcgetattr(term_fd, &old_term) < 0)
44 return -1;
45 sigchain_push_common(restore_term_on_signal);
47 return 0;
50 static int disable_bits(tcflag_t bits)
52 struct termios t;
54 if (save_term(0) < 0)
55 goto error;
57 t = old_term;
59 t.c_lflag &= ~bits;
60 if (!tcsetattr(term_fd, TCSAFLUSH, &t))
61 return 0;
63 sigchain_pop_common();
64 error:
65 close(term_fd);
66 term_fd = -1;
67 return -1;
70 static int disable_echo(void)
72 return disable_bits(ECHO);
75 static int enable_non_canonical(void)
77 return disable_bits(ICANON | ECHO);
80 #elif defined(GIT_WINDOWS_NATIVE)
82 #define INPUT_PATH "CONIN$"
83 #define OUTPUT_PATH "CONOUT$"
84 #define FORCE_TEXT "t"
86 static int use_stty = 1;
87 static struct string_list stty_restore = STRING_LIST_INIT_DUP;
88 static HANDLE hconin = INVALID_HANDLE_VALUE;
89 static HANDLE hconout = INVALID_HANDLE_VALUE;
90 static DWORD cmode_in, cmode_out;
92 void restore_term(void)
94 if (use_stty) {
95 int i;
96 struct child_process cp = CHILD_PROCESS_INIT;
98 if (stty_restore.nr == 0)
99 return;
101 strvec_push(&cp.args, "stty");
102 for (i = 0; i < stty_restore.nr; i++)
103 strvec_push(&cp.args, stty_restore.items[i].string);
104 run_command(&cp);
105 string_list_clear(&stty_restore, 0);
106 return;
109 sigchain_pop_common();
111 if (hconin == INVALID_HANDLE_VALUE)
112 return;
114 SetConsoleMode(hconin, cmode_in);
115 CloseHandle(hconin);
116 if (cmode_out) {
117 assert(hconout != INVALID_HANDLE_VALUE);
118 SetConsoleMode(hconout, cmode_out);
119 CloseHandle(hconout);
122 hconin = hconout = INVALID_HANDLE_VALUE;
125 int save_term(int full_duplex)
127 hconin = CreateFileA("CONIN$", GENERIC_READ | GENERIC_WRITE,
128 FILE_SHARE_READ, NULL, OPEN_EXISTING,
129 FILE_ATTRIBUTE_NORMAL, NULL);
130 if (hconin == INVALID_HANDLE_VALUE)
131 return -1;
133 if (full_duplex) {
134 hconout = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE,
135 FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
136 FILE_ATTRIBUTE_NORMAL, NULL);
137 if (hconout == INVALID_HANDLE_VALUE)
138 goto error;
140 GetConsoleMode(hconout, &cmode_out);
143 GetConsoleMode(hconin, &cmode_in);
144 use_stty = 0;
145 sigchain_push_common(restore_term_on_signal);
146 return 0;
147 error:
148 CloseHandle(hconin);
149 hconin = INVALID_HANDLE_VALUE;
150 return -1;
153 static int disable_bits(DWORD bits)
155 if (use_stty) {
156 struct child_process cp = CHILD_PROCESS_INIT;
158 strvec_push(&cp.args, "stty");
160 if (bits & ENABLE_LINE_INPUT) {
161 string_list_append(&stty_restore, "icanon");
162 strvec_push(&cp.args, "-icanon");
165 if (bits & ENABLE_ECHO_INPUT) {
166 string_list_append(&stty_restore, "echo");
167 strvec_push(&cp.args, "-echo");
170 if (bits & ENABLE_PROCESSED_INPUT) {
171 string_list_append(&stty_restore, "-ignbrk");
172 string_list_append(&stty_restore, "intr");
173 string_list_append(&stty_restore, "^c");
174 strvec_push(&cp.args, "ignbrk");
175 strvec_push(&cp.args, "intr");
176 strvec_push(&cp.args, "");
179 if (run_command(&cp) == 0)
180 return 0;
182 /* `stty` could not be executed; access the Console directly */
183 use_stty = 0;
186 if (save_term(0) < 0)
187 return -1;
189 if (!SetConsoleMode(hconin, cmode_in & ~bits)) {
190 CloseHandle(hconin);
191 hconin = INVALID_HANDLE_VALUE;
192 sigchain_pop_common();
193 return -1;
196 return 0;
199 static int disable_echo(void)
201 return disable_bits(ENABLE_ECHO_INPUT);
204 static int enable_non_canonical(void)
206 return disable_bits(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
210 * Override `getchar()`, as the default implementation does not use
211 * `ReadFile()`.
213 * This poses a problem when we want to see whether the standard
214 * input has more characters, as the default of Git for Windows is to start the
215 * Bash in a MinTTY, which uses a named pipe to emulate a pty, in which case
216 * our `poll()` emulation calls `PeekNamedPipe()`, which seems to require
217 * `ReadFile()` to be called first to work properly (it only reports 0
218 * available bytes, otherwise).
220 * So let's just override `getchar()` with a version backed by `ReadFile()` and
221 * go our merry ways from here.
223 static int mingw_getchar(void)
225 DWORD read = 0;
226 unsigned char ch;
228 if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), &ch, 1, &read, NULL))
229 return EOF;
231 if (!read) {
232 error("Unexpected 0 read");
233 return EOF;
236 return ch;
238 #define getchar mingw_getchar
240 #endif
242 #ifndef FORCE_TEXT
243 #define FORCE_TEXT
244 #endif
246 char *git_terminal_prompt(const char *prompt, int echo)
248 static struct strbuf buf = STRBUF_INIT;
249 int r;
250 FILE *input_fh, *output_fh;
252 input_fh = fopen(INPUT_PATH, "r" FORCE_TEXT);
253 if (!input_fh)
254 return NULL;
256 output_fh = fopen(OUTPUT_PATH, "w" FORCE_TEXT);
257 if (!output_fh) {
258 fclose(input_fh);
259 return NULL;
262 if (!echo && disable_echo()) {
263 fclose(input_fh);
264 fclose(output_fh);
265 return NULL;
268 fputs(prompt, output_fh);
269 fflush(output_fh);
271 r = strbuf_getline_lf(&buf, input_fh);
272 if (!echo) {
273 putc('\n', output_fh);
274 fflush(output_fh);
277 restore_term();
278 fclose(input_fh);
279 fclose(output_fh);
281 if (r == EOF)
282 return NULL;
283 return buf.buf;
287 * The `is_known_escape_sequence()` function returns 1 if the passed string
288 * corresponds to an Escape sequence that the terminal capabilities contains.
290 * To avoid depending on ncurses or other platform-specific libraries, we rely
291 * on the presence of the `infocmp` executable to do the job for us (failing
292 * silently if the program is not available or refused to run).
294 struct escape_sequence_entry {
295 struct hashmap_entry entry;
296 char sequence[FLEX_ARRAY];
299 static int sequence_entry_cmp(const void *hashmap_cmp_fn_data,
300 const struct escape_sequence_entry *e1,
301 const struct escape_sequence_entry *e2,
302 const void *keydata)
304 return strcmp(e1->sequence, keydata ? keydata : e2->sequence);
307 static int is_known_escape_sequence(const char *sequence)
309 static struct hashmap sequences;
310 static int initialized;
312 if (!initialized) {
313 struct child_process cp = CHILD_PROCESS_INIT;
314 struct strbuf buf = STRBUF_INIT;
315 char *p, *eol;
317 hashmap_init(&sequences, (hashmap_cmp_fn)sequence_entry_cmp,
318 NULL, 0);
320 strvec_pushl(&cp.args, "infocmp", "-L", "-1", NULL);
321 if (pipe_command(&cp, NULL, 0, &buf, 0, NULL, 0))
322 strbuf_setlen(&buf, 0);
324 for (eol = p = buf.buf; *p; p = eol + 1) {
325 p = strchr(p, '=');
326 if (!p)
327 break;
328 p++;
329 eol = strchrnul(p, '\n');
331 if (starts_with(p, "\\E")) {
332 char *comma = memchr(p, ',', eol - p);
333 struct escape_sequence_entry *e;
335 p[0] = '^';
336 p[1] = '[';
337 FLEX_ALLOC_MEM(e, sequence, p, comma - p);
338 hashmap_entry_init(&e->entry,
339 strhash(e->sequence));
340 hashmap_add(&sequences, &e->entry);
342 if (!*eol)
343 break;
345 initialized = 1;
348 return !!hashmap_get_from_hash(&sequences, strhash(sequence), sequence);
351 int read_key_without_echo(struct strbuf *buf)
353 static int warning_displayed;
354 int ch;
356 if (warning_displayed || enable_non_canonical() < 0) {
357 if (!warning_displayed) {
358 warning("reading single keystrokes not supported on "
359 "this platform; reading line instead");
360 warning_displayed = 1;
363 return strbuf_getline(buf, stdin);
366 strbuf_reset(buf);
367 ch = getchar();
368 if (ch == EOF) {
369 restore_term();
370 return EOF;
372 strbuf_addch(buf, ch);
374 if (ch == '\033' /* ESC */) {
376 * We are most likely looking at an Escape sequence. Let's try
377 * to read more bytes, waiting at most half a second, assuming
378 * that the sequence is complete if we did not receive any byte
379 * within that time.
381 * Start by replacing the Escape byte with ^[ */
382 strbuf_splice(buf, buf->len - 1, 1, "^[", 2);
385 * Query the terminal capabilities once about all the Escape
386 * sequences it knows about, so that we can avoid waiting for
387 * half a second when we know that the sequence is complete.
389 while (!is_known_escape_sequence(buf->buf)) {
390 struct pollfd pfd = { .fd = 0, .events = POLLIN };
392 if (poll(&pfd, 1, 500) < 1)
393 break;
395 ch = getchar();
396 if (ch == EOF)
397 break;
398 strbuf_addch(buf, ch);
402 restore_term();
403 return 0;
406 #else
408 int save_term(int full_duplex)
410 /* full_duplex == 1, but no support available */
411 return -full_duplex;
414 void restore_term(void)
418 char *git_terminal_prompt(const char *prompt, int echo)
420 return getpass(prompt);
423 int read_key_without_echo(struct strbuf *buf)
425 static int warning_displayed;
426 const char *res;
428 if (!warning_displayed) {
429 warning("reading single keystrokes not supported on this "
430 "platform; reading line instead");
431 warning_displayed = 1;
434 res = getpass("");
435 strbuf_reset(buf);
436 if (!res)
437 return EOF;
438 strbuf_addstr(buf, res);
439 return 0;
442 #endif