/* =========================================================================== Copyright (C) 1999-2005 Id Software, Inc. Copyright (C) 2000-2013 Darklegion Development Copyright (C) 2015-2019 GrangerHub This file is part of Tremulous. Tremulous 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 3 of the License, or (at your option) any later version. Tremulous 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 Tremulous; if not, see =========================================================================== */ #include "sys_local.h" #include #include #include #include #include #ifndef DEDICATED #include "client/client.h" #endif #include "qcommon/cvar.h" #include "qcommon/q_shared.h" #include "qcommon/qcommon.h" /* ============================================================= tty console routines NOTE: if the user is editing a line when something gets printed to the early console then it won't look good so we provide CON_Hide and CON_Show to be called before and after a stdout or stderr output ============================================================= */ extern bool stdinIsATTY; static bool stdin_active = false; // general flag to tell about tty console mode static bool ttycon_on = false; static int ttycon_hide = 0; static int ttycon_show_overdue = 0; // some key codes that the terminal may be using, initialised on start up static int TTY_erase; static int TTY_eof; static struct termios TTY_tc; static field_t TTY_con; // This is somewhat of aduplicate of the graphical console history // but it's safer more modular to have our own here #define CON_HISTORY 32 static field_t ttyEditLines[ CON_HISTORY ]; static int hist_current = -1, hist_count = 0; #ifndef DEDICATED // Don't use "]" as it would be the same as in-game console, // this makes it clear where input came from. #define TTY_CONSOLE_PROMPT "tty]" #else #define TTY_CONSOLE_PROMPT "]" #endif /* ================== CON_FlushIn Flush stdin, I suspect some terminals are sending a LOT of shit FIXME relevant? ================== */ static void CON_FlushIn( void ) { char key; while (read(STDIN_FILENO, &key, 1)!=-1); } /* ================== CON_Back Output a backspace NOTE: it seems on some terminals just sending '\b' is not enough so instead we send "\b \b" (FIXME there may be a way to find out if '\b' alone would work though) ================== */ static void CON_Back( void ) { char key; size_t UNUSED_VAR size; key = '\b'; size = write(STDOUT_FILENO, &key, 1); key = ' '; size = write(STDOUT_FILENO, &key, 1); key = '\b'; size = write(STDOUT_FILENO, &key, 1); } /* ================== CON_Hide Clear the display of the line currently edited bring cursor back to beginning of line ================== */ static void CON_Hide( void ) { if( ttycon_on ) { int i; if (ttycon_hide) { ttycon_hide++; return; } if (TTY_con.cursor>0) { for (i=0; i 0; i--) { CON_Back(); } ttycon_hide++; } } /* ================== CON_Show Show the current line FIXME need to position the cursor if needed? ================== */ static void CON_Show( void ) { if( ttycon_on ) { int i; assert(ttycon_hide>0); ttycon_hide--; if (ttycon_hide == 0) { size_t UNUSED_VAR size; size = write(STDOUT_FILENO, TTY_CONSOLE_PROMPT, strlen(TTY_CONSOLE_PROMPT)); if (TTY_con.cursor) { for (i=0; icursor) return; assert(hist_count <= CON_HISTORY); assert(hist_count >= 0); assert(hist_current >= -1); assert(hist_current <= hist_count); // make some room for (i=CON_HISTORY-1; i>0; i--) { ttyEditLines[i] = ttyEditLines[i-1]; } ttyEditLines[0] = *field; if (hist_count= 0); assert(hist_current >= -1); assert(hist_current <= hist_count); hist_prev = hist_current + 1; if (hist_prev >= hist_count) { return NULL; } hist_current++; return &(ttyEditLines[hist_current]); } /* ================== Hist_Next ================== */ field_t *Hist_Next( void ) { assert(hist_count <= CON_HISTORY); assert(hist_count >= 0); assert(hist_current >= -1); assert(hist_current <= hist_count); if (hist_current >= 0) { hist_current--; } if (hist_current == -1) { return NULL; } return &(ttyEditLines[hist_current]); } /* ================== CON_SigCont Reinitialize console input after receiving SIGCONT, as on Linux the terminal seems to lose all set attributes if user did CTRL+Z and then does fg again. ================== */ void CON_SigCont(int signum) { CON_Init(); } /* ================== CON_Init Initialize the console input (tty mode if possible) ================== */ void CON_Init( void ) { struct termios tc; // If the process is backgrounded (running non interactively) // then SIGTTIN or SIGTOU is emitted, if not caught, turns into a SIGSTP signal(SIGTTIN, SIG_IGN); signal(SIGTTOU, SIG_IGN); // If SIGCONT is received, reinitialize console signal(SIGCONT, CON_SigCont); // Make stdin reads non-blocking fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL, 0) | O_NONBLOCK ); if (!stdinIsATTY) { Com_Printf("tty console mode disabled\n"); ttycon_on = false; stdin_active = true; return; } Field_Clear(&TTY_con); tcgetattr (STDIN_FILENO, &TTY_tc); TTY_erase = TTY_tc.c_cc[VERASE]; TTY_eof = TTY_tc.c_cc[VEOF]; tc = TTY_tc; /* ECHO: don't echo input characters ICANON: enable canonical mode. This enables the special characters EOF, EOL, EOL2, ERASE, KILL, REPRINT, STATUS, and WERASE, and buffers by lines. ISIG: when any of the characters INTR, QUIT, SUSP, or DSUSP are received, generate the corresponding signal */ tc.c_lflag &= ~(ECHO | ICANON); /* ISTRIP strip off bit 8 INPCK enable input parity checking */ tc.c_iflag &= ~(ISTRIP | INPCK); tc.c_cc[VMIN] = 1; tc.c_cc[VTIME] = 0; tcsetattr (STDIN_FILENO, TCSADRAIN, &tc); ttycon_on = true; ttycon_hide = 1; // Mark as hidden, so prompt is shown in CON_Show CON_Show(); } /* ================== CON_Input ================== */ char *CON_Input( void ) { // we use this when sending back commands static char text[MAX_EDIT_LINE]; int avail; char key; field_t *history; size_t UNUSED_VAR size; if(ttycon_on) { avail = read(STDIN_FILENO, &key, 1); if (avail != -1) { // we have something // backspace? // NOTE TTimo testing a lot of values .. seems it's the only way to get it to work everywhere if ((key == TTY_erase) || (key == 127) || (key == 8)) { if (TTY_con.cursor > 0) { TTY_con.cursor--; TTY_con.buffer[TTY_con.cursor] = '\0'; CON_Back(); } return NULL; } // check if this is a control char if ((key) && (key) < ' ') { if (key == '\n') { #ifndef DEDICATED // if not in the game explicitly prepend a slash if needed if (clc.state != CA_ACTIVE && TTY_con.cursor && TTY_con.buffer[0] != '/' && TTY_con.buffer[0] != '\\') { memmove(TTY_con.buffer + 1, TTY_con.buffer, sizeof(TTY_con.buffer) - 1); TTY_con.buffer[0] = '\\'; TTY_con.cursor++; } if (TTY_con.buffer[0] == '/' || TTY_con.buffer[0] == '\\') { Q_strncpyz(text, TTY_con.buffer + 1, sizeof(text)); } else if (TTY_con.cursor) { Com_sprintf(text, sizeof(text), "cmd say %s", TTY_con.buffer); } else { text[0] = '\0'; } // push it in history Hist_Add(&TTY_con); CON_Hide(); Com_Printf("%s%s\n", TTY_CONSOLE_PROMPT, TTY_con.buffer); Field_Clear(&TTY_con); CON_Show(); #else // push it in history Hist_Add(&TTY_con); Q_strncpyz(text, TTY_con.buffer, sizeof(text)); Field_Clear(&TTY_con); key = '\n'; size = write(STDOUT_FILENO, &key, 1); size = write(STDOUT_FILENO, TTY_CONSOLE_PROMPT, strlen(TTY_CONSOLE_PROMPT)); #endif return text; } if (key == '\t') { CON_Hide(); Field_AutoComplete( &TTY_con ); CON_Show(); return NULL; } avail = read(STDIN_FILENO, &key, 1); if (avail != -1) { // VT 100 keys if (key == '[' || key == 'O') { avail = read(STDIN_FILENO, &key, 1); if (avail != -1) { switch (key) { case 'A': history = Hist_Prev(); if (history) { CON_Hide(); TTY_con = *history; CON_Show(); } CON_FlushIn(); return NULL; break; case 'B': history = Hist_Next(); CON_Hide(); if (history) { TTY_con = *history; } else { Field_Clear(&TTY_con); } CON_Show(); CON_FlushIn(); return NULL; break; case 'C': return NULL; case 'D': return NULL; } } } } Com_DPrintf("droping ISCTL sequence: %d, TTY_erase: %d\n", key, TTY_erase); CON_FlushIn(); return NULL; } if (TTY_con.cursor >= sizeof(text) - 1) return NULL; // push regular character TTY_con.buffer[TTY_con.cursor] = key; TTY_con.cursor++; // next char will always be '\0' // print the current line (this is differential) size = write(STDOUT_FILENO, &key, 1); } return NULL; } else if (stdin_active) { int len; fd_set fdset; struct timeval timeout; FD_ZERO(&fdset); FD_SET(STDIN_FILENO, &fdset); // stdin timeout.tv_sec = 0; timeout.tv_usec = 0; if(select (STDIN_FILENO + 1, &fdset, NULL, NULL, &timeout) == -1 || !FD_ISSET(STDIN_FILENO, &fdset)) return NULL; len = read(STDIN_FILENO, text, sizeof(text)); if (len == 0) { // eof! stdin_active = false; return NULL; } if (len < 1) return NULL; text[len-1] = 0; // rip off the /n and terminate return text; } return NULL; } /* ================== CON_Print ================== */ void CON_Print( const char *msg ) { if (!msg[0]) return; CON_Hide( ); if( com_ansiColor && com_ansiColor->integer ) Sys_AnsiColorPrint( msg ); else fputs( msg, stderr ); if (!ttycon_on) { // CON_Hide didn't do anything. return; } // Only print prompt when msg ends with a newline, otherwise the console // might get garbled when output does not fit on one line. if (msg[strlen(msg) - 1] == '\n') { CON_Show(); // Run CON_Show the number of times it was deferred. while (ttycon_show_overdue > 0) { CON_Show(); ttycon_show_overdue--; } } else { // Defer calling CON_Show ttycon_show_overdue++; } }