cnq3/code/linux/linux_tty.cpp

590 lines
15 KiB
C++

/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.
This file is part of Quake III Arena source code.
Quake III Arena source code 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.
Quake III Arena source code 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 Quake III Arena source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===========================================================================
*/
#include <termios.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
#include "linux_local.h"
static qbool stdin_active = qtrue;
// enable/disable tty input mode
// NOTE TTimo this is used during startup, cannot be changed during run
static cvar_t *ttycon = NULL;
// general flag to tell about tty console mode
static qbool ttycon_on = qfalse;
// when printing general stuff to stdout stderr (Sys_Printf)
// we need to disable the tty console stuff
// this increments so we can recursively disable
static int ttycon_hide = 0;
// some key codes that the terminal may be using
// TTimo NOTE: I'm not sure how relevant this is
static int tty_erase;
static int tty_eof;
static struct termios tty_tc;
static field_t tty_con;
static cvar_t *ttycon_ansicolor = NULL;
static history_t tty_history;
// =============================================================
// 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 tty_Clear and tty_Show to be called before and after a stdout or stderr output
// =============================================================
// flush stdin, I suspect some terminals are sending a LOT of shit
// FIXME TTimo relevant?
static void tty_FlushIn()
{
char key;
while (read(0, &key, 1)!=-1);
}
// do a backspace
// TTimo NOTE: it seems on some terminals just sending '\b' is not enough
// so for now, in any case we send "\b \b" .. yeah well ..
// (there may be a way to find out if '\b' alone would work though)
static void tty_Back()
{
char key;
key = '\b';
write(STDOUT_FILENO, &key, 1);
key = ' ';
write(STDOUT_FILENO, &key, 1);
key = '\b';
write(STDOUT_FILENO, &key, 1);
}
// clear the display of the line currently edited
// bring cursor back to beginning of line
void tty_Hide()
{
assert(ttycon_on);
if (ttycon_hide)
{
ttycon_hide++;
return;
}
const int length = strlen(tty_con.buffer);
if (length > 0)
{
const int stepsForward = length - tty_con.cursor;
// a single write call for this didn't work on my terminal
for (int i = 0; i < stepsForward; ++i)
{
write(STDOUT_FILENO, "\033[1C", 4);
}
for (int i = 0; i < length; ++i)
{
tty_Back();
}
}
tty_Back(); // delete the leading "]"
ttycon_hide++;
}
// show the current line
// FIXME TTimo need to position the cursor if needed??
static void tty_Show()
{
assert(ttycon_on);
assert(ttycon_hide>0);
ttycon_hide--;
if (ttycon_hide == 0)
{
write(STDOUT_FILENO, "]", 1);
const int length = strlen(tty_con.buffer);
if (length > 0)
{
const int stepsBack = length - tty_con.cursor;
write(STDOUT_FILENO, tty_con.buffer, length);
// a single write call for this didn't work on my terminal
for (int i = 0; i < stepsBack; ++i)
{
write(STDOUT_FILENO, "\033[1D", 4);
}
}
}
}
// initialize the console input (tty mode if wanted and possible)
void Lin_ConsoleInputInit()
{
struct termios tc;
// TTimo
// https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=390
// ttycon 0 or 1, if the process is backgrounded (running non interactively)
// then SIGTTIN or SIGTOU is emitted, if not catched, turns into a SIGSTP
signal(SIGTTIN, SIG_IGN);
signal(SIGTTOU, SIG_IGN);
// FIXME TTimo initialize this in Sys_Init or something?
ttycon = Cvar_Get("ttycon", "1", 0);
if (ttycon && ttycon->value)
{
if (isatty(STDIN_FILENO)!=1)
{
Com_Printf("stdin is not a tty, tty console mode failed: %s\n", strerror(errno));
Cvar_Set("ttycon", "0");
ttycon_on = qfalse;
return;
}
Com_Printf("Started tty console (use +set ttycon 0 to disable)\n");
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 = qtrue;
ttycon_ansicolor = Cvar_Get( "ttycon_ansicolor", "0", CVAR_ARCHIVE );
} else
ttycon_on = qfalse;
// make stdin non-blocking
fcntl( STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL, 0) | FNDELAY );
}
const char* Lin_ConsoleInput()
{
// we use this when sending back commands
static char text[256];
int avail;
char key;
field_t field;
if (ttycon && ttycon->value)
{
avail = read(STDIN_FILENO, &key, 1);
if (avail != -1)
{
// we have something
if (key != '\t')
tty_con.acOffset = 0;
if (key == tty_erase || key == 127 || key == 8) // backspace
{
const int length = strlen(tty_con.buffer);
if (length > 0)
{
if (tty_con.cursor == length)
{
tty_con.buffer[length - 1] = '\0';
tty_con.cursor--;
tty_Back();
}
else if (tty_con.cursor > 0)
{
tty_Hide();
const int toMove = length + 1 - tty_con.cursor; // with the null terminator
memmove(tty_con.buffer + tty_con.cursor - 1, tty_con.buffer + tty_con.cursor, toMove);
tty_con.cursor--;
tty_Show();
}
}
return NULL;
}
// check if this is a control char
if ((key) && (key) < ' ')
{
if (key == '\n')
{
#ifdef DEDICATED
History_SaveCommand(&tty_history, &tty_con);
Q_strncpyz(text, tty_con.buffer, sizeof(text));
Field_Clear(&tty_con);
write(STDOUT_FILENO, "\n]", 2);
#else
// not in a game yet and no leading slash?
if (cls.state != CA_ACTIVE &&
tty_con.buffer[0] != '/' && tty_con.buffer[0] != '\\')
{
// there's no one to chat with, so we consider this a command
const int length = strlen(tty_con.buffer);
if (length > 0)
{
memmove(tty_con.buffer + 1, tty_con.buffer, length + 1);
}
else
{
tty_con.buffer[1] = '\0';
}
tty_con.buffer[0] = '\\';
tty_con.cursor++;
}
// decide what the final command will be
const int length = strlen(tty_con.buffer);
if (tty_con.buffer[0] == '/' || tty_con.buffer[0] == '\\')
Q_strncpyz(text, tty_con.buffer + 1, sizeof(text));
else if (length > 0)
Com_sprintf(text, sizeof(text), "say %s", tty_con.buffer);
else
*text = '\0';
History_SaveCommand(&tty_history, &tty_con);
tty_Hide();
Com_Printf("tty]%s\n", tty_con.buffer);
Field_Clear(&tty_con);
tty_Show();
#endif
return text;
}
if (key == '\t')
{
tty_Hide();
#ifdef DEDICATED
Field_AutoComplete( &tty_con, qfalse );
#else
Field_AutoComplete( &tty_con, qtrue );
#endif
tty_Show();
return NULL;
}
avail = read(0, &key, 1);
if (avail != -1)
{
// VT 100 keys
if (key == '[' || key == 'O')
{
avail = read(0, &key, 1);
if (avail != -1)
{
switch (key)
{
case 'A': // up arrow (move cursor up)
History_GetPreviousCommand(&field, &tty_history);
if (field.buffer[0] != '\0')
{
tty_Hide();
tty_con = field;
tty_Show();
}
tty_FlushIn();
return NULL;
break;
case 'B': // down arrow (move cursor down)
History_GetNextCommand(&field, &tty_history, 0);
tty_Hide();
if (tty_con.buffer[0] != '\0')
{
tty_con = field;
}
else
{
Field_Clear(&tty_con);
}
tty_Show();
tty_FlushIn();
return NULL;
break;
case 'C': // right arrow (move cursor right)
if (tty_con.cursor < strlen(tty_con.buffer))
{
write(STDOUT_FILENO, "\033[1C", 4);
tty_con.cursor++;
}
return NULL;
case 'D': // left arrow (move cursor left)
if (tty_con.cursor > 0)
{
write(STDOUT_FILENO, "\033[1D", 4);
tty_con.cursor--;
}
return NULL;
case '3': // delete (might not work on all terminals)
{
const int length = strlen(tty_con.buffer);
if (tty_con.cursor < length)
{
tty_Hide();
const int toMove = length - tty_con.cursor; // with terminating NULL
memmove(tty_con.buffer + tty_con.cursor, tty_con.buffer + tty_con.cursor + 1, toMove);
tty_Show();
}
}
tty_FlushIn();
return NULL;
case 'H': // home (move cursor to upper left corner)
if (tty_con.cursor > 0)
{
tty_Hide();
tty_con.cursor = 0;
tty_Show();
}
return NULL;
case 'F': // end (might not work on all terminals)
{
const int length = strlen(tty_con.buffer);
if (tty_con.cursor < length)
{
tty_Hide();
tty_con.cursor = length;
tty_Show();
}
}
return NULL;
}
}
}
}
Com_DPrintf("droping ISCTL sequence: %d, tty_erase: %d\n", key, tty_erase);
tty_FlushIn();
return NULL;
}
// if we get here, key is a regular character
const int length = strlen(tty_con.buffer);
if (tty_con.cursor == length)
{
write(STDOUT_FILENO, &key, 1);
tty_con.buffer[tty_con.cursor + 0] = key;
tty_con.buffer[tty_con.cursor + 1] = '\0';
tty_con.cursor++;
}
else
{
tty_Hide();
const int toMove = length + 1 - tty_con.cursor; // need to move the null terminator too
memmove(tty_con.buffer + tty_con.cursor + 1, tty_con.buffer + tty_con.cursor, toMove);
tty_con.buffer[tty_con.cursor] = key;
tty_con.cursor++;
tty_Show();
}
}
return NULL;
} else
{
int len;
fd_set fdset;
struct timeval timeout;
if (!com_dedicated || !com_dedicated->value)
return NULL;
if (!stdin_active)
return NULL;
FD_ZERO(&fdset);
FD_SET(STDIN_FILENO, &fdset);
timeout.tv_sec = 0;
timeout.tv_usec = 0;
if (select (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 = qfalse;
return NULL;
}
if (len < 1)
return NULL;
text[len-1] = 0; // rip off the /n and terminate
return text;
}
}
// never exit without calling this, or your terminal will be left in a pretty bad state
void Lin_ConsoleInputShutdown()
{
if (ttycon_on)
{
tty_Back(); // delete the leading "]"
tcsetattr (STDIN_FILENO, TCSADRAIN, &tty_tc);
ttycon_on = qfalse;
}
// make stdin blocking
fcntl( STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL, 0) & (~FNDELAY) );
}
static struct Q3ToAnsiColorTable_s
{
char Q3color;
const char* ANSIcolor;
} const tty_colorTable[] =
{
{ COLOR_BLACK, "30" },
{ COLOR_RED, "31" },
{ COLOR_GREEN, "32" },
{ COLOR_YELLOW, "33" },
{ COLOR_BLUE, "34" },
{ COLOR_CYAN, "36" },
{ COLOR_MAGENTA, "35" },
{ COLOR_WHITE, "0" }
};
static const int tty_colorTableSize = sizeof( tty_colorTable ) / sizeof( tty_colorTable[0] );
static void ANSIColorify( const char *msg, char *buffer, int bufferSize )
{
int msgLength, pos;
int i, j;
const char* escapeCode;
char tempBuffer[ 7 ];
if( !msg || !buffer )
return;
msgLength = strlen( msg );
pos = 0;
i = 0;
buffer[ 0 ] = '\0';
while( i < msgLength )
{
if( msg[ i ] == '\n' )
{
Com_sprintf( tempBuffer, 7, "%c[0m\n", 0x1B );
strncat( buffer, tempBuffer, bufferSize );
i++;
}
else if( msg[ i ] == Q_COLOR_ESCAPE )
{
i++;
if( i < msgLength )
{
escapeCode = NULL;
for( j = 0; j < tty_colorTableSize; j++ )
{
if( msg[ i ] == tty_colorTable[ j ].Q3color )
{
escapeCode = tty_colorTable[ j ].ANSIcolor;
break;
}
}
if( escapeCode )
{
Com_sprintf( tempBuffer, 7, "%c[%sm", 0x1B, escapeCode );
strncat( buffer, tempBuffer, bufferSize );
}
i++;
}
}
else
{
Com_sprintf( tempBuffer, 7, "%c", msg[ i++ ] );
strncat( buffer, tempBuffer, bufferSize );
}
}
}
static char* CleanTermStr( char* string )
{
char* s = string;
char* d = string;
char c;
while ((c = *s) != 0 ) {
if ( Q_IsColorString( s ) )
s++;
else
*d++ = c;
s++;
}
*d = '\0';
return string;
}
void Sys_Print( const char *msg )
{
static char finalMsg[ MAXPRINTMSG ];
if (ttycon_on)
{
tty_Hide();
}
if( ttycon_on && ttycon_ansicolor && ttycon_ansicolor->integer )
{
ANSIColorify( msg, finalMsg, sizeof(finalMsg) );
}
else
{
Q_strncpyz( finalMsg, msg, sizeof(finalMsg) );
CleanTermStr( finalMsg );
}
fputs( finalMsg, stdout );
if (ttycon_on)
{
tty_Show();
}
}
qbool tty_Enabled()
{
return ttycon_on;
}
history_t* tty_GetHistory()
{
return &tty_history;
}