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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
|
/* Entry point and argument parsing.
*
* slosh attach to session "main", creating it if needed
* slosh -s NAME ... a named session
* slosh ls live sessions
* slosh cmd LINE one control command against a session
* slosh --server NAME run a session in the foreground (internal)
* slosh --script the headless driver (no server, no tty)
* slosh --headless run once, print the screen
*/
#define _GNU_SOURCE
#include "slosh.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#include "app.h"
#include "proto.h"
#include "server.h"
#include "config.h"
#include "png.h"
#include "version.h"
#ifndef _WIN32
/* The write itself lives in pty.c (pty_terminfo_install), because panes do
* it on their own when the entry is missing everywhere. This flag is the
* by-hand spelling, with a report around it. */
static int install_terminfo(void) {
if (pty_terminfo_install() != 0) {
fputs("slosh: could not write ~/.terminfo (no $HOME, or not writable)\n",
stderr);
return 1;
}
printf("installed xterm-ghostty (and the ghostty alias) into ~/.terminfo\n"
"new sessions use it; a session already running keeps the TERM it\n"
"chose at startup until it is restarted\n");
return 0;
}
#endif
int run_headless(const char *const argv[], uint16_t cols, uint16_t rows,
int idle_ms, bool script, const char *layout);
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;
}
}
/* Whether a file is a layout rather than a config, decided the same way both
* loaders decide it: by what its top-level names are. A file that will not
* parse has no top-level names, and then the extension is all there is -- which
* is exactly the job D2 gave it, and better than telling somebody their layout
* is a config whose "defaults would stand". */
static bool looks_like_layout(const char *path) {
char buf[1024];
kdl_node_t *root =
kdl_parse_file(path_expand(path, buf, sizeof buf), NULL, 0);
if (!root) {
/* `x.layout` is the spelling, `x.layout.kdl` the one it replaced: both
* are somebody's layout, whatever it was that would not parse. */
size_t n = strlen(path), s = strlen(".layout");
if (n >= s && strcmp(path + n - s, ".layout") == 0) return true;
return strstr(path, ".layout.") != NULL;
}
bool layout = false;
for (size_t i = 0; i < root->nkids && !layout; i++) {
const char *name = root->kids[i] ? root->kids[i]->name : NULL;
if (!name) continue;
if (strcmp(name, "layout") == 0 || strcmp(name, "tab") == 0) layout = true;
}
kdl_free(root);
return layout;
}
/* `slosh --check FILE` on a layout. Telling somebody they handed a layout to
* the config checker was honest and unhelpful: the file they want checked is
* the one they were told to check. Same output shape, same exit status. */
static int check_layout(const char *path) {
layout_msg_t msgs[LAYOUT_MSGS_MAX];
size_t dropped = 0;
size_t n = layout_check_file(path, msgs, LAYOUT_MSGS_MAX, &dropped);
for (size_t i = 0; i < n; i++) fprintf(stderr, " %s\n", msgs[i]);
if (dropped) fprintf(stderr, " ...and %zu more\n", dropped);
if (n)
fprintf(stderr, "%s: %zu problem%s\n", path, n + dropped,
n + dropped == 1 ? "" : "s");
else
printf("%s: ok, a layout\n", path);
return n ? 1 : 0;
}
/* `slosh --check [FILE]`: read a config the way a session would and say what
* it could not honour, one problem per line, `file:line: what`. Exits 1 when
* there is anything to say, so it drops into an editor's compile step or a
* pre-commit hook without any glue.
*
* Worth being a mode rather than a script: the only checker that cannot drift
* from the loader is the loader, and the difference between this and a session
* is that a session shows the first complaint (it has one status line) while
* this shows all of them. */
static int check_config(const char *path) {
config_t cfg;
config_defaults(&cfg);
char err[256] = {0};
const char *file = path ? path : config_default_path();
bool parsed = config_load(&cfg, file, err, sizeof err);
const char *msgs[CONFIG_MSGS_MAX];
size_t n = config_messages(&cfg, msgs, CONFIG_MSGS_MAX);
for (size_t i = 0; i < n; i++) fprintf(stderr, " %s\n", msgs[i]);
if (!parsed) {
/* The file could not be read or parsed at all, so nothing in it applied --
* a different thing from a line that was skipped, and worth saying so. */
fprintf(stderr, "%s: not loaded (defaults would stand)\n", file);
config_free(&cfg);
return 1;
}
/* What it did read, so a clean run is evidence rather than silence: an empty
* answer and a file that was never opened look identical otherwise. */
const char *files[CONFIG_FILES_MAX];
size_t nf = config_files(&cfg, files, CONFIG_FILES_MAX);
if (n) {
fprintf(stderr, "%s: %zu problem%s\n", file, n, n == 1 ? "" : "s");
} else {
char prefix[24];
config_chord_name(cfg.prefix_key, cfg.prefix_mods, prefix, sizeof prefix);
printf("%s: ok\n", file);
for (size_t i = 0; i < nf; i++) printf(" read %s\n", files[i]);
printf(" prefix %s\n", prefix);
printf(" bindings %zu\n", cfg.nbinds);
}
config_free(&cfg);
return n ? 1 : 0;
}
static void usage(void) {
fputs("usage: slosh [-s NAME] [--layout FILE] [--no-reload]\n"
" [--version] [--dump-config] [--dump-theme]\n"
" [--check [FILE]]\n"
" [--install-terminfo] [ls | cmd LINE | -- CMD...]\n",
stderr);
}
int main(int argc, char **argv) {
/* Process-global and required before any terminal exists, so it happens
* here rather than in whichever mode we turn out to be: server, headless
* and the script driver all create panes, and a pane created without this
* silently drops every PNG a program sends it. */
png_init();
bool headless = false, script = false, server = false;
uint16_t cols = 80, rows = 24;
int idle_ms = 300;
const char *name = "main";
const char *cmd_line = NULL;
const char *layout = NULL;
bool watch = true;
bool list = false;
const char *cmd_argv[64];
int cmd_n = 0;
for (int i = 1; i < argc; i++) {
const char *a = argv[i];
if (strcmp(a, "--headless") == 0)
headless = true;
else if (strcmp(a, "--script") == 0)
headless = script = true;
else if (strcmp(a, "--server") == 0)
server = true;
else if (strcmp(a, "-s") == 0 && i + 1 < argc)
name = argv[++i];
else if (strcmp(a, "--layout") == 0 && i + 1 < argc)
layout = argv[++i];
else if (strcmp(a, "--no-reload") == 0)
watch = false;
else if (strcmp(a, "--check") == 0) {
/* An optional path, so it lints a file you have not installed yet:
* `slosh --check theme.kdl`. Without one it checks the config a session
* would actually read. A layout is checked as a layout: one flag, and the
* file decides which schema it is held to. */
const char *path =
(i + 1 < argc && argv[i + 1][0] != '-') ? argv[++i] : NULL;
return path && looks_like_layout(path) ? check_layout(path)
: check_config(path);
} else if (strcmp(a, "--dump-config") == 0) {
/* Every setting with its *default*, as a file you could have written:
* `slosh --dump-config > ~/.config/slosh/config.kdl` is a supported
* way to start one. Deliberately not the values a config in effect gives
* them -- this is the file to begin from, and `--check` is the one that
* reads yours and says what it understood. */
char *text = config_dump_defaults();
fputs(text, stdout);
free(text);
return 0;
} else if (strcmp(a, "--dump-theme") == 0) {
/* The other half of --dump-config, which writes a theme's *name* rather
* than its colours: this writes the colours, so a theme can be started
* from the one in front of you instead of from a blank file. */
char *text = config_dump_theme();
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. */
printf("slosh %s\n", SLOSH_VERSION);
return 0;
} else if (strcmp(a, "ls") == 0)
list = true;
else if (strcmp(a, "cmd") == 0 && i + 1 < argc)
cmd_line = argv[++i];
else if (strcmp(a, "--cols") == 0 && i + 1 < argc)
cols = (uint16_t)atoi(argv[++i]);
else if (strcmp(a, "--rows") == 0 && i + 1 < argc)
rows = (uint16_t)atoi(argv[++i]);
else if (strcmp(a, "--idle-ms") == 0 && i + 1 < argc)
idle_ms = atoi(argv[++i]);
else if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) {
usage();
return 0;
} else if (strcmp(a, "--") == 0) {
for (int j = i + 1; j < argc && cmd_n < 63; j++)
cmd_argv[cmd_n++] = argv[j];
break;
} else {
fprintf(stderr, "slosh: unknown argument: %s\n", a);
usage();
return 2;
}
}
/* No `--` command: leave it empty and let the session decide, so the
* config's `shell` is consulted when a pane is made rather than baked in
* here -- where it was not consulted at all, which made `shell` a setting
* that did nothing. */
cmd_argv[cmd_n] = NULL;
if (list) {
char **names = NULL;
size_t n = session_list(&names);
for (size_t i = 0; i < n; i++) {
int fd = server_connect(names[i]);
printf("%-20s %s\n", names[i], fd >= 0 ? "running" : "stale");
if (fd >= 0) close(fd);
free(names[i]);
}
free(names);
return 0;
}
if (cmd_line) {
int fd = server_connect(name);
if (fd < 0) {
fprintf(stderr, "slosh: no session named %s\n", name);
return 1;
}
return client_control(fd, cmd_line);
}
if (headless)
return run_headless(cmd_argv, cols, rows, idle_ms, script, layout);
if (isatty(STDOUT_FILENO)) term_size(&cols, &rows);
if (server) return server_run(name, cmd_argv, cols, rows, layout, watch);
/* Not to the session this process is already inside. `slosh` typed into a
* pane of "main" would attach that pane's terminal back to the session which
* is drawing it: a recursive view whose own output becomes its next input.
* SLOSH_SESSION names the pane's own session; asking for any *other* name
* is deliberate nesting and none of our business. The one legitimate shape
* this refuses — a fresh terminal launched from a pane, inheriting the
* variables, deliberately making that recursive attachment — gets its escape hatch
* named in the message rather than a flag: it is exactly what unsetting
* the variable means. */
const char *inside = getenv("SLOSH") ? getenv("SLOSH_SESSION") : NULL;
if (inside && *inside && strcmp(inside, name) == 0) {
fprintf(stderr,
"slosh: this pane is already inside session %s\n"
" (C-a d detaches; `slosh -s NAME` nests another session;\n"
" unset SLOSH_SESSION to attach recursively anyway)\n",
name);
return 1;
}
/* attach, creating the session if nobody is home */
int fd = server_connect(name);
if (fd < 0) fd = server_spawn(name, cmd_argv, cols, rows, layout, watch);
if (fd < 0) {
fprintf(stderr, "slosh: cannot start session %s\n", name);
return 1;
}
return client_run(fd);
}
|