diff --git a/src/lib/direct.c b/src/lib/direct.c index e23e8f6fc..f35304b97 100644 --- a/src/lib/direct.c +++ b/src/lib/direct.c @@ -1574,7 +1574,7 @@ int ncdirect_stream(ncdirect* n, const char* filename, ncstreamcb streamer, if(n->tcache.pixel_remove){ fbuf f = {0}; fbuf_init_small(&f); - if(n->tcache.pixel_remove(lastid, &f)){ + if(n->tcache.pixel_remove(&n->tcache, lastid, &f)){ fbuf_free(&f); ncvisual_destroy(ncv); return -1; diff --git a/src/lib/in.c b/src/lib/in.c index 064b645bc..9b200a9a8 100644 --- a/src/lib/in.c +++ b/src/lib/in.c @@ -1846,13 +1846,11 @@ build_cflow_automaton(inputctx* ictx){ { "[?1;0c", da1_cb, }, // CSI ? 1 ; 0 c ("VT101 with No Options") { "[?1;2c", da1_cb, }, // CSI ? 1 ; 2 c ("VT100 with Advanced Video Option") { "[?4;6c", da1_cb, }, // CSI ? 4 ; 6 c ("VT132 with Advanced Video and Graphics") - // CSI ? 1 2 ; Ps c ("VT125") - // CSI ? 6 0 ; Ps c (kmscon) - // CSI ? 6 2 ; Ps c ("VT220") - // CSI ? 6 3 ; Ps c ("VT320") - // CSI ? 6 4 ; Ps c ("VT420") - // CSI ? 6 5 ; Ps c (WezTerm, VT5xx?) - { "[?\\N;\\Dc", da1_attrs_cb, }, + { "[?1;2;\\Dc", da1_attrs_cb, }, // CSI ? 1 ; 2 ; Ps... c (VT100 with extended attrs, e.g. tmux sixel) + { "[?62;\\Dc", da1_attrs_cb, }, // CSI ? 6 2 ; Ps c ("VT220", Ghostty) + { "[?63;\\Dc", da1_attrs_cb, }, // CSI ? 6 3 ; Ps c ("VT320") + { "[?64;\\Dc", da1_attrs_cb, }, // CSI ? 6 4 ; Ps c ("VT420", iTerm2) + { "[?65;\\Dc", da1_attrs_cb, }, // CSI ? 6 5 ; Ps c ("VT520", WezTerm) { "[?1;0;\\NS", xtsmgraphics_cregs_cb, }, { "[?2;0;\\N;\\NS", xtsmgraphics_sixel_cb, }, { "[>83;\\N;0c", da2_screen_cb, }, diff --git a/src/lib/internal.h b/src/lib/internal.h index 4b49994f9..7ed8ac54d 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -736,7 +736,7 @@ sprite_redraw(notcurses* nc, const ncpile* p, sprixel* s, fbuf* f, int y, int x) // not emit it. we use sixel_maxy_pristine as a side channel to encode // this version information. bool noscroll = !ti->sixel_maxy_pristine; - return ti->pixel_move(s, f, noscroll, y, x); + return ti->pixel_move(ti, s, f, noscroll, y, x); }else{ if(!ti->pixel_draw){ return 0; @@ -758,7 +758,7 @@ sprite_commit(tinfo* ti, fbuf* f, sprixel* s, unsigned forcescroll){ // not emit it. we use sixel_maxy_pristine as a side channel to encode // this version information. direct mode, meanwhile, sets forcescroll. bool noscroll = !ti->sixel_maxy_pristine && !forcescroll; - if(ti->pixel_commit(f, s, noscroll) < 0){ + if(ti->pixel_commit(ti, f, s, noscroll) < 0){ return -1; } } diff --git a/src/lib/kitty.c b/src/lib/kitty.c index 7594d6193..197a9124c 100644 --- a/src/lib/kitty.c +++ b/src/lib/kitty.c @@ -1,11 +1,85 @@ #include "internal.h" #include "base64.h" +#include "in.h" // for TERMINAL_TMUX #ifdef USE_DEFLATE #include #else #include #endif +// Check if we're running in tmux and need passthrough +static inline bool +kitty_in_tmux(const tinfo* ti){ + return ti && ti->qterm == TERMINAL_TMUX; +} + +// Write a single APC sequence with tmux DCS passthrough wrapping. +// In tmux passthrough, all ESC (0x1b) bytes must be doubled. +static int +kitty_write_single_passthrough(fbuf* f, const char* buf, size_t len){ + // tmux DCS passthrough prefix: ESC P tmux ; + if(fbuf_puts(f, "\x1bPtmux;") < 0){ + return -1; + } + // Write data with doubled ESC bytes + size_t written = 0; + for(size_t i = 0; i < len; i++){ + if(buf[i] == '\x1b'){ + // Double the ESC byte for tmux passthrough + if(fbuf_putc(f, '\x1b') != 1){ + return -1; + } + written++; + } + if(fbuf_putc(f, buf[i]) != 1){ + return -1; + } + written++; + } + // tmux DCS passthrough suffix: ESC backslash (ST) + if(fbuf_puts(f, "\x1b\\") < 0){ + return -1; + } + return written; +} + +// Write kitty graphics data with tmux DCS passthrough wrapping. +// The buffer may contain multiple APC sequences (chunks). +// Each APC sequence (ESC _ ... ESC \) must be wrapped in its own passthrough. +static int +kitty_write_tmux_passthrough(fbuf* f, const char* buf, size_t len){ + size_t total_written = 0; + size_t i = 0; + + while(i < len){ + // Look for APC start: ESC _ + if(buf[i] == '\x1b' && i + 1 < len && buf[i + 1] == '_'){ + // Find the end of this APC sequence: ESC \ (ST) + size_t apc_start = i; + i += 2; // skip ESC _ + while(i < len){ + if(buf[i] == '\x1b' && i + 1 < len && buf[i + 1] == '\\'){ + // Found ST - end of APC + size_t apc_end = i + 2; + size_t apc_len = apc_end - apc_start; + int ret = kitty_write_single_passthrough(f, buf + apc_start, apc_len); + if(ret < 0){ + return -1; + } + total_written += ret; + i = apc_end; + break; + } + i++; + } + }else{ + // Not an APC sequence - skip this byte (shouldn't happen for valid kitty data) + i++; + } + } + return total_written; +} + // Kitty has its own bitmap graphics protocol, rather superior to DEC Sixel. // A header is written with various directives, followed by a number of // chunks. Each chunk carries up to 4096B of base64-encoded pixels. Bitmaps @@ -531,16 +605,49 @@ int kitty_wipe(sprixel* s, int ycell, int xcell){ return -1; } -int kitty_commit(fbuf* f, sprixel* s, unsigned noscroll){ +int kitty_commit(const tinfo* ti, fbuf* f, sprixel* s, unsigned noscroll){ loginfo("committing Kitty graphic id %u", s->id); - int i; + int ret; + + // In tmux, we need to send cursor positioning through passthrough + // because the outer terminal has a different cursor position than tmux + if(kitty_in_tmux(ti) && s->n && s->n->pile && s->n->pile->nc){ + int y, x; + ncplane_abs_yx(s->n, &y, &x); + // Add margins to get actual screen position (same as render.c does) + const struct notcurses* nc = s->n->pile->nc; + int screen_y = y + nc->margin_t; + int screen_x = x + nc->margin_l; + // Send cursor position through passthrough: CSI row;col H + // Note: CSI uses 1-based coordinates + char cup[32]; + int cuplen = snprintf(cup, sizeof(cup), "\e[%d;%dH", screen_y + 1, screen_x + 1); + if(cuplen > 0 && cuplen < (int)sizeof(cup)){ + ret = kitty_write_single_passthrough(f, cup, cuplen); + if(ret < 0){ + return -1; + } + } + } + + char cmd[128]; + int cmdlen; if(s->pxoffx || s->pxoffy){ - i = fbuf_printf(f, "\e_Ga=p,i=%u,p=1,X=%u,Y=%u%s,q=2\e\\", s->id, - s->pxoffx, s->pxoffy, noscroll ? ",C=1" : ""); + cmdlen = snprintf(cmd, sizeof(cmd), "\e_Ga=p,i=%u,p=1,X=%u,Y=%u%s,q=2\e\\", + s->id, s->pxoffx, s->pxoffy, noscroll ? ",C=1" : ""); + }else{ + cmdlen = snprintf(cmd, sizeof(cmd), "\e_Ga=p,i=%u,p=1,q=2%s\e\\", + s->id, noscroll ? ",C=1" : ""); + } + if(cmdlen < 0 || cmdlen >= (int)sizeof(cmd)){ + return -1; + } + if(kitty_in_tmux(ti)){ + ret = kitty_write_tmux_passthrough(f, cmd, cmdlen); }else{ - i = fbuf_printf(f, "\e_Ga=p,i=%u,p=1,q=2%s\e\\", s->id, noscroll ? ",C=1" : ""); + ret = fbuf_putn(f, cmd, cmdlen); } - if(i < 0){ + if(ret < 0){ return -1; } s->invalidated = SPRIXEL_QUIESCENT; @@ -783,7 +890,11 @@ write_kitty_data(fbuf* f, int linesize, int leny, int lenx, int cols, // alas. see https://github.com/dankamongmen/notcurses/issues/1910 =[. // parse_start isn't used in animation mode, so no worries about the // fact that this doesn't complete the header in that case. - *parse_start = fbuf_printf(f, "\e_Gf=32,s=%d,v=%d,i=%d,p=1,a=t,%s", + // NOTE: Removed p=1 from a=t command. With p=1, kitty creates a placement + // immediately at cursor position during transmit. Without p=1, image is + // only stored in memory and displayed later with a=p (kitty_commit). + // This is needed for tmux passthrough where cursor position is unreliable. + *parse_start = fbuf_printf(f, "\e_Gf=32,s=%d,v=%d,i=%d,a=t,%s", lenx, leny, s->id, animated ? "q=2" : chunks ? "m=1;" : "q=2;"); if(*parse_start < 0){ @@ -1127,12 +1238,20 @@ int kitty_blit_selfref(ncplane* n, int linesize, const void* data, NCPIXEL_KITTY_SELFREF); } -int kitty_remove(int id, fbuf* f){ +int kitty_remove(const tinfo* ti, int id, fbuf* f){ loginfo("removing graphic %u", id); - if(fbuf_printf(f, "\e_Ga=d,d=I,i=%d\e\\", id) < 0){ + char cmd[64]; + int cmdlen = snprintf(cmd, sizeof(cmd), "\e_Ga=d,d=I,i=%d\e\\", id); + if(cmdlen < 0 || cmdlen >= (int)sizeof(cmd)){ return -1; } - return 0; + int ret; + if(kitty_in_tmux(ti)){ + ret = kitty_write_tmux_passthrough(f, cmd, cmdlen); + }else{ + ret = fbuf_putn(f, cmd, cmdlen); + } + return ret < 0 ? -1 : 0; } // damages cells underneath the graphic which were OPAQUE @@ -1169,8 +1288,9 @@ int kitty_scrub(const ncpile* p, sprixel* s){ // returns the number of bytes written int kitty_draw(const tinfo* ti, const ncpile* p, sprixel* s, fbuf* f, int yoff, int xoff){ - (void)ti; (void)p; + (void)yoff; + (void)xoff; bool animated = false; if(s->animating){ // active animation s->animating = false; @@ -1179,8 +1299,43 @@ int kitty_draw(const tinfo* ti, const ncpile* p, sprixel* s, fbuf* f, int ret = s->glyph.used; logdebug("dumping %" PRIu64 "b for %u at %d %d", s->glyph.used, s->id, yoff, xoff); if(ret){ - if(fbuf_putn(f, s->glyph.buf, s->glyph.used) < 0){ - ret = -1; + // Check if running inside tmux - use DCS passthrough to send kitty graphics to outer terminal + if(ti && ti->qterm == TERMINAL_TMUX){ + int screen_y = 0, screen_x = 0; + // Get screen position for cursor + if(s->n && s->n->pile && s->n->pile->nc){ + int y, x; + ncplane_abs_yx(s->n, &y, &x); + const struct notcurses* nc = s->n->pile->nc; + screen_y = y + nc->margin_t; + screen_x = x + nc->margin_l; + } + // Send kitty graphics data through passthrough (without p=1, so no display yet) + ret = kitty_write_tmux_passthrough(f, s->glyph.buf, s->glyph.used); + if(ret < 0){ + return -1; + } + // Now send a=p (placement) with cursor position through passthrough. + // Image is in memory, this displays it at specified position. + char place_cmd[128]; + int place_len = snprintf(place_cmd, sizeof(place_cmd), + "\e[%d;%dH\e_Ga=p,i=%u,p=1,q=2\e\\", + screen_y + 1, screen_x + 1, s->id); + if(place_len > 0 && place_len < (int)sizeof(place_cmd)){ + if(kitty_write_single_passthrough(f, place_cmd, place_len) < 0){ + return -1; + } + } + s->invalidated = SPRIXEL_QUIESCENT; // Mark as done, skip kitty_commit + if(animated){ + fbuf_free(&s->glyph); + } + return ret; // Early return for tmux - don't overwrite invalidated state + }else{ + // Normal kitty output (not in tmux) + if(fbuf_putn(f, s->glyph.buf, s->glyph.used) < 0){ + ret = -1; + } } } if(animated){ @@ -1191,26 +1346,43 @@ int kitty_draw(const tinfo* ti, const ncpile* p, sprixel* s, fbuf* f, } // returns -1 on failure, 0 on success (move bytes do not count for sprixel stats) -int kitty_move(sprixel* s, fbuf* f, unsigned noscroll, int yoff, int xoff){ +int kitty_move(const tinfo* ti, sprixel* s, fbuf* f, unsigned noscroll, int yoff, int xoff){ const int targy = s->n->absy; const int targx = s->n->absx; logdebug("moving %u to %d %d", s->id, targy, targx); int ret = 0; if(goto_location(ncplane_notcurses(s->n), f, targy + yoff, targx + xoff, s->n)){ ret = -1; - }else if(fbuf_printf(f, "\e_Ga=p,i=%d,p=1,q=2%s\e\\", s->id, - noscroll ? ",C=1" : "") < 0){ - ret = -1; + }else{ + char cmd[64]; + int cmdlen = snprintf(cmd, sizeof(cmd), "\e_Ga=p,i=%d,p=1,q=2%s\e\\", + s->id, noscroll ? ",C=1" : ""); + if(cmdlen < 0 || cmdlen >= (int)sizeof(cmd)){ + ret = -1; + }else if(kitty_in_tmux(ti)){ + if(kitty_write_tmux_passthrough(f, cmd, cmdlen) < 0){ + ret = -1; + } + }else{ + if(fbuf_putn(f, cmd, cmdlen) < 0){ + ret = -1; + } + } } s->invalidated = SPRIXEL_QUIESCENT; return ret; } // clears all kitty bitmaps -int kitty_clear_all(fbuf* f){ +int kitty_clear_all(const tinfo* ti, fbuf* f){ //fprintf(stderr, "KITTY UNIVERSAL ERASE\n"); - if(fbuf_putn(f, "\x1b_Ga=d,q=2\x1b\\", 12) < 0){ - return -1; + const char cmd[] = "\x1b_Ga=d,q=2\x1b\\"; + const size_t cmdlen = sizeof(cmd) - 1; // exclude null terminator + int ret; + if(kitty_in_tmux(ti)){ + ret = kitty_write_tmux_passthrough(f, cmd, cmdlen); + }else{ + ret = fbuf_putn(f, cmd, cmdlen); } - return 0; + return ret < 0 ? -1 : 0; } diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c index 121b0d5a0..562a0e500 100644 --- a/src/lib/notcurses.c +++ b/src/lib/notcurses.c @@ -379,6 +379,15 @@ int update_term_dimensions(unsigned* rows, unsigned* cols, tinfo* tcache, tcache->cellpxx = cpixx; *pgeo_changed = 1; } + // Fallback for tmux: use reasonable default cell pixel values + // so that sixel passthrough can work. Common terminal fonts are ~9x18 to 10x20. + if((tcache->cellpxy == 0 || tcache->cellpxx == 0) && getenv("TMUX") != NULL){ + tcache->cellpxy = 20; + tcache->cellpxx = 10; + *pgeo_changed = 1; + loginfo("tmux detected with no cell pixel info, using defaults %ux%u", + tcache->cellpxx, tcache->cellpxy); + } if(tcache->cellpxy == 0 || tcache->cellpxx == 0){ tcache->pixel_draw = NULL; // disable support } diff --git a/src/lib/render.c b/src/lib/render.c index df023bc2c..b5d14ad9c 100644 --- a/src/lib/render.c +++ b/src/lib/render.c @@ -1056,7 +1056,7 @@ rasterize_sprixels(notcurses* nc, ncpile* p, fbuf* f){ } }else if(s->invalidated == SPRIXEL_HIDE){ if(nc->tcache.pixel_remove){ - if(nc->tcache.pixel_remove(s->id, f) < 0){ + if(nc->tcache.pixel_remove(&nc->tcache, s->id, f) < 0){ return -1; } if( (*parent = s->next) ){ diff --git a/src/lib/sixel.c b/src/lib/sixel.c index 37db90252..ff7ecbd3d 100644 --- a/src/lib/sixel.c +++ b/src/lib/sixel.c @@ -1,4 +1,5 @@ #include +#include // for getenv() #include #include "internal.h" #include "fbuf.h" @@ -1473,10 +1474,39 @@ int sixel_scrub(const ncpile* p, sprixel* s){ return 1; } +// Write sixel data with tmux DCS passthrough wrapping. +// In tmux passthrough, all ESC (0x1b) bytes must be doubled. +static int +sixel_write_tmux_passthrough(fbuf* f, const char* buf, size_t len){ + // tmux DCS passthrough prefix: ESC P tmux ; + if(fbuf_puts(f, "\x1bPtmux;") < 0){ + return -1; + } + // Write sixel data with doubled ESC bytes + size_t written = 0; + for(size_t i = 0; i < len; i++){ + if(buf[i] == '\x1b'){ + // Double the ESC byte for tmux passthrough + if(fbuf_putc(f, '\x1b') != 1){ + return -1; + } + written++; + } + if(fbuf_putc(f, buf[i]) != 1){ + return -1; + } + written++; + } + // tmux DCS passthrough suffix: ESC backslash (ST) + if(fbuf_puts(f, "\x1b\\") < 0){ + return -1; + } + return written; +} + // returns the number of bytes written int sixel_draw(const tinfo* ti, const ncpile* p, sprixel* s, fbuf* f, int yoff, int xoff){ - (void)ti; // if we've wiped or rebuilt any cells, effect those changes now, or else // we'll get flicker when we move to the new location. if(s->wipes_outstanding){ @@ -1508,6 +1538,17 @@ int sixel_draw(const tinfo* ti, const ncpile* p, sprixel* s, fbuf* f, } } } + // Check if running inside tmux - use DCS passthrough to send sixel to outer terminal + // Use TMUX env var as it's more reliable than terminal query detection + if(getenv("TMUX") != NULL){ + int ret = sixel_write_tmux_passthrough(f, s->glyph.buf, s->glyph.used); + if(ret < 0){ + return -1; + } + s->invalidated = SPRIXEL_QUIESCENT; + return ret; + } + // Normal sixel output (not in tmux) if(fbuf_putn(f, s->glyph.buf, s->glyph.used) < 0){ return -1; } diff --git a/src/lib/sprite.c b/src/lib/sprite.c index a4a3b6d4f..33c599a5e 100644 --- a/src/lib/sprite.c +++ b/src/lib/sprite.c @@ -205,7 +205,7 @@ int sprite_clear_all(const tinfo* t, fbuf* f){ if(t->pixel_clear_all == NULL){ return 0; } - return t->pixel_clear_all(f); + return t->pixel_clear_all(t, f); } // we don't want to seed the process-wide prng, but we do want to stir in a diff --git a/src/lib/sprite.h b/src/lib/sprite.h index 209eb0e78..26d53b1d4 100644 --- a/src/lib/sprite.h +++ b/src/lib/sprite.h @@ -190,18 +190,18 @@ int sixel_draw(const struct tinfo* ti, const struct ncpile *p, sprixel* s, fbuf* f, int yoff, int xoff); int kitty_draw(const struct tinfo* ti, const struct ncpile *p, sprixel* s, fbuf* f, int yoff, int xoff); -int kitty_move(sprixel* s, fbuf* f, unsigned noscroll, int yoff, int xoff); +int kitty_move(const struct tinfo* ti, sprixel* s, fbuf* f, unsigned noscroll, int yoff, int xoff); int sixel_scrub(const struct ncpile* p, sprixel* s); int kitty_scrub(const struct ncpile* p, sprixel* s); int fbcon_scrub(const struct ncpile* p, sprixel* s); -int kitty_remove(int id, fbuf* f); -int kitty_clear_all(fbuf* f); +int kitty_remove(const struct tinfo* ti, int id, fbuf* f); +int kitty_clear_all(const struct tinfo* ti, fbuf* f); int sixel_init_forcesdm(struct tinfo* ti, int fd); int sixel_init_inverted(struct tinfo* ti, int fd); int sixel_init(struct tinfo* ti, int fd); uint8_t* sixel_trans_auxvec(const struct ncpile* p); uint8_t* kitty_trans_auxvec(const struct ncpile* p); -int kitty_commit(fbuf* f, sprixel* s, unsigned noscroll); +int kitty_commit(const struct tinfo* ti, fbuf* f, sprixel* s, unsigned noscroll); int sixel_blit(struct ncplane* nc, int linesize, const void* data, int leny, int lenx, const struct blitterargs* bargs); int kitty_blit(struct ncplane* nc, int linesize, const void* data, diff --git a/src/lib/termdesc.c b/src/lib/termdesc.c index b5a072d4c..f98fae639 100644 --- a/src/lib/termdesc.c +++ b/src/lib/termdesc.c @@ -450,12 +450,14 @@ init_terminfo_esc(tinfo* ti, const char* name, escape_e idx, 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 +// KITTYQUERY is sent separately in send_initial_directives() to allow +// skipping it in tmux (where we detect kitty graphics via env vars and +// the response would leak to the application with DRAIN_INPUT) // kitty keyboard push, used at start #define KKEYBOARD_PUSH "\x1b[>u" @@ -505,6 +507,16 @@ send_initial_directives(queried_terminals_e qterm, int fd){ return -1; } total += strlen(DIRECTIVES); + // Send KITTYQUERY only when NOT in tmux. In tmux, we detect kitty graphics + // support via environment variables (TERM_PROGRAM, GHOSTTY_RESOURCES_DIR, + // KITTY_WINDOW_ID), and the query response would leak to applications using + // DRAIN_INPUT since tmux doesn't properly consume it. + if(qterm != TERMINAL_TMUX){ + if(blocking_write(fd, KITTYQUERY, strlen(KITTYQUERY))){ + return -1; + } + total += strlen(KITTYQUERY); + } return total; } @@ -827,6 +839,25 @@ apply_mlterm_heuristics(tinfo* ti){ return "MLterm"; } +// tmux doesn't respond to sixel capability queries, but can pass through sixel +// to the outer terminal via DCS passthrough. Force sixel support when running +// inside tmux (TMUX env var is set) to enable passthrough rendering. +static const char* +apply_tmux_heuristics(tinfo* ti){ + // Check if we're actually running inside tmux + if(getenv("TMUX") != NULL){ + // Force sixel color registers to enable sixel setup + // The outer terminal (e.g., Ghostty, iTerm2) will actually render the sixel + if(ti->color_registers == 0){ + ti->color_registers = 256; + loginfo("tmux detected - forcing sixel color_registers to 256 for passthrough"); + } + ti->caps.rgb = true; + ti->caps.quadrants = true; + } + return "tmux"; +} + static const char* apply_wezterm_heuristics(tinfo* ti, size_t* tablelen, size_t* tableused){ ti->caps.rgb = true; @@ -927,9 +958,16 @@ apply_konsole_heuristics(tinfo* ti){ } static const char* -apply_ghostty_heuristics(tinfo* ti){ +apply_ghostty_heuristics(tinfo* ti, size_t* tablelen, size_t* tableused){ + // Ghostty supports kitty graphics protocol ti->caps.quadrants = true; ti->caps.sextants = true; + ti->caps.rgb = true; + if(add_smulx_escapes(ti, tablelen, tableused)){ + return NULL; + } + // Ghostty uses kitty graphics protocol (static mode for compatibility) + setup_kitty_bitmaps(ti, ti->ttyfd, NCPIXEL_KITTY_STATIC); return "ghostty"; } @@ -1004,7 +1042,7 @@ apply_term_heuristics(tinfo* ti, const char* tname, queried_terminals_e qterm, forcesdm, invertsixel); break; case TERMINAL_TMUX: - newname = "tmux"; // FIXME what, oh what to do with tmux? + newname = apply_tmux_heuristics(ti); break; case TERMINAL_GNUSCREEN: newname = apply_gnuscreen_heuristics(ti); @@ -1049,7 +1087,7 @@ apply_term_heuristics(tinfo* ti, const char* tname, queried_terminals_e qterm, newname = apply_konsole_heuristics(ti); break; case TERMINAL_GHOSTTY: - newname = apply_ghostty_heuristics(ti); + newname = apply_ghostty_heuristics(ti, tablelen, tableused); break; default: logwarn("no match for qterm %d tname %s", qterm, tname); @@ -1122,6 +1160,12 @@ build_supported_styles(tinfo* ti){ // i'm likewise unsure what we're supposed to do should you ssh anywhere =[. static queried_terminals_e macos_early_matches(void){ + // Detect tmux early via TMUX environment variable. This is important for + // skipping KITTYQUERY (whose response would leak with DRAIN_INPUT) since + // we detect kitty graphics support in tmux via env vars instead. + if(getenv("TMUX") != NULL){ + return TERMINAL_TMUX; + } const char* tp = getenv("TERM_PROGRAM"); if(tp == NULL){ return TERMINAL_UNKNOWN; @@ -1143,6 +1187,12 @@ macos_early_matches(void){ // replies, and don't bother sending any identification requests. static queried_terminals_e unix_early_matches(const char* term){ + // Detect tmux early via TMUX environment variable. This is important for + // skipping KITTYQUERY (whose response would leak with DRAIN_INPUT) since + // we detect kitty graphics support in tmux via env vars instead. + if(getenv("TMUX") != NULL){ + return TERMINAL_TMUX; + } if(term == NULL){ return TERMINAL_UNKNOWN; } @@ -1523,6 +1573,24 @@ int interrogate_terminfo(tinfo* ti, FILE* out, unsigned utf8, &forcesdm, &invertsixel, nonewfonts)){ goto err; } + // If running in tmux, check if outer terminal supports kitty graphics. + // Ghostty and Kitty use kitty protocol, not sixel. tmux doesn't report + // kitty graphics support, but we can detect the outer terminal via env vars. + if(ti->qterm == TERMINAL_TMUX){ + const char* term_program = getenv("TERM_PROGRAM"); + const char* ghostty_resources = getenv("GHOSTTY_RESOURCES_DIR"); + const char* kitty_window_id = getenv("KITTY_WINDOW_ID"); + if((term_program && (strcmp(term_program, "ghostty") == 0 || + strcmp(term_program, "Ghostty") == 0 || + strcmp(term_program, "kitty") == 0)) || + ghostty_resources != NULL || kitty_window_id != NULL){ + // Outer terminal is Ghostty or Kitty - use kitty graphics with passthrough + loginfo("tmux: outer terminal supports kitty graphics, using passthrough"); + kitty_graphics = 1; + // Don't use sixel for these terminals (they don't support it) + ti->color_registers = 0; + } + } build_supported_styles(ti); if(ti->pixel_draw == NULL && ti->pixel_draw_late == NULL){ // color_registers was only assigned if kitty_graphics were unavailable diff --git a/src/lib/termdesc.h b/src/lib/termdesc.h index 66f091ed3..d108cd56f 100644 --- a/src/lib/termdesc.h +++ b/src/lib/termdesc.h @@ -141,17 +141,17 @@ typedef struct tinfo { // redrawn in a sixel (when old was not transparent, and new is not opaque). // it leaves the sprixel in INVALIDATED so that it's drawn in phase 2. void (*pixel_refresh)(const struct ncpile* p, sprixel* s); - int (*pixel_remove)(int id, fbuf* f); // kitty only, issue actual delete command + int (*pixel_remove)(const struct tinfo*, int id, fbuf* f); // kitty only, issue actual delete command int (*pixel_init)(struct tinfo* ti, int fd); // called when support is detected int (*pixel_draw)(const struct tinfo*, const struct ncpile* p, sprixel* s, fbuf* f, int y, int x); int (*pixel_draw_late)(const struct tinfo*, sprixel* s, int yoff, int xoff); // execute move (erase old graphic, place at new location) if non-NULL - int (*pixel_move)(sprixel* s, fbuf* f, unsigned noscroll, int yoff, int xoff); + int (*pixel_move)(const struct tinfo*, sprixel* s, fbuf* f, unsigned noscroll, int yoff, int xoff); int (*pixel_scrub)(const struct ncpile* p, sprixel* s); - int (*pixel_clear_all)(fbuf* f); // called during context startup + int (*pixel_clear_all)(const struct tinfo*, fbuf* f); // called during context startup // make a loaded graphic visible. only used with kitty. - int (*pixel_commit)(fbuf* f, sprixel* s, unsigned noscroll); + int (*pixel_commit)(const struct tinfo*, fbuf* f, sprixel* s, unsigned noscroll); // scroll all graphics up. only used with fbcon. void (*pixel_scroll)(const struct ncpile* p, struct tinfo*, int rows); void (*pixel_cleanup)(struct tinfo*); // called at shutdown