You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
433 lines
9.0 KiB
433 lines
9.0 KiB
#include "git-compat-util.h" |
|
#include "compat/terminal.h" |
|
#include "sigchain.h" |
|
#include "strbuf.h" |
|
#include "run-command.h" |
|
#include "string-list.h" |
|
#include "hashmap.h" |
|
|
|
#if defined(HAVE_DEV_TTY) || defined(GIT_WINDOWS_NATIVE) |
|
|
|
static void restore_term_on_signal(int sig) |
|
{ |
|
restore_term(); |
|
sigchain_pop(sig); |
|
raise(sig); |
|
} |
|
|
|
#ifdef HAVE_DEV_TTY |
|
|
|
#define INPUT_PATH "/dev/tty" |
|
#define OUTPUT_PATH "/dev/tty" |
|
|
|
static int term_fd = -1; |
|
static struct termios old_term; |
|
|
|
void restore_term(void) |
|
{ |
|
if (term_fd < 0) |
|
return; |
|
|
|
tcsetattr(term_fd, TCSAFLUSH, &old_term); |
|
close(term_fd); |
|
term_fd = -1; |
|
} |
|
|
|
int save_term(int full_duplex) |
|
{ |
|
if (term_fd < 0) |
|
term_fd = open("/dev/tty", O_RDWR); |
|
|
|
return (term_fd < 0) ? -1 : tcgetattr(term_fd, &old_term); |
|
} |
|
|
|
static int disable_bits(tcflag_t bits) |
|
{ |
|
struct termios t; |
|
|
|
if (save_term(0) < 0) |
|
goto error; |
|
|
|
t = old_term; |
|
sigchain_push_common(restore_term_on_signal); |
|
|
|
t.c_lflag &= ~bits; |
|
if (!tcsetattr(term_fd, TCSAFLUSH, &t)) |
|
return 0; |
|
|
|
error: |
|
close(term_fd); |
|
term_fd = -1; |
|
return -1; |
|
} |
|
|
|
static int disable_echo(void) |
|
{ |
|
return disable_bits(ECHO); |
|
} |
|
|
|
static int enable_non_canonical(void) |
|
{ |
|
return disable_bits(ICANON | ECHO); |
|
} |
|
|
|
#elif defined(GIT_WINDOWS_NATIVE) |
|
|
|
#define INPUT_PATH "CONIN$" |
|
#define OUTPUT_PATH "CONOUT$" |
|
#define FORCE_TEXT "t" |
|
|
|
static int use_stty = 1; |
|
static struct string_list stty_restore = STRING_LIST_INIT_DUP; |
|
static HANDLE hconin = INVALID_HANDLE_VALUE; |
|
static HANDLE hconout = INVALID_HANDLE_VALUE; |
|
static DWORD cmode_in, cmode_out; |
|
|
|
void restore_term(void) |
|
{ |
|
if (use_stty) { |
|
int i; |
|
struct child_process cp = CHILD_PROCESS_INIT; |
|
|
|
if (stty_restore.nr == 0) |
|
return; |
|
|
|
strvec_push(&cp.args, "stty"); |
|
for (i = 0; i < stty_restore.nr; i++) |
|
strvec_push(&cp.args, stty_restore.items[i].string); |
|
run_command(&cp); |
|
string_list_clear(&stty_restore, 0); |
|
return; |
|
} |
|
|
|
if (hconin == INVALID_HANDLE_VALUE) |
|
return; |
|
|
|
SetConsoleMode(hconin, cmode_in); |
|
CloseHandle(hconin); |
|
if (cmode_out) { |
|
assert(hconout != INVALID_HANDLE_VALUE); |
|
SetConsoleMode(hconout, cmode_out); |
|
CloseHandle(hconout); |
|
} |
|
|
|
hconin = hconout = INVALID_HANDLE_VALUE; |
|
} |
|
|
|
int save_term(int full_duplex) |
|
{ |
|
hconin = CreateFileA("CONIN$", GENERIC_READ | GENERIC_WRITE, |
|
FILE_SHARE_READ, NULL, OPEN_EXISTING, |
|
FILE_ATTRIBUTE_NORMAL, NULL); |
|
if (hconin == INVALID_HANDLE_VALUE) |
|
return -1; |
|
|
|
if (full_duplex) { |
|
hconout = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE, |
|
FILE_SHARE_WRITE, NULL, OPEN_EXISTING, |
|
FILE_ATTRIBUTE_NORMAL, NULL); |
|
if (hconout == INVALID_HANDLE_VALUE) |
|
goto error; |
|
|
|
GetConsoleMode(hconout, &cmode_out); |
|
} |
|
|
|
GetConsoleMode(hconin, &cmode_in); |
|
use_stty = 0; |
|
return 0; |
|
error: |
|
CloseHandle(hconin); |
|
hconin = INVALID_HANDLE_VALUE; |
|
return -1; |
|
} |
|
|
|
static int disable_bits(DWORD bits) |
|
{ |
|
if (use_stty) { |
|
struct child_process cp = CHILD_PROCESS_INIT; |
|
|
|
strvec_push(&cp.args, "stty"); |
|
|
|
if (bits & ENABLE_LINE_INPUT) { |
|
string_list_append(&stty_restore, "icanon"); |
|
strvec_push(&cp.args, "-icanon"); |
|
} |
|
|
|
if (bits & ENABLE_ECHO_INPUT) { |
|
string_list_append(&stty_restore, "echo"); |
|
strvec_push(&cp.args, "-echo"); |
|
} |
|
|
|
if (bits & ENABLE_PROCESSED_INPUT) { |
|
string_list_append(&stty_restore, "-ignbrk"); |
|
string_list_append(&stty_restore, "intr"); |
|
string_list_append(&stty_restore, "^c"); |
|
strvec_push(&cp.args, "ignbrk"); |
|
strvec_push(&cp.args, "intr"); |
|
strvec_push(&cp.args, ""); |
|
} |
|
|
|
if (run_command(&cp) == 0) |
|
return 0; |
|
|
|
/* `stty` could not be executed; access the Console directly */ |
|
use_stty = 0; |
|
} |
|
|
|
if (save_term(0) < 0) |
|
return -1; |
|
|
|
sigchain_push_common(restore_term_on_signal); |
|
if (!SetConsoleMode(hconin, cmode_in & ~bits)) { |
|
CloseHandle(hconin); |
|
hconin = INVALID_HANDLE_VALUE; |
|
return -1; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static int disable_echo(void) |
|
{ |
|
return disable_bits(ENABLE_ECHO_INPUT); |
|
} |
|
|
|
static int enable_non_canonical(void) |
|
{ |
|
return disable_bits(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); |
|
} |
|
|
|
/* |
|
* Override `getchar()`, as the default implementation does not use |
|
* `ReadFile()`. |
|
* |
|
* This poses a problem when we want to see whether the standard |
|
* input has more characters, as the default of Git for Windows is to start the |
|
* Bash in a MinTTY, which uses a named pipe to emulate a pty, in which case |
|
* our `poll()` emulation calls `PeekNamedPipe()`, which seems to require |
|
* `ReadFile()` to be called first to work properly (it only reports 0 |
|
* available bytes, otherwise). |
|
* |
|
* So let's just override `getchar()` with a version backed by `ReadFile()` and |
|
* go our merry ways from here. |
|
*/ |
|
static int mingw_getchar(void) |
|
{ |
|
DWORD read = 0; |
|
unsigned char ch; |
|
|
|
if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), &ch, 1, &read, NULL)) |
|
return EOF; |
|
|
|
if (!read) { |
|
error("Unexpected 0 read"); |
|
return EOF; |
|
} |
|
|
|
return ch; |
|
} |
|
#define getchar mingw_getchar |
|
|
|
#endif |
|
|
|
#ifndef FORCE_TEXT |
|
#define FORCE_TEXT |
|
#endif |
|
|
|
char *git_terminal_prompt(const char *prompt, int echo) |
|
{ |
|
static struct strbuf buf = STRBUF_INIT; |
|
int r; |
|
FILE *input_fh, *output_fh; |
|
|
|
input_fh = fopen(INPUT_PATH, "r" FORCE_TEXT); |
|
if (!input_fh) |
|
return NULL; |
|
|
|
output_fh = fopen(OUTPUT_PATH, "w" FORCE_TEXT); |
|
if (!output_fh) { |
|
fclose(input_fh); |
|
return NULL; |
|
} |
|
|
|
if (!echo && disable_echo()) { |
|
fclose(input_fh); |
|
fclose(output_fh); |
|
return NULL; |
|
} |
|
|
|
fputs(prompt, output_fh); |
|
fflush(output_fh); |
|
|
|
r = strbuf_getline_lf(&buf, input_fh); |
|
if (!echo) { |
|
putc('\n', output_fh); |
|
fflush(output_fh); |
|
} |
|
|
|
restore_term(); |
|
fclose(input_fh); |
|
fclose(output_fh); |
|
|
|
if (r == EOF) |
|
return NULL; |
|
return buf.buf; |
|
} |
|
|
|
/* |
|
* The `is_known_escape_sequence()` function returns 1 if the passed string |
|
* corresponds to an Escape sequence that the terminal capabilities contains. |
|
* |
|
* To avoid depending on ncurses or other platform-specific libraries, we rely |
|
* on the presence of the `infocmp` executable to do the job for us (failing |
|
* silently if the program is not available or refused to run). |
|
*/ |
|
struct escape_sequence_entry { |
|
struct hashmap_entry entry; |
|
char sequence[FLEX_ARRAY]; |
|
}; |
|
|
|
static int sequence_entry_cmp(const void *hashmap_cmp_fn_data, |
|
const struct escape_sequence_entry *e1, |
|
const struct escape_sequence_entry *e2, |
|
const void *keydata) |
|
{ |
|
return strcmp(e1->sequence, keydata ? keydata : e2->sequence); |
|
} |
|
|
|
static int is_known_escape_sequence(const char *sequence) |
|
{ |
|
static struct hashmap sequences; |
|
static int initialized; |
|
|
|
if (!initialized) { |
|
struct child_process cp = CHILD_PROCESS_INIT; |
|
struct strbuf buf = STRBUF_INIT; |
|
char *p, *eol; |
|
|
|
hashmap_init(&sequences, (hashmap_cmp_fn)sequence_entry_cmp, |
|
NULL, 0); |
|
|
|
strvec_pushl(&cp.args, "infocmp", "-L", "-1", NULL); |
|
if (pipe_command(&cp, NULL, 0, &buf, 0, NULL, 0)) |
|
strbuf_setlen(&buf, 0); |
|
|
|
for (eol = p = buf.buf; *p; p = eol + 1) { |
|
p = strchr(p, '='); |
|
if (!p) |
|
break; |
|
p++; |
|
eol = strchrnul(p, '\n'); |
|
|
|
if (starts_with(p, "\\E")) { |
|
char *comma = memchr(p, ',', eol - p); |
|
struct escape_sequence_entry *e; |
|
|
|
p[0] = '^'; |
|
p[1] = '['; |
|
FLEX_ALLOC_MEM(e, sequence, p, comma - p); |
|
hashmap_entry_init(&e->entry, |
|
strhash(e->sequence)); |
|
hashmap_add(&sequences, &e->entry); |
|
} |
|
if (!*eol) |
|
break; |
|
} |
|
initialized = 1; |
|
} |
|
|
|
return !!hashmap_get_from_hash(&sequences, strhash(sequence), sequence); |
|
} |
|
|
|
int read_key_without_echo(struct strbuf *buf) |
|
{ |
|
static int warning_displayed; |
|
int ch; |
|
|
|
if (warning_displayed || enable_non_canonical() < 0) { |
|
if (!warning_displayed) { |
|
warning("reading single keystrokes not supported on " |
|
"this platform; reading line instead"); |
|
warning_displayed = 1; |
|
} |
|
|
|
return strbuf_getline(buf, stdin); |
|
} |
|
|
|
strbuf_reset(buf); |
|
ch = getchar(); |
|
if (ch == EOF) { |
|
restore_term(); |
|
return EOF; |
|
} |
|
strbuf_addch(buf, ch); |
|
|
|
if (ch == '\033' /* ESC */) { |
|
/* |
|
* We are most likely looking at an Escape sequence. Let's try |
|
* to read more bytes, waiting at most half a second, assuming |
|
* that the sequence is complete if we did not receive any byte |
|
* within that time. |
|
* |
|
* Start by replacing the Escape byte with ^[ */ |
|
strbuf_splice(buf, buf->len - 1, 1, "^[", 2); |
|
|
|
/* |
|
* Query the terminal capabilities once about all the Escape |
|
* sequences it knows about, so that we can avoid waiting for |
|
* half a second when we know that the sequence is complete. |
|
*/ |
|
while (!is_known_escape_sequence(buf->buf)) { |
|
struct pollfd pfd = { .fd = 0, .events = POLLIN }; |
|
|
|
if (poll(&pfd, 1, 500) < 1) |
|
break; |
|
|
|
ch = getchar(); |
|
if (ch == EOF) |
|
return 0; |
|
strbuf_addch(buf, ch); |
|
} |
|
} |
|
|
|
restore_term(); |
|
return 0; |
|
} |
|
|
|
#else |
|
|
|
int save_term(int full_duplex) |
|
{ |
|
/* full_duplex == 1, but no support available */ |
|
return -full_duplex; |
|
} |
|
|
|
void restore_term(void) |
|
{ |
|
} |
|
|
|
char *git_terminal_prompt(const char *prompt, int echo) |
|
{ |
|
return getpass(prompt); |
|
} |
|
|
|
int read_key_without_echo(struct strbuf *buf) |
|
{ |
|
static int warning_displayed; |
|
const char *res; |
|
|
|
if (!warning_displayed) { |
|
warning("reading single keystrokes not supported on this " |
|
"platform; reading line instead"); |
|
warning_displayed = 1; |
|
} |
|
|
|
res = getpass(""); |
|
strbuf_reset(buf); |
|
if (!res) |
|
return EOF; |
|
strbuf_addstr(buf, res); |
|
return 0; |
|
} |
|
|
|
#endif
|
|
|