1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
|
/* pty spawn. Hand-rolled rather than forkpty() so we need no libutil and
* behave identically under musl.
*
* Windows has no fork and no pty; src/pty_win.c implements the same three
* functions on ConPTY instead. */
#ifndef _WIN32
#define _GNU_SOURCE
#include "slosh.h"
#include <errno.h>
#include <sys/stat.h>
#include "terminfo.h"
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <time.h>
#include <termios.h>
#include <unistd.h>
/* What a pane's TERM should be. xterm-ghostty is the truth -- the terminal
* core is ghostty's -- but the truth is useless to a program whose curses
* cannot find the entry: the entry ships with ghostty, and a machine that
* has never seen ghostty has ncurses' database and nothing else. There,
* nano exits on the spot ("Error opening terminal"), which reads as `C-a e`
* flashing a pane; and a shell that cannot look up kbs guesses at what
* backspace sends. So the entry is looked up the way curses would --
* $TERMINFO, ~/.terminfo, $TERMINFO_DIRS, then the system directories, in
* the letter layout (x/) and Darwin's hashed one (78/) -- and when it is
* not there, TERM says xterm-256color: in every ncurses database since
* forever, and close enough to what we speak that everything works. The
* lookup happens where it matters: panes run on the server's machine.
*
* Computed once, in the parent, before the first fork. */
/* 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. Written under every name the
* entry answers to, in the letter layout and Darwin's hashed one, because
* the lookup is by filename. Quiet: pty_term() below calls this on its own
* when the entry is nowhere to be found, and `--install-terminfo` is the
* same write with a friendly report around it. */
int pty_terminfo_install(void) {
const char *home = getenv("HOME");
if (!home || !*home) return -1;
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) 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) 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) {
if (f) fclose(f);
return -1;
}
fclose(f);
}
return 0;
}
static bool term_add(char *dirs, size_t cap, size_t *n, const char *d) {
if (!d || !*d) return false;
int w = snprintf(dirs + *n, cap - *n, "%s%s", *n ? ":" : "", d);
if (w > 0) *n = *n + (size_t)w < cap ? *n + (size_t)w : cap - 1;
return true;
}
static const char *pty_term(void) {
static const char *cached = NULL;
if (cached) return cached;
cached = "xterm-256color";
char dirs[2048] = "";
size_t n = 0;
term_add(dirs, sizeof dirs, &n, getenv("TERMINFO"));
const char *home = getenv("HOME");
char user_ti[1024];
if (home) {
snprintf(user_ti, sizeof user_ti, "%s/.terminfo", home);
term_add(dirs, sizeof dirs, &n, user_ti);
}
term_add(dirs, sizeof dirs, &n, getenv("TERMINFO_DIRS"));
term_add(dirs, sizeof dirs, &n,
"/etc/terminfo:/lib/terminfo:/usr/share/terminfo:"
"/usr/local/share/terminfo");
char *save = NULL;
for (char *d = strtok_r(dirs, ":", &save); d;
d = strtok_r(NULL, ":", &save)) {
char path[1200];
snprintf(path, sizeof path, "%s/x/xterm-ghostty", d);
if (access(path, R_OK) == 0) {
cached = "xterm-ghostty";
break;
}
snprintf(path, sizeof path, "%s/78/xterm-ghostty", d);
if (access(path, R_OK) == 0) {
cached = "xterm-ghostty";
break;
}
}
/* Nowhere at all: put our own copy in ~/.terminfo and use it. The write
* happens only when the entry is missing everywhere -- an entry that
* exists is somebody's, possibly newer than ours, and stays theirs. On a
* $HOME that cannot be written the fallback stands, and everything still
* works at xterm-256color. */
if (strcmp(cached, "xterm-256color") == 0 && pty_terminfo_install() == 0)
cached = "xterm-ghostty";
return cached;
}
int pty_spawn(pty_t *p, const char *const argv[], uint16_t cols, uint16_t rows,
const char *cwd, uint16_t cell_w, uint16_t cell_h,
uint32_t pane_id) {
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;
}
/* The slave is opened *here*, before the fork, and not by the child.
*
* Darwin attaches no line discipline to a pty master until its slave has
* been opened: until then every TIOCSWINSZ on the master fails with ENOTTY
* and the size is silently dropped. A pane is born 1x1 and told its real
* size by the layout an instant later -- so with the child opening the
* slave, that message raced the child's open() and usually lost, and
* nothing ever sent it again. The pane's program spent its life believing
* it had one row and one column, which is how `C-a e` produced an editor
* drawing into a corner of the pane on macOS and nowhere else.
*
* Opening it in the parent closes the window entirely: the fork copies the
* fd table, so the slave is open from the child's first instruction, and
* every resize the parent sends afterwards lands. It is also what BSD
* forkpty() does, for this reason.
*
* O_NOCTTY: we are not the process that wants this terminal. The child is,
* and it says so with TIOCSCTTY after setsid(). */
int slave = open(slave_name, O_RDWR | O_NOCTTY);
if (slave < 0) {
close(master);
return -1;
}
/* The pixel fields matter: a program drawing images asks the tty how big a
* cell is, and a winsize that says zero is a program that cannot size an
* image. They are the *terminal's* pixels, so they are cols/rows times the
* cell size the client reported.
*
* Set on the slave, which works on Linux and Darwin alike, and set before
* the fork so it is ordered ahead of any resize the parent sends after it.
* The child must not set it: by the time it ran, the size it was spawned
* with could already be stale, and writing it back would undo the resize we
* are here to deliver. */
struct winsize ws = {.ws_col = cols,
.ws_row = rows,
.ws_xpixel = (unsigned short)(cols * cell_w),
.ws_ypixel = (unsigned short)(rows * cell_h)};
ioctl(slave, TIOCSWINSZ, &ws);
pty_term(); /* warm the cache in the parent; the child only reads it */
pid_t pid = fork();
if (pid < 0) {
close(slave);
close(master);
return -1;
}
if (pid == 0) {
/* child: new session, pty becomes the controlling terminal */
setsid();
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", pty_term(), 1);
setenv("SLOSH", "1", 1);
/* Which pane this is, so a program can act on its own pane instead of on
* whichever one has focus. Set in the child, after the fork, because it is
* per-pane: setting it in the parent would leak one pane's id into the
* next spawn. Unset when there is no id, so a stale inherited value from
* an outer session cannot be read as this one's. */
if (pane_id) {
char idbuf[16];
snprintf(idbuf, sizeof idbuf, "%u", pane_id);
setenv("SLOSH_PANE", idbuf, 1);
} else {
unsetenv("SLOSH_PANE");
}
unsetenv("ZELLIJ");
signal(SIGPIPE, SIG_DFL);
execvp(argv[0], (char *const *)argv);
_exit(127);
}
/* parent: the child holds the slave now, and holding a second copy would
* keep the master readable long after the program in the pane had gone. */
close(slave);
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, uint16_t cell_w,
uint16_t cell_h) {
struct winsize ws = {.ws_col = cols,
.ws_row = rows,
.ws_xpixel = (unsigned short)(cols * cell_w),
.ws_ypixel = (unsigned short)(rows * cell_h)};
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);
/* Collect it, so a session that opens and closes panes all day does not
* accumulate zombies. Bounded and short: almost everything is gone by the
* first pass, and a program that outlives a SIGHUP is a bigger problem
* than the process-table entry it leaves behind. */
for (int i = 0; i < 5; i++) {
if (waitpid(p->pid, NULL, WNOHANG) != 0) break;
nanosleep(&(struct timespec){0, 200000}, NULL);
}
}
p->pid = -1;
}
#endif /* !_WIN32 */
|