ioq3quest/code/sys/con_tty.c
2020-09-06 13:40:32 +02:00

557 lines
12 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 "../qcommon/q_shared.h"
#include "../qcommon/qcommon.h"
#include "sys_local.h"
#ifndef DEDICATED
#include "../client/client.h"
#endif
#include <unistd.h>
#include <signal.h>
#include <termios.h>
#include <fcntl.h>
#include <sys/time.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 qboolean stdinIsATTY;
static qboolean stdin_active;
// general flag to tell about tty console mode
static qboolean ttycon_on = qfalse;
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_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<TTY_con.cursor; i++)
{
CON_Back();
}
}
// Delete prompt
for (i = strlen(TTY_CONSOLE_PROMPT); 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; i<TTY_con.cursor; i++)
{
size = write(STDOUT_FILENO, TTY_con.buffer+i, 1);
}
}
}
}
}
/*
==================
CON_Shutdown
Never exit without calling this, or your terminal will be left in a pretty bad state
==================
*/
void CON_Shutdown( void )
{
if (ttycon_on)
{
CON_Hide();
tcsetattr (STDIN_FILENO, TCSADRAIN, &TTY_tc);
}
// Restore blocking to stdin reads
fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL, 0) & ~O_NONBLOCK);
}
/*
==================
Hist_Add
==================
*/
void Hist_Add(field_t *field)
{
int i;
// Don't save blank lines in history.
if (!field->cursor)
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<CON_HISTORY)
{
hist_count++;
}
hist_current = -1; // re-init
}
/*
==================
Hist_Prev
==================
*/
field_t *Hist_Prev( void )
{
int hist_prev;
assert(hist_count <= CON_HISTORY);
assert(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 = qfalse;
stdin_active = qtrue;
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 = qtrue;
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 && con_autochat->integer && 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) {
if (con_autochat->integer) {
Com_sprintf(text, sizeof(text), "cmd say %s", TTY_con.buffer);
} else {
Q_strncpyz(text, TTY_con.buffer, sizeof(text));
}
} 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();
}
tcflush(STDIN_FILENO, TCIFLUSH);
return NULL;
break;
case 'B':
history = Hist_Next();
CON_Hide();
if (history)
{
TTY_con = *history;
} else
{
Field_Clear(&TTY_con);
}
CON_Show();
tcflush(STDIN_FILENO, TCIFLUSH);
return NULL;
break;
case 'C':
return NULL;
case 'D':
return NULL;
}
}
}
}
Com_DPrintf("droping ISCTL sequence: %d, TTY_erase: %d\n", key, TTY_erase);
tcflush(STDIN_FILENO, TCIFLUSH);
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 = qfalse;
return NULL;
}
if (len < 1)
return NULL;
text[len-1] = 0; // rip off the /n and terminate
return text;
}
return NULL;
}
/*
==================
CON_Print
==================
*/
void (*GLogcatFn) ( const char *msg );
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 __ANDROID__
if (GLogcatFn) {
(*GLogcatFn)(msg);
}
#endif
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++;
}
}
#if __ANDROID__
/*
==================
CON_LogcatFn
==================
*/
void CON_LogcatFn( void (*LogcatFn)( const char* message ) )
{
GLogcatFn = LogcatFn;
}
#endif