aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-02 16:22:03 +0200
committeruser <user@clank>2026-09-02 16:22:03 +0200
commitf9986df0da09cc692e168e33ef9ccfab5b9d2faf (patch)
treec0cc554269af4260a41eca2c3ea1a56efb4a730f
parentpty: fall back to xterm-256color where xterm-ghostty has no terminfo (diff)
downloadslosh-f9986df0da09cc692e168e33ef9ccfab5b9d2faf.tar.gz
slosh-f9986df0da09cc692e168e33ef9ccfab5b9d2faf.tar.xz
slosh-f9986df0da09cc692e168e33ef9ccfab5b9d2faf.zip
slosh --install-terminfo: the binary carries its own terminfo entry
The xterm-256color fallback works everywhere; the real entry is better, and a tarball user on a bare box had no easy way to get it. The entry is generated from the vendored tree (contrib/terminfo, so it describes the terminal core we actually ship), compiled with tic, embedded as bytes, and written to ~/.terminfo under every name it answers to, letter and Darwin-hashed layouts both. No root, no package, no conflict with a ghostty installed later. Proven end to end in a mount namespace: bare box falls back, installs, and the next pane says xterm-ghostty.
-rw-r--r--Makefile12
-rw-r--r--contrib/terminfo/README.md34
-rw-r--r--contrib/terminfo/gen.zig11
-rw-r--r--contrib/terminfo/xterm-ghostty.terminfobin0 -> 3884 bytes
-rw-r--r--contrib/terminfo/xterm-ghostty.ti271
-rw-r--r--docs/slosh.117
-rw-r--r--src/main.c67
7 files changed, 410 insertions, 2 deletions
diff --git a/Makefile b/Makefile
index 7ac9736..c4aa82a 100644
--- a/Makefile
+++ b/Makefile
@@ -83,7 +83,7 @@ $(BIN): $(OBJ) $(VT_LIB)
$(call say,[LD],$@)
$(Q)$(CC) $(OBJ) $(VT_LIB) -o $@ $(LDFLAGS)
-build/%.o: src/%.c build/version.h build/logo.h $(VT_LIB) | build
+build/%.o: src/%.c build/version.h build/logo.h build/terminfo.h $(VT_LIB) | build
$(call say,[CC],$<)
$(Q)$(CC) $(CFLAGS) -c $< -o $@
@@ -106,6 +106,16 @@ build/logo.h: logo.txt | build
sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g' -e 's/^/ "/' -e 's/$$/",/' logo.txt; \
printf '};\n'; } > $@
+# The compiled xterm-ghostty terminfo entry, as bytes the binary carries so
+# `slosh --install-terminfo` can hand it to a machine that lacks it. See
+# contrib/terminfo/README.md for where the entry comes from.
+build/terminfo.h: contrib/terminfo/xterm-ghostty.terminfo | build
+ $(call say,[GEN],$@)
+ $(Q){ printf '/* generated from contrib/terminfo/xterm-ghostty.terminfo */\n'; \
+ printf 'static const unsigned char TERMINFO_GHOSTTY[] = {\n'; \
+ od -An -v -tx1 $< | sed -e 's/ \([0-9a-f][0-9a-f]\)/0x\1,/g'; \
+ printf '};\n'; } > $@
+
build/version.h: FORCE | build
@printf '/* generated by the Makefile */\n#define SLOSH_VERSION "%s"\n' \
'$(VERSION)' > $@.tmp
diff --git a/contrib/terminfo/README.md b/contrib/terminfo/README.md
new file mode 100644
index 0000000..30d579d
--- /dev/null
+++ b/contrib/terminfo/README.md
@@ -0,0 +1,34 @@
+# The xterm-ghostty terminfo, for machines that have never met ghostty
+
+Panes advertise `TERM=xterm-ghostty` when the entry resolves, and fall back
+to `xterm-256color` when it does not (see `pty_term()` in `src/pty.c`). The
+fallback works everywhere; the real entry is better. `slosh
+--install-terminfo` writes it into `~/.terminfo`, which is the first place
+curses looks, needs no root, and conflicts with no package manager.
+
+The files here:
+
+| file | what |
+|---|---|
+| `gen.zig` | prints the vendored libghostty-vt's terminfo *source* |
+| `xterm-ghostty.ti` | that output, committed for review and provenance |
+| `xterm-ghostty.terminfo` | the same, compiled with `tic -x`; what the binary embeds |
+
+The Makefile turns the compiled entry into `build/terminfo.h`, so the binary
+carries its own copy: a static tarball on a bare box can fix that box.
+
+## Regenerating, on a re-vendor
+
+The entry is generated **from the vendored tree**, so it describes the
+terminal core we actually ship, not whatever ghostty is installed here:
+
+```sh
+cd contrib/terminfo
+zig build-exe -femit-bin=/tmp/tigen --dep terminfo -Mroot=gen.zig \
+ -Mterminfo=../../vendor/libghostty-vt/src/terminfo/main.zig
+/tmp/tigen > xterm-ghostty.ti
+tic -x -o /tmp/ti xterm-ghostty.ti && cp /tmp/ti/x/xterm-ghostty xterm-ghostty.terminfo
+```
+
+Commit both outputs. `tic` warning about "older tic versions" is about the
+description field and harmless.
diff --git a/contrib/terminfo/gen.zig b/contrib/terminfo/gen.zig
new file mode 100644
index 0000000..8ed817a
--- /dev/null
+++ b/contrib/terminfo/gen.zig
@@ -0,0 +1,11 @@
+//! Print the vendored libghostty-vt's terminfo source to stdout.
+//! Run at re-vendor time (see README.md here); the output is committed.
+const std = @import("std");
+const terminfo = @import("terminfo");
+
+pub fn main(init: std.process.Init) !void {
+ var buf: [64 * 1024]u8 = undefined;
+ var w = std.Io.File.stdout().writerStreaming(init.io, &buf);
+ try terminfo.ghostty.encode(&w.interface);
+ try w.interface.flush();
+}
diff --git a/contrib/terminfo/xterm-ghostty.terminfo b/contrib/terminfo/xterm-ghostty.terminfo
new file mode 100644
index 0000000..f41a6d0
--- /dev/null
+++ b/contrib/terminfo/xterm-ghostty.terminfo
Binary files differ
diff --git a/contrib/terminfo/xterm-ghostty.ti b/contrib/terminfo/xterm-ghostty.ti
new file mode 100644
index 0000000..92cbd69
--- /dev/null
+++ b/contrib/terminfo/xterm-ghostty.ti
@@ -0,0 +1,271 @@
+xterm-ghostty|ghostty|Ghostty,
+ am,
+ bce,
+ ccc,
+ hs,
+ km,
+ mc5i,
+ mir,
+ msgr,
+ npc,
+ xenl,
+ AX,
+ Tc,
+ Su,
+ XT,
+ fullkbd,
+ colors#256,
+ cols#80,
+ it#8,
+ lines#24,
+ pairs#32767,
+ acsc=++\,\,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
+ Smulx=\E[4:%p1%dm,
+ Smol=\E[53m,
+ Rmol=\E[55m,
+ Setulc=\E[58:2::%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m,
+ Ss=\E[%p1%d q,
+ Se=\E[0 q,
+ Ms=\E]52;%p1%s;%p2%s\007,
+ Sync=\E[?2026%?%p1%{1}%-%tl%eh%;,
+ BD=\E[?2004l,
+ BE=\E[?2004h,
+ PS=\E[200~,
+ PE=\E[201~,
+ XM=\E[?1006;1000%?%p1%{1}%=%th%el%;,
+ xm=\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;,
+ RV=\E[>c,
+ rv=\E\\[[0-9]+;[0-9]+;[0-9]+c,
+ XR=\E[>0q,
+ xr=\EP>\\|[ -~]+a\E\\,
+ Enmg=\E[?69h,
+ Dsmg=\E[?69l,
+ Clmg=\E[s,
+ Cmg=\E[%i%p1%d;%p2%ds,
+ clear=\E[H\E[2J,
+ E3=\E[3J,
+ fe=\E[?1004h,
+ fd=\E[?1004l,
+ kxIN=\E[I,
+ kxOUT=\E[O,
+ bel=^G,
+ blink=\E[5m,
+ bold=\E[1m,
+ cbt=\E[Z,
+ civis=\E[?25l,
+ cnorm=\E[?12l\E[?25h,
+ cr=\r,
+ csr=\E[%i%p1%d;%p2%dr,
+ cub=\E[%p1%dD,
+ cub1=^H,
+ cud=\E[%p1%dB,
+ cud1=^J,
+ cuf=\E[%p1%dC,
+ cuf1=\E[C,
+ cup=\E[%i%p1%d;%p2%dH,
+ cuu=\E[%p1%dA,
+ cuu1=\E[A,
+ cvvis=\E[?12;25h,
+ dch=\E[%p1%dP,
+ dch1=\E[P,
+ dim=\E[2m,
+ dl=\E[%p1%dM,
+ dl1=\E[M,
+ dsl=\E]2;\007,
+ ech=\E[%p1%dX,
+ ed=\E[J,
+ el=\E[K,
+ el1=\E[1K,
+ flash=\E[?5h$<100/>\E[?5l,
+ fsl=^G,
+ home=\E[H,
+ hpa=\E[%i%p1%dG,
+ ht=^I,
+ hts=\EH,
+ ich=\E[%p1%d@,
+ ich1=\E[@,
+ il=\E[%p1%dL,
+ il1=\E[L,
+ ind=\n,
+ indn=\E[%p1%dS,
+ initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\,
+ invis=\E[8m,
+ oc=\E]104\007,
+ op=\E[39;49m,
+ rc=\E8,
+ rep=%p1%c\E[%p2%{1}%-%db,
+ rev=\E[7m,
+ ri=\EM,
+ rin=\E[%p1%dT,
+ ritm=\E[23m,
+ rmacs=\E(B,
+ rmam=\E[?7l,
+ rmcup=\E[?1049l,
+ rmir=\E[4l,
+ rmkx=\E[?1l\E>,
+ rmso=\E[27m,
+ rmul=\E[24m,
+ rmxx=\E[29m,
+ setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m,
+ setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m,
+ setrgbb=\E[48:2:%p1%d:%p2%d:%p3%dm,
+ setrgbf=\E[38:2:%p1%d:%p2%d:%p3%dm,
+ sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p5%t;2%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m,
+ sgr0=\E(B\E[m,
+ sitm=\E[3m,
+ smacs=\E(0,
+ smam=\E[?7h,
+ smcup=\E[?1049h,
+ smir=\E[4h,
+ smkx=\E[?1h\E=,
+ smso=\E[7m,
+ smul=\E[4m,
+ smxx=\E[9m,
+ tbc=\E[3g,
+ tsl=\E]2;,
+ u6=\E[%i%d;%dR,
+ u7=\E[6n,
+ u8=\E[?%[;0123456789]c,
+ u9=\E[c,
+ vpa=\E[%i%p1%dd,
+ kDC=\E[3;2~,
+ kDC3=\E[3;3~,
+ kDC4=\E[3;4~,
+ kDC5=\E[3;5~,
+ kDC6=\E[3;6~,
+ kDC7=\E[3;7~,
+ kDN=\E[1;2B,
+ kDN3=\E[1;3B,
+ kDN4=\E[1;4B,
+ kDN5=\E[1;5B,
+ kDN6=\E[1;6B,
+ kDN7=\E[1;7B,
+ kEND=\E[1;2F,
+ kEND3=\E[1;3F,
+ kEND4=\E[1;4F,
+ kEND5=\E[1;5F,
+ kEND6=\E[1;6F,
+ kEND7=\E[1;7F,
+ kHOM=\E[1;2H,
+ kHOM3=\E[1;3H,
+ kHOM4=\E[1;4H,
+ kHOM5=\E[1;5H,
+ kHOM6=\E[1;6H,
+ kHOM7=\E[1;7H,
+ kIC=\E[2;2~,
+ kIC3=\E[2;3~,
+ kIC4=\E[2;4~,
+ kIC5=\E[2;5~,
+ kIC6=\E[2;6~,
+ kIC7=\E[2;7~,
+ kLFT=\E[1;2D,
+ kLFT3=\E[1;3D,
+ kLFT4=\E[1;4D,
+ kLFT5=\E[1;5D,
+ kLFT6=\E[1;6D,
+ kLFT7=\E[1;7D,
+ kNXT=\E[6;2~,
+ kNXT3=\E[6;3~,
+ kNXT4=\E[6;4~,
+ kNXT5=\E[6;5~,
+ kNXT6=\E[6;6~,
+ kNXT7=\E[6;7~,
+ kPRV=\E[5;2~,
+ kPRV3=\E[5;3~,
+ kPRV4=\E[5;4~,
+ kPRV5=\E[5;5~,
+ kPRV6=\E[5;6~,
+ kPRV7=\E[5;7~,
+ kRIT=\E[1;2C,
+ kRIT3=\E[1;3C,
+ kRIT4=\E[1;4C,
+ kRIT5=\E[1;5C,
+ kRIT6=\E[1;6C,
+ kRIT7=\E[1;7C,
+ kUP=\E[1;2A,
+ kUP3=\E[1;3A,
+ kUP4=\E[1;4A,
+ kUP5=\E[1;5A,
+ kUP6=\E[1;6A,
+ kUP7=\E[1;7A,
+ kbs=^?,
+ kcbt=\E[Z,
+ kcub1=\EOD,
+ kcud1=\EOB,
+ kcuf1=\EOC,
+ kcuu1=\EOA,
+ kdch1=\E[3~,
+ kend=\EOF,
+ kent=\EOM,
+ kf1=\EOP,
+ kf10=\E[21~,
+ kf11=\E[23~,
+ kf12=\E[24~,
+ kf13=\E[1;2P,
+ kf14=\E[1;2Q,
+ kf15=\E[1;2R,
+ kf16=\E[1;2S,
+ kf17=\E[15;2~,
+ kf18=\E[17;2~,
+ kf19=\E[18;2~,
+ kf2=\EOQ,
+ kf20=\E[19;2~,
+ kf21=\E[20;2~,
+ kf22=\E[21;2~,
+ kf23=\E[23;2~,
+ kf24=\E[24;2~,
+ kf25=\E[1;5P,
+ kf26=\E[1;5Q,
+ kf27=\E[1;5R,
+ kf28=\E[1;5S,
+ kf29=\E[15;5~,
+ kf3=\EOR,
+ kf30=\E[17;5~,
+ kf31=\E[18;5~,
+ kf32=\E[19;5~,
+ kf33=\E[20;5~,
+ kf34=\E[21;5~,
+ kf35=\E[23;5~,
+ kf36=\E[24;5~,
+ kf37=\E[1;6P,
+ kf38=\E[1;6Q,
+ kf39=\E[1;6R,
+ kf4=\EOS,
+ kf40=\E[1;6S,
+ kf41=\E[15;6~,
+ kf42=\E[17;6~,
+ kf43=\E[18;6~,
+ kf44=\E[19;6~,
+ kf45=\E[20;6~,
+ kf46=\E[21;6~,
+ kf47=\E[23;6~,
+ kf48=\E[24;6~,
+ kf49=\E[1;3P,
+ kf5=\E[15~,
+ kf50=\E[1;3Q,
+ kf51=\E[1;3R,
+ kf52=\E[1;3S,
+ kf53=\E[15;3~,
+ kf54=\E[17;3~,
+ kf55=\E[18;3~,
+ kf56=\E[19;3~,
+ kf57=\E[20;3~,
+ kf58=\E[21;3~,
+ kf59=\E[23;3~,
+ kf6=\E[17~,
+ kf60=\E[24;3~,
+ kf61=\E[1;4P,
+ kf62=\E[1;4Q,
+ kf63=\E[1;4R,
+ kf7=\E[18~,
+ kf8=\E[19~,
+ kf9=\E[20~,
+ khome=\EOH,
+ kich1=\E[2~,
+ kind=\E[1;2B,
+ kmous=\E[<,
+ knp=\E[6~,
+ kpp=\E[5~,
+ kri=\E[1;2A,
+ rs1=\E]\E\\\Ec,
+ sc=\E7,
diff --git a/docs/slosh.1 b/docs/slosh.1
index 9bb623e..cce0b64 100644
--- a/docs/slosh.1
+++ b/docs/slosh.1
@@ -147,6 +147,19 @@ Print the version and exit.
It is the same string the status line shows, so \(lqwhich build is
this\(rq has one answer whether you ask the binary or the session.
.TP
+.B \-\-install\-terminfo
+Write the
+.B xterm-ghostty
+terminfo entry the binary carries into
+.I ~/.terminfo
+(no root, no package), so panes on a machine without ghostty's entry
+get the full
+.B TERM
+instead of the
+.B xterm-256color
+fallback.
+See ENVIRONMENT.
+.TP
.BI \-\-server\ name
Run a session's server in the foreground.
Internal: attaching spawns this for you.
@@ -355,6 +368,10 @@ when the machine's terminfo database has the entry (it ships with ghostty),
.B xterm-256color
when it does not: an entry curses cannot find makes editors exit on
the spot and backspace guess.
+.B slosh \-\-install\-terminfo
+puts the real entry in
+.IR ~/.terminfo ;
+new sessions then use it.
.TP
.B SLOSH
.B 1
diff --git a/src/main.c b/src/main.c
index 798b0a0..3fdbfe6 100644
--- a/src/main.c
+++ b/src/main.c
@@ -25,6 +25,63 @@
#include "png.h"
#include "version.h"
+#ifndef _WIN32
+#include <errno.h>
+#include <sys/stat.h>
+
+#include "terminfo.h"
+
+/* Write the embedded xterm-ghostty terminfo entry into ~/.terminfo, which
+ * is the first place curses looks: no root, no package, no conflict with a
+ * ghostty the machine may install later (its own entry in the system
+ * database simply stops being needed). Written under every name the entry
+ * answers to, in the letter layout and Darwin's hashed one, because the
+ * lookup is by filename. */
+static int install_terminfo(void) {
+ const char *home = getenv("HOME");
+ if (!home || !*home) {
+ fputs("slosh: no $HOME to install into\n", stderr);
+ return 1;
+ }
+ /* dir, then name inside it: x/ and 78/ for xterm-ghostty, g/ and 67/ for
+ * the ghostty alias. */
+ static const char *const spots[][2] = {
+ {"x", "xterm-ghostty"},
+ {"78", "xterm-ghostty"},
+ {"g", "ghostty"},
+ {"67", "ghostty"},
+ };
+ char path[1024];
+ snprintf(path, sizeof path, "%s/.terminfo", home);
+ if (mkdir(path, 0755) != 0 && errno != EEXIST) {
+ fprintf(stderr, "slosh: mkdir %s: %s\n", path, strerror(errno));
+ return 1;
+ }
+ for (size_t i = 0; i < sizeof spots / sizeof *spots; i++) {
+ snprintf(path, sizeof path, "%s/.terminfo/%s", home, spots[i][0]);
+ if (mkdir(path, 0755) != 0 && errno != EEXIST) {
+ fprintf(stderr, "slosh: mkdir %s: %s\n", path, strerror(errno));
+ return 1;
+ }
+ snprintf(path, sizeof path, "%s/.terminfo/%s/%s", home, spots[i][0],
+ spots[i][1]);
+ FILE *f = fopen(path, "wb");
+ if (!f || fwrite(TERMINFO_GHOSTTY, 1, sizeof TERMINFO_GHOSTTY, f) !=
+ sizeof TERMINFO_GHOSTTY) {
+ fprintf(stderr, "slosh: write %s: %s\n", path, strerror(errno));
+ if (f) fclose(f);
+ return 1;
+ }
+ fclose(f);
+ }
+ printf("installed xterm-ghostty (and the ghostty alias) into %s/.terminfo\n"
+ "new sessions use it; a session already running keeps the TERM it\n"
+ "chose at startup until it is restarted\n",
+ home);
+ return 0;
+}
+#endif
+
int run_headless(const char *const argv[], uint16_t cols, uint16_t rows,
int idle_ms, bool script, const char *layout);
@@ -131,7 +188,7 @@ static int check_config(const char *path) {
static void usage(void) {
fputs("usage: slosh [-s NAME] [--layout FILE] [--no-reload]\n"
" [--version] [--dump-config] [--check [FILE]]\n"
- " [ls | cmd LINE | -- CMD...]\n",
+ " [--install-terminfo] [ls | cmd LINE | -- CMD...]\n",
stderr);
}
@@ -186,6 +243,14 @@ int main(int argc, char **argv) {
fputs(text, stdout);
free(text);
return 0;
+ } else if (strcmp(a, "--install-terminfo") == 0) {
+#ifdef _WIN32
+ fputs("slosh: terminfo is not a Windows concept; nothing to install\n",
+ stderr);
+ return 1;
+#else
+ return install_terminfo();
+#endif
} else if (strcmp(a, "--version") == 0) {
/* The same string the status line shows, so "which build is this" has
* one answer whether you ask the binary or the session. */