/* term.c: terminal handling, cursor movement etc. (C) 2000-2007 Hans Lub This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License , or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. You may contact the author by: e-mail: hanslub42@gmail.com */ #include "rlwrap.h" /* rlwrap needs a little bit of cursor acrobatics to cope with multi-line prompts, recovering from CTRL-Z and editors that may have used the alternate screen. All such cursor movements are done via the functions in this file. In the past, rlwrap relied on only the termcap library (or an emulation of it), and would fall back on reasonable defaults ("\r", "\b" etc) if this was not found. In the future it will also use terminfo or even ncurses. But the fall-back to older libraries is still important, as rlwrap seems to be especially needed on older machines */ /*global vars */ char term_eof; /* end_of_file char */ char term_stop; /* stop (suspend) key */ char *term_backspace; /* backspace control seq (or 0, if none defined in terminfo) */ char *term_cursor_hpos; /* control seq to position cursor st given position on current line */ char *term_clear_screen; char *term_cursor_up; char *term_cursor_down; char *term_cursor_left; /* only used for debugging (the SHOWCURSOR macro) */ char *term_cursor_right; /* only used to emulate a missing term_cursor_hpos */ char *term_smcup; /* smcup - char sequence to switch to alternate screen */ char *term_rmcup; /* rmcup - char sequence to return from alternate screen */ char *term_rmkx; /* rmkx - char sequence to return from keyboard application mode */ char *term_enable_bracketed_paste; /* If we write this to some terminals (xterm etc.) they will */ /* "bracket" pasted input with \e[200 and \e201 */ int term_has_colours; int redisplay = 1; int newline_came_last = TRUE; /* used to determine whether rlwrap needs to ouptut a newline at the very end */ struct termios saved_terminal_settings; /* original terminal settings */ int terminal_settings_saved = FALSE; /* saved_terminal_settings is valid */ struct winsize winsize; /* current window size */ static char *term_cr; /* carriage return (or 0, if none defined in terminfo) */ static char *term_clear_line; char *term_name; static char *my_tgetstr (char *id, const char *capability_name) { #ifdef HAVE_TERMCAP_H char *term_string_buf = (char *)mymalloc(2048), *tb = term_string_buf; char *stringcap = tgetstr(id, &tb); /* rl_get_termcap(id) only gets capabilities used by readline */ char *retval = stringcap ? mysavestring(stringcap) : NULL; DPRINTF3(DEBUG_TERMIO, "tgetstr(\"%s\") = %s (%s)", id, (stringcap ? M(stringcap) : "NULL"), capability_name); free(term_string_buf); return retval; #else return NULL; #endif } static char *my_tigetstr (char *tid, const char *capability_name) { #ifdef HAVE_CURSES_H static int term_has_been_setup = FALSE; char *stringcap, *retval; if (!term_has_been_setup) { setupterm(term_name, STDIN_FILENO, (int *)0); /* like tgetent() before tgetstr(), we have to call setupterm() befor tigetstr() */ term_has_been_setup = TRUE; } stringcap = tigetstr(tid); retval = stringcap ? mysavestring(stringcap) : NULL; DPRINTF3(DEBUG_TERMIO, "tigetstr(\"%s\") = %s (%s)", tid, (stringcap ? M(stringcap) : "NULL"), capability_name); return retval; #else return NULL; #endif } /* Get (copies of) escape codes ("string capabilities") by name. First try terminfo name, then termcap name */ static char * tigetstr_or_else_tgetstr(char *capname, char *tcap_code, const char *capability_name) { char *retval; retval = my_tigetstr(capname, capability_name); if (!retval) retval = my_tgetstr(tcap_code, capability_name); return retval; } #define FALLBACK_TERMINAL "vt100" /* hard-coded terminal name to use when we don't know which terminal we're on */ void init_terminal(void) { /* save term settings and determine term type */ char *term_buf = mymalloc(2048); int we_have_stringcaps; int we_are_on_hp_ux11; if (!isatty(STDIN_FILENO)) myerror(FATAL|NOERRNO, "stdin is not a tty"); if (tcgetattr(STDIN_FILENO, &saved_terminal_settings) < 0) myerror(FATAL|USE_ERRNO, "tcgetattr error on stdin"); else terminal_settings_saved = TRUE; if (ioctl(STDIN_FILENO, TIOCGWINSZ, &winsize) < 0) myerror(FATAL|USE_ERRNO, "Could not get terminal size"); DPRINTF2(DEBUG_TERMIO, "winsize.ws_col (term width): %d; winsize.ws_row (term height): %d", winsize.ws_col, winsize.ws_row); if (winsize.ws_col == 0) myerror(FATAL|NOERRNO, "My terminal reports width=0 (is it emacs?) I can't handle this, sorry!"); /* init some variables: */ term_name = getenv("TERM"); DPRINTF1(DEBUG_TERMIO, "found TERM = %s", term_name); /* On weird and scary HP-UX 11 succesful tgetent() returns 0, so we need the followig to portably catch errors: */ we_are_on_hp_ux11 = tgetent(term_buf, "vt100") == 0 && tgetent(term_buf,"QzBgt57gr6xwxw") == -1; /* assuming there is no terminal called Qz... */ #define T_OK(retval) ((retval) > 0 || ((retval)== 0 && we_are_on_hp_ux11)) /* if TERM is not set, use FALLBACK_TERMINAL */ if (!term_name || strlen(term_name)==0) { myerror(WARNING|USE_ERRNO, "environment variable TERM not set, assuming %s", FALLBACK_TERMINAL); term_name = FALLBACK_TERMINAL; } /* If we cannot find stringcaps, use FALLBACK_TERMINAL */ if (! (we_have_stringcaps = T_OK(tgetent(term_buf, term_name))) && strcmp(term_name, FALLBACK_TERMINAL)) { myerror(WARNING|NOERRNO, "your $TERM is '%s' but %s couldn't find it in the terminfo database. We'll use '%s'", term_name, program_name, FALLBACK_TERMINAL); term_name = FALLBACK_TERMINAL; } /* If we cannot find stringcaps, even for FALLBACK_TERMINAL, complain */ if (!we_have_stringcaps && ! (we_have_stringcaps = T_OK(tgetent(term_buf, FALLBACK_TERMINAL)))) myerror(WARNING|NOERRNO, "Even %s is not found in the terminfo database. Expect some problems.", FALLBACK_TERMINAL); DPRINTF1(DEBUG_TERMIO, "using TERM = %s", term_name); mysetenv("TERM", term_name); if (we_have_stringcaps) { term_backspace = tigetstr_or_else_tgetstr("cub1", "le", "backspace"); term_cr = tigetstr_or_else_tgetstr("cr", "cr", "carriage return"); term_clear_line = tigetstr_or_else_tgetstr("el", "ce", "clear line"); term_clear_screen = tigetstr_or_else_tgetstr("clear", "cl", "clear screen"); term_cursor_hpos = tigetstr_or_else_tgetstr("hpa", "ch", "set cursor horizontal position"); term_cursor_left = tigetstr_or_else_tgetstr("cub1", "le", "move cursor left"); term_cursor_right = tigetstr_or_else_tgetstr("cuf1", "nd", "move cursor right"); term_cursor_up = tigetstr_or_else_tgetstr("cuu1", "up", "move cursor up"); term_cursor_down = tigetstr_or_else_tgetstr("cud1", "do", "move cursor down"); term_has_colours = tigetstr_or_else_tgetstr("initc", "Ic", "initialise colour") ? TRUE : FALSE; /* the following codes are never output by rlwrap, but used to recognize the use of the the alternate screen by the client, and to filter out "garbage" that is coming from commands that use them */ term_smcup = tigetstr_or_else_tgetstr("smcup", "ti", "switch to alternate screen"); term_rmcup = tigetstr_or_else_tgetstr("rmcup", "te", "exit alternate screen"); term_rmkx = tigetstr_or_else_tgetstr("rmkx", "ke", "leave keypad-transmit mode"); /* there is no way (yet) to determine whether a terminal knows about bracketed paste - we cannot use tigetstr(). "Dumb" terminals do not, of course */ term_enable_bracketed_paste = strings_are_equal(term_name, "dumb") ? NULL : "\033[?2004h"; if (!term_cursor_right) /* probably only on 'dumb' terminal */ term_cursor_right = " "; /* term_colors = tigetnum("colors"); DPRINTF1(DEBUG_TERMIO, "terminal colors: %d", term_colors); */ } if (!(term_cursor_hpos || term_cursor_right) || !term_cursor_up || !term_cursor_down) myerror(WARNING|NOERRNO, "Your terminal '%s' is not fully functional, expect some problems.", term_name); term_eof = saved_terminal_settings.c_cc[VEOF]; term_stop = saved_terminal_settings.c_cc[VSTOP]; DPRINTF1(DEBUG_TERMIO, "term_eof=%d", term_eof); free(term_buf); } void set_echo(int yes) { struct termios *pterm = my_tcgetattr(slave_pty_sensing_fd, "slave pty"); /* mimic terminal settings of client */ if (!pterm) /* child has probably died */ return; pterm->c_lflag &= ~ICANON; /* except a few details... */ pterm->c_cc[VMIN] = 1; pterm->c_cc[VTIME] = 0; if (yes) pterm->c_lflag |= ECHO; else /* no */ pterm->c_lflag &= ~ECHO; log_terminal_settings(pterm); if (tcsetattr(STDIN_FILENO, TCSANOW, pterm) < 0 && errno != ENOTTY) { /* nothing */ } /* myerror(FATAL|USE_ERRNO, "cannot prepare terminal (tcsetattr error on stdin)"); */ free(pterm); } int cursor_hpos(int col) { char *instantiated; if (term_cursor_hpos) { instantiated = tgoto(term_cursor_hpos, 0, col); /* tgoto with a command that takes one parameter: parameter goes to 2nd arg ("vertical position"). */ assert(instantiated); DPRINTF2(DEBUG_TERMIO, "tgoto(term_cursor_hpos, 0, %d) = %s", col, M(instantiated)); tputs(instantiated, 1, my_putchar); } else { int i; cr(); for (i = 0; i < col; i++) tputs(term_cursor_right, 1, my_putchar); } return TRUE; } void cr() { if (term_cr) tputs(term_cr, 1, my_putchar); else my_putchar('\r'); } void clear_the_screen() { /* clear_screen is a macro in term.h */ int i; if (term_clear_screen) tputs(term_clear_screen, 1, my_putchar); else for (i = 0; i < winsize.ws_row; i++) my_putchar('\n'); /* poor man's clear screen */ } void clear_line() { int i; int width = winsize.ws_col; char *p, *spaces; cr(); if (term_clear_line) tputs(term_clear_line, 1, my_putchar); else { spaces = (char *) mymalloc(width +1); for (i = 0, p = spaces; i < width; i++, p++) *p = ' '; *p = '\0'; my_putstr(spaces); /* poor man's clear line */ free((void *)spaces); } cr(); } void backspace(int count) { int i; if (term_backspace) for (i = 0; i < count; i++) tputs(term_backspace, 1, my_putchar); else for (i = 0; i < count; i++) my_putchar('\b'); } void curs_left() { if (term_cursor_left) tputs(term_cursor_left, 1, my_putchar); else backspace(1); } void curs_up() { if (term_cursor_up) tputs(term_cursor_up, 1, my_putchar); } void curs_down() { if (term_cursor_down) tputs(term_cursor_down, 1, my_putchar); } int my_putchar(TPUTS_PUTC_ARGTYPE c) { char ch = c; ssize_t nwritten = write(STDOUT_FILENO, &ch, 1); return (nwritten == -1 ? -1 : c); } void my_putstr(const char *string) { int string_length = strlen(string); DPRINTF2(DEBUG_TERMIO,"wrote %d bytes to stdout: %s", string_length, M(string)); if (string_length == 0) return; write_patiently(STDOUT_FILENO, string, string_length, "to stdout"); newline_came_last = (string[string_length - 1] == '\n'); /* remember whether newline came last */ } static void test_termfunc(char *control_string, char *control_string_name, char* start, void (* termfunc)(), char *end) { char *mangled_control_string = (control_string ? add3strings("\"", mangle_string_for_debug_log(control_string,20),"\"") : mysavestring("NULL")); printf("\n%s = %s\n", control_string_name, mangled_control_string); if (!control_string) printf("trying without suitable control string, fasten seatbelts and brace for impact... \n"); my_putstr(start); sleep(1); termfunc(); my_putstr(end); sleep(1); free(mangled_control_string); } static void backspace1 () { backspace(1); } static void cursor_hpos4 () { cursor_hpos(4);} void test_terminal() { if (debug) { debug_fp = fopen(DEBUG_FILENAME, "w"); setbuf(debug_fp, NULL); } init_terminal(); printf("\nTerminal is \"%s\"\n", term_name); test_termfunc(term_backspace, "term_backspace", "This should print \"grape\": \ngras", &backspace1, "pe\n"); test_termfunc(term_cr, "term_cr", "This should print \"lemon juice\": \napple", &cr, "lemon juice\n"); test_termfunc(term_clear_line, "term_clear_line","This should print \"apple\": \npomegranate", &clear_line, "apple\n"); test_termfunc(term_cursor_hpos, "term_cursor_hpos", "This should print \"pomegranate\": \npomelo", &cursor_hpos4, "granate\n"); test_termfunc(term_cursor_up, "term_cursor_up", "This should print \"blackberry\": \n blueberry\n", &curs_up,"black\n"); printf("\n"); }