]> git.ipfire.org Git - thirdparty/git.git/blame - compat/terminal.c
Merge branch 'ea/blame-use-oideq'
[thirdparty/git.git] / compat / terminal.c
CommitLineData
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
EFL
10
11static void restore_term(void);
12
13static void restore_term_on_signal(int sig)
14{
15 restore_term();
16 sigchain_pop(sig);
17 raise(sig);
18}
19
21aeafce
JK
20#ifdef HAVE_DEV_TTY
21
afb43561
EFL
22#define INPUT_PATH "/dev/tty"
23#define OUTPUT_PATH "/dev/tty"
24
21aeafce
JK
25static int term_fd = -1;
26static struct termios old_term;
27
28static void restore_term(void)
29{
30 if (term_fd < 0)
31 return;
32
33 tcsetattr(term_fd, TCSAFLUSH, &old_term);
9df92e63 34 close(term_fd);
21aeafce
JK
35 term_fd = -1;
36}
37
94ac3c31 38static int disable_bits(tcflag_t bits)
9df92e63
EFL
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
94ac3c31 49 t.c_lflag &= ~bits;
9df92e63
EFL
50 if (!tcsetattr(term_fd, TCSAFLUSH, &t))
51 return 0;
52
53error:
54 close(term_fd);
55 term_fd = -1;
56 return -1;
57}
58
94ac3c31
JS
59static int disable_echo(void)
60{
61 return disable_bits(ECHO);
62}
63
a5e46e6b
JS
64static int enable_non_canonical(void)
65{
66 return disable_bits(ICANON | ECHO);
67}
68
380395d0 69#elif defined(GIT_WINDOWS_NATIVE)
afb43561
EFL
70
71#define INPUT_PATH "CONIN$"
72#define OUTPUT_PATH "CONOUT$"
73#define FORCE_TEXT "t"
74
9ea416cb
JS
75static int use_stty = 1;
76static struct string_list stty_restore = STRING_LIST_INIT_DUP;
afb43561
EFL
77static HANDLE hconin = INVALID_HANDLE_VALUE;
78static DWORD cmode;
79
80static void restore_term(void)
81{
9ea416cb
JS
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
ef8d7ac4 89 strvec_push(&cp.args, "stty");
9ea416cb 90 for (i = 0; i < stty_restore.nr; i++)
ef8d7ac4 91 strvec_push(&cp.args, stty_restore.items[i].string);
9ea416cb
JS
92 run_command(&cp);
93 string_list_clear(&stty_restore, 0);
94 return;
95 }
96
afb43561
EFL
97 if (hconin == INVALID_HANDLE_VALUE)
98 return;
99
100 SetConsoleMode(hconin, cmode);
101 CloseHandle(hconin);
102 hconin = INVALID_HANDLE_VALUE;
103}
104
94ac3c31 105static int disable_bits(DWORD bits)
afb43561 106{
9ea416cb
JS
107 if (use_stty) {
108 struct child_process cp = CHILD_PROCESS_INIT;
109
ef8d7ac4 110 strvec_push(&cp.args, "stty");
9ea416cb
JS
111
112 if (bits & ENABLE_LINE_INPUT) {
113 string_list_append(&stty_restore, "icanon");
ef8d7ac4 114 strvec_push(&cp.args, "-icanon");
9ea416cb
JS
115 }
116
117 if (bits & ENABLE_ECHO_INPUT) {
118 string_list_append(&stty_restore, "echo");
ef8d7ac4 119 strvec_push(&cp.args, "-echo");
9ea416cb
JS
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");
ef8d7ac4
JK
126 strvec_push(&cp.args, "ignbrk");
127 strvec_push(&cp.args, "intr");
128 strvec_push(&cp.args, "");
9ea416cb
JS
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
afb43561
EFL
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);
94ac3c31 146 if (!SetConsoleMode(hconin, cmode & ~bits)) {
afb43561
EFL
147 CloseHandle(hconin);
148 hconin = INVALID_HANDLE_VALUE;
149 return -1;
150 }
151
152 return 0;
153}
154
94ac3c31
JS
155static int disable_echo(void)
156{
157 return disable_bits(ENABLE_ECHO_INPUT);
158}
159
a5e46e6b
JS
160static int enable_non_canonical(void)
161{
162 return disable_bits(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
163}
94ac3c31 164
e118f063
JS
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 */
179static 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
afb43561
EFL
196#endif
197
198#ifndef FORCE_TEXT
199#define FORCE_TEXT
200#endif
201
21aeafce
JK
202char *git_terminal_prompt(const char *prompt, int echo)
203{
204 static struct strbuf buf = STRBUF_INIT;
205 int r;
67fe7356 206 FILE *input_fh, *output_fh;
21aeafce 207
afb43561 208 input_fh = fopen(INPUT_PATH, "r" FORCE_TEXT);
67fe7356 209 if (!input_fh)
21aeafce
JK
210 return NULL;
211
afb43561 212 output_fh = fopen(OUTPUT_PATH, "w" FORCE_TEXT);
67fe7356
EFL
213 if (!output_fh) {
214 fclose(input_fh);
215 return NULL;
216 }
217
9df92e63 218 if (!echo && disable_echo()) {
67fe7356
EFL
219 fclose(input_fh);
220 fclose(output_fh);
9df92e63 221 return NULL;
21aeafce
JK
222 }
223
67fe7356
EFL
224 fputs(prompt, output_fh);
225 fflush(output_fh);
21aeafce 226
8f309aeb 227 r = strbuf_getline_lf(&buf, input_fh);
21aeafce 228 if (!echo) {
67fe7356
EFL
229 putc('\n', output_fh);
230 fflush(output_fh);
21aeafce
JK
231 }
232
233 restore_term();
67fe7356
EFL
234 fclose(input_fh);
235 fclose(output_fh);
21aeafce
JK
236
237 if (r == EOF)
238 return NULL;
239 return buf.buf;
240}
241
12acdf57
JS
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 */
250struct escape_sequence_entry {
251 struct hashmap_entry entry;
252 char sequence[FLEX_ARRAY];
253};
254
255static 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
263static 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
ef8d7ac4 276 strvec_pushl(&cp.args, "infocmp", "-L", "-1", NULL);
12acdf57
JS
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
a5e46e6b
JS
307int 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 }
a5e46e6b 328 strbuf_addch(buf, ch);
e118f063
JS
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
12acdf57
JS
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)) {
e118f063
JS
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
a5e46e6b
JS
358 restore_term();
359 return 0;
360}
361
21aeafce
JK
362#else
363
364char *git_terminal_prompt(const char *prompt, int echo)
365{
366 return getpass(prompt);
367}
368
a5e46e6b
JS
369int 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
21aeafce 388#endif