quakeforge/ruamoko/qwaq/qwaq-input.c

470 lines
12 KiB
C

/*
qwaq-input.c
Input handling
Copyright (C) 2020 Bill Currie <bill@taniwha.org>
Author: Bill Currie <bill@taniwha.org>
Date: 2020/03/21
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; if not, write to:
Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <sys/ioctl.h>
#include <ctype.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include "QF/dstring.h"
#include "QF/hash.h"
#include "QF/keys.h"
#include "QF/sys.h"
#include "qwaq.h"
#include "event.h"
#include "qwaq-curses.h"
#define always_inline inline __attribute__((__always_inline__))
#define MOUSE_MOVES_ON "\033[?1003h"
#define MOUSE_MOVES_OFF "\033[?1003l"
#define SGR_ON "\033[?1006h"
#define SGR_OFF "\033[?1006l"
#define WHEEL_BUTTONS 0x78 // scroll up/down/left/right - always click
typedef struct qwaq_key_s {
const char *sequence;
knum_t key;
unsigned shift;
} qwaq_key_t;
static qwaq_key_t default_keys[] = {
{ "\033OP", QFK_F1 },
{ "\033OQ", QFK_F2 },
{ "\033OR", QFK_F3 },
{ "\033OS", QFK_F4 },
{ "\033[15~", QFK_F5 },
{ "\033[17~", QFK_F6 },
{ "\033[18~", QFK_F7 },
{ "\033[19~", QFK_F8 },
{ "\033[20~", QFK_F9 },
{ "\033[21~", QFK_F10 },
{ "\033[23~", QFK_F11 },
{ "\033[24~", QFK_F12 },
// shift F1-F12
{ "\033[1;2P", QFK_F13 },
{ "\033[1;2Q", QFK_F14 },
{ "\033[1;2R", QFK_F15 },
{ "\033[1;2S", QFK_F16 },
{ "\033[15;2~", QFK_F17 },
{ "\033[17;2~", QFK_F18 },
{ "\033[18;2~", QFK_F19 },
{ "\033[19;2~", QFK_F20 },
{ "\033[20;2~", QFK_F21 },
{ "\033[21;2~", QFK_F22 },
{ "\033[23;2~", QFK_F23 },
{ "\033[24;2~", QFK_F24 },
// control F1-F12
{ "\033[1;5P", QFK_F25 },
{ "\033[1;5Q", QFK_F26 },
{ "\033[1;5R", QFK_F27 },
{ "\033[1;5S", QFK_F28 },
{ "\033[15;5~", QFK_F29 },
{ "\033[17;5~", QFK_F30 },
{ "\033[18;5~", QFK_F31 },
{ "\033[19;5~", QFK_F32 },
{ "\033[20;5~", QFK_F33 },
{ "\033[21;5~", QFK_F34 },
{ "\033[23;5~", QFK_F35 },
{ "\033[24;5~", QFK_F36 },
// shift control F1-F12
{ "\033[1;6P", QFK_F37 },
{ "\033[1;6Q", QFK_F38 },
{ "\033[1;6R", QFK_F39 },
{ "\033[1;6S", QFK_F40 },
{ "\033[15;6~", QFK_F41 },
{ "\033[17;6~", QFK_F42 },
{ "\033[18;6~", QFK_F43 },
{ "\033[19;6~", QFK_F44 },
{ "\033[20;6~", QFK_F45 },
{ "\033[21;6~", QFK_F46 },
{ "\033[23;6~", QFK_F47 },
{ "\033[24;6~", QFK_F48 },
{ "\033[2~", QFK_INSERT, 0 },
{ "\033[3~", QFK_DELETE, 0 },
{ "\033[H", QFK_HOME, 0 },
{ "\033[F", QFK_END, 0 },
{ "\033[5~", QFK_PAGEUP, 0 },
{ "\033[6~", QFK_PAGEDOWN, 0 },
{ "\033[A", QFK_UP, 0 },
{ "\033[B", QFK_DOWN, 0 },
{ "\033[C", QFK_RIGHT, 0 },
{ "\033[D", QFK_LEFT, 0 },
// shift
// there may be a setting to tell xterm to NOT act on shift ins/pgup/pgdn
{ "\033[2;2~", QFK_INSERT, 1 }, // xterm gobbles (and pastes)
{ "\033[3;2~", QFK_DELETE, 1 },
{ "\033[1;2H", QFK_HOME, 1 },
{ "\033[1;2F", QFK_END, 1 },
{ "\033[5;2~", QFK_PAGEUP, 1 }, // xterm gobbles (scrolls term)
{ "\033[6;2~", QFK_PAGEDOWN, 1 }, // xterm gobbles (scrolls term)
{ "\033[1;2A", QFK_UP, 1 },
{ "\033[1;2B", QFK_DOWN, 1 },
{ "\033[1;2C", QFK_RIGHT, 1 },
{ "\033[1;2D", QFK_LEFT, 1 },
{ "\033[Z", QFK_TAB, 1 },
// control
{ "\033[2;5~", QFK_INSERT, 4 },
{ "\033[3;5~", QFK_DELETE, 4 },
{ "\033[1;5H", QFK_HOME, 4 },
{ "\033[1;5F", QFK_END, 4 },
{ "\033[5;5~", QFK_PAGEUP, 4 },
{ "\033[6;5~", QFK_PAGEDOWN, 4 },
{ "\033[1;5A", QFK_UP, 4 },
{ "\033[1;5B", QFK_DOWN, 4 },
{ "\033[1;5C", QFK_RIGHT, 4 },
{ "\033[1;5D", QFK_LEFT, 4 },
// shift control
{ "\033[2;6~", QFK_INSERT, 5 },
{ "\033[3;6~", QFK_DELETE, 5 },
{ "\033[1;6H", QFK_HOME, 5 },
{ "\033[1;6F", QFK_END, 5 },
{ "\033[5;6~", QFK_PAGEUP, 5 },
{ "\033[6;6~", QFK_PAGEDOWN, 5 },
{ "\033[1;6A", QFK_UP, 5 },
{ "\033[1;6B", QFK_DOWN, 5 },
{ "\033[1;6C", QFK_RIGHT, 5 },
{ "\033[1;6D", QFK_LEFT, 5 },
};
static struct sigaction save_winch;
static sigset_t winch_mask;
static volatile sig_atomic_t winch_arrived;
static void
handle_winch (int sig)
{
winch_arrived = 1;
}
int
qwaq_add_event (qwaq_resources_t *res, qwaq_event_t *event)
{
struct timespec timeout;
int merged = 0;
int ret = 0;
// merge motion events
pthread_mutex_lock (&res->event_cond.mut);
unsigned last = RB_DATA_AVAILABLE (res->event_queue);
if (event->what == qe_mousemove && last > 1
&& RB_PEEK_DATA(res->event_queue, last - 1).what == qe_mousemove) {
RB_POKE_DATA(res->event_queue, last - 1, *event);
merged = 1;
pthread_cond_broadcast (&res->event_cond.rcond);
}
pthread_mutex_unlock (&res->event_cond.mut);
if (merged) {
return 0;
}
pthread_mutex_lock (&res->event_cond.mut);
qwaq_init_timeout (&timeout, 5000 * 1000000L);
while (RB_SPACE_AVAILABLE (res->event_queue) < 1 && ret == 0) {
ret = pthread_cond_timedwait (&res->event_cond.wcond,
&res->event_cond.mut, &timeout);
}
RB_WRITE_DATA (res->event_queue, event, 1);
pthread_cond_broadcast (&res->event_cond.rcond);
pthread_mutex_unlock (&res->event_cond.mut);
return ret;
}
static void
resize_event (qwaq_resources_t *res)
{
qwaq_event_t event = {};
struct winsize size;
if (ioctl (fileno (stdout), TIOCGWINSZ, &size) != 0) {
return;
}
event.what = qe_resize;
event.resize.width = size.ws_col;
event.resize.height = size.ws_row;
qwaq_add_event (res, &event);
}
static void
key_event (qwaq_resources_t *res, int key, unsigned shift)
{
qwaq_event_t event = {};
event.what = qe_keydown;
event.when = Sys_DoubleTime ();
event.key.code = key;
event.key.shift = shift;
qwaq_add_event (res, &event);
}
static void
mouse_event (qwaq_resources_t *res, int what, int x, int y)
{
qwaq_event_t event = {};
event.what = what;
event.when = Sys_DoubleTime ();
event.mouse.x = x;
event.mouse.y = y;
event.mouse.buttons = res->button_state;
if (event.what == qe_mousedown) {
if (event.when - res->lastClick.when <= 0.4
&& res->lastClick.mouse.buttons == event.mouse.buttons
&& res->lastClick.mouse.click < 3) {
event.mouse.click = res->lastClick.mouse.click + 1;
event.what = qe_mouseclick;
}
res->lastClick = event;
} else if (event.what == qe_mouseclick) {
// scroll button event, so always single click
event.mouse.click = 1;
}
qwaq_add_event (res, &event);
}
static void
parse_mouse (qwaq_resources_t *res, unsigned ctrl, int x, int y, int cmd)
{
int button = ctrl & 3;
int ext = (ctrl >> 6);
int what = qe_none;
if (ctrl & 0x20) {
// motion-only event
button = -1;
what = qe_mousemove;
} else {
// xterm doesn't send release events for buttons 4-7
res->button_state &= ~(0x78);
if (!ext && button == 3 && !cmd) {
res->button_state = 0; // unknown which button was released
button = -1;
what = qe_mouseup;
} else {
if (ext) {
button += 4 * ext - 1;
}
if (cmd == 'm') {
res->button_state &= ~(1 << button);
what = qe_mouseup;
} else {
res->button_state |= 1 << button;
if (res->button_state & WHEEL_BUTTONS) {
what = qe_mouseclick;
} else {
what = qe_mousedown;
}
}
}
}
res->mouse_x = x;
res->mouse_y = y;
mouse_event (res, what, x, y);
}
static void
parse_x10 (qwaq_resources_t *res)
{
int x = (byte) res->escbuff.str[1] - '!'; // want 0-based
int y = (byte) res->escbuff.str[2] - '!'; // want 0-based
unsigned ctrl = (byte) res->escbuff.str[0] - ' ';
parse_mouse (res, ctrl, x, y, 0);
}
static void
parse_sgr (qwaq_resources_t *res, char cmd)
{
unsigned ctrl, x, y;
sscanf (res->escbuff.str, "%u;%u;%u", &ctrl, &x, &y);
// mouse coords are 1-based, but want 0-based
parse_mouse (res, ctrl, x - 1, y - 1, cmd);
}
static void
parse_key (qwaq_resources_t *res)
{
qwaq_key_t *key = Hash_Find (res->key_sequences, res->escbuff.str);
if (key) {
key_event (res, key->key, key->shift);
}
}
static void process_char (qwaq_resources_t *res, char ch)
{
if (ch == 0x1b) {
// always reset if escape is seen, may be a desync
res->escstate = esc_escape;
} else {
switch (res->escstate) {
case esc_ground:
key_event (res, (byte) ch, 0); // shift state unknown
break;
case esc_escape:
if (ch == '[') {
res->escstate = esc_csi;
} else if (ch == 'O') {
// will wind up accepting P;P... but meh
res->escstate = esc_key;
dstring_clear (&res->escbuff);
// start the buffer with what got us hear: eases key lookup
dstring_append (&res->escbuff, "\033O", 2);
} else {
res->escstate = esc_ground;
}
break;
case esc_csi:
if (ch == 'M') {
res->escstate = esc_mouse;
dstring_clear (&res->escbuff);
} else if (ch == '<') {
res->escstate = esc_sgr;
dstring_clear (&res->escbuff);
} else if (ch >= '0' && ch < 127) {
res->escstate = esc_key;
dstring_clear (&res->escbuff);
// start the buffer with what got us hear: eases key lookup
dstring_append (&res->escbuff, "\033[", 2);
// the csi code might be short (eg, \e[H for home) so
// need to check for end of string right away
goto esc_key_jump;
} else {
res->escstate = esc_ground;
}
break;
case esc_mouse:
dstring_append (&res->escbuff, &ch, 1);
if (res->escbuff.size == 3) {
parse_x10 (res);
res->escstate = esc_ground;
}
break;
case esc_sgr:
if (isdigit (ch) || ch == ';') {
dstring_append (&res->escbuff, &ch, 1);
} else {
if (ch == 'm' || ch == 'M') {
// terminate the string
dstring_append (&res->escbuff, "", 1);
parse_sgr (res, ch);
}
res->escstate = esc_ground;
}
break;
case esc_key:
esc_key_jump:
dstring_append (&res->escbuff, &ch, 1);
if (!isdigit (ch) && ch != ';') {
// terminate the string
dstring_append (&res->escbuff, "", 1);
// parse_key will sort out whether it was a valid sequence
parse_key (res);
res->escstate = esc_ground;
}
break;
}
//printf("res->escstate %d\n", res->escstate);
}
}
static const char *
key_sequence_getkey (const void *_seq, void *unused)
{
__auto_type seq = (const qwaq_key_t *) _seq;
return seq->sequence;
}
void qwaq_input_init (qwaq_resources_t *res)
{
if (res->key_sequences) {
Hash_FlushTable (res->key_sequences);
} else {
res->key_sequences = Hash_NewTable (127, key_sequence_getkey, 0, 0);
}
for (size_t i = 0; i < sizeof (default_keys) / sizeof (default_keys[0]);
i++) {
Hash_Add (res->key_sequences, &default_keys[i]);
}
sigemptyset (&winch_mask);
sigaddset (&winch_mask, SIGWINCH);
struct sigaction action = {};
action.sa_handler = handle_winch;
sigaction (SIGWINCH, &action, &save_winch);
// ncurses takes care of input mode for us, so need only tell xterm
// what we need
write(1, MOUSE_MOVES_ON, sizeof (MOUSE_MOVES_ON) - 1);
write(1, SGR_ON, sizeof (SGR_ON) - 1);
}
void qwaq_input_shutdown (qwaq_resources_t *res)
{
// ncurses takes care of input mode for us, so need only tell xterm
// what we need
write(1, SGR_OFF, sizeof (SGR_OFF) - 1);
write(1, MOUSE_MOVES_OFF, sizeof (MOUSE_MOVES_OFF) - 1);
sigaction (SIGWINCH, &save_winch, 0);
}
void qwaq_process_input (qwaq_resources_t *res)
{
char buf[256];
int len;
sigset_t save_set;
int saw_winch;
pthread_sigmask (SIG_BLOCK, &winch_mask, &save_set);
saw_winch = winch_arrived;
winch_arrived = 0;
pthread_sigmask (SIG_SETMASK, &save_set, 0);
if (saw_winch) {
resize_event (res);
}
while (Sys_CheckInput (1, -1)) {
len = read(0, buf, sizeof (buf));
for (int i = 0; i < len; i++) {
process_char (res, buf[i]);
}
}
}