aboutsummaryrefslogtreecommitdiffstats
path: root/docs/windows.md
blob: f43c9c9f1153dc6d57e9bbc2eebb534813e1b885 (plain) (blame)
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
# Windows

slosh runs natively on Windows 10/11, on both x86-64 and ARM64. It is one
`slosh.exe` with no runtime dependency beyond the OS.

The build is a cross-compile, from a mac or a Linux box:

```bash
make -f Makefile.windows ARCH=x86_64
make -f Makefile.windows ARCH=aarch64
```

`ARCH` defaults to `aarch64`. The vendored terminal core is cross-compiled the
same way and lands in `vendor/libghostty-vt/zig-out-win-<arch>`, so the two
architectures never share an output directory.

The binary appears at `build/win-<arch>/slosh.exe`; copy that to the Windows
machine.

For a release, `make -f Makefile.windows ARCH=<arch> dist` stages the same
build as `dist/slosh-<version>-windows-<arch>.zip` (the binary plus the same
licensing paperwork the linux tarballs carry), and `make release` builds both
architectures alongside the linux and macOS artifacts, covered by the same
`SHA256SUMS`.

## What it does there

Everything the POSIX build does: panes, splits, tabs, detachable sessions, the
config file and its live reload, the control socket, the mouse. Panes run
`cmd.exe` by default; `%ComSpec%` if you have pointed it elsewhere, and
`shell` in the config file wins over both. `slosh ls`, `slosh -s NAME cmd ...`
and `slosh --script` behave as documented.

Sessions live in `%LOCALAPPDATA%\slosh` rather than `$XDG_RUNTIME_DIR`: a
session socket is machine-local by definition and must not follow a roaming
profile onto another host.

## How it works

The port is a compatibility layer, not 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. The existing sources keep their POSIX includes and their POSIX call
sites, and the platform differences are answered in one place.

Three 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: a ConPTY hands back pipe `HANDLE`s and
`WSAPoll` waits on `SOCKET`s and nothing else. Rather than rewrite the 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. `server.c`'s loop is then byte-for-byte the
POSIX one, and only what sits behind the descriptor differs. The config watcher
(`ReadDirectoryChangesW`) and the client's console input reach the loop the
same way.

**Socket descriptors are tagged, not guessed.** A wrapped `SOCKET` is returned
as `SL_SOCK_BASE + slot`, so `read`, `write`, `close` and `poll` can tell a
socket from a CRT file descriptor with certainty rather than 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.

**A pane is a ConPTY.** `src/pty_win.c` implements the same three functions as
`src/pty.c` (spawn, resize, close) on `CreatePseudoConsole`, with the pty's
`fd` being the pollable end of its pump pair and its `pid` a real Windows
process id, so the existing kill and wait paths work unchanged.

Two details are worth knowing, because both are invisible until they bite.

**A pseudoconsole does not stop the child inheriting your stdio.** A child
attached to one 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
symptom was a pane with a correct *title* and an empty *body*, because ConPTY
emits the title itself. `pty_win.c` clears the three standard handles for the
duration of the `CreateProcess` call, leaving the child nothing to inherit.

**A ConPTY never reports EOF.** On POSIX the pty master reports end-of-file
once the last slave descriptor closes, which happens as the child dies, and
`pane_pump()`'s `read()` returning 0 is the entire 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`
sat there looking alive forever. So the exit is waited for explicitly, on the
process handle, and `ClosePseudoConsole` is what then produces the EOF: the
console flushes what the child left behind and closes its end, and the output
pump sees the end of the stream exactly as it would on a pty.

**There are two quoting conventions, and cmd.exe uses the other one.** Windows
passes a command *string*, not a vector, so the quoting rules belong to whoever
parses it at the far end, and there are two sets. Ordinary programs are parsed
by `CommandLineToArgvW`, where a literal quote is written `\"`. `cmd.exe` is
not: it has its own rules, in which `\"` is a backslash followed by the end of
a quoted section. Applying the first set to the second turned
`cmd /c notepad "C:\...\config.kdl"` into an editor being handed
`\"C:\...\config.kdl\"`, backslashes and all. So `cmd /c` is given 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 still quoted for `CommandLineToArgvW`.

**Paths are mixed, and only some programs mind.** slosh builds paths with `/`
throughout, because that is what it splits and compares on. The environment
supplies `\`, so the moment a literal is joined to a variable the result is
mixed: `$HOME` is `C:\Users\you`, and the config lands at
`C:\Users\you/.config/slosh/config.kdl`. The CRT opens that without complaint,
which is why `--check` reported it as fine, but a GUI file dialog rejects it
outright, and `C-a e` produced a notepad saying "Not a valid file name" about a
path that unambiguously existed. Anything handed to another program now goes
through `sl_path_native()` first, which canonicalises it and settles the
separators; the internal representation is left alone.

The reference counting is also why the per-pty state is not simply freed by
`pty_close`: two pumps, a exit watcher and the caller all reach
it, and a pump blocked on a dead pipe must not be waited for: that would hang
the session on the one program that misbehaves. Whoever finishes last frees.

## What is different

- **No `SIGWINCH`.** A resize is noticed by comparing the console size on a
  short poll timeout, which is why the client's `poll()` there is bounded
  rather than infinite.
- **No `fork`.** The session daemon is started as a fresh detached process that
  reconstructs its arguments on the command line (`--server`), rather than as a
  copy of the process that asked for it.
- **No process groups on a pty**, so "what is this pane running" is answered
  with the most recently started descendant of the pane's shell: the process
  `tcgetpgrp()` would have named. Windows offers the image name rather than a
  full argv without reading another process's memory, so pane titles are
  correspondingly shorter.
- **No `VEOF`, so Ctrl-D is made to mean something.** The console driver has
  no EOF character: `^D` reaches cmd.exe and PowerShell as an ordinary
  keystroke and they ignore it, so the key that closes a pane on every other
  platform does nothing at all. `ctrl_d_exits` (on by default here, off
  elsewhere) sends the shell the word `exit` instead, which is what you would
  have typed, and what makes the pane end through the shell's own cleanup and
  exit status rather than by being closed from outside. It stays out of the
  way unless all three hold: the shell itself in the foreground, the primary
  screen, and an empty line. A program that is running may want the EOF
  itself; in an editor or a pager `^D` is half a page down; and a line with
  something on it is not an EOF anywhere.
- **No editor is guaranteed.** `C-a e` prefers whichever console editor is
  actually installed (`nvim`, `vim`, `vi`, `nano`, `micro`, `hx`, `edit`),
  because `notepad` is a GUI program: a pane running it draws nothing and looks
  broken. Microsoft Edit (`edit`) ships from Windows 11 24H2 and fills a pane
  like any other editor; it comes last because it arrives on its own, and so
  says nothing about what you want. If nothing is found, slosh still opens
  notepad, and says so, rather than leaving an empty pane to puzzle over.
  `EDITOR` and the config's `editor` are honoured ahead of the search.
- **`--check`, `--dump-config`, layouts, workspaces and shaders** are
  unaffected: they never touched a pty or a socket.