diff options
| author | Peter Geissler <peter@haxx.in> | 2026-08-28 00:11:24 +0200 |
|---|---|---|
| committer | Peter Geissler <peter@haxx.in> | 2026-08-28 00:23:19 +0200 |
| commit | d1b5a64c3855b07074b3c958739df4e16f6084a5 (patch) | |
| tree | 10579e84bd972661738f93fdd8c221674f09b857 /Makefile.windows | |
| parent | macos zip: carry the same paperwork as the linux tarballs (diff) | |
| download | slosh-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 'Makefile.windows')
| -rw-r--r-- | Makefile.windows | 93 |
1 files changed, 93 insertions, 0 deletions
diff --git a/Makefile.windows b/Makefile.windows new file mode 100644 index 0000000..99ba6f7 --- /dev/null +++ b/Makefile.windows @@ -0,0 +1,93 @@ +# slosh for Windows — cross-compiled with zig, or built natively on the box. +# +# `compat/win` comes first on the include path: it holds stub <poll.h>, +# <sys/socket.h> and friends that redirect to include/compat_win.h, so the +# existing sources keep their POSIX includes and call sites unchanged. +# +# make -f Makefile.windows # host arch, native +# make -f Makefile.windows ARCH=x86_64 # cross to x86_64 +# +ZIG ?= zig +ARCH ?= aarch64 +TARGET := $(ARCH)-windows-gnu +CC := $(ZIG) cc -target $(TARGET) + +VT := vendor/libghostty-vt +VT_OUT := $(VT)/zig-out-win-$(ARCH) +VT_LIB := $(VT_OUT)/lib/ghostty-vt-static.lib +VT_INC := $(VT_OUT)/include +VT_VERSION := $(shell sed -n 's/^[[:space:]]*\.version = "\([^"]*\)".*/\1/p' $(VT)/build.zig.zon | head -1) + +OUT := build/win-$(ARCH) +VERSION := $(shell contrib/version 2>/dev/null || cat VERSION) + +CFLAGS := -std=c23 -O1 -g -Wall -Wextra -Wno-unused-parameter -MMD -MP \ + -Icompat/win -I$(VT_INC) -Iinclude -I$(OUT) \ + -DGHOSTTY_STATIC \ + -include compat_win.h +LDFLAGS := -lws2_32 -lshlwapi -lkernel32 -luser32 -ladvapi32 -lole32 + +SRC := $(wildcard src/*.c) +OBJ := $(SRC:src/%.c=$(OUT)/%.o) +DEPS := $(OBJ:.o=.d) +BIN := $(OUT)/slosh.exe + +V ?= 0 +ifeq ($(V),0) + Q := @ + say = @printf ' %-5s %s\n' '$(1)' '$(2)' +else + Q := + say = @: +endif + +.PHONY: all clean vendor-win +.DEFAULT_GOAL := all + +all: $(BIN) + +$(BIN): $(OBJ) $(VT_LIB) + $(call say,[LD],$@) + $(Q)$(CC) $(OBJ) $(VT_LIB) -o $@ $(LDFLAGS) + +$(OUT)/%.o: src/%.c $(OUT)/version.h $(OUT)/logo.h $(VT_LIB) | $(OUT) + $(call say,[CC],$<) + $(Q)$(CC) $(CFLAGS) -c $< -o $@ + +$(OUT)/png.o: src/png.c $(OUT)/version.h $(VT_LIB) | $(OUT) + $(call say,[CC],$<) + $(Q)$(CC) $(CFLAGS) -I$(VT)/src/stb -Wno-unused-function -c $< -o $@ + +# The shim itself calls the real socket API, so it is built with the +# redirections switched off. Everything else sees the POSIX names. +$(OUT)/compat_win.o: src/compat_win.c $(OUT)/version.h | $(OUT) + $(call say,[CC],$<) + $(Q)$(CC) $(CFLAGS) -DSL_COMPAT_IMPL -c $< -o $@ + +$(OUT)/pty_win.o: src/pty_win.c $(OUT)/version.h $(VT_LIB) | $(OUT) + $(call say,[CC],$<) + $(Q)$(CC) $(CFLAGS) -DSL_COMPAT_IMPL -c $< -o $@ + +$(OUT): + @mkdir -p $(OUT) + +$(OUT)/version.h: | $(OUT) + @printf '/* generated by Makefile.windows */\n#define SLOSH_VERSION "%s"\n' '$(VERSION)' > $@ + +$(OUT)/logo.h: logo.txt | $(OUT) + $(Q){ printf '/* generated from logo.txt */\n'; \ + printf 'static const char *const LOGO[] = {\n'; \ + sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g' -e 's/^/ "/' -e 's/$$/",/' logo.txt; \ + printf '};\n'; } > $@ + +$(VT_LIB): vendor-win + +vendor-win: + cd $(VT) && $(ZIG) build -Demit-lib-vt -Demit-xcframework=false \ + -Dversion-string=$(VT_VERSION) -Doptimize=ReleaseFast \ + -Dtarget=$(TARGET) -p zig-out-win-$(ARCH) + +clean: + rm -rf $(OUT) + +-include $(DEPS) |
