You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1616 lines
50 KiB
C
1616 lines
50 KiB
C
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#include <curses.h>
|
|
#ifdef __linux__
|
|
#include <sys/utsname.h>
|
|
#endif
|
|
#include "internal.h"
|
|
#include "windows.h"
|
|
#include "linux.h"
|
|
|
|
// there does not exist any true standard terminal size. with that said, we
|
|
// need assume *something* for the case where we're not actually attached to
|
|
// a terminal (mainly unit tests, but also daemon environments). in preference
|
|
// to this, we use the geometries defined by (in order of precedence):
|
|
//
|
|
// * TIOGWINSZ ioctl(2)
|
|
// * LINES/COLUMNS environment variables
|
|
// * lines/cols terminfo variables
|
|
//
|
|
// this function sets up ti->default_rows and ti->default_cols
|
|
static int
|
|
get_default_dimension(const char* envvar, const char* tinfovar, int def){
|
|
const char* env = getenv(envvar);
|
|
int num;
|
|
if(env){
|
|
num = atoi(env);
|
|
if(num > 0){
|
|
return num;
|
|
}
|
|
}
|
|
num = tigetnum(tinfovar);
|
|
if(num > 0){
|
|
return num;
|
|
}
|
|
return def;
|
|
}
|
|
|
|
static void
|
|
get_default_geometry(tinfo* ti){
|
|
ti->default_rows = get_default_dimension("LINES", "lines", 24);
|
|
ti->default_cols = get_default_dimension("COLUMNS", "cols", 80);
|
|
loginfo("default geometry: %d row%s, %d column%s",
|
|
ti->default_rows, ti->default_rows != 1 ? "s" : "",
|
|
ti->default_cols, ti->default_cols != 1 ? "s" : "");
|
|
ti->dimy = ti->default_rows;
|
|
ti->dimx = ti->default_cols;
|
|
}
|
|
|
|
// we found Sixel support -- set up its API. invert80 refers to whether the
|
|
// terminal implements DECSDM correctly (enabling it with \e[?80h), or inverts
|
|
// the meaning (*disabling* it with \e[?80h) (we always want it disabled).
|
|
static inline void
|
|
setup_sixel_bitmaps(tinfo* ti, int fd, unsigned forcesdm, unsigned invert80){
|
|
if(forcesdm){
|
|
if(invert80){
|
|
ti->pixel_init = sixel_init_inverted;
|
|
}else{
|
|
ti->pixel_init = sixel_init_forcesdm;
|
|
}
|
|
}else{
|
|
ti->pixel_init = sixel_init;
|
|
}
|
|
ti->pixel_scrub = sixel_scrub;
|
|
ti->pixel_remove = NULL;
|
|
ti->pixel_draw = sixel_draw;
|
|
ti->pixel_refresh = sixel_refresh;
|
|
ti->pixel_draw_late = NULL;
|
|
ti->pixel_commit = NULL;
|
|
ti->pixel_move = NULL;
|
|
ti->pixel_scroll = NULL;
|
|
ti->pixel_wipe = sixel_wipe;
|
|
ti->pixel_clear_all = NULL;
|
|
ti->pixel_rebuild = sixel_rebuild;
|
|
ti->pixel_trans_auxvec = sixel_trans_auxvec;
|
|
ti->sprixel_scale_height = 6;
|
|
set_pixel_blitter(sixel_blit);
|
|
ti->pixel_implementation = NCPIXEL_SIXEL;
|
|
ti->pixel_cleanup = sixel_cleanup;
|
|
sprite_init(ti, fd);
|
|
}
|
|
|
|
// kitty 0.19.3 didn't have C=1, and thus needs sixel_maxy_pristine. it also
|
|
// lacked animation, and must thus redraw the complete image every time it
|
|
// changes. requires the older interface.
|
|
static inline void
|
|
setup_kitty_bitmaps(tinfo* ti, int fd, ncpixelimpl_e level){
|
|
ti->pixel_scrub = kitty_scrub;
|
|
ti->pixel_remove = kitty_remove;
|
|
ti->pixel_draw = kitty_draw;
|
|
ti->pixel_draw_late = NULL;
|
|
ti->pixel_refresh = NULL;
|
|
ti->pixel_commit = kitty_commit;
|
|
ti->pixel_move = kitty_move;
|
|
ti->pixel_scroll = NULL;
|
|
ti->pixel_clear_all = kitty_clear_all;
|
|
if(level == NCPIXEL_KITTY_STATIC){
|
|
ti->pixel_wipe = kitty_wipe;
|
|
ti->pixel_trans_auxvec = kitty_trans_auxvec;
|
|
ti->pixel_rebuild = kitty_rebuild;
|
|
ti->sixel_maxy_pristine = INT_MAX;
|
|
set_pixel_blitter(kitty_blit);
|
|
ti->pixel_implementation = NCPIXEL_KITTY_STATIC;
|
|
}else{
|
|
if(level == NCPIXEL_KITTY_ANIMATED){
|
|
ti->pixel_wipe = kitty_wipe_animation;
|
|
ti->pixel_rebuild = kitty_rebuild_animation;
|
|
ti->sixel_maxy_pristine = 0;
|
|
set_pixel_blitter(kitty_blit_animated);
|
|
ti->pixel_implementation = NCPIXEL_KITTY_ANIMATED;
|
|
}else{
|
|
ti->pixel_wipe = kitty_wipe_selfref;
|
|
ti->pixel_rebuild = kitty_rebuild_selfref;
|
|
ti->sixel_maxy_pristine = 0;
|
|
set_pixel_blitter(kitty_blit_selfref);
|
|
ti->pixel_implementation = NCPIXEL_KITTY_SELFREF;
|
|
}
|
|
}
|
|
sprite_init(ti, fd);
|
|
}
|
|
|
|
#ifdef __linux__
|
|
static inline void
|
|
setup_fbcon_bitmaps(tinfo* ti, int fd){
|
|
ti->pixel_scrub = fbcon_scrub;
|
|
ti->pixel_remove = NULL;
|
|
ti->pixel_draw = NULL;
|
|
ti->pixel_draw_late = fbcon_draw;
|
|
ti->pixel_commit = NULL;
|
|
ti->pixel_refresh = NULL;
|
|
ti->pixel_move = NULL;
|
|
ti->pixel_scroll = fbcon_scroll;
|
|
ti->pixel_clear_all = NULL;
|
|
ti->pixel_rebuild = fbcon_rebuild;
|
|
ti->pixel_wipe = fbcon_wipe;
|
|
ti->pixel_trans_auxvec = kitty_trans_auxvec;
|
|
set_pixel_blitter(fbcon_blit);
|
|
ti->pixel_implementation = NCPIXEL_LINUXFB;
|
|
sprite_init(ti, fd);
|
|
}
|
|
#endif
|
|
|
|
static bool
|
|
query_rgb(void){
|
|
bool rgb = (tigetflag("RGB") > 0 || tigetflag("Tc") > 0);
|
|
if(!rgb){
|
|
// RGB terminfo capability being a new thing (as of ncurses 6.1), it's not
|
|
// commonly found in terminal entries today. COLORTERM, however, is a
|
|
// de-facto (if imperfect/kludgy) standard way of indicating TrueColor
|
|
// support for a terminal. The variable takes one of two case-sensitive
|
|
// values:
|
|
//
|
|
// truecolor
|
|
// 24bit
|
|
//
|
|
// https://gist.github.com/XVilka/8346728#true-color-detection gives some
|
|
// more information about the topic.
|
|
const char* cterm = getenv("COLORTERM");
|
|
rgb = cterm && (strcmp(cterm, "truecolor") == 0 || strcmp(cterm, "24bit") == 0);
|
|
}
|
|
return rgb;
|
|
}
|
|
|
|
void free_terminfo_cache(tinfo* ti){
|
|
stop_inputlayer(ti);
|
|
loginfo("brought down input layer");
|
|
if(ti->pixel_cleanup){
|
|
ti->pixel_cleanup(ti);
|
|
}
|
|
free(ti->termversion);
|
|
free(ti->esctable);
|
|
#ifdef __linux__
|
|
if(ti->linux_fb_fd >= 0){
|
|
close(ti->linux_fb_fd);
|
|
}
|
|
free(ti->linux_fb_dev);
|
|
if(ti->linux_fbuffer != MAP_FAILED){
|
|
munmap(ti->linux_fbuffer, ti->linux_fb_len);
|
|
}
|
|
#endif
|
|
free(ti->tpreserved);
|
|
loginfo("destroyed terminfo cache");
|
|
}
|
|
|
|
// compare one terminal version against another. numerics, separated by
|
|
// periods, and comparison ends otherwise (so "20.0 alpha" doesn't compare
|
|
// as greater than "20.0", mainly). returns -1 if v1 < v2 (or v1 is NULL),
|
|
// 0 if v1 == v2, or 1 if v1 > v2.
|
|
static int
|
|
compare_versions(const char* restrict v1, const char* restrict v2){
|
|
if(v1 == NULL){
|
|
return -1;
|
|
}
|
|
const char* v1e = v1;
|
|
const char* v2e = v2;
|
|
while(*v1 && *v2){
|
|
long v1v = strtol(v1, (char **)&v1e, 10);
|
|
long v2v = strtol(v2, (char **)&v2e, 10);
|
|
if(v1e == v1 && v2e == v2){ // both are done
|
|
return 0;
|
|
}else if(v1e == v1){ // first is done
|
|
return -1;
|
|
}else if(v2e == v2){ // second is done
|
|
return 1;
|
|
}
|
|
if(v1v > v2v){
|
|
return 1;
|
|
}else if(v2v > v1v){
|
|
return -1;
|
|
}
|
|
if(*v1e != '.' && *v2e != '.'){
|
|
break;
|
|
}else if(*v1e != '.' || *v2e != '.'){
|
|
if(*v1e == '.'){
|
|
return 1;
|
|
}else{
|
|
return -1;
|
|
}
|
|
}
|
|
v1 = v1e + 1;
|
|
v2 = v2e + 1;
|
|
}
|
|
if(*v1e == *v2e){
|
|
return 0;
|
|
}
|
|
// can only get out here if at least one was not a period
|
|
if(*v1e == '.'){
|
|
return 1;
|
|
}
|
|
if(*v2e == '.'){
|
|
return -1;
|
|
}
|
|
if(!*v1e){
|
|
return -1;
|
|
}
|
|
if(!*v2e){
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static inline int
|
|
terminfostr(char** gseq, const char* name){
|
|
*gseq = tigetstr(name);
|
|
if(*gseq == NULL || *gseq == (char*)-1){
|
|
*gseq = NULL;
|
|
return -1;
|
|
}
|
|
// terminfo syntax allows a number N of milliseconds worth of pause to be
|
|
// specified using $<N> syntax. this is then honored by tputs(). but we don't
|
|
// use tputs(), instead preferring the much faster stdio+tiparm() (at the
|
|
// expense of terminals which do require these delays). to avoid dumping
|
|
// "$<N>" sequences all over stdio, we chop them out. real text can follow
|
|
// them, so we continue on, copying back once out of the delay.
|
|
char* wnext = NULL; // NULL until we hit a delay, then place to write
|
|
bool indelay = false; // true iff we're in a delay section
|
|
// we consider it a delay as soon as we see '$', and the delay ends at '>'
|
|
for(char* cur = *gseq ; *cur ; ++cur){
|
|
if(!indelay){
|
|
// if we're not in a delay section, make sure we're not starting one,
|
|
// and otherwise copy the current character back (if necessary).
|
|
if(*cur == '$'){
|
|
wnext = cur;
|
|
indelay = true;
|
|
}else{
|
|
if(wnext){
|
|
*wnext++ = *cur;
|
|
}
|
|
}
|
|
}else{
|
|
// we are in a delay section. make sure we're not ending one.
|
|
if(*cur == '>'){
|
|
indelay = false;
|
|
}
|
|
}
|
|
}
|
|
if(wnext){
|
|
*wnext = '\0';
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static inline int
|
|
init_terminfo_esc(tinfo* ti, const char* name, escape_e idx,
|
|
size_t* tablelen, size_t* tableused){
|
|
char* tstr;
|
|
if(ti->escindices[idx]){
|
|
return 0;
|
|
}
|
|
if(terminfostr(&tstr, name) == 0){
|
|
if(grow_esc_table(ti, tstr, idx, tablelen, tableused)){
|
|
return -1;
|
|
}
|
|
}else{
|
|
ti->escindices[idx] = 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Tertiary Device Attributes, necessary to identify VTE.
|
|
// https://vt100.net/docs/vt510-rm/DA3.html
|
|
// Replies with DCS ! | ... ST
|
|
#define TRIDEVATTR "\x1b[=c"
|
|
|
|
// Primary Device Attributes, necessary to elicit a response from terminals
|
|
// which don't respond to other queries. All known terminals respond to DA1.
|
|
// https://vt100.net/docs/vt510-rm/DA1.html
|
|
// Device Attributes; replies with (depending on decTerminalID resource):
|
|
// ⇒ 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")
|
|
#define PRIDEVATTR "\x1b[c"
|
|
|
|
// XTVERSION. Replies with DCS > | ... ST
|
|
#define XTVERSION "\x1b[>0q"
|
|
|
|
// ideally we'd abandon terminfo entirely (terminfo is great; TERM sucks), and
|
|
// get all properties through terminal queries. we don't yet, but grab a few
|
|
// of importance that we know to oftentimes be incorrect:
|
|
// * TN (544e): terminal name; a poor man's XTVERSION
|
|
// * RGB (524742): 24-bit color is supported via setaf/setab
|
|
// * hpa (687061): broken in Kitty FreeBSD terminfo (#2541)
|
|
// XTGETTCAP['TN', 'RGB', 'hpa']
|
|
// (Terminal Name, RGB, Horizontal Position Absolute)
|
|
#define XTGETTCAP "\x1bP+q544e;524742;687061\x1b\\"
|
|
|
|
// Secondary Device Attributes, necessary to get Alacritty's version. Since
|
|
// this doesn't uniquely identify a terminal, we ask it last, so that if any
|
|
// queries which *do* unambiguously identify a terminal have succeeded, this
|
|
// needn't be paid attention to.
|
|
// https://vt100.net/docs/vtk510-rm/DA2.html
|
|
// (note that tmux uses 84 rather than common 60/61)
|
|
// Replies with CSI > \d \d ; Pv ; [01] c
|
|
#define SECDEVATTR "\x1b[>c"
|
|
|
|
// query for kitty graphics. if they are supported, we'll get a response to
|
|
// this using the kitty response syntax. otherwise, we'll get nothing. we
|
|
// send this with the other identification queries, since APCs tend not to
|
|
// be consumed by certain terminal emulators (looking at you, Linux console)
|
|
// which can be identified directly, sans queries.
|
|
// we do not send this query on Windows because it is bled through ConHost,
|
|
// and echoed onto the standard output.
|
|
#ifndef __MINGW32__
|
|
#define KITTYQUERY "\x1b_Gi=1,a=q;\x1b\\"
|
|
#else
|
|
#define KITTYQUERY
|
|
#endif
|
|
|
|
// request kitty keyboard protocol features 1, 2, 8 and 16, first pushing current.
|
|
// see https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
|
|
#define KKBDSUPPORT "\x1b[=27u"
|
|
|
|
// the kitty keyboard protocol allows unambiguous, complete identification of
|
|
// input events. this queries for the level of support. we want to do this
|
|
// because the "keyboard pop" control code is mishandled by kitty < 0.20.0.
|
|
#define KKBDQUERY "\x1b[?u"
|
|
|
|
// set modifyFunctionKeys (2) if supported, allowing us to disambiguate
|
|
// function keys when used with modifiers. set modifyOtherKeys (4) if
|
|
// supported. these ought follow keyboard push and precede kitty keyboard.
|
|
#define XTMODKEYS "\x1b[>2;1m\x1b[>4;2m"
|
|
|
|
// these queries can hopefully uniquely and unquestionably identify the
|
|
// terminal to which we are talking. if we already know what we're talking
|
|
// to, there's no point in sending them.
|
|
#define IDQUERIES TRIDEVATTR \
|
|
XTVERSION \
|
|
XTGETTCAP \
|
|
SECDEVATTR
|
|
|
|
// query background, replies in X color https://www.x.org/releases/X11R7.7/doc/man/man7/X.7.xhtml#heading11
|
|
// GNU screen passes this on to the underlying terminal rather than answering itself,
|
|
// unlike most other queries, so send this first since it will take longer to be
|
|
// answered. note the "\x1b]"; this is an Operating System Command, not CSI.
|
|
#define DEFBGQ "\x1b]11;?\e\\"
|
|
#define DEFFGQ "\x1b]10;?\e\\"
|
|
|
|
// FIXME ought be using the u7 terminfo string here, if it exists. the great
|
|
// thing is, if we get a response to this, we know we can use it for u7!
|
|
// we send this first because terminals which don't consume the entire escape
|
|
// sequences following will bleed the excess into the terminal, and we want
|
|
// to blow any such output away (or at least return to the cell where such
|
|
// output started).
|
|
#define DSRCPR "\x1b[6n"
|
|
|
|
// check for Synchronized Update Mode support. the p is necessary, but at
|
|
// least Konsole and Terminal.app fail to consume it =[.
|
|
#define SUMQUERY "\x1b[?2026$p"
|
|
|
|
// check for mouse mode 1016, pixel-based reports
|
|
#define PIXELMOUSEQUERY "\x1b[?1016$p"
|
|
|
|
// XTSMGRAPHICS query for the number of color registers.
|
|
#define CREGSXTSM "\x1b[?2;1;0S"
|
|
|
|
// XTSMGRAPHICS query for the maximum supported geometry.
|
|
#define GEOMXTSM "\x1b[?1;1;0S"
|
|
|
|
// non-standard CSI for total pixel geometry
|
|
#define GEOMPIXEL "\x1b[14t"
|
|
|
|
// request the cell geometry of the textual area
|
|
#define GEOMCELL "\x1b[18t"
|
|
|
|
// palette queries are logically part of DIRECTIVES, but we generate
|
|
// those on the fly (they would otherwise be quite a lot of rodata).
|
|
#define DIRECTIVES DEFFGQ \
|
|
DEFBGQ \
|
|
KKBDQUERY \
|
|
SUMQUERY \
|
|
PIXELMOUSEQUERY \
|
|
"\x1b[?1;3;256S" /* try to set 256 cregs */ \
|
|
"\x1b[?1;3;1024S" /* try to set 1024 cregs */ \
|
|
KITTYQUERY \
|
|
CREGSXTSM \
|
|
GEOMXTSM \
|
|
GEOMPIXEL \
|
|
GEOMCELL \
|
|
PRIDEVATTR
|
|
|
|
// kitty keyboard push, used at start
|
|
#define KKEYBOARD_PUSH "\x1b[>u"
|
|
|
|
// written whenever we switch between standard and alternate screen, or upon
|
|
// startup (that's an entry into a screen! presumably the standard one).
|
|
#define KKBDENTER KKEYBOARD_PUSH KKBDSUPPORT
|
|
|
|
// enter the alternate screen (smcup). we could technically get this from
|
|
// terminfo, but everyone who supports it supports it the same way, and we
|
|
// need to send it before our other directives if we're going to use it.
|
|
// we warn later in setup if what we get from terminfo doesn't match what
|
|
// we sent here.
|
|
#define SMCUP DECSET(SET_SMCUP)
|
|
#define RMCUP DECRST(SET_SMCUP)
|
|
|
|
static ssize_t
|
|
send_initial_directives(queried_terminals_e qterm, int fd){
|
|
int total = 0;
|
|
// 4096 is more than sufficient for up through 256 OSC queries
|
|
#define PQUERYBUFLEN 4096
|
|
if(qterm != TERMINAL_LINUX){
|
|
// FIXME linux kernel does not yet support OSC4, and bleeds it. don't send
|
|
// palette queries on linux VT.
|
|
char* pqueries = malloc(PQUERYBUFLEN);
|
|
if(pqueries == NULL){
|
|
return -1;
|
|
}
|
|
// bunch the queries up according to known palette sizes, so that we don't
|
|
// knock out batched OSCs with error responses.
|
|
const int qsets[] = { 0, 8, 16, 88, 256 };
|
|
for(size_t q = 1 ; q < sizeof(qsets) / sizeof(*qsets) ; ++q){
|
|
int len = 0;
|
|
for(int i = qsets[q - 1] ; i < qsets[q] ; ++i){
|
|
len += sprintf(pqueries + len, "\x1b]4;%d;?\e\\", i);
|
|
assert(len < PQUERYBUFLEN);
|
|
}
|
|
if(blocking_write(fd, pqueries, len)){
|
|
return -1;
|
|
}
|
|
total += len;
|
|
}
|
|
free(pqueries);
|
|
}
|
|
#undef PQUERYBUFLEN
|
|
if(blocking_write(fd, DIRECTIVES, strlen(DIRECTIVES))){
|
|
return -1;
|
|
}
|
|
total += strlen(DIRECTIVES);
|
|
return total;
|
|
}
|
|
|
|
// we send an XTSMGRAPHICS to set up 256 (or ideally 1024) color registers.
|
|
// maybe that works, maybe it doesn't. then query both color registers and
|
|
// geometry. send XTGETTCAP for terminal name. if 'minimal' is set, don't send
|
|
// any identification queries (we've already identified the terminal). write
|
|
// DSRCPR as early as possible, so that it precedes any query material that's
|
|
// bled onto stdin and echoed. if 'noaltscreen' is set, do not send an smcup.
|
|
// if 'draininput' is set, do not send any keyboard modifiers.
|
|
// precondition: ti->ttyfd is a valid fd (we're connected to a terminal)
|
|
static int
|
|
send_initial_queries(tinfo* ti, unsigned minimal, unsigned noaltscreen,
|
|
unsigned draininput){
|
|
int fd = ti->ttyfd;
|
|
size_t total = 0;
|
|
// everything sends DSRCPR, and everything sends DIRECTIVES afterwards.
|
|
// we send KKBDENTER immediately before DIRECTIVES unless input is being
|
|
// drained. we send IDQUERIES unless minimal is set. we send SMCUP (as
|
|
// the first thing) unless noaltscreen is set.
|
|
if(!noaltscreen){
|
|
if(blocking_write(fd, SMCUP, strlen(SMCUP))){
|
|
return -1;
|
|
}
|
|
total += strlen(SMCUP);
|
|
}
|
|
if(blocking_write(fd, DSRCPR, strlen(DSRCPR))){
|
|
return -1;
|
|
}
|
|
total += strlen(DSRCPR);
|
|
if(!draininput){
|
|
if(blocking_write(fd, KKBDENTER, strlen(KKBDENTER))){
|
|
return -1;
|
|
}
|
|
total += strlen(KKBDENTER);
|
|
}
|
|
if(!minimal){
|
|
if(blocking_write(fd, IDQUERIES, strlen(IDQUERIES))){
|
|
return -1;
|
|
}
|
|
total += strlen(IDQUERIES);
|
|
}
|
|
ssize_t directiveb = send_initial_directives(ti->qterm, fd);
|
|
if(directiveb < 0){
|
|
return -1;
|
|
}
|
|
total += directiveb;
|
|
loginfo("sent %" PRIuPTR "B", total);
|
|
return 0;
|
|
}
|
|
|
|
int enter_alternate_screen(int fd, FILE* ttyfp, tinfo* ti, unsigned drain){
|
|
if(ti->in_alt_screen){
|
|
return 0;
|
|
}
|
|
const char* popcolors = get_escape(ti, ESCAPE_RESTORECOLORS);
|
|
if(popcolors){
|
|
if(term_emit(popcolors, ttyfp, true)){
|
|
return -1;
|
|
}
|
|
}
|
|
const char* smcup = get_escape(ti, ESCAPE_SMCUP);
|
|
if(smcup == NULL){
|
|
logerror("alternate screen is unavailable");
|
|
return -1;
|
|
}
|
|
if(!drain){
|
|
if(ti->kbdlevel){
|
|
if(tty_emit(KKEYBOARD_POP, fd)){
|
|
return -1;
|
|
}
|
|
}else{
|
|
if(tty_emit(XTMODKEYSUNDO, fd)){
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
if(tty_emit(smcup, fd) < 0){
|
|
return -1;
|
|
}
|
|
if(!drain){
|
|
if(ti->kbdlevel){
|
|
if(tty_emit(KKBDENTER, fd)){
|
|
return -1;
|
|
}
|
|
}else{
|
|
if(tty_emit(XTMODKEYS, fd)){
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
const char* pushcolors = get_escape(ti, ESCAPE_SAVECOLORS);
|
|
if(pushcolors){
|
|
if(term_emit(pushcolors, ttyfp, true)){
|
|
return -1;
|
|
}
|
|
}
|
|
ti->in_alt_screen = true;
|
|
return 0;
|
|
}
|
|
|
|
// we need to send the palette push/pop to the bulk out (as that's where the
|
|
// palette reprogramming happens), but rmcup+keyboard go to ttyfd.
|
|
int leave_alternate_screen(int fd, FILE* fp, tinfo* ti, unsigned drain){
|
|
if(!ti->in_alt_screen){
|
|
return 0;
|
|
}
|
|
const char* rmcup = get_escape(ti, ESCAPE_RMCUP);
|
|
if(rmcup == NULL){
|
|
logerror("can't leave alternate screen");
|
|
return -1;
|
|
}
|
|
if(!drain){
|
|
if(ti->kbdlevel){
|
|
if(tty_emit(KKEYBOARD_POP, fd)){
|
|
return -1;
|
|
}
|
|
}else{
|
|
if(tty_emit(XTMODKEYSUNDO, fd)){
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
const char* popcolors = get_escape(ti, ESCAPE_RESTORECOLORS);
|
|
if(popcolors){
|
|
if(term_emit(popcolors, fp, true)){
|
|
return -1;
|
|
}
|
|
}
|
|
if(tty_emit(rmcup, fd)){
|
|
return -1;
|
|
}
|
|
if(!drain){
|
|
if(ti->kbdlevel){
|
|
if(tty_emit(KKBDENTER, fd)){
|
|
return -1;
|
|
}
|
|
}else{
|
|
if(tty_emit(XTMODKEYS, fd)){
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
const char* pushcolors = get_escape(ti, ESCAPE_SAVECOLORS);
|
|
if(pushcolors){
|
|
if(term_emit(popcolors, fp, true)){
|
|
return -1;
|
|
}
|
|
}
|
|
ti->in_alt_screen = false;
|
|
return 0;
|
|
}
|
|
|
|
// if we get a response to the standard cursor locator escape, we know this
|
|
// terminal supports it, hah.
|
|
static int
|
|
add_u7_escape(tinfo* ti, size_t* tablelen, size_t* tableused){
|
|
const char* u7 = get_escape(ti, ESCAPE_U7);
|
|
if(u7){
|
|
return 0; // already present
|
|
}
|
|
if(grow_esc_table(ti, DSRCPR, ESCAPE_U7, tablelen, tableused)){
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
add_smulx_escapes(tinfo* ti, size_t* tablelen, size_t* tableused){
|
|
if(get_escape(ti, ESCAPE_SMULX)){
|
|
return 0;
|
|
}
|
|
if(grow_esc_table(ti, "\x1b[4:3m", ESCAPE_SMULX, tablelen, tableused) ||
|
|
grow_esc_table(ti, "\x1b[4:0m", ESCAPE_SMULNOX, tablelen, tableused)){
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static inline void
|
|
kill_escape(tinfo* ti, escape_e e){
|
|
ti->escindices[e] = 0;
|
|
}
|
|
|
|
static void
|
|
kill_appsync_escapes(tinfo* ti){
|
|
kill_escape(ti, ESCAPE_BSUM);
|
|
kill_escape(ti, ESCAPE_ESUM);
|
|
}
|
|
|
|
static int
|
|
add_appsync_escapes_sm(tinfo* ti, size_t* tablelen, size_t* tableused){
|
|
if(get_escape(ti, ESCAPE_BSUM)){
|
|
return 0;
|
|
}
|
|
if(grow_esc_table(ti, "\x1b[?2026h", ESCAPE_BSUM, tablelen, tableused) ||
|
|
grow_esc_table(ti, "\x1b[?2026l", ESCAPE_ESUM, tablelen, tableused)){
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
add_appsync_escapes_dcs(tinfo* ti, size_t* tablelen, size_t* tableused){
|
|
if(get_escape(ti, ESCAPE_BSUM)){
|
|
return 0;
|
|
}
|
|
if(grow_esc_table(ti, "\x1bP=1s\x1b\\", ESCAPE_BSUM, tablelen, tableused) ||
|
|
grow_esc_table(ti, "\x1bP=2s\x1b\\", ESCAPE_ESUM, tablelen, tableused)){
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
add_pushcolors_escapes(tinfo* ti, size_t* tablelen, size_t* tableused){
|
|
if(get_escape(ti, ESCAPE_SAVECOLORS)){
|
|
return 0;
|
|
}
|
|
if(grow_esc_table(ti, "\x1b[#P", ESCAPE_SAVECOLORS, tablelen, tableused) ||
|
|
grow_esc_table(ti, "\x1b[#Q", ESCAPE_RESTORECOLORS, tablelen, tableused)){
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static const char*
|
|
apply_kitty_heuristics(tinfo* ti, size_t* tablelen, size_t* tableused){
|
|
// see https://sw.kovidgoyal.net/kitty/protocol-extensions.html
|
|
ti->bg_collides_default |= 0x1000000;
|
|
ti->caps.sextants = true; // work since bugfix in 0.19.3
|
|
ti->caps.quadrants = true;
|
|
ti->caps.rgb = true;
|
|
if(add_smulx_escapes(ti, tablelen, tableused)){
|
|
return NULL;
|
|
}
|
|
/*if(compare_versions(ti->termversion, "0.22.1") >= 0){
|
|
setup_kitty_bitmaps(ti, ti->ttyfd, NCPIXEL_KITTY_SELFREF);
|
|
}else*/ if(compare_versions(ti->termversion, "0.20.0") >= 0){
|
|
setup_kitty_bitmaps(ti, ti->ttyfd, NCPIXEL_KITTY_ANIMATED);
|
|
// XTPOPCOLORS didn't reliably work until a bugfix late in 0.23.1 (see
|
|
// https://github.com/kovidgoyal/kitty/issues/4351), so reprogram the
|
|
// font directly until we exceed that version.
|
|
if(compare_versions(ti->termversion, "0.23.1") > 0){
|
|
if(add_pushcolors_escapes(ti, tablelen, tableused)){
|
|
return NULL;
|
|
}
|
|
}
|
|
}else{
|
|
setup_kitty_bitmaps(ti, ti->ttyfd, NCPIXEL_KITTY_STATIC);
|
|
}
|
|
// kitty SUM doesn't want long sequences, which is exactly where we use
|
|
// it. remove support (we pick it up from queries).
|
|
kill_appsync_escapes(ti);
|
|
ti->gratuitous_hpa = true;
|
|
return "Kitty";
|
|
}
|
|
|
|
static const char*
|
|
apply_alacritty_heuristics(tinfo* ti, size_t* tablelen, size_t* tableused,
|
|
bool* forcesdm, bool* invertsixel){
|
|
ti->caps.quadrants = true;
|
|
// ti->caps.sextants = true; // alacritty https://github.com/alacritty/alacritty/issues/4409
|
|
ti->caps.rgb = true;
|
|
// Alacritty implements DCS ASU, but no detection for it
|
|
if(add_appsync_escapes_dcs(ti, tablelen, tableused)){
|
|
return NULL;
|
|
}
|
|
*forcesdm = true;
|
|
if(compare_versions(ti->termversion, "0.15.1") < 0){
|
|
*invertsixel = true;
|
|
}
|
|
return "Alacritty";
|
|
}
|
|
|
|
static const char*
|
|
apply_vte_heuristics(tinfo* ti, size_t* tablelen, size_t* tableused){
|
|
ti->caps.quadrants = true;
|
|
ti->caps.sextants = true; // VTE has long enjoyed good sextant support
|
|
if(add_smulx_escapes(ti, tablelen, tableused)){
|
|
return NULL;
|
|
}
|
|
// VTE understands DSC ACU, but doesn't do anything with it; don't use it
|
|
return "VTE";
|
|
}
|
|
|
|
static const char*
|
|
apply_foot_heuristics(tinfo* ti, bool* forcesdm, bool* invertsixel){
|
|
ti->caps.sextants = true;
|
|
ti->caps.quadrants = true;
|
|
ti->caps.rgb = true;
|
|
*forcesdm = true;
|
|
if(compare_versions(ti->termversion, "1.8.2") < 0){
|
|
*invertsixel = true;
|
|
}
|
|
return "foot";
|
|
}
|
|
|
|
static const char*
|
|
apply_gnuscreen_heuristics(tinfo* ti){
|
|
if(compare_versions(ti->termversion, "5.0") < 0){
|
|
ti->caps.rgb = false;
|
|
}
|
|
return "GNU screen";
|
|
}
|
|
|
|
static const char*
|
|
apply_mlterm_heuristics(tinfo* ti){
|
|
ti->caps.quadrants = true; // good caps.quadrants, no caps.sextants as of 3.9.0
|
|
return "MLterm";
|
|
}
|
|
|
|
static const char*
|
|
apply_wezterm_heuristics(tinfo* ti, size_t* tablelen, size_t* tableused){
|
|
ti->caps.rgb = true;
|
|
ti->caps.quadrants = true;
|
|
if(ti->termversion && strcmp(ti->termversion, "20210610") >= 0){
|
|
ti->caps.sextants = true; // good caps.sextants as of 2021-06-10
|
|
if(add_smulx_escapes(ti, tablelen, tableused)){
|
|
return NULL;
|
|
}
|
|
}
|
|
return "WezTerm";
|
|
}
|
|
|
|
static const char*
|
|
apply_xterm_heuristics(tinfo* ti, size_t* tablelen, size_t* tableused,
|
|
bool* forcesdm, bool* invertsixel){
|
|
*forcesdm = true;
|
|
if(compare_versions(ti->termversion, "369") < 0){
|
|
*invertsixel = true; // xterm 369 inverted DECSDM
|
|
}
|
|
// xterm 357 added color palette escapes XT{PUSH,POP,REPORT}COLORS
|
|
if(compare_versions(ti->termversion, "357") >= 0){
|
|
if(add_pushcolors_escapes(ti, tablelen, tableused)){
|
|
return NULL;
|
|
}
|
|
}
|
|
return "XTerm";
|
|
}
|
|
|
|
static const char*
|
|
apply_mintty_heuristics(tinfo* ti, size_t* tablelen, size_t* tableused,
|
|
bool* forcesdm, bool* invertsixel){
|
|
if(add_smulx_escapes(ti, tablelen, tableused)){
|
|
return NULL;
|
|
}
|
|
*forcesdm = true;
|
|
if(compare_versions(ti->termversion, "3.5.2") < 0){
|
|
*invertsixel = true;
|
|
}
|
|
ti->bce = true;
|
|
return "MinTTY";
|
|
}
|
|
|
|
static const char*
|
|
apply_msterminal_heuristics(tinfo* ti){
|
|
ti->caps.rgb = true;
|
|
ti->caps.quadrants = true;
|
|
return "Windows ConHost";
|
|
}
|
|
|
|
static const char*
|
|
apply_contour_heuristics(tinfo* ti, size_t* tablelen, size_t* tableused,
|
|
bool* forcesdm, bool* invertsixel){
|
|
if(add_smulx_escapes(ti, tablelen, tableused)){
|
|
return NULL;
|
|
}
|
|
if(add_pushcolors_escapes(ti, tablelen, tableused)){
|
|
return NULL;
|
|
}
|
|
ti->caps.quadrants = true;
|
|
ti->caps.sextants = true;
|
|
ti->caps.rgb = true;
|
|
*forcesdm = true;
|
|
*invertsixel = false;
|
|
return "Contour";
|
|
}
|
|
|
|
static const char*
|
|
apply_iterm_heuristics(tinfo* ti, size_t* tablelen, size_t* tableused){
|
|
// iTerm implements DCS ASU, but has no detection for it
|
|
if(add_appsync_escapes_dcs(ti, tablelen, tableused)){
|
|
return NULL;
|
|
}
|
|
ti->caps.quadrants = true;
|
|
ti->caps.rgb = true;
|
|
return "iTerm2";
|
|
}
|
|
|
|
static const char*
|
|
apply_rxvt_heuristics(tinfo* ti){
|
|
ti->caps.braille = false;
|
|
ti->caps.quadrants = true;
|
|
return "RXVT";
|
|
}
|
|
|
|
static const char*
|
|
apply_terminology_heuristics(tinfo* ti){
|
|
// no RGB as of at least 1.9.0
|
|
ti->caps.quadrants = true;
|
|
return "Terminology";
|
|
}
|
|
|
|
static const char*
|
|
apply_konsole_heuristics(tinfo* ti){
|
|
ti->caps.rgb = true;
|
|
ti->caps.quadrants = true;
|
|
return "Konsole";
|
|
}
|
|
|
|
static const char*
|
|
apply_linux_heuristics(tinfo* ti, unsigned nonewfonts){
|
|
const char* tname = NULL;
|
|
#ifdef __linux__
|
|
struct utsname un;
|
|
if(uname(&un) == 0){
|
|
ti->termversion = strdup(un.release);
|
|
}
|
|
if(is_linux_framebuffer(ti)){
|
|
tname = "FBcon";
|
|
setup_fbcon_bitmaps(ti, ti->linux_fb_fd);
|
|
}else{
|
|
tname = "VT";
|
|
}
|
|
ti->caps.halfblocks = false;
|
|
ti->caps.braille = false; // no caps.braille, no caps.sextants in linux console
|
|
if(ti->ttyfd >= 0){
|
|
reprogram_console_font(ti, nonewfonts, &ti->caps.halfblocks,
|
|
&ti->caps.quadrants);
|
|
}
|
|
// assume no useful unicode drawing unless we're positively sure
|
|
#else
|
|
(void)ti;
|
|
(void)nonewfonts;
|
|
#endif
|
|
return tname;
|
|
}
|
|
|
|
// qui si convien lasciare ogne sospetto; ogne viltà convien che qui sia morta.
|
|
// in a more perfect world, this function would not exist, but this is a
|
|
// regrettably imperfect world, and thus all manner of things are not maintained
|
|
// in terminfo, and old terminfos abound, and users don't understand terminfo,
|
|
// so we override and/or supply various properties based on terminal
|
|
// identification performed earlier. we still get most things from terminfo,
|
|
// though, so it's something of a worst-of-all-worlds deal where TERM still
|
|
// needs be correct, even though we identify the terminal. le sigh.
|
|
static int
|
|
apply_term_heuristics(tinfo* ti, const char* tname, queried_terminals_e qterm,
|
|
size_t* tablelen, size_t* tableused,
|
|
bool* forcesdm, bool* invertsixel,
|
|
unsigned nonewfonts){
|
|
#ifdef __MINGW32__
|
|
if(qterm == TERMINAL_UNKNOWN){
|
|
qterm = TERMINAL_MSTERMINAL;
|
|
}
|
|
#endif
|
|
if(!tname){
|
|
// setupterm interprets a missing/empty TERM variable as the special value “unknown”.
|
|
tname = ti->termname ? ti->termname : "unknown";
|
|
}
|
|
// st had neither caps.sextants nor caps.quadrants last i checked (0.8.4)
|
|
ti->caps.braille = true; // most everyone has working caps.braille, even from fonts
|
|
ti->caps.halfblocks = true; // most everyone has working halfblocks
|
|
const char* newname = NULL;
|
|
switch(qterm){
|
|
case TERMINAL_KITTY:
|
|
newname = apply_kitty_heuristics(ti, tablelen, tableused);
|
|
break;
|
|
case TERMINAL_ALACRITTY:
|
|
newname = apply_alacritty_heuristics(ti, tablelen, tableused,
|
|
forcesdm, invertsixel);
|
|
break;
|
|
case TERMINAL_VTE:
|
|
newname = apply_vte_heuristics(ti, tablelen, tableused);
|
|
break;
|
|
case TERMINAL_FOOT:
|
|
newname = apply_foot_heuristics(ti, forcesdm, invertsixel);
|
|
break;
|
|
case TERMINAL_TMUX:
|
|
newname = "tmux"; // FIXME what, oh what to do with tmux?
|
|
break;
|
|
case TERMINAL_GNUSCREEN:
|
|
newname = apply_gnuscreen_heuristics(ti);
|
|
break;
|
|
case TERMINAL_MLTERM:
|
|
newname = apply_mlterm_heuristics(ti);
|
|
break;
|
|
case TERMINAL_WEZTERM:
|
|
newname = apply_wezterm_heuristics(ti, tablelen, tableused);
|
|
break;
|
|
case TERMINAL_XTERM:
|
|
newname = apply_xterm_heuristics(ti, tablelen, tableused,
|
|
forcesdm, invertsixel);
|
|
break;
|
|
case TERMINAL_MINTTY:
|
|
newname = apply_mintty_heuristics(ti, tablelen, tableused,
|
|
forcesdm, invertsixel);
|
|
break;
|
|
case TERMINAL_MSTERMINAL:
|
|
newname = apply_msterminal_heuristics(ti);
|
|
break;
|
|
case TERMINAL_CONTOUR:
|
|
newname = apply_contour_heuristics(ti, tablelen, tableused,
|
|
forcesdm, invertsixel);
|
|
break;
|
|
case TERMINAL_ITERM:
|
|
newname = apply_iterm_heuristics(ti, tablelen, tableused);
|
|
break;
|
|
case TERMINAL_RXVT:
|
|
newname = apply_rxvt_heuristics(ti);
|
|
break;
|
|
case TERMINAL_APPLE:
|
|
newname = "Terminal.app"; // no quadrants, no sextants, no rgb, but it does have braille
|
|
break;
|
|
case TERMINAL_LINUX:
|
|
newname = apply_linux_heuristics(ti, nonewfonts);
|
|
break;
|
|
case TERMINAL_TERMINOLOGY:
|
|
newname = apply_terminology_heuristics(ti);
|
|
break;
|
|
case TERMINAL_KONSOLE:
|
|
newname = apply_konsole_heuristics(ti);
|
|
break;
|
|
default:
|
|
newname = tname;
|
|
break;
|
|
}
|
|
if(newname == NULL){
|
|
logerror("no name provided for termtype %d", qterm);
|
|
return -1;
|
|
}
|
|
tname = newname;
|
|
// run a wcwidth(⣿) to guarantee libc Unicode 3 support, independent of term
|
|
if(wcwidth(L'⣿') < 0){
|
|
ti->caps.braille = false;
|
|
}
|
|
// run a wcwidth(🬸) to guarantee libc Unicode 13 support, independent of term
|
|
if(wcwidth(L'🬸') < 0){
|
|
ti->caps.sextants = false;
|
|
}
|
|
ti->termname = tname;
|
|
return 0;
|
|
}
|
|
|
|
// some terminals cannot combine certain styles with colors, as expressed in
|
|
// the "ncv" terminfo capability (using ncurses-style constants). don't
|
|
// advertise support for the style in that case. otherwise, if the style is
|
|
// supported, OR it into supported_styles (using Notcurses-style constants).
|
|
static void
|
|
build_supported_styles(tinfo* ti){
|
|
const struct style {
|
|
unsigned s; // NCSTYLE_* value
|
|
int esc; // ESCAPE_* value for enable
|
|
const char* tinfo; // terminfo capability for conditional permit
|
|
unsigned ncvbit; // bit in "ncv" mask for unconditional deny
|
|
} styles[] = {
|
|
{ NCSTYLE_BOLD, ESCAPE_BOLD, "bold", A_BOLD },
|
|
{ NCSTYLE_UNDERLINE, ESCAPE_SMUL, "smul", A_UNDERLINE },
|
|
{ NCSTYLE_ITALIC, ESCAPE_SITM, "sitm", A_ITALIC },
|
|
{ NCSTYLE_STRUCK, ESCAPE_SMXX, "smxx", 0 },
|
|
{ NCSTYLE_UNDERCURL, ESCAPE_SMULX, "Smulx", 0 },
|
|
{ 0, 0, NULL, 0 }
|
|
};
|
|
int nocolor_stylemask = tigetnum("ncv");
|
|
for(typeof(*styles)* s = styles ; s->s ; ++s){
|
|
if(get_escape(ti, s->esc)){
|
|
if(nocolor_stylemask > 0){
|
|
if(nocolor_stylemask & s->ncvbit){
|
|
ti->escindices[s->esc] = 0;
|
|
continue;
|
|
}
|
|
}
|
|
ti->supported_styles |= s->s;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef __APPLE__
|
|
// Terminal.App is a wretched piece of shit that can't handle even the most
|
|
// basic of queries, instead bleeding them through to stdout like a great
|
|
// wounded hippopotamus. it does export "TERM_PROGRAM=Apple_Terminal", becuase
|
|
// it is a committee on sewage and drainage where all the members have
|
|
// tourette's. on mac os, if TERM_PROGRAM=Apple_Terminal, accept this hideous
|
|
// existence, circumvent all queries, and may god have mercy on our souls.
|
|
// of course that means if a terminal launched from Terminal.App doesn't clear
|
|
// or reset this environment variable, they're cursed to live as Terminal.App.
|
|
// i'm likewise unsure what we're supposed to do should you ssh anywhere =[.
|
|
static queried_terminals_e
|
|
macos_early_matches(void){
|
|
const char* tp = getenv("TERM_PROGRAM");
|
|
if(tp == NULL){
|
|
return TERMINAL_UNKNOWN;
|
|
}
|
|
if(strcmp(tp, "Apple_Terminal")){
|
|
return TERMINAL_UNKNOWN;
|
|
}
|
|
return TERMINAL_APPLE;
|
|
}
|
|
#endif
|
|
|
|
#ifndef __APPLE__
|
|
#ifndef __MINGW32__
|
|
// rxvt has a deeply fucked up palette code implementation. its responses are
|
|
// terminated with a bare ESC instead of BEL or ST, impossible to encode in
|
|
// our automaton alongside the proper flow. its "oc" doesn't reset the palette,
|
|
// meaning we must preserve and reload it ourselves. there's no way to identify
|
|
// rxvt via query, so if we get it in TERM, set up our automaton for its fubar
|
|
// replies, and don't bother sending any identification requests.
|
|
static queried_terminals_e
|
|
unix_early_matches(const char* term){
|
|
if(term == NULL){
|
|
return TERMINAL_UNKNOWN;
|
|
}
|
|
// urxvt likewise declares TERM=rxvt-whatever
|
|
if(strncmp(term, "rxvt", 4) == 0){
|
|
return TERMINAL_RXVT;
|
|
}
|
|
return TERMINAL_UNKNOWN;
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
static int
|
|
do_terminfo_lookups(tinfo *ti, size_t* tablelen, size_t* tableused){
|
|
// don't list any here for which we also send XTGETTCAP sequences
|
|
const struct strtdesc {
|
|
escape_e esc;
|
|
const char* tinfo;
|
|
} strtdescs[] = {
|
|
{ ESCAPE_CUP, "cup", },
|
|
{ ESCAPE_VPA, "vpa", },
|
|
// Not all terminals support setting the fore/background independently
|
|
{ ESCAPE_SETAF, "setaf", },
|
|
{ ESCAPE_SETAB, "setab", },
|
|
{ ESCAPE_OP, "op", },
|
|
{ ESCAPE_CNORM, "cnorm", },
|
|
{ ESCAPE_CIVIS, "civis", },
|
|
{ ESCAPE_SGR0, "sgr0", },
|
|
{ ESCAPE_SITM, "sitm", },
|
|
{ ESCAPE_RITM, "ritm", },
|
|
{ ESCAPE_BOLD, "bold", },
|
|
{ ESCAPE_CUD, "cud", },
|
|
{ ESCAPE_CUU, "cuu", },
|
|
{ ESCAPE_CUF, "cuf", },
|
|
{ ESCAPE_CUB, "cub", },
|
|
{ ESCAPE_U7, "u7", },
|
|
{ ESCAPE_SMKX, "smkx", },
|
|
{ ESCAPE_SMXX, "smxx", },
|
|
{ ESCAPE_EL, "el", },
|
|
{ ESCAPE_RMXX, "rmxx", },
|
|
{ ESCAPE_SMUL, "smul", },
|
|
{ ESCAPE_RMUL, "rmul", },
|
|
{ ESCAPE_SC, "sc", },
|
|
{ ESCAPE_RC, "rc", },
|
|
{ ESCAPE_IND, "ind", },
|
|
{ ESCAPE_INDN, "indn", },
|
|
{ ESCAPE_CLEAR, "clear", },
|
|
{ ESCAPE_OC, "oc", },
|
|
{ ESCAPE_RMKX, "rmkx", },
|
|
{ ESCAPE_INITC, "initc", },
|
|
{ ESCAPE_MAX, NULL, },
|
|
};
|
|
for(typeof(*strtdescs)* strtdesc = strtdescs ; strtdesc->esc < ESCAPE_MAX ; ++strtdesc){
|
|
if(init_terminfo_esc(ti, strtdesc->tinfo, strtdesc->esc, tablelen, tableused)){
|
|
return -1;
|
|
}
|
|
}
|
|
// verify that the terminal provides cursor addressing (absolute movement)
|
|
if(ti->escindices[ESCAPE_CUP] == 0){
|
|
logpanic("required terminfo capability 'cup' not defined");
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// handle any terminal query responses.
|
|
static int
|
|
handle_responses(tinfo* ti, size_t* tablelen, size_t* tableused,
|
|
int* cursor_y, int* cursor_x, unsigned draininput,
|
|
unsigned* kitty_graphics){
|
|
struct initial_responses* iresp;
|
|
if((iresp = inputlayer_get_responses(ti->ictx)) == NULL){
|
|
goto err;
|
|
}
|
|
if(ti->termversion){
|
|
free(iresp->version);
|
|
}else{
|
|
ti->termversion = iresp->version; // takes ownership
|
|
}
|
|
if(iresp->appsync_supported){
|
|
if(add_appsync_escapes_sm(ti, tablelen, tableused)){
|
|
free(iresp->hpa);
|
|
goto err;
|
|
}
|
|
}
|
|
if(iresp->hpa){
|
|
if(grow_esc_table(ti, iresp->hpa, ESCAPE_HPA, tablelen, tableused)){
|
|
free(iresp->hpa);
|
|
goto err;
|
|
}
|
|
free(iresp->hpa);
|
|
}
|
|
if((ti->kbdlevel = iresp->kbdlevel) == UINT_MAX){
|
|
ti->kbdlevel = 0;
|
|
if(!draininput){
|
|
if(tty_emit(XTMODKEYS, ti->ttyfd) < 0){
|
|
goto err;
|
|
}
|
|
}
|
|
}else{
|
|
ti->kittykbdsupport = true;
|
|
}
|
|
if(iresp->qterm != TERMINAL_UNKNOWN){
|
|
ti->qterm = iresp->qterm;
|
|
}
|
|
*cursor_y = iresp->cursory;
|
|
*cursor_x = iresp->cursorx;
|
|
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;
|
|
ti->dimy = iresp->dimy;
|
|
ti->dimx = iresp->dimx;
|
|
}
|
|
if(iresp->maxpaletteread >= 0){
|
|
memcpy(ti->originalpalette.chans, iresp->palette.chans,
|
|
sizeof(*ti->originalpalette.chans) * (iresp->maxpaletteread + 1));
|
|
ti->maxpaletteread = iresp->maxpaletteread;
|
|
}
|
|
if(iresp->rgb){
|
|
ti->caps.rgb = true;
|
|
}
|
|
if(iresp->pixy && iresp->pixx){
|
|
ti->pixy = iresp->pixy;
|
|
ti->pixx = iresp->pixx;
|
|
}
|
|
if(ti->default_rows && ti->default_cols){
|
|
ti->cellpxy = ti->pixy / ti->default_rows;
|
|
ti->cellpxx = ti->pixx / ti->default_cols;
|
|
}
|
|
if(iresp->got_bg){
|
|
// reset the 0xfe000000 we loaded during initialization. if we're
|
|
// kitty, we'll add the 0x01000000 in during heuristics.
|
|
ti->bg_collides_default = iresp->bg;
|
|
}
|
|
if(iresp->got_fg){
|
|
ti->fg_default = iresp->fg;
|
|
}
|
|
// kitty trumps sixel, when both are available
|
|
if((*kitty_graphics = iresp->kitty_graphics) == 0){
|
|
if((ti->color_registers = iresp->color_registers) > SIXEL_MAX_REGISTERS){
|
|
ti->color_registers = SIXEL_MAX_REGISTERS;
|
|
}
|
|
ti->sixel_maxy_pristine = iresp->sixely;
|
|
ti->sixel_maxy = iresp->sixely;
|
|
ti->sixel_maxx = iresp->sixelx;
|
|
}
|
|
ti->pixelmice = iresp->pixelmice;
|
|
if(iresp->rectangular_edits){
|
|
if(grow_esc_table(ti, "\x1b[%p1%d;%p2%d;%p3%d;$z", ESCAPE_DECERA, tablelen, tableused)){
|
|
goto err;
|
|
}
|
|
}
|
|
free(iresp);
|
|
return 0;
|
|
|
|
err:
|
|
free(iresp);
|
|
return -1;
|
|
}
|
|
|
|
// if |termtype| is not NULL, it is used to look up the terminfo database entry
|
|
// via setupterm(). the value of the TERM environment variable is otherwise
|
|
// (implicitly) used. some details are not exposed via terminfo, and we must
|
|
// make heuristic decisions based on the detected terminal type, yuck :/.
|
|
// the first thing we do is fire off any queries we have (XTSMGRAPHICS, etc.)
|
|
// with a trailing Device Attributes. all known terminals will reply to a
|
|
// Device Attributes, allowing us to get a negative response if our queries
|
|
// aren't supported by the terminal. we fire it off early because we have a
|
|
// full round trip before getting the reply, which is likely to pace init.
|
|
int interrogate_terminfo(tinfo* ti, FILE* out, unsigned utf8,
|
|
unsigned noaltscreen, unsigned nocbreak, unsigned nonewfonts,
|
|
int* cursor_y, int* cursor_x, ncsharedstats* stats,
|
|
int lmargin, int tmargin, int rmargin, int bmargin,
|
|
unsigned draininput){
|
|
// if a specified termtype was provided in the notcurses_options, it was
|
|
// loaded into our environment at TERM.
|
|
const char* termtype = getenv("TERM");
|
|
int foolcursor_x, foolcursor_y;
|
|
if(!cursor_x){
|
|
cursor_x = &foolcursor_x;
|
|
}
|
|
if(!cursor_y){
|
|
cursor_y = &foolcursor_y;
|
|
}
|
|
*cursor_x = *cursor_y = -1;
|
|
ti->sixelengine = NULL;
|
|
ti->bg_collides_default = 0xfe000000;
|
|
ti->fg_default = 0xff000000;
|
|
ti->kbdlevel = UINT_MAX; // see comment in tinfo definition
|
|
ti->maxpaletteread = -1;
|
|
ti->qterm = TERMINAL_UNKNOWN;
|
|
// we don't need a controlling tty for everything we do; allow a failure here
|
|
ti->ttyfd = get_tty_fd(out);
|
|
ti->gpmfd = -1;
|
|
size_t tablelen = 0;
|
|
size_t tableused = 0;
|
|
const char* tname = NULL;
|
|
#ifdef __APPLE__
|
|
ti->qterm = macos_early_matches();
|
|
#elif defined(__MINGW32__)
|
|
if(termtype){
|
|
logwarn("termtype (%s) ignored on windows", termtype);
|
|
}
|
|
if(prepare_windows_terminal(ti, &tablelen, &tableused)){
|
|
logpanic("failed opening Windows ConPTY");
|
|
return -1;
|
|
}
|
|
#else
|
|
ti->qterm = unix_early_matches(termtype);
|
|
#if defined(__linux__)
|
|
ti->linux_fb_fd = -1;
|
|
ti->linux_fbuffer = MAP_FAILED;
|
|
// we might or might not program quadrants into the console font
|
|
if(is_linux_console(ti->ttyfd)){
|
|
ti->qterm = TERMINAL_LINUX;
|
|
}
|
|
#endif
|
|
#endif
|
|
if(ti->ttyfd >= 0){
|
|
if((ti->tpreserved = calloc(1, sizeof(*ti->tpreserved))) == NULL){
|
|
return -1;
|
|
}
|
|
if(tcgetattr(ti->ttyfd, ti->tpreserved)){
|
|
logpanic("couldn't preserve terminal state for %d (%s)", ti->ttyfd, strerror(errno));
|
|
free(ti->tpreserved);
|
|
return -1;
|
|
}
|
|
// enter cbreak mode regardless of user preference until we've performed
|
|
// terminal interrogation. at that point, we might restore original mode.
|
|
if(cbreak_mode(ti)){
|
|
free(ti->tpreserved);
|
|
return -1;
|
|
}
|
|
// if we already know our terminal (e.g. on the linux console), there's no
|
|
// need to send the identification queries. the controls are sufficient.
|
|
bool minimal = (ti->qterm != TERMINAL_UNKNOWN);
|
|
if(send_initial_queries(ti, minimal, noaltscreen, draininput)){
|
|
goto err;
|
|
}
|
|
}
|
|
#ifndef __MINGW32__
|
|
// 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.).
|
|
int termerr;
|
|
if(setupterm(termtype, ti->ttyfd, &termerr)){
|
|
logpanic("terminfo error %d for [%s] (see terminfo(3ncurses))",
|
|
termerr, termtype ? termtype : "");
|
|
goto err;
|
|
}
|
|
tname = termname(); // longname() is also available
|
|
#endif
|
|
int linesigs_enabled = 1;
|
|
if(ti->tpreserved){
|
|
if(!(ti->tpreserved->c_lflag & ISIG)){
|
|
linesigs_enabled = 0;
|
|
}
|
|
}
|
|
if(init_inputlayer(ti, stdin, lmargin, tmargin, rmargin, bmargin,
|
|
stats, draininput, linesigs_enabled)){
|
|
goto err;
|
|
}
|
|
ti->sprixel_scale_height = 1;
|
|
get_default_geometry(ti);
|
|
ti->caps.utf8 = utf8;
|
|
// allow the "rgb" boolean terminfo capability, a COLORTERM environment
|
|
// variable of either "truecolor" or "24bit", or unconditionally enable it
|
|
// for several terminals known to always support 8bpc rgb setaf/setab.
|
|
if(ti->caps.colors == 0){
|
|
int colors = tigetnum("colors");
|
|
if(colors <= 0){
|
|
ti->caps.colors = 1;
|
|
}else{
|
|
ti->caps.colors = colors;
|
|
}
|
|
ti->caps.rgb = query_rgb(); // independent of colors
|
|
}
|
|
if(do_terminfo_lookups(ti, &tablelen, &tableused)){
|
|
goto err;
|
|
}
|
|
if(ti->ttyfd >= 0){
|
|
// if the keypad needn't be explicitly enabled, smkx is not present
|
|
const char* smkx = get_escape(ti, ESCAPE_SMKX);
|
|
if(smkx){
|
|
if(tty_emit(tiparm(smkx), ti->ttyfd) < 0){
|
|
logpanic("error enabling keypad transmit mode");
|
|
goto err;
|
|
}
|
|
}
|
|
}
|
|
if(tigetflag("bce") > 0){
|
|
ti->bce = true;
|
|
}
|
|
if(ti->caps.colors > 1){
|
|
const char* initc = get_escape(ti, ESCAPE_INITC);
|
|
if(initc){
|
|
ti->caps.can_change_colors = true;
|
|
}
|
|
}else{ // disable initc if there's no color support
|
|
ti->escindices[ESCAPE_INITC] = 0;
|
|
}
|
|
// neither of these is supported on e.g. the "linux" virtual console.
|
|
if(!noaltscreen){
|
|
if(init_terminfo_esc(ti, "smcup", ESCAPE_SMCUP, &tablelen, &tableused) ||
|
|
init_terminfo_esc(ti, "rmcup", ESCAPE_RMCUP, &tablelen, &tableused)){
|
|
goto err;
|
|
}
|
|
const char* smcup = get_escape(ti, ESCAPE_SMCUP);
|
|
if(smcup){
|
|
ti->in_alt_screen = 1;
|
|
// if we're not using the standard smcup, our initial hardcoded use of it
|
|
// presumably had no effect; warn the user.
|
|
if(strcmp(smcup, SMCUP)){
|
|
logwarn("warning: non-standard smcup!");
|
|
}
|
|
}
|
|
}else{
|
|
ti->escindices[ESCAPE_SMCUP] = 0;
|
|
ti->escindices[ESCAPE_RMCUP] = 0;
|
|
}
|
|
if(get_escape(ti, ESCAPE_CIVIS) == NULL){
|
|
char* chts;
|
|
if(terminfostr(&chts, "chts") == 0){
|
|
if(grow_esc_table(ti, chts, ESCAPE_CIVIS, &tablelen, &tableused)){
|
|
goto err;
|
|
}
|
|
}
|
|
}
|
|
if(get_escape(ti, ESCAPE_BOLD)){
|
|
if(grow_esc_table(ti, "\e[22m", ESCAPE_NOBOLD, &tablelen, &tableused)){
|
|
goto err;
|
|
}
|
|
}
|
|
// if op is defined as ansi 39 + ansi 49, make the split definitions
|
|
// available. this ought be asserted by extension capability "ax", but
|
|
// no terminal i've found seems to do so. =[
|
|
const char* op = get_escape(ti, ESCAPE_OP);
|
|
if(op && strcmp(op, "\x1b[39;49m") == 0){
|
|
if(grow_esc_table(ti, "\x1b[39m", ESCAPE_FGOP, &tablelen, &tableused) ||
|
|
grow_esc_table(ti, "\x1b[49m", ESCAPE_BGOP, &tablelen, &tableused)){
|
|
goto err;
|
|
}
|
|
}
|
|
unsigned kitty_graphics = 0;
|
|
if(ti->ttyfd >= 0){
|
|
if(handle_responses(ti, &tablelen, &tableused, cursor_y, cursor_x,
|
|
draininput, &kitty_graphics)){
|
|
goto err;
|
|
}
|
|
if(nocbreak){
|
|
// FIXME do this in input later, upon signaling completion?
|
|
if(tcsetattr(ti->ttyfd, TCSANOW, ti->tpreserved)){
|
|
goto err;
|
|
}
|
|
}
|
|
}else{
|
|
ti->kbdlevel = 0; // confirmed no support, don't bother popping
|
|
}
|
|
// now look up any terminfo elements we might not have received via requests
|
|
if(ti->escindices[ESCAPE_HPA] == 0){
|
|
if(init_terminfo_esc(ti, "hpa", ESCAPE_HPA, &tablelen, &tableused)){
|
|
goto err;
|
|
}
|
|
}
|
|
if(*cursor_x >= 0 && *cursor_y >= 0){
|
|
if(add_u7_escape(ti, &tablelen, &tableused)){
|
|
goto err;
|
|
}
|
|
}
|
|
bool forcesdm = false;
|
|
bool invertsixel = false;
|
|
if(apply_term_heuristics(ti, tname, ti->qterm, &tablelen, &tableused,
|
|
&forcesdm, &invertsixel, nonewfonts)){
|
|
goto err;
|
|
}
|
|
build_supported_styles(ti);
|
|
if(ti->pixel_draw == NULL && ti->pixel_draw_late == NULL){
|
|
// color_registers was only assigned if kitty_graphics were unavailable
|
|
if(ti->color_registers > 0){
|
|
setup_sixel_bitmaps(ti, ti->ttyfd, forcesdm, invertsixel);
|
|
}
|
|
if(kitty_graphics){
|
|
setup_kitty_bitmaps(ti, ti->ttyfd, NCPIXEL_KITTY_STATIC);
|
|
}
|
|
}
|
|
return 0;
|
|
|
|
err:
|
|
if(ti->ttyfd >= 0){
|
|
// if we haven't yet received a reply confirming lack of kitty keyboard
|
|
// support, it'll be UINT_MAX, and we ought try to pop (in case we died
|
|
// following the keyboard set, but before confirming support).
|
|
if(ti->kbdlevel){
|
|
tty_emit(KKEYBOARD_POP, ti->ttyfd);
|
|
}
|
|
tty_emit(RMCUP, ti->ttyfd);
|
|
}
|
|
if(ti->tpreserved){
|
|
(void)tcsetattr(ti->ttyfd, TCSANOW, ti->tpreserved);
|
|
free(ti->tpreserved);
|
|
ti->tpreserved = NULL;
|
|
}
|
|
stop_inputlayer(ti);
|
|
free(ti->esctable);
|
|
free(ti->termversion);
|
|
del_curterm(cur_term);
|
|
close(ti->ttyfd);
|
|
ti->ttyfd = -1;
|
|
return -1;
|
|
}
|
|
|
|
char* termdesc_longterm(const tinfo* ti){
|
|
size_t tlen = strlen(ti->termname) + 1;
|
|
size_t slen = tlen;
|
|
if(ti->termversion){
|
|
slen += strlen(ti->termversion) + 1;
|
|
}
|
|
char* ret = malloc(slen);
|
|
if(ret){
|
|
memcpy(ret, ti->termname, tlen);
|
|
if(ti->termversion){
|
|
ret[tlen - 1] = ' ';
|
|
strcpy(ret + tlen, ti->termversion);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// send a u7 request, and wait until we have a cursor report. if input's ttyfd
|
|
// is valid, we can just camp there. otherwise, we need dance with potential
|
|
// user input looking at infd. note that we do not use Windows's
|
|
// GetConsoleScreenBufferInfo() because it is unreliable for this purpose
|
|
// when the viewing area is not aligned with the forward edge of the buffer,
|
|
// and also due to negative interactions with ssh.
|
|
int locate_cursor(tinfo* ti, unsigned* cursor_y, unsigned* cursor_x){
|
|
const char* u7 = get_escape(ti, ESCAPE_U7);
|
|
if(u7 == NULL){
|
|
logwarn("no support in terminfo");
|
|
return -1;
|
|
}
|
|
if(ti->ttyfd < 0){
|
|
logwarn("no valid path for cursor report");
|
|
return -1;
|
|
}
|
|
int fd = ti->ttyfd;
|
|
if(get_cursor_location(ti->ictx, u7, cursor_y, cursor_x)){
|
|
return -1;
|
|
}
|
|
loginfo("got a report from %d %d/%d", fd, *cursor_y, *cursor_x);
|
|
return 0;
|
|
}
|
|
|
|
int tiocgwinsz(int fd, struct winsize* ws){
|
|
#ifndef __MINGW32__
|
|
int i = ioctl(fd, TIOCGWINSZ, ws);
|
|
if(i < 0){
|
|
logerror("TIOCGWINSZ failed on %d (%s)", fd, strerror(errno));
|
|
return -1;
|
|
}
|
|
if(ws->ws_row <= 0 || ws->ws_col <= 0){
|
|
logerror("bogon from TIOCGWINSZ on %d (%d/%d)",
|
|
fd, ws->ws_row, ws->ws_col);
|
|
return -1;
|
|
}
|
|
#else
|
|
(void)fd;
|
|
(void)ws;
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
int cbreak_mode(tinfo* ti){
|
|
#ifndef __MINGW32__
|
|
int ttyfd = ti->ttyfd;
|
|
if(ttyfd < 0){
|
|
return 0;
|
|
}
|
|
// assume it's not a true terminal (e.g. we might be redirected to a file)
|
|
struct termios modtermios;
|
|
memcpy(&modtermios, ti->tpreserved, sizeof(modtermios));
|
|
// see termios(3). disabling ECHO and ICANON means input will not be echoed
|
|
// to the screen, input is made available without enter-based buffering, and
|
|
// line editing is disabled. since we have not gone into raw mode, ctrl+c
|
|
// etc. still have their typical effects. ICRNL maps return to 13 (Ctrl+M)
|
|
// instead of 10 (Ctrl+J).
|
|
modtermios.c_lflag &= (~ECHO & ~ICANON);
|
|
modtermios.c_iflag &= ~ICRNL;
|
|
if(tcsetattr(ttyfd, TCSANOW, &modtermios)){
|
|
logerror("error disabling echo / canonical on %d (%s)", ttyfd, strerror(errno));
|
|
return -1;
|
|
}
|
|
#else
|
|
// we don't yet have a way to take Cygwin/MSYS2 out of canonical mode FIXME.
|
|
DWORD mode;
|
|
if(!GetConsoleMode(ti->inhandle, &mode)){
|
|
logerror("error acquiring input mode");
|
|
return -1;
|
|
}
|
|
mode &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
|
|
if(!SetConsoleMode(ti->inhandle, mode)){
|
|
logerror("error setting input mode");
|
|
return -1;
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
// replace or populate the TERM environment variable with 'termname'
|
|
int putenv_term(const char* tname){
|
|
#define ENVVAR "TERM"
|
|
const char* oldterm = getenv(ENVVAR);
|
|
if(oldterm){
|
|
logdebug("replacing %s value %s with %s", ENVVAR, oldterm, tname);
|
|
}else{
|
|
loginfo("provided %s value %s", ENVVAR, tname);
|
|
}
|
|
if(oldterm && strcmp(oldterm, tname) == 0){
|
|
return 0;
|
|
}
|
|
char* buf = malloc(strlen(tname) + strlen(ENVVAR) + 1);
|
|
if(buf == NULL){
|
|
return -1;
|
|
}
|
|
int c = putenv(buf);
|
|
if(c){
|
|
logerror("couldn't export %s", buf);
|
|
}
|
|
free(buf);
|
|
return c;
|
|
}
|