gambas-source-code/gb.ncurses/src/c_input.c
Tobias Boege 0a4dd6f9d2 [GB.NCURSES]
* NEW: Add ReadLine() function to Window that does the same as ncurses
  getstr(3X) but usable in all input modes
* NEW: Encapsulate Input, Cursor and Border modes into static objects
* NEW: Tidy up Color class in various places
* NEW: Specifiying color values is now done via floats instead of ints
* NEW: Remove ContainerW and ContainerH (and corresponding long forms)
  properties from Window class. It is always .Width/.Height + 2
* NEW: Rename Input.Repeater to Input.RepeatDelay
* OPT: Remove unnecessary ncurses input mode changes
* OPT: Tidy up Input and Screen code
* OPT: Reduce useless calls to input queue read callback in Screen class
* BUG: Add constant for already implemented "very visible" cursor mode
* BUG: Use opaque input module in Window.Ask()
* BUG: Note that NoDelay and Window/Screen events still not work



git-svn-id: svn://localhost/gambas/trunk@4827 867c0c6c-44f3-4631-809d-bfa615b0a4ec
2012-06-13 15:47:00 +00:00

700 lines
15 KiB
C

/*
* c_input.c - gb.ncurses opaque input routines
*
* Copyright (C) 2012 Tobias Boege <tobias@gambas-buch.de>
*
* 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, 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 the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#define __C_INPUT_C
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <linux/kd.h>
#include <time.h>
#include <sys/time.h>
#include <errno.h>
#include <ncurses.h>
#include "gambas.h"
#include "gb_common.h"
#include "main.h"
#include "c_input.h"
#include "c_window.h"
#define E_UNSUPP "Unsupported value"
#define E_NO_NODELAY "Could not initialise NoDelay mode"
/* True input mode is inherited from terminal, we need a surely invalid
* value to be reset upon initialisation */
static int _input = -1;
#define IN_NODELAY (_input == INPUT_NODELAY)
static char _watch_fd = -1;
/**
* Input initialisation
*/
int INPUT_init()
{
INPUT_mode(INPUT_CBREAK);
INPUT_watch(0);
NODELAY_repeater_delay(1);
return 0;
}
/**
* Input cleanup
*/
void INPUT_exit()
{
/* If we are still in NoDelay mode, exit it */
if (IN_NODELAY) {
INPUT_mode(INPUT_CBREAK);
}
}
#define MY_DEBUG() fprintf(stderr, "in %s\n", __func__)
/**
* Begin watching the given fd (for read)
* @fd: fd to watch. -1 means to stop watching at all.
* This automatically stops watching the previously watched fd, if any
*/
static int INPUT_watch(int fd)
{
MY_DEBUG();
if (fd == _watch_fd)
return 0;
if (_watch_fd != -1)
GB.Watch(_watch_fd, GB_WATCH_NONE, NULL, 0);
if (fd == -1)
return 0;
GB.Watch(fd, GB_WATCH_READ, INPUT_callback, 0);
_watch_fd = fd;
return 0;
}
/**
* Function to be called by Gambas when data arrives
* Params currently not used
*/
static void INPUT_callback(int fd, int flag, intptr_t arg)
{
MY_DEBUG();
/* if (IN_NODELAY)
NODELAY_change_pressed(NODELAY_get(-1));
else
*/ WINDOW_raise_read(NULL);
}
/**
* Return or set the current input mode
* @mode: one of INPUT_* enums
*/
int INPUT_mode(int mode)
{
if (mode == INPUT_RETURN)
return _input;
if (mode == _input)
return 0;
if (IN_NODELAY)
NODELAY_exit();
switch (mode) {
case INPUT_COOKED:
nocbreak();
break;
case INPUT_CBREAK:
cbreak();
break;
case INPUT_RAW:
raw();
break;
case INPUT_NODELAY:
if (NODELAY_init() == -1) {
GB.Error(E_NO_NODELAY);
/* We return 0 to not override the previous
* error message with the one emitted by the
* caller if we return error */
return 0;
}
break;
default:
GB.Error(E_UNSUPP);
return -1;
}
_input = mode;
return 0;
}
/**
* Drain the input queue
*/
void INPUT_drain()
{
if (IN_NODELAY)
NODELAY_drain();
else
flushinp();
}
/**
* Retrieve input from ncurses
*/
static int INPUT_get_ncurses(int timeout)
{
int ret;
if (timeout >= 0)
timeout(timeout);
ret = getch();
if (ret == ERR) {
/* Had a timeout, the manual doesn't define any errors to
happen for wgetch() besides NULL pointer arguments. The
only source of ERR is timeout expired. */
if (timeout >= 0)
ret = 0;
}
if (timeout >= 0)
timeout(-1);
return ret;
}
/**
* Get a keypress within the given timeout
* @timeout: number of milliseconds to wait. If no key is pressed during it,
* 0 will be returned.
*/
int INPUT_get(int timeout)
{
if (IN_NODELAY)
return NODELAY_get(timeout);
else
return INPUT_get_ncurses(timeout);
}
BEGIN_PROPERTY(Input_IsConsole)
int fd = NODELAY_consolefd();
if (fd == -1) {
GB.ReturnBoolean(FALSE);
return;
}
close(fd);
GB.ReturnBoolean(TRUE);
END_PROPERTY
BEGIN_PROPERTY(Input_RepeatDelay)
if (READ_PROPERTY) {
GB.ReturnInteger(NODELAY_repeater_delay(REPEATER_RETURN));
return;
}
if (NODELAY_repeater_delay(VPROP(GB_INTEGER)) == -1) {
GB.Error("Invalid value");
return;
}
END_PROPERTY
GB_DESC CInputDesc[] = {
GB_DECLARE("Input", 0),
GB_NOT_CREATABLE(),
GB_CONSTANT("NoTimeout", "i", TIMEOUT_NOTIMEOUT),
GB_CONSTANT("Cooked", "i", INPUT_COOKED),
GB_CONSTANT("CBreak", "i", INPUT_CBREAK),
GB_CONSTANT("Raw", "i", INPUT_RAW),
GB_CONSTANT("NoDelay", "i", INPUT_NODELAY),
GB_STATIC_PROPERTY_READ("IsConsole", "b", Input_IsConsole),
GB_STATIC_PROPERTY("RepeatDelay", "i", Input_RepeatDelay),
};
/*
* NODELAY routines
*/
static struct {
struct {
struct termios term;
int kbmode;
void (*error_hook)();
} old;
int fd;
unsigned short pressed;
unsigned int delay;
GB_TIMER *timer;
} no_delay;
static char _exiting_nodelay = 0;
/**
* Init NoDelay mode
* We save old settings and prepare the TTY driver and Gambas
* Precisely:
* - (TTY driver:)
* - Save all related values
* - Set terminal to (ncurses) raw()-like input mode as base for NoDelay
* - Set keyboard to K_MEDIUMRAW mode to see key make and break codes
* - (Gambas:)
* - Install specific error hook
* - Reset repeater timer
* - Begin watching console fd
*/
static int NODELAY_init()
{
int fd = NODELAY_consolefd();
struct termios term;
if (fd == -1)
return -1;
/* TODO: implement switching between vts, need available signals to
* be sent */
tcgetattr(fd, &no_delay.old.term);
ioctl(fd, KDGKBMODE, &no_delay.old.kbmode);
memcpy(&term, &no_delay.old.term, sizeof(term));
term.c_lflag &= ~(ICANON | ECHO | ISIG);
term.c_iflag &= ~(ISTRIP | IGNCR | ICRNL | INLCR | IXOFF | IXON);
/* Have no timeout per default */
term.c_cc[VMIN] = 0;
term.c_cc[VTIME] = TIMEOUT_NOTIMEOUT;
tcsetattr(fd, TCSAFLUSH, &term);
no_delay.old.error_hook = GB.Hook(GB_HOOK_ERROR,
NODELAY_error_hook);
no_delay.fd = fd;
no_delay.timer = NULL;
ioctl(no_delay.fd, KDSKBMODE, K_MEDIUMRAW);
INPUT_watch(fd);
return 0;
}
/**
* Cleanup NoDelay mode
* Restore old settings, see NODELAY_init() for details
* This function gets called by our error hook and the error hook is removed
* here. When removing a hook, it gets automatically called: Hence
* @_exiting_nodelay
*/
static int NODELAY_exit()
{
if (_exiting_nodelay)
return 0;
_exiting_nodelay = 1;
INPUT_watch(0);
ioctl(no_delay.fd, KDSKBMODE, no_delay.old.kbmode);
if (no_delay.timer)
GB.Unref((void **) &no_delay.timer);
GB.Hook(GB_HOOK_ERROR, no_delay.old.error_hook);
tcsetattr(no_delay.fd, TCSANOW, &no_delay.old.term);
close(no_delay.fd);
_exiting_nodelay = 0;
return 0;
}
/**
* The NoDelay mode error hook
* This calls the former error hook, saved by NODELAY_init() to not
* disturb any piece code
*/
static void NODELAY_error_hook()
{
if (_exiting_nodelay)
return;
NODELAY_exit();
no_delay.old.error_hook();
}
/**
* Return if the given fd can be used with console_ioctls
* @fd: file descriptor to test
* The idea was derived from "kbd" package, getfd.c, is_a_console()
*/
static inline char NODELAY_is_cons(int fd)
{
char type;
if (fd != -1 && isatty(fd) && ioctl(fd, KDGKBTYPE, &type) != -1
&& (type == KB_101 || type == KB_84))
return 1;
return 0;
}
/**
* Returns an fd that can be used with console_ioctls or -1 if none
* available
*/
static int NODELAY_consolefd()
{
int fd;
if (NODELAY_is_cons(0))
return 0;
fd = open("/dev/tty", O_RDWR);
if (fd == -1)
return -1;
if (NODELAY_is_cons(fd))
return fd;
close(fd);
return -1;
}
/**
* Drain NoDelay input queue
*/
static inline void NODELAY_drain()
{
tcflush(no_delay.fd, TCIFLUSH);
}
/**
* Return or set the repeater delay
* @val: value to set the delay to. This value must be at least 1 or an
* error is returned. If it is REPEATER_RETURN, the current value is
* returned to the caller.
* Note that this setting affects the repeater function itself, that gets
* called in this interval to generate events and the INPUT_get_nodelay()
* function which will wait to return the amount of milliseconds if it is to
* return the pressed key.
*/
static int NODELAY_repeater_delay(int val)
{
if (val == REPEATER_RETURN)
return no_delay.delay;
if (val < 1)
return -1;
no_delay.delay = (unsigned int) val;
return 0;
}
/**
* Post callback to insert new read event during next event loop.
* This is important because the NODELAY_repeater() gets called by the
* timer. If we raise an event from there, the event handler may destroy the
* timer we are currently in by issuing a NODELAY_change_pressed(). This has
* consequently be done outside the timer tick.
* @arg: unused
*/
static void NODELAY_post_read(intptr_t arg)
{
WINDOW_raise_read(NULL);
}
/**
* NoDelay mode event repeater. This function is the timer callback
* Used to insert Window_Read events if there is a key pressed
*/
static int NODELAY_repeater()
{
MY_DEBUG();
if (!no_delay.pressed)
return TRUE;
GB.Post(NODELAY_post_read, 0);
return FALSE;
}
/*
* Return codes from NODELAY_trans_keycode()
*/
enum {
TRANS_NEED_MORE = -1,
TRANS_KEY_MIN
};
/*
* States of the modifier keys that we recognise
*/
enum {
MOD_NONE = 0,
MOD_SHIFT = 1,
MOD_CTRL = 2,
MOD_ALT = 4
};
#define IS_BREAK(k) ((k) & 0x80)
/**
* Translate a keycode (or a sequence) to an ncurses compatible int
* @kc: keycode to translate
* This function returns one of the above codes. TRANS_NEED_MORE means that
* the given @kc is considered part of a multi-keycode sequence (or is a
* Shift, Control, Alt), so we need more keys which are (in first case
* likely) available without waiting from the tty driver then; in the latter
* case (modifier keys), it does not count as a keypress anyway.
* Note that after a usual tty key sequence is assembled it is passed to the
* driver to try to get an escape sequence for it. If there is no escape
* seqeuence for that key, we can simply return the plain value. Otherwise
* we exploit ncurses key_defined() routine to translate the sequence to an
* int for us. This gives an int like ncurses getch() would do.
*/
static int NODELAY_trans_keycode(unsigned char kc)
{
/* Pause/Break has the largest scancode: e1 1d 45 e1 9d c5 */
static unsigned char codes[8];
static int num = 0;
static int modifiers = MOD_NONE;
struct kbentry kbe;
struct kbsentry kbs;
register int mod;
MY_DEBUG();
#define KEYCODE_LCTRL 0x1d
#define KEYCODE_RCTRL 0x61
#define KEYCODE_ALT 0x38
#define KEYCODE_LSHIFT 0x2a
#define KEYCODE_RSHIFT 0x36
/* Modifiers */
switch (kc) {
case KEYCODE_LCTRL:
case KEYCODE_RCTRL:
mod = MOD_CTRL;
goto apply_mod;
case KEYCODE_ALT:
mod = MOD_ALT;
goto apply_mod;
case KEYCODE_LSHIFT:
case KEYCODE_RSHIFT:
mod = MOD_SHIFT;
goto apply_mod;
default:
goto account_key;
}
apply_mod:
if (IS_BREAK(kc))
modifiers &= ~mod;
else
modifiers |= mod;
return TRANS_NEED_MORE;
account_key:
codes[num++] = kc;
/* Break key, sends make and break code together */
if (codes[0] == '\xe1') {
if (num == 6)
return KEY_BREAK;
else
return TRANS_NEED_MORE;
}
/* Keys with two keycodes, no matter, those correspond to
* single-keycode keys, we can safely use @kc != 0xe0 */
if (codes[0] == '\xe0' && num != 2)
return TRANS_NEED_MORE;
/* TODO: what to do with ctrl- ? */
/* Set table and get action code */
if (modifiers & MOD_ALT) {
if (modifiers & MOD_SHIFT)
kbe.kb_table = K_ALTSHIFTTAB;
else
kbe.kb_table = K_ALTTAB;
} else if (modifiers & MOD_SHIFT) {
kbe.kb_table = K_SHIFTTAB;
} else {
kbe.kb_table = K_NORMTAB;
}
kbe.kb_index = kc & 0x7f;
kbe.kb_value = 0;
ioctl(no_delay.fd, KDGKBENT, &kbe);
/* Has an escape sequence? */
kbs.kb_func = (unsigned char) kbe.kb_value;
ioctl(no_delay.fd, KDGKBSENT, &kbs);
if (kbs.kb_string[0])
return key_defined((char *) kbs.kb_string);
else
return (int) ((unsigned char) kbe.kb_value);
}
/**
* Change the currently pressed key
* @key: current key. 0 means that no key is pressed
* This installs the repeater
*/
static void NODELAY_change_pressed(int key)
{
MY_DEBUG();
/* if (key == no_delay.pressed)
return;
if (key == 0) {
GB.Unref((void **) no_delay.timer);
} else {
no_delay.timer = GB.Every(no_delay.delay,
(GB_TIMER_CALLBACK) NODELAY_repeater, 0);
}
*/ no_delay.pressed = key;
//NODELAY_repeater();
WINDOW_raise_read(NULL);
}
/**
* Retrieve input in NoDelay mode, using console_ioctl(4).
* If there is a key pressed and no input available on the input queue,
* the pressed key is returned.
* @timeout: timeout in milliseconds. If that timeout expires, we return 0.
* Note that @timeout can only be in the scope of deciseconds and is
* silently cut down to those.
* The Repeater Delay applies to this function, too, in that we only return
* a pressed key when we waited for that delay.
* Note carefully, that there is an emergency exit: pressing ESC thrice
* during one second will immediately abort NoDelay mode and enter CBreak.
*/
static int NODELAY_get(int timeout)
{
static char esc = 0;
struct termios old, new;
unsigned char b;
static time_t stamp;
int ret, res, num;
int key; /* This will already be ncurses compatible */
struct timeval tv1, tv2;
MY_DEBUG();
if (timeout > -1) {
gettimeofday(&tv1, NULL);
tcgetattr(no_delay.fd, &old);
memcpy(&new, &old, sizeof(new));
}
recalc_timeout:
#define USEC2MSEC(us) (us / 1000)
#define MSEC2USEC(ms) (ms * 1000)
#define MSEC2DSEC(ms) (ms / 100)
#define DSEC2MSEC(ds) (ds * 100)
#define SEC2MSEC(s) (s * 1000)
#define MSEC2SEC(ms) (ms / 1000)
if (timeout > -1) {
gettimeofday(&tv2, NULL);
timeout -= USEC2MSEC(tv2.tv_usec - tv1.tv_usec);
timeout -= SEC2MSEC(tv2.tv_sec - tv1.tv_sec);
if (timeout < 0) {
ret = 0;
goto cleanup;
}
}
/* Set timeout */
ioctl(no_delay.fd, TIOCINQ, &num);
/* We don't need to set any timeout if there are bytes available */
if (!num && timeout > -1) {
new.c_cc[VTIME] = MSEC2DSEC(timeout);
tcsetattr(no_delay.fd, TCSANOW, &new);
gettimeofday(&tv1, NULL);
}
/* Begin reading */
if (no_delay.pressed && !num) {
usleep(MSEC2USEC(no_delay.delay));
ret = no_delay.pressed;
goto cleanup;
}
/* Try to stick to the user-supplied timeout */
ioctl(no_delay.fd, TIOCINQ, &num);
if ((res = read(no_delay.fd, &b, 1)) == -1 && errno == EINTR) {
goto recalc_timeout;
} else if (res == 0) { /* Timeout expired */
ret = 0;
goto cleanup;
} else { /* Got a key */
/* Emergency exit from NoDelay mode */
#define KEYCODE_ESC 0x01
if (b == KEYCODE_ESC) {
if (time(NULL) - stamp > 0)
esc = 0;
if (++esc == 3) {
NODELAY_exit();
INPUT_mode(INPUT_CBREAK);
return 0;
}
stamp = time(NULL);
}
/* We use ncurses keys for operations on our no_delay data */
if ((key = NODELAY_trans_keycode(b)) == TRANS_NEED_MORE)
goto recalc_timeout;
/* Ignore break codes, except when it is the currently
pressed key */
if (IS_BREAK(b)) {
if (no_delay.pressed == key)
// NODELAY_change_pressed(0);
/* Key release isn't visible to the gambas programmer
* and thus not really an event to gb.ncurses. If
* time is left, we try again reading another key */
goto recalc_timeout;
} else {
// NODELAY_change_pressed(key);
}
}
ret = key;
cleanup:
if (timeout > -1)
tcsetattr(no_delay.fd, TCSANOW, &old);
return ret;
}