aboutsummaryrefslogtreecommitdiffstats
path: root/Makefile.windows
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 /Makefile.windows
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 'Makefile.windows')
-rw-r--r--Makefile.windows93
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)