aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorPeter Geissler <peter@haxx.in>2026-08-28 00:11:24 +0200
committerPeter Geissler <peter@haxx.in>2026-08-28 00:23:19 +0200
commitd1b5a64c3855b07074b3c958739df4e16f6084a5 (patch)
tree10579e84bd972661738f93fdd8c221674f09b857 /src
parentmacos zip: carry the same paperwork as the linux tarballs (diff)
downloadslosh-d1b5a64c3855b07074b3c958739df4e16f6084a5.tar.gz
slosh-d1b5a64c3855b07074b3c958739df4e16f6084a5.tar.xz
slosh-d1b5a64c3855b07074b3c958739df4e16f6084a5.zip
windows: a native port, on ConPTY and AF_UNIX
slosh builds and runs on Windows 10/11, x86-64 and ARM64, as one exe with no runtime dependency beyond the OS: panes, splits, tabs, detachable sessions, the config file and its live reload, the control socket, the mouse. The port is a compatibility layer rather than a fork. compat/win holds stub <poll.h>, <sys/socket.h>, <termios.h> and friends that redirect to include/compat_win.h, which is force-included ahead of every translation unit -- so the existing sources keep their POSIX includes and their POSIX call sites, and the platform differences are answered in one place. Two decisions carry most of it. Everything the event loop waits on is a socket. On POSIX a pty master, an inotify descriptor and a unix socket are all file descriptors, so one poll() covers them; Windows has no such union, and WSAPoll waits on SOCKETs and nothing else. Rather than rewrite server.c's loop around WaitForMultipleObjects -- which caps at 64 objects and cannot express POLLOUT -- each non-socket source gets a pump thread that copies it into one end of a loopback socketpair. The loop is then the POSIX one unchanged, and only what sits behind the descriptor differs. Socket descriptors are tagged rather than guessed: a wrapped SOCKET comes back as SL_SOCK_BASE + slot, so read, write, close and poll can tell a socket from a CRT descriptor with certainty instead of inferring it from the numeric range. That is why socket() and accept() are shimmed too -- an untagged descriptor escaping into the loop is the one failure mode this scheme has, and it is worth closing completely. Three things bit hard enough to be worth naming. A pseudoconsole does not stop the child inheriting your stdio. CreateProcess copies the parent's standard handles into the child when STARTF_USESTDHANDLES is absent, and those win. With slosh's own stdout redirected -- a pipe under ssh, a file under --script, a log under the daemon -- the shell wrote its banner into slosh's output instead of into the pane, and the pane had a correct title (ConPTY emits that itself) over an empty body. The three handles are cleared for the duration of the call, leaving the child nothing to inherit. A ConPTY never reports EOF. The output pipe belongs to the pseudoconsole, not to the child, so it stays open after the child exits and pane_pump()'s read() -- the entire mechanism by which a pane learns its program is gone -- never returns 0. A pane whose shell had been told `exit` sat there looking alive forever. So the exit is waited for on the process handle, and ClosePseudoConsole is what then produces the EOF. That third thread is also why the per-pty state is reference counted: a pump blocked on a dead pipe must not be waited for, or pty_close would hang the session on the one program that misbehaves, so whoever finishes last frees. cmd.exe does not use CommandLineToArgvW's quoting. A literal quote is \" to an ordinary program and a backslash-then-end-of-quoted-section to cmd, so building `cmd /c notepad "C:\...\config.kdl"` the first way handed the editor \"C:\...\config.kdl\", backslashes and all, and notepad answered "Not a valid file name" about a path that existed. cmd /c now gets the rest of the line verbatim -- the caller composed a complete command with its own quoting, exactly as it does for sh -c. Paths are mixed, and only some programs mind. slosh joins with '/' while the environment supplies '\', so the config landed at C:\Users\you/.config/slosh/config.kdl. The CRT opens that without complaint, which is why --check reported it fine, but a GUI file dialog rejects it outright. Anything handed to another program now goes through sl_path_native() first; the internal representation is left alone. The rest is small. Panes run %ComSpec%, sessions live in %LOCALAPPDATA%\slosh rather than $XDG_RUNTIME_DIR because a session socket must not follow a roaming profile to another host, there is no SIGWINCH so a resize is noticed by comparing the console size on a bounded poll, no fork so the daemon is a fresh detached process that reconstructs its arguments on the command line, and no process group on a pty so the foreground program is the youngest descendant of the pane's shell. C-a e prefers whichever console editor is actually installed -- notepad is a GUI program, so a pane running it draws nothing and looks broken -- and says so when it has to fall back. make -f Makefile.windows ARCH=x86_64 (or aarch64) cross-compiles from a mac or Linux, and builds natively on the box with zig on PATH.
Diffstat (limited to '')
-rw-r--r--src/app.c7
-rw-r--r--src/app_layout.c33
-rw-r--r--src/app_session.c4
-rw-r--r--src/client.c41
-rw-r--r--src/compat_win.c903
-rw-r--r--src/pane.c11
-rw-r--r--src/proto.c33
-rw-r--r--src/pty.c8
-rw-r--r--src/pty_win.c385
-rw-r--r--src/server.c73
10 files changed, 1485 insertions, 13 deletions
diff --git a/src/app.c b/src/app.c
index 0c8e43d..795eb97 100644
--- a/src/app.c
+++ b/src/app.c
@@ -762,7 +762,12 @@ const char *const *default_argv(app_t *a) {
static const char *argv[2];
if (a->argv && a->argv[0]) return a->argv;
const char *sh = CFG.shell && *CFG.shell ? CFG.shell : getenv("SHELL");
- argv[0] = sh && *sh ? sh : "/bin/sh";
+#ifdef _WIN32
+ /* Windows sets no SHELL; ComSpec is the equivalent and respects a user who
+ * has pointed it somewhere else. */
+ if (!sh || !*sh) sh = getenv("ComSpec");
+#endif
+ argv[0] = sh && *sh ? sh : SLOSH_SHELL_DEFAULT;
argv[1] = NULL;
return argv;
}
diff --git a/src/app_layout.c b/src/app_layout.c
index 8ba4a3f..59a00d7 100644
--- a/src/app_layout.c
+++ b/src/app_layout.c
@@ -1507,8 +1507,39 @@ bool app_edit_config(app_t *a) {
if (!editor || !*editor) editor = "vi";
char cmd[1024];
+#ifdef _WIN32
+ /* Windows has no editor it is *required* to have, and the obvious fallback
+ * is the wrong shape: notepad is a GUI program, so a pane running it draws
+ * nothing and looks broken. Prefer whichever console editor is actually
+ * installed, and only then fall back to opening a window.
+ *
+ * `editor` here is still whatever EDITOR or the config asked for; the search
+ * only runs when nothing was asked for, or when the POSIX default of `vi`
+ * came through and would not exist. */
+ bool windowed = false;
+ if (!editor || !*editor || strcmp(editor, "vi") == 0) {
+ static const char *const consoles[] = {"nvim", "vim", "vi", "nano",
+ "micro", "hx", NULL};
+ editor = NULL;
+ for (int i = 0; consoles[i] && !editor; i++)
+ if (sl_which(consoles[i])) editor = consoles[i];
+ if (!editor) {
+ editor = "notepad";
+ windowed = true;
+ }
+ }
+ /* cmd.exe does not treat single quotes as quoting, and the path is handed
+ * over in Windows spelling -- slosh joins with '/', $HOME arrives with '\\',
+ * and a GUI editor rejects the mixture outright. */
+ char native[PATH_MAX];
+ snprintf(cmd, sizeof cmd, "%s \"%s\"", editor,
+ sl_path_native(path, native, sizeof native));
+ if (windowed)
+ app_toast(a, "no console editor found: opened the config in notepad");
+#else
snprintf(cmd, sizeof cmd, "%s '%s'", editor, path);
- const char *argv[] = {"/bin/sh", "-c", cmd, NULL};
+#endif
+ const char *argv[] = {SLOSH_SHELL_DEFAULT, SLOSH_SHELL_CFLAG, cmd, NULL};
/* Down rather than across: a config file is lines, and half the width of a
* pane is a worse place to read them than half the height. */
diff --git a/src/app_session.c b/src/app_session.c
index 57371aa..1e78d9e 100644
--- a/src/app_session.c
+++ b/src/app_session.c
@@ -99,8 +99,8 @@ static node_t *build_pane(app_t *a, const kdl_node_t *node, const char *cwd,
bool suspended = a->force_suspend || kdl_prop_bool(node, "suspended", false);
const char *argv[4];
if (command) {
- argv[0] = "/bin/sh";
- argv[1] = "-c";
+ argv[0] = SLOSH_SHELL_DEFAULT;
+ argv[1] = SLOSH_SHELL_CFLAG;
argv[2] = command;
argv[3] = NULL;
} else {
diff --git a/src/client.c b/src/client.c
index e826164..de7115c 100644
--- a/src/client.c
+++ b/src/client.c
@@ -27,18 +27,26 @@
"\x1b[?25h\x1b[0m\x1b[?1049l"
static volatile sig_atomic_t g_winch = 0, g_quit = 0;
+#ifndef _WIN32
static struct termios g_saved;
+#endif
static bool g_saved_ok = false;
+#ifndef _WIN32
static void on_sig(int s) {
if (s == SIGWINCH)
g_winch = 1;
else
g_quit = 1;
}
+#endif
static void restore(void) {
+#ifdef _WIN32
+ if (g_saved_ok) sl_console_restore();
+#else
if (g_saved_ok) tcsetattr(STDIN_FILENO, TCSAFLUSH, &g_saved);
+#endif
ssize_t r = write(STDOUT_FILENO, TERM_LEAVE, strlen(TERM_LEAVE));
(void)r;
}
@@ -88,6 +96,27 @@ int client_run(int fd) {
uint16_t cols, rows, cell_w, cell_h;
term_size(&cols, &rows, &cell_w, &cell_h);
+#ifdef _WIN32
+ /* Raw mode is a console mode rather than a line discipline, and stdin is a
+ * console handle that WSAPoll cannot wait on -- so a pump thread turns it
+ * into a socket and the poll set below stays two entries wide, as on POSIX.
+ * There is no SIGWINCH either: a resize is noticed by comparing the size,
+ * which the loop already re-reads whenever it is about to send one. */
+ if (!sl_console_raw()) {
+ fprintf(stderr, "slosh: not a terminal\n");
+ return 1;
+ }
+ g_saved_ok = true;
+ int stdin_fd = sl_console_stdin_socket();
+ if (stdin_fd < 0) {
+ sl_console_restore();
+ fprintf(stderr, "slosh: cannot read the console\n");
+ return 1;
+ }
+ atexit(restore);
+ write_all(STDOUT_FILENO, TERM_ENTER, strlen(TERM_ENTER));
+#else
+ int stdin_fd = STDIN_FILENO;
if (tcgetattr(STDIN_FILENO, &g_saved) != 0) {
fprintf(stderr, "slosh: not a terminal\n");
return 1;
@@ -106,6 +135,7 @@ int client_run(int fd) {
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGHUP, &sa, NULL);
signal(SIGPIPE, SIG_IGN);
+#endif
send_size(fd, MSG_HELLO, cols, rows, cell_w, cell_h);
@@ -115,10 +145,17 @@ int client_run(int fd) {
while (!g_quit) {
struct pollfd pfds[2] = {
- {.fd = STDIN_FILENO, .events = POLLIN},
+ {.fd = stdin_fd, .events = POLLIN},
{.fd = fd, .events = POLLIN},
};
+#ifdef _WIN32
+ /* Bounded, so a console that changed size without producing a byte is
+ * still noticed promptly. */
+ int n = poll(pfds, 2, 200);
+ if (sl_console_resized()) g_winch = 1;
+#else
int n = poll(pfds, 2, -1);
+#endif
if (g_winch) {
g_winch = 0;
@@ -134,7 +171,7 @@ int client_run(int fd) {
if (pfds[0].revents & POLLIN) {
uint8_t buf[65536];
- ssize_t got = read(STDIN_FILENO, buf, sizeof buf);
+ ssize_t got = read(stdin_fd, buf, sizeof buf);
if (got > 0 && msg_send(fd, MSG_INPUT, buf, (size_t)got) != 0) break;
if (got == 0) break;
}
diff --git a/src/compat_win.c b/src/compat_win.c
new file mode 100644
index 0000000..efa3d66
--- /dev/null
+++ b/src/compat_win.c
@@ -0,0 +1,903 @@
+/* Windows implementations of the POSIX surface slosh uses.
+ * See include/compat_win.h for why this exists and how fds are tagged. */
+#ifdef _WIN32
+
+#include "compat_win.h"
+
+#include <shlwapi.h>
+#include <tlhelp32.h>
+
+/* ---- one-time winsock start --------------------------------------------- */
+
+static void wsa_once(void) {
+ static LONG done = 0;
+ if (InterlockedCompareExchange(&done, 1, 0) == 0) {
+ WSADATA w;
+ WSAStartup(MAKEWORD(2, 2), &w);
+ }
+}
+
+/* ---- socket table -------------------------------------------------------- */
+
+/* Tagged fds: a slot index, not a SOCKET value. Slots are reused, so a long
+ * session that opens and closes panes all day does not walk off the end. */
+#define SL_SOCK_MAX 512
+static SOCKET g_socks[SL_SOCK_MAX];
+static CRITICAL_SECTION g_sock_lock;
+static LONG g_sock_init = 0;
+
+static void sock_lock_once(void) {
+ if (InterlockedCompareExchange(&g_sock_init, 1, 0) == 0) {
+ InitializeCriticalSection(&g_sock_lock);
+ for (int i = 0; i < SL_SOCK_MAX; i++) g_socks[i] = INVALID_SOCKET;
+ InterlockedExchange(&g_sock_init, 2);
+ }
+ while (InterlockedCompareExchange(&g_sock_init, 2, 2) != 2) Sleep(0);
+}
+
+int sl_sock_wrap(SOCKET s) {
+ if (s == INVALID_SOCKET) return -1;
+ sock_lock_once();
+ EnterCriticalSection(&g_sock_lock);
+ int slot = -1;
+ for (int i = 0; i < SL_SOCK_MAX; i++) {
+ if (g_socks[i] == INVALID_SOCKET) {
+ g_socks[i] = s;
+ slot = i;
+ break;
+ }
+ }
+ LeaveCriticalSection(&g_sock_lock);
+ if (slot < 0) {
+ closesocket(s);
+ return -1;
+ }
+ return SL_SOCK_BASE + slot;
+}
+
+SOCKET sl_sock_get(int fd) {
+ if (!sl_is_sock(fd)) return INVALID_SOCKET;
+ int slot = fd - SL_SOCK_BASE;
+ if (slot < 0 || slot >= SL_SOCK_MAX) return INVALID_SOCKET;
+ sock_lock_once();
+ EnterCriticalSection(&g_sock_lock);
+ SOCKET s = g_socks[slot];
+ LeaveCriticalSection(&g_sock_lock);
+ return s;
+}
+
+static void sock_release(int fd) {
+ int slot = fd - SL_SOCK_BASE;
+ if (slot < 0 || slot >= SL_SOCK_MAX) return;
+ sock_lock_once();
+ EnterCriticalSection(&g_sock_lock);
+ g_socks[slot] = INVALID_SOCKET;
+ LeaveCriticalSection(&g_sock_lock);
+}
+
+/* ---- socketpair ---------------------------------------------------------- */
+
+/* Windows has no socketpair(). A listener bound to 127.0.0.1:0 that accepts
+ * exactly one connection is the standard stand-in, and is what the pump
+ * threads below hang their output on. */
+static int raw_socketpair(SOCKET out[2]) {
+ wsa_once();
+ out[0] = out[1] = INVALID_SOCKET;
+ SOCKET l = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+ if (l == INVALID_SOCKET) return -1;
+
+ struct sockaddr_in a = {0};
+ a.sin_family = AF_INET;
+ a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ a.sin_port = 0;
+ int alen = (int)sizeof a;
+ if (bind(l, (struct sockaddr *)&a, alen) != 0 || listen(l, 1) != 0 ||
+ getsockname(l, (struct sockaddr *)&a, &alen) != 0) {
+ closesocket(l);
+ return -1;
+ }
+
+ SOCKET c = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+ if (c == INVALID_SOCKET) {
+ closesocket(l);
+ return -1;
+ }
+ if (connect(c, (struct sockaddr *)&a, alen) != 0) {
+ closesocket(c);
+ closesocket(l);
+ return -1;
+ }
+ SOCKET s = accept(l, NULL, NULL);
+ closesocket(l);
+ if (s == INVALID_SOCKET) {
+ closesocket(c);
+ return -1;
+ }
+ /* Nagle would add 40ms to every keystroke round trip. */
+ BOOL yes = TRUE;
+ setsockopt(c, IPPROTO_TCP, TCP_NODELAY, (const char *)&yes, sizeof yes);
+ setsockopt(s, IPPROTO_TCP, TCP_NODELAY, (const char *)&yes, sizeof yes);
+ out[0] = c;
+ out[1] = s;
+ return 0;
+}
+
+int sl_socketpair(int fds[2]) {
+ SOCKET s[2];
+ if (raw_socketpair(s) != 0) return -1;
+ fds[0] = sl_sock_wrap(s[0]);
+ fds[1] = sl_sock_wrap(s[1]);
+ if (fds[0] < 0 || fds[1] < 0) {
+ if (fds[0] >= 0)
+ sl_close(fds[0]);
+ else
+ closesocket(s[0]);
+ if (fds[1] >= 0)
+ sl_close(fds[1]);
+ else
+ closesocket(s[1]);
+ return -1;
+ }
+ return 0;
+}
+
+int sl_socket(int domain, int type, int proto) {
+ wsa_once();
+ SOCKET s = socket(domain, type, proto);
+ if (s == INVALID_SOCKET) {
+ errno = EIO;
+ return -1;
+ }
+ return sl_sock_wrap(s);
+}
+
+int sl_bind(int fd, const struct sockaddr *a, int len) {
+ SOCKET s = sl_sock_get(fd);
+ if (s == INVALID_SOCKET) return -1;
+ if (bind(s, a, len) != 0) {
+ /* WSAEADDRINUSE is what a leftover socket file looks like here; report it
+ * as EEXIST so the caller's existing message stays true. */
+ int e = WSAGetLastError();
+ errno = (e == WSAEADDRINUSE) ? EEXIST : EIO;
+ return -1;
+ }
+ return 0;
+}
+
+int sl_listen(int fd, int backlog) {
+ SOCKET s = sl_sock_get(fd);
+ if (s == INVALID_SOCKET) return -1;
+ return listen(s, backlog) == 0 ? 0 : -1;
+}
+
+int sl_accept(int fd, struct sockaddr *a, int *len) {
+ SOCKET s = sl_sock_get(fd);
+ if (s == INVALID_SOCKET) return -1;
+ SOCKET c = accept(s, a, len);
+ if (c == INVALID_SOCKET) {
+ int e = WSAGetLastError();
+ errno = (e == WSAEWOULDBLOCK) ? EAGAIN : EIO;
+ return -1;
+ }
+ return sl_sock_wrap(c);
+}
+
+int sl_connect(int fd, const struct sockaddr *a, int len) {
+ SOCKET s = sl_sock_get(fd);
+ if (s == INVALID_SOCKET) return -1;
+ if (connect(s, a, len) != 0) {
+ errno = ECONNREFUSED;
+ return -1;
+ }
+ return 0;
+}
+
+long sl_send(int fd, const void *b, size_t n, int flags) {
+ SOCKET s = sl_sock_get(fd);
+ if (s == INVALID_SOCKET) {
+ errno = EBADF;
+ return -1;
+ }
+ int r = send(s, (const char *)b, (int)n, flags);
+ if (r < 0) {
+ int e = WSAGetLastError();
+ errno = (e == WSAEWOULDBLOCK) ? EAGAIN : EPIPE;
+ return -1;
+ }
+ return r;
+}
+
+long sl_recv(int fd, void *b, size_t n, int flags) {
+ SOCKET s = sl_sock_get(fd);
+ if (s == INVALID_SOCKET) {
+ errno = EBADF;
+ return -1;
+ }
+ int r = recv(s, (char *)b, (int)n, flags);
+ if (r < 0) {
+ int e = WSAGetLastError();
+ errno = (e == WSAEWOULDBLOCK) ? EAGAIN : EIO;
+ return -1;
+ }
+ return r;
+}
+
+int sl_unlink(const char *path) {
+ if (DeleteFileA(path)) return 0;
+ DWORD e = GetLastError();
+ if (e == ERROR_FILE_NOT_FOUND || e == ERROR_PATH_NOT_FOUND) {
+ errno = ENOENT;
+ return -1;
+ }
+ errno = EACCES;
+ return -1;
+}
+
+int sl_socketpair_pump(SOCKET *inner, int *outer_fd) {
+ SOCKET s[2];
+ if (raw_socketpair(s) != 0) return -1;
+ int fd = sl_sock_wrap(s[1]);
+ if (fd < 0) {
+ closesocket(s[0]);
+ closesocket(s[1]);
+ return -1;
+ }
+ *inner = s[0];
+ *outer_fd = fd;
+ return 0;
+}
+
+/* ---- read / write / close ------------------------------------------------ */
+
+long sl_read(int fd, void *buf, size_t n) {
+ if (sl_is_sock(fd)) {
+ SOCKET s = sl_sock_get(fd);
+ if (s == INVALID_SOCKET) {
+ errno = EBADF;
+ return -1;
+ }
+ int r = recv(s, (char *)buf, (int)n, 0);
+ if (r < 0) {
+ int e = WSAGetLastError();
+ errno = (e == WSAEWOULDBLOCK) ? EAGAIN : EIO;
+ return -1;
+ }
+ return r;
+ }
+ return _read(fd, buf, (unsigned)n);
+}
+
+long sl_write(int fd, const void *buf, size_t n) {
+ if (sl_is_sock(fd)) {
+ SOCKET s = sl_sock_get(fd);
+ if (s == INVALID_SOCKET) {
+ errno = EBADF;
+ return -1;
+ }
+ int r = send(s, (const char *)buf, (int)n, 0);
+ if (r < 0) {
+ int e = WSAGetLastError();
+ errno = (e == WSAEWOULDBLOCK) ? EAGAIN : EPIPE;
+ return -1;
+ }
+ return r;
+ }
+ return _write(fd, buf, (unsigned)n);
+}
+
+int sl_close(int fd) {
+ if (sl_is_sock(fd)) {
+ SOCKET s = sl_sock_get(fd);
+ sock_release(fd);
+ if (s != INVALID_SOCKET) closesocket(s);
+ return 0;
+ }
+ if (fd < 0) return 0;
+ return _close(fd);
+}
+
+/* ---- poll ---------------------------------------------------------------- */
+
+int sl_poll(struct sl_pollfd *fds, nfds_t n, int timeout_ms) {
+ wsa_once();
+ WSAPOLLFD wp[256];
+ if (n > 256) n = 256;
+
+ nfds_t m = 0;
+ int map[256];
+ for (nfds_t i = 0; i < n; i++) {
+ fds[i].revents = 0;
+ SOCKET s = sl_sock_get(fds[i].fd);
+ if (s == INVALID_SOCKET) continue; /* non-socket: never ready */
+ wp[m].fd = s;
+ wp[m].events = 0;
+ if (fds[i].events & POLLIN) wp[m].events |= POLLRDNORM;
+ if (fds[i].events & POLLOUT) wp[m].events |= POLLWRNORM;
+ map[m] = (int)i;
+ m++;
+ }
+ if (m == 0) {
+ if (timeout_ms > 0) Sleep((DWORD)timeout_ms);
+ return 0;
+ }
+
+ int r = WSAPoll(wp, (ULONG)m, timeout_ms);
+ if (r < 0) {
+ errno = EIO;
+ return -1;
+ }
+ int ready = 0;
+ for (nfds_t i = 0; i < m; i++) {
+ short re = 0;
+ if (wp[i].revents & (POLLRDNORM | POLLIN)) re |= POLLIN;
+ if (wp[i].revents & (POLLWRNORM | POLLOUT)) re |= POLLOUT;
+ if (wp[i].revents & POLLERR) re |= POLLERR;
+ if (wp[i].revents & POLLHUP) re |= POLLHUP;
+ if (wp[i].revents & POLLNVAL) re |= POLLNVAL;
+ if (re) {
+ fds[map[i]].revents = re;
+ ready++;
+ }
+ }
+ return ready;
+}
+
+/* ---- fcntl / ioctl ------------------------------------------------------- */
+
+int sl_fcntl(int fd, int cmd, ...) {
+ if (cmd == F_GETFL || cmd == F_GETFD) return 0;
+ if (cmd == F_SETFD) return 0; /* CLOEXEC: handles are not inherited anyway */
+ if (cmd == F_SETFL) {
+ va_list ap;
+ va_start(ap, cmd);
+ int flags = va_arg(ap, int);
+ va_end(ap);
+ if (sl_is_sock(fd)) {
+ SOCKET s = sl_sock_get(fd);
+ if (s == INVALID_SOCKET) return -1;
+ u_long nb = (flags & O_NONBLOCK) ? 1 : 0;
+ return ioctlsocket(s, FIONBIO, &nb) == 0 ? 0 : -1;
+ }
+ return 0;
+ }
+ return 0;
+}
+
+/* Window size lives on the console, not on the fd; TIOCSWINSZ is the ConPTY's
+ * job and is handled in pty_win.c, so here it is a no-op that reports success
+ * rather than an error the caller would log every resize. */
+int sl_ioctl(int fd, unsigned long req, ...) {
+ if (req == TIOCGWINSZ) {
+ va_list ap;
+ va_start(ap, req);
+ struct winsize *ws = va_arg(ap, struct winsize *);
+ va_end(ap);
+ if (!ws) return -1;
+ CONSOLE_SCREEN_BUFFER_INFO ci;
+ HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
+ if (h != INVALID_HANDLE_VALUE && GetConsoleScreenBufferInfo(h, &ci)) {
+ ws->ws_col = (unsigned short)(ci.srWindow.Right - ci.srWindow.Left + 1);
+ ws->ws_row = (unsigned short)(ci.srWindow.Bottom - ci.srWindow.Top + 1);
+ ws->ws_xpixel = 0;
+ ws->ws_ypixel = 0;
+ return 0;
+ }
+ return -1;
+ }
+ return 0;
+}
+
+/* ---- small libc gaps ----------------------------------------------------- */
+
+char *sl_strndup(const char *s, size_t n) {
+ if (!s) return NULL;
+ size_t len = 0;
+ while (len < n && s[len]) len++;
+ char *p = (char *)malloc(len + 1);
+ if (!p) return NULL;
+ memcpy(p, s, len);
+ p[len] = 0;
+ return p;
+}
+
+/* Normalises separators to '/' on the way out: the rest of slosh compares and
+ * splits paths on '/', and a mix of the two would make two spellings of one
+ * directory look like two directories. */
+char *sl_realpath(const char *path, char *resolved) {
+ if (!path) return NULL;
+ char *out = resolved ? resolved : (char *)malloc(PATH_MAX);
+ if (!out) return NULL;
+ DWORD n = GetFullPathNameA(path, PATH_MAX, out, NULL);
+ if (n == 0 || n >= PATH_MAX) {
+ if (!resolved) free(out);
+ return NULL;
+ }
+ if (GetFileAttributesA(out) == INVALID_FILE_ATTRIBUTES) {
+ if (!resolved) free(out);
+ return NULL;
+ }
+ for (char *p = out; *p; p++)
+ if (*p == '\\') *p = '/';
+ return out;
+}
+
+long sl_readlink(const char *path, char *buf, size_t cap) {
+ (void)path;
+ (void)buf;
+ (void)cap;
+ errno = EINVAL; /* no /proc to chase; callers fall back */
+ return -1;
+}
+
+int sl_setenv(const char *k, const char *v, int overwrite) {
+ if (!overwrite && getenv(k)) return 0;
+ return _putenv_s(k, v ? v : "") == 0 ? 0 : -1;
+}
+int sl_unsetenv(const char *k) { return _putenv_s(k, "") == 0 ? 0 : -1; }
+int sl_nanosleep_ms(long ms) {
+ Sleep((DWORD)ms);
+ return 0;
+}
+
+/* ---- processes ----------------------------------------------------------- */
+
+int sl_kill(pid_t pid, int sig) {
+ if (pid <= 0) return -1;
+ HANDLE h = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, (DWORD)pid);
+ if (!h) return -1;
+ int r = 0;
+ if (sig == 0) {
+ r = 0; /* existence probe */
+ } else if (!TerminateProcess(h, 1)) {
+ r = -1;
+ }
+ CloseHandle(h);
+ return r;
+}
+
+pid_t sl_waitpid(pid_t pid, int *status, int options) {
+ if (pid <= 0) return -1;
+ HANDLE h =
+ OpenProcess(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, FALSE, (DWORD)pid);
+ if (!h) {
+ if (status) *status = 0;
+ return pid; /* already gone and reaped */
+ }
+ DWORD w = WaitForSingleObject(h, (options & WNOHANG) ? 0 : INFINITE);
+ if (w == WAIT_TIMEOUT) {
+ CloseHandle(h);
+ return 0;
+ }
+ DWORD code = 0;
+ GetExitCodeProcess(h, &code);
+ CloseHandle(h);
+ if (status) *status = (int)((code & 0xff) << 8);
+ return pid;
+}
+
+bool sl_proc_cmdline(pid_t pid, char *out, size_t cap) {
+ /* The image name is what a pane title needs, and it is available without
+ * the remote-memory read a full command line would require. */
+ if (pid <= 0 || !out || cap == 0) return false;
+ HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+ if (snap == INVALID_HANDLE_VALUE) return false;
+ PROCESSENTRY32 pe = {.dwSize = sizeof pe};
+ bool found = false;
+ if (Process32First(snap, &pe)) {
+ do {
+ if (pe.th32ProcessID == (DWORD)pid) {
+ snprintf(out, cap, "%s", pe.szExeFile);
+ found = true;
+ break;
+ }
+ } while (Process32Next(snap, &pe));
+ }
+ CloseHandle(snap);
+ return found;
+}
+
+pid_t sl_proc_foreground(pid_t root) {
+ if (root <= 0) return -1;
+ HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+ if (snap == INVALID_HANDLE_VALUE) return -1;
+
+ /* Walk down: the shell's child, then that child's child, and so on. A depth
+ * cap keeps a cycle in a corrupt snapshot from spinning. */
+ DWORD cur = (DWORD)root;
+ for (int depth = 0; depth < 16; depth++) {
+ PROCESSENTRY32 pe = {.dwSize = sizeof pe};
+ DWORD child = 0;
+ if (Process32First(snap, &pe)) {
+ do {
+ if (pe.th32ParentProcessID == cur && pe.th32ProcessID != cur)
+ child = pe.th32ProcessID; /* last wins: the most recent */
+ } while (Process32Next(snap, &pe));
+ }
+ if (!child) break;
+ cur = child;
+ }
+ CloseHandle(snap);
+ return cur == (DWORD)root ? -1 : (pid_t)cur;
+}
+
+bool sl_self_exe(char *out, size_t cap) {
+ DWORD n = GetModuleFileNameA(NULL, out, (DWORD)cap);
+ if (n == 0 || n >= cap) return false;
+ for (char *p = out; *p; p++)
+ if (*p == '\\') *p = '/';
+ return true;
+}
+
+bool sl_which(const char *exe) {
+ if (!exe || !*exe) return false;
+ char found[PATH_MAX];
+ /* SearchPath appends only the one extension it is given, so the executable
+ * suffixes are tried in turn -- an editor installed as a .cmd shim (which is
+ * how several arrive on Windows) is on PATH just as much as a .exe. A name
+ * that already carries an extension is looked up as written. */
+ if (strchr(exe, '.')) {
+ DWORD n = SearchPathA(NULL, exe, NULL, (DWORD)sizeof found, found, NULL);
+ return n > 0 && n < sizeof found;
+ }
+ static const char *const exts[] = {".exe", ".cmd", ".bat", ".com", NULL};
+ for (int i = 0; exts[i]; i++) {
+ DWORD n = SearchPathA(NULL, exe, exts[i], (DWORD)sizeof found, found, NULL);
+ if (n > 0 && n < sizeof found) return true;
+ }
+ return false;
+}
+
+const char *sl_path_native(const char *in, char *out, size_t cap) {
+ if (!in || !out || cap == 0) return in;
+ /* GetFullPathName canonicalises and normalises the separators in one step;
+ * the manual pass is the fallback for a path it will not take. */
+ DWORD n = GetFullPathNameA(in, (DWORD)cap, out, NULL);
+ if (n == 0 || n >= cap) {
+ snprintf(out, cap, "%s", in);
+ for (char *p = out; *p; p++)
+ if (*p == '/') *p = '\\';
+ return out;
+ }
+ for (char *p = out; *p; p++)
+ if (*p == '/') *p = '\\';
+ return out;
+}
+
+int sl_spawn_detached(const char *const argv[], const char *logpath) {
+ /* fork() has no Windows equivalent, so the daemon is started as a fresh
+ * detached process rather than as a copy of this one. Everything the server
+ * needs is already on its command line, which is what makes that possible. */
+ char cmd[8192];
+ size_t off = 0;
+ for (int i = 0; argv[i]; i++) {
+ int need_q = strchr(argv[i], ' ') != NULL;
+ off +=
+ (size_t)snprintf(cmd + off, sizeof cmd - off, "%s%s%s%s", i ? " " : "",
+ need_q ? "\"" : "", argv[i], need_q ? "\"" : "");
+ if (off >= sizeof cmd) return -1;
+ }
+
+ SECURITY_ATTRIBUTES sa = {.nLength = sizeof sa, .bInheritHandle = TRUE};
+ HANDLE log = INVALID_HANDLE_VALUE;
+ if (logpath) {
+ log = CreateFileA(logpath, FILE_APPEND_DATA,
+ FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_ALWAYS,
+ FILE_ATTRIBUTE_NORMAL, NULL);
+ }
+
+ STARTUPINFOA si = {.cb = sizeof si};
+ if (log != INVALID_HANDLE_VALUE) {
+ si.dwFlags = STARTF_USESTDHANDLES;
+ si.hStdInput = NULL;
+ si.hStdOutput = log;
+ si.hStdError = log;
+ }
+ PROCESS_INFORMATION pi = {0};
+ BOOL ok = CreateProcessA(NULL, cmd, NULL, NULL, log != INVALID_HANDLE_VALUE,
+ CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS, NULL,
+ NULL, &si, &pi);
+ if (log != INVALID_HANDLE_VALUE) CloseHandle(log);
+ if (!ok) return -1;
+ CloseHandle(pi.hThread);
+ CloseHandle(pi.hProcess);
+ return (int)pi.dwProcessId;
+}
+
+/* ---- dynamic loading ----------------------------------------------------- */
+
+static char g_dlerr[256];
+
+void *sl_dlopen(const char *path, int flags) {
+ (void)flags;
+ HMODULE h = LoadLibraryA(path);
+ if (!h)
+ snprintf(g_dlerr, sizeof g_dlerr, "LoadLibrary failed (%lu)",
+ (unsigned long)GetLastError());
+ return (void *)h;
+}
+void *sl_dlsym(void *lib, const char *sym) {
+ FARPROC p = GetProcAddress((HMODULE)lib, sym);
+ if (!p) snprintf(g_dlerr, sizeof g_dlerr, "no symbol %s", sym);
+ return (void *)(uintptr_t)p;
+}
+int sl_dlclose(void *lib) { return FreeLibrary((HMODULE)lib) ? 0 : -1; }
+const char *sl_dlerror(void) { return g_dlerr[0] ? g_dlerr : NULL; }
+
+/* ---- glob ---------------------------------------------------------------- */
+
+/* Only what shader.c asks for: one directory, one wildcard in the last
+ * component. FindFirstFile does the matching. */
+int sl_glob(const char *pat, int flags, int (*errfn)(const char *, int),
+ glob_t *g) {
+ (void)flags;
+ (void)errfn;
+ if (!g) return GLOB_NOMATCH;
+ memset(g, 0, sizeof *g);
+
+ char dir[PATH_MAX];
+ snprintf(dir, sizeof dir, "%s", pat);
+ char *slash = strrchr(dir, '/');
+ char *bslash = strrchr(dir, '\\');
+ if (bslash > slash) slash = bslash;
+ if (slash)
+ *slash = 0;
+ else
+ dir[0] = 0;
+
+ WIN32_FIND_DATAA fd;
+ HANDLE h = FindFirstFileA(pat, &fd);
+ if (h == INVALID_HANDLE_VALUE) return GLOB_NOMATCH;
+
+ size_t cap = 16, n = 0;
+ char **v = (char **)malloc(cap * sizeof *v);
+ if (!v) {
+ FindClose(h);
+ return GLOB_NOMATCH;
+ }
+ do {
+ if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0)
+ continue;
+ if (n + 1 >= cap) {
+ cap *= 2;
+ char **nv = (char **)realloc(v, cap * sizeof *v);
+ if (!nv) break;
+ v = nv;
+ }
+ char full[PATH_MAX];
+ if (dir[0])
+ snprintf(full, sizeof full, "%s/%s", dir, fd.cFileName);
+ else
+ snprintf(full, sizeof full, "%s", fd.cFileName);
+ v[n] = _strdup(full);
+ if (v[n]) n++;
+ } while (FindNextFileA(h, &fd));
+ FindClose(h);
+
+ if (n == 0) {
+ free(v);
+ return GLOB_NOMATCH;
+ }
+ v[n] = NULL;
+ g->gl_pathc = n;
+ g->gl_pathv = v;
+ return 0;
+}
+
+void sl_globfree(glob_t *g) {
+ if (!g || !g->gl_pathv) return;
+ for (size_t i = 0; i < g->gl_pathc; i++) free(g->gl_pathv[i]);
+ free(g->gl_pathv);
+ g->gl_pathv = NULL;
+ g->gl_pathc = 0;
+}
+
+/* ---- console ------------------------------------------------------------- */
+
+static DWORD g_in_mode = 0, g_out_mode = 0;
+static bool g_console_saved = false;
+
+bool sl_console_raw(void) {
+ HANDLE in = GetStdHandle(STD_INPUT_HANDLE);
+ HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
+ if (in == INVALID_HANDLE_VALUE || out == INVALID_HANDLE_VALUE) return false;
+ if (!GetConsoleMode(in, &g_in_mode) || !GetConsoleMode(out, &g_out_mode))
+ return false;
+ g_console_saved = true;
+
+ /* VIRTUAL_TERMINAL_INPUT makes conhost hand us the same escape sequences a
+ * POSIX tty would, so input.c needs no Windows-specific decoding. */
+ DWORD nin = g_in_mode;
+ nin &=
+ (DWORD) ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
+ nin |= ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_WINDOW_INPUT;
+ SetConsoleMode(in, nin);
+
+ DWORD nout = g_out_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING |
+ DISABLE_NEWLINE_AUTO_RETURN;
+ SetConsoleMode(out, nout);
+ SetConsoleOutputCP(CP_UTF8);
+ return true;
+}
+
+void sl_console_restore(void) {
+ if (!g_console_saved) return;
+ SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), g_in_mode);
+ SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), g_out_mode);
+ g_console_saved = false;
+}
+
+static volatile LONG g_resized = 0;
+
+static unsigned __stdcall stdin_pump(void *arg) {
+ SOCKET s = (SOCKET)(uintptr_t)arg;
+ HANDLE in = GetStdHandle(STD_INPUT_HANDLE);
+ for (;;) {
+ /* ReadFile on the console handle returns the VT bytes that
+ * ENABLE_VIRTUAL_TERMINAL_INPUT produces. Window-resize records are not
+ * bytes, so they are noticed separately via the size poll in the client. */
+ char buf[4096];
+ DWORD got = 0;
+ if (!ReadFile(in, buf, sizeof buf, &got, NULL) || got == 0) break;
+ size_t off = 0;
+ while (off < got) {
+ int w = send(s, buf + off, (int)(got - off), 0);
+ if (w <= 0) goto done;
+ off += (size_t)w;
+ }
+ }
+done:
+ shutdown(s, SD_SEND);
+ return 0;
+}
+
+int sl_console_stdin_socket(void) {
+ SOCKET sp[2];
+ if (raw_socketpair(sp) != 0) return -1;
+ /* sp[0] is written by the pump, sp[1] is what the caller polls. */
+ HANDLE t = (HANDLE)_beginthreadex(NULL, 0, stdin_pump,
+ (void *)(uintptr_t)sp[0], 0, NULL);
+ if (!t) {
+ closesocket(sp[0]);
+ closesocket(sp[1]);
+ return -1;
+ }
+ CloseHandle(t);
+ return sl_sock_wrap(sp[1]);
+}
+
+int sl_console_resized(void) {
+ static SHORT last_c = 0, last_r = 0;
+ CONSOLE_SCREEN_BUFFER_INFO ci;
+ HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
+ if (h == INVALID_HANDLE_VALUE || !GetConsoleScreenBufferInfo(h, &ci))
+ return 0;
+ SHORT c = (SHORT)(ci.srWindow.Right - ci.srWindow.Left + 1);
+ SHORT r = (SHORT)(ci.srWindow.Bottom - ci.srWindow.Top + 1);
+ if (c != last_c || r != last_r) {
+ last_c = c;
+ last_r = r;
+ return 1;
+ }
+ (void)g_resized;
+ return 0;
+}
+
+/* ---- directory watching -------------------------------------------------- */
+
+typedef struct {
+ SOCKET notify; /* written when something changes */
+ HANDLE dirs[8];
+ char paths[8][PATH_MAX];
+ int ndirs;
+ volatile LONG stop;
+} watcher_t;
+
+static watcher_t *g_watchers[16];
+static int g_nwatchers = 0;
+
+static unsigned __stdcall watch_pump(void *arg) {
+ watcher_t *w = (watcher_t *)arg;
+ /* One overlapped ReadDirectoryChangesW per watched directory, all waited on
+ * together, each re-armed after it fires. The event is coalesced to a single
+ * byte because the reader only needs to know that it should look again --
+ * the debounce upstream turns a burst of these into one reload. */
+ OVERLAPPED ov[8] = {0};
+ HANDLE evs[8];
+ char bufs[8][4096];
+ int n = w->ndirs;
+ for (int i = 0; i < n; i++) {
+ evs[i] = CreateEventA(NULL, TRUE, FALSE, NULL);
+ ov[i].hEvent = evs[i];
+ ReadDirectoryChangesW(w->dirs[i], bufs[i], sizeof bufs[i], FALSE,
+ FILE_NOTIFY_CHANGE_FILE_NAME |
+ FILE_NOTIFY_CHANGE_LAST_WRITE |
+ FILE_NOTIFY_CHANGE_SIZE,
+ NULL, &ov[i], NULL);
+ }
+ while (!InterlockedCompareExchange(&w->stop, 0, 0)) {
+ DWORD r = WaitForMultipleObjects((DWORD)n, evs, FALSE, 200);
+ if (r == WAIT_TIMEOUT) continue;
+ if (r >= WAIT_OBJECT_0 + (DWORD)n) break;
+ int i = (int)(r - WAIT_OBJECT_0);
+ DWORD got = 0;
+ GetOverlappedResult(w->dirs[i], &ov[i], &got, FALSE);
+ ResetEvent(evs[i]);
+ char b = 1;
+ send(w->notify, &b, 1, 0);
+ ReadDirectoryChangesW(w->dirs[i], bufs[i], sizeof bufs[i], FALSE,
+ FILE_NOTIFY_CHANGE_FILE_NAME |
+ FILE_NOTIFY_CHANGE_LAST_WRITE |
+ FILE_NOTIFY_CHANGE_SIZE,
+ NULL, &ov[i], NULL);
+ }
+ for (int i = 0; i < n; i++) CloseHandle(evs[i]);
+ return 0;
+}
+
+int sl_watch_init(void) {
+ SOCKET sp[2];
+ if (raw_socketpair(sp) != 0) return -1;
+ watcher_t *w = (watcher_t *)calloc(1, sizeof *w);
+ if (!w) {
+ closesocket(sp[0]);
+ closesocket(sp[1]);
+ return -1;
+ }
+ w->notify = sp[0];
+ int fd = sl_sock_wrap(sp[1]);
+ if (fd < 0) {
+ closesocket(sp[0]);
+ free(w);
+ return -1;
+ }
+ u_long nb = 1;
+ ioctlsocket(sp[1], FIONBIO, &nb);
+ if (g_nwatchers < 16) g_watchers[g_nwatchers++] = w;
+ /* The thread starts on the first add, when there is something to watch. */
+ return fd;
+}
+
+static watcher_t *watcher_last(void) {
+ return g_nwatchers ? g_watchers[g_nwatchers - 1] : NULL;
+}
+
+int sl_watch_add(int fd, const char *dir) {
+ (void)fd;
+ watcher_t *w = watcher_last();
+ if (!w || w->ndirs >= 8) return -1;
+ HANDLE h = CreateFileA(
+ dir, FILE_LIST_DIRECTORY,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
+ OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL);
+ if (h == INVALID_HANDLE_VALUE) return -1;
+ w->dirs[w->ndirs] = h;
+ snprintf(w->paths[w->ndirs], PATH_MAX, "%s", dir);
+ w->ndirs++;
+ if (w->ndirs == 1) {
+ HANDLE t = (HANDLE)_beginthreadex(NULL, 0, watch_pump, w, 0, NULL);
+ if (t) CloseHandle(t);
+ }
+ return 0;
+}
+
+int sl_watch_drain(int fd) {
+ char buf[256];
+ int any = 0;
+ for (;;) {
+ long r = sl_read(fd, buf, sizeof buf);
+ if (r <= 0) break;
+ any = 1;
+ }
+ return any;
+}
+
+void sl_watch_close(int fd) {
+ for (int i = 0; i < g_nwatchers; i++) {
+ watcher_t *w = g_watchers[i];
+ if (!w) continue;
+ InterlockedExchange(&w->stop, 1);
+ }
+ if (fd >= 0) sl_close(fd);
+}
+
+#endif /* _WIN32 */
diff --git a/src/pane.c b/src/pane.c
index 1fbf6c5..70de8a2 100644
--- a/src/pane.c
+++ b/src/pane.c
@@ -457,12 +457,21 @@ static void sq_append(char *out, size_t cap, size_t *len, const char *arg) {
const char *pane_foreground(const pane_t *p, char *buf, size_t cap) {
if (!p || p->pty.fd < 0 || p->pty.pid <= 0) return NULL;
+#ifdef _WIN32
+ pid_t fg = sl_proc_foreground(p->pty.pid);
+#else
pid_t fg = tcgetpgrp(p->pty.fd);
+#endif
if (fg <= 0 || fg == p->pty.pid) return NULL; /* the pane's own shell */
char raw[4096];
size_t n = 0;
-#ifdef __APPLE__
+#ifdef _WIN32
+ /* The image name, which is all a Windows snapshot offers without reading
+ * another process's memory. Shaped like the others: NUL-terminated. */
+ if (!sl_proc_cmdline(fg, raw, sizeof raw)) return NULL;
+ n = strlen(raw) + 1;
+#elif defined(__APPLE__)
/* No /proc: KERN_PROCARGS2 is where Darwin keeps a process's argv. The block
* is argc as an int, then the executable path, then padding, then argc
* NUL-terminated arguments -- so we step over the path and the padding to
diff --git a/src/proto.c b/src/proto.c
index 33f6bf8..6b7a8c4 100644
--- a/src/proto.c
+++ b/src/proto.c
@@ -62,6 +62,26 @@ bool msg_reader_next(msg_reader_t *r, msg_t *out) {
int msg_send(int fd, uint8_t type, const void *data, size_t len) {
uint8_t hdr[HDR] = {type, (uint8_t)(len >> 24), (uint8_t)(len >> 16),
(uint8_t)(len >> 8), (uint8_t)len};
+#ifdef _WIN32
+ /* No sendmsg: the two buffers are simply sent in order. The gather was only
+ * ever an optimisation -- what matters is that the header and its payload
+ * reach the socket as one contiguous stream, which sequential sends on a
+ * stream socket also guarantee. */
+ const void *parts[2] = {hdr, data};
+ size_t lens[2] = {HDR, len};
+ for (int i = 0; i < 2; i++) {
+ size_t off = 0;
+ while (off < lens[i]) {
+ ssize_t w = sl_write(fd, (const char *)parts[i] + off, lens[i] - off);
+ if (w < 0) {
+ if (errno == EINTR || errno == EAGAIN) continue;
+ return -1;
+ }
+ off += (size_t)w;
+ }
+ }
+ return 0;
+#else
struct iovec iov[2] = {{hdr, HDR}, {(void *)data, len}};
size_t total = HDR + len, sent = 0;
while (sent < total) {
@@ -90,14 +110,27 @@ int msg_send(int fd, uint8_t type, const void *data, size_t len) {
sent += (size_t)w;
}
return 0;
+#endif
}
static int session_dir(char *out, size_t cap) {
+#ifdef _WIN32
+ /* Per-user and non-roaming, which is what LOCALAPPDATA is for: a session
+ * socket is machine-local by definition and must not follow a roaming
+ * profile to another host. */
+ const char *base = getenv("LOCALAPPDATA");
+ if (!base || !*base) base = getenv("TEMP");
+ if (!base || !*base) return -1;
+ snprintf(out, cap, "%s/slosh", base);
+ for (char *p = out; *p; p++)
+ if (*p == '\\') *p = '/';
+#else
const char *run = getenv("XDG_RUNTIME_DIR");
if (run && *run)
snprintf(out, cap, "%s/slosh", run);
else
snprintf(out, cap, "/tmp/slosh-%u", (unsigned)getuid());
+#endif
if (mkdir(out, 0700) != 0 && errno != EEXIST) return -1;
return 0;
}
diff --git a/src/pty.c b/src/pty.c
index dbabcef..eca0b26 100644
--- a/src/pty.c
+++ b/src/pty.c
@@ -1,5 +1,9 @@
/* pty spawn. Hand-rolled rather than forkpty() so we need no libutil and
- * behave identically under musl. */
+ * 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"
@@ -137,3 +141,5 @@ void pty_close(pty_t *p) {
}
p->pid = -1;
}
+
+#endif /* !_WIN32 */
diff --git a/src/pty_win.c b/src/pty_win.c
new file mode 100644
index 0000000..d4386ed
--- /dev/null
+++ b/src/pty_win.c
@@ -0,0 +1,385 @@
+/* pty_spawn/resize/close on top of ConPTY.
+ *
+ * A ConPTY is driven through two anonymous pipes, and a pipe HANDLE cannot be
+ * given to WSAPoll. So each pty gets a pair of pump threads that copy between
+ * its pipes and one end of a loopback socketpair, and `pty_t.fd` is the other
+ * end -- a socket, which server.c's poll set accepts alongside the session
+ * socket and its clients. The event loop upstream is therefore identical to
+ * the POSIX one; only what is behind the fd differs.
+ *
+ * `pty_t.pid` is a real Windows process id, so the existing kill/waitpid paths
+ * (shimmed in compat_win.c) work on it unchanged. */
+#ifdef _WIN32
+
+#include "compat_win.h"
+#include "slosh.h"
+
+/* Per-pty state the POSIX pty_t has nowhere to put: the console handle needed
+ * to resize, and the pipe ends the pumps own. Keyed by fd. */
+typedef struct {
+ int fd;
+ HPCON hpc;
+ HANDLE to_child; /* we write -> ConPTY input */
+ HANDLE from_child; /* ConPTY output -> we read */
+ HANDLE proc;
+ SOCKET inner; /* pump side of the socketpair */
+ /* Three threads and pty_close all reach this struct, and pty_close does not
+ * wait for the threads (a pump blocked on a dead pipe would hang the whole
+ * session). Each holds a reference and the last one out frees. */
+ volatile LONG refs;
+ volatile LONG console_closed;
+} conpty_t;
+
+/* ClosePseudoConsole is what makes the ConPTY release its pipe ends, and it
+ * must happen exactly once: both the exit watcher and pty_close ask for it,
+ * whichever notices first. */
+static void conpty_close_console(conpty_t *c) {
+ if (InterlockedExchange(&c->console_closed, 1) == 0)
+ ClosePseudoConsole(c->hpc);
+}
+
+static void conpty_release(conpty_t *c) {
+ if (InterlockedDecrement(&c->refs) != 0) return;
+ conpty_close_console(c);
+ if (c->proc) CloseHandle(c->proc);
+ if (c->to_child) CloseHandle(c->to_child);
+ if (c->from_child) CloseHandle(c->from_child);
+ if (c->inner != INVALID_SOCKET) closesocket(c->inner);
+ free(c);
+}
+
+#define MAX_CONPTY 128
+static conpty_t *g_ptys[MAX_CONPTY];
+
+static conpty_t *pty_find(int fd) {
+ for (int i = 0; i < MAX_CONPTY; i++)
+ if (g_ptys[i] && g_ptys[i]->fd == fd) return g_ptys[i];
+ return NULL;
+}
+
+static void pty_forget(conpty_t *c) {
+ for (int i = 0; i < MAX_CONPTY; i++)
+ if (g_ptys[i] == c) g_ptys[i] = NULL;
+}
+
+/* ConPTY output -> socket, so the pane's bytes arrive as socket readability. */
+static unsigned __stdcall pump_out(void *arg) {
+ conpty_t *c = (conpty_t *)arg;
+ char buf[8192];
+ for (;;) {
+ DWORD got = 0;
+ if (!ReadFile(c->from_child, buf, sizeof buf, &got, NULL) || got == 0)
+ break;
+ size_t off = 0;
+ while (off < got) {
+ int w = send(c->inner, buf + off, (int)(got - off), 0);
+ if (w <= 0) goto done;
+ off += (size_t)w;
+ }
+ }
+done:
+ /* Half-close so the reader sees EOF and the pane reports its program gone,
+ * which is the same signal a POSIX master gives when the slave closes. */
+ shutdown(c->inner, SD_SEND);
+ conpty_release(c);
+ return 0;
+}
+
+/* The pane's program has exited -- now make the pane notice.
+ *
+ * On POSIX the pty master reports EOF once the last slave fd is closed, which
+ * happens as the child dies, and pane_pump()'s read() returning 0 is the whole
+ * mechanism by which a pane learns its program is gone. A ConPTY gives no such
+ * signal: the output pipe belongs to the *pseudoconsole*, not to the child, so
+ * it stays open after the child exits and the read never ends. A pane whose
+ * shell had been told `exit` therefore sat there looking alive forever.
+ *
+ * So the exit is waited for explicitly, and closing the console is what
+ * produces the EOF: ConPTY flushes what the child left behind, then closes its
+ * end, and pump_out above sees the end of the stream exactly as it would on a
+ * pty. */
+static unsigned __stdcall pump_exit(void *arg) {
+ conpty_t *c = (conpty_t *)arg;
+ WaitForSingleObject(c->proc, INFINITE);
+ conpty_close_console(c);
+ conpty_release(c);
+ return 0;
+}
+
+/* socket -> ConPTY input. */
+static unsigned __stdcall pump_in(void *arg) {
+ conpty_t *c = (conpty_t *)arg;
+ char buf[8192];
+ for (;;) {
+ int got = recv(c->inner, buf, sizeof buf, 0);
+ if (got <= 0) break;
+ DWORD off = 0;
+ while (off < (DWORD)got) {
+ DWORD w = 0;
+ if (!WriteFile(c->to_child, buf + off, (DWORD)got - off, &w, NULL) ||
+ w == 0)
+ goto done;
+ off += w;
+ }
+ }
+done:
+ conpty_release(c);
+ return 0;
+}
+
+/* Is this argv the shell being handed a command line to interpret? */
+static bool is_shell_c(const char *const argv[]) {
+ if (!argv[0] || !argv[1] || !argv[2]) return false;
+ if (_stricmp(argv[1], "/c") != 0 && _stricmp(argv[1], "/k") != 0)
+ return false;
+ const char *base = argv[0];
+ for (const char *p = argv[0]; *p; p++)
+ if (*p == '/' || *p == '\\') base = p + 1;
+ return _stricmp(base, "cmd.exe") == 0 || _stricmp(base, "cmd") == 0;
+}
+
+/* argv -> a Windows command line.
+ *
+ * Windows passes a *string*, not a vector, so the quoting rules belong to
+ * whoever parses it at the other end -- and there are two different sets.
+ * Ordinary programs are parsed by CommandLineToArgvW, where a literal quote is
+ * written `\"`. cmd.exe is not: it has its own rules, and a `\"` means a
+ * backslash followed by the end of a quoted section. Applying the first set to
+ * the second is how `C-a e` produced a notepad complaining "Not a valid file
+ * name" about a path that existed -- the editor was handed
+ * `\"C:\...\config.kdl\"`, backslashes and all.
+ *
+ * So `cmd /c` gets the rest of the line verbatim: the caller composed a
+ * complete command with its own quoting, exactly as it composes one for
+ * `sh -c`, and cmd.exe is the thing that should parse it. Everything else is
+ * quoted for CommandLineToArgvW as before. */
+static void build_cmdline(const char *const argv[], char *out, size_t cap) {
+ size_t off = 0;
+
+ if (is_shell_c(argv)) {
+ off = (size_t)snprintf(out, cap, "%s %s", argv[0], argv[1]);
+ for (int i = 2; argv[i] && off < cap; i++)
+ off += (size_t)snprintf(out + off, cap - off, "%s%s", i > 2 ? " " : " ",
+ argv[i]);
+ if (off >= cap) out[cap - 1] = 0;
+ return;
+ }
+
+ for (int i = 0; argv[i] && off + 2 < cap; i++) {
+ if (i) out[off++] = ' ';
+ bool q = strchr(argv[i], ' ') != NULL || argv[i][0] == 0;
+ if (q && off < cap - 1) out[off++] = '"';
+ for (const char *p = argv[i]; *p && off < cap - 2; p++) {
+ if (*p == '"') out[off++] = '\\';
+ out[off++] = *p;
+ }
+ if (q && off < cap - 1) out[off++] = '"';
+ }
+ out[off < cap ? off : cap - 1] = 0;
+}
+
+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) {
+ (void)cell_w;
+ (void)cell_h; /* ConPTY carries no pixel geometry */
+ if (!p || !argv || !argv[0]) return -1;
+ if (cols == 0) cols = 80;
+ if (rows == 0) rows = 24;
+
+ conpty_t *c = (conpty_t *)calloc(1, sizeof *c);
+ if (!c) return -1;
+
+ HANDLE in_r = NULL, in_w = NULL, out_r = NULL, out_w = NULL;
+ if (!CreatePipe(&in_r, &in_w, NULL, 0) ||
+ !CreatePipe(&out_r, &out_w, NULL, 0)) {
+ free(c);
+ return -1;
+ }
+
+ COORD size = {(SHORT)cols, (SHORT)rows};
+ HPCON hpc = NULL;
+ HRESULT hr = CreatePseudoConsole(size, in_r, out_w, 0, &hpc);
+ /* The console owns its ends now. */
+ CloseHandle(in_r);
+ CloseHandle(out_w);
+ if (FAILED(hr)) {
+ CloseHandle(in_w);
+ CloseHandle(out_r);
+ free(c);
+ return -1;
+ }
+
+ /* PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE is what attaches the child to the
+ * console; without it the child would inherit ours and draw over the UI. */
+ STARTUPINFOEXA si = {0};
+ si.StartupInfo.cb = sizeof si;
+ SIZE_T attr_sz = 0;
+ InitializeProcThreadAttributeList(NULL, 1, 0, &attr_sz);
+ si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(attr_sz);
+ if (!si.lpAttributeList ||
+ !InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attr_sz) ||
+ !UpdateProcThreadAttribute(si.lpAttributeList, 0,
+ PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, hpc,
+ sizeof hpc, NULL, NULL)) {
+ free(si.lpAttributeList);
+ ClosePseudoConsole(hpc);
+ CloseHandle(in_w);
+ CloseHandle(out_r);
+ free(c);
+ return -1;
+ }
+
+ char cmd[8192];
+ build_cmdline(argv, cmd, sizeof cmd);
+
+ /* Same contract as the POSIX child: we say what terminal this is. */
+ SetEnvironmentVariableA("TERM", "xterm-ghostty");
+ SetEnvironmentVariableA("SLOSH", "1");
+
+ /* A child attached to a pseudoconsole is supposed to take its stdio from
+ * that console -- but CreateProcess copies the *parent's* standard handles
+ * into the child when STARTF_USESTDHANDLES is absent, and those win. With
+ * slosh's own stdout redirected (a pipe under ssh, a file under `--script`,
+ * a log under the daemon) the shell wrote its banner into slosh's output
+ * instead of into the pane: the pane's title arrived, because ConPTY emits
+ * that itself, while the body stayed empty.
+ *
+ * Clearing the three standard handles for the duration of the call leaves
+ * the child nothing to inherit, so it falls back to the console it is
+ * attached to -- which is the one we just made for it. */
+ HANDLE saved_in = GetStdHandle(STD_INPUT_HANDLE);
+ HANDLE saved_out = GetStdHandle(STD_OUTPUT_HANDLE);
+ HANDLE saved_err = GetStdHandle(STD_ERROR_HANDLE);
+ SetStdHandle(STD_INPUT_HANDLE, NULL);
+ SetStdHandle(STD_OUTPUT_HANDLE, NULL);
+ SetStdHandle(STD_ERROR_HANDLE, NULL);
+
+ /* Same reason the config path is converted before an editor sees it: slosh
+ * joins with '/' while the environment supplies '\\', and the mixture is
+ * worth resolving once here rather than trusting every callee with it. */
+ char cwd_native[PATH_MAX];
+ const char *cwd_arg =
+ (cwd && *cwd) ? sl_path_native(cwd, cwd_native, sizeof cwd_native) : NULL;
+
+ PROCESS_INFORMATION pi = {0};
+ BOOL ok =
+ CreateProcessA(NULL, cmd, NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT,
+ NULL, cwd_arg, &si.StartupInfo, &pi);
+
+ SetStdHandle(STD_INPUT_HANDLE, saved_in);
+ SetStdHandle(STD_OUTPUT_HANDLE, saved_out);
+ SetStdHandle(STD_ERROR_HANDLE, saved_err);
+
+ DeleteProcThreadAttributeList(si.lpAttributeList);
+ free(si.lpAttributeList);
+ if (!ok) {
+ ClosePseudoConsole(hpc);
+ CloseHandle(in_w);
+ CloseHandle(out_r);
+ free(c);
+ return -1;
+ }
+ CloseHandle(pi.hThread);
+
+ SOCKET inner = INVALID_SOCKET;
+ int outer = -1;
+ if (sl_socketpair_pump(&inner, &outer) != 0) {
+ TerminateProcess(pi.hProcess, 1);
+ CloseHandle(pi.hProcess);
+ ClosePseudoConsole(hpc);
+ CloseHandle(in_w);
+ CloseHandle(out_r);
+ free(c);
+ return -1;
+ }
+ p->fd = outer; /* what the event loop polls */
+
+ c->fd = p->fd;
+ c->hpc = hpc;
+ c->to_child = in_w;
+ c->from_child = out_r;
+ c->proc = pi.hProcess;
+ c->inner = inner;
+
+ int slot = -1;
+ for (int i = 0; i < MAX_CONPTY; i++)
+ if (!g_ptys[i]) {
+ g_ptys[i] = c;
+ slot = i;
+ break;
+ }
+ if (slot < 0) {
+ TerminateProcess(pi.hProcess, 1);
+ CloseHandle(pi.hProcess);
+ ClosePseudoConsole(hpc);
+ CloseHandle(in_w);
+ CloseHandle(out_r);
+ closesocket(inner);
+ sl_close(p->fd);
+ free(c);
+ return -1;
+ }
+
+ /* One reference for this function's caller, one for each thread below. */
+ c->refs = 1;
+ HANDLE th[3];
+ int nth = 0;
+ InterlockedIncrement(&c->refs);
+ th[nth] = (HANDLE)_beginthreadex(NULL, 0, pump_out, c, 0, NULL);
+ if (th[nth])
+ nth++;
+ else
+ InterlockedDecrement(&c->refs);
+ InterlockedIncrement(&c->refs);
+ th[nth] = (HANDLE)_beginthreadex(NULL, 0, pump_in, c, 0, NULL);
+ if (th[nth])
+ nth++;
+ else
+ InterlockedDecrement(&c->refs);
+ InterlockedIncrement(&c->refs);
+ th[nth] = (HANDLE)_beginthreadex(NULL, 0, pump_exit, c, 0, NULL);
+ if (th[nth])
+ nth++;
+ else
+ InterlockedDecrement(&c->refs);
+ for (int i = 0; i < nth; i++) CloseHandle(th[i]);
+
+ /* Non-blocking, as the POSIX build leaves its master. */
+ u_long nb = 1;
+ ioctlsocket(sl_sock_get(p->fd), FIONBIO, &nb);
+
+ p->pid = (pid_t)pi.dwProcessId;
+ return 0;
+}
+
+int pty_resize(pty_t *p, uint16_t cols, uint16_t rows, uint16_t cell_w,
+ uint16_t cell_h) {
+ (void)cell_w;
+ (void)cell_h;
+ if (!p) return -1;
+ conpty_t *c = pty_find(p->fd);
+ if (!c) return -1;
+ COORD s = {(SHORT)(cols ? cols : 80), (SHORT)(rows ? rows : 24)};
+ return SUCCEEDED(ResizePseudoConsole(c->hpc, s)) ? 0 : -1;
+}
+
+void pty_close(pty_t *p) {
+ if (!p) return;
+ conpty_t *c = pty_find(p->fd);
+ /* Closing our end first is what unblocks pump_in, which is sitting in recv
+ * on the other side of it. */
+ if (p->fd >= 0) sl_close(p->fd);
+ p->fd = -1;
+ if (c) {
+ pty_forget(c);
+ /* Asks the child to leave, and ends pump_out's read. */
+ conpty_close_console(c);
+ if (c->proc && WaitForSingleObject(c->proc, 200) == WAIT_TIMEOUT)
+ TerminateProcess(c->proc, 1);
+ /* The threads own their own references; whichever finishes last frees. */
+ conpty_release(c);
+ }
+ p->pid = -1;
+}
+
+#endif /* _WIN32 */
diff --git a/src/server.c b/src/server.c
index 3a7a366..3a3669b 100644
--- a/src/server.c
+++ b/src/server.c
@@ -112,6 +112,11 @@ static void conn_close(server_t *s, size_t i) {
* the peer is gone, or it is so far behind that keeping up is not the word for
* what we would be doing. EAGAIN is not a failure: it is the socket saying to
* come back when poll() says so. */
+#ifdef _WIN32
+/* There is no SIGPIPE to suppress, so there is nothing for MSG_NOSIGNAL to
+ * ask for: a send to a closed socket already just returns an error. */
+#define MSG_NOSIGNAL 0
+#endif
static bool conn_flush(conn_t *c) {
while (c->out_off < c->out_len) {
ssize_t w =
@@ -277,7 +282,10 @@ typedef struct {
static void watch_config(watchset_t *w) {
if (w->fd < 0) return;
-#ifdef __APPLE__
+#ifdef _WIN32
+ /* The watcher owns its directory handles and re-arms them itself, so there
+ * is no per-file descriptor to drop here. */
+#elif defined(__APPLE__)
/* kqueue drops a vnode filter when its fd closes, so closing is the unwatch. */
for (size_t i = 0; i < w->nwds; i++) close(w->wds[i]);
#else
@@ -329,6 +337,11 @@ static void watch_config(watchset_t *w) {
else
close(vfd);
}
+#elif defined(_WIN32)
+ /* Watch the directory and let the debounced re-read decide, which is the
+ * same conclusion the kqueue branch reaches for the same reason: an editor
+ * that saves by rename would slip out from under a watch on the file. */
+ sl_watch_add(w->fd, dir);
#else
int wd =
inotify_add_watch(w->fd, dir, IN_CLOSE_WRITE | IN_MOVED_TO | IN_CREATE);
@@ -346,7 +359,9 @@ static void watch_config(watchset_t *w) {
}
}
-#ifndef __APPLE__
+/* Only inotify reports which file changed; the kqueue and Windows watchers
+ * both re-read and let the debounce decide. */
+#if !defined(__APPLE__) && !defined(_WIN32)
static bool watch_hit(const watchset_t *w, const char *name) {
for (size_t i = 0; i < w->nnames; i++)
if (strcmp(w->names[i], name) == 0) return true;
@@ -359,7 +374,9 @@ static bool watch_hit(const watchset_t *w, const char *name) {
* debounces, because one save is not always one event. */
static bool watch_drain(watchset_t *w) {
bool touched = false;
-#ifdef __APPLE__
+#ifdef _WIN32
+ touched = sl_watch_drain(w->fd) != 0;
+#elif defined(__APPLE__)
struct kevent evs[16];
struct timespec zero = {0, 0};
for (;;) {
@@ -411,7 +428,9 @@ void session_env(const char *name) {
else
unsetenv("SLOSH_SESSION");
char self[512];
-#ifdef __APPLE__
+#ifdef _WIN32
+ if (sl_self_exe(self, sizeof self)) setenv("SLOSH_BIN", self, 1);
+#elif defined(__APPLE__)
/* Darwin's answer to /proc/self/exe. What it hands back can still contain
* symlinks and .., so realpath() finishes the job. */
char raw[512];
@@ -439,6 +458,12 @@ int server_run(const char *name, const char *const argv[], uint16_t cols,
session_env(name);
+#ifdef _WIN32
+ /* Windows delivers Ctrl-C and shutdown through a console control handler
+ * rather than as signals, and has no SIGPIPE or SIGCHLD to arrange for. */
+ signal(SIGINT, on_sig);
+ signal(SIGTERM, on_sig);
+#else
struct sigaction sa = {.sa_handler = on_sig};
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
@@ -451,6 +476,7 @@ int server_run(const char *name, const char *const argv[], uint16_t cols,
* looked at. SIGCHLD's default action is to be discarded, so nothing is
* interrupted and the only difference is that a status waits to be read. */
signal(SIGCHLD, SIG_DFL);
+#endif
server_t s = {0};
s.app = app_new(argv, cols, rows);
@@ -491,7 +517,9 @@ int server_run(const char *name, const char *const argv[], uint16_t cols,
int inofd = -1;
watchset_t watches = {.fd = -1};
if (watch) {
-#ifdef __APPLE__
+#ifdef _WIN32
+ inofd = sl_watch_init();
+#elif defined(__APPLE__)
inofd = kqueue();
if (inofd >= 0) fcntl(inofd, F_SETFD, FD_CLOEXEC);
#else
@@ -745,6 +773,40 @@ int server_spawn(const char *name, const char *const argv[], uint16_t cols,
if (session_socket_path(name, path, sizeof path) != 0) return -1;
session_log_path(name, logp, sizeof logp);
+#ifdef _WIN32
+ /* No fork: the daemon is started as a fresh detached process rather than as
+ * a copy of this one, reconstructing on its command line what the forked
+ * child would have inherited. `--server` is the flag main.c uses to run
+ * server_run() directly instead of spawning again. */
+ {
+ char self[512];
+ if (!sl_self_exe(self, sizeof self)) return -1;
+ const char *av[16];
+ char colbuf[16], rowbuf[16];
+ snprintf(colbuf, sizeof colbuf, "%u", (unsigned)cols);
+ snprintf(rowbuf, sizeof rowbuf, "%u", (unsigned)rows);
+ size_t n = 0;
+ av[n++] = self;
+ av[n++] = "--server";
+ av[n++] = "-s";
+ av[n++] = name;
+ av[n++] = "--cols";
+ av[n++] = colbuf;
+ av[n++] = "--rows";
+ av[n++] = rowbuf;
+ if (layout) {
+ av[n++] = "--layout";
+ av[n++] = layout;
+ }
+ if (!watch) av[n++] = "--no-reload";
+ if (argv && argv[0]) {
+ av[n++] = "--";
+ for (int i = 0; argv[i] && n < 15; i++) av[n++] = argv[i];
+ }
+ av[n] = NULL;
+ if (sl_spawn_detached(av, logp) < 0) return -1;
+ }
+#else
pid_t pid = fork();
if (pid < 0) return -1;
if (pid == 0) {
@@ -760,6 +822,7 @@ int server_spawn(const char *name, const char *const argv[], uint16_t cols,
_exit(server_run(name, argv, cols, rows, layout, watch));
}
waitpid(pid, NULL, 0); /* the intermediate exits immediately */
+#endif
/* Wait for the socket to accept, rather than sleeping and hoping. */
for (int i = 0; i < 400; i++) {