aboutsummaryrefslogtreecommitdiffstats
path: root/src/pty.c
diff options
context:
space:
mode:
authorsl0p <sl0p@sl0p.foo>2026-08-13 22:00:03 +0200
committersl0p <sl0p@sl0p.foo>2026-08-13 22:00:03 +0200
commit2c1b855c3cf8cf8841bf25cd939e478a7f0bfac8 (patch)
tree7cd6274303ba1da1f8bd57371aec336dd687fb58 /src/pty.c
downloadslosh-2c1b855c3cf8cf8841bf25cd939e478a7f0bfac8.tar.gz
slosh-2c1b855c3cf8cf8841bf25cd939e478a7f0bfac8.tar.xz
slosh-2c1b855c3cf8cf8841bf25cd939e478a7f0bfac8.zip
sl0ptty: M0 — libghostty-vt vendored, one pane, compositor, diff renderer
Design contract in DESIGN.md. libghostty-vt vendored fresh from ghostty main@bedb6f2 (zig 0.16), built to a 14MB static lib. M0: pty spawn (no libutil), a lib-vt terminal per pane, a screen cell buffer, a diff emitter that repaints only what changed, raw mode, SIGWINCH, and a ~120Hz frame cap that coalesces pty reads. Also the seed of the headless harness: --headless dumps the composited screen without a tty, so composition is testable in milliseconds. tests/live_m0.py covers what headless cannot: raw mode, the diff emitter, resize propagation, and the quit path. 6/6 green.
Diffstat (limited to 'src/pty.c')
-rw-r--r--src/pty.c86
1 files changed, 86 insertions, 0 deletions
diff --git a/src/pty.c b/src/pty.c
new file mode 100644
index 0000000..5ce166e
--- /dev/null
+++ b/src/pty.c
@@ -0,0 +1,86 @@
+/* pty spawn. Hand-rolled rather than forkpty() so we need no libutil and
+ * behave identically under musl. */
+#define _GNU_SOURCE
+#include "sl0ptty.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/wait.h>
+#include <termios.h>
+#include <unistd.h>
+
+int pty_spawn(pty_t *p, const char *const argv[], uint16_t cols, uint16_t rows,
+ const char *cwd) {
+ int master = posix_openpt(O_RDWR | O_NOCTTY);
+ if (master < 0) return -1;
+ if (grantpt(master) < 0 || unlockpt(master) < 0) {
+ close(master);
+ return -1;
+ }
+
+ char slave_name[128];
+ if (ptsname_r(master, slave_name, sizeof slave_name) != 0) {
+ close(master);
+ return -1;
+ }
+
+ struct winsize ws = {.ws_col = cols, .ws_row = rows};
+ ioctl(master, TIOCSWINSZ, &ws);
+
+ pid_t pid = fork();
+ if (pid < 0) {
+ close(master);
+ return -1;
+ }
+
+ if (pid == 0) {
+ /* child: new session, pty becomes the controlling terminal */
+ setsid();
+ int slave = open(slave_name, O_RDWR);
+ if (slave < 0) _exit(127);
+ ioctl(slave, TIOCSCTTY, 0);
+ dup2(slave, STDIN_FILENO);
+ dup2(slave, STDOUT_FILENO);
+ dup2(slave, STDERR_FILENO);
+ if (slave > STDERR_FILENO) close(slave);
+ close(master);
+
+ if (cwd) { if (chdir(cwd) != 0) { /* keep going in the old cwd */ } }
+
+ /* We are opinionated about the outer terminal (D11), and we present the
+ * same contract inward. xterm-ghostty is what libghostty-vt implements. */
+ setenv("TERM", "xterm-ghostty", 1);
+ setenv("SL0PTTY", "1", 1);
+ unsetenv("ZELLIJ");
+
+ signal(SIGPIPE, SIG_DFL);
+ execvp(argv[0], (char *const *)argv);
+ _exit(127);
+ }
+
+ /* parent */
+ int flags = fcntl(master, F_GETFL, 0);
+ fcntl(master, F_SETFL, flags | O_NONBLOCK);
+ p->fd = master;
+ p->pid = pid;
+ return 0;
+}
+
+int pty_resize(pty_t *p, uint16_t cols, uint16_t rows) {
+ struct winsize ws = {.ws_col = cols, .ws_row = rows};
+ return ioctl(p->fd, TIOCSWINSZ, &ws);
+}
+
+void pty_close(pty_t *p) {
+ if (p->fd >= 0) close(p->fd);
+ p->fd = -1;
+ if (p->pid > 0) {
+ kill(p->pid, SIGHUP);
+ waitpid(p->pid, NULL, WNOHANG);
+ }
+ p->pid = -1;
+}