<feed xmlns='http://www.w3.org/2005/Atom'>
<title>sl0ppij.git/zellij-utils/src/input/unit/layout_test.rs, branch main</title>
<subtitle>a sl0p.foo repository</subtitle>
<id>https://git.sl0p.foo/sl0ppij.git/atom/zellij-utils/src/input/unit/layout_test.rs?h=main</id>
<link rel='self' href='https://git.sl0p.foo/sl0ppij.git/atom/zellij-utils/src/input/unit/layout_test.rs?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/'/>
<updated>2026-08-12T16:44:24Z</updated>
<entry>
<title>sl0p: --purpose flag and tab-level purpose</title>
<updated>2026-08-12T16:44:24Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T16:44:24Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=2c67579398835d51e625085ce3b22c22bc8f0cba'/>
<id>urn:sha1:2c67579398835d51e625085ce3b22c22bc8f0cba</id>
<content type='text'>
Completes the two pieces deferred from the first workspace-hints pass.

`zellij run --purpose X` / `zellij action new-pane --purpose X` tag a pane at
spawn time, which is what an agent wants for panes it creates itself; the OSC
wrapper still works for programs that declare their own role. Not supported with
--in-place, which replaces another pane rather than being a pane of its own: clap
rejects the combination rather than accepting a flag that would do nothing.

`tab name="work" purpose="agent-work"` gives a whole tab a role, reported by
`list-tabs --json`. It rides the tab's ROOT layout node, reusing the pane field
because a tab's root is the tab container and never itself a pane -- asserted by a
test for the tightest case (a tab holding exactly one pane), since a leak there
would silently mislabel a pane.

The interesting part is why the flag first shipped broken. There are TWO protobuf
boundaries -- the plugin API and the client-&gt;server IPC contract -- and a CLI flag
must cross the second one. Defaulting the field in the conversion (what "no
protoc" usually forces) compiles cleanly and then silently drops the value in
transit, so --purpose parsed, printed a pane id, and did nothing. Fixed by adding
the field to both contracts by hand, in the .proto AND the committed generated
prost, kept in lockstep so a future regeneration reproduces it: protoc is only
needed to regenerate, not to build. Both boundaries now have a round-trip test,
and the IPC one was confirmed to fail when the wire read is removed.

Sanitising moved to Grid::set_authoritative_purpose, the single choke point every
out-of-band source passes through, after the CLI path was observed delivering
`bad purpose; rm -rf /` intact into list-panes --json.
</content>
</entry>
<entry>
<title>sl0p: per-pane workspace hints (purpose=)</title>
<updated>2026-08-12T15:05:34Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T15:05:34Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=df052db4608f4d835fe7b4ca76dbb4059e78943b'/>
<id>urn:sha1:df052db4608f4d835fe7b4ca76dbb4059e78943b</id>
<content type='text'>
Implements the zellij half of sl0p/workspace-hints-proposal.md: a pane can carry
an opaque agent-facing hint for what it is FOR, so a project can ship a layout
that an agent is able to navigate instead of guessing.

zellij never interprets a purpose. The consuming agent owns the entire vocabulary,
which is what keeps this to one string threaded along the existing status_bar path
rather than a policy engine, and lets the vocabulary change without a rebuild.

Two ways in, because declared panes and agent-created panes are different:
  layout:  pane name="server" purpose="dev-server"
  in-band: printf '\033]5577;1;purpose;agent:build\033\\'
The second exists because tagging at spawn time would mean threading a field
through 28 SpawnTerminal call sites, and a new action variant is impossible while
protobuf enums cannot be regenerated. Wrapping the command costs one printf.

A declared purpose outranks an in-band one and cannot be overridden or cleared by
it. OSC arrives from whatever the pane happens to be showing, so otherwise a
`cat hostile.txt` in the human's editor pane could relabel it as the agent's
scratch pane and invite an agent to run commands where the human is working. A
layout is written by a human in a file they can read; in-band data only fills a
blank.

Exposed on PaneListEntry (`list-panes --json`) rather than PaneInfo, which crosses
the plugin protobuf API we cannot regenerate; resolved in the screen thread where
panes live rather than via route.rs's per-pane PTY round-trips. Values are
sanitised to a bounded [A-Za-z0-9_.:/-] token at both entry points, because a
purpose is never rendered -- it is matched against and pasted into consumers'
shell and JSON pipelines.

Verified end to end rather than by inspection: purposes from a layout appear in
list-panes --json against the right pane ids, an agent-spawned pane tags itself,
a declared purpose survives an in-band override attempt, all of it comes back
after kill + resurrect, an unknown pane property is still a parse error, and a
layout with no purpose leaves dump-layout unchanged.
</content>
</entry>
<entry>
<title>sl0p: add an indexed style for status bar buttons</title>
<updated>2026-08-12T14:15:19Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T14:15:19Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=c3764fca00c1d91c06612a33162b2354e9d42be0'/>
<id>urn:sha1:c3764fca00c1d91c06612a33162b2354e9d42be0</id>
<content type='text'>
`status_bar_button_style "indexed"` renders `1:Approve 2:Reject` instead of
`[Approve] [Reject]`, so the bar doubles as a key legend for a harness that binds
alt+1..9 to the same actions.

A config option rather than a protocol field: whether the numbers mean anything
depends on what the USER has bound, not on what the program knows. It needs no
wire change and so no version bump, and the click report still carries the id --
never the index -- so a program is unaffected by which style is in force.

The style is threaded into layout_status_bar, the one function the renderer draws
from and the hit-test resolves against. That is what makes this safe: changing a
button's rendered width necessarily changes its clickable span too, verified by a
test asserting span width equals the drawn width and that every drawn column
resolves back, for both styles.

Also fixes the web client's Style construction to read the option from config
rather than defaulting it -- my first pass claimed that code had no pane_frames
config available, which was wrong; it uses it two lines further down.
</content>
</entry>
<entry>
<title>sl0p: clickable action buttons in the pane status bar</title>
<updated>2026-08-12T13:18:33Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T13:18:33Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=549f052ae5451d57b58f5de22de57b2a586e6d23'/>
<id>urn:sha1:549f052ae5451d57b58f5de22de57b2a586e6d23</id>
<content type='text'>
Second slice of the status/action frame. A program declares buttons, and a click
is reported back to it:

    printf '\033]5577;1;buttons;approve:Approve;reject:Reject\033\\'
    -&gt; click -&gt; program's stdin receives  OSC 5577 ; 1 ; click ; approve  ST

The report is a FIXED format carrying only the id, never bytes the payload chose,
and ids are restricted to [A-Za-z0-9_-]. The sequence that creates a button is
in-band and untrusted -- `cat` of a hostile file emits it as readily as a
cooperating program -- so a click that could send arbitrary bytes would let
viewing a file inject into the stdin of whatever is running, a shell included.
Labels are '%'-escaped since the wire format is ';'-separated, and both the
button count and id length are capped because the list lives as long as the pane.

Positions come from one layout_status_bar function that the renderer draws from
and the hit-test resolves against. Computing that geometry twice is how the
stacked-pane picker once selected the wrong pane; a test asserts every drawn
column resolves back to the button drawn there, across 100 widths.

Two things the drive caught that unit tests could not:

- The first delivery path pushed onto grid.pending_messages_to_pty, which is
  only drained when the PANE produces output -- so a click on an idle program sat
  in the queue forever. The write now happens at the tab level, which owns the
  senders.
- The hit-test derived the frame row from the pane's raw geometry, but this fork
  draws the frame inset by gap_offset, so with `gap 1` every click missed by one
  row. It now derives from the CONTENT rect, which is by definition the inside of
  the drawn frame and therefore the same columns the renderer lays out.

Buttons serialise with the session like the status text, so a resurrected pane
comes back with its buttons intact.
</content>
</entry>
<entry>
<title>sl0p: keep plugin panes pinned when synthesizing a layout</title>
<updated>2026-08-11T18:26:57Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-11T18:26:57Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=77818ea9b24eb5d1a57f85b94aedc7bba7cbe8b1'/>
<id>urn:sha1:77818ea9b24eb5d1a57f85b94aedc7bba7cbe8b1</id>
<content type='text'>
Collapsing and expanding a tab from the default layout stranded the status bar
halfway down the screen, and left the collapsed stack three rows tall.

A synthesized layout describes only where the user's panes go. Applied bare, it
laid out EVERY pane in the tab -- including the status bar, which became an
ordinary grid cell. Real swap layouts do not hit this because their `tab` node
is expanded with the default tab template, which carries the pinned plugin
slots; a layout built at runtime has to take those from BASE instead.

Synthesis now builds into the tab's own structure: the first non-plugin leaf
becomes the stack or grid and inherits its size, further content leaves are
dropped since the replacement accounts for every pane itself, and plugin panes
keep their slots and sizes.

Falls back to the bare replacement when there is no content slot at all -- a
layout of nothing but plugin panes would otherwise be returned unchanged,
silently dropping every user pane. Found by the test, not by the drive.

Only reproducible with a layout that has plugin panes, which is why the
quaddie.kdl drives never showed it and the default layout did immediately.
</content>
</entry>
<entry>
<title>sl0p: add `arrangement` to build a tree for N panes</title>
<updated>2026-08-10T22:50:30Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-10T22:50:30Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=f72c7266fe88070149639d66ae09b43c2d990f6e'/>
<id>urn:sha1:f72c7266fe88070149639d66ae09b43c2d990f6e</id>
<content type='text'>
A `children` placeholder inserts every pane it receives as a flat sibling.
That single hardcoded policy is why a layout can only express "N equal
columns", "N equal rows" or "one stack" without hand-writing a variant per
pane count -- the max_panes=5 / max_panes=8 / max_panes=12 ladders in a swap
layout file exist purely to work around it.

`pane arrangement="grid" { children; }` builds a tree for N panes instead.
Two to start: grid (ceil(sqrt(n)) columns, rows filled left to right) and
master-stack (one 60%-wide master, the rest beside it).

Generates a TiledPaneLayout rather than geometry, so everything downstream
works unchanged: split_space, the minimum-size feasibility check,
LayoutApplier, swap-layout damage tracking, and the responsive collapse. It
composes with that last one for free -- a grid is simply infeasible on a
narrow terminal, so it is skipped and the collapse takes over.

The arrangement builds ONE self-contained subtree rather than splicing a
variable number of nodes in among the placeholder's siblings, so it stays well
defined next to the pre-existing "children node with sibling nodes" TODO
rather than leaning on it.

Like default_fg/default_bg, the field has no protobuf representation, so it is
defaulted at both plugin-API boundaries and does not survive a round trip.
This is why it is a field rather than a LayoutConstraint variant: a new enum
variant would have no representation when converting TO protobuf, and the
committed prost assets cannot be regenerated here (no protoc).

The 48 snapshot updates are the new field appearing in TiledPaneLayout debug
dumps (324 lines, all `children_arrangement: None`). No snapshot content was
removed. The `expression:` metadata churn in those files is pre-existing drift
that regeneration swept up: the stored snapshots recorded the expression
before normalize_layout_debug was introduced.
</content>
</entry>
<entry>
<title>Proper error when providing layout name with a tab (#4958)</title>
<updated>2026-03-30T18:34:12Z</updated>
<author>
<name>Aram Drevekenin</name>
<email>aram@poor.dev</email>
</author>
<published>2026-03-30T18:34:12Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=22ace8e54fbdadd08973854ae7178535a62065b1'/>
<id>urn:sha1:22ace8e54fbdadd08973854ae7178535a62065b1</id>
<content type='text'>
* proper error when providing layout name with a tab

* rustfmt</content>
</entry>
<entry>
<title>[Windows port PR8] feature: add Windows support (#4768)</title>
<updated>2026-03-04T06:29:16Z</updated>
<author>
<name>divens</name>
<email>divens@users.noreply.github.com</email>
</author>
<published>2026-03-04T06:29:16Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=f6bf1af1cef0138d42429ddcb1246ae4f2c6216b'/>
<id>urn:sha1:f6bf1af1cef0138d42429ddcb1246ae4f2c6216b</id>
<content type='text'>
* feat: add Windows signal and process implementations

- AsyncSignalListener: polls crossterm size at 100ms, tokio ctrl_c/break/close
- BlockingSignalIterator: SetConsoleCtrlHandler + AtomicBool, polls size at 50ms
- signal_process: GenerateConsoleCtrlEvent for Interrupt, TerminateProcess for Kill
- Add windows-sys workspace dependency

* feat: add Windows IPC support with named pipes and marker files

- ipc_connect/ipc_bind/ipc_bind_async helpers with cfg gates for named pipes
- Dual-pipe architecture on Windows: main pipe + reply pipe per connection to
  prevent IPC deadlock (half-duplex named pipe limitation)
- is_ipc_socket returns is_file() on non-Unix for marker file discovery
- ipc_bind creates empty marker file after binding named pipe on Windows
- assert_socket split: cfg(unix) probes with ConnStatus, cfg(not(unix)) returns
  true to avoid deadlocking the server accept loop
- Replace tokio UnixListener/UnixStream with interprocess tokio Listener/Stream
  in web server IPC for cross-platform support
- Add interprocess tokio feature to workspace deps

* feat: add Windows client support for input, server spawn, and stack size

- Two-mode Windows input: crossterm event reader for native console,
  termwiz byte parser for terminal emulators (VT input mode)
- cast_crossterm_key: convert crossterm KeyEvent to KeyWithModifier
- ENABLE_VIRTUAL_TERMINAL_INPUT on Windows stdin for raw VT byte reading
- Skip ANSI terminal query on Windows to unblock startup
- spawn_server: CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP on Windows
- 8MB stack trampoline in main() for Windows (default 1MB overflows)
- 8MB stack for server worker threads (WASM compilation needs it)
- Enable crossterm events and bracketed-paste features

* test: make tests cross-platform for Windows

- Socket tests: cross-platform IpcGuard/IpcName helpers with GenericNamespaced
  on Windows vs GenericFilePath/TempDir on Unix
- Layout tests: normalize_layout_debug() replaces backslash separators in Debug
  output on Windows; gate env_var_expansion test with cfg(unix)
- Setup tests: use PathBuf::join + display() instead of hardcoded / separators
- Server OS tests: cross-platform command helpers (long_running_cmd, echo_cmd,
  stdin_reader_cmd) with CREATE_NO_WINDOW on Windows
- zellij_exports: replace GenericFilePath with ipc_connect() helper

* ci: add Windows build and test jobs with debug logging

- Add build-windows job: cargo check --no-default-features on windows-latest
- Add test-windows job: cargo test --no-default-features for utils/server/client
- Enable manual workflow_dispatch trigger
- Add debug log::info! calls in server startup, plugin loading, PTY spawning,
  and screen layout application for troubleshooting

* feat: implement Windows stubs for shell, command hooks, and process listing

Replace unimplemented!() stubs with working Windows implementations:
- get_default_shell: reads $COMSPEC, falls back to cmd.exe
- run_command_hook: uses cmd /C with RESURRECT_COMMAND env var
- get_all_cmds_by_ppid: uses sysinfo to enumerate processes by parent PID

* feat: implement Windows daemonize_web_server with detached process spawning

Replace the `unimplemented!()` stub with a proper background process
spawner that mirrors the existing `spawn_server` pattern: launch a
foreground `zellij web --start` child with CREATE_NO_WINDOW |
CREATE_NEW_PROCESS_GROUP, poll the TCP port to confirm it's listening,
then exit the parent process.

* feat: implement native ConPTY backend for Windows PTY

Replace all WindowsPtyBackend stubs with a full ConPTY implementation:

- CreatePseudoConsole with overlapped named pipes for IOCP-based async
  I/O (zero extra threads on the read path per pane)
- CreateProcessW with PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE and proper
  UTF-16 command line quoting / environment block construction
- ConPtyAsyncReader with lazy promotion from OwnedHandle to tokio
  NamedPipeServer (reactor not available at spawn time)
- Per-terminal exit monitoring thread using WaitForSingleObject
- ResizePseudoConsole for terminal resize, WriteFile for stdin,
  TerminateProcess / GenerateConsoleCtrlEvent for signals

Note: for PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, the HPCON value must be
passed directly as lpValue (not a pointer to it), matching the C
convention where HPCON is void*. In Rust (windows-sys) HPCON is isize,
requiring an explicit cast. See microsoft/terminal#6705.

* chore: remove debug logging from ConPTY backend

* fix: resolve Windows plugin directory and WASI mount errors

Two Windows-specific fixes for the plugin system:

1. Replace colons with underscores in URL-derived plugin directory names
   (e.g. "zellij:tab-bar" -&gt; "zellij_tab-bar"). Colons are illegal in
   Windows path components, causing OS error 123 on plugin dir creation.

2. Use FILE_FLAG_BACKUP_SEMANTICS when opening directories for WASI
   pre-opened mounts. Windows requires this flag to open directory
   handles, otherwise File::open() returns OS error 5 (access denied).

Both fixes are behind #[cfg(windows)] — Unix behavior is unchanged.

* fix: enable mouse support on Windows native console path

Mouse events were silently dropped in the crossterm event loop, and
enable_mouse() wrote VT escape sequences that don't set the required
ENABLE_MOUSE_INPUT console mode flag. Add from_crossterm_mouse()
converter, InputInstruction::MouseEvent variant, and use crossterm's
EnableMouseCapture/DisableMouseCapture on Windows.

* fix: Windows get_default_shell() now checks $SHELL before $COMSPEC

* fix: resolve bare command names via PATH and PATHEXT on Windows

CreateProcessW doesn't reliably resolve bare command names (e.g.
"pwsh") through PATH the way Unix execvp does. Additionally, the
existing command_exists() check didn't consider Windows PATHEXT
extensions (.exe, .cmd, .bat, etc.).

Add find_executable() and resolve_command() which search PATH with
PATHEXT support on Windows, and use the resolved absolute path when
spawning via CreateProcessW.

* chore: run cargo fmt --all

* fix: canonicalize expected paths in setup tests for Windows \\?\ prefix

* fix: Windows CI test failures in spawn_and_read_output and snapshot test

Use cmd /K instead of /C in spawn test so ConPTY has time to flush
output before the pipe closes. Normalize backslashes in snapshot test
to handle Windows path separators.

* fix: strip redundant Shift and AltGr modifiers from Windows key events

On the Windows native console path, crossterm reports physical modifier
flags alongside the already-translated character. For example on French
AZERTY, Shift+ù produces Char('%') with SHIFT, and AltGr+_ produces
Char('\') with CTRL+ALT. These extra modifiers cause keybinding
mismatches and prevent modified characters from being typed in plugin
text inputs.

Strip Shift for all Char events and Ctrl+Alt (AltGr) when the character
is printable, matching Unix terminal behavior where only the resulting
character is reported. Also fix raw byte synthesis for AltGr characters
to emit plain UTF-8 instead of ESC-prefixed sequences.

* fix: detect and clean up stale Windows sessions via PID-based liveness check

Write server PID to marker files instead of creating empty files.
assert_socket() on Windows now reads the PID and checks process liveness
via OpenProcess, cleaning up stale marker files for dead sessions.

* fix: connect reply pipe on Windows for kill-session and delete-session

On Windows the server blocks on reply_listener.accept() after accepting
the main pipe connection. Without a reply pipe connection, the route
thread is never spawned and the KillSession message is never processed.

* fix: wait for server Exit response in kill/delete session on Windows

The previous fire-and-forget approach was unreliable: the client would
disconnect before the server's route thread had time to read and process
the KillSession message. Now we wait for the Exit response on the reply
pipe, ensuring the server has fully processed the kill before we return.

* fix: resolve Windows console mode race between VT input and mouse setup

Two threads race over SetConsoleMode on the stdin console handle:
- stdin_handler calls enable_vt_input() (read-modify-write)
- input_handler calls enable_mouse() via crossterm's EnableMouseCapture
  (full SetConsoleMode overwrite to 0x0098)

Depending on ordering, crossterm could clobber ENABLE_VIRTUAL_TERMINAL_INPUT
(breaking the VT byte reader), or enable_vt_input could preserve
ENABLE_QUICK_EDIT_MODE (intercepting mouse events at the console level).

Fix enable_vt_input() to explicitly set the exact console mode needed
(0x0298) instead of read-modify-write, clearing QUICK_EDIT_MODE and other
line-editing flags.

Fix enable_mouse()/disable_mouse() on the VT path (TERM is set) to use
ANSI escape sequences instead of crossterm's Console API, avoiding the
competing SetConsoleMode call entirely. Enable ENABLE_VIRTUAL_TERMINAL_PROCESSING
on stdout first so ConPTY forwards the DEC private mode sequences to the
terminal emulator.

* fix: session-manager plugin kill session on Windows dual-pipe IPC

The plugin's kill_sessions() only connected the main pipe and sent
KillSession, but on Windows the server blocks on reply_listener.accept()
before spawning the route thread. Without connecting the reply pipe the
message was never processed.

Add the same dual-pipe handling as the CLI kill_session(): connect the
reply pipe first, send the message, then wait for the Exit response.

* fix(windows): replace resize polling with event forwarding from stdin thread

Forward crossterm Event::Resize from the stdin thread to the signal
handler thread via an mpsc channel, replacing the 50ms polling loop.
This makes terminal resizing much more responsive during mouse-driven
resizes and eliminates wasteful polling when idle.

When the VT reader path is active (terminal emulators with TERM set),
the sender is dropped, causing the signal handler to automatically
fall back to size-polling — preserving existing behavior for that path.

Unix is unaffected: handle_signals receives None for the receiver and
BlockingSignalIterator continues using signal_hook as before.

* fix(windows): enable ANSI terminal queries on the VT reader path

The startup ANSI query mechanism (pixel dimensions, colors, sync output)
was blanket-skipped on Windows. The actual limitation is specific to the
native console path, where crossterm's event::read() uses
ReadConsoleInput (INPUT_RECORDs) and never reaches the byte-reader loop
that parses ANSI responses.

On the VT reader path (terminal emulators like Alacritty with TERM set),
fill_buf() reads raw VT bytes via ReadFile — same as Unix — so ANSI
query responses work normally. Move the use_vt_reader decision earlier
and use it to gate the query, enabling it for the VT reader path while
still skipping it on native console.

* fix: handle early TerminalResize before client is fully registered

On Windows, enable_mouse()'s SetConsoleMode call can generate a
WINDOW_BUFFER_SIZE_EVENT that arrives as a resize before the server
finishes processing the initial client connection. At that point
set_client_data() hasn't been called yet, so set_client_size() silently
no-ops on the None entry and min_client_terminal_size() returns None.

Instead of panicking, skip the resize when the minimum size can't be
determined — the server will query the terminal size once setup
completes.

* fix: remove unnecessary custom stack size

Early versions of zelilj windows port crashed with stack overflow.
Increasing stack size allowed proper startup and moving further with the
porting effort. Current implementation no longer suffers from stack
issues, so we can revert back to default stack size.

* fix(windows): re-enable Ctrl+C handling inherited by ConPTY children

The server is spawned with CREATE_NEW_PROCESS_GROUP, which disables
Ctrl+C for the process.  Children inherit this disabled state, so
ConPTY child processes (shells, ping, etc.) silently ignore
CTRL_C_EVENT signals generated by the pseudo-console.

Call SetConsoleCtrlHandler(NULL, FALSE) early in start_server() to
clear the inherited ignore flag before any ConPTY children are created.

* refactor(windows): move inline cfg(windows) blocks to platform files

Break out inline #[cfg(windows)] blocks from shared client code into
the existing _unix.rs / _windows.rs platform files, per upstream
feedback.

- Split spawn_server into two #[cfg]-gated function definitions
- Extract setup_ipc, enable_mouse_support, disable_mouse_support into
  os_input_output_unix.rs and os_input_output_windows.rs
- Unify BlockingSignalIterator::new() signature across platforms
- Extract native console stdin loop into stdin_handler_windows.rs

* fix: remove sporadic info log statements

* ci(windows): add Windows x64 release builds and upgrade CI

- Add Windows x64 target to release.yml for both normal and no-web
  release matrices, with NASM, static CRT, .zip packaging, and
  PowerShell checksum steps
- Upgrade build-windows in rust.yml from cargo check to cargo xtask
  build, add wasm32-wasip1 target and NASM
- Add build_release() function and platform-aware e2e_build() to
  xtask/src/ci.rs
- Add BuildRelease command definition to xtask/src/flags.rs

* refactor(client): make resize channel unconditional across platforms

Create the resize mpsc channel on all platforms instead of gating it
behind #[cfg(windows)], avoiding conditionally-existing variables that
are hard to follow. On Unix the sender is immediately dropped and the
receiver is passed as None-equivalent to handle_signals.

* refactor(windows): restructure signal iterator with if/else for channel vs poll

Consolidate the quit check at the top of a single loop and use if/else
for channel mode (native console) vs poll mode (VT reader). On channel
disconnect, set resize_receiver to None and continue into the else
branch instead of falling through.

* docs(server): clarify comment about early resize before client init

Explain why min_client_terminal_size() can return None even after
set_client_size(): new_client() inserts None, and set_client_size()
is a no-op on None entries since it uses as_mut().map().

* feat(web): add --server-startup-timeout flag for web server

Make the hardcoded 10-second TCP-poll timeout in the non-Unix
daemonize_web_server configurable via a CLI flag, defaulting to 10s.

* fix(windows): use unique pipe names for ConPTY output on re-spawn

When a held pane re-runs a command or drops to shell, do_spawn tried
to create a named pipe with the same name as the previous spawn.  The
old ConPtyAsyncReader still held the read handle, so CreateNamedPipeW
failed (FILE_FLAG_FIRST_PIPE_INSTANCE, max instances = 1) and the
error was silently swallowed, leaving the pane blank.

Add a monotonic AtomicU64 counter to the pipe name so each spawn gets
a globally unique pipe, regardless of whether the previous one has
been cleaned up yet.

* ci(windows): enable static CRT for regular Windows release build

The no-web Windows build already had RUSTFLAGS="-C target-feature=+crt-static"
but the regular build was missing it, resulting in a dynamic CRT dependency.

* docs(web): clarify --server-startup-timeout is Windows-only

---------

Co-authored-by: divens &lt;divens.dev@gmail.com&gt;</content>
</entry>
<entry>
<title>Allow toggling pane frames for specific panes (#4615)</title>
<updated>2026-01-20T09:13:37Z</updated>
<author>
<name>Aram Drevekenin</name>
<email>aram@poor.dev</email>
</author>
<published>2026-01-20T09:13:37Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=03c86d09cf301db9a62427170f956a632812d67e'/>
<id>urn:sha1:03c86d09cf301db9a62427170f956a632812d67e</id>
<content type='text'>
* initial implementation

* add borderless to edit and plugin cli commands

* allow changing existing pane to borderless

* allow setting in layouts and keybindings

* get to work through the existing plugin api commands

* fix when applying (swap) layouts

* fix tests

* allow setting pane as bordered (borderful?) when changing its coordinates (eg. through the cli)

* allow explicitly setting a pane as bordered or borderless without toggling

* rustfmt

* adjust snapshots

* docs(changelog): add PR</content>
</entry>
<entry>
<title>Fix: allow 0% coordinate specification for floating panes (#4609)</title>
<updated>2026-01-15T13:10:59Z</updated>
<author>
<name>Aram Drevekenin</name>
<email>aram@poor.dev</email>
</author>
<published>2026-01-15T13:10:59Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=005769cfa58b65cc6b2d6ce273ad367bf802bfb0'/>
<id>urn:sha1:005769cfa58b65cc6b2d6ce273ad367bf802bfb0</id>
<content type='text'>
* fix: allow 0% when specifying x/y coordinates but not when specifying width/height

* rustfmt

* add pr</content>
</entry>
</feed>
