You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2892 lines
90 KiB
C
2892 lines
90 KiB
C
#include <stdio.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#include "automaton.h"
|
|
#include "internal.h"
|
|
#include "unixsig.h"
|
|
#include "render.h"
|
|
#include "in.h"
|
|
|
|
// Notcurses takes over stdin, and if it is not connected to a terminal, also
|
|
// tries to make a connection to the controlling terminal. If such a connection
|
|
// is made, it will read from that source (in addition to stdin). We dump one or
|
|
// both into distinct buffers. We then try to lex structured elements out of
|
|
// the buffer(s). We can extract cursor location reports, mouse events, and
|
|
// UTF-8 characters. Completely extracted ones are placed in their appropriate
|
|
// queues, and removed from the depository buffer. We aim to consume the
|
|
// entirety of the deposit before going back to read more data, but let anyone
|
|
// blocking on data wake up as soon as we've processed any input.
|
|
//
|
|
// The primary goal is to react to terminal messages (mostly cursor location
|
|
// reports) as quickly as possible, and definitely not with unbounded latency,
|
|
// without unbounded allocation, and also without losing data. We'd furthermore
|
|
// like to reliably differentiate escapes and regular input, even when that
|
|
// latter contains escapes. Unbounded input will hopefully only be present when
|
|
// redirected from a file.
|
|
|
|
static sig_atomic_t cont_seen;
|
|
static sig_atomic_t resize_seen;
|
|
|
|
// called for SIGWINCH and SIGCONT, and causes block_on_input to return
|
|
void sigwinch_handler(int signo){
|
|
if(signo == SIGWINCH){
|
|
resize_seen = signo;
|
|
sigcont_seen_for_render = 1;
|
|
}else if(signo == SIGCONT){
|
|
cont_seen = signo;
|
|
sigcont_seen_for_render = 1;
|
|
}
|
|
}
|
|
|
|
typedef struct cursorloc {
|
|
int y, x; // 0-indexed cursor location
|
|
} cursorloc;
|
|
|
|
#ifndef __MINGW32__
|
|
typedef int ipipe;
|
|
#else
|
|
typedef HANDLE ipipe;
|
|
#endif
|
|
|
|
// local state for the input thread. don't put this large struct on the stack.
|
|
typedef struct inputctx {
|
|
// these two are not ringbuffers; we always move any leftover materia to the
|
|
// front of the queue (it ought be a handful of bytes at most).
|
|
unsigned char tbuf[BUFSIZ]; // only used if we have distinct terminal fd
|
|
unsigned char ibuf[BUFSIZ]; // might be intermingled bulk/control data
|
|
|
|
int stdinfd; // bulk in fd. always >= 0 (almost always 0). we do not
|
|
// own this descriptor, and must not close() it.
|
|
int termfd; // terminal fd: -1 with no controlling terminal, or
|
|
// if stdin is a terminal, or on MSFT Terminal.
|
|
#ifdef __MINGW32__
|
|
HANDLE stdinhandle; // handle to input terminal for MSFT Terminal
|
|
#endif
|
|
|
|
int lmargin, tmargin; // margins in use at left and top
|
|
int rmargin, bmargin; // margins in use at right and bottom
|
|
|
|
automaton amata;
|
|
int ibufvalid; // we mustn't read() if ibufvalid == sizeof(ibuf)
|
|
int tbufvalid; // only used if we have distinct terminal connection
|
|
|
|
uint8_t backspace; // backspace is usually not an escape sequence, but
|
|
// instead ^H or ^? or something. only escape sequences
|
|
// go into our automaton, so we handle this one
|
|
// out-of-band. set to non-zero; match with ctrl.
|
|
// ringbuffers for processed, structured input
|
|
cursorloc* csrs; // cursor reports are dumped here
|
|
ncinput* inputs; // processed input is dumped here
|
|
int coutstanding; // outstanding cursor location requests
|
|
int csize, isize; // total number of slots in csrs/inputs
|
|
int cvalid, ivalid; // population count of csrs/inputs
|
|
int cwrite, iwrite; // slot where we'll write the next csr/input;
|
|
// we cannot write if valid == size
|
|
int cread, iread; // slot from which clients read the next csr/input;
|
|
// they cannot read if valid == 0
|
|
pthread_mutex_t ilock; // lock for ncinput ringbuffer, also initial state
|
|
pthread_cond_t icond; // condvar for ncinput ringbuffer
|
|
pthread_mutex_t clock; // lock for csrs ringbuffer
|
|
pthread_cond_t ccond; // condvar for csrs ringbuffer
|
|
tinfo* ti; // link back to tinfo
|
|
pthread_t tid; // tid for input thread
|
|
|
|
unsigned midescape; // we're in the middle of a potential escape. we need
|
|
// to do a nonblocking read and try to complete it.
|
|
unsigned stdineof; // have we seen an EOF on stdin?
|
|
|
|
unsigned linesigs; // are line discipline signals active?
|
|
unsigned drain; // drain away bulk input?
|
|
ncsharedstats *stats; // stats shared with notcurses context
|
|
|
|
ipipe ipipes[2];
|
|
ipipe readypipes[2]; // pipes[0]: poll()able fd indicating the presence of user input
|
|
// initially, initdata is non-NULL and initdata_complete is NULL. once we
|
|
// get DA1, initdata_complete is non-NULL (it is the same value as
|
|
// initdata). once we complete reading the input payload that the DA1 arrived
|
|
// in, initdata is set to NULL, and we broadcast availability. once it has
|
|
// been taken, both become NULL.
|
|
struct initial_responses* initdata;
|
|
struct initial_responses* initdata_complete;
|
|
int kittykbd; // kitty keyboard protocol support level
|
|
bool failed; // error initializing input automaton, abort
|
|
} inputctx;
|
|
|
|
static inline void
|
|
inc_input_events(inputctx* ictx){
|
|
pthread_mutex_lock(&ictx->stats->lock);
|
|
++ictx->stats->s.input_events;
|
|
pthread_mutex_unlock(&ictx->stats->lock);
|
|
}
|
|
|
|
static inline void
|
|
inc_input_errors(inputctx* ictx){
|
|
pthread_mutex_lock(&ictx->stats->lock);
|
|
++ictx->stats->s.input_errors;
|
|
pthread_mutex_unlock(&ictx->stats->lock);
|
|
}
|
|
|
|
// load representations used by XTMODKEYS
|
|
static int
|
|
prep_xtmodkeys(inputctx* ictx){
|
|
// XTMODKEYS enables unambiguous representations of certain inputs. We
|
|
// enable XTMODKEYS where supported.
|
|
static const struct {
|
|
const char* esc;
|
|
uint32_t key;
|
|
unsigned modifiers;
|
|
} keys[] = {
|
|
{ .esc = "\x1b\x8", .key = NCKEY_BACKSPACE,
|
|
.modifiers = NCKEY_MOD_ALT, },
|
|
{ .esc = "\x1b[2P", .key = NCKEY_F01,
|
|
.modifiers = NCKEY_MOD_SHIFT, },
|
|
{ .esc = "\x1b[5P", .key = NCKEY_F01,
|
|
.modifiers = NCKEY_MOD_CTRL, },
|
|
{ .esc = "\x1b[6P", .key = NCKEY_F01,
|
|
.modifiers = NCKEY_MOD_CTRL | NCKEY_MOD_SHIFT, },
|
|
{ .esc = "\x1b[2Q", .key = NCKEY_F02,
|
|
.modifiers = NCKEY_MOD_SHIFT, },
|
|
{ .esc = "\x1b[5Q", .key = NCKEY_F02,
|
|
.modifiers = NCKEY_MOD_CTRL, },
|
|
{ .esc = "\x1b[6Q", .key = NCKEY_F02,
|
|
.modifiers = NCKEY_MOD_CTRL | NCKEY_MOD_SHIFT, },
|
|
{ .esc = "\x1b[2R", .key = NCKEY_F03,
|
|
.modifiers = NCKEY_MOD_SHIFT, },
|
|
{ .esc = "\x1b[5R", .key = NCKEY_F03,
|
|
.modifiers = NCKEY_MOD_CTRL, },
|
|
{ .esc = "\x1b[6R", .key = NCKEY_F03,
|
|
.modifiers = NCKEY_MOD_CTRL | NCKEY_MOD_SHIFT, },
|
|
{ .esc = "\x1b[2S", .key = NCKEY_F04,
|
|
.modifiers = NCKEY_MOD_SHIFT, },
|
|
{ .esc = "\x1b[5S", .key = NCKEY_F04,
|
|
.modifiers = NCKEY_MOD_CTRL, },
|
|
{ .esc = "\x1b[6S", .key = NCKEY_F04,
|
|
.modifiers = NCKEY_MOD_CTRL | NCKEY_MOD_SHIFT, },
|
|
{ .esc = NULL, .key = 0, },
|
|
}, *k;
|
|
for(k = keys ; k->esc ; ++k){
|
|
if(inputctx_add_input_escape(&ictx->amata, k->esc, k->key,
|
|
k->modifiers)){
|
|
return -1;
|
|
}
|
|
logdebug("added %u", k->key);
|
|
}
|
|
loginfo("added all xtmodkeys");
|
|
return 0;
|
|
}
|
|
|
|
// load all known special keys from terminfo, and build the input sequence trie
|
|
static int
|
|
prep_special_keys(inputctx* ictx){
|
|
#ifndef __MINGW32__
|
|
static const struct {
|
|
const char* tinfo;
|
|
uint32_t key;
|
|
bool shift, ctrl, alt;
|
|
} keys[] = {
|
|
// backspace (kbs) is handled seperately at the end
|
|
{ .tinfo = "kbeg", .key = NCKEY_BEGIN, },
|
|
{ .tinfo = "kcbt", .key = '\t', .shift = true, }, // "back-tab"
|
|
{ .tinfo = "kcub1", .key = NCKEY_LEFT, },
|
|
{ .tinfo = "kcuf1", .key = NCKEY_RIGHT, },
|
|
{ .tinfo = "kcuu1", .key = NCKEY_UP, },
|
|
{ .tinfo = "kcud1", .key = NCKEY_DOWN, },
|
|
{ .tinfo = "kdch1", .key = NCKEY_DEL, },
|
|
{ .tinfo = "kbs", .key = NCKEY_BACKSPACE, },
|
|
{ .tinfo = "kich1", .key = NCKEY_INS, },
|
|
{ .tinfo = "kend", .key = NCKEY_END, },
|
|
{ .tinfo = "khome", .key = NCKEY_HOME, },
|
|
{ .tinfo = "knp", .key = NCKEY_PGDOWN, },
|
|
{ .tinfo = "kpp", .key = NCKEY_PGUP, },
|
|
{ .tinfo = "kf0", .key = NCKEY_F01, },
|
|
{ .tinfo = "kf1", .key = NCKEY_F01, },
|
|
{ .tinfo = "kf2", .key = NCKEY_F02, },
|
|
{ .tinfo = "kf3", .key = NCKEY_F03, },
|
|
{ .tinfo = "kf4", .key = NCKEY_F04, },
|
|
{ .tinfo = "kf5", .key = NCKEY_F05, },
|
|
{ .tinfo = "kf6", .key = NCKEY_F06, },
|
|
{ .tinfo = "kf7", .key = NCKEY_F07, },
|
|
{ .tinfo = "kf8", .key = NCKEY_F08, },
|
|
{ .tinfo = "kf9", .key = NCKEY_F09, },
|
|
{ .tinfo = "kf10", .key = NCKEY_F10, },
|
|
{ .tinfo = "kf11", .key = NCKEY_F11, },
|
|
{ .tinfo = "kf12", .key = NCKEY_F12, },
|
|
{ .tinfo = "kf13", .key = NCKEY_F13, },
|
|
{ .tinfo = "kf14", .key = NCKEY_F14, },
|
|
{ .tinfo = "kf15", .key = NCKEY_F15, },
|
|
{ .tinfo = "kf16", .key = NCKEY_F16, },
|
|
{ .tinfo = "kf17", .key = NCKEY_F17, },
|
|
{ .tinfo = "kf18", .key = NCKEY_F18, },
|
|
{ .tinfo = "kf19", .key = NCKEY_F19, },
|
|
{ .tinfo = "kf20", .key = NCKEY_F20, },
|
|
{ .tinfo = "kf21", .key = NCKEY_F21, },
|
|
{ .tinfo = "kf22", .key = NCKEY_F22, },
|
|
{ .tinfo = "kf23", .key = NCKEY_F23, },
|
|
{ .tinfo = "kf24", .key = NCKEY_F24, },
|
|
{ .tinfo = "kf25", .key = NCKEY_F25, },
|
|
{ .tinfo = "kf26", .key = NCKEY_F26, },
|
|
{ .tinfo = "kf27", .key = NCKEY_F27, },
|
|
{ .tinfo = "kf28", .key = NCKEY_F28, },
|
|
{ .tinfo = "kf29", .key = NCKEY_F29, },
|
|
{ .tinfo = "kf30", .key = NCKEY_F30, },
|
|
{ .tinfo = "kf31", .key = NCKEY_F31, },
|
|
{ .tinfo = "kf32", .key = NCKEY_F32, },
|
|
{ .tinfo = "kf33", .key = NCKEY_F33, },
|
|
{ .tinfo = "kf34", .key = NCKEY_F34, },
|
|
{ .tinfo = "kf35", .key = NCKEY_F35, },
|
|
{ .tinfo = "kf36", .key = NCKEY_F36, },
|
|
{ .tinfo = "kf37", .key = NCKEY_F37, },
|
|
{ .tinfo = "kf38", .key = NCKEY_F38, },
|
|
{ .tinfo = "kf39", .key = NCKEY_F39, },
|
|
{ .tinfo = "kf40", .key = NCKEY_F40, },
|
|
{ .tinfo = "kf41", .key = NCKEY_F41, },
|
|
{ .tinfo = "kf42", .key = NCKEY_F42, },
|
|
{ .tinfo = "kf43", .key = NCKEY_F43, },
|
|
{ .tinfo = "kf44", .key = NCKEY_F44, },
|
|
{ .tinfo = "kf45", .key = NCKEY_F45, },
|
|
{ .tinfo = "kf46", .key = NCKEY_F46, },
|
|
{ .tinfo = "kf47", .key = NCKEY_F47, },
|
|
{ .tinfo = "kf48", .key = NCKEY_F48, },
|
|
{ .tinfo = "kf49", .key = NCKEY_F49, },
|
|
{ .tinfo = "kf50", .key = NCKEY_F50, },
|
|
{ .tinfo = "kf51", .key = NCKEY_F51, },
|
|
{ .tinfo = "kf52", .key = NCKEY_F52, },
|
|
{ .tinfo = "kf53", .key = NCKEY_F53, },
|
|
{ .tinfo = "kf54", .key = NCKEY_F54, },
|
|
{ .tinfo = "kf55", .key = NCKEY_F55, },
|
|
{ .tinfo = "kf56", .key = NCKEY_F56, },
|
|
{ .tinfo = "kf57", .key = NCKEY_F57, },
|
|
{ .tinfo = "kf58", .key = NCKEY_F58, },
|
|
{ .tinfo = "kf59", .key = NCKEY_F59, },
|
|
{ .tinfo = "kent", .key = NCKEY_ENTER, },
|
|
{ .tinfo = "kclr", .key = NCKEY_CLS, },
|
|
{ .tinfo = "kc1", .key = NCKEY_DLEFT, },
|
|
{ .tinfo = "kc3", .key = NCKEY_DRIGHT, },
|
|
{ .tinfo = "ka1", .key = NCKEY_ULEFT, },
|
|
{ .tinfo = "ka3", .key = NCKEY_URIGHT, },
|
|
{ .tinfo = "kb2", .key = NCKEY_CENTER, },
|
|
{ .tinfo = "kbeg", .key = NCKEY_BEGIN, },
|
|
{ .tinfo = "kcan", .key = NCKEY_CANCEL, },
|
|
{ .tinfo = "kclo", .key = NCKEY_CLOSE, },
|
|
{ .tinfo = "kcmd", .key = NCKEY_COMMAND, },
|
|
{ .tinfo = "kcpy", .key = NCKEY_COPY, },
|
|
{ .tinfo = "kext", .key = NCKEY_EXIT, },
|
|
{ .tinfo = "kprt", .key = NCKEY_PRINT, },
|
|
{ .tinfo = "krfr", .key = NCKEY_REFRESH, },
|
|
{ .tinfo = "kBEG", .key = NCKEY_BEGIN, .shift = 1, },
|
|
{ .tinfo = "kBEG3", .key = NCKEY_BEGIN, .alt = 1, },
|
|
{ .tinfo = "kBEG4", .key = NCKEY_BEGIN, .alt = 1, .shift = 1, },
|
|
{ .tinfo = "kBEG5", .key = NCKEY_BEGIN, .ctrl = 1, },
|
|
{ .tinfo = "kBEG6", .key = NCKEY_BEGIN, .ctrl = 1, .shift = 1, },
|
|
{ .tinfo = "kBEG7", .key = NCKEY_BEGIN, .alt = 1, .ctrl = 1, },
|
|
{ .tinfo = "kDC", .key = NCKEY_DEL, .shift = 1, },
|
|
{ .tinfo = "kDC3", .key = NCKEY_DEL, .alt = 1, },
|
|
{ .tinfo = "kDC4", .key = NCKEY_DEL, .alt = 1, .shift = 1, },
|
|
{ .tinfo = "kDC5", .key = NCKEY_DEL, .ctrl = 1, },
|
|
{ .tinfo = "kDC6", .key = NCKEY_DEL, .ctrl = 1, .shift = 1, },
|
|
{ .tinfo = "kDC7", .key = NCKEY_DEL, .alt = 1, .ctrl = 1, },
|
|
{ .tinfo = "kDN", .key = NCKEY_DOWN, .shift = 1, },
|
|
{ .tinfo = "kDN3", .key = NCKEY_DOWN, .alt = 1, },
|
|
{ .tinfo = "kDN4", .key = NCKEY_DOWN, .alt = 1, .shift = 1, },
|
|
{ .tinfo = "kDN5", .key = NCKEY_DOWN, .ctrl = 1, },
|
|
{ .tinfo = "kDN6", .key = NCKEY_DOWN, .ctrl = 1, .shift = 1, },
|
|
{ .tinfo = "kDN7", .key = NCKEY_DOWN, .alt = 1, .ctrl = 1, },
|
|
{ .tinfo = "kEND", .key = NCKEY_END, .shift = 1, },
|
|
{ .tinfo = "kEND3", .key = NCKEY_END, .alt = 1, },
|
|
{ .tinfo = "kEND4", .key = NCKEY_END, .alt = 1, .shift = 1, },
|
|
{ .tinfo = "kEND5", .key = NCKEY_END, .ctrl = 1, },
|
|
{ .tinfo = "kEND6", .key = NCKEY_END, .ctrl = 1, .shift = 1, },
|
|
{ .tinfo = "kEND7", .key = NCKEY_END, .alt = 1, .ctrl = 1, },
|
|
{ .tinfo = "kHOM", .key = NCKEY_HOME, .shift = 1, },
|
|
{ .tinfo = "kHOM3", .key = NCKEY_HOME, .alt = 1, },
|
|
{ .tinfo = "kHOM4", .key = NCKEY_HOME, .alt = 1, .shift = 1, },
|
|
{ .tinfo = "kHOM5", .key = NCKEY_HOME, .ctrl = 1, },
|
|
{ .tinfo = "kHOM6", .key = NCKEY_HOME, .ctrl = 1, .shift = 1, },
|
|
{ .tinfo = "kHOM7", .key = NCKEY_HOME, .alt = 1, .ctrl = 1, },
|
|
{ .tinfo = "kIC", .key = NCKEY_INS, .shift = 1, },
|
|
{ .tinfo = "kIC3", .key = NCKEY_INS, .alt = 1, },
|
|
{ .tinfo = "kIC4", .key = NCKEY_INS, .alt = 1, .shift = 1, },
|
|
{ .tinfo = "kIC5", .key = NCKEY_INS, .ctrl = 1, },
|
|
{ .tinfo = "kIC6", .key = NCKEY_INS, .ctrl = 1, .shift = 1, },
|
|
{ .tinfo = "kIC7", .key = NCKEY_INS, .alt = 1, .ctrl = 1, },
|
|
{ .tinfo = "kLFT", .key = NCKEY_LEFT, .shift = 1, },
|
|
{ .tinfo = "kLFT3", .key = NCKEY_LEFT, .alt = 1, },
|
|
{ .tinfo = "kLFT4", .key = NCKEY_LEFT, .alt = 1, .shift = 1, },
|
|
{ .tinfo = "kLFT5", .key = NCKEY_LEFT, .ctrl = 1, },
|
|
{ .tinfo = "kLFT6", .key = NCKEY_LEFT, .ctrl = 1, .shift = 1, },
|
|
{ .tinfo = "kLFT7", .key = NCKEY_LEFT, .alt = 1, .ctrl = 1, },
|
|
{ .tinfo = "kNXT", .key = NCKEY_PGDOWN, .shift = 1, },
|
|
{ .tinfo = "kNXT3", .key = NCKEY_PGDOWN, .alt = 1, },
|
|
{ .tinfo = "kNXT4", .key = NCKEY_PGDOWN, .alt = 1, .shift = 1, },
|
|
{ .tinfo = "kNXT5", .key = NCKEY_PGDOWN, .ctrl = 1, },
|
|
{ .tinfo = "kNXT6", .key = NCKEY_PGDOWN, .ctrl = 1, .shift = 1, },
|
|
{ .tinfo = "kNXT7", .key = NCKEY_PGDOWN, .alt = 1, .ctrl = 1, },
|
|
{ .tinfo = "kPRV", .key = NCKEY_PGUP, .shift = 1, },
|
|
{ .tinfo = "kPRV3", .key = NCKEY_PGUP, .alt = 1, },
|
|
{ .tinfo = "kPRV4", .key = NCKEY_PGUP, .alt = 1, .shift = 1, },
|
|
{ .tinfo = "kPRV5", .key = NCKEY_PGUP, .ctrl = 1, },
|
|
{ .tinfo = "kPRV6", .key = NCKEY_PGUP, .ctrl = 1, .shift = 1, },
|
|
{ .tinfo = "kPRV7", .key = NCKEY_PGUP, .alt = 1, .ctrl = 1, },
|
|
{ .tinfo = "kRIT", .key = NCKEY_RIGHT, .shift = 1, },
|
|
{ .tinfo = "kRIT3", .key = NCKEY_RIGHT, .alt = 1, },
|
|
{ .tinfo = "kRIT4", .key = NCKEY_RIGHT, .alt = 1, .shift = 1, },
|
|
{ .tinfo = "kRIT5", .key = NCKEY_RIGHT, .ctrl = 1, },
|
|
{ .tinfo = "kRIT6", .key = NCKEY_RIGHT, .ctrl = 1, .shift = 1, },
|
|
{ .tinfo = "kRIT7", .key = NCKEY_RIGHT, .alt = 1, .ctrl = 1, },
|
|
{ .tinfo = "kUP", .key = NCKEY_UP, .shift = 1, },
|
|
{ .tinfo = "kUP3", .key = NCKEY_UP, .alt = 1, },
|
|
{ .tinfo = "kUP4", .key = NCKEY_UP, .alt = 1, .shift = 1, },
|
|
{ .tinfo = "kUP5", .key = NCKEY_UP, .ctrl = 1, },
|
|
{ .tinfo = "kUP6", .key = NCKEY_UP, .ctrl = 1, .shift = 1, },
|
|
{ .tinfo = "kUP7", .key = NCKEY_UP, .alt = 1, .ctrl = 1, },
|
|
{ .tinfo = NULL, .key = 0, }
|
|
}, *k;
|
|
for(k = keys ; k->tinfo ; ++k){
|
|
char* seq = tigetstr(k->tinfo);
|
|
if(seq == NULL || seq == (char*)-1){
|
|
loginfo("no terminfo declaration for %s", k->tinfo);
|
|
continue;
|
|
}
|
|
if(seq[0] != NCKEY_ESC || strlen(seq) < 2){ // assume ESC prefix + content
|
|
logwarn("invalid escape: %s (0x%x)", k->tinfo, k->key);
|
|
continue;
|
|
}
|
|
unsigned modifiers = (k->shift ? NCKEY_MOD_SHIFT : 0)
|
|
| (k->alt ? NCKEY_MOD_ALT : 0)
|
|
| (k->ctrl ? NCKEY_MOD_CTRL : 0);
|
|
if(inputctx_add_input_escape(&ictx->amata, seq, k->key, modifiers)){
|
|
return -1;
|
|
}
|
|
logdebug("support for terminfo's %s: %s", k->tinfo, seq);
|
|
}
|
|
const char* bs = tigetstr("kbs");
|
|
if(bs == NULL){
|
|
logwarn("no backspace key was defined");
|
|
}else{
|
|
if(bs[0] == NCKEY_ESC){
|
|
if(inputctx_add_input_escape(&ictx->amata, bs, NCKEY_BACKSPACE, 0)){
|
|
return -1;
|
|
}
|
|
}else{
|
|
ictx->backspace = bs[0];
|
|
}
|
|
}
|
|
#else
|
|
(void)ictx;
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
// starting from the current amata match point, match any necessary prefix, then
|
|
// extract the (possibly empty) content, then match the follow. as we are only
|
|
// called from a callback context, and know we've been properly matched, there
|
|
// is no error-checking per se (we do require prefix/follow matches, but if
|
|
// missed, we just return NULL). indicate empty prefix with "", not NULL.
|
|
// updates ictx->amata.matchstart to be pointing past the follow. follow ought
|
|
// not be NUL.
|
|
static char*
|
|
amata_next_kleene(automaton* amata, const char* prefix, char follow){
|
|
char c;
|
|
while( (c = *prefix++) ){
|
|
if(*amata->matchstart != c){
|
|
logerror("matchstart didn't match prefix (%c vs %c)", c, *amata->matchstart);
|
|
return NULL;
|
|
}
|
|
++amata->matchstart;
|
|
}
|
|
// prefix has been matched. mark start of string and find follow.
|
|
const unsigned char* start = amata->matchstart;
|
|
while(*amata->matchstart != follow){
|
|
++amata->matchstart;
|
|
}
|
|
char* ret = malloc(amata->matchstart - start + 1);
|
|
if(ret){
|
|
memcpy(ret, start, amata->matchstart - start);
|
|
ret[amata->matchstart - start] = '\0';
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// starting from the current amata match point, match any necessary prefix, then
|
|
// extract the numeric (possibly empty), then match the follow. as we are only
|
|
// called from a callback context, and know we've been properly matched, there
|
|
// is no error-checking per se (we do require prefix/follow matches, but if
|
|
// missed, we just return 0). indicate empty prefix with "", not NULL.
|
|
// updates ictx->amata.matchstart to be pointing past the follow. follow ought
|
|
// not be a digit nor NUL.
|
|
static unsigned
|
|
amata_next_numeric(automaton* amata, const char* prefix, char follow){
|
|
char c;
|
|
while( (c = *prefix++) ){
|
|
if(*amata->matchstart != c){
|
|
logerror("matchstart didn't match prefix (%c vs %c)", c, *amata->matchstart);
|
|
return 0;
|
|
}
|
|
++amata->matchstart;
|
|
}
|
|
// prefix has been matched
|
|
unsigned ret = 0;
|
|
while(isdigit(*amata->matchstart)){
|
|
int addend = *amata->matchstart - '0';
|
|
if((UINT_MAX - addend) / 10 < ret){
|
|
logerror("overflow: %u * 10 + %u > %u", ret, addend, UINT_MAX);
|
|
}
|
|
ret *= 10;
|
|
ret += addend;
|
|
++amata->matchstart;
|
|
}
|
|
char candidate = *amata->matchstart++;
|
|
if(candidate != follow){
|
|
logerror("didn't see follow (%c vs %c)", candidate, follow);
|
|
return 0;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// same deal as amata_next_numeric, but returns a heap-allocated string.
|
|
// strings always end with ST ("x1b\\"). this one *does* return NULL on
|
|
// either a match failure or an alloc failure.
|
|
static char*
|
|
amata_next_string(automaton* amata, const char* prefix){
|
|
return amata_next_kleene(amata, prefix, '\x1b');
|
|
}
|
|
|
|
static inline void
|
|
send_synth_signal(int sig){
|
|
if(sig){
|
|
#ifndef __MINGW32__
|
|
raise(sig);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
static void
|
|
mark_pipe_ready(ipipe pipes[static 2]){
|
|
char sig = 1;
|
|
#ifndef __MINGW32__
|
|
if(write(pipes[1], &sig, sizeof(sig)) != 1){
|
|
logwarn("error writing to pipe (%d) (%s)", pipes[1], strerror(errno));
|
|
#else
|
|
DWORD wrote;
|
|
if(!WriteFile(pipes[1], &sig, sizeof(sig), &wrote, NULL) || wrote != sizeof(sig)){
|
|
logwarn("error writing to pipe");
|
|
#endif
|
|
}else{
|
|
loginfo("wrote to readiness pipe");
|
|
}
|
|
}
|
|
|
|
// shove the assembled input |tni| into the input queue (if there's room, and
|
|
// we're not draining, and we haven't hit EOF). send any synthesized signal as
|
|
// the last thing we do. if Ctrl or Shift are among the modifiers, we replace
|
|
// any lowercase letter with its uppercase form, to maintain compatibility with
|
|
// other input methods.
|
|
//
|
|
// note that this w orks entirely off 'modifiers', not the obsolete
|
|
// shift/alt/ctrl booleans, which it neither sets nor tests!
|
|
static void
|
|
load_ncinput(inputctx* ictx, ncinput *tni){
|
|
int synth = 0;
|
|
if(tni->modifiers & (NCKEY_MOD_CTRL | NCKEY_MOD_SHIFT | NCKEY_MOD_CAPSLOCK)){
|
|
// when ctrl/shift are used with an ASCII (0..127) lowercase letter, always
|
|
// supply the capitalized form, to maintain compatibility among solutions.
|
|
if(tni->id < 0x7f){
|
|
if(islower(tni->id)){
|
|
tni->id = toupper(tni->id);
|
|
}
|
|
}
|
|
}
|
|
// if the kitty keyboard protocol is in use, any input without an explicit
|
|
// evtype can be safely considered a PRESS.
|
|
if(ictx->kittykbd){
|
|
if(tni->evtype == NCTYPE_UNKNOWN){
|
|
tni->evtype = NCTYPE_PRESS;
|
|
}
|
|
}
|
|
if(tni->modifiers == NCKEY_MOD_CTRL){ // exclude all other modifiers
|
|
if(ictx->linesigs){
|
|
if(tni->id == 'C'){
|
|
synth = SIGINT;
|
|
}else if(tni->id == 'Z'){
|
|
synth = SIGSTOP;
|
|
}else if(tni->id == '\\'){
|
|
synth = SIGQUIT;
|
|
}
|
|
}
|
|
}
|
|
inc_input_events(ictx);
|
|
if(ictx->drain || ictx->stdineof){
|
|
send_synth_signal(synth);
|
|
return;
|
|
}
|
|
pthread_mutex_lock(&ictx->ilock);
|
|
if(ictx->ivalid == ictx->isize){
|
|
pthread_mutex_unlock(&ictx->ilock);
|
|
logwarn("dropping input 0x%08x", tni->id);
|
|
inc_input_errors(ictx);
|
|
send_synth_signal(synth);
|
|
return;
|
|
}
|
|
ncinput* ni = ictx->inputs + ictx->iwrite;
|
|
memcpy(ni, tni, sizeof(*tni));
|
|
// perform final normalizations
|
|
if(ni->id == 0x7f || ni->id == 0x8){
|
|
ni->id = NCKEY_BACKSPACE;
|
|
}else if(ni->id == '\n' || ni->id == '\r'){
|
|
ni->id = NCKEY_ENTER;
|
|
}else if(ni->id == ictx->backspace){
|
|
ni->id = NCKEY_BACKSPACE;
|
|
}else if(ni->id > 0 && ni->id <= 26 && ni->id != '\t'){
|
|
ni->id = ni->id + 'A' - 1;
|
|
ni->modifiers |= NCKEY_MOD_CTRL;
|
|
}
|
|
if(++ictx->iwrite == ictx->isize){
|
|
ictx->iwrite = 0;
|
|
}
|
|
++ictx->ivalid;
|
|
// FIXME we don't always need to write here; write if ictx->ivalid was 0, and
|
|
// also write *from the client context* if we empty the input buffer there..?
|
|
mark_pipe_ready(ictx->readypipes);
|
|
pthread_mutex_unlock(&ictx->ilock);
|
|
pthread_cond_broadcast(&ictx->icond);
|
|
send_synth_signal(synth);
|
|
}
|
|
|
|
static void
|
|
pixelmouse_click(inputctx* ictx, ncinput* ni, long y, long x){
|
|
--x;
|
|
--y;
|
|
if(ictx->ti->cellpxy == 0 || ictx->ti->cellpxx == 0){
|
|
logerror("pixelmouse event without pixel info (%ld/%ld)", y, x);
|
|
inc_input_errors(ictx);
|
|
return;
|
|
}
|
|
ni->ypx = y % ictx->ti->cellpxy;
|
|
ni->xpx = x % ictx->ti->cellpxx;
|
|
y /= ictx->ti->cellpxy;
|
|
x /= ictx->ti->cellpxx;
|
|
x -= ictx->lmargin;
|
|
y -= ictx->tmargin;
|
|
// convert from 1- to 0-indexing, and account for margins
|
|
if(x < 0 || y < 0){ // click was in margins, drop it
|
|
logwarn("dropping click in margins %ld/%ld", y, x);
|
|
return;
|
|
}
|
|
if((unsigned)x >= ictx->ti->dimx - (ictx->rmargin + ictx->lmargin)){
|
|
logwarn("dropping click in margins %ld/%ld", y, x);
|
|
return;
|
|
}
|
|
if((unsigned)y >= ictx->ti->dimy - (ictx->bmargin + ictx->tmargin)){
|
|
logwarn("dropping click in margins %ld/%ld", y, x);
|
|
return;
|
|
}
|
|
ni->y = y;
|
|
ni->x = x;
|
|
load_ncinput(ictx, ni);
|
|
}
|
|
|
|
// ictx->numeric, ictx->p3, and ictx->p2 have the two parameters. we're using
|
|
// SGR (1006) mouse encoding, so use the final character to determine release
|
|
// ('M' for click, 'm' for release).
|
|
static void
|
|
mouse_click(inputctx* ictx, unsigned release, char follow){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[<", ';');
|
|
long x = amata_next_numeric(&ictx->amata, "", ';');
|
|
long y = amata_next_numeric(&ictx->amata, "", follow);
|
|
ncinput tni = {
|
|
.ctrl = mods & 0x10,
|
|
.alt = mods & 0x08,
|
|
.shift = mods & 0x04,
|
|
};
|
|
tni.modifiers = (tni.shift ? NCKEY_MOD_SHIFT : 0)
|
|
| (tni.ctrl ? NCKEY_MOD_CTRL : 0)
|
|
| (tni.alt ? NCKEY_MOD_ALT : 0);
|
|
// SGR mouse reporting: lower two bits signify base button + {0, 1, 2} press
|
|
// and no button pressed/release/{3}. bit 5 indicates motion. bits 6 and 7
|
|
// select device groups: 64 is buttons 4--7, 128 is 8--11. a pure motion
|
|
// report (no button) is 35 (32 + 3 (no button pressed)) with (oddly enough)
|
|
// 'M' (i.e. release == true).
|
|
if(release){
|
|
tni.evtype = NCTYPE_RELEASE;
|
|
}else{
|
|
tni.evtype = NCTYPE_PRESS;
|
|
}
|
|
if(mods % 4 == 3){
|
|
tni.id = NCKEY_MOTION;
|
|
tni.evtype = NCTYPE_RELEASE;
|
|
}else{
|
|
if(mods < 64){
|
|
tni.id = NCKEY_BUTTON1 + (mods % 4);
|
|
}else if(mods >= 64 && mods < 128){
|
|
tni.id = NCKEY_BUTTON4 + (mods % 4);
|
|
}else if(mods >= 128 && mods < 192){
|
|
tni.id = NCKEY_BUTTON8 + (mods % 4);
|
|
}
|
|
}
|
|
if(ictx->ti->pixelmice){
|
|
if(ictx->ti->cellpxx == 0){
|
|
logerror("pixelmouse but no pixel info");
|
|
}
|
|
return pixelmouse_click(ictx, &tni, y, x);
|
|
}
|
|
x -= (1 + ictx->lmargin);
|
|
y -= (1 + ictx->tmargin);
|
|
// convert from 1- to 0-indexing, and account for margins
|
|
if(x < 0 || y < 0){ // click was in margins, drop it
|
|
logwarn("dropping click in margins %ld/%ld", y, x);
|
|
return;
|
|
}
|
|
if((unsigned)x >= ictx->ti->dimx - (ictx->rmargin + ictx->lmargin)){
|
|
logwarn("dropping click in margins %ld/%ld", y, x);
|
|
return;
|
|
}
|
|
if((unsigned)y >= ictx->ti->dimy - (ictx->bmargin + ictx->tmargin)){
|
|
logwarn("dropping click in margins %ld/%ld", y, x);
|
|
return;
|
|
}
|
|
tni.x = x;
|
|
tni.y = y;
|
|
tni.ypx = -1;
|
|
tni.xpx = -1;
|
|
load_ncinput(ictx, &tni);
|
|
}
|
|
|
|
static int
|
|
mouse_press_cb(inputctx* ictx){
|
|
mouse_click(ictx, 0, 'M');
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
mouse_release_cb(inputctx* ictx){
|
|
mouse_click(ictx, 1, 'm');
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
cursor_location_cb(inputctx* ictx){
|
|
unsigned y = amata_next_numeric(&ictx->amata, "\x1b[", ';') - 1;
|
|
unsigned x = amata_next_numeric(&ictx->amata, "", 'R') - 1;
|
|
// the first one doesn't go onto the queue; consume it here
|
|
pthread_mutex_lock(&ictx->clock);
|
|
--ictx->coutstanding;
|
|
if(ictx->initdata){
|
|
pthread_mutex_unlock(&ictx->clock);
|
|
ictx->initdata->cursory = y;
|
|
ictx->initdata->cursorx = x;
|
|
return 2;
|
|
}
|
|
if(ictx->cvalid == ictx->csize){
|
|
pthread_mutex_unlock(&ictx->clock);
|
|
logwarn("dropping cursor location report %u/%u", y, x);
|
|
inc_input_errors(ictx);
|
|
}else{
|
|
cursorloc* cloc = &ictx->csrs[ictx->cwrite];
|
|
if(++ictx->cwrite == ictx->csize){
|
|
ictx->cwrite = 0;
|
|
}
|
|
cloc->y = y;
|
|
cloc->x = x;
|
|
++ictx->cvalid;
|
|
pthread_mutex_unlock(&ictx->clock);
|
|
pthread_cond_broadcast(&ictx->ccond);
|
|
loginfo("cursor location: %u/%u", y, x);
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
geom_cb(inputctx* ictx){
|
|
unsigned kind = amata_next_numeric(&ictx->amata, "\x1b[", ';');
|
|
unsigned y = amata_next_numeric(&ictx->amata, "", ';');
|
|
unsigned x = amata_next_numeric(&ictx->amata, "", 't');
|
|
if(kind == 4){ // pixel geometry
|
|
if(ictx->initdata){
|
|
ictx->initdata->pixy = y;
|
|
ictx->initdata->pixx = x;
|
|
}
|
|
loginfo("pixel geom report %d/%d", y, x);
|
|
}else if(kind == 8){ // cell geometry
|
|
if(ictx->initdata){
|
|
ictx->initdata->dimy = y;
|
|
ictx->initdata->dimx = x;
|
|
}
|
|
loginfo("cell geom report %d/%d", y, x);
|
|
}else{
|
|
logerror("invalid geom report type: %d", kind);
|
|
return -1;
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
static void
|
|
xtmodkey(inputctx* ictx, int val, int mods){
|
|
assert(mods >= 0);
|
|
assert(val > 0);
|
|
logdebug("v/m %d %d", val, mods);
|
|
ncinput tni = {
|
|
.id = val,
|
|
.evtype = NCTYPE_UNKNOWN,
|
|
};
|
|
if(mods == 2 || mods == 4 || mods == 6 || mods == 8 || mods == 10
|
|
|| mods == 12 || mods == 14 || mods == 16){
|
|
tni.shift = 1;
|
|
tni.modifiers |= NCKEY_MOD_SHIFT;
|
|
}
|
|
if(mods == 5 || mods == 6 || mods == 7 || mods == 8 ||
|
|
(mods >= 13 && mods <= 16)){
|
|
tni.ctrl = 1;
|
|
tni.modifiers |= NCKEY_MOD_CTRL;
|
|
}
|
|
if(mods == 3 || mods == 4 || mods == 7 || mods == 8 || mods == 11
|
|
|| mods == 12 || mods == 15 || mods == 16){
|
|
tni.alt = 1;
|
|
tni.modifiers |= NCKEY_MOD_ALT;
|
|
}
|
|
if(mods >= 9 && mods <= 16){
|
|
tni.modifiers |= NCKEY_MOD_META;
|
|
}
|
|
load_ncinput(ictx, &tni);
|
|
}
|
|
|
|
static uint32_t
|
|
kitty_functional(uint32_t val){
|
|
if(val >= 57344 && val <= 63743){
|
|
if(val >= 57376 && val <= 57398){
|
|
val = NCKEY_F13 + val - 57376;
|
|
}else if(val >= 57428 && val <= 57440){
|
|
val = NCKEY_MEDIA_PLAY + val - 57428;
|
|
}else if(val >= 57399 && val <= 57408){
|
|
val = '0' + val - 57399;
|
|
}else if(val >= 57441 && val <= 57454){ // up through NCKEY_L5SHIFT
|
|
val = NCKEY_LSHIFT + val - 57441;
|
|
}else switch(val){
|
|
case 57358: val = NCKEY_CAPS_LOCK; break;
|
|
case 57400: val = '1'; break;
|
|
case 57359: val = NCKEY_SCROLL_LOCK; break;
|
|
case 57360: val = NCKEY_NUM_LOCK; break;
|
|
case 57361: val = NCKEY_PRINT_SCREEN; break;
|
|
case 57362: val = NCKEY_PAUSE; break;
|
|
case 57363: val = NCKEY_MENU; break;
|
|
case 57409: val = '.'; break;
|
|
case 57410: val = '/'; break;
|
|
case 57411: val = '*'; break;
|
|
case 57412: val = '-'; break;
|
|
case 57413: val = '+'; break;
|
|
case 57414: val = NCKEY_ENTER; break;
|
|
case 57415: val = '='; break;
|
|
case 57416: val = NCKEY_SEPARATOR; break;
|
|
case 57417: val = NCKEY_LEFT; break;
|
|
case 57418: val = NCKEY_RIGHT; break;
|
|
case 57419: val = NCKEY_UP; break;
|
|
case 57420: val = NCKEY_DOWN; break;
|
|
case 57421: val = NCKEY_PGUP; break;
|
|
case 57422: val = NCKEY_PGDOWN; break;
|
|
case 57423: val = NCKEY_HOME; break;
|
|
case 57424: val = NCKEY_END; break;
|
|
case 57425: val = NCKEY_INS; break;
|
|
case 57426: val = NCKEY_DEL; break;
|
|
case 57427: val = NCKEY_BEGIN; break;
|
|
}
|
|
}else{
|
|
switch(val){
|
|
case 0xd: val = NCKEY_ENTER; break;
|
|
}
|
|
}
|
|
return val;
|
|
}
|
|
|
|
static void
|
|
kitty_kbd_txt(inputctx* ictx, int val, int mods, uint32_t *txt, int evtype){
|
|
assert(evtype >= 0);
|
|
assert(mods >= 0);
|
|
assert(val > 0);
|
|
logdebug("v/m/e %d %d %d", val, mods, evtype);
|
|
// "If the modifier field is not present in the escape code, its default value
|
|
// is 1 which means no modifiers."
|
|
if(mods == 0){
|
|
mods = 1;
|
|
}
|
|
ncinput tni = {
|
|
.id = kitty_functional(val),
|
|
.shift = mods && !!((mods - 1) & 0x1),
|
|
.alt = mods && !!((mods - 1) & 0x2),
|
|
.ctrl = mods && !!((mods - 1) & 0x4),
|
|
.modifiers = mods - 1,
|
|
};
|
|
switch(evtype){
|
|
case 0:
|
|
__attribute__ ((fallthrough));
|
|
case 1:
|
|
tni.evtype = NCTYPE_PRESS;
|
|
break;
|
|
case 2:
|
|
tni.evtype = NCTYPE_REPEAT;
|
|
break;
|
|
case 3:
|
|
tni.evtype = NCTYPE_RELEASE;
|
|
break;
|
|
default:
|
|
tni.evtype = NCTYPE_UNKNOWN;
|
|
break;
|
|
}
|
|
//note: if we don't set eff_text here, it will be set to .id later.
|
|
if(txt && txt[0]!=0){
|
|
for(int i=0 ; i<4 ; i++){
|
|
tni.eff_text[i] = txt[i];
|
|
}
|
|
}
|
|
load_ncinput(ictx, &tni);
|
|
}
|
|
|
|
static void
|
|
kitty_kbd(inputctx* ictx, int val, int mods, int evtype){
|
|
kitty_kbd_txt(ictx, val, mods, NULL, evtype);
|
|
}
|
|
|
|
static int
|
|
kitty_cb_simple(inputctx* ictx){
|
|
unsigned val = amata_next_numeric(&ictx->amata, "\x1b[", 'u');
|
|
val = kitty_functional(val);
|
|
kitty_kbd(ictx, val, 0, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb(inputctx* ictx){
|
|
unsigned val = amata_next_numeric(&ictx->amata, "\x1b[", ';');
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "", 'u');
|
|
kitty_kbd(ictx, val, mods, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_atxtn(inputctx* ictx, int n, int with_event){
|
|
uint32_t txt[5]={0};
|
|
unsigned val = amata_next_numeric(&ictx->amata, "\x1b[", ';');
|
|
unsigned ev = 0;
|
|
unsigned mods = 0;
|
|
if (with_event) {
|
|
mods = amata_next_numeric(&ictx->amata, "", ':');
|
|
ev = amata_next_numeric(&ictx->amata, "", ';');
|
|
} else {
|
|
mods = amata_next_numeric(&ictx->amata, "", ';');
|
|
}
|
|
for (int i = 0; i<n; i++) {
|
|
txt[i] = amata_next_numeric(&ictx->amata, "", (i==n-1)?'u':';');
|
|
}
|
|
kitty_kbd_txt(ictx, val, mods, txt, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_atxt1(inputctx* ictx){
|
|
return kitty_cb_atxtn(ictx, 1, 0);
|
|
}
|
|
|
|
static int
|
|
kitty_cb_atxt2(inputctx* ictx){
|
|
return kitty_cb_atxtn(ictx, 2, 0);
|
|
}
|
|
|
|
static int
|
|
kitty_cb_atxt3(inputctx* ictx){
|
|
return kitty_cb_atxtn(ictx, 3, 0);
|
|
}
|
|
|
|
static int
|
|
kitty_cb_atxt4(inputctx* ictx){
|
|
return kitty_cb_atxtn(ictx, 4, 0);
|
|
}
|
|
|
|
|
|
static int
|
|
kitty_cb_complex_atxt1(inputctx* ictx){
|
|
return kitty_cb_atxtn(ictx, 1, 1);
|
|
}
|
|
|
|
static int
|
|
kitty_cb_complex_atxt2(inputctx* ictx){
|
|
return kitty_cb_atxtn(ictx, 2, 1);
|
|
}
|
|
|
|
static int
|
|
kitty_cb_complex_atxt3(inputctx* ictx){
|
|
return kitty_cb_atxtn(ictx, 3, 1);
|
|
}
|
|
|
|
static int
|
|
kitty_cb_complex_atxt4(inputctx* ictx){
|
|
return kitty_cb_atxtn(ictx, 4, 1);
|
|
}
|
|
|
|
static uint32_t
|
|
legacy_functional(uint32_t id){
|
|
switch(id){
|
|
case 2: id = NCKEY_INS; break;
|
|
case 3: id = NCKEY_DEL; break;
|
|
case 5: id = NCKEY_PGUP; break;
|
|
case 6: id = NCKEY_PGDOWN; break;
|
|
case 7: id = NCKEY_HOME; break;
|
|
case 8: id = NCKEY_END; break;
|
|
case 11: id = NCKEY_F01; break;
|
|
case 12: id = NCKEY_F02; break;
|
|
case 13: id = NCKEY_F03; break;
|
|
case 14: id = NCKEY_F04; break;
|
|
case 15: id = NCKEY_F05; break;
|
|
case 17: id = NCKEY_F06; break;
|
|
case 18: id = NCKEY_F07; break;
|
|
case 19: id = NCKEY_F08; break;
|
|
case 20: id = NCKEY_F09; break;
|
|
case 21: id = NCKEY_F10; break;
|
|
case 23: id = NCKEY_F11; break;
|
|
case 24: id = NCKEY_F12; break;
|
|
}
|
|
return id;
|
|
}
|
|
|
|
static int
|
|
simple_cb_begin(inputctx* ictx){
|
|
kitty_kbd(ictx, NCKEY_BEGIN, 0, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_functional(inputctx* ictx){
|
|
unsigned val = amata_next_numeric(&ictx->amata, "\x1b[", ';');
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "", ':');
|
|
unsigned ev = amata_next_numeric(&ictx->amata, "", '~');
|
|
uint32_t kval = kitty_functional(val);
|
|
if(kval == val){
|
|
kval = legacy_functional(val);
|
|
}
|
|
kitty_kbd(ictx, kval, mods, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
wezterm_cb(inputctx* ictx){
|
|
unsigned val = amata_next_numeric(&ictx->amata, "\x1b[", ';');
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "", '~');
|
|
uint32_t kval = legacy_functional(val);
|
|
kitty_kbd(ictx, kval, mods, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
legacy_cb_f1(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'P');
|
|
kitty_kbd(ictx, NCKEY_F01, mods, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
legacy_cb_f2(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'Q');
|
|
kitty_kbd(ictx, NCKEY_F02, mods, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
legacy_cb_f4(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'S');
|
|
kitty_kbd(ictx, NCKEY_F04, mods, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_f1(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
|
|
unsigned ev = amata_next_numeric(&ictx->amata, "", 'P');
|
|
kitty_kbd(ictx, NCKEY_F01, mods, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_f2(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
|
|
unsigned ev = amata_next_numeric(&ictx->amata, "", 'Q');
|
|
kitty_kbd(ictx, NCKEY_F02, mods, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_f3(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
|
|
unsigned ev = amata_next_numeric(&ictx->amata, "", 'R');
|
|
kitty_kbd(ictx, NCKEY_F03, mods, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_f4(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
|
|
unsigned ev = amata_next_numeric(&ictx->amata, "", 'S');
|
|
kitty_kbd(ictx, NCKEY_F04, mods, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
legacy_cb_right(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'C');
|
|
kitty_kbd(ictx, NCKEY_RIGHT, mods, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
legacy_cb_left(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'D');
|
|
kitty_kbd(ictx, NCKEY_LEFT, mods, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
legacy_cb_down(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'B');
|
|
kitty_kbd(ictx, NCKEY_DOWN, mods, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
legacy_cb_up(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'A');
|
|
kitty_kbd(ictx, NCKEY_UP, mods, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_right(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
|
|
unsigned ev = amata_next_numeric(&ictx->amata, "", 'C');
|
|
kitty_kbd(ictx, NCKEY_RIGHT, mods, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_left(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
|
|
unsigned ev = amata_next_numeric(&ictx->amata, "", 'D');
|
|
kitty_kbd(ictx, NCKEY_LEFT, mods, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_down(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
|
|
unsigned ev = amata_next_numeric(&ictx->amata, "", 'B');
|
|
kitty_kbd(ictx, NCKEY_DOWN, mods, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_up(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
|
|
unsigned ev = amata_next_numeric(&ictx->amata, "", 'A');
|
|
kitty_kbd(ictx, NCKEY_UP, mods, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
legacy_cb_begin(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'E');
|
|
kitty_kbd(ictx, NCKEY_BEGIN, mods, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
legacy_cb_end(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'F');
|
|
kitty_kbd(ictx, NCKEY_END, mods, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
legacy_cb_home(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'H');
|
|
kitty_kbd(ictx, NCKEY_HOME, mods, 0);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_begin(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
|
|
unsigned ev = amata_next_numeric(&ictx->amata, "", 'E');
|
|
kitty_kbd(ictx, NCKEY_BEGIN, mods, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_end(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
|
|
unsigned ev = amata_next_numeric(&ictx->amata, "", 'F');
|
|
kitty_kbd(ictx, NCKEY_END, mods, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_home(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
|
|
unsigned ev = amata_next_numeric(&ictx->amata, "", 'H');
|
|
kitty_kbd(ictx, NCKEY_HOME, mods, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_cb_complex(inputctx* ictx){
|
|
unsigned val = amata_next_numeric(&ictx->amata, "\x1b[", ';');
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "", ':');
|
|
unsigned ev = amata_next_numeric(&ictx->amata, "", 'u');
|
|
val = kitty_functional(val);
|
|
kitty_kbd(ictx, val, mods, ev);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kitty_keyboard_cb(inputctx* ictx){
|
|
unsigned level = amata_next_numeric(&ictx->amata, "\x1b[?", 'u');
|
|
if(ictx->initdata){
|
|
ictx->initdata->kbdlevel = level;
|
|
}
|
|
loginfo("kitty keyboard level %u (was %u)", level, ictx->kittykbd);
|
|
ictx->kittykbd = level;
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
xtmodkey_cb(inputctx* ictx){
|
|
unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[27;", ';');
|
|
unsigned val = amata_next_numeric(&ictx->amata, "", '~');
|
|
xtmodkey(ictx, val, mods);
|
|
return 2;
|
|
}
|
|
|
|
// the only xtsmgraphics reply with a single Pv arg is color registers
|
|
static int
|
|
xtsmgraphics_cregs_cb(inputctx* ictx){
|
|
unsigned pv = amata_next_numeric(&ictx->amata, "\x1b[?1;0;", 'S');
|
|
if(ictx->initdata){
|
|
ictx->initdata->color_registers = pv;
|
|
}
|
|
loginfo("sixel color registers: %d", pv);
|
|
return 2;
|
|
}
|
|
|
|
// the only xtsmgraphics reply with a dual Pv arg we want is sixel geometry
|
|
static int
|
|
xtsmgraphics_sixel_cb(inputctx* ictx){
|
|
unsigned width = amata_next_numeric(&ictx->amata, "\x1b[?2;0;", ';');
|
|
unsigned height = amata_next_numeric(&ictx->amata, "", 'S');
|
|
if(ictx->initdata){
|
|
ictx->initdata->sixelx = width;
|
|
ictx->initdata->sixely = height;
|
|
}
|
|
loginfo("max sixel geometry: %dx%d", height, width);
|
|
return 2;
|
|
}
|
|
|
|
static void
|
|
handoff_initial_responses_late(inputctx* ictx){
|
|
bool sig = false;
|
|
pthread_mutex_lock(&ictx->ilock);
|
|
if(ictx->initdata_complete && ictx->initdata){
|
|
ictx->initdata = NULL;
|
|
sig = true;
|
|
}
|
|
pthread_mutex_unlock(&ictx->ilock);
|
|
if(sig){
|
|
pthread_cond_broadcast(&ictx->icond);
|
|
loginfo("handing off initial responses");
|
|
}
|
|
}
|
|
|
|
|
|
// mark the initdata as complete, but don't yet broadcast it off.
|
|
static void
|
|
handoff_initial_responses_early(inputctx* ictx){
|
|
pthread_mutex_lock(&ictx->ilock);
|
|
// set initdata_complete, but don't clear initdata
|
|
ictx->initdata_complete = ictx->initdata;
|
|
pthread_mutex_unlock(&ictx->ilock);
|
|
}
|
|
|
|
// if XTSMGRAPHICS responses were provided, but DA1 didn't advertise sixel
|
|
// support, we need scrub those responses, lest we try to use Sixel.
|
|
static inline void
|
|
scrub_sixel_responses(struct initial_responses* idata){
|
|
if(idata->color_registers || idata->sixelx || idata->sixely){
|
|
logwarn("answered XTSMGRAPHICS, but no sixel in DA1");
|
|
idata->color_registers = 0;
|
|
idata->sixelx = 0;
|
|
idata->sixely = 0;
|
|
}
|
|
}
|
|
|
|
// annoyingly, alacritty (well, branches of alacritty) supports Sixel, but
|
|
// does not indicate this in their Primary Device Attributes response (there
|
|
// is no room for attributes in a VT102-style DA1, which alacritty uses).
|
|
// so, iff we've determined we're alacritty, don't scrub out Sixel details.
|
|
static int
|
|
da1_vt102_cb(inputctx* ictx){
|
|
loginfo("read primary device attributes");
|
|
if(ictx->initdata){
|
|
if(ictx->initdata->qterm != TERMINAL_ALACRITTY){
|
|
scrub_sixel_responses(ictx->initdata);
|
|
}
|
|
handoff_initial_responses_early(ictx);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
da1_cb(inputctx* ictx){
|
|
loginfo("read primary device attributes");
|
|
if(ictx->initdata){
|
|
scrub_sixel_responses(ictx->initdata);
|
|
handoff_initial_responses_early(ictx);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
da1_attrs_cb(inputctx* ictx){
|
|
loginfo("read primary device attributes");
|
|
unsigned val = amata_next_numeric(&ictx->amata, "\x1b[?", ';');
|
|
char* attrlist = amata_next_kleene(&ictx->amata, "", 'c');
|
|
logdebug("DA1: %u [%s]", val, attrlist);
|
|
if(ictx->initdata){
|
|
int foundsixel = 0;
|
|
unsigned curattr = 0;
|
|
for(const char* a = attrlist ; *a ; ++a){
|
|
if(isdigit(*a)){
|
|
curattr *= 10;
|
|
curattr += *a - '0';
|
|
}else if(*a == ';'){
|
|
if(curattr == 4){
|
|
foundsixel = 1;
|
|
if(ictx->initdata->color_registers <= 0){
|
|
ictx->initdata->color_registers = 256;
|
|
}
|
|
}else if(curattr == 28){
|
|
ictx->initdata->rectangular_edits = true;
|
|
}
|
|
curattr = 0;
|
|
}
|
|
}
|
|
if(curattr == 4){
|
|
foundsixel = 1;
|
|
if(ictx->initdata->color_registers <= 0){
|
|
ictx->initdata->color_registers = 256;
|
|
}
|
|
}else if(curattr == 28){
|
|
ictx->initdata->rectangular_edits = true;
|
|
}
|
|
if(!foundsixel){
|
|
scrub_sixel_responses(ictx->initdata);
|
|
}
|
|
handoff_initial_responses_early(ictx);
|
|
}
|
|
free(attrlist);
|
|
return 1;
|
|
}
|
|
|
|
// GNU screen primarily identifies itself via an "83" as the first parameter
|
|
// of its DA2 reply. the version is the second parameter.
|
|
static int
|
|
da2_screen_cb(inputctx* ictx){
|
|
if(ictx->initdata == NULL){
|
|
return 2;
|
|
}
|
|
if(ictx->initdata->qterm != TERMINAL_UNKNOWN){
|
|
logwarn("already identified term (%d)", ictx->initdata->qterm);
|
|
return 2;
|
|
}
|
|
unsigned ver = amata_next_numeric(&ictx->amata, "\x1b[>83;", ';');
|
|
if(ver < 10000){
|
|
logwarn("version %u doesn't look like GNU screen", ver);
|
|
return 2;
|
|
}
|
|
char verstr[9]; // three two-digit components plus two delims
|
|
int s = snprintf(verstr, sizeof(verstr), "%u.%02u.%02u",
|
|
ver / 10000, ver / 100 % 100, ver % 100);
|
|
if(s < 0 || (unsigned)s >= sizeof(verstr)){
|
|
logwarn("bad screen version %u", ver);
|
|
return 2;
|
|
}
|
|
ictx->initdata->version = strdup(verstr);
|
|
ictx->initdata->qterm = TERMINAL_GNUSCREEN;
|
|
return 2;
|
|
}
|
|
|
|
// we use secondary device attributes to recognize the alacritty crate
|
|
// version, and to ascertain the version of old, pre-XTVERSION XTerm.
|
|
static int
|
|
da2_cb(inputctx* ictx){
|
|
loginfo("read secondary device attributes");
|
|
if(ictx->initdata == NULL){
|
|
return 2;
|
|
}
|
|
amata_next_numeric(&ictx->amata, "\x1b[>", ';');
|
|
unsigned pv = amata_next_numeric(&ictx->amata, "", ';');
|
|
int maj, min, patch;
|
|
if(pv == 0){
|
|
return 2;
|
|
}
|
|
// modern XTerm replies to XTVERSION, but older versions require extracting
|
|
// the version from secondary DA
|
|
if(ictx->initdata->qterm == TERMINAL_XTERM){
|
|
if(ictx->initdata->version == NULL){
|
|
char ver[8];
|
|
int s = snprintf(ver, sizeof(ver), "%u", pv);
|
|
if(s < 0 || (unsigned)s >= sizeof(ver)){
|
|
logerror("bad version: %u", pv);
|
|
}else{
|
|
ictx->initdata->version = strdup(ver);
|
|
}
|
|
return 2;
|
|
}
|
|
}
|
|
// SDA yields up Alacritty's crate version, but it doesn't unambiguously
|
|
// identify Alacritty. If we've got any other version information, don't
|
|
// use this. use the second parameter (Pv).
|
|
if(ictx->initdata->qterm != TERMINAL_UNKNOWN || ictx->initdata->version){
|
|
loginfo("termtype was %d %s, not alacritty", ictx->initdata->qterm,
|
|
ictx->initdata->version);
|
|
return 2;
|
|
}
|
|
// if a termname was manually supplied in setup, it was written to the env
|
|
const char* termname = getenv("TERM");
|
|
if(termname == NULL || strstr(termname, "alacritty") == NULL){
|
|
loginfo("termname was [%s], probably not alacritty",
|
|
termname ? termname : "unset");
|
|
return 2;
|
|
}
|
|
maj = pv / 10000;
|
|
min = (pv % 10000) / 100;
|
|
patch = pv % 100;
|
|
if(maj >= 100 || min >= 100 || patch >= 100){
|
|
return 2;
|
|
}
|
|
// 3x components (two digits max each), 2x '.', NUL would suggest 9 bytes,
|
|
// but older gcc __builtin___sprintf_chk insists on 13. fuck it. FIXME.
|
|
char* buf = malloc(13);
|
|
if(buf){
|
|
sprintf(buf, "%d.%d.%d", maj, min, patch);
|
|
loginfo("might be alacritty %s", buf);
|
|
ictx->initdata->version = buf;
|
|
ictx->initdata->qterm = TERMINAL_ALACRITTY;
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
// weird form of Ternary Device Attributes used only by WezTerm
|
|
static int
|
|
wezterm_tda_cb(inputctx* ictx){
|
|
if(ictx->initdata){
|
|
loginfo("read ternary device attributes");
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
kittygraph_cb(inputctx* ictx){
|
|
loginfo("kitty graphics message");
|
|
if(ictx->initdata){
|
|
ictx->initdata->kitty_graphics = 1;
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
decrpm_pixelmice(inputctx* ictx){
|
|
unsigned ps = amata_next_numeric(&ictx->amata, "\x1b[?1016;", '$');
|
|
loginfo("received decrpm 1016 %u", ps);
|
|
if(ps == 2){
|
|
if(ictx->initdata){
|
|
ictx->initdata->pixelmice = 1;
|
|
}
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
decrpm_asu_cb(inputctx* ictx){
|
|
unsigned ps = amata_next_numeric(&ictx->amata, "\x1b[?2026;", '$');
|
|
loginfo("received decrpm 2026 %u", ps);
|
|
if(ps == 2){
|
|
if(ictx->initdata){
|
|
ictx->initdata->appsync_supported = 1;
|
|
}
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
get_default_color(const char* str, uint32_t* color){
|
|
int r, g, b;
|
|
if(sscanf(str, "%02x/%02x/%02x", &r, &g, &b) == 3){
|
|
// great! =]
|
|
}else if(sscanf(str, "%04x/%04x/%04x", &r, &g, &b) == 3){
|
|
r /= 256;
|
|
g /= 256;
|
|
b /= 256;
|
|
}else{
|
|
logerror("couldn't extract rgb from %s", str);
|
|
return -1;
|
|
}
|
|
if(r < 0 || g < 0 || b < 0){
|
|
logerror("invalid colors %d %d %d", r, g, b);
|
|
return -1;
|
|
}
|
|
*color = (r << 16u) | (g << 8u) | b;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
bgdef_cb(inputctx* ictx){
|
|
if(ictx->initdata){
|
|
char* str = amata_next_string(&ictx->amata, "\x1b]11;rgb:");
|
|
if(str == NULL){
|
|
logerror("empty bg string");
|
|
}else{
|
|
if(get_default_color(str, &ictx->initdata->bg) == 0){
|
|
ictx->initdata->got_bg = true;
|
|
loginfo("default background 0x%06x", ictx->initdata->bg);
|
|
}
|
|
free(str);
|
|
}
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
fgdef_cb(inputctx* ictx){
|
|
if(ictx->initdata){
|
|
char* str = amata_next_string(&ictx->amata, "\x1b]10;rgb:");
|
|
if(str == NULL){
|
|
logerror("empty fg string");
|
|
}else{
|
|
if(get_default_color(str, &ictx->initdata->fg) == 0){
|
|
ictx->initdata->got_fg = true;
|
|
loginfo("default foreground 0x%06x", ictx->initdata->fg);
|
|
}
|
|
free(str);
|
|
}
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
palette_cb(inputctx* ictx){
|
|
if(ictx->initdata){
|
|
unsigned idx = amata_next_numeric(&ictx->amata, "\x1b]4;", ';');
|
|
char* str = amata_next_string(&ictx->amata, "rgb:");
|
|
if(idx > sizeof(ictx->initdata->palette.chans) / sizeof(*ictx->initdata->palette.chans)){
|
|
logerror("invalid index %u", idx);
|
|
}else if(str == NULL){
|
|
logerror("empty palette string");
|
|
}else{
|
|
if(get_default_color(str, &ictx->initdata->palette.chans[idx]) == 0){
|
|
if((int)idx > ictx->initdata->maxpaletteread){
|
|
ictx->initdata->maxpaletteread = idx;
|
|
}
|
|
logverbose("index %u 0x%06x", idx, ictx->initdata->palette.chans[idx]);
|
|
}
|
|
free(str);
|
|
}
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
extract_xtversion(inputctx* ictx, const char* str, char suffix){
|
|
size_t slen = strlen(str);
|
|
if(slen == 0){
|
|
logwarn("empty version in xtversion");
|
|
return -1;
|
|
}
|
|
if(suffix){
|
|
if(str[slen - 1] != suffix){
|
|
return -1;
|
|
}
|
|
--slen;
|
|
}
|
|
if(slen == 0){
|
|
logwarn("empty version in xtversion");
|
|
return -1;
|
|
}
|
|
ictx->initdata->version = strndup(str, slen);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
xtversion_cb(inputctx* ictx){
|
|
if(ictx->initdata == NULL){
|
|
return 2;
|
|
}
|
|
char* xtversion = amata_next_string(&ictx->amata, "\x1bP>|");
|
|
if(xtversion == NULL){
|
|
logwarn("empty xtversion");
|
|
return 2; // don't replay as input
|
|
}
|
|
static const struct {
|
|
const char* prefix;
|
|
char suffix;
|
|
queried_terminals_e term;
|
|
} xtvers[] = {
|
|
{ .prefix = "XTerm(", .suffix = ')', .term = TERMINAL_XTERM, },
|
|
{ .prefix = "WezTerm ", .suffix = 0, .term = TERMINAL_WEZTERM, },
|
|
{ .prefix = "contour ", .suffix = 0, .term = TERMINAL_CONTOUR, },
|
|
{ .prefix = "kitty(", .suffix = ')', .term = TERMINAL_KITTY, },
|
|
{ .prefix = "foot(", .suffix = ')', .term = TERMINAL_FOOT, },
|
|
{ .prefix = "mlterm(", .suffix = ')', .term = TERMINAL_MLTERM, },
|
|
{ .prefix = "tmux ", .suffix = 0, .term = TERMINAL_TMUX, },
|
|
{ .prefix = "iTerm2 ", .suffix = 0, .term = TERMINAL_ITERM, },
|
|
{ .prefix = "mintty ", .suffix = 0, .term = TERMINAL_MINTTY, },
|
|
{ .prefix = "terminology ", .suffix = 0, .term = TERMINAL_TERMINOLOGY, },
|
|
{ .prefix = NULL, .suffix = 0, .term = TERMINAL_UNKNOWN, },
|
|
}, *xtv;
|
|
for(xtv = xtvers ; xtv->prefix ; ++xtv){
|
|
if(strncmp(xtversion, xtv->prefix, strlen(xtv->prefix)) == 0){
|
|
if(extract_xtversion(ictx, xtversion + strlen(xtv->prefix), xtv->suffix) == 0){
|
|
loginfo("found terminal type %d version %s", xtv->term, ictx->initdata->version);
|
|
ictx->initdata->qterm = xtv->term;
|
|
}else{
|
|
free(xtversion);
|
|
return 2;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if(xtv->prefix == NULL){
|
|
logwarn("unknown xtversion [%s]", xtversion);
|
|
}
|
|
free(xtversion);
|
|
return 2;
|
|
}
|
|
|
|
// precondition: s starts with two hex digits, the first of which is not
|
|
// greater than 7.
|
|
static inline char
|
|
toxdigit(const char* s){
|
|
char c = isalpha(*s) ? tolower(*s) - 'a' + 10 : *s - '0';
|
|
c *= 16;
|
|
++s;
|
|
c += isalpha(*s) ? tolower(*s) - 'a' + 10 : *s - '0';
|
|
return c;
|
|
}
|
|
|
|
// on success, the subsequent character is returned, and |key| and |val| have
|
|
// heap-allocated, decoded, nul-terminated copies of the appropriate input.
|
|
static const char*
|
|
gettcap(const char* s, char** key, char** val){
|
|
const char* equals = s;
|
|
// we don't want anything bigger than 7 in the first nibble
|
|
unsigned firstnibble = true;
|
|
while(*equals != '='){
|
|
if(!isxdigit(*equals)){ // rejects a NUL byte
|
|
logerror("bad key in %s", s);
|
|
return NULL;
|
|
}
|
|
if(firstnibble && (!isdigit(*equals) || *equals - '0' >= 8)){
|
|
logerror("bad key in %s", s);
|
|
return NULL;
|
|
}
|
|
firstnibble = !firstnibble;
|
|
++equals;
|
|
}
|
|
if(equals - s == 0 || !firstnibble){
|
|
logerror("bad key in %s", s);
|
|
return NULL;
|
|
}
|
|
if((*key = malloc((equals - s) / 2 + 1)) == NULL){
|
|
return NULL;
|
|
}
|
|
char* keytarg = *key;
|
|
do{
|
|
*keytarg = toxdigit(s);
|
|
s += 2;
|
|
++keytarg;
|
|
}while(*s != '=');
|
|
*keytarg = '\0';
|
|
++equals; // now one past the equal sign
|
|
const char *end = equals;
|
|
firstnibble = true;
|
|
while(*end != ';' && *end){
|
|
if(!isxdigit(*end)){
|
|
logerror("bad value in %s", s);
|
|
goto valerr;
|
|
}
|
|
if(firstnibble && (!isdigit(*end) || *end - '0' >= 8)){
|
|
logerror("bad value in %s", s);
|
|
goto valerr;
|
|
}
|
|
firstnibble = !firstnibble;
|
|
++end;
|
|
}
|
|
if(end - equals == 0 || !firstnibble){
|
|
logerror("bad value in %s", s);
|
|
goto valerr;
|
|
}
|
|
if((*val = malloc((end - equals) / 2 + 1)) == NULL){
|
|
goto valerr;
|
|
}
|
|
char* valtarg = *val;
|
|
++s;
|
|
do{
|
|
*valtarg = toxdigit(s);
|
|
s += 2;
|
|
++valtarg;
|
|
}while(s != end);
|
|
*valtarg = '\0';
|
|
loginfo("key: %s val: %s", *key, *val);
|
|
return end;
|
|
|
|
valerr:
|
|
free(*key);
|
|
*key = NULL;
|
|
return NULL;
|
|
}
|
|
|
|
// replace \E with actual 0x1b for use as a terminfo-like format string,
|
|
// writing in-place (and updating the nul terminator, if necessary).
|
|
// returns its input.
|
|
static inline char*
|
|
determinfo(char* old){
|
|
bool escaped = false;
|
|
char* targo = old;
|
|
for(char* o = old ; *o ; ++o){
|
|
if(escaped){
|
|
if(*o == 'E'){
|
|
*targo = 0x1b;
|
|
++targo;
|
|
}else{
|
|
*targo = '\\';
|
|
++targo;
|
|
*targo = *o;
|
|
++targo;
|
|
}
|
|
escaped = false;
|
|
}else if(*o == '\\'){
|
|
escaped = true;
|
|
}else{
|
|
*targo = *o;
|
|
++targo;
|
|
}
|
|
}
|
|
*targo = '\0';
|
|
return old;
|
|
}
|
|
|
|
// XTGETTCAP responses are delimited by semicolons
|
|
static int
|
|
tcap_cb(inputctx* ictx){
|
|
char* str = amata_next_string(&ictx->amata, "\x1bP1+r");
|
|
if(str == NULL){
|
|
return 2;
|
|
}
|
|
loginfo("xtgettcap [%s]", str);
|
|
if(ictx->initdata == NULL){
|
|
free(str);
|
|
return 2;
|
|
}
|
|
const char* s = str;
|
|
char* key;
|
|
char* val;
|
|
// answers are delimited with semicolons, hex-encoded, key=value
|
|
while(*s && (s = gettcap(s, &val, &key)) ){
|
|
if(strcmp(val, "TN") == 0){
|
|
if(ictx->initdata->qterm == TERMINAL_UNKNOWN){
|
|
if(strcmp(key, "xterm") == 0){
|
|
ictx->initdata->qterm = TERMINAL_XTERM;
|
|
}else if(strcmp(key, "mlterm") == 0){
|
|
ictx->initdata->qterm = TERMINAL_MLTERM;
|
|
}else if(strcmp(key, "xterm-kitty") == 0){
|
|
ictx->initdata->qterm = TERMINAL_KITTY;
|
|
}else if(strcmp(key, "xterm-256color") == 0){
|
|
ictx->initdata->qterm = TERMINAL_XTERM;
|
|
}else{
|
|
logdebug("unknown terminal name %s", key);
|
|
}
|
|
}
|
|
}else if(strcmp(val, "RGB") == 0){
|
|
loginfo("got rgb (%s)", s);
|
|
ictx->initdata->rgb = true;
|
|
}else if(strcmp(val, "hpa") == 0){
|
|
loginfo("got hpa (%s)", key);
|
|
ictx->initdata->hpa = determinfo(key);
|
|
key = NULL;
|
|
}else{
|
|
logwarn("unknown capability: %s", str);
|
|
}
|
|
free(val);
|
|
free(key);
|
|
if(*s == ';'){
|
|
++s;
|
|
}
|
|
}
|
|
if(!s || *s){
|
|
free(str);
|
|
return -1;
|
|
}
|
|
free(str);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
tda_cb(inputctx* ictx){
|
|
char* str = amata_next_string(&ictx->amata, "\x1bP!|");
|
|
if(str == NULL){
|
|
logwarn("empty ternary device attribute");
|
|
return 2; // don't replay
|
|
}
|
|
if(ictx->initdata && ictx->initdata->qterm == TERMINAL_UNKNOWN){
|
|
if(strcmp(str, "7E565445") == 0){ // "~VTE"
|
|
ictx->initdata->qterm = TERMINAL_VTE;
|
|
}else if(strcmp(str, "7E7E5459") == 0){ // "~~TY"
|
|
ictx->initdata->qterm = TERMINAL_TERMINOLOGY;
|
|
}else if(strcmp(str, "464F4F54") == 0){ // "FOOT"
|
|
ictx->initdata->qterm = TERMINAL_FOOT;
|
|
}else if(strcmp(str, "7E4B4445") == 0){
|
|
ictx->initdata->qterm = TERMINAL_KONSOLE;
|
|
}
|
|
loginfo("got TDA: %s, terminal type %d", str, ictx->initdata->qterm);
|
|
}
|
|
free(str);
|
|
return 2;
|
|
}
|
|
|
|
static int
|
|
build_cflow_automaton(inputctx* ictx){
|
|
// syntax: literals are matched. \N is a numeric. \D is a drain (Kleene
|
|
// closure). \S is a ST-terminated string. this working is very dependent on
|
|
// order, and very delicate! hands off!
|
|
const struct {
|
|
const char* cflow;
|
|
triefunc fxn;
|
|
} csis[] = {
|
|
// CSI (\e[)
|
|
{ "[E", simple_cb_begin, },
|
|
{ "[<\\N;\\N;\\NM", mouse_press_cb, },
|
|
{ "[<\\N;\\N;\\Nm", mouse_release_cb, },
|
|
// technically these must begin with "4" or "8"; enforce in callbacks
|
|
{ "[\\N;\\N;\\Nt", geom_cb, },
|
|
{ "[\\Nu", kitty_cb_simple, },
|
|
{ "[\\N;\\N~", wezterm_cb, },
|
|
{ "[\\N;\\Nu", kitty_cb, },
|
|
{ "[\\N;\\N;\\Nu", kitty_cb_atxt1, },
|
|
{ "[\\N;\\N;\\N;\\Nu", kitty_cb_atxt2, },
|
|
{ "[\\N;\\N;\\N;\\N;\\Nu", kitty_cb_atxt3, },
|
|
{ "[\\N;\\N;\\N;\\N;\\N;\\Nu", kitty_cb_atxt4, },
|
|
{ "[\\N;\\N:\\Nu", kitty_cb_complex, },
|
|
{ "[\\N;\\N:\\N;\\Nu", kitty_cb_complex_atxt1, },
|
|
{ "[\\N;\\N:\\N;\\N;\\Nu", kitty_cb_complex_atxt2, },
|
|
{ "[\\N;\\N:\\N;\\N;\\N;\\Nu", kitty_cb_complex_atxt3, },
|
|
{ "[\\N;\\N:\\N;\\N;\\N;\\N;\\Nu", kitty_cb_complex_atxt4, },
|
|
{ "[\\N;\\N;\\N~", xtmodkey_cb, },
|
|
{ "[\\N;\\N:\\N~", kitty_cb_functional, },
|
|
{ "[1;\\NP", legacy_cb_f1, },
|
|
{ "[1;\\NQ", legacy_cb_f2, },
|
|
{ "[1;\\NS", legacy_cb_f4, },
|
|
{ "[1;\\ND", legacy_cb_left, },
|
|
{ "[1;\\NC", legacy_cb_right, },
|
|
{ "[1;\\NB", legacy_cb_down, },
|
|
{ "[1;\\NA", legacy_cb_up, },
|
|
{ "[1;\\NE", legacy_cb_begin, },
|
|
{ "[1;\\NF", legacy_cb_end, },
|
|
{ "[1;\\NH", legacy_cb_home, },
|
|
{ "[1;\\N:\\NP", kitty_cb_f1, },
|
|
{ "[1;\\N:\\NQ", kitty_cb_f2, },
|
|
{ "[1;\\N:\\NR", kitty_cb_f3, },
|
|
{ "[1;\\N:\\NS", kitty_cb_f4, },
|
|
{ "[1;\\N:\\ND", kitty_cb_left, },
|
|
{ "[1;\\N:\\NC", kitty_cb_right, },
|
|
{ "[1;\\N:\\NB", kitty_cb_down, },
|
|
{ "[1;\\N:\\NA", kitty_cb_up, },
|
|
{ "[1;\\N:\\NE", kitty_cb_begin, },
|
|
{ "[1;\\N:\\NF", kitty_cb_end, },
|
|
{ "[1;\\N:\\NH", kitty_cb_home, },
|
|
{ "[?\\Nu", kitty_keyboard_cb, },
|
|
{ "[?1016;\\N$y", decrpm_pixelmice, },
|
|
{ "[?2026;\\N$y", decrpm_asu_cb, },
|
|
{ "[\\N;\\NR", cursor_location_cb, },
|
|
{ "[?1;1S", NULL, }, // negative cregs XTSMGRAPHICS
|
|
{ "[?1;2S", NULL, }, // negative cregs XTSMGRAPHICS
|
|
{ "[?1;3S", NULL, }, // negative cregs XTSMGRAPHICS
|
|
{ "[?1;3;S", NULL, }, // iterm2 negative cregs XTSMGRAPHICS
|
|
{ "[?1;3;0S", NULL, }, // negative cregs XTSMGRAPHICS
|
|
{ "[?2;1S", NULL, }, // negative pixels XTSMGRAPHICS
|
|
{ "[?2;2S", NULL, }, // negative pixels XTSMGRAPHICS
|
|
{ "[?2;3S", NULL, }, // negative pixels XTSMGRAPHICS
|
|
{ "[?2;3;S", NULL, }, // iterm2 negative pixels XTSMGRAPHICS
|
|
{ "[?2;3;0S", NULL, }, // negative pixels XTSMGRAPHICS
|
|
{ "[?6c", da1_vt102_cb, }, // CSI ? 6 c ("VT102")
|
|
{ "[?7c", da1_cb, }, // CSI ? 7 c ("VT131")
|
|
{ "[?1;0c", da1_cb, }, // CSI ? 1 ; 0 c ("VT101 with No Options")
|
|
{ "[?1;2c", da1_cb, }, // CSI ? 1 ; 2 c ("VT100 with Advanced Video Option")
|
|
{ "[?4;6c", da1_cb, }, // CSI ? 4 ; 6 c ("VT132 with Advanced Video and Graphics")
|
|
// CSI ? 1 2 ; Ps c ("VT125")
|
|
// CSI ? 6 0 ; Ps c (kmscon)
|
|
// CSI ? 6 2 ; Ps c ("VT220")
|
|
// CSI ? 6 3 ; Ps c ("VT320")
|
|
// CSI ? 6 4 ; Ps c ("VT420")
|
|
// CSI ? 6 5 ; Ps c (WezTerm, VT5xx?)
|
|
{ "[?\\N;\\Dc", da1_attrs_cb, },
|
|
{ "[?1;0;\\NS", xtsmgraphics_cregs_cb, },
|
|
{ "[?2;0;\\N;\\NS", xtsmgraphics_sixel_cb, },
|
|
{ "[>83;\\N;0c", da2_screen_cb, },
|
|
{ "[>\\N;\\N;\\Nc", da2_cb, },
|
|
{ "[=\\Sc", wezterm_tda_cb, }, // CSI da3 form as issued by WezTerm
|
|
// DCS (\eP...ST)
|
|
{ "P0+\\S", NULL, }, // negative XTGETTCAP
|
|
{ "P1+r\\S", tcap_cb, }, // positive XTGETTCAP
|
|
{ "P!|\\S", tda_cb, }, // DCS da3 form used by XTerm
|
|
{ "P>|\\S", xtversion_cb, },
|
|
// OSC (\e_...ST)
|
|
{ "_G\\S", kittygraph_cb, },
|
|
// a mystery to everyone!
|
|
{ "]10;rgb:\\S", fgdef_cb, },
|
|
{ "]11;rgb:\\S", bgdef_cb, },
|
|
{ NULL, NULL, },
|
|
}, *csi;
|
|
for(csi = csis ; csi->cflow ; ++csi){
|
|
if(inputctx_add_cflow(&ictx->amata, csi->cflow, csi->fxn)){
|
|
logerror("failed adding %p via %s", csi->fxn, csi->cflow);
|
|
return -1;
|
|
}
|
|
loginfo("added %p via %s", csi->fxn, csi->cflow);
|
|
}
|
|
if(ictx->ti->qterm == TERMINAL_RXVT){
|
|
if(inputctx_add_cflow(&ictx->amata, "]4;\\N;rgb:\\R", palette_cb)){
|
|
logerror("failed adding palette_cb");
|
|
return -1;
|
|
}
|
|
}else{
|
|
if(inputctx_add_cflow(&ictx->amata, "]4;\\N;rgb:\\S", palette_cb)){
|
|
logerror("failed adding palette_cb");
|
|
return -1;
|
|
}
|
|
// handle old-style contour responses, though we can't make use of them
|
|
if(inputctx_add_cflow(&ictx->amata, "]4;rgb:\\S", palette_cb)){
|
|
logerror("failed adding palette_cb");
|
|
return -1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
closepipe(ipipe p){
|
|
#ifndef __MINGW32__
|
|
if(p >= 0){
|
|
close(p);
|
|
}
|
|
#else
|
|
if(p){
|
|
CloseHandle(p);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void
|
|
endpipes(ipipe pipes[static 2]){
|
|
closepipe(pipes[0]);
|
|
closepipe(pipes[1]);
|
|
}
|
|
|
|
// only linux and freebsd13+ have eventfd(), so we'll fall back to pipes sigh.
|
|
static int
|
|
getpipes(ipipe pipes[static 2]){
|
|
#ifndef __MINGW32__
|
|
#ifndef __APPLE__
|
|
if(pipe2(pipes, O_CLOEXEC | O_NONBLOCK)){
|
|
logerror("couldn't get pipes (%s)", strerror(errno));
|
|
return -1;
|
|
}
|
|
#else
|
|
if(pipe(pipes)){
|
|
logerror("couldn't get pipes (%s)", strerror(errno));
|
|
return -1;
|
|
}
|
|
if(set_fd_cloexec(pipes[0], 1, NULL) || set_fd_nonblocking(pipes[0], 1, NULL)){
|
|
logerror("couldn't prep pipe[0] (%s)", strerror(errno));
|
|
endpipes(pipes);
|
|
return -1;
|
|
}
|
|
if(set_fd_cloexec(pipes[1], 1, NULL) || set_fd_nonblocking(pipes[1], 1, NULL)){
|
|
logerror("couldn't prep pipe[1] (%s)", strerror(errno));
|
|
endpipes(pipes);
|
|
return -1;
|
|
}
|
|
#endif
|
|
#else // windows
|
|
if(!CreatePipe(&pipes[0], &pipes[1], NULL, BUFSIZ)){
|
|
logerror("couldn't get pipes");
|
|
return -1;
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
static inline inputctx*
|
|
create_inputctx(tinfo* ti, FILE* infp, int lmargin, int tmargin, int rmargin,
|
|
int bmargin, ncsharedstats* stats, unsigned drain,
|
|
int linesigs_enabled){
|
|
bool sent_queries = (ti->ttyfd >= 0) ? true : false;
|
|
inputctx* i = malloc(sizeof(*i));
|
|
if(i){
|
|
i->csize = 64;
|
|
if( (i->csrs = malloc(sizeof(*i->csrs) * i->csize)) ){
|
|
i->isize = BUFSIZ;
|
|
if( (i->inputs = malloc(sizeof(*i->inputs) * i->isize)) ){
|
|
if(pthread_mutex_init(&i->ilock, NULL) == 0){
|
|
if(pthread_condmonotonic_init(&i->icond) == 0){
|
|
if(pthread_mutex_init(&i->clock, NULL) == 0){
|
|
if(pthread_condmonotonic_init(&i->ccond) == 0){
|
|
if((i->stdinfd = fileno(infp)) >= 0){
|
|
if( (i->initdata = malloc(sizeof(*i->initdata))) ){
|
|
if(getpipes(i->readypipes) == 0){
|
|
if(getpipes(i->ipipes) == 0){
|
|
memset(&i->amata, 0, sizeof(i->amata));
|
|
if(prep_special_keys(i) == 0){
|
|
if(set_fd_nonblocking(i->stdinfd, 1, &ti->stdio_blocking_save) == 0){
|
|
i->termfd = tty_check(i->stdinfd) ? -1 : get_tty_fd(infp);
|
|
memset(i->initdata, 0, sizeof(*i->initdata));
|
|
if(sent_queries){
|
|
i->coutstanding = 1; // one in initial request set
|
|
i->initdata->qterm = ti->qterm;
|
|
i->initdata->cursory = -1;
|
|
i->initdata->cursorx = -1;
|
|
i->initdata->maxpaletteread = -1;
|
|
i->initdata->kbdlevel = UINT_MAX;
|
|
}else{
|
|
free(i->initdata);
|
|
i->initdata = NULL;
|
|
i->coutstanding = 0;
|
|
}
|
|
i->kittykbd = 0;
|
|
i->iread = i->iwrite = i->ivalid = 0;
|
|
i->cread = i->cwrite = i->cvalid = 0;
|
|
i->initdata_complete = NULL;
|
|
i->stats = stats;
|
|
i->ti = ti;
|
|
i->stdineof = 0;
|
|
#ifdef __MINGW32__
|
|
i->stdinhandle = ti->inhandle;
|
|
#endif
|
|
i->ibufvalid = 0;
|
|
i->linesigs = linesigs_enabled;
|
|
i->tbufvalid = 0;
|
|
i->midescape = 0;
|
|
i->lmargin = lmargin;
|
|
i->tmargin = tmargin;
|
|
i->rmargin = rmargin;
|
|
i->bmargin = bmargin;
|
|
i->drain = drain;
|
|
i->failed = false;
|
|
logdebug("input descriptors: %d/%d", i->stdinfd, i->termfd);
|
|
return i;
|
|
}
|
|
}
|
|
input_free_esctrie(&i->amata);
|
|
}
|
|
endpipes(i->ipipes);
|
|
}
|
|
endpipes(i->readypipes);
|
|
}
|
|
free(i->initdata);
|
|
}
|
|
pthread_cond_destroy(&i->ccond);
|
|
}
|
|
pthread_mutex_destroy(&i->clock);
|
|
}
|
|
pthread_cond_destroy(&i->icond);
|
|
}
|
|
pthread_mutex_destroy(&i->ilock);
|
|
}
|
|
free(i->inputs);
|
|
}
|
|
free(i->csrs);
|
|
}
|
|
free(i);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static inline void
|
|
free_inputctx(inputctx* i){
|
|
if(i){
|
|
// we *do not* own stdinfd; don't close() it! we do own termfd.
|
|
if(i->termfd >= 0){
|
|
close(i->termfd);
|
|
}
|
|
pthread_mutex_destroy(&i->ilock);
|
|
pthread_cond_destroy(&i->icond);
|
|
pthread_mutex_destroy(&i->clock);
|
|
pthread_cond_destroy(&i->ccond);
|
|
input_free_esctrie(&i->amata);
|
|
// do not kill the thread here, either.
|
|
if(i->initdata){
|
|
free(i->initdata->version);
|
|
free(i->initdata);
|
|
}else if(i->initdata_complete){
|
|
free(i->initdata_complete->version);
|
|
free(i->initdata_complete);
|
|
}
|
|
endpipes(i->readypipes);
|
|
endpipes(i->ipipes);
|
|
free(i->inputs);
|
|
free(i->csrs);
|
|
free(i);
|
|
}
|
|
}
|
|
|
|
// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions
|
|
static int
|
|
prep_kitty_special_keys(inputctx* ictx){
|
|
// we do not list here those already handled by prep_windows_special_keys()
|
|
static const struct {
|
|
const char* esc;
|
|
uint32_t key;
|
|
unsigned modifiers;
|
|
} keys[] = {
|
|
{ .esc = "\x1b[P", .key = NCKEY_F01, },
|
|
{ .esc = "\x1b[Q", .key = NCKEY_F02, },
|
|
{ .esc = "\x1b[R", .key = NCKEY_F03, },
|
|
{ .esc = "\x1b[S", .key = NCKEY_F04, },
|
|
{ .esc = "\x1b[127;2u", .key = NCKEY_BACKSPACE,
|
|
.modifiers = NCKEY_MOD_SHIFT, },
|
|
{ .esc = "\x1b[127;3u", .key = NCKEY_BACKSPACE,
|
|
.modifiers = NCKEY_MOD_ALT, },
|
|
{ .esc = "\x1b[127;5u", .key = NCKEY_BACKSPACE,
|
|
.modifiers = NCKEY_MOD_CTRL, },
|
|
{ .esc = NULL, .key = 0, },
|
|
}, *k;
|
|
for(k = keys ; k->esc ; ++k){
|
|
if(inputctx_add_input_escape(&ictx->amata, k->esc, k->key, k->modifiers)){
|
|
return -1;
|
|
}
|
|
}
|
|
loginfo("added all kitty special keys");
|
|
return 0;
|
|
}
|
|
|
|
// add the hardcoded windows input sequences to ti->input. should only
|
|
// be called after verifying that this is TERMINAL_MSTERMINAL.
|
|
static int
|
|
prep_windows_special_keys(inputctx* ictx){
|
|
// here, lacking terminfo, we hardcode the sequences. they can be found at
|
|
// https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
|
|
// under the "Input Sequences" heading.
|
|
static const struct {
|
|
const char* esc;
|
|
uint32_t key;
|
|
unsigned modifiers;
|
|
} keys[] = {
|
|
{ .esc = "\x1b[A", .key = NCKEY_UP, },
|
|
{ .esc = "\x1b[B", .key = NCKEY_DOWN, },
|
|
{ .esc = "\x1b[C", .key = NCKEY_RIGHT, },
|
|
{ .esc = "\x1b[D", .key = NCKEY_LEFT, },
|
|
{ .esc = "\x1b[1;5A", .key = NCKEY_UP,
|
|
.modifiers = NCKEY_MOD_CTRL, },
|
|
{ .esc = "\x1b[1;5B", .key = NCKEY_DOWN,
|
|
.modifiers = NCKEY_MOD_CTRL, },
|
|
{ .esc = "\x1b[1;5C", .key = NCKEY_RIGHT,
|
|
.modifiers = NCKEY_MOD_CTRL, },
|
|
{ .esc = "\x1b[1;5D", .key = NCKEY_LEFT,
|
|
.modifiers = NCKEY_MOD_CTRL, },
|
|
{ .esc = "\x1b[H", .key = NCKEY_HOME, },
|
|
{ .esc = "\x1b[F", .key = NCKEY_END, },
|
|
{ .esc = "\x1b[2~", .key = NCKEY_INS, },
|
|
{ .esc = "\x1b[3~", .key = NCKEY_DEL, },
|
|
{ .esc = "\x1b[5~", .key = NCKEY_PGUP, },
|
|
{ .esc = "\x1b[6~", .key = NCKEY_PGDOWN, },
|
|
{ .esc = "\x1bOP", .key = NCKEY_F01, },
|
|
{ .esc = "\x1bOQ", .key = NCKEY_F02, },
|
|
{ .esc = "\x1bOR", .key = NCKEY_F03, },
|
|
{ .esc = "\x1bOS", .key = NCKEY_F04, },
|
|
{ .esc = "\x1b[15~", .key = NCKEY_F05, },
|
|
{ .esc = "\x1b[17~", .key = NCKEY_F06, },
|
|
{ .esc = "\x1b[18~", .key = NCKEY_F07, },
|
|
{ .esc = "\x1b[19~", .key = NCKEY_F08, },
|
|
{ .esc = "\x1b[20~", .key = NCKEY_F09, },
|
|
{ .esc = "\x1b[21~", .key = NCKEY_F10, },
|
|
{ .esc = "\x1b[23~", .key = NCKEY_F11, },
|
|
{ .esc = "\x1b[24~", .key = NCKEY_F12, },
|
|
{ .esc = NULL, .key = 0, },
|
|
}, *k;
|
|
for(k = keys ; k->esc ; ++k){
|
|
if(inputctx_add_input_escape(&ictx->amata, k->esc, k->key, k->modifiers)){
|
|
return -1;
|
|
}
|
|
logdebug("added %s %u", k->esc, k->key);
|
|
}
|
|
loginfo("added all windows special keys");
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
prep_all_keys(inputctx* ictx){
|
|
if(prep_windows_special_keys(ictx)){
|
|
return -1;
|
|
}
|
|
if(prep_kitty_special_keys(ictx)){
|
|
return -1;
|
|
}
|
|
if(prep_xtmodkeys(ictx)){
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// populate |buf| with any new data from the specified file descriptor |fd|.
|
|
static void
|
|
read_input_nblock(int fd, unsigned char* buf, size_t buflen, int *bufused,
|
|
unsigned* goteof){
|
|
if(fd < 0){
|
|
return;
|
|
}
|
|
size_t space = buflen - *bufused;
|
|
if(space == 0){
|
|
return;
|
|
}
|
|
ssize_t r = read(fd, buf + *bufused, space);
|
|
if(r <= 0){
|
|
if(r < 0 && (errno != EAGAIN && errno != EBUSY && errno == EWOULDBLOCK)){
|
|
logwarn("couldn't read from %d (%s)", fd, strerror(errno));
|
|
}else{
|
|
if(r < 0){
|
|
logerror("error reading from %d (%s)", fd, strerror(errno));
|
|
}else{
|
|
logwarn("got EOF on %d", fd);
|
|
}
|
|
if(goteof){
|
|
*goteof = 1;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
*bufused += r;
|
|
space -= r;
|
|
loginfo("read %" PRIdPTR "B from %d (%" PRIuPTR "B left)", r, fd, space);
|
|
}
|
|
|
|
// are terminal and stdin distinct for this inputctx?
|
|
static inline bool
|
|
ictx_independent_p(const inputctx* ictx){
|
|
return ictx->termfd >= 0;
|
|
}
|
|
|
|
// try to lex a single control sequence off of buf. return the number of bytes
|
|
// consumed if we do so. otherwise, return the negative number of bytes
|
|
// examined. set ictx->midescape if we're uncertain. we preserve a->used,
|
|
// a->state, etc. across runs to avoid reprocessing. buf is almost certainly
|
|
// *not* NUL-terminated.
|
|
//
|
|
// our rule is: an escape must arrive as a single unit to be interpreted as
|
|
// an escape. this is most relevant for Alt+keypress (Esc followed by the
|
|
// character), which is ambiguous with regards to pressing 'Escape' followed
|
|
// by some other character. if they arrive together, we consider it to be
|
|
// the escape. we might need to allow more than one process_escape call,
|
|
// however, in case the escape ended the previous read buffer.
|
|
// precondition: buflen >= 1. precondition: buf[0] == 0x1b.
|
|
static int
|
|
process_escape(inputctx* ictx, const unsigned char* buf, int buflen){
|
|
assert(ictx->amata.used <= buflen);
|
|
while(ictx->amata.used < buflen){
|
|
unsigned char candidate = buf[ictx->amata.used++];
|
|
unsigned used = ictx->amata.used;
|
|
if(candidate >= 0x80){
|
|
ictx->amata.used = 0;
|
|
return -(used - 1);
|
|
}
|
|
// an escape always resets the trie (unless we're in the middle of an
|
|
// ST-terminated string), as does a NULL transition.
|
|
if(candidate == NCKEY_ESC && !ictx->amata.instring){
|
|
ictx->amata.matchstart = buf + ictx->amata.used - 1;
|
|
ictx->amata.state = ictx->amata.escapes;
|
|
logtrace("initialized automaton to %u", ictx->amata.state);
|
|
ictx->amata.used = 1;
|
|
if(used > 1){ // we got reset; replay as input
|
|
return -(used - 1);
|
|
}
|
|
// validated first byte as escape! keep going. otherwise, check trie.
|
|
// we can safely check trie[candidate] above because we are either coming
|
|
// off the initial node, which definitely has a valid ->trie, or we're
|
|
// coming from a transition, where ictx->triepos->trie is checked below.
|
|
}else{
|
|
ncinput ni = {0};
|
|
int w = walk_automaton(&ictx->amata, ictx, candidate, &ni);
|
|
logdebug("walk result on %u (%c): %d %u", candidate,
|
|
isprint(candidate) ? candidate : ' ', w, ictx->amata.state);
|
|
if(w > 0){
|
|
if(ni.id){
|
|
load_ncinput(ictx, &ni);
|
|
}
|
|
ictx->amata.used = 0;
|
|
return used;
|
|
}else if(w < 0){
|
|
// all inspected characters are invalid; return full negative "used"
|
|
ictx->amata.used = 0;
|
|
return -used;
|
|
}
|
|
}
|
|
}
|
|
// we exhausted input without knowing whether or not this is a valid control
|
|
// sequence; we're still on-trie, and need more (immediate) input.
|
|
ictx->midescape = 1;
|
|
return -ictx->amata.used;
|
|
}
|
|
|
|
// process as many control sequences from |buf|, having |bufused| bytes,
|
|
// as we can. this text needn't be valid UTF-8. this is always called on
|
|
// tbuf; if we find bulk data here, we need replay it into ibuf (assuming
|
|
// that there's room).
|
|
static void
|
|
process_escapes(inputctx* ictx, unsigned char* buf, int* bufused){
|
|
int offset = 0;
|
|
while(*bufused){
|
|
int consumed = process_escape(ictx, buf + offset, *bufused);
|
|
// negative |consumed| means either that we're not sure whether it's an
|
|
// escape, or it definitely is not.
|
|
if(consumed < 0){
|
|
// if midescape is not set, the negative return means invalid escape.
|
|
// replay it to the bulk input buffer; our automaton will have been reset.
|
|
if(!ictx->midescape){
|
|
consumed = -consumed;
|
|
int available = sizeof(ictx->ibuf) - ictx->ibufvalid;
|
|
if(available){
|
|
if(available > consumed){
|
|
available = consumed;
|
|
}
|
|
logwarn("replaying %dB of %dB to ibuf", available, consumed);
|
|
memcpy(ictx->ibuf + ictx->ibufvalid, buf + offset, available);
|
|
ictx->ibufvalid += available;
|
|
}
|
|
offset += consumed;
|
|
ictx->midescape = 0;
|
|
*bufused -= consumed;
|
|
assert(0 <= *bufused);
|
|
}else{
|
|
break;
|
|
}
|
|
}
|
|
*bufused -= consumed;
|
|
offset += consumed;
|
|
assert(0 <= *bufused);
|
|
}
|
|
// move any leftovers to the front; only happens if we fill output queue,
|
|
// or ran out of input data mid-escape
|
|
if(*bufused){
|
|
ictx->amata.matchstart = buf;
|
|
memmove(buf, buf + offset, *bufused);
|
|
}
|
|
}
|
|
|
|
// precondition: buflen >= 1. attempts to consume UTF8 input from buf. the
|
|
// expected length of a UTF8 character can be determined from its first byte.
|
|
// if we don't have that much data, return 0 and read more. if we determine
|
|
// an error, return -1 to consume 1 byte, restarting the UTF8 lex on the next
|
|
// byte. on a valid UTF8 character, set up the ncinput and return its length.
|
|
static int
|
|
process_input(const unsigned char* buf, int buflen, ncinput* ni){
|
|
assert(1 <= buflen);
|
|
memset(ni, 0, sizeof(*ni));
|
|
const int cpointlen = utf8_codepoint_length(*buf);
|
|
if(cpointlen <= 0){
|
|
logwarn("invalid UTF8 initiator on input (0x%02x)", *buf);
|
|
return -1;
|
|
}else if(cpointlen == 1){ // pure ascii can't show up mid-utf8-character
|
|
ni->id = buf[0];
|
|
return 1;
|
|
}
|
|
if(cpointlen > buflen){
|
|
logwarn("utf8 character (%dB) broken across read", cpointlen);
|
|
return 0; // need read more data; we don't have the complete character
|
|
}
|
|
wchar_t w;
|
|
mbstate_t mbstate = {0};
|
|
//fprintf(stderr, "CANDIDATE: %d cpointlen: %zu cpoint: %d\n", candidate, cpointlen, cpoint[cpointlen]);
|
|
// FIXME how the hell does this work with 16-bit wchar_t?
|
|
size_t r = mbrtowc(&w, (const char*)buf, cpointlen, &mbstate);
|
|
if(r == (size_t)-1 || r == (size_t)-2){
|
|
logerror("invalid utf8 prefix (%dB) on input", cpointlen);
|
|
return -1;
|
|
}
|
|
ni->id = w;
|
|
return cpointlen;
|
|
}
|
|
|
|
// precondition: buflen >= 1. gets an ncinput prepared by process_input, and
|
|
// sticks that into the bulk queue.
|
|
static int
|
|
process_ncinput(inputctx* ictx, const unsigned char* buf, int buflen){
|
|
ncinput ni;
|
|
int r = process_input(buf, buflen, &ni);
|
|
if(r > 0){
|
|
load_ncinput(ictx, &ni);
|
|
}else if(r < 0){
|
|
inc_input_errors(ictx);
|
|
r = 1; // we want to consume a single byte, upstairs
|
|
}
|
|
return r;
|
|
}
|
|
|
|
// handle redirected input (i.e. not from our connected terminal). process as
|
|
// much bulk UTF-8 input as we can, knowing it to be free of control sequences.
|
|
// anything not a valid UTF-8 character is dropped.
|
|
static void
|
|
process_bulk(inputctx* ictx, unsigned char* buf, int* bufused){
|
|
int offset = 0;
|
|
while(*bufused){
|
|
bool noroom = false;
|
|
pthread_mutex_lock(&ictx->ilock);
|
|
if(ictx->ivalid == ictx->isize){
|
|
noroom = true;
|
|
}
|
|
pthread_mutex_unlock(&ictx->ilock);
|
|
if(noroom){
|
|
break;
|
|
}
|
|
int consumed = process_ncinput(ictx, buf + offset, *bufused);
|
|
if(consumed <= 0){
|
|
break;
|
|
}
|
|
*bufused -= consumed;
|
|
offset += consumed;
|
|
}
|
|
// move any leftovers to the front
|
|
if(*bufused){
|
|
memmove(buf, buf + offset, *bufused);
|
|
}
|
|
}
|
|
|
|
// process as much mixed input as we can. we might find UTF-8 bulk input and
|
|
// control sequences mixed (though each individual character/sequence ought be
|
|
// contiguous). known control sequences are removed for internal processing.
|
|
// everything else will be handed up to the client (assuming it to be valid
|
|
// UTF-8).
|
|
static void
|
|
process_melange(inputctx* ictx, const unsigned char* buf, int* bufused){
|
|
int offset = 0;
|
|
int origlen = *bufused;
|
|
while(*bufused){
|
|
logdebug("input %d (%u)/%d [0x%02x] (%c)", offset, ictx->amata.used,
|
|
*bufused, buf[offset], isprint(buf[offset]) ? buf[offset] : ' ');
|
|
int consumed = 0;
|
|
if(buf[offset] == '\x1b'){
|
|
consumed = process_escape(ictx, buf + offset, *bufused);
|
|
if(consumed < 0){
|
|
if(ictx->midescape){
|
|
if(*bufused != -consumed || *bufused == origlen){
|
|
// not at the end; treat it as input. no need to move between
|
|
// buffers; simply ensure we process it as input, and don't mark
|
|
// anything as consumed.
|
|
ictx->midescape = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// don't process as input only if we just read a valid control character,
|
|
// or if we need to read more to determine what it is.
|
|
if(consumed <= 0 && !ictx->midescape){
|
|
consumed = process_ncinput(ictx, buf + offset, *bufused);
|
|
}
|
|
if(consumed < 0){
|
|
break;
|
|
}
|
|
*bufused -= consumed;
|
|
offset += consumed;
|
|
}
|
|
handoff_initial_responses_late(ictx);
|
|
}
|
|
|
|
// walk the matching automaton from wherever we were.
|
|
static void
|
|
process_ibuf(inputctx* ictx){
|
|
if(resize_seen){
|
|
ncinput tni = {
|
|
.id = NCKEY_RESIZE,
|
|
};
|
|
load_ncinput(ictx, &tni);
|
|
resize_seen = 0;
|
|
}
|
|
if(cont_seen){
|
|
ncinput tni = {
|
|
.id = NCKEY_SIGNAL,
|
|
};
|
|
load_ncinput(ictx, &tni);
|
|
cont_seen = 0;
|
|
}
|
|
if(ictx->tbufvalid){
|
|
// we could theoretically do this in parallel with process_bulk, but it
|
|
// hardly seems worthwhile without breaking apart the fetches of input.
|
|
process_escapes(ictx, ictx->tbuf, &ictx->tbufvalid);
|
|
handoff_initial_responses_late(ictx);
|
|
}
|
|
if(ictx->ibufvalid){
|
|
if(ictx_independent_p(ictx)){
|
|
process_bulk(ictx, ictx->ibuf, &ictx->ibufvalid);
|
|
}else{
|
|
int valid = ictx->ibufvalid;
|
|
process_melange(ictx, ictx->ibuf, &ictx->ibufvalid);
|
|
// move any leftovers to the front
|
|
if(ictx->ibufvalid){
|
|
memmove(ictx->ibuf, ictx->ibuf + valid - ictx->ibufvalid, ictx->ibufvalid);
|
|
if(ictx->amata.matchstart){
|
|
ictx->amata.matchstart = ictx->ibuf;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int ncinput_shovel(inputctx* ictx, const void* buf, int len){
|
|
process_melange(ictx, buf, &len);
|
|
if(len){
|
|
logwarn("dropping %d byte%s", len, len == 1 ? "" : "s");
|
|
inc_input_errors(ictx);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// here, we always block for an arbitrarily long time, or not at all,
|
|
// doing the latter only when ictx->midescape is set. |rtfd| and/or |rifd|
|
|
// are set high iff they are ready for reading, and otherwise cleared.
|
|
static int
|
|
block_on_input(inputctx* ictx, unsigned* rtfd, unsigned* rifd){
|
|
logtrace("blocking on input availability");
|
|
*rtfd = *rifd = 0;
|
|
unsigned nonblock = ictx->midescape;
|
|
if(nonblock){
|
|
loginfo("nonblocking read to check for completion");
|
|
ictx->midescape = 0;
|
|
}
|
|
#ifdef __MINGW32__
|
|
int timeoutms = nonblock ? 0 : -1;
|
|
DWORD ncount = 0;
|
|
HANDLE handles[2];
|
|
if(!ictx->stdineof){
|
|
if(ictx->ibufvalid != sizeof(ictx->ibuf)){
|
|
handles[ncount++] = ictx->stdinhandle;
|
|
}
|
|
}
|
|
if(ncount == 0){
|
|
handles[ncount++] = ictx->ipipes[0];
|
|
}
|
|
DWORD d = WaitForMultipleObjects(ncount, handles, false, timeoutms);
|
|
if(d == WAIT_TIMEOUT){
|
|
return 0;
|
|
}else if(d == WAIT_FAILED){
|
|
return -1;
|
|
}else if(d - WAIT_OBJECT_0 == 0){
|
|
*rifd = 1;
|
|
return 1;
|
|
}
|
|
return -1;
|
|
#else
|
|
int inevents = POLLIN;
|
|
#ifdef POLLRDHUP
|
|
inevents |= POLLRDHUP;
|
|
#endif
|
|
struct pollfd pfds[2];
|
|
int pfdcount = 0;
|
|
if(!ictx->stdineof){
|
|
if(ictx->ibufvalid != sizeof(ictx->ibuf)){
|
|
pfds[pfdcount].fd = ictx->stdinfd;
|
|
pfds[pfdcount].events = inevents;
|
|
pfds[pfdcount].revents = 0;
|
|
++pfdcount;
|
|
}
|
|
}
|
|
if(pfdcount == 0){
|
|
loginfo("output queues full; blocking on ipipes");
|
|
pfds[pfdcount].fd = ictx->ipipes[0];
|
|
pfds[pfdcount].events = inevents;
|
|
pfds[pfdcount].revents = 0;
|
|
++pfdcount;
|
|
}
|
|
if(ictx->termfd >= 0){
|
|
pfds[pfdcount].fd = ictx->termfd;
|
|
pfds[pfdcount].events = inevents;
|
|
pfds[pfdcount].revents = 0;
|
|
++pfdcount;
|
|
}
|
|
logtrace("waiting on %d fds (ibuf: %u/%"PRIuPTR")", pfdcount, ictx->ibufvalid, sizeof(ictx->ibuf));
|
|
sigset_t smask;
|
|
sigfillset(&smask);
|
|
sigdelset(&smask, SIGCONT);
|
|
sigdelset(&smask, SIGWINCH);
|
|
#ifdef SIGTHR
|
|
// freebsd uses SIGTHR for thread cancellation; need this to ensure wakeup
|
|
// on exit (in cancel_and_join()).
|
|
sigdelset(&smask, SIGTHR);
|
|
#endif
|
|
int events;
|
|
#if defined(__APPLE__) || defined(__MINGW32__)
|
|
int timeoutms = nonblock ? 0 : -1;
|
|
while((events = poll(pfds, pfdcount, timeoutms)) < 0){ // FIXME smask?
|
|
#else
|
|
struct timespec ts = { .tv_sec = 0, .tv_nsec = 0, };
|
|
struct timespec* pts = nonblock ? &ts : NULL;
|
|
while((events = ppoll(pfds, pfdcount, pts, &smask)) < 0){
|
|
#endif
|
|
if(errno == EINTR){
|
|
loginfo("interrupted by signal");
|
|
return resize_seen;
|
|
}else if(errno != EAGAIN && errno != EBUSY && errno != EWOULDBLOCK){
|
|
logerror("error polling (%s)", strerror(errno));
|
|
return -1;
|
|
}
|
|
}
|
|
loginfo("poll returned %d", events);
|
|
pfdcount = 0;
|
|
while(events){
|
|
if(pfds[pfdcount].revents){
|
|
if(pfds[pfdcount].fd == ictx->stdinfd){
|
|
*rifd = 1;
|
|
}else if(pfds[pfdcount].fd == ictx->termfd){
|
|
*rtfd = 1;
|
|
}else if(pfds[pfdcount].fd == ictx->ipipes[0]){
|
|
char c;
|
|
while(read(ictx->ipipes[0], &c, sizeof(c)) == 1){
|
|
// FIXME accelerate?
|
|
}
|
|
}
|
|
--events;
|
|
}
|
|
++pfdcount;
|
|
}
|
|
loginfo("got events: %c%c", *rtfd ? 'T' : 't', *rifd ? 'I' : 'i');
|
|
return events;
|
|
#endif
|
|
}
|
|
|
|
// populate the ibuf with any new data, up through its size, but do not block.
|
|
// don't loop around this call without some kind of readiness notification.
|
|
static void
|
|
read_inputs_nblock(inputctx* ictx){
|
|
unsigned rtfd, rifd;
|
|
block_on_input(ictx, &rtfd, &rifd);
|
|
// first we read from the terminal, if that's a distinct source.
|
|
if(rtfd){
|
|
read_input_nblock(ictx->termfd, ictx->tbuf, sizeof(ictx->tbuf),
|
|
&ictx->tbufvalid, NULL);
|
|
}
|
|
// now read bulk, possibly with term escapes intermingled within (if there
|
|
// was not a distinct terminal source).
|
|
if(rifd){
|
|
unsigned eof = ictx->stdineof;
|
|
read_input_nblock(ictx->stdinfd, ictx->ibuf, sizeof(ictx->ibuf),
|
|
&ictx->ibufvalid, &ictx->stdineof);
|
|
// did we switch from non-EOF state to EOF? if so, mark us ready
|
|
if(!eof && ictx->stdineof){
|
|
// we hit EOF; write an event to the readiness fd
|
|
mark_pipe_ready(ictx->readypipes);
|
|
pthread_cond_broadcast(&ictx->icond);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void*
|
|
input_thread(void* vmarshall){
|
|
setup_alt_sig_stack();
|
|
inputctx* ictx = vmarshall;
|
|
if(prep_all_keys(ictx) || build_cflow_automaton(ictx)){
|
|
ictx->failed = true;
|
|
handoff_initial_responses_early(ictx);
|
|
handoff_initial_responses_late(ictx);
|
|
}
|
|
for(;;){
|
|
read_inputs_nblock(ictx);
|
|
// process anything we've read
|
|
process_ibuf(ictx);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
int init_inputlayer(tinfo* ti, FILE* infp, int lmargin, int tmargin,
|
|
int rmargin, int bmargin, ncsharedstats* stats,
|
|
unsigned drain, int linesigs_enabled){
|
|
inputctx* ictx = create_inputctx(ti, infp, lmargin, tmargin, rmargin,
|
|
bmargin, stats, drain, linesigs_enabled);
|
|
if(ictx == NULL){
|
|
return -1;
|
|
}
|
|
if(pthread_create(&ictx->tid, NULL, input_thread, ictx)){
|
|
free_inputctx(ictx);
|
|
return -1;
|
|
}
|
|
ti->ictx = ictx;
|
|
loginfo("spun up input thread");
|
|
return 0;
|
|
}
|
|
|
|
int stop_inputlayer(tinfo* ti){
|
|
int ret = 0;
|
|
if(ti){
|
|
// FIXME cancellation on shutdown does not yet work on windows #2192
|
|
#ifndef __MINGW32__
|
|
if(ti->ictx){
|
|
loginfo("tearing down input thread");
|
|
ret |= cancel_and_join("input", ti->ictx->tid, NULL);
|
|
ret |= set_fd_nonblocking(ti->ictx->stdinfd, ti->stdio_blocking_save, NULL);
|
|
free_inputctx(ti->ictx);
|
|
ti->ictx = NULL;
|
|
}
|
|
#endif
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
int inputready_fd(const inputctx* ictx){
|
|
#ifndef __MINGW32__
|
|
return ictx->readypipes[0];
|
|
#else
|
|
(void)ictx;
|
|
logerror("readiness descriptor unavailable on windows");
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
static inline uint32_t
|
|
internal_get(inputctx* ictx, const struct timespec* ts, ncinput* ni){
|
|
uint32_t id;
|
|
if(ictx->drain){
|
|
logerror("input is being drained");
|
|
if(ni){
|
|
memset(ni, 0, sizeof(*ni));
|
|
ni->id = (uint32_t)-1;
|
|
}
|
|
return (uint32_t)-1;
|
|
}
|
|
pthread_mutex_lock(&ictx->ilock);
|
|
while(!ictx->ivalid){
|
|
if(ictx->stdineof){
|
|
pthread_mutex_unlock(&ictx->ilock);
|
|
logwarn("read eof on stdin");
|
|
if(ni){
|
|
memset(ni, 0, sizeof(*ni));
|
|
ni->id = NCKEY_EOF;
|
|
}
|
|
return NCKEY_EOF;
|
|
}
|
|
if(ts == NULL){
|
|
pthread_cond_wait(&ictx->icond, &ictx->ilock);
|
|
}else{
|
|
int r = pthread_cond_timedwait(&ictx->icond, &ictx->ilock, ts);
|
|
if(r == ETIMEDOUT){
|
|
pthread_mutex_unlock(&ictx->ilock);
|
|
if(ni){
|
|
memset(ni, 0, sizeof(*ni));
|
|
}
|
|
return 0;
|
|
}else if(r < 0){
|
|
inc_input_errors(ictx);
|
|
if(ni){
|
|
memset(ni, 0, sizeof(*ni));
|
|
ni->id = (uint32_t)-1;
|
|
}
|
|
return (uint32_t)-1;
|
|
}
|
|
}
|
|
}
|
|
id = ictx->inputs[ictx->iread].id;
|
|
if(ni){
|
|
memcpy(ni, &ictx->inputs[ictx->iread], sizeof(*ni));
|
|
if(notcurses_ucs32_to_utf8(&ni->id, 1, (unsigned char*)ni->utf8, sizeof(ni->utf8)) < 0){
|
|
ni->utf8[0] = 0;
|
|
}
|
|
if (ni->eff_text[0]==0) ni->eff_text[0]=ni->id;
|
|
}
|
|
if(++ictx->iread == ictx->isize){
|
|
ictx->iread = 0;
|
|
}
|
|
bool sendsignal = false;
|
|
if(ictx->ivalid-- == ictx->isize){
|
|
sendsignal = true;
|
|
}else{
|
|
logtrace("draining event readiness pipe %d", ictx->ivalid);
|
|
#ifndef __MINGW32__
|
|
char c;
|
|
while(read(ictx->readypipes[0], &c, sizeof(c)) == 1){
|
|
// FIXME accelerate?
|
|
}
|
|
#else
|
|
// we ought be draining this, but it breaks everything, as we can't easily
|
|
// do nonblocking input from a pipe in windows, augh...
|
|
// Ne pleure pas, Alfred! J'ai besoin de tout mon courage pour mourir a vingt ans!
|
|
/*while(ReadFile(ictx->readypipes[0], &c, sizeof(c), NULL, NULL)){
|
|
// FIXME accelerate?
|
|
}*/
|
|
#endif
|
|
}
|
|
pthread_mutex_unlock(&ictx->ilock);
|
|
if(sendsignal){
|
|
mark_pipe_ready(ictx->ipipes);
|
|
}
|
|
return id;
|
|
}
|
|
|
|
// infp has already been set non-blocking
|
|
uint32_t notcurses_get(notcurses* nc, const struct timespec* absdl, ncinput* ni){
|
|
uint32_t ret = internal_get(nc->tcache.ictx, absdl, ni);
|
|
return ret;
|
|
}
|
|
|
|
// FIXME better performance if we move this within the locked area
|
|
int notcurses_getvec(notcurses* n, const struct timespec* absdl,
|
|
ncinput* ni, int vcount){
|
|
for(int v = 0 ; v < vcount ; ++v){
|
|
uint32_t u = notcurses_get(n, absdl, &ni[v]);
|
|
if(u == (uint32_t)-1){
|
|
if(v == 0){
|
|
return -1;
|
|
}
|
|
return v;
|
|
}else if(u == 0){
|
|
return v;
|
|
}
|
|
}
|
|
return vcount;
|
|
}
|
|
|
|
uint32_t ncdirect_get(ncdirect* n, const struct timespec* absdl, ncinput* ni){
|
|
if(n->eof){
|
|
logerror("already got EOF");
|
|
return -1;
|
|
}
|
|
uint32_t r = internal_get(n->tcache.ictx, absdl, ni);
|
|
if(r == NCKEY_EOF){
|
|
n->eof = 1;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
int get_cursor_location(inputctx* ictx, const char* u7, unsigned* y, unsigned* x){
|
|
pthread_mutex_lock(&ictx->clock);
|
|
while(ictx->cvalid == 0){
|
|
if(ictx->coutstanding == 0){
|
|
if(tty_emit(u7, ictx->ti->ttyfd)){
|
|
pthread_mutex_unlock(&ictx->clock);
|
|
return -1;
|
|
}
|
|
++ictx->coutstanding;
|
|
}
|
|
pthread_cond_wait(&ictx->ccond, &ictx->clock);
|
|
}
|
|
const cursorloc* cloc = &ictx->csrs[ictx->cread];
|
|
if(++ictx->cread == ictx->csize){
|
|
ictx->cread = 0;
|
|
}
|
|
--ictx->cvalid;
|
|
if(y){
|
|
*y = cloc->y;
|
|
}
|
|
if(x){
|
|
*x = cloc->x;
|
|
}
|
|
pthread_mutex_unlock(&ictx->clock);
|
|
return 0;
|
|
}
|
|
|
|
// Disable signals originating from the terminal's line discipline, i.e.
|
|
// SIGINT (^C), SIGQUIT (^\), and SIGTSTP (^Z). They are enabled by default.
|
|
static int
|
|
linesigs_disable(tinfo* ti){
|
|
if(!ti->ictx->linesigs){
|
|
logwarn("linedisc signals already disabled");
|
|
}
|
|
#ifndef __MINGW32__
|
|
if(ti->ttyfd < 0){
|
|
return 0;
|
|
}
|
|
struct termios tios;
|
|
if(tcgetattr(ti->ttyfd, &tios)){
|
|
logerror("Couldn't preserve terminal state for %d (%s)", ti->ttyfd, strerror(errno));
|
|
return -1;
|
|
}
|
|
tios.c_lflag &= ~ISIG;
|
|
if(tcsetattr(ti->ttyfd, TCSANOW, &tios)){
|
|
logerror("Error disabling signals on %d (%s)", ti->ttyfd, strerror(errno));
|
|
return -1;
|
|
}
|
|
#else
|
|
DWORD mode;
|
|
if(!GetConsoleMode(ti->inhandle, &mode)){
|
|
logerror("error acquiring input mode");
|
|
return -1;
|
|
}
|
|
mode &= ~ENABLE_PROCESSED_INPUT;
|
|
if(!SetConsoleMode(ti->inhandle, mode)){
|
|
logerror("error setting input mode");
|
|
return -1;
|
|
}
|
|
#endif
|
|
ti->ictx->linesigs = 0;
|
|
loginfo("disabled line discipline signals");
|
|
return 0;
|
|
}
|
|
|
|
int notcurses_linesigs_disable(notcurses* nc){
|
|
return linesigs_disable(&nc->tcache);
|
|
}
|
|
|
|
static int
|
|
linesigs_enable(tinfo* ti){
|
|
if(ti->ictx->linesigs){
|
|
logwarn("linedisc signals already enabled");
|
|
}
|
|
#ifndef __MINGW32__
|
|
if(ti->ttyfd < 0){
|
|
return 0;
|
|
}
|
|
struct termios tios;
|
|
if(tcgetattr(ti->ttyfd, &tios)){
|
|
logerror("couldn't preserve terminal state for %d (%s)", ti->ttyfd, strerror(errno));
|
|
return -1;
|
|
}
|
|
tios.c_lflag |= ISIG;
|
|
if(tcsetattr(ti->ttyfd, TCSANOW, &tios)){
|
|
logerror("error disabling signals on %d (%s)", ti->ttyfd, strerror(errno));
|
|
return -1;
|
|
}
|
|
#else
|
|
DWORD mode;
|
|
if(!GetConsoleMode(ti->inhandle, &mode)){
|
|
logerror("error acquiring input mode");
|
|
return -1;
|
|
}
|
|
mode |= ENABLE_PROCESSED_INPUT;
|
|
if(!SetConsoleMode(ti->inhandle, mode)){
|
|
logerror("error setting input mode");
|
|
return -1;
|
|
}
|
|
#endif
|
|
ti->ictx->linesigs = 1;
|
|
loginfo("enabled line discipline signals");
|
|
return 0;
|
|
}
|
|
|
|
// Restore signals originating from the terminal's line discipline, i.e.
|
|
// SIGINT (^C), SIGQUIT (^\), and SIGTSTP (^Z), if disabled.
|
|
int notcurses_linesigs_enable(notcurses* n){
|
|
return linesigs_enable(&n->tcache);
|
|
}
|
|
|
|
struct initial_responses* inputlayer_get_responses(inputctx* ictx){
|
|
struct initial_responses* iresp;
|
|
pthread_mutex_lock(&ictx->ilock);
|
|
while(ictx->initdata || !ictx->initdata_complete){
|
|
pthread_cond_wait(&ictx->icond, &ictx->ilock);
|
|
}
|
|
iresp = ictx->initdata_complete;
|
|
ictx->initdata_complete = NULL;
|
|
pthread_mutex_unlock(&ictx->ilock);
|
|
if(ictx->failed){
|
|
logpanic("aborting after automaton construction failure");
|
|
free(iresp);
|
|
return NULL;
|
|
}
|
|
return iresp;
|
|
}
|