Unify the dynamic, dataflow special keys automaton and the static, codeflow terminal response automaton, yielding a single automaton. Add kitty keyboard support information to `notcurses-info`. Closes #2183.pull/2220/head
parent
05635aa60d
commit
b5c161a07c
@ -0,0 +1,530 @@
|
||||
#include "automaton.h"
|
||||
#include "internal.h"
|
||||
|
||||
// the input automaton, walked for all escape sequences. an escape sequence is
|
||||
// everything from an escape through recognized termination of that escape, or
|
||||
// abort of the sequence via another escape, save the case of DCS sequences
|
||||
// (those beginning with Escape-P), which are terminated by the ST sequence
|
||||
// Escape-\. in the case of an aborted sequence, the sequence in its entirety
|
||||
// is replayed as regular input. regular input is not driven through this
|
||||
// automaton.
|
||||
//
|
||||
// one complication is that the user can just press escape themselves, followed
|
||||
// by arbitrary other keypresses. when input is redirected from some source
|
||||
// other than the connected terminal, this is no problem: we know control
|
||||
// sequences to be coming in from the connected terminal, and everything else
|
||||
// is bulk input.
|
||||
|
||||
// we assumed escapes can only be composed of 7-bit chars
|
||||
typedef struct esctrie {
|
||||
// if non-NULL, this is the next level of radix-128 trie. it is NULL on
|
||||
// accepting nodes, since no valid control sequence is a prefix of another
|
||||
// valid control sequence.
|
||||
struct esctrie** trie;
|
||||
enum {
|
||||
NODE_SPECIAL, // an accepting node, or pure transit (if ni.id == 0)
|
||||
NODE_NUMERIC, // accumulates a number
|
||||
NODE_STRING, // accumulates a string
|
||||
NODE_FUNCTION, // invokes a function
|
||||
} ntype;
|
||||
ncinput ni; // composed key terminating here
|
||||
int number; // accumulated number; reset to 0 on entry
|
||||
char* str; // accumulated string; reset to NULL on entry
|
||||
triefunc fxn; // function to call on match
|
||||
struct esctrie* kleene; // kleene match
|
||||
} esctrie;
|
||||
|
||||
uint32_t esctrie_id(const esctrie* e){
|
||||
return e->ni.id;
|
||||
}
|
||||
|
||||
const char* esctrie_string(const esctrie* e){
|
||||
return e->str;
|
||||
}
|
||||
|
||||
esctrie** esctrie_trie(esctrie* e){
|
||||
return e->trie;
|
||||
}
|
||||
|
||||
int esctrie_numeric(const esctrie* e){
|
||||
if(e->ntype != NODE_NUMERIC){
|
||||
return -1;
|
||||
}
|
||||
return e->number;
|
||||
}
|
||||
|
||||
static inline esctrie*
|
||||
create_esctrie_node(int special){
|
||||
esctrie* e = malloc(sizeof(*e));
|
||||
if(e){
|
||||
memset(e, 0, sizeof(*e));
|
||||
e->ntype = NODE_SPECIAL;
|
||||
if((e->ni.id = special) == 0){
|
||||
const size_t tsize = sizeof(*e->trie) * 0x80;
|
||||
if( (e->trie = malloc(tsize)) ){
|
||||
memset(e->trie, 0, tsize);
|
||||
return e;
|
||||
}
|
||||
free(e);
|
||||
return NULL;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
static void
|
||||
free_trienode(esctrie** eptr){
|
||||
esctrie* e;
|
||||
if( (e = *eptr) ){
|
||||
if(e->trie){
|
||||
int z;
|
||||
for(z = 0 ; z < 0x80 ; ++z){
|
||||
// don't recurse down a link to ourselves
|
||||
if(e->trie[z] && e->trie[z] != e){
|
||||
free_trienode(&e->trie[z]);
|
||||
}
|
||||
// if it's a numeric path, only recurse once
|
||||
if(z == '0'){
|
||||
if(e->trie['1'] == e->trie[z]){
|
||||
z = '9';
|
||||
}
|
||||
}
|
||||
// if it's an all-strings path, only recurse once
|
||||
if(z == ' '){
|
||||
if(e->trie['!'] == e->trie[z]){
|
||||
z = 0x80;
|
||||
}
|
||||
}
|
||||
}
|
||||
free(e->str);
|
||||
free(e->trie);
|
||||
}
|
||||
free(e);
|
||||
}
|
||||
}
|
||||
|
||||
void input_free_esctrie(automaton* a){
|
||||
free_trienode(&a->escapes);
|
||||
}
|
||||
|
||||
static int
|
||||
esctrie_make_numeric(esctrie* e){
|
||||
if(e->ntype == NODE_NUMERIC){
|
||||
return 0;
|
||||
}
|
||||
if(e->ntype != NODE_SPECIAL){
|
||||
logerror("can't make node type %d numeric\n", e->ntype);
|
||||
return -1;
|
||||
}
|
||||
for(int i = '0' ; i < '9' ; ++i){
|
||||
if(e->trie[i]){
|
||||
logerror("can't make %c-followed numeric\n", i);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
e->ntype = NODE_NUMERIC;
|
||||
for(int i = '0' ; i < '9' ; ++i){
|
||||
e->trie[i] = e;
|
||||
}
|
||||
logdebug("made numeric: %p\n", e);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
esctrie_make_kleene(esctrie* e, unsigned follow, esctrie* term){
|
||||
if(e->ntype != NODE_SPECIAL){
|
||||
logerror("can't make node type %d string\n", e->ntype);
|
||||
return -1;
|
||||
}
|
||||
for(unsigned i = 0 ; i < 0x80 ; ++i){
|
||||
if(i == follow){
|
||||
e->trie[i] = term;
|
||||
}else if(e->trie[i] == NULL){
|
||||
e->trie[i] = e;
|
||||
}
|
||||
}
|
||||
logdebug("made kleene: %p\n", e);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
esctrie_make_function(esctrie* e, triefunc fxn){
|
||||
if(e->ntype != NODE_SPECIAL){
|
||||
logerror("can't make node type %d function\n", e->ntype);
|
||||
return -1;
|
||||
}
|
||||
if(e->trie){
|
||||
logerror("can't make followed function\n");
|
||||
return -1;
|
||||
}
|
||||
e->ntype = NODE_FUNCTION;
|
||||
e->fxn = fxn;
|
||||
logdebug("made function %p: %p\n", fxn, e);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
esctrie_make_string(esctrie* e, triefunc fxn){
|
||||
if(e->ntype == NODE_STRING){
|
||||
return 0;
|
||||
}
|
||||
if(e->ntype != NODE_SPECIAL){
|
||||
logerror("can't make node type %d string\n", e->ntype);
|
||||
return -1;
|
||||
}
|
||||
for(int i = 0 ; i < 0x80 ; ++i){
|
||||
if(!isprint(i)){
|
||||
continue;
|
||||
}
|
||||
if(e->trie[i]){
|
||||
logerror("can't make %c-followed string\n", i);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
esctrie* newe = create_esctrie_node(0);
|
||||
if(newe == NULL){
|
||||
return -1;
|
||||
}
|
||||
for(int i = 0 ; i < 0x80 ; ++i){
|
||||
if(!isprint(i)){
|
||||
continue;
|
||||
}
|
||||
e->trie[i] = newe;
|
||||
}
|
||||
e = newe;
|
||||
e->ntype = NODE_STRING;
|
||||
for(int i = 0 ; i < 0x80 ; ++i){
|
||||
if(!isprint(i)){
|
||||
continue;
|
||||
}
|
||||
e->trie[i] = newe;
|
||||
}
|
||||
if((e->trie[0x1b] = create_esctrie_node(0)) == NULL){
|
||||
return -1;
|
||||
}
|
||||
e = e->trie[0x1b];
|
||||
if((e->trie['\\'] = create_esctrie_node(NCKEY_INVALID)) == NULL){
|
||||
return -1;
|
||||
}
|
||||
e = e->trie['\\'];
|
||||
e->ni.id = 0;
|
||||
e->ntype = NODE_SPECIAL;
|
||||
if(esctrie_make_function(e, fxn)){
|
||||
return -1;
|
||||
}
|
||||
logdebug("made string: %p\n", e);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static esctrie*
|
||||
link_kleene(esctrie* e, unsigned follow){
|
||||
if(e->kleene){
|
||||
return e->kleene;
|
||||
}
|
||||
esctrie* term = create_esctrie_node(0);
|
||||
if(term == NULL){
|
||||
return NULL;
|
||||
}
|
||||
esctrie* targ = NULL;
|
||||
if( (targ = create_esctrie_node(0)) ){
|
||||
if(esctrie_make_kleene(targ, follow, term)){
|
||||
free_trienode(&targ);
|
||||
free_trienode(&term);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
// fill in all NULL numeric links with the new target
|
||||
for(unsigned int i = 0 ; i < 0x80 ; ++i){
|
||||
if(i == follow){
|
||||
if(e->trie[i]){
|
||||
logerror("drain terminator already registered\n");
|
||||
free_trienode(&targ);
|
||||
free_trienode(&term);
|
||||
}
|
||||
e->trie[follow] = term;
|
||||
}else if(e->trie[i] == NULL){
|
||||
e->trie[i] = targ;
|
||||
// FIXME travel to the ends and link targ there
|
||||
}
|
||||
}
|
||||
targ->kleene = targ;
|
||||
return e->trie[follow];
|
||||
}
|
||||
|
||||
static void
|
||||
fill_in_numerics(esctrie* e, esctrie* targ, unsigned follow, esctrie* efollow){
|
||||
// fill in all NULL numeric links with the new target
|
||||
for(int i = '0' ; i <= '9' ; ++i){
|
||||
if(e->trie[i] == NULL){
|
||||
e->trie[i] = targ;
|
||||
}else if(e->trie[i] != e){
|
||||
fill_in_numerics(e->trie[i], targ, follow, efollow);
|
||||
}
|
||||
}
|
||||
e->trie[follow] = efollow;
|
||||
}
|
||||
|
||||
// accept any digit and transition to a numeric node.
|
||||
static esctrie*
|
||||
link_numeric(esctrie* e, unsigned follow){
|
||||
esctrie* targ = NULL;
|
||||
// find a linked NODE_NUMERIC, if one exists. we'll want to reuse it.
|
||||
for(int i = '0' ; i <= '9' ; ++i){
|
||||
targ = e->trie[i];
|
||||
if(targ && targ->ntype == NODE_NUMERIC){
|
||||
break;
|
||||
}
|
||||
targ = NULL;
|
||||
}
|
||||
// we either have a numeric target, or will make one now
|
||||
if(targ == NULL){
|
||||
if( (targ = create_esctrie_node(0)) ){
|
||||
if(esctrie_make_numeric(targ)){
|
||||
free_trienode(&targ);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
// targ is the numeric node we're either creating or coopting
|
||||
esctrie* efollow = targ->trie[follow];
|
||||
if(efollow == NULL){
|
||||
if((efollow = create_esctrie_node(0)) == NULL){
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
for(int i = '0' ; i <= '9' ; ++i){
|
||||
if(e->trie[i] == NULL){
|
||||
e->trie[i] = targ;
|
||||
}
|
||||
fill_in_numerics(e->trie[i], targ, follow, efollow);
|
||||
}
|
||||
return efollow;
|
||||
}
|
||||
|
||||
// add a cflow path to the automaton
|
||||
int inputctx_add_cflow(automaton* a, const char* csi, triefunc fxn){
|
||||
if(a->escapes == NULL){
|
||||
if((a->escapes = create_esctrie_node(0)) == NULL){
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
esctrie* eptr = a->escapes;
|
||||
bool inescape = false;
|
||||
unsigned char c;
|
||||
while( (c = *csi++) ){
|
||||
if(c == '\\'){
|
||||
if(inescape){
|
||||
logerror("illegal escape: \\\n");
|
||||
return -1;
|
||||
}
|
||||
inescape = true;
|
||||
}else if(inescape){
|
||||
if(c == 'N'){
|
||||
// a numeric must be followed by some terminator
|
||||
if(!*csi){
|
||||
logerror("illegal numeric terminator\n");
|
||||
return -1;
|
||||
}
|
||||
c = *csi++;
|
||||
eptr = link_numeric(eptr, c);
|
||||
if(eptr == NULL){
|
||||
return -1;
|
||||
}
|
||||
}else if(c == 'S'){
|
||||
if(esctrie_make_string(eptr, fxn)){
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}else if(c == 'D'){ // drain (kleene closure)
|
||||
// a kleene must be followed by some terminator
|
||||
if(!*csi){
|
||||
logerror("illegal kleene terminator\n");
|
||||
return -1;
|
||||
}
|
||||
c = *csi++;
|
||||
eptr = link_kleene(eptr, c);
|
||||
if(eptr == NULL){
|
||||
return -1;
|
||||
}
|
||||
}else{
|
||||
logerror("illegal escape: %u\n", c);
|
||||
return -1;
|
||||
}
|
||||
inescape = false;
|
||||
}else{
|
||||
if(eptr->trie[c] == NULL){
|
||||
if((eptr->trie[c] = create_esctrie_node(0)) == NULL){
|
||||
return -1;
|
||||
}
|
||||
if(isdigit(c)){
|
||||
eptr->trie[c]->number = c - '0';
|
||||
}
|
||||
}else if(eptr->trie[c] == eptr->kleene){
|
||||
if((eptr->trie[c] = create_esctrie_node(0)) == NULL){
|
||||
return -1;
|
||||
}
|
||||
}else if(eptr->trie[c]->ntype == NODE_NUMERIC){
|
||||
// punch a hole through the numeric loop. create a new one, and fill
|
||||
// it in with the existing target.
|
||||
struct esctrie* newe;
|
||||
if((newe = create_esctrie_node(0)) == NULL){
|
||||
return -1;
|
||||
}
|
||||
for(int i = 0 ; i < 0x80 ; ++i){
|
||||
newe->trie[i] = eptr->trie[c]->trie[i];
|
||||
}
|
||||
eptr->trie[c] = newe;
|
||||
}
|
||||
eptr = eptr->trie[c];
|
||||
}
|
||||
logdebug("added %c, now at %p (%d) %d (%u)\n", c, eptr, eptr->ntype, eptr->number, *csi);
|
||||
}
|
||||
if(inescape){
|
||||
logerror("illegal escape at end of line\n");
|
||||
return -1;
|
||||
}
|
||||
free(eptr->trie);
|
||||
eptr->trie = NULL;
|
||||
return esctrie_make_function(eptr, fxn);
|
||||
}
|
||||
|
||||
// multiple input escapes might map to the same input
|
||||
int inputctx_add_input_escape(automaton* a, const char* esc, uint32_t special,
|
||||
unsigned shift, unsigned ctrl, unsigned alt){
|
||||
if(esc[0] != NCKEY_ESC || strlen(esc) < 2){ // assume ESC prefix + content
|
||||
logerror("not an escape (0x%x)\n", special);
|
||||
return -1;
|
||||
}
|
||||
esctrie** eptr = &a->escapes;
|
||||
if(*eptr == NULL){
|
||||
if((*eptr = create_esctrie_node(0)) == NULL){
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
esctrie* cur = *eptr;
|
||||
++esc; // don't encode initial escape as a transition
|
||||
do{
|
||||
int valid = *esc;
|
||||
if(valid <= 0 || valid >= 0x80 || valid == NCKEY_ESC){
|
||||
logerror("invalid character %d in escape\n", valid);
|
||||
return -1;
|
||||
}
|
||||
if(cur->trie[valid] == NULL){
|
||||
if((cur->trie[valid] = create_esctrie_node(0)) == NULL){
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
cur = cur->trie[valid];
|
||||
++esc;
|
||||
}while(*esc);
|
||||
// it appears that multiple keys can be mapped to the same escape string. as
|
||||
// an example, see "kend" and "kc1" in st ("simple term" from suckless) :/.
|
||||
if(cur->ni.id){ // already had one here!
|
||||
if(cur->ni.id != special){
|
||||
logwarn("already added escape (got 0x%x, wanted 0x%x)\n", cur->ni.id, special);
|
||||
}
|
||||
}else{
|
||||
cur->ni.id = special;
|
||||
cur->ni.shift = shift;
|
||||
cur->ni.ctrl = ctrl;
|
||||
cur->ni.alt = alt;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
growstring(automaton* a, esctrie* e, unsigned candidate){
|
||||
if(!isprint(candidate)){
|
||||
logerror("unexpected char %u in string\n", candidate);
|
||||
return -1;
|
||||
}
|
||||
char* tmp = realloc(e->str, a->stridx + 1);
|
||||
if(tmp == NULL){
|
||||
return -1;
|
||||
}
|
||||
e->str = tmp;
|
||||
e->str[a->stridx - 1] = candidate;
|
||||
e->str[a->stridx] = '\0';
|
||||
++a->stridx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// returns -1 for non-match, 0 for match, 1 for acceptance. if we are in the
|
||||
// middle of a sequence, and receive an escape, *do not call this*, but
|
||||
// instead call reset_automaton() after replaying the used characters to the
|
||||
// bulk input buffer, and *then* call this with the escape.
|
||||
int walk_automaton(automaton* a, struct inputctx* ictx, unsigned candidate,
|
||||
ncinput* ni){
|
||||
if(candidate >= 0x80){
|
||||
logerror("eight-bit char %u in control sequence\n", candidate);
|
||||
return -1;
|
||||
}
|
||||
esctrie* e = a->state;
|
||||
logdebug("state: %p candidate: %c %u type: %d\n", e, candidate, candidate, e->ntype);
|
||||
// we ought not have been called for an escape with any state!
|
||||
if(candidate == 0x1b && !a->instring){
|
||||
assert(NULL == e);
|
||||
a->state = a->escapes;
|
||||
return 0;
|
||||
}
|
||||
if(e->ntype == NODE_NUMERIC){
|
||||
if(isdigit(candidate)){
|
||||
int digit = candidate - '0';
|
||||
if((INT_MAX - digit) / 10 < e->number){
|
||||
logwarn("digit %c will overflow %d\n", candidate, e->number);
|
||||
}
|
||||
e->number = e->number * 10 + digit;
|
||||
return 0;
|
||||
}
|
||||
}else if(e->ntype == NODE_STRING){
|
||||
if(candidate == 0x1b){
|
||||
a->state = e->trie[candidate];
|
||||
a->instring = 0;
|
||||
}else if(growstring(a, e, candidate)){
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if((a->state = e->trie[candidate]) == NULL){
|
||||
if(isprint(candidate)){
|
||||
if(e == a->escapes){
|
||||
memset(ni, 0, sizeof(*ni));
|
||||
ni->id = candidate;
|
||||
ni->alt = true;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
loginfo("unexpected transition %u\n", candidate);
|
||||
return -1;
|
||||
}
|
||||
e = a->state;
|
||||
// initialize any node we've just stepped into
|
||||
switch(e->ntype){
|
||||
case NODE_NUMERIC:
|
||||
e->number = candidate - '0';
|
||||
break;
|
||||
case NODE_STRING:
|
||||
a->stridx = 1;
|
||||
a->instring = 1;
|
||||
if(growstring(a, e, candidate)){
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
case NODE_SPECIAL:
|
||||
if(e->ni.id){
|
||||
memcpy(ni, &e->ni, sizeof(*ni));
|
||||
a->state = NULL; // FIXME?
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
case NODE_FUNCTION:
|
||||
a->state = NULL; // FIXME?
|
||||
if(e->fxn == NULL){
|
||||
return 2;
|
||||
}
|
||||
return e->fxn(ictx);
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
#ifndef NOTCURSES_AUTOMATON
|
||||
#define NOTCURSES_AUTOMATON
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct ncinput;
|
||||
struct esctrie;
|
||||
struct inputctx;
|
||||
|
||||
typedef int (*triefunc)(struct inputctx*);
|
||||
|
||||
// the state necessary for matching input against our automaton of control
|
||||
// sequences. we *do not* match the bulk UTF-8 input. we match online (i.e.
|
||||
// we can be passed a byte at a time).
|
||||
typedef struct automaton {
|
||||
struct esctrie* escapes; // head Esc node of trie
|
||||
int used; // bytes consumed thus far
|
||||
int instring; // are we in an ST-terminated string?
|
||||
struct esctrie* state;
|
||||
unsigned stridx; // bytes of accumulating string (includes NUL)
|
||||
} automaton;
|
||||
|
||||
void input_free_esctrie(automaton *a);
|
||||
|
||||
int inputctx_add_input_escape(automaton* a, const char* esc,
|
||||
uint32_t special, unsigned shift,
|
||||
unsigned ctrl, unsigned alt);
|
||||
|
||||
int inputctx_add_cflow(automaton* a, const char* csi, triefunc fxn)
|
||||
__attribute__ ((nonnull (1, 2, 3)));
|
||||
|
||||
int walk_automaton(automaton* a, struct inputctx* ictx, unsigned candidate,
|
||||
struct ncinput* ni)
|
||||
__attribute__ ((nonnull (1, 2, 4)));
|
||||
|
||||
uint32_t esctrie_id(const struct esctrie* e);
|
||||
const char* esctrie_string(const struct esctrie* e);
|
||||
// returns 128-way array of esctrie pointers
|
||||
struct esctrie** esctrie_trie(struct esctrie* e);
|
||||
int esctrie_numeric(const struct esctrie* e);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue