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