From 3cf5a67b84994fca99cb088203c18bc6fa7288bb Mon Sep 17 00:00:00 2001 From: nick black Date: Sun, 12 Sep 2021 03:25:37 -0400 Subject: [PATCH] input thread #2136 --- NEWS.md | 8 + README.md | 19 + USAGE.md | 34 +- doc/man/man3/notcurses_direct.3.md | 5 + doc/man/man3/notcurses_init.3.md | 6 + doc/man/man3/notcurses_input.3.md | 6 + include/notcurses/direct.h | 10 +- include/notcurses/notcurses.h | 244 +----- src/lib/direct.c | 6 +- src/lib/gpm.c | 2 +- src/lib/in.c | 1182 ++++++++++++++++++++++++++-- src/lib/in.h | 82 ++ src/lib/input.h | 69 -- src/lib/internal.h | 1 + src/lib/linux.c | 32 +- src/lib/notcurses.c | 9 +- src/lib/termdesc.c | 74 +- src/lib/termdesc.h | 42 +- 18 files changed, 1348 insertions(+), 483 deletions(-) create mode 100644 src/lib/in.h delete mode 100644 src/lib/input.h diff --git a/NEWS.md b/NEWS.md index 5c5bf0147..5d680655b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,14 @@ rearrangements of Notcurses. * `notcurses_check_pixel_support()` still returns 0 if there is no support for bitmap graphics, but now returns an `ncpixelimple_e` to differentiate the pixel backend otherwise. This result is strictly informative. + * Added `NCOPTION_DRAIN_INPUT`. Notcurses now launches a thread to process + input, so that it can respond to terminal messages with minimal latency. + Input read from `stdin` intended for the client is buffered until + retrieved. If your client never intends to read this input, provide this + flag to eliminate unnecessary processing, and ensure Notcurses can always + retrieve terminal messages (if buffers are full, Notcurses cannot + continue reading). Likewise added `NCDIRECT_OPTION_DRAIN_INPUT`. + * Removed a bunch of deprecated `static inline` functions from the headers. * 2.4.0 (2021-09-06) * Mouse events in the Linux console are now reported from GPM when built diff --git a/README.md b/README.md index 3b5022d4c..c0c417381 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,25 @@ If things break or seem otherwise lackluster, **please** consult the more importantly, it will link against minimal Notcurses installations. +
+ Does it work with hardware terminals? + With the correct `TERM` value, many hardware terminals are supported. The VT100 + is sadly unsupported due to its extensive need for delays. In general, if the + terminfo database entry indicates mandatory delays, Notcurses will not currently + support that terminal properly. It's known that Notcurses can drive the VT320 + and VT340, including Sixel graphics on the latter. +
+ +
+ What happens if I try blitting bitmap graphics on a terminal which + doesn't support them? + Notcurses will not make use of bitmap protocols unless the terminal positively + indicates support for them, even if `NCBLIT_PIXEL` has been requested. Likewise, + sextants (`NCBLIT_3x2`) won't be used without Unicode 13 support, etc. + `ncvisual_render()` will use the best blitter available, unless + `NCVISUAL_OPTION_NODEGRADE` is provided (in which case it will fail). +
+
Notcurses looks like absolute crap in screen. screen doesn't support RGB colors (at least as of 4.08.00); diff --git a/USAGE.md b/USAGE.md index 8dc2a0611..95e09cbd8 100644 --- a/USAGE.md +++ b/USAGE.md @@ -95,7 +95,7 @@ typedef enum { // to do this, pass NCOPTION_NO_CLEAR_BITMAPS. Note that they might still // get cleared even if this is set, and they might not get cleared even if // this is not set. It's a tough world out there. -#define NCOPTION_NO_CLEAR_BITMAPS 0x0002ull +#define NCOPTION_NO_CLEAR_BITMAPS 0x0002 // We typically install a signal handler for SIGWINCH that generates a resize // event in the notcurses_get() queue. Set to inhibit this handler. @@ -110,7 +110,7 @@ typedef enum { // at context creation time. Together with NCOPTION_NO_ALTERNATE_SCREEN and a // scrolling standard plane, this facilitates easy scrolling-style programs in // rendered mode. -#define NCOPTION_PRESERVE_CURSOR 0x0010ull +#define NCOPTION_PRESERVE_CURSOR 0x0010 // Notcurses typically prints version info in notcurses_init() and performance // info in notcurses_stop(). This inhibits that output. @@ -120,6 +120,17 @@ typedef enum { // of the "alternate screen". This flag inhibits use of smcup/rmcup. #define NCOPTION_NO_ALTERNATE_SCREEN 0x0040 +// Do not modify the font. Notcurses might attempt to change the font slightly, +// to support certain glyphs (especially on the Linux console). If this is set, +// no such modifications will be made. Note that font changes will not affect +// anything but the virtual console/terminal in which Notcurses is running. +#define NCOPTION_NO_FONT_CHANGES 0x0080 + +// Input may be freely dropped. This ought be provided when the program does not +// intend to handle input. Otherwise, input can accumulate in internal buffers, +// eventually preventing Notcurses from processing terminal messages. +#define NCOPTION_DRAIN_INPUT 0x0100 + // Configuration for notcurses_init(). typedef struct notcurses_options { // The name of the terminfo database entry describing this terminal. If NULL, @@ -378,11 +389,23 @@ struct ncdirect* ncdirect_core_init(const char* termtype, FILE* fp, uint64_t fla // echo and line buffering are turned off. #define NCDIRECT_OPTION_INHIBIT_CBREAK 0x0002ull +// Input may be freely dropped. This ought be provided when the program does not +// intend to handle input. Otherwise, input can accumulate in internal buffers, +// eventually preventing Notcurses from processing terminal messages. +#define NCDIRECT_OPTION_DRAIN_INPUT 0x0004ull + // We typically install a signal handler for SIG{INT, SEGV, ABRT, QUIT} that // restores the screen, and then calls the old signal handler. Set to inhibit // registration of these signal handlers. Chosen to match fullscreen mode. #define NCDIRECT_OPTION_NO_QUIT_SIGHANDLERS 0x0008ull +// Enable logging (to stderr) at the NCLOGLEVEL_WARNING level. +#define NCDIRECT_OPTION_VERBOSE 0x0010ull + +// Enable logging (to stderr) at the NCLOGLEVEL_TRACE level. This will enable +// all diagnostics, a superset of NCDIRECT_OPTION_VERBOSE (which this implies). +#define NCDIRECT_OPTION_VERY_VERBOSE 0x0020ull + // Release 'nc' and any associated resources. 0 on success, non-0 on failure. int ncdirect_stop(struct ncdirect* nc); ``` @@ -671,7 +694,12 @@ typedef struct ncinput { // event is processed, the return value is the 'id' field from that event. // 'ni' may be NULL. uint32_t notcurses_get(struct notcurses* n, const struct timespec* ts, - ncinput* ni) + ncinput* ni); + +// Acquire up to 'vcount' ncinputs at the vector 'ni'. The number read will be +// returned, or -1 on error without any reads, 0 on timeout. +int notcurses_getvec(struct notcurses* n, const struct timespec* ts, + ncinput* ni, int vcount); // 'ni' may be NULL if the caller is uninterested in event details. If no event // is ready, returns 0. diff --git a/doc/man/man3/notcurses_direct.3.md b/doc/man/man3/notcurses_direct.3.md index 00b5f8330..1fe633ec9 100644 --- a/doc/man/man3/notcurses_direct.3.md +++ b/doc/man/man3/notcurses_direct.3.md @@ -169,6 +169,11 @@ The following flags are defined: will place the terminal into cbreak mode (i.e. disabling echo and line buffering; see **tcgetattr(3)**). +* **NCDIRECT_OPTION_DRAIN_INPUT**: Standard input may be freely discarded. If + you do not intend to process input, pass this flag. Otherwise, input can + buffer up, eventually preventing Notcurses from processing terminal + messages. It will furthermore avoid wasting time processing useless input. + * **NCDIRECT_OPTION_NO_QUIT_SIGHANDLERS**: A signal handler will usually be installed for **SIGABRT**, **SIGFPE**, **SIGILL**, **SIGINT**, **SIGQUIT**, **SIGSEGV**, and **SIGTERM**, cleaning up the terminal on such exceptions. diff --git a/doc/man/man3/notcurses_init.3.md b/doc/man/man3/notcurses_init.3.md index efcece480..b27d570ae 100644 --- a/doc/man/man3/notcurses_init.3.md +++ b/doc/man/man3/notcurses_init.3.md @@ -19,6 +19,7 @@ notcurses_init - initialize a notcurses instance #define NCOPTION_SUPPRESS_BANNERS 0x0020ull #define NCOPTION_NO_ALTERNATE_SCREEN 0x0040ull #define NCOPTION_NO_FONT_CHANGES 0x0080ull +#define NCOPTION_DRAIN_INPUT 0x0100ull typedef enum { NCLOGLEVEL_SILENT, // print nothing once fullscreen service begins @@ -139,6 +140,11 @@ zero. The following flags are defined: * **NCOPTION_NO_FONT_CHANGES**: Do not touch the font. Notcurses might otherwise attempt to extend the font, especially in the Linux console. +* **NCOPTION_DRAIN_INPUT**: Standard input may be freely discarded. If you do not + intend to process input, pass this flag. Otherwise, input can buffer up, and + eventually prevent Notcurses from processing messages from the terminal. It + will furthermore avoid wasting time processing useless input. + ## Fatal signals It is important to reset the terminal before exiting, whether terminating due diff --git a/doc/man/man3/notcurses_input.3.md b/doc/man/man3/notcurses_input.3.md index ba75a59c5..6f50564cf 100644 --- a/doc/man/man3/notcurses_input.3.md +++ b/doc/man/man3/notcurses_input.3.md @@ -31,6 +31,8 @@ typedef struct ncinput { **uint32_t notcurses_get(struct notcurses* ***n***, const struct timespec* ***ts***, ncinput* ***ni***);** +**int notcurses_getvec(struct notcurses* ***n***, const struct timespec* ***ts***, ncinput* ***ni***, int vcount);** + **uint32_t notcurses_getc_nblock(struct notcurses* ***n***, ncinput* ***ni***);** **uint32_t notcurses_getc_blocking(struct notcurses* ***n***, ncinput* ***ni***);** @@ -128,6 +130,10 @@ temporary one (especially e.g. **EINTR**), **notcurses_get** probably cannot be usefully called forthwith. On a timeout, 0 is returned. Otherwise, the UCS-32 value of a Unicode codepoint, or a synthesized event, is returned. +If an error is encountered before **notcurses_getvec** has read any input, +it will return -1. If it times out before reading any input, it will return +0. Otherwise, it returns the number of **ncinput** objects written back. + **notcurses_mouse_enable** returns 0 on success, and non-zero on failure, as does **notcurses_mouse_disable**. diff --git a/include/notcurses/direct.h b/include/notcurses/direct.h index 86651d39c..00bd10d30 100644 --- a/include/notcurses/direct.h +++ b/include/notcurses/direct.h @@ -24,6 +24,11 @@ extern "C" { // echo and input's line buffering are turned off. #define NCDIRECT_OPTION_INHIBIT_CBREAK 0x0002ull +// Input may be freely dropped. This ought be provided when the program does not +// intend to handle input. Otherwise, input can accumulate in internal buffers, +// eventually preventing Notcurses from processing terminal messages. +#define NCDIRECT_OPTION_DRAIN_INPUT 0x0004ull + // We typically install a signal handler for SIG{INT, SEGV, ABRT, QUIT} that // restores the screen, and then calls the old signal handler. Set to inhibit // registration of these signal handlers. Chosen to match fullscreen mode. @@ -489,11 +494,6 @@ ncdirect_canbraille(const struct ncdirect* nc){ API bool ncdirect_canget_cursor(const struct ncdirect* nc) __attribute__ ((nonnull (1))); -// Deprecated, to be removed for ABI3. Use ncdirect_get() in new code. -API uint32_t ncdirect_getc(struct ncdirect* n, const struct timespec* ts, - const void* unused, ncinput* ni) - __attribute__ ((deprecated)) __attribute__ ((nonnull (1))); - #undef ALLOC #undef API diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h index 8268be1b8..b3e4127fc 100644 --- a/include/notcurses/notcurses.h +++ b/include/notcurses/notcurses.h @@ -900,6 +900,11 @@ typedef enum { // anything but the virtual console/terminal in which Notcurses is running. #define NCOPTION_NO_FONT_CHANGES 0x0080ull +// Input may be freely dropped. This ought be provided when the program does not +// intend to handle input. Otherwise, input can accumulate in internal buffers, +// eventually preventing Notcurses from processing terminal messages. +#define NCOPTION_DRAIN_INPUT 0x0100ull + // Configuration for notcurses_init(). typedef struct notcurses_options { // The name of the terminfo database entry describing this terminal. If NULL, @@ -1066,6 +1071,12 @@ API uint32_t notcurses_get(struct notcurses* n, const struct timespec* ts, ncinput* ni) __attribute__ ((nonnull (1))); +// Acquire up to 'vcount' ncinputs at the vector 'ni'. The number read will be +// returned, or -1 on error without any reads, 0 on timeout. +API int notcurses_getvec(struct notcurses* n, const struct timespec* ts, + ncinput* ni, int vcount) + __attribute__ ((nonnull (1, 3))); + // Get a file descriptor suitable for input event poll()ing. When this // descriptor becomes available, you can call notcurses_getc_nblock(), // and input ought be ready. This file descriptor is *not* necessarily @@ -4244,233 +4255,6 @@ palette256_get_rgb8(const ncpalette* p, int idx, unsigned* RESTRICT r, unsigned* API void palette256_free(ncpalette* p) __attribute__ ((deprecated)); -__attribute__ ((deprecated)) static inline unsigned -channel_r(uint32_t channel){ - return ncchannel_r(channel); -} - -// Extract the 8-bit green component from a 32-bit channel. -__attribute__ ((deprecated)) static inline unsigned -channel_g(uint32_t channel){ - return ncchannel_g(channel); -} - -// Extract the 8-bit blue component from a 32-bit channel. -__attribute__ ((deprecated)) static inline unsigned -channel_b(uint32_t channel){ - return ncchannel_b(channel); -} - -// Extract the three 8-bit R/G/B components from a 32-bit channel. -__attribute__ ((deprecated)) static inline unsigned -channel_rgb8(uint32_t channel, unsigned* RESTRICT r, unsigned* RESTRICT g, - unsigned* RESTRICT b){ - return ncchannel_rgb8(channel, r, g, b); -} - -// Set the three 8-bit components of a 32-bit channel, and mark it as not using -// the default color. Retain the other bits unchanged. -__attribute__ ((deprecated)) static inline int -channel_set_rgb8(uint32_t* channel, int r, int g, int b){ - return ncchannel_set_rgb8(channel, r, g, b); -} - -// Set the three 8-bit components of a 32-bit channel, and mark it as not using -// the default color. Retain the other bits unchanged. r, g, and b will be -// clipped to the range [0..255]. -__attribute__ ((deprecated)) static inline void -channel_set_rgb8_clipped(unsigned* channel, int r, int g, int b){ - return ncchannel_set_rgb8_clipped(channel, r, g, b); -} - -// Same, but provide an assembled, packed 24 bits of rgb. -__attribute__ ((deprecated)) static inline int -channel_set(unsigned* channel, unsigned rgb){ - return ncchannel_set(channel, rgb); -} - -// Extract the 2-bit alpha component from a 32-bit channel. -__attribute__ ((deprecated)) static inline unsigned -channel_alpha(unsigned channel){ - return ncchannel_alpha(channel); -} - -__attribute__ ((deprecated)) static inline unsigned -channel_palindex(uint32_t channel){ - return ncchannel_palindex(channel); -} - -// Set the 2-bit alpha component of the 32-bit channel. -__attribute__ ((deprecated)) static inline int -channel_set_alpha(unsigned* channel, unsigned alpha){ - return ncchannel_set_alpha(channel, alpha); -} - -__attribute__ ((deprecated)) static inline int -channel_set_palindex(uint32_t* channel, int idx){ - return ncchannel_set_palindex(channel, idx); -} - -__attribute__ ((deprecated)) static inline bool -channel_default_p(unsigned channel){ - return ncchannel_default_p(channel); -} - -__attribute__ ((deprecated)) static inline bool -channel_palindex_p(unsigned channel){ - return ncchannel_palindex_p(channel); -} - -__attribute__ ((deprecated)) static inline unsigned -channel_set_default(unsigned* channel){ - return ncchannel_set_default(channel); -} - -__attribute__ ((deprecated)) static inline uint32_t -channels_bchannel(uint64_t channels){ - return ncchannels_bchannel(channels); -} - -__attribute__ ((deprecated)) static inline uint32_t -channels_fchannel(uint64_t channels){ - return ncchannels_fchannel(channels); -} - -__attribute__ ((deprecated)) static inline uint64_t -channels_set_bchannel(uint64_t* channels, uint32_t channel){ - return ncchannels_set_bchannel(channels, channel); -} - -__attribute__ ((deprecated)) static inline uint64_t -channels_set_fchannel(uint64_t* channels, uint32_t channel){ - return ncchannels_set_fchannel(channels, channel); -} - -__attribute__ ((deprecated)) static inline uint64_t -channels_combine(uint32_t fchan, uint32_t bchan){ - return ncchannels_combine(fchan, bchan); -} - -__attribute__ ((deprecated)) static inline unsigned -channels_fg_palindex(uint64_t channels){ - return ncchannels_fg_palindex(channels); -} - -__attribute__ ((deprecated)) static inline unsigned -channels_bg_palindex(uint64_t channels){ - return ncchannels_bg_palindex(channels); -} - -__attribute__ ((deprecated)) static inline unsigned -channels_fg_rgb(uint64_t channels){ - return ncchannels_fg_rgb(channels); -} - -__attribute__ ((deprecated)) static inline unsigned -channels_bg_rgb(uint64_t channels){ - return ncchannels_bg_rgb(channels); -} - -__attribute__ ((deprecated)) static inline unsigned -channels_fg_alpha(uint64_t channels){ - return ncchannels_fg_alpha(channels); -} - -__attribute__ ((deprecated)) static inline unsigned -channels_bg_alpha(uint64_t channels){ - return ncchannels_bg_alpha(channels); -} - -__attribute__ ((deprecated)) static inline unsigned -channels_fg_rgb8(uint64_t channels, unsigned* r, unsigned* g, unsigned* b){ - return ncchannels_fg_rgb8(channels, r, g, b); -} - -__attribute__ ((deprecated)) static inline unsigned -channels_bg_rgb8(uint64_t channels, unsigned* r, unsigned* g, unsigned* b){ - return ncchannels_bg_rgb8(channels, r, g, b); -} - -__attribute__ ((deprecated)) static inline int -channels_set_fg_rgb8(uint64_t* channels, int r, int g, int b){ - return ncchannels_set_fg_rgb8(channels, r, g, b); -} - -__attribute__ ((deprecated)) static inline void -channels_set_fg_rgb8_clipped(uint64_t* channels, int r, int g, int b){ - ncchannels_set_fg_rgb8_clipped(channels, r, g, b); -} - -__attribute__ ((deprecated)) static inline int -channels_set_fg_alpha(uint64_t* channels, unsigned alpha){ - return ncchannels_set_fg_alpha(channels, alpha); -} - -__attribute__ ((deprecated)) static inline int -channels_set_fg_palindex(uint64_t* channels, int idx){ - return ncchannels_set_bg_palindex(channels, idx); -} - -__attribute__ ((deprecated)) static inline int -channels_set_fg_rgb(uint64_t* channels, unsigned rgb){ - return ncchannels_set_fg_rgb(channels, rgb); -} - -__attribute__ ((deprecated)) static inline int -channels_set_bg_rgb8(uint64_t* channels, int r, int g, int b){ - return ncchannels_set_bg_rgb8(channels, r, g, b); -} - -__attribute__ ((deprecated)) static inline void -channels_set_bg_rgb8_clipped(uint64_t* channels, int r, int g, int b){ - ncchannels_set_bg_rgb8_clipped(channels, r, g, b); -} - -__attribute__ ((deprecated)) static inline int -channels_set_bg_alpha(uint64_t* channels, unsigned alpha){ - return ncchannels_set_bg_alpha(channels, alpha); -} - -__attribute__ ((deprecated)) static inline int -channels_set_bg_palindex(uint64_t* channels, int idx){ - return ncchannels_set_bg_palindex(channels, idx); -} - -__attribute__ ((deprecated)) static inline int -channels_set_bg_rgb(uint64_t* channels, unsigned rgb){ - return ncchannels_set_bg_rgb(channels, rgb); -} - -__attribute__ ((deprecated)) static inline bool -channels_fg_default_p(uint64_t channels){ - return ncchannels_fg_default_p(channels); -} - -__attribute__ ((deprecated)) static inline bool -channels_fg_palindex_p(uint64_t channels){ - return ncchannels_fg_palindex_p(channels); -} - -__attribute__ ((deprecated)) static inline bool -channels_bg_default_p(uint64_t channels){ - return ncchannels_bg_default_p(channels); -} - -__attribute__ ((deprecated)) static inline bool -channels_bg_palindex_p(uint64_t channels){ - return ncchannels_bg_palindex_p(channels); -} - -__attribute__ ((deprecated)) static inline uint64_t -channels_set_fg_default(uint64_t* channels){ - return ncchannels_set_fg_default(channels); -} - -__attribute__ ((deprecated)) static inline uint64_t -channels_set_bg_default(uint64_t* channels){ - return ncchannels_set_bg_default(channels); -} - // Inflate each pixel in the image to 'scale'x'scale' pixels. It is an error // if 'scale' is less than 1. The original color is retained. // Deprecated; use ncvisual_resize_noninterpolative(), which this now wraps. @@ -4488,12 +4272,6 @@ typedef nccell cell; // FIXME backwards-compat, remove in ABI3 API void notcurses_debug_caps(const struct notcurses* nc, FILE* debugfp) __attribute__ ((deprecated)) __attribute__ ((nonnull (1, 2))); -// Backwards-compatibility wrapper; this will be removed for ABI3. -// Use notcurses_get() in new code. -API uint32_t notcurses_getc(struct notcurses* n, const struct timespec* ts, - const void* unused, ncinput* ni) - __attribute__ ((deprecated)) __attribute__ ((nonnull (1))); - __attribute__ ((deprecated)) API int nccell_width(const struct ncplane* n, const nccell* c); API ALLOC char* ncvisual_subtitle(const struct ncvisual* ncv) diff --git a/src/lib/direct.c b/src/lib/direct.c index 9a6dc5f34..99f5d7102 100644 --- a/src/lib/direct.c +++ b/src/lib/direct.c @@ -864,9 +864,7 @@ ncdirect_stop_minimal(void* vnc){ ret |= fbuf_finalize(&f, stdout); } if(nc->tcache.ttyfd >= 0){ - if(nc->tcache.kittykbd){ - ret |= tty_emit("\x1b[tcache.ttyfd); - } + ret |= tty_emit("\x1b[tcache.ttyfd); const char* cnorm = get_escape(&nc->tcache, ESCAPE_CNORM); if(cnorm && tty_emit(cnorm, nc->tcache.ttyfd)){ ret = -1; @@ -883,7 +881,7 @@ ncdirect* ncdirect_core_init(const char* termtype, FILE* outfp, uint64_t flags){ if(outfp == NULL){ outfp = stdout; } - if(flags > (NCDIRECT_OPTION_VERY_VERBOSE << 1)){ // allow them through with warning + if(flags > (NCDIRECT_OPTION_DRAIN_INPUT << 1)){ // allow them through with warning logwarn("Passed unsupported flags 0x%016jx\n", (uintmax_t)flags); } ncdirect* ret = malloc(sizeof(ncdirect)); diff --git a/src/lib/gpm.c b/src/lib/gpm.c index 3ade79abe..40f3fafc3 100644 --- a/src/lib/gpm.c +++ b/src/lib/gpm.c @@ -32,7 +32,7 @@ gpmwatcher(void* vti){ logwarn("input overflowed %hd %hd\n", gev.x, gev.y); continue; } - ncinput_shovel(&ti->input, cmdbuf, strlen(cmdbuf)); + ncinput_shovel(ti->ictx, cmdbuf, strlen(cmdbuf)); } return NULL; } diff --git a/src/lib/in.c b/src/lib/in.c index 4cbcca682..cb2a221df 100644 --- a/src/lib/in.c +++ b/src/lib/in.c @@ -2,6 +2,23 @@ #include "internal.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 (NCOPTION_TOSS_INPUT) + static sig_atomic_t resize_seen; // called for SIGWINCH and SIGCONT @@ -24,21 +41,79 @@ typedef struct cursorloc { int y, x; // 0-indexed cursor location } cursorloc; -// local state for the input thread +typedef enum { + STATE_NULL, + STATE_ESC, // escape; aborts any active sequence + STATE_CSI, // control sequence introducer + STATE_DCS, // device control string + // XTVERSION replies with DCS > | ... ST + STATE_XTVERSION1, + STATE_XTVERSION2, + // XTGETTCAP replies with DCS 1 + r for a good request, or 0 + r for bad + STATE_XTGETTCAP1, // XTGETTCAP, got '0/1' (DCS 0/1 + r Pt ST) + STATE_XTGETTCAP2, // XTGETTCAP, got '+' (DCS 0/1 + r Pt ST) + STATE_XTGETTCAP3, // XTGETTCAP, got 'r' (DCS 0/1 + r Pt ST) + STATE_XTGETTCAP_TERMNAME1, // got property 544E, 'TN' (terminal name) first hex nibble + STATE_XTGETTCAP_TERMNAME2, // got property 544E, 'TN' (terminal name) second hex nibble + STATE_DCS_DRAIN, // throw away input until we hit escape + STATE_APC, // application programming command, starts with \x1b_ + STATE_APC_DRAIN, // looking for \x1b + STATE_APC_ST, // looking for ST + STATE_BG1, // got '1' + STATE_BG2, // got second '1' + STATE_BGSEMI, // got '11;', draining string to ESC ST + STATE_TDA1, // tertiary DA, got '!' + STATE_TDA2, // tertiary DA, got '|', first hex nibble + STATE_TDA3, // tertiary DA, second hex nibble + STATE_SDA, // secondary DA (CSI > Pp ; Pv ; Pc c) + STATE_SDA_VER, // secondary DA, got semi, reading to next semi + STATE_SDA_DRAIN, // drain secondary DA to 'c' + STATE_DA, // primary DA (CSI ? ... c) OR XTSMGRAPHICS OR DECRPM or kittykbd + STATE_DA_DRAIN, // drain out the primary DA to an alpha + STATE_DA_SEMI, // got first semicolon following numeric + STATE_DA_SEMI2, // got second semicolon following numeric ; numeric + STATE_DA_SEMI3, // got third semicolon following numeric ; numeric ; numeric + STATE_APPSYNC_REPORT, // got DECRPT ?2026 + STATE_APPSYNC_REPORT_DRAIN, // drain out decrpt to 'y' + // cursor location report: CSI row ; col R + // text area pixel geometry: CSI 4 ; rows ; cols t + // text area cell geometry: CSI 8 ; rows ; cols t + // so we handle them the same until we hit either a second semicolon or an + // 'R' or 't'. at the second ';', we verify that the first variable was + // '4' or '8', and continue to 't' via STATE_{PIXELS,CELLS}_WIDTH. + STATE_CURSOR_OR_PIXELGEOM, // reading row of cursor location to ';' + STATE_CURSOR_COL, // reading col of cursor location to 'R', 't', or ';' + STATE_PIXELS_WIDTH, // reading text area width in pixels to ';' + STATE_CELLS_WIDTH, // reading text area width in cells to ';' +} initstates_e; + +// local state for the input thread. don't put this large struct on the stack. typedef struct inputctx { - int termfd; // terminal fd: -1 with no controlling terminal, or - // if stdin is a terminal, and on Windows Terminal. 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 __MINGW64__ - HANDLE stdinhandle; // handle to input terminal + HANDLE stdinhandle; // handle to input terminal for MSFT Terminal #endif - unsigned char ibuf[BUFSIZ]; // we dump raw reads into this ringbuffer, and - // process them all post-read - int ibufvalid; // we mustn't read() if ibufvalid == sizeof(ibuf) - int ibufwrite; // we write here next - int ibufread; // first valid byte here (if any are valid) + // 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 ibuf[BUFSIZ]; // might be intermingled bulk/control data + unsigned char tbuf[BUFSIZ]; // only used if we have distinct terminal fd + int ibufvalid; // we mustn't read() if ibufvalid == sizeof(ibuf) + int tbufvalid; // only used if we have distinct terminal connection + + // transient state for processing control sequences + // stringstate is the state at which this string was initialized, and can be + // one of STATE_XTVERSION1, STATE_XTGETTCAP_TERMNAME1, STATE_TDA1, and STATE_BG1 + initstates_e state, stringstate; + int numeric; // currently-lexed numeric + char runstring[BUFSIZ]; // running string (when stringstate != STATE_NULL) + int stridx; // length of runstring + int p2, p3, p4; // holders for numeric params + + // ringbuffers for processed, structured input cursorloc* csrs; // cursor reports are dumped here ncinput* inputs; // processed input is dumped here int csize, isize; // total number of slots in csrs/inputs @@ -47,8 +122,15 @@ typedef struct inputctx { // 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 + + struct initial_responses* initdata; + struct initial_responses* initdata_complete; } inputctx; static inline inputctx* @@ -59,18 +141,39 @@ create_inputctx(tinfo* ti, FILE* infp){ if( (i->csrs = malloc(sizeof(*i->csrs) * i->csize)) ){ i->isize = BUFSIZ; if( (i->inputs = malloc(sizeof(*i->inputs) * i->isize)) ){ - if((i->stdinfd = fileno(infp)) >= 0){ - if(set_fd_nonblocking(i->stdinfd, 1, &ti->stdio_blocking_save) == 0){ - i->termfd = tty_check(i->stdinfd) ? -1 : get_tty_fd(infp); - i->ti = ti; - i->cvalid = i->ivalid = 0; - i->cwrite = i->iwrite = 0; - i->cread = i->iread = 0; - i->ibufvalid = i->ibufwrite = 0; - i->ibufread = 0; - logdebug("input descriptors: %d/%d\n", i->stdinfd, i->termfd); - return i; + if(pthread_mutex_init(&i->ilock, NULL) == 0){ + if(pthread_cond_init(&i->icond, NULL) == 0){ + if(pthread_mutex_init(&i->clock, NULL) == 0){ + if(pthread_cond_init(&i->ccond, NULL) == 0){ + if((i->stdinfd = fileno(infp)) >= 0){ + if( (i->initdata = malloc(sizeof(*i->initdata))) ){ + if(set_fd_nonblocking(i->stdinfd, 1, &ti->stdio_blocking_save) == 0){ + memset(i->initdata, 0, sizeof(*i->initdata)); + i->termfd = tty_check(i->stdinfd) ? -1 : get_tty_fd(infp); + i->ti = ti; + i->cvalid = i->ivalid = 0; + i->cwrite = i->iwrite = 0; + i->cread = i->iread = 0; + i->ibufvalid = 0; + i->tbufvalid = 0; + i->state = i->stringstate = STATE_NULL; + i->numeric = 0; + i->stridx = 0; + i->initdata_complete = NULL; + i->runstring[i->stridx] = '\0'; + logdebug("input descriptors: %d/%d\n", i->stdinfd, i->termfd); + return i; + } + } + 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); } @@ -88,79 +191,895 @@ free_inputctx(inputctx* i){ 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); // do not kill the thread here, either. + if(i->initdata){ + free(i->initdata->version); + free(i->initdata); + } + if(i->initdata_complete){ + free(i->initdata_complete->version); + free(i->initdata_complete); + } free(i->inputs); free(i->csrs); free(i); } } -// how many bytes can a single read fill in the ibuf? this might be fewer than -// the actual number of free bytes, due to reading on the right or left side. -static inline size_t -space_for_read(inputctx* ictx){ - // if we are valid everywhere, there's no space to read into. - if(ictx->ibufvalid == sizeof(ictx->ibuf)){ - 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){ + if(fd < 0){ + return; } - // if we are valid nowhere, we can read into the head of the buffer - if(ictx->ibufvalid == 0){ - ictx->ibufread = 0; - ictx->ibufwrite = 0; - return sizeof(ictx->ibuf); + size_t space = buflen - *bufused; + if(space == 0){ + return; } - // otherwise, we can read either from ibufwrite to the end of the buffer, - // or from ibufwrite to ibufread. - if(ictx->ibufwrite < ictx->ibufread){ - return ictx->ibufread - ictx->ibufwrite; + ssize_t r = read(fd, buf + *bufused, space); + if(r <= 0){ + if(r < 0){ + logwarn("couldn't read from %d (%s)\n", fd, strerror(errno)); + } + return; } - return sizeof(ictx->ibuf) - ictx->ibufwrite; + *bufused += r; + space -= r; + loginfo("read %lldB from %d (%lluB left)\n", (long long)r, fd, (unsigned long long)space); +} + +// are terminal and stdin distinct for this inputctx? +static inline bool +ictx_independent_p(const inputctx* ictx){ + return ictx->termfd >= 0; // FIXME does this hold on MSFT Terminal? +} + +static int +ruts_numeric(int* numeric, unsigned char c){ + if(!isdigit(c)){ + return -1; + } + int digit = c - '0'; + if(INT_MAX / 10 - digit < *numeric){ // would overflow + return -1; + } + *numeric *= 10; + *numeric += digit; + return 0; +} + +static int +ruts_hex(int* numeric, unsigned char c){ + if(!isxdigit(c)){ + return -1; + } + int digit; + if(isdigit(c)){ + digit = c - '0'; + }else if(islower(c)){ + digit = c - 'a' + 10; + }else if(isupper(c)){ + digit = c - 'A' + 10; + }else{ + return -1; // should be impossible to reach + } + if(INT_MAX / 10 - digit < *numeric){ // would overflow + return -1; + } + *numeric *= 16; + *numeric += digit; + return 0; +} + +// add a decoded hex byte to the string +static int +ruts_string(inputctx* ictx, initstates_e state){ + if(ictx->stridx == sizeof(ictx->runstring)){ + return -1; // overflow, too long + } + if(ictx->numeric > 255){ + return -1; + } + unsigned char c = ictx->numeric; + if(!isprint(c)){ + return -1; + } + ictx->stringstate = state; + ictx->runstring[ictx->stridx] = c; + ictx->runstring[++ictx->stridx] = '\0'; + return 0; +} + +// extract the terminal version from the running string, following 'prefix' +static int +extract_version(inputctx* ictx, size_t slen){ + size_t bytes = strlen(ictx->runstring + slen) + 1; + ictx->initdata->version = malloc(bytes); + if(ictx->initdata->version == NULL){ + return -1; + } + memcpy(ictx->initdata->version, ictx->runstring + slen, bytes); + return 0; +} + +static int +extract_xtversion(inputctx* ictx, size_t slen, char suffix){ + if(suffix){ + if(ictx->runstring[ictx->stridx - 1] != suffix){ + return -1; + } + ictx->runstring[ictx->stridx - 1] = '\0'; + } + return extract_version(ictx, slen); +} + +static int +stash_string(inputctx* ictx){ + struct initial_responses* inits = ictx->initdata; +//fprintf(stderr, "string terminator after %d [%s]\n", inits->stringstate, inits->runstring); + switch(ictx->stringstate){ + case STATE_XTVERSION1:{ + 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 = NULL, .suffix = 0, .term = TERMINAL_UNKNOWN, }, + }, *xtv; + for(xtv = xtvers ; xtv->prefix ; ++xtv){ + if(strncmp(ictx->runstring, xtv->prefix, strlen(xtv->prefix)) == 0){ + if(extract_xtversion(ictx, strlen(xtv->prefix), xtv->suffix) == 0){ + inits->qterm = xtv->term; + } + break; + } + } + if(xtv->prefix == NULL){ + logwarn("Unrecognizable XTVERSION [%s]\n", ictx->runstring); + } + break; + }case STATE_XTGETTCAP_TERMNAME1: + if(strcmp(ictx->runstring, "xterm-kitty") == 0){ + inits->qterm = TERMINAL_KITTY; + }else if(strcmp(ictx->runstring, "mlterm") == 0){ + // MLterm prior to late 3.9.1 only reports via XTGETTCAP + inits->qterm = TERMINAL_MLTERM; + } + break; + case STATE_TDA1: + if(strcmp(ictx->runstring, "~VTE") == 0){ + inits->qterm = TERMINAL_VTE; + }else if(strcmp(ictx->runstring, "~~TY") == 0){ + inits->qterm = TERMINAL_TERMINOLOGY; + }else if(strcmp(ictx->runstring, "FOOT") == 0){ + inits->qterm = TERMINAL_FOOT; + } + break; + case STATE_BG1:{ + int r, g, b; + if(sscanf(ictx->runstring, "rgb:%02x/%02x/%02x", &r, &g, &b) == 3){ + // great! =] + }else if(sscanf(ictx->runstring, "rgb:%04x/%04x/%04x", &r, &g, &b) == 3){ + r /= 256; + g /= 256; + b /= 256; + }else{ + break; + } + inits->bg = (r << 16u) | (g << 8u) | b; + break; + }default: +// don't generally enable this -- XTerm terminates TDA with ST +//fprintf(stderr, "invalid string [%s] stashed %d\n", inits->runstring, inits->stringstate); + break; + } + ictx->runstring[0] = '\0'; + ictx->stridx = 0; + return 0; +} + +// use the version extracted from Secondary Device Attributes, assuming that +// it is Alacritty (we ought check the specified terminfo database entry). +// Alacritty writes its crate version with each more significant portion +// multiplied by 100^{portion ID}, where major, minor, patch are 2, 1, 0. +// what happens when a component exceeds 99? who cares. support XTVERSION. +static char* +set_sda_version(inputctx* ictx){ + int maj, min, patch; + if(ictx->numeric <= 0){ + return NULL; + } + maj = ictx->numeric / 10000; + min = (ictx->numeric % 10000) / 100; + patch = ictx->numeric % 100; + if(maj >= 100 || min >= 100 || patch >= 100){ + return NULL; + } + // 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); + } + return buf; +} + +// FIXME ought implement the full Williams automaton +// FIXME sloppy af in general +// returns 1 after handling the Device Attributes response, 0 if more input +// ought be fed to the machine, and -1 on an invalid state transition. +static int +pump_control_read(inputctx* ictx, unsigned char c){ + logdebug("state: %2d char: %1c %3d %02x\n", ictx->state, isprint(c) ? c : ' ', c, c); + if(c == NCKEY_ESC){ + ictx->state = STATE_ESC; + return 0; + } + switch(ictx->state){ + case STATE_NULL: + // not an escape -- throw into user queue + break; + case STATE_ESC: + ictx->numeric = 0; + if(c == '['){ + ictx->state = STATE_CSI; + }else if(c == 'P'){ + ictx->state = STATE_DCS; + }else if(c == '\\'){ + if(stash_string(ictx)){ + return -1; + } + ictx->state = STATE_NULL; + }else if(c == '1'){ + ictx->state = STATE_BG1; + }else if(c == '_'){ + ictx->state = STATE_APC; + } + break; + case STATE_APC: + if(c == 'G'){ + ictx->initdata->kitty_graphics = true; + } + ictx->state = STATE_APC_DRAIN; + break; + case STATE_APC_DRAIN: + if(c == '\x1b'){ + ictx->state = STATE_APC_ST; + } + break; + case STATE_APC_ST: + if(c == '\\'){ + ictx->state = STATE_NULL; + }else{ + ictx->state = STATE_APC_DRAIN; + } + break; + case STATE_BG1: + if(c == '1'){ + ictx->state = STATE_BG2; + }else{ + // FIXME + } + break; + case STATE_BG2: + if(c == ';'){ + ictx->state = STATE_BGSEMI; + ictx->stridx = 0; + ictx->runstring[0] = '\0'; + }else{ + // FIXME + } + break; + case STATE_BGSEMI: // drain string + if(c == '\x07'){ // contour sends this at the end for some unknown reason + if(stash_string(ictx)){ + return -1; + } + ictx->state = STATE_NULL; + break; + } + ictx->numeric = c; + if(ruts_string(ictx, STATE_BG1)){ + return -1; + } + break; + case STATE_CSI: // terminated by 0x40--0x7E ('@'--'~') + if(c == '?'){ + ictx->state = STATE_DA; // could also be DECRPM/XTSMGRAPHICS/kittykbd + }else if(c == '>'){ + // SDA yields up Alacritty's crate version, but it doesn't unambiguously + // identify Alacritty. If we've got any other version information, skip + // directly to STATE_SDA_DRAIN, rather than doing STATE_SDA_VER. + if(ictx->initdata->qterm || ictx->initdata->version){ + loginfo("Identified terminal already; ignoring DA2\n"); + ictx->state = STATE_SDA_DRAIN; + }else{ + ictx->state = STATE_SDA; + } + }else if(isdigit(c)){ + ictx->numeric = 0; + if(ruts_numeric(&ictx->numeric, c)){ + return -1; + } + ictx->state = STATE_CURSOR_OR_PIXELGEOM; + }else if(c >= 0x40 && c <= 0x7E){ + ictx->state = STATE_NULL; + } + break; + case STATE_CURSOR_OR_PIXELGEOM: + if(isdigit(c)){ + if(ruts_numeric(&ictx->numeric, c)){ + return -1; + } + }else if(c == ';'){ + ictx->p2 = ictx->numeric; + ictx->state = STATE_CURSOR_COL; + ictx->numeric = 0; + }else{ + ictx->state = STATE_NULL; + } + break; + case STATE_CURSOR_COL: + if(isdigit(c)){ + if(ruts_numeric(&ictx->numeric, c)){ + return -1; + } + }else if(c == 'R'){ +//fprintf(stderr, "CURSOR X: %d\n", ictx->numeric); + if(ictx->initdata){ + ictx->initdata->cursorx = ictx->numeric - 1; + ictx->initdata->cursory = ictx->p2 - 1; + }else{ + pthread_mutex_lock(&ictx->clock); + if(ictx->cvalid == ictx->csize){ + pthread_mutex_unlock(&ictx->clock); + logwarn("dropping cursor location report\n"); + }else{ + cursorloc* cloc = &ictx->csrs[ictx->cwrite]; + cloc->x = ictx->numeric - 1; + cloc->y = ictx->p2 - 1; + if(++ictx->cwrite == ictx->csize){ + ictx->cwrite = 0; + } + ++ictx->cvalid; + pthread_mutex_unlock(&ictx->clock); + pthread_cond_broadcast(&ictx->ccond); + } + } + ictx->state = STATE_NULL; + }else if(c == 't'){ +//fprintf(stderr, "CELLS X: %d\n", ictx->numeric); + ictx->initdata->dimx = ictx->numeric; + ictx->initdata->dimy = ictx->p2; + ictx->state = STATE_NULL; + }else if(c == ';'){ + if(ictx->p2 == 4){ + ictx->initdata->pixy = ictx->numeric; + ictx->state = STATE_PIXELS_WIDTH; + ictx->numeric = 0; + }else if(ictx->p2 == 8){ + ictx->initdata->dimy = ictx->numeric; + ictx->state = STATE_CELLS_WIDTH; + ictx->numeric = 0; + }else{ + logerror("expected 4 to lead pixel report, got %d\n", ictx->p2); + return -1; + } + }else{ + ictx->state = STATE_NULL; + } + break; + case STATE_PIXELS_WIDTH: + if(isdigit(c)){ + if(ruts_numeric(&ictx->numeric, c)){ + return -1; + } + }else if(c == 't'){ + ictx->initdata->pixx = ictx->numeric; + loginfo("got pixel geometry: %d/%d\n", ictx->initdata->pixy, ictx->initdata->pixx); + ictx->state = STATE_NULL; + }else{ + ictx->state = STATE_NULL; + } + break; + case STATE_CELLS_WIDTH: + if(isdigit(c)){ + if(ruts_numeric(&ictx->numeric, c)){ + return -1; + } + }else if(c == 't'){ + ictx->initdata->dimx = ictx->numeric; + loginfo("got cell geometry: %d/%d\n", ictx->initdata->dimy, ictx->initdata->dimx); + ictx->state = STATE_NULL; + }else{ + ictx->state = STATE_NULL; + } + break; + case STATE_DCS: // terminated by ST + if(c == '\\'){ +//fprintf(stderr, "terminated DCS\n"); + ictx->state = STATE_NULL; + }else if(c == '1'){ + ictx->state = STATE_XTGETTCAP1; // we have tcap + }else if(c == '0'){ + ictx->state = STATE_XTGETTCAP1; // no tcap for us + }else if(c == '>'){ + ictx->state = STATE_XTVERSION1; + }else if(c == '!'){ + ictx->state = STATE_TDA1; + }else{ + ictx->state = STATE_DCS_DRAIN; + } + break; + case STATE_DCS_DRAIN: + // we drain to ST, which is an escape, and thus already handled, so... + break; + case STATE_XTVERSION1: + if(c == '|'){ + ictx->state = STATE_XTVERSION2; + ictx->stridx = 0; + ictx->runstring[0] = '\0'; + }else{ + // FIXME error? + } + break; + case STATE_XTVERSION2: + ictx->numeric = c; + if(ruts_string(ictx, STATE_XTVERSION1)){ + return -1; + } + break; + case STATE_XTGETTCAP1: + if(c == '+'){ + ictx->state = STATE_XTGETTCAP2; + }else{ + // FIXME malformed + } + break; + case STATE_XTGETTCAP2: + if(c == 'r'){ + ictx->state = STATE_XTGETTCAP3; + }else{ + // FIXME malformed + } + break; + case STATE_XTGETTCAP3: + if(c == '='){ + if(ictx->numeric == 0x544e){ + ictx->state = STATE_XTGETTCAP_TERMNAME1; + ictx->stridx = 0; + ictx->numeric = 0; + ictx->runstring[0] = '\0'; + }else{ + ictx->state = STATE_DCS_DRAIN; + } + }else if(ruts_hex(&ictx->numeric, c)){ + return -1; + } + break; + case STATE_XTGETTCAP_TERMNAME1: + if(ruts_hex(&ictx->numeric, c)){ + return -1; + } + ictx->state = STATE_XTGETTCAP_TERMNAME2; + break; + case STATE_XTGETTCAP_TERMNAME2: + if(ruts_hex(&ictx->numeric, c)){ + return -1; + } + ictx->state = STATE_XTGETTCAP_TERMNAME1; + if(ruts_string(ictx, STATE_XTGETTCAP_TERMNAME1)){ + return -1; + } + ictx->numeric = 0; + break; + case STATE_TDA1: + if(c == '|'){ + ictx->state = STATE_TDA2; + ictx->stridx = 0; + ictx->runstring[0] = '\0'; + }else{ + // FIXME + } + break; + case STATE_TDA2: + if(ruts_hex(&ictx->numeric, c)){ + return -1; + } + ictx->state = STATE_TDA3; + break; + case STATE_TDA3: + if(ruts_hex(&ictx->numeric, c)){ + return -1; + } + ictx->state = STATE_TDA2; + if(ruts_string(ictx, STATE_TDA1)){ + ictx->state = STATE_DCS_DRAIN; // FIXME return -1? + } + ictx->numeric = 0; + break; + case STATE_SDA: + if(c == ';'){ + ictx->state = STATE_SDA_VER; + ictx->numeric = 0; + }else if(c == 'c'){ + ictx->state = STATE_NULL; + } + break; + case STATE_SDA_VER: + if(c == ';'){ + ictx->state = STATE_SDA_DRAIN; + loginfo("Got DA2 Pv: %u\n", ictx->numeric); + // if a version was set, we couldn't have arrived here. alacritty + // writes its crate version here, in an encoded form. nothing else + // necessarily does, though, so allow failure. this value will be + // interpreted as the version only if TERM indicates alacritty. + ictx->initdata->version = set_sda_version(ictx); + }else if(ruts_numeric(&ictx->numeric, c)){ + return -1; + } + break; + case STATE_SDA_DRAIN: + if(c == 'c'){ + ictx->state = STATE_NULL; + } + break; + // primary device attributes and XTSMGRAPHICS replies are generally + // indistinguishable until well into the escape. one can get: + // XTSMGRAPHICS: CSI ? Pi ; Ps ; Pv S {Pi: 123} {Ps: 0123} + // DECRPM: CSI ? Pd ; Ps $ y {Pd: many} {Ps: 01234} + // DA: CSI ? 1 ; 2 c ("VT100 with Advanced Video Option") + // CSI ? 1 ; 0 c ("VT101 with No Options") + // CSI ? 4 ; 6 c ("VT132 with Advanced Video and Graphics") + // CSI ? 6 c ("VT102") + // CSI ? 7 c ("VT131") + // CSI ? 1 2 ; Ps c ("VT125") + // CSI ? 6 2 ; Ps c ("VT220") + // CSI ? 6 3 ; Ps c ("VT320") + // CSI ? 6 4 ; Ps c ("VT420") + // KITTYKBD: CSI ? flags u + case STATE_DA: // return success on end of DA +//fprintf(stderr, "DA: %c\n", c); + // FIXME several of these numbers could be DECRPM/XTSM/kittykbd. probably + // just want to read number, *then* make transition on non-number. + if(isdigit(c)){ + if(ruts_numeric(&ictx->numeric, c)){ // stash for DECRPM/XTSM/kittykbd + return -1; + } + }else if(c == 'u'){ // kitty keyboard + loginfo("keyboard protocol 0x%x\n", ictx->numeric); + ictx->state = STATE_NULL; + }else if(c == ';'){ + ictx->p2 = ictx->numeric; + ictx->numeric = 0; + ictx->state = STATE_DA_SEMI; + }else if(c >= 0x40 && c <= 0x7E){ + ictx->state = STATE_NULL; + if(c == 'c'){ + return 1; + } + } + break; + case STATE_DA_SEMI: + if(c == ';'){ + ictx->p3 = ictx->numeric; + ictx->numeric = 0; + ictx->state = STATE_DA_SEMI2; + }else if(isdigit(c)){ + if(ruts_numeric(&ictx->numeric, c)){ + return -1; + } + }else if(c == '$'){ + if(ictx->p2 == 2026){ + ictx->state = STATE_APPSYNC_REPORT; + loginfo("terminal reported SUM support\n"); + }else{ + ictx->state = STATE_APPSYNC_REPORT_DRAIN; + } + }else if(c >= 0x40 && c <= 0x7E){ + ictx->state = STATE_NULL; + if(c == 'c'){ + return 1; + } + } + break; + case STATE_DA_SEMI2: + if(c == ';'){ + ictx->p4 = ictx->numeric; + ictx->numeric = 0; + ictx->state = STATE_DA_SEMI3; + }else if(isdigit(c)){ + if(ruts_numeric(&ictx->numeric, c)){ + return -1; + } + }else if(c == 'S'){ + if(ictx->p2 == 1){ + ictx->initdata->color_registers = ictx->numeric; + loginfo("sixel color registers: %d\n", ictx->initdata->color_registers); + ictx->numeric = 0; + } + ictx->state = STATE_NULL; + }else if(c >= 0x40 && c <= 0x7E){ + ictx->state = STATE_NULL; + if(c == 'c'){ + return 1; + } + } + break; + case STATE_DA_DRAIN: + if(c >= 0x40 && c <= 0x7E){ + ictx->state = STATE_NULL; + if(c == 'c'){ + return 1; + } + } + break; + case STATE_DA_SEMI3: + if(c == ';'){ + ictx->numeric = 0; + ictx->state = STATE_DA_DRAIN; + }else if(isdigit(c)){ + if(ruts_numeric(&ictx->numeric, c)){ + return -1; + } + }else if(c == 'S'){ + ictx->initdata->sixelx = ictx->p4; + ictx->initdata->sixely = ictx->numeric; + loginfo("max sixel geometry: %dx%d\n", ictx->initdata->sixely, ictx->initdata->sixelx); + }else if(c >= 0x40 && c <= 0x7E){ + ictx->state = STATE_NULL; + if(c == 'c'){ + return 1; + } + } + break; + case STATE_APPSYNC_REPORT: + if(ictx->numeric == '2'){ + ictx->initdata->appsync_supported = 1; + ictx->state = STATE_APPSYNC_REPORT_DRAIN; + } + break; + case STATE_APPSYNC_REPORT_DRAIN: + if(c == 'y'){ + ictx->state = STATE_NULL; + } + break; + default: + fprintf(stderr, "Reached invalid init state %d\n", ictx->state); + return -1; + } + return 0; } -// populate the ibuf with any new data from the specified file descriptor. static void -read_input_nblock(inputctx* ictx, int fd){ - if(fd >= 0){ - size_t space = space_for_read(ictx); - if(space == 0){ - return; +handoff_initial_responses(inputctx* ictx){ + pthread_mutex_lock(&ictx->ilock); + ictx->initdata_complete = ictx->initdata; + ictx->initdata = NULL; + pthread_mutex_unlock(&ictx->ilock); + pthread_cond_broadcast(&ictx->icond); +} + +// try to lex a control sequence off of buf. return the number of bytes +// consumed if we do so, and -1 otherwise. buf is *not* necessarily +// NUL-terminated. precondition: buflen >= 1. +// FIXME we ought play complete failures into the general input buffer? +static int +process_escape(inputctx* ictx, const unsigned char* buf, int buflen){ + int used = 0; + while(used < buflen){ + int r = pump_control_read(ictx, buf[used]); + if(r == 1){ + handoff_initial_responses(ictx); } - ssize_t r = read(fd, ictx->ibuf + ictx->ibufwrite, space); - if(r >= 0){ - ictx->ibufwrite += r; - if(ictx->ibufwrite == sizeof(ictx->ibuf)){ - ictx->ibufwrite = 0; - } - ictx->ibufvalid += r; - space -= r; - loginfo("read %lldB from %d (%lluB left)\n", (long long)r, fd, (unsigned long long)space); - if(space == 0){ - return; - } - // might have been falsely limited by space (only reading on the right). - // this will recurse one time at most. - if(ictx->ibufwrite == 0){ - read_input_nblock(ictx, fd); - } + ++used; + } + return used; +} + +// process as many control sequences from |buf|, having |bufused| bytes, +// as we can. anything not a valid control sequence is dropped. this text +// needn't be valid UTF-8. +static void +process_escapes(inputctx* ictx, unsigned char* buf, int* bufused){ + int offset = 0; + while(*bufused){ + int consumed = process_escape(ictx, buf + offset, *bufused); + if(consumed < 0){ + break; } + *bufused -= consumed; + offset += consumed; + } + // move any leftovers to the front; only happens if we fill output queue + if(*bufused){ + memmove(buf, buf + offset, *bufused); + } +} + +// precondition: buflen >= 1. +static int +process_input(const unsigned char* buf, int buflen, ncinput* ni){ + memset(ni, 0, sizeof(*ni)); + if(buf[0] < 0x80){ // pure ascii can't show up mid-utf8 + ni->id = buf[0]; + return 1; + } + fprintf(stderr, "wanna parse us up an input! %d\n", buflen); + // FIXME extract supraascii UTF8, modifiers, mice + return 0; +} + +// 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){ + if(ictx->ivalid == sizeof(ictx->ivalid)){ + logwarn("blocking on input output queue (%d+%d)\n", ictx->ivalid, buflen); + return 0; + } + ncinput* ni = ictx->inputs + ictx->iwrite; + int r = process_input(buf, buflen, ni); + if(r > 0){ + if(++ictx->iwrite == sizeof(ictx->ivalid)){ + ictx->iwrite = 0; + } + } + return r; +} + +// 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. a control +// sequence will be chopped up and passed up (assuming it to be valid UTF-8). +static void +process_bulk(inputctx* ictx, unsigned char* buf, int* bufused){ + int offset = 0; + while(*bufused){ + int consumed = process_ncinput(ictx, buf + offset, *bufused); + if(consumed <= 0){ + break; + } + } + // 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; + while(*bufused){ + logdebug("input %d/%d [0x%02x]\n", offset, *bufused, buf[offset]); + int consumed = 0; + if(buf[offset] == '\x1b'){ + consumed = process_escape(ictx, buf + offset, *bufused); + }else{ + consumed = process_ncinput(ictx, buf + offset, *bufused); + } + if(consumed < 0){ + break; + } + *bufused -= consumed; + offset += consumed; } } // walk the matching automaton from wherever we were. static void process_ibuf(inputctx* ictx){ - // FIXME + 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); + } + 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); + } + } + } +} + +int ncinput_shovel(inputctx* ictx, const void* buf, int len){ + process_melange(ictx, buf, &len); + if(len){ + logwarn("dropping %d byte%s\n", len, len == 1 ? "" : "s"); + } + return 0; +} + +static int +block_on_input(inputctx* ictx){ + struct timespec* ts = NULL; // FIXME +#ifdef __MINGW64__ + int timeoutms = ts ? ts->tv_sec * 1000 + ts->tv_nsec / 1000000 : -1; + DWORD d = WaitForMultipleObjects(1, &ti->inhandle, FALSE, timeoutms); + if(d == WAIT_TIMEOUT){ + return 0; + }else if(d == WAIT_FAILED){ + return -1; + }else if(d - WAIT_OBJECT_0 == 0){ + return 1; + } + return -1; +#else + int inevents = POLLIN; +#ifdef POLLRDHUP + inevents |= POLLRDHUP; +#endif + struct pollfd pfds[2] = { + { + .fd = ictx->stdinfd, + .events = inevents, + .revents = 0, + } + }; + int pfdcount = 1; + if(ictx->termfd >= 0){ + pfds[pfdcount].fd = ictx->termfd; + pfds[pfdcount].events = inevents; + pfds[pfdcount].revents = 0; + ++pfdcount; + } + int events; +#if defined(__APPLE__) || defined(__MINGW64__) + int timeoutms = ts ? ts->tv_sec * 1000 + ts->tv_nsec / 1000000 : -1; + while((events = poll(pfds, pfdcount, timeoutms)) < 0){ // FIXME smask? +#else + sigset_t smask; + sigfillset(&smask); + sigdelset(&smask, SIGCONT); + sigdelset(&smask, SIGWINCH); + while((events = ppoll(pfds, pfdcount, ts, &smask)) < 0){ +#endif + if(errno != EINTR && errno != EAGAIN && errno != EBUSY && errno != EWOULDBLOCK){ + return -1; + } + if(resize_seen){ + return 1; + } + } + 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){ + block_on_input(ictx); // first we read from the terminal, if that's a distinct source. - read_input_nblock(ictx, ictx->termfd); + read_input_nblock(ictx->termfd, ictx->tbuf, sizeof(ictx->tbuf), + &ictx->tbufvalid); // now read bulk, possibly with term escapes intermingled within (if there // was not a distinct terminal source). - read_input_nblock(ictx, ictx->stdinfd); + read_input_nblock(ictx->stdinfd, ictx->ibuf, sizeof(ictx->ibuf), + &ictx->ibufvalid); } static void* @@ -206,8 +1125,20 @@ int inputready_fd(const inputctx* ictx){ return ictx->stdinfd; } -// infp has already been set non-blocking -uint32_t notcurses_get(notcurses* nc, const struct timespec* ts, ncinput* ni){ +static inline uint32_t +internal_get(inputctx* ictx, const struct timespec* ts, ncinput* ni, + int lmargin, int tmargin){ + pthread_mutex_lock(&ictx->ilock); + while(!ictx->ivalid){ + pthread_cond_wait(&ictx->icond, &ictx->ilock); + } + memcpy(ni, &ictx->inputs[ictx->iread], sizeof(*ni)); + if(++ictx->iread == ictx->isize){ + ictx->iread = 0; + } + pthread_mutex_unlock(&ictx->ilock); + // FIXME adjust mouse coordinates for margins + return ni->id; /* uint32_t r = ncinputlayer_prestamp(&nc->tcache, ts, ni, nc->margin_l, nc->margin_t); @@ -216,11 +1147,35 @@ uint32_t notcurses_get(notcurses* nc, const struct timespec* ts, ncinput* ni){ if(ni){ ni->seqnum = stamp; } - ++nc->stats.s.input_events; } return r; */ - return -1; +} + +struct initial_responses* inputlayer_get_responses(inputctx* ictx){ + struct initial_responses* iresp; + pthread_mutex_lock(&ictx->ilock); + while(!ictx->initdata_complete){ + pthread_cond_wait(&ictx->icond, &ictx->ilock); + } + iresp = ictx->initdata_complete; + ictx->initdata_complete = NULL; + pthread_mutex_unlock(&ictx->ilock); + return iresp; +} + +// infp has already been set non-blocking +uint32_t notcurses_get(notcurses* nc, const struct timespec* ts, ncinput* ni){ + uint32_t r = internal_get(nc->tcache.ictx, ts, ni, + nc->margin_l, nc->margin_t); + if(r != (uint32_t)-1){ + ++nc->stats.s.input_events; + } + return r; +} + +uint32_t ncdirect_get(ncdirect* n, const struct timespec* ts, ncinput* ni){ + return internal_get(n->tcache.ictx, ts, ni, 0, 0); } uint32_t notcurses_getc(notcurses* nc, const struct timespec* ts, @@ -229,23 +1184,88 @@ uint32_t notcurses_getc(notcurses* nc, const struct timespec* ts, return notcurses_get(nc, ts, ni); } -uint32_t ncdirect_get(struct ncdirect* n, const struct timespec* ts, ncinput* ni){ - /* - uint32_t r = ncinputlayer_prestamp(&n->tcache, ts, ni, 0, 0); - if(r != (uint32_t)-1){ - uint64_t stamp = n->tcache.input.input_events++; // need increment even if !ni - if(ni){ - ni->seqnum = stamp; - } - } - return r; - */ - return -1; -} - uint32_t ncdirect_getc(ncdirect* nc, const struct timespec *ts, const void* unused, ncinput* ni){ (void)unused; // FIXME remove for abi3 return ncdirect_get(nc, ts, ni); } +int get_cursor_location(struct inputctx* ictx, int* y, int* x){ + pthread_mutex_lock(&ictx->clock); + while(ictx->cvalid == 0){ + pthread_cond_wait(&ictx->ccond, &ictx->clock); + } + const cursorloc* cloc = &ictx->csrs[ictx->cread]; + if(++ictx->cread == ictx->csize){ + ictx->cread = 0; + } + --ictx->cvalid; + *y = cloc->y; + *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. +int notcurses_linesigs_disable(notcurses* n){ +#ifndef __MINGW64__ + if(n->tcache.ttyfd < 0){ + return 0; + } + struct termios tios; + if(tcgetattr(n->tcache.ttyfd, &tios)){ + logerror("Couldn't preserve terminal state for %d (%s)\n", n->tcache.ttyfd, strerror(errno)); + return -1; + } + tios.c_lflag &= ~ISIG; + if(tcsetattr(n->tcache.ttyfd, TCSANOW, &tios)){ + logerror("Error disabling signals on %d (%s)\n", n->tcache.ttyfd, strerror(errno)); + return -1; + } +#else + DWORD mode; + if(!GetConsoleMode(n->tcache.inhandle, &mode)){ + logerror("error acquiring input mode\n"); + return -1; + } + mode &= ~ENABLE_PROCESSED_INPUT; + if(!SetConsoleMode(n->tcache.inhandle, mode)){ + logerror("error setting input mode\n"); + return -1; + } +#endif + 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){ +#ifndef __MINGW64__ + if(n->tcache.ttyfd < 0){ + return 0; + } + struct termios tios; + if(tcgetattr(n->tcache.ttyfd, &tios)){ + logerror("Couldn't preserve terminal state for %d (%s)\n", n->tcache.ttyfd, strerror(errno)); + return -1; + } + tios.c_lflag |= ~ISIG; + if(tcsetattr(n->tcache.ttyfd, TCSANOW, &tios)){ + logerror("Error disabling signals on %d (%s)\n", n->tcache.ttyfd, strerror(errno)); + return -1; + } +#else + DWORD mode; + if(!GetConsoleMode(n->tcache.inhandle, &mode)){ + logerror("error acquiring input mode\n"); + return -1; + } + mode |= ENABLE_PROCESSED_INPUT; + if(!SetConsoleMode(n->tcache.inhandle, mode)){ + logerror("error setting input mode\n"); + return -1; + } +#endif + return 0; +} diff --git a/src/lib/in.h b/src/lib/in.h new file mode 100644 index 000000000..311061f69 --- /dev/null +++ b/src/lib/in.h @@ -0,0 +1,82 @@ +#ifndef NOTCURSES_IN +#define NOTCURSES_IN + +#ifdef __cplusplus +extern "C" { +#endif + +// internal header, not installed + +#include + +struct tinfo; +struct inputctx; + +int init_inputlayer(struct tinfo* ti, FILE* infp) + __attribute__ ((nonnull (1, 2))); + +int stop_inputlayer(struct tinfo* ti); + +int inputready_fd(const struct inputctx* ictx) + __attribute__ ((nonnull (1))); + +// allow another source provide raw input for distribution to client code. +// drops input if there is no room in appropriate output queue. +int ncinput_shovel(struct inputctx* ictx, const void* buf, int len) + __attribute__ ((nonnull (1, 2))); + +typedef enum { + TERMINAL_UNKNOWN, // no useful information from queries; use termname + // the very limited linux VGA/serial console, or possibly the (deprecated, + // pixel-drawable, RGBA8888) linux framebuffer console. *not* fbterm. + TERMINAL_LINUX, // ioctl()s + // the linux KMS/DRM console, *not* kmscon, but DRM direct dumb buffers + TERMINAL_LINUXDRM, // ioctl()s + TERMINAL_XTERM, // XTVERSION == 'XTerm(ver)' + TERMINAL_VTE, // TDA: "~VTE" + TERMINAL_KITTY, // XTGETTCAP['TN'] == 'xterm-kitty' + TERMINAL_FOOT, // TDA: "\EP!|464f4f54\E\\" + TERMINAL_MLTERM, // XTGETTCAP['TN'] == 'mlterm' + TERMINAL_TMUX, // XTVERSION == "tmux ver" + TERMINAL_WEZTERM, // XTVERSION == 'WezTerm *' + TERMINAL_ALACRITTY, // can't be detected; match TERM+DA2 + TERMINAL_CONTOUR, // XTVERSION == 'contour ver' + TERMINAL_ITERM, // XTVERSION == 'iTerm2 [ver]' + TERMINAL_TERMINOLOGY, // TDA: "~~TY" + TERMINAL_APPLE, // Terminal.App, determined by TERM_PROGRAM + macOS + TERMINAL_MSTERMINAL, // Microsoft Windows Terminal + TERMINAL_MINTTY, // XTVERSION == 'mintty ver' MinTTY (Cygwin, MSYS2) +} queried_terminals_e; + +// after spawning the input layer, send initial queries to the terminal. its +// responses will be built up herein. it's dangerous to go alone! take this! +struct initial_responses { + int cursory; // cursor location + int cursorx; // cursor location + unsigned appsync_supported; // is application-synchronized mode supported? + queried_terminals_e qterm; // determined terminal + unsigned kitty_graphics; // kitty graphics supported + uint32_t bg; // default background + int pixx; // screen geometry in pixels + int pixy; // screen geometry in pixels + int dimx; // screen geometry in cells + int dimy; // screen geometry in cells + int color_registers; // sixel color registers + int sixely; // maximum sixel height + int sixelx; // maximum sixel width + char* version; // version string, heap-allocated +}; + +// Blocking call. Waits until the input thread has processed all responses to +// our initial queries, and returns them. +struct initial_responses* inputlayer_get_responses(struct inputctx* ictx) + __attribute__ ((nonnull (1))); + +int get_cursor_location(struct inputctx* ictx, int* y, int* x) + __attribute__ ((nonnull (1, 2, 3))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/lib/input.h b/src/lib/input.h deleted file mode 100644 index 24fbbd5f0..000000000 --- a/src/lib/input.h +++ /dev/null @@ -1,69 +0,0 @@ -#ifndef NOTCURSES_INPUT -#define NOTCURSES_INPUT - -#ifdef __cplusplus -extern "C" { -#endif - -// internal header, not installed - -#include - -struct tinfo; -struct termios; -struct ncinputlayer; -struct ncsharedstats; - -typedef enum { - TERMINAL_UNKNOWN, // no useful information from queries; use termname - // the very limited linux VGA/serial console, or possibly the (deprecated, - // pixel-drawable, RGBA8888) linux framebuffer console. *not* fbterm. - TERMINAL_LINUX, // ioctl()s - // the linux KMS/DRM console, *not* kmscon, but DRM direct dumb buffers - TERMINAL_LINUXDRM, // ioctl()s - TERMINAL_XTERM, // XTVERSION == 'XTerm(ver)' - TERMINAL_VTE, // TDA: "~VTE" - TERMINAL_KITTY, // XTGETTCAP['TN'] == 'xterm-kitty' - TERMINAL_FOOT, // TDA: "\EP!|464f4f54\E\\" - TERMINAL_MLTERM, // XTGETTCAP['TN'] == 'mlterm' - TERMINAL_TMUX, // XTVERSION == "tmux ver" - TERMINAL_WEZTERM, // XTVERSION == 'WezTerm *' - TERMINAL_ALACRITTY, // can't be detected; match TERM+DA2 - TERMINAL_CONTOUR, // XTVERSION == 'contour ver' - TERMINAL_ITERM, // XTVERSION == 'iTerm2 [ver]' - TERMINAL_TERMINOLOGY, // TDA: "~~TY" - TERMINAL_APPLE, // Terminal.App, determined by TERM_PROGRAM + macOS - TERMINAL_MSTERMINAL, // Microsoft Windows Terminal - TERMINAL_MINTTY, // XTVERSION == 'mintty ver' MinTTY (Cygwin, MSYS2) -} queried_terminals_e; - -// sets up the input layer, building a trie of escape sequences and their -// nckey equivalents. if we are connected to a tty, this also completes the -// terminal detection sequence (we ought have already written our initial -// queries, ideally as early as possible). if we are able to determine the -// terminal conclusively, it will be written to |detected|. if the terminal -// advertised support for application-sychronized updates, |appsync| will be -// non-zero. -int ncinputlayer_init(struct tinfo* tcache, FILE* infp, - queried_terminals_e* detected, unsigned* appsync, - int* cursor_y, int* cursor_x, - struct ncsharedstats* stats, - unsigned* kittygraphs); - -void ncinputlayer_stop(struct ncinputlayer* nilayer); - -// FIXME absorb into ncinputlayer_init() -int cbreak_mode(struct tinfo* ti); - -// assuming the user context is not active, go through current data looking -// for a cursor location report. if we find none, block on input, and read if -// appropriate. we can be interrupted by a new user context. -void ncinput_extract_clrs(struct tinfo* ti); - -int ncinput_shovel(struct ncinputlayer* ni, const char* buf, size_t len); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/src/lib/internal.h b/src/lib/internal.h index e2d162c90..b02b8babc 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -9,6 +9,7 @@ extern "C" { #include "builddef.h" #include "compat/compat.h" #include "notcurses/notcurses.h" +#include "notcurses/direct.h" // KEY_EVENT is defined by both ncurses.h and wincon.h. since we don't use // either definition, kill it before inclusion of ncurses.h. diff --git a/src/lib/linux.c b/src/lib/linux.c index d93d13476..17fecf4e1 100644 --- a/src/lib/linux.c +++ b/src/lib/linux.c @@ -406,7 +406,7 @@ program_line_drawing_chars(int fd, struct unimapdesc* map){ return 0; } if(ioctl(fd, PIO_UNIMAP, map)){ - logwarn("Error setting kernel unicode map (%s)\n", strerror(errno)); + logwarn("error setting kernel unicode map (%s)\n", strerror(errno)); return -1; } loginfo("Successfully added %d kernel unicode mapping%s\n", @@ -533,12 +533,12 @@ program_block_drawing_chars(tinfo* ti, int fd, struct console_font_op* cfo, } } if(candidate == 0){ - logwarn("Ran out of replaceable glyphs for U+%04lx\n", (long)half[s].w); + logwarn("ran out of replaceable glyphs for U+%04lx\n", (long)half[s].w); // FIXME maybe don't want to error out here? return -1; } if(shim_quad_block(cfo, candidate, half[s].qbits)){ - logwarn("Error replacing glyph for U+%04lx at %u\n", (long)half[s].w, candidate); + logwarn("error replacing glyph for U+%04lx at %u\n", (long)half[s].w, candidate); return -1; } if(add_to_map(map, half[s].w, candidate)){ @@ -555,12 +555,12 @@ program_block_drawing_chars(tinfo* ti, int fd, struct console_font_op* cfo, } } if(candidate == 0){ - logwarn("Ran out of replaceable glyphs for U+%04lx\n", (long)quads[s].w); + logwarn("ran out of replaceable glyphs for U+%04lx\n", (long)quads[s].w); // FIXME maybe don't want to error out here? return -1; } if(shim_quad_block(cfo, candidate, quads[s].qbits)){ - logwarn("Error replacing glyph for U+%04lx at %u\n", (long)quads[s].w, candidate); + logwarn("error replacing glyph for U+%04lx at %u\n", (long)quads[s].w, candidate); return -1; } if(add_to_map(map, quads[s].w, candidate)){ @@ -577,11 +577,11 @@ program_block_drawing_chars(tinfo* ti, int fd, struct console_font_op* cfo, } } if(candidate == 0){ - logwarn("Ran out of replaceable glyphs for U+%04lx\n", (long)eighths[s].w); + logwarn("ran out of replaceable glyphs for U+%04lx\n", (long)eighths[s].w); return -1; } if(shim_lower_eighths(cfo, candidate, eighths[s].qbits)){ - logwarn("Error replacing glyph for U+%04lx at %u\n", (long)eighths[s].w, candidate); + logwarn("error replacing glyph for U+%04lx at %u\n", (long)eighths[s].w, candidate); return -1; } if(add_to_map(map, eighths[s].w, candidate)){ @@ -600,12 +600,12 @@ program_block_drawing_chars(tinfo* ti, int fd, struct console_font_op* cfo, } cfo->op = KD_FONT_OP_SET; if(ioctl(fd, KDFONTOP, cfo)){ - logwarn("Error programming kernel font (%s)\n", strerror(errno)); + logwarn("error programming kernel font (%s)\n", strerror(errno)); kill_fbcopy(&fbdup); return -1; } if(ioctl(fd, PIO_UNIMAP, map)){ - logwarn("Error setting kernel unicode map (%s)\n", strerror(errno)); + logwarn("error setting kernel unicode map (%s)\n", strerror(errno)); kill_fbcopy(&fbdup); return -1; } @@ -636,7 +636,7 @@ reprogram_linux_font(tinfo* ti, int fd, struct console_font_op* cfo, struct unimapdesc* map, unsigned no_font_changes, bool* halfblocks, bool* quadrants){ if(ioctl(fd, KDFONTOP, cfo)){ - logwarn("Error reading Linux kernelfont (%s)\n", strerror(errno)); + logwarn("error reading Linux kernelfont (%s)\n", strerror(errno)); return -1; } loginfo("Kernel font size (glyphcount): %hu\n", cfo->charcount); @@ -646,7 +646,7 @@ reprogram_linux_font(tinfo* ti, int fd, struct console_font_op* cfo, return -1; } if(ioctl(fd, GIO_UNIMAP, map)){ - logwarn("Error reading Linux unimap (%s)\n", strerror(errno)); + logwarn("error reading Linux unimap (%s)\n", strerror(errno)); return -1; } loginfo("Kernel Unimap size: %hu/%hu\n", map->entry_ct, USHRT_MAX); @@ -676,7 +676,7 @@ int reprogram_console_font(tinfo* ti, unsigned no_font_changes, size_t totsize = 128 * cfo.charcount; // FIXME enough? cfo.data = malloc(totsize); if(cfo.data == NULL){ - logwarn("Error acquiring %zub for font descriptors (%s)\n", totsize, strerror(errno)); + logwarn("error acquiring %zub for font descriptors (%s)\n", totsize, strerror(errno)); return -1; } struct unimapdesc map = {}; @@ -684,7 +684,7 @@ int reprogram_console_font(tinfo* ti, unsigned no_font_changes, totsize = map.entry_ct * sizeof(struct unipair); map.entries = malloc(totsize); if(map.entries == NULL){ - logwarn("Error acquiring %zub for Unicode font map (%s)\n", totsize, strerror(errno)); + logwarn("error acquiring %zub for Unicode font map (%s)\n", totsize, strerror(errno)); free(cfo.data); return -1; } @@ -704,10 +704,10 @@ bool is_linux_console(int fd){ } int mode; if(ioctl(fd, KDGETMODE, &mode)){ - logdebug("Not a Linux console, KDGETMODE failed\n"); + logdebug("not a Linux console, KDGETMODE failed\n"); return false; } - loginfo("Verified Linux console, mode %d\n", mode); + loginfo("verified Linux console, mode %d\n", mode); return true; } @@ -721,7 +721,7 @@ int get_linux_fb_pixelgeom(tinfo* ti, unsigned* ypix, unsigned *xpix){ } struct fb_var_screeninfo fbi = {}; if(ioctl(ti->linux_fb_fd, FBIOGET_VSCREENINFO, &fbi)){ - logwarn("No framebuffer info from %s (%s?)\n", ti->linux_fb_dev, strerror(errno)); + logwarn("no framebuffer info from %s (%s?)\n", ti->linux_fb_dev, strerror(errno)); return -1; } loginfo("Linux %s geometry: %dx%d\n", ti->linux_fb_dev, fbi.yres, fbi.xres); diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c index 964d8e1dc..0ccf2f957 100644 --- a/src/lib/notcurses.c +++ b/src/lib/notcurses.c @@ -1,4 +1,3 @@ -#include "input.h" #include "linux.h" #include "version.h" #include "egcpool.h" @@ -107,10 +106,8 @@ notcurses_stop_minimal(void* vnc){ if(nc->tcache.tpreserved){ ret |= tcsetattr(nc->tcache.ttyfd, TCSAFLUSH, nc->tcache.tpreserved); } - if(nc->tcache.kittykbd){ - if(tty_emit("\x1b[tcache.ttyfd)){ - ret = -1; - } + if(tty_emit("\x1b[tcache.ttyfd)){ + ret = -1; } if((esc = get_escape(&nc->tcache, ESCAPE_RMCUP))){ if(sprite_clear_all(&nc->tcache, f)){ // send this to f @@ -1001,7 +998,7 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){ fprintf(stderr, "Provided an illegal negative margin, refusing to start\n"); return NULL; } - if(opts->flags >= (NCOPTION_NO_FONT_CHANGES << 1u)){ + if(opts->flags >= (NCOPTION_DRAIN_INPUT << 1u)){ fprintf(stderr, "Warning: unknown Notcurses options %016" PRIu64 "\n", opts->flags); } notcurses* ret = malloc(sizeof(*ret)); diff --git a/src/lib/termdesc.c b/src/lib/termdesc.c index aa0034de0..aed57e99d 100644 --- a/src/lib/termdesc.c +++ b/src/lib/termdesc.c @@ -5,7 +5,6 @@ #endif #include "internal.h" #include "windows.h" -#include "input.h" #include "linux.h" // there does not exist any true standard terminal size. with that said, we @@ -780,6 +779,9 @@ int interrogate_terminfo(tinfo* ti, const char* termtype, FILE* out, unsigned ut goto err; } } + if(init_inputlayer(ti, stdin)){ + goto err; + } #ifndef __MINGW64__ // windows doesn't really have a concept of terminfo. you might ssh into other // machines, but they'll use the terminfo installed thereon (putty, etc.). @@ -916,19 +918,51 @@ int interrogate_terminfo(tinfo* ti, const char* termtype, FILE* out, unsigned ut goto err; } } - unsigned appsync_advertised = 0; - unsigned kittygraphs = 0; - if(init_inputlayer(ti, stdin)){ - goto err; + unsigned kitty_graphics = 0; + if(ti->ttyfd >= 0){ + struct initial_responses* iresp; + if((iresp = inputlayer_get_responses(ti->ictx)) == NULL){ + goto err; + } + if(iresp->appsync_supported){ + if(add_appsync_escapes_sm(ti, &tablelen, &tableused)){ + free(iresp->version); + free(iresp); + goto err; + } + } + if(iresp->qterm != TERMINAL_UNKNOWN){ + ti->qterm = iresp->qterm; + } + *cursor_y = iresp->cursory; + *cursor_x = iresp->cursorx; + ti->termversion = iresp->version; + if(iresp->dimy && iresp->dimx){ + // FIXME probably oughtn't be setting the defaults, as this is just some + // random transient measurement? + ti->default_rows = iresp->dimy; + ti->default_cols = iresp->dimx; + } + if(iresp->pixy && iresp->pixx){ + ti->pixy = iresp->pixy; + ti->pixx = iresp->pixx; + } + if(ti->default_rows && ti->default_cols){ + ti->cellpixy = ti->pixy / ti->default_rows; + ti->cellpixx = ti->pixx / ti->default_cols; + } + ti->bg_collides_default = iresp->bg; + // kitty trumps sixel, when both are available + if((kitty_graphics = iresp->kitty_graphics) == 0){ + ti->color_registers = iresp->color_registers; + ti->sixel_maxy = iresp->sixely; + ti->sixel_maxx = iresp->sixelx; + } + free(iresp); } - /* - if(ncinputlayer_init(ti, stdin, &ti->qterm, &appsync_advertised, - cursor_y, cursor_x, stats, &kittygraphs)){ - goto err; - } - */ if(nocbreak){ if(ti->ttyfd >= 0){ + // FIXME do this in input later, upon signaling completion? if(tcsetattr(ti->ttyfd, TCSANOW, ti->tpreserved)){ goto err; } @@ -939,11 +973,6 @@ int interrogate_terminfo(tinfo* ti, const char* termtype, FILE* out, unsigned ut goto err; } } - if(appsync_advertised){ - if(add_appsync_escapes_sm(ti, &tablelen, &tableused)){ - goto err; - } - } bool invertsixel = false; if(apply_term_heuristics(ti, tname, ti->qterm, &tablelen, &tableused, &invertsixel, nonewfonts)){ @@ -951,8 +980,8 @@ int interrogate_terminfo(tinfo* ti, const char* termtype, FILE* out, unsigned ut } build_supported_styles(ti); if(ti->pixel_draw == NULL && ti->pixel_draw_late == NULL){ - if(kittygraphs){ - setup_kitty_bitmaps(ti, ti->ttyfd, KITTY_SELFREF); + if(kitty_graphics){ + setup_kitty_bitmaps(ti, ti->ttyfd, KITTY_ANIMATION); } // our current sixel quantization algorithm requires at least 64 color // registers. we make use of no more than 256. this needs to happen @@ -1028,18 +1057,13 @@ int locate_cursor(tinfo* ti, int* cursor_y, int* cursor_x){ if(tty_emit(u7, fd)){ return -1; } - // FIXME get that report - /* - loginfo("Got a report from %d %d/%d\n", fd, clr->y, clr->x); - *cursor_y = clr->y; - *cursor_x = clr->x; + get_cursor_location(ti->ictx, cursor_y, cursor_x); + loginfo("got a report from %d %d/%d\n", fd, *cursor_y, *cursor_x); if(ti->inverted_cursor){ int tmp = *cursor_y; *cursor_y = *cursor_x; *cursor_x = tmp; } - free(clr); - */ return 0; } diff --git a/src/lib/termdesc.h b/src/lib/termdesc.h index 144559b71..3c6b99cf6 100644 --- a/src/lib/termdesc.h +++ b/src/lib/termdesc.h @@ -13,7 +13,6 @@ extern "C" { #include #include #include -#include "input.h" #include "fbuf.h" #include "in.h" @@ -89,43 +88,6 @@ typedef struct cursorreport { struct cursorreport* next; } cursorreport; -// we read input from one or two places. if stdin is connected to our -// controlling tty, we read only from that file descriptor. if it is -// connected to something else, and we have a controlling tty, we will -// read data only from stdin and control only from the tty. if we have -// no connected tty, only data is available. -typedef struct ncinputlayer { - // only allow one reader at a time, whether it's the user trying to do so, - // or our desire for a cursor report competing with the user. - pthread_mutex_t lock; - // must be held to operate on the cursor report queue shared between pure - // input and the control layer. - pthread_cond_t creport_cond; - // ttyfd is only valid if we are connected to a tty, *and* stdin is not - // connected to that tty (this usually means stdin was redirected). in that - // case, we read control sequences only from ttyfd. - int ttyfd; // file descriptor for connected tty - int infd; // file descriptor for processing input, from stdin - unsigned char inputbuf[BUFSIZ]; - unsigned char csibuf[BUFSIZ]; // running buffer while parsing CSIs - // we keep a wee ringbuffer of input queued up for delivery. if - // inputbuf_occupied == sizeof(inputbuf), there is no room. otherwise, data - // can be read to inputbuf_write_at until we fill up. the first datum - // available for the app is at inputbuf_valid_starts iff inputbuf_occupied is - // not 0. the main purpose is working around bad predictions of escapes. - unsigned inputbuf_occupied; - unsigned inputbuf_valid_starts; - unsigned inputbuf_write_at; - // number of input events seen. does not belong in ncstats, since it must not - // be reset (semantics are relied upon by widgets for mouse click detection). - uint64_t input_events; - struct esctrie* inputescapes; // trie of input escapes -> ncspecial_keys - cursorreport* creport_queue; // queue of cursor reports - bool user_wants_data; // a user context is active - bool inner_wants_data; // if we're blocking on input - struct ncsharedstats* stats; // notcurses sharedstats object -} ncinputlayer; - // terminal interface description. most of these are acquired from terminfo(5) // (using a database entry specified by TERM). some are determined via // heuristics based off terminal interrogation or the TERM environment @@ -208,8 +170,6 @@ typedef struct tinfo { int default_rows; // LINES environment var / lines terminfo / 24 int default_cols; // COLUMNS environment var / cols terminfo / 80 - unsigned kittykbd; // kitty keyboard support level - int gpmfd; // connection to GPM daemon pthread_t gpmthread; // thread handle for GPM watcher #ifdef __linux__ @@ -384,6 +344,8 @@ leave_alternate_screen(FILE* fp, tinfo* ti){ return 0; } +int cbreak_mode(tinfo* ti); + #ifdef __cplusplus } #endif