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
|
/* M0: one pty, one fullscreen pane, input passthrough, resize.
* Plus a seed of the headless harness (M0.5) so this is testable without a tty.
*/
#define _GNU_SOURCE
#include "sl0ptty.h"
#include "app.h"
#include <ghostty/vt.h>
#include <errno.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#define MAX_PANES 64 /* a screen cannot usefully hold more */
#define FRAME_MS 8 /* ~120Hz cap; pty reads are coalesced into one paint */
#define ESC_MS 40 /* how long a bare ESC waits to become a sequence */
/* What we ask the outer terminal for. Kitty keyboard flag 1 (disambiguate)
* costs nothing on terminals that ignore it, and everything a pane receives is
* re-encoded for that pane anyway, so an inner app that has never heard of
* kitty still gets legacy bytes. */
#define TERM_ENTER \
"\x1b[?1049h\x1b[0m\x1b[2J" /* alt screen */ \
"\x1b[?1002h\x1b[?1006h" /* mouse: button+drag, SGR encoding */ \
"\x1b[?1004h" /* focus in/out */ \
"\x1b[?2004h" /* bracketed paste */ \
"\x1b[>1u" /* kitty keyboard, pushed */
#define TERM_LEAVE \
"\x1b[<u\x1b[?2004l\x1b[?1004l\x1b[?1006l\x1b[?1002l" \
"\x1b[?25h\x1b[0m\x1b[?1049l"
static volatile sig_atomic_t g_winch = 0, g_quit = 0;
static struct termios g_saved_tio;
static bool g_tio_saved = false;
static void on_signal(int sig) {
if (sig == SIGWINCH) g_winch = 1;
else g_quit = 1;
}
static void restore_tty(void) {
if (g_tio_saved) tcsetattr(STDIN_FILENO, TCSAFLUSH, &g_saved_tio);
ssize_t r = write(STDOUT_FILENO, TERM_LEAVE, strlen(TERM_LEAVE));
(void)r;
}
static int raw_mode(void) {
if (tcgetattr(STDIN_FILENO, &g_saved_tio) != 0) return -1;
g_tio_saved = true;
struct termios t = g_saved_tio;
cfmakeraw(&t);
t.c_cc[VMIN] = 1;
t.c_cc[VTIME] = 0;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &t) != 0) return -1;
atexit(restore_tty);
ssize_t r = write(STDOUT_FILENO, TERM_ENTER, strlen(TERM_ENTER));
(void)r;
return 0;
}
static void term_size(uint16_t *cols, uint16_t *rows) {
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col && ws.ws_row) {
*cols = ws.ws_col;
*rows = ws.ws_row;
} else {
*cols = 80;
*rows = 24;
}
}
static int64_t now_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/* headless lives in headless.c */
int run_headless(const char *const argv[], uint16_t cols, uint16_t rows,
int idle_ms, bool script);
/* ---- interactive -------------------------------------------------------- */
/* Every decoded event goes to the app, which owns the prefix, the layout and
* what a key means. The client is only a decoder and a screen. */
static void on_event(const input_event_t *ev, void *ud) {
app_event((app_t *)ud, ev);
}
static int run_interactive(const char *const argv[]) {
uint16_t cols, rows;
term_size(&cols, &rows);
if (raw_mode() != 0) {
fprintf(stderr, "sl0ptty: stdin is not a terminal (try --headless)\n");
return 1;
}
app_t *a = app_new(argv, cols, rows);
if (!a) {
fprintf(stderr, "sl0ptty: cannot spawn pane\n");
return 1;
}
app_resize(a, cols, rows);
screen_t s;
screen_init(&s, cols, rows);
input_parser_t *in = input_new();
bool pending_paint = true;
int64_t next_frame = 0;
int64_t esc_due = 0;
while (!g_quit && !app_should_quit(a)) {
int timeout = -1;
if (pending_paint) {
int64_t due = next_frame - now_ms();
timeout = due <= 0 ? 0 : (int)due;
}
if (input_pending(in)) {
int64_t due = esc_due - now_ms();
int t = due <= 0 ? 0 : (int)due;
if (timeout < 0 || t < timeout) timeout = t;
}
int fds[MAX_PANES];
size_t npanes = app_fds(a, fds, MAX_PANES);
struct pollfd pfds[MAX_PANES + 1];
pfds[0] = (struct pollfd){.fd = STDIN_FILENO, .events = POLLIN};
for (size_t i = 0; i < npanes; i++)
pfds[i + 1] = (struct pollfd){.fd = fds[i], .events = POLLIN};
int n = poll(pfds, (nfds_t)(npanes + 1), timeout);
if (g_winch) {
g_winch = 0;
term_size(&cols, &rows);
screen_resize(&s, cols, rows);
app_resize(a, cols, rows);
pending_paint = true;
next_frame = now_ms();
}
if (n < 0 && errno != EINTR) break;
if (n > 0 && (pfds[0].revents & POLLIN)) {
uint8_t buf[8192];
ssize_t r = read(STDIN_FILENO, buf, sizeof buf);
if (r > 0) {
input_feed(in, buf, (size_t)r, on_event, a);
esc_due = now_ms() + ESC_MS;
pending_paint = true; /* focus, splits and closes are visible changes */
if (!next_frame) next_frame = now_ms();
}
}
/* A lone ESC is only the Escape key once nothing follows it. */
if (input_pending(in) && now_ms() >= esc_due) input_timeout(in, on_event, a);
for (size_t i = 0; i < npanes; i++) {
if (n > 0 && (pfds[i + 1].revents & (POLLIN | POLLHUP))) {
app_pump_fd(a, pfds[i + 1].fd);
if (!pending_paint) {
pending_paint = true;
next_frame = now_ms() + FRAME_MS;
}
}
}
app_reap(a);
if (pending_paint && now_ms() >= next_frame) {
app_compose(a, &s);
screen_flush(&s, STDOUT_FILENO);
pending_paint = false;
next_frame = 0;
}
}
input_free(in);
screen_free(&s);
app_free(a);
restore_tty();
return 0;
}
int main(int argc, char **argv) {
bool headless = false, script = false;
uint16_t cols = 80, rows = 24;
int idle_ms = 300;
const char *cmd_argv[64];
int cmd_n = 0;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--headless") == 0) headless = true;
else if (strcmp(argv[i], "--script") == 0) headless = script = true;
else if (strcmp(argv[i], "--cols") == 0 && i + 1 < argc) cols = (uint16_t)atoi(argv[++i]);
else if (strcmp(argv[i], "--rows") == 0 && i + 1 < argc) rows = (uint16_t)atoi(argv[++i]);
else if (strcmp(argv[i], "--idle-ms") == 0 && i + 1 < argc) idle_ms = atoi(argv[++i]);
else if (strcmp(argv[i], "--") == 0) {
for (int j = i + 1; j < argc && cmd_n < 63; j++) cmd_argv[cmd_n++] = argv[j];
break;
} else {
fprintf(stderr, "sl0ptty: unknown argument: %s\n", argv[i]);
return 2;
}
}
if (cmd_n == 0) {
const char *shell = getenv("SHELL");
cmd_argv[cmd_n++] = shell && *shell ? shell : "/bin/sh";
}
cmd_argv[cmd_n] = NULL;
struct sigaction sa = {.sa_handler = on_signal};
sigaction(SIGWINCH, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGHUP, &sa, NULL);
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN); /* no zombies; EOF on the pty is our signal */
return headless ? run_headless(cmd_argv, cols, rows, idle_ms, script)
: run_interactive(cmd_argv);
}
|