aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-client
diff options
context:
space:
mode:
authordivens <divens@users.noreply.github.com>2026-03-04 07:29:16 +0100
committerGitHub <noreply@github.com>2026-03-04 07:29:16 +0100
commitf6bf1af1cef0138d42429ddcb1246ae4f2c6216b (patch)
treed2c2caefa891cd33ec79906f4892ff61c89f3294 /zellij-client
parentVisual indication for terminal bells (#4781) (diff)
downloadsl0ppij-f6bf1af1cef0138d42429ddcb1246ae4f2c6216b.tar.gz
sl0ppij-f6bf1af1cef0138d42429ddcb1246ae4f2c6216b.tar.xz
sl0ppij-f6bf1af1cef0138d42429ddcb1246ae4f2c6216b.zip
[Windows port PR8] feature: add Windows support (#4768)
* 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" -> "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 <divens.dev@gmail.com>
Diffstat (limited to 'zellij-client')
-rw-r--r--zellij-client/Cargo.toml10
-rw-r--r--zellij-client/src/input_handler.rs7
-rw-r--r--zellij-client/src/lib.rs40
-rw-r--r--zellij-client/src/os_input_output.rs56
-rw-r--r--zellij-client/src/os_input_output_unix.rs39
-rw-r--r--zellij-client/src/os_input_output_windows.rs218
-rw-r--r--zellij-client/src/remote_attach/unit/remote_attach_tests.rs8
-rw-r--r--zellij-client/src/stdin_handler.rs133
-rw-r--r--zellij-client/src/stdin_handler_windows.rs64
-rw-r--r--zellij-client/src/unit/terminal_loop_tests.rs8
-rw-r--r--zellij-client/src/web_client/ipc_listener.rs14
-rw-r--r--zellij-client/src/web_client/mod.rs77
-rw-r--r--zellij-client/src/web_client/unit/web_client_tests.rs8
13 files changed, 606 insertions, 76 deletions
diff --git a/zellij-client/Cargo.toml b/zellij-client/Cargo.toml
index 7bb51e83..d8cb8f2f 100644
--- a/zellij-client/Cargo.toml
+++ b/zellij-client/Cargo.toml
@@ -21,7 +21,7 @@ tower-http = { version = "0.6.4", features = ["cors"], optional = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
-crossterm = { workspace = true }
+crossterm = { workspace = true, features = ["events", "bracketed-paste"] }
dialoguer = { workspace = true }
interprocess = { workspace = true }
lazy_static = { workspace = true }
@@ -51,6 +51,14 @@ signal-hook = { workspace = true }
daemonize = { workspace = true }
nix = { workspace = true }
+[target.'cfg(windows)'.dependencies]
+crossterm = { workspace = true }
+tokio = { workspace = true }
+windows-sys = { workspace = true, features = [
+ "Win32_Foundation",
+ "Win32_System_Console",
+] }
+
[dev-dependencies]
insta = "1.6.0"
serde_json = "1.0"
diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs
index 271eb33f..ae7b3210 100644
--- a/zellij-client/src/input_handler.rs
+++ b/zellij-client/src/input_handler.rs
@@ -229,10 +229,13 @@ impl InputHandler {
}
},
Ok((
- InputInstruction::KeyWithModifierEvent(key_with_modifier, raw_bytes),
+ InputInstruction::KeyWithModifierEvent(key_with_modifier, raw_bytes, is_kitty),
_error_context,
)) => {
- self.handle_key(&key_with_modifier, raw_bytes, true);
+ self.handle_key(&key_with_modifier, raw_bytes, is_kitty);
+ },
+ Ok((InputInstruction::MouseEvent(mouse_event), _error_context)) => {
+ self.handle_mouse_event(&mouse_event);
},
Ok((
InputInstruction::AnsiStdinInstructions(ansi_stdin_instructions),
diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs
index 4447c191..817a7338 100644
--- a/zellij-client/src/lib.rs
+++ b/zellij-client/src/lib.rs
@@ -16,6 +16,8 @@ pub mod old_config_converter;
pub mod remote_attach;
mod stdin_ansi_parser;
mod stdin_handler;
+#[cfg(windows)]
+mod stdin_handler_windows;
#[cfg(feature = "web_server_capability")]
pub mod web_client;
@@ -255,15 +257,18 @@ fn spawn_web_server(_cli_args: &CliArgs) -> Result<String, String> {
Ok("".to_owned())
}
+/// Spawn the Zellij server process.
+///
+/// On Unix the server daemonizes (double-fork) inside start_server(), so
+/// the intermediate child exits immediately and `cmd.status()` returns.
+#[cfg(not(windows))]
pub fn spawn_server(socket_path: &Path, debug: bool) -> io::Result<()> {
let mut cmd = Command::new(current_exe()?);
- cmd.arg("--server");
- cmd.arg(socket_path);
+ cmd.arg("--server").arg(socket_path);
if debug {
cmd.arg("--debug");
}
let status = cmd.status()?;
-
if status.success() {
Ok(())
} else {
@@ -276,6 +281,28 @@ pub fn spawn_server(socket_path: &Path, debug: bool) -> io::Result<()> {
}
}
+/// Spawn the Zellij server process.
+///
+/// On Windows there is no daemonize — we launch the server as a background
+/// process with a hidden console. We use CREATE_NO_WINDOW (not
+/// DETACHED_PROCESS) so the server gets valid standard handles;
+/// DETACHED_PROCESS leaves stdin/stdout/stderr as NULL, which breaks PTY
+/// creation, WASM plugin loading, and logging.
+#[cfg(windows)]
+pub fn spawn_server(socket_path: &Path, debug: bool) -> io::Result<()> {
+ use std::os::windows::process::CommandExt;
+ let mut cmd = Command::new(current_exe()?);
+ cmd.arg("--server").arg(socket_path);
+ if debug {
+ cmd.arg("--debug");
+ }
+ const CREATE_NO_WINDOW: u32 = 0x08000000;
+ const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
+ cmd.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP);
+ cmd.spawn()?;
+ Ok(())
+}
+
#[derive(Debug, Clone)]
pub enum ClientInfo {
Attach(String, Options),
@@ -311,7 +338,8 @@ impl ClientInfo {
#[derive(Debug, Clone)]
pub(crate) enum InputInstruction {
KeyEvent(InputEvent, Vec<u8>),
- KeyWithModifierEvent(KeyWithModifier, Vec<u8>),
+ KeyWithModifierEvent(KeyWithModifier, Vec<u8>, bool), // bool = is_kitty_keyboard_protocol
+ MouseEvent(zellij_utils::input::mouse::MouseEvent),
AnsiStdinInstructions(Vec<AnsiStdinInstruction>),
StartedParsing,
DoneParsing,
@@ -870,6 +898,8 @@ pub fn start_client(
let on_force_close = config_options.on_force_close.unwrap_or_default();
let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new()));
+ let (resize_sender, resize_receiver) = std::sync::mpsc::channel::<()>();
+
let _stdin_thread = thread::Builder::new()
.name("stdin_handler".to_string())
.spawn({
@@ -882,6 +912,7 @@ pub fn start_client(
send_input_instructions,
stdin_ansi_parser,
explicitly_disable_kitty_keyboard_protocol,
+ Some(resize_sender),
)
}
});
@@ -931,6 +962,7 @@ pub fn start_client(
});
}
}),
+ Some(resize_receiver),
);
}
})
diff --git a/zellij-client/src/os_input_output.rs b/zellij-client/src/os_input_output.rs
index ee29403f..2522322b 100644
--- a/zellij-client/src/os_input_output.rs
+++ b/zellij-client/src/os_input_output.rs
@@ -1,14 +1,18 @@
-use anyhow::{Context, Result};
+use anyhow::Result;
use async_trait::async_trait;
-use interprocess;
use zellij_utils::pane_size::Size;
#[cfg(not(windows))]
-use crate::os_input_output_unix::{AsyncSignalListener, BlockingSignalIterator};
+use crate::os_input_output_unix::{
+ disable_mouse_support, enable_mouse_support, setup_ipc, AsyncSignalListener,
+ BlockingSignalIterator,
+};
#[cfg(windows)]
-use crate::os_input_output_windows::{AsyncSignalListener, BlockingSignalIterator};
+use crate::os_input_output_windows::{
+ disable_mouse_support, enable_mouse_support, setup_ipc, AsyncSignalListener,
+ BlockingSignalIterator,
+};
-use interprocess::local_socket::{prelude::*, GenericFilePath, Stream as LocalSocketStream};
use std::io::prelude::*;
use std::io::IsTerminal;
use std::path::Path;
@@ -23,9 +27,9 @@ use zellij_utils::{
const SIGWINCH_CB_THROTTLE_DURATION: time::Duration = time::Duration::from_millis(50);
-const ENABLE_MOUSE_SUPPORT: &str =
+pub(crate) const ENABLE_MOUSE_SUPPORT: &str =
"\u{1b}[?1000h\u{1b}[?1002h\u{1b}[?1003h\u{1b}[?1015h\u{1b}[?1006h";
-const DISABLE_MOUSE_SUPPORT: &str =
+pub(crate) const DISABLE_MOUSE_SUPPORT: &str =
"\u{1b}[?1006l\u{1b}[?1015l\u{1b}[?1003l\u{1b}[?1002l\u{1b}[?1000l";
/// Trait for async stdin reading, allowing for testable implementations
@@ -125,7 +129,12 @@ pub trait ClientOsApi: Send + Sync + std::fmt::Debug {
/// Receives a message on client-side IPC channel
// This should be called from the client-side router thread only.
fn recv_from_server(&self) -> Option<(ServerToClientMsg, ErrorContext)>;
- fn handle_signals(&self, sigwinch_cb: Box<dyn Fn()>, quit_cb: Box<dyn Fn()>);
+ fn handle_signals(
+ &self,
+ sigwinch_cb: Box<dyn Fn()>,
+ quit_cb: Box<dyn Fn()>,
+ resize_receiver: Option<std::sync::mpsc::Receiver<()>>,
+ );
/// Establish a connection with the server socket.
fn connect_to_server(&self, path: &Path);
fn load_palette(&self) -> Palette;
@@ -236,9 +245,14 @@ impl ClientOsApi for ClientOsInputOutput {
.unwrap()
.recv_server_msg()
}
- fn handle_signals(&self, sigwinch_cb: Box<dyn Fn()>, quit_cb: Box<dyn Fn()>) {
+ fn handle_signals(
+ &self,
+ sigwinch_cb: Box<dyn Fn()>,
+ quit_cb: Box<dyn Fn()>,
+ resize_receiver: Option<std::sync::mpsc::Receiver<()>>,
+ ) {
let mut sigwinch_cb_timestamp = time::Instant::now();
- let signals = BlockingSignalIterator::new().unwrap();
+ let signals = BlockingSignalIterator::new(resize_receiver).unwrap();
for event in signals {
match event {
SignalEvent::Resize => {
@@ -257,12 +271,9 @@ impl ClientOsApi for ClientOsInputOutput {
}
}
fn connect_to_server(&self, path: &Path) {
- let fs_name = path
- .to_fs_name::<GenericFilePath>()
- .expect("failed to convert path to socket name");
let socket;
loop {
- match LocalSocketStream::connect(fs_name.clone()) {
+ match zellij_utils::consts::ipc_connect(path) {
Ok(sock) => {
socket = sock;
break;
@@ -272,8 +283,7 @@ impl ClientOsApi for ClientOsInputOutput {
},
}
}
- let sender = IpcSenderWithContext::new(socket);
- let receiver = sender.get_receiver();
+ let (sender, receiver) = setup_ipc(socket, path);
*self.send_instructions_to_server.lock().unwrap() = Some(sender);
*self.receive_instructions_from_server.lock().unwrap() = Some(receiver);
}
@@ -292,23 +302,13 @@ impl ClientOsApi for ClientOsInputOutput {
default_palette()
}
fn enable_mouse(&self) -> Result<()> {
- let err_context = "failed to enable mouse mode";
let mut stdout = self.get_stdout_writer();
- stdout
- .write_all(ENABLE_MOUSE_SUPPORT.as_bytes())
- .context(err_context)?;
- stdout.flush().context(err_context)?;
- Ok(())
+ enable_mouse_support(&mut *stdout)
}
fn disable_mouse(&self) -> Result<()> {
- let err_context = "failed to enable mouse mode";
let mut stdout = self.get_stdout_writer();
- stdout
- .write_all(DISABLE_MOUSE_SUPPORT.as_bytes())
- .context(err_context)?;
- stdout.flush().context(err_context)?;
- Ok(())
+ disable_mouse_support(&mut *stdout)
}
fn env_variable(&self, name: &str) -> Option<String> {
diff --git a/zellij-client/src/os_input_output_unix.rs b/zellij-client/src/os_input_output_unix.rs
index d087eb7d..cf71b418 100644
--- a/zellij-client/src/os_input_output_unix.rs
+++ b/zellij-client/src/os_input_output_unix.rs
@@ -5,7 +5,11 @@ use signal_hook::consts::signal::*;
use signal_hook::iterator::Signals;
use tokio::signal::unix::{signal, SignalKind};
+use anyhow::{Context, Result};
use std::io;
+use std::io::Write;
+use std::path::Path;
+use zellij_utils::ipc::{IpcReceiverWithContext, IpcSenderWithContext};
/// Async signal listener that maps Unix signals to `SignalEvent` variants.
pub(crate) struct AsyncSignalListener {
@@ -48,7 +52,7 @@ pub(crate) struct BlockingSignalIterator {
}
impl BlockingSignalIterator {
- pub fn new() -> io::Result<Self> {
+ pub fn new(_resize_receiver: Option<std::sync::mpsc::Receiver<()>>) -> io::Result<Self> {
let signals = Signals::new([SIGWINCH, SIGTERM, SIGINT, SIGQUIT, SIGHUP])?;
Ok(Self { signals })
}
@@ -68,3 +72,36 @@ impl Iterator for BlockingSignalIterator {
None
}
}
+
+/// Set up client IPC channels from a connected socket.
+///
+/// On Unix a single socket is cloned for both send and receive directions.
+pub(crate) fn setup_ipc(
+ socket: interprocess::local_socket::Stream,
+ _path: &Path,
+) -> (
+ IpcSenderWithContext<zellij_utils::ipc::ClientToServerMsg>,
+ IpcReceiverWithContext<zellij_utils::ipc::ServerToClientMsg>,
+) {
+ let sender = IpcSenderWithContext::new(socket);
+ let receiver = sender.get_receiver();
+ (sender, receiver)
+}
+
+pub(crate) fn enable_mouse_support(stdout: &mut dyn Write) -> Result<()> {
+ let err_context = "failed to enable mouse mode";
+ stdout
+ .write_all(super::os_input_output::ENABLE_MOUSE_SUPPORT.as_bytes())
+ .context(err_context)?;
+ stdout.flush().context(err_context)?;
+ Ok(())
+}
+
+pub(crate) fn disable_mouse_support(stdout: &mut dyn Write) -> Result<()> {
+ let err_context = "failed to disable mouse mode";
+ stdout
+ .write_all(super::os_input_output::DISABLE_MOUSE_SUPPORT.as_bytes())
+ .context(err_context)?;
+ stdout.flush().context(err_context)?;
+ Ok(())
+}
diff --git a/zellij-client/src/os_input_output_windows.rs b/zellij-client/src/os_input_output_windows.rs
index bfced557..d71bc279 100644
--- a/zellij-client/src/os_input_output_windows.rs
+++ b/zellij-client/src/os_input_output_windows.rs
@@ -1,31 +1,115 @@
use crate::os_input_output::SignalEvent;
+use anyhow::{Context, Result};
use async_trait::async_trait;
use std::io;
+use std::io::Write;
+use std::path::Path;
+use zellij_utils::ipc::{IpcReceiverWithContext, IpcSenderWithContext};
-/// Windows async signal listener stub. Not yet implemented.
-pub(crate) struct AsyncSignalListener;
+/// Windows async signal listener.
+///
+/// Polls `crossterm::terminal::size()` at 100ms intervals for resize events,
+/// and listens to `tokio::signal::windows` for ctrl_c/ctrl_break/ctrl_close.
+pub(crate) struct AsyncSignalListener {
+ interval: tokio::time::Interval,
+ last_size: (u16, u16),
+ ctrl_c: tokio::signal::windows::CtrlC,
+ ctrl_break: tokio::signal::windows::CtrlBreak,
+ ctrl_close: tokio::signal::windows::CtrlClose,
+}
impl AsyncSignalListener {
pub fn new() -> io::Result<Self> {
- unimplemented!("Windows AsyncSignalListener not yet implemented")
+ let size = crossterm::terminal::size().unwrap_or((80, 24));
+ Ok(Self {
+ interval: tokio::time::interval(std::time::Duration::from_millis(100)),
+ last_size: size,
+ ctrl_c: tokio::signal::windows::ctrl_c()?,
+ ctrl_break: tokio::signal::windows::ctrl_break()?,
+ ctrl_close: tokio::signal::windows::ctrl_close()?,
+ })
}
}
#[async_trait]
impl crate::os_input_output::AsyncSignals for AsyncSignalListener {
async fn recv(&mut self) -> Option<SignalEvent> {
- unimplemented!("Windows AsyncSignalListener not yet implemented")
+ loop {
+ tokio::select! {
+ _ = self.interval.tick() => {
+ if let Ok(new_size) = crossterm::terminal::size() {
+ if new_size != self.last_size {
+ self.last_size = new_size;
+ return Some(SignalEvent::Resize);
+ }
+ }
+ }
+ result = self.ctrl_c.recv() => {
+ return result.map(|_| SignalEvent::Quit);
+ }
+ result = self.ctrl_break.recv() => {
+ return result.map(|_| SignalEvent::Quit);
+ }
+ result = self.ctrl_close.recv() => {
+ return result.map(|_| SignalEvent::Quit);
+ }
+ }
+ }
}
}
-/// Windows blocking signal iterator stub. Not yet implemented.
-pub(crate) struct BlockingSignalIterator;
+/// Windows blocking signal iterator.
+///
+/// Uses `SetConsoleCtrlHandler` with an `AtomicBool` for quit signals.
+/// For resize detection, operates in two modes:
+/// - **Channel mode**: receives resize notifications forwarded from the stdin
+/// thread (which gets `Event::Resize` from crossterm). Much more responsive
+/// than polling.
+/// - **Poll fallback**: polls `crossterm::terminal::size()` at 50ms intervals.
+/// Used when no receiver is provided or when the sender is dropped (VT reader
+/// path).
+pub(crate) struct BlockingSignalIterator {
+ last_size: (u16, u16),
+ resize_receiver: Option<std::sync::mpsc::Receiver<()>>,
+}
+
+mod win_ctrl_handler {
+ use std::sync::atomic::{AtomicBool, Ordering};
+
+ use windows_sys::Win32::Foundation::BOOL;
+ use windows_sys::Win32::System::Console::{CTRL_BREAK_EVENT, CTRL_CLOSE_EVENT, CTRL_C_EVENT};
+
+ pub static CTRL_QUIT_RECEIVED: AtomicBool = AtomicBool::new(false);
+
+ pub unsafe extern "system" fn ctrl_handler(ctrl_type: u32) -> BOOL {
+ match ctrl_type {
+ CTRL_C_EVENT | CTRL_BREAK_EVENT | CTRL_CLOSE_EVENT => {
+ CTRL_QUIT_RECEIVED.store(true, Ordering::SeqCst);
+ 1 // TRUE — handled
+ },
+ _ => 0, // FALSE — not handled
+ }
+ }
+}
impl BlockingSignalIterator {
- pub fn new() -> io::Result<Self> {
- unimplemented!("Windows BlockingSignalIterator not yet implemented")
+ pub fn new(resize_receiver: Option<std::sync::mpsc::Receiver<()>>) -> io::Result<Self> {
+ use windows_sys::Win32::System::Console::SetConsoleCtrlHandler;
+
+ win_ctrl_handler::CTRL_QUIT_RECEIVED.store(false, std::sync::atomic::Ordering::SeqCst);
+
+ let ok = unsafe { SetConsoleCtrlHandler(Some(win_ctrl_handler::ctrl_handler), 1) };
+ if ok == 0 {
+ return Err(io::Error::last_os_error());
+ }
+
+ let size = crossterm::terminal::size().unwrap_or((80, 24));
+ Ok(Self {
+ last_size: size,
+ resize_receiver,
+ })
}
}
@@ -33,6 +117,122 @@ impl Iterator for BlockingSignalIterator {
type Item = SignalEvent;
fn next(&mut self) -> Option<SignalEvent> {
- unimplemented!("Windows BlockingSignalIterator not yet implemented")
+ use std::sync::mpsc::RecvTimeoutError;
+ use std::time::Duration;
+
+ loop {
+ if win_ctrl_handler::CTRL_QUIT_RECEIVED.load(std::sync::atomic::Ordering::SeqCst) {
+ return Some(SignalEvent::Quit);
+ }
+
+ if let Some(ref rx) = self.resize_receiver {
+ // Channel mode: the native console stdin loop sends resize
+ // notifications through this channel. Block with a timeout so
+ // we can periodically check the quit flag above.
+ match rx.recv_timeout(Duration::from_millis(100)) {
+ Ok(()) => return Some(SignalEvent::Resize),
+ Err(RecvTimeoutError::Timeout) => continue,
+ Err(RecvTimeoutError::Disconnected) => {
+ // Sender dropped (VT reader path) — switch to poll mode.
+ self.resize_receiver = None;
+ continue;
+ },
+ }
+ } else {
+ // Poll mode: used on the VT reader path where crossterm's
+ // event::read() isn't used, so resize events don't come through
+ // the channel. Periodically compare the terminal size instead.
+ if let Ok(new_size) = crossterm::terminal::size() {
+ if new_size != self.last_size {
+ self.last_size = new_size;
+ return Some(SignalEvent::Resize);
+ }
+ }
+ std::thread::sleep(Duration::from_millis(50));
+ }
+ }
+ }
+}
+
+/// Set up client IPC channels from a connected socket.
+///
+/// On Windows we use two separate named pipes to avoid DuplicateHandle
+/// deadlock: the command pipe (socket) for client→server, and a reply pipe
+/// for server→client.
+pub(crate) fn setup_ipc(
+ socket: interprocess::local_socket::Stream,
+ path: &Path,
+) -> (
+ IpcSenderWithContext<zellij_utils::ipc::ClientToServerMsg>,
+ IpcReceiverWithContext<zellij_utils::ipc::ServerToClientMsg>,
+) {
+ let reply_socket;
+ loop {
+ match zellij_utils::consts::ipc_connect_reply(path) {
+ Ok(sock) => {
+ reply_socket = sock;
+ break;
+ },
+ Err(_) => {
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ },
+ }
+ }
+ let sender = IpcSenderWithContext::new(socket);
+ let receiver = IpcReceiverWithContext::new(reply_socket);
+ (sender, receiver)
+}
+
+/// Enable ENABLE_VIRTUAL_TERMINAL_PROCESSING on stdout so that ConPTY enters
+/// passthrough mode and forwards DEC private mode sequences (like mouse-enable)
+/// to the terminal emulator. Uses crossterm's safe wrapper which handles the
+/// GetConsoleMode/SetConsoleMode internally.
+fn enable_vt_processing_on_stdout() {
+ crossterm::ansi_support::supports_ansi();
+}
+
+/// Enable mouse support on Windows.
+///
+/// When TERM is set we're on the VT input path (terminal emulator like
+/// Alacritty via ConPTY). We must NOT use crossterm's EnableMouseCapture
+/// because it does a full SetConsoleMode() that would overwrite the mode
+/// set by enable_vt_input(), clobbering ENABLE_VIRTUAL_TERMINAL_INPUT.
+///
+/// Instead, we enable ENABLE_VIRTUAL_TERMINAL_PROCESSING on stdout so
+/// ConPTY enters passthrough mode, then write ANSI mouse-enable sequences.
+///
+/// When TERM is not set we're in a native console (cmd, PowerShell,
+/// Windows Terminal) and use crossterm's Console API approach.
+pub(crate) fn enable_mouse_support(stdout: &mut dyn Write) -> Result<()> {
+ let err_context = "failed to enable mouse mode";
+ if std::env::var("TERM").is_ok() {
+ enable_vt_processing_on_stdout();
+ stdout
+ .write_all(super::os_input_output::ENABLE_MOUSE_SUPPORT.as_bytes())
+ .context(err_context)?;
+ stdout.flush().context(err_context)?;
+ } else {
+ // crossterm::execute! requires Sized, so we use std::io::stdout()
+ // directly rather than the trait-object writer.
+ crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture)
+ .context(err_context)?;
+ }
+ Ok(())
+}
+
+/// Disable mouse support on Windows.
+///
+/// See `enable_mouse_support()` for rationale on VT vs Console API paths.
+pub(crate) fn disable_mouse_support(stdout: &mut dyn Write) -> Result<()> {
+ let err_context = "failed to disable mouse mode";
+ if std::env::var("TERM").is_ok() {
+ stdout
+ .write_all(super::os_input_output::DISABLE_MOUSE_SUPPORT.as_bytes())
+ .context(err_context)?;
+ stdout.flush().context(err_context)?;
+ } else {
+ crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture)
+ .context(err_context)?;
}
+ Ok(())
}
diff --git a/zellij-client/src/remote_attach/unit/remote_attach_tests.rs b/zellij-client/src/remote_attach/unit/remote_attach_tests.rs
index 076e9ced..92c13cda 100644
--- a/zellij-client/src/remote_attach/unit/remote_attach_tests.rs
+++ b/zellij-client/src/remote_attach/unit/remote_attach_tests.rs
@@ -268,7 +268,13 @@ impl crate::os_input_output::ClientOsApi for MockClientOsApi {
None
}
- fn handle_signals(&self, _sigwinch_cb: Box<dyn Fn()>, _quit_cb: Box<dyn Fn()>) {}
+ fn handle_signals(
+ &self,
+ _sigwinch_cb: Box<dyn Fn()>,
+ _quit_cb: Box<dyn Fn()>,
+ _resize_receiver: Option<std::sync::mpsc::Receiver<()>>,
+ ) {
+ }
fn connect_to_server(&self, _path: &std::path::Path) {}
diff --git a/zellij-client/src/stdin_handler.rs b/zellij-client/src/stdin_handler.rs
index a7ddf4e2..5eb1add5 100644
--- a/zellij-client/src/stdin_handler.rs
+++ b/zellij-client/src/stdin_handler.rs
@@ -20,14 +20,79 @@ fn send_done_parsing_after_query_timeout(
});
}
+/// On Windows, set the stdin console mode for raw VT input.
+///
+/// Instead of just ORing in ENABLE_VIRTUAL_TERMINAL_INPUT on top of whatever
+/// the current mode happens to be, we explicitly set the exact mode we need.
+/// This avoids a TOCTOU race with crossterm's EnableMouseCapture (which also
+/// does GetConsoleMode/SetConsoleMode) and ensures flags like
+/// ENABLE_QUICK_EDIT_MODE are always cleared — that flag intercepts mouse
+/// events at the console level, breaking application mouse support.
+#[cfg(windows)]
+fn enable_vt_input() -> bool {
+ use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
+ use windows_sys::Win32::System::Console::{
+ GetConsoleMode, GetStdHandle, SetConsoleMode, ENABLE_EXTENDED_FLAGS, ENABLE_MOUSE_INPUT,
+ ENABLE_VIRTUAL_TERMINAL_INPUT, ENABLE_WINDOW_INPUT, STD_INPUT_HANDLE,
+ };
+ unsafe {
+ let handle = GetStdHandle(STD_INPUT_HANDLE);
+ if handle == 0 || handle == INVALID_HANDLE_VALUE {
+ return false;
+ }
+ let mut mode: u32 = 0;
+ if GetConsoleMode(handle, &mut mode) == 0 {
+ return false;
+ }
+ // Explicitly set the mode we need rather than read-modify-write.
+ // This eliminates the race with crossterm's EnableMouseCapture which
+ // also calls GetConsoleMode/SetConsoleMode concurrently.
+ //
+ // Flags we set:
+ // ENABLE_WINDOW_INPUT (0x0008) - receive window resize events
+ // ENABLE_MOUSE_INPUT (0x0010) - receive mouse events; on ConPTY
+ // this signals the terminal emulator
+ // to capture and forward mouse input
+ // ENABLE_EXTENDED_FLAGS (0x0080) - required to clear QUICK_EDIT
+ // ENABLE_VIRTUAL_TERMINAL_INPUT (0x0200) - stdin returns raw VT bytes
+ //
+ // Flags we deliberately clear:
+ // ENABLE_PROCESSED_INPUT (0x0001) - let VT sequences through raw
+ // ENABLE_LINE_INPUT (0x0002) - no line buffering
+ // ENABLE_ECHO_INPUT (0x0004) - no echo
+ // ENABLE_QUICK_EDIT_MODE (0x0040) - would intercept mouse events
+ let new_mode = ENABLE_WINDOW_INPUT
+ | ENABLE_MOUSE_INPUT
+ | ENABLE_EXTENDED_FLAGS
+ | ENABLE_VIRTUAL_TERMINAL_INPUT;
+ if SetConsoleMode(handle, new_mode) == 0 {
+ return false;
+ }
+ true
+ }
+}
+
pub(crate) fn stdin_loop(
mut os_input: Box<dyn ClientOsApi>,
send_input_instructions: SenderWithContext<InputInstruction>,
stdin_ansi_parser: Arc<Mutex<StdinAnsiParser>>,
explicitly_disable_kitty_keyboard_protocol: bool,
+ resize_sender: Option<std::sync::mpsc::Sender<()>>,
) {
- let mut input_parser = InputParser::new();
- let mut current_buffer = vec![];
+ // On Windows, choose between two input strategies early — we need this
+ // decision before the startup ANSI query below.
+ //
+ // 1. Native console (no TERM env var): Use crossterm's event::read() which
+ // reads INPUT_RECORDs via ReadConsoleInput. Works in cmd.exe, PowerShell,
+ // and Windows Terminal where ALT is reported as a modifier flag.
+ //
+ // 2. Terminal emulator (TERM is set, e.g. Alacritty): Enable
+ // ENABLE_VIRTUAL_TERMINAL_INPUT so ReadFile on stdin returns raw VT bytes,
+ // bypassing conpty's lossy VT→INPUT_RECORD translation. Then use the
+ // termwiz byte parser (same as Unix) which understands ESC-prefixed ALT.
+ #[cfg(windows)]
+ let use_vt_reader = std::env::var("TERM").is_ok() && enable_vt_input();
+
{
// on startup we send a query to the terminal emulator for stuff like the pixel size and colors
// we get a response through STDIN, so it makes sense to do this here
@@ -41,23 +106,58 @@ pub(crate) fn stdin_loop(
.unwrap();
},
None => {
- send_input_instructions
- .send(InputInstruction::StartedParsing)
- .unwrap();
- let terminal_emulator_query_string =
- stdin_ansi_parser.terminal_emulator_query_string();
- let _ = os_input
- .get_stdout_writer()
- .write(terminal_emulator_query_string.as_bytes())
- .unwrap();
- let query_duration = stdin_ansi_parser.startup_query_duration();
- send_done_parsing_after_query_timeout(
- send_input_instructions.clone(),
- query_duration,
- );
+ // On Windows native console, the crossterm event::read() loop
+ // reads INPUT_RECORDs via ReadConsoleInput — not raw bytes — so
+ // ANSI query responses can never be read on that path. On the
+ // VT reader path (TERM is set), fill_buf() reads raw VT bytes
+ // just like Unix, so terminal queries work normally.
+ #[cfg(windows)]
+ let can_query_terminal = use_vt_reader;
+ #[cfg(not(windows))]
+ let can_query_terminal = true;
+
+ if can_query_terminal {
+ send_input_instructions
+ .send(InputInstruction::StartedParsing)
+ .unwrap();
+ let terminal_emulator_query_string =
+ stdin_ansi_parser.terminal_emulator_query_string();
+ let _ = os_input
+ .get_stdout_writer()
+ .write(terminal_emulator_query_string.as_bytes())
+ .unwrap();
+ let query_duration = stdin_ansi_parser.startup_query_duration();
+ send_done_parsing_after_query_timeout(
+ send_input_instructions.clone(),
+ query_duration,
+ );
+ } else {
+ let _ = send_input_instructions.send(InputInstruction::DoneParsing);
+ }
},
}
}
+
+ #[cfg(windows)]
+ if !use_vt_reader {
+ crate::stdin_handler_windows::native_console_stdin_loop(
+ send_input_instructions,
+ resize_sender,
+ );
+ return;
+ }
+
+ // Drop the resize sender so the signal handler thread falls back to
+ // polling. Only the Windows native console path (above) keeps it alive;
+ // the VT reader path and Unix don't produce crossterm resize events.
+ drop(resize_sender);
+
+ // Byte reader + termwiz/kitty parser path.
+ // Used on Unix always, and on Windows inside terminal emulators (Alacritty,
+ // etc.) with ENABLE_VIRTUAL_TERMINAL_INPUT enabled so stdin delivers raw VT
+ // byte sequences.
+ let mut input_parser = InputParser::new();
+ let mut current_buffer = vec![];
let mut ansi_stdin_events = vec![];
loop {
match os_input.read_from_stdin() {
@@ -96,6 +196,7 @@ pub(crate) fn stdin_loop(
.send(InputInstruction::KeyWithModifierEvent(
key_with_modifier,
current_buffer.drain(..).collect(),
+ true,
))
.unwrap();
continue;
diff --git a/zellij-client/src/stdin_handler_windows.rs b/zellij-client/src/stdin_handler_windows.rs
new file mode 100644
index 00000000..a2e57d47
--- /dev/null
+++ b/zellij-client/src/stdin_handler_windows.rs
@@ -0,0 +1,64 @@
+use crate::InputInstruction;
+use crossterm::event::{self, Event, KeyEventKind};
+use zellij_utils::channels::SenderWithContext;
+use zellij_utils::input::{cast_crossterm_key, from_crossterm_mouse};
+
+/// Windows native console event loop.
+///
+/// Uses crossterm's `event::read()` which reads INPUT_RECORDs via
+/// ReadConsoleInput. Works in cmd.exe, PowerShell, and Windows Terminal
+/// where ALT is reported as a modifier flag.
+///
+/// Resize events are forwarded to the signal handler thread via `resize_sender`.
+pub(crate) fn native_console_stdin_loop(
+ send_input_instructions: SenderWithContext<InputInstruction>,
+ resize_sender: Option<std::sync::mpsc::Sender<()>>,
+) {
+ loop {
+ match event::read() {
+ Ok(Event::Key(key_event)) => {
+ if key_event.kind != KeyEventKind::Press {
+ continue;
+ }
+ if let Some((key, bytes)) = cast_crossterm_key(key_event) {
+ if send_input_instructions
+ .send(InputInstruction::KeyWithModifierEvent(key, bytes, false))
+ .is_err()
+ {
+ break;
+ }
+ }
+ },
+ Ok(Event::Mouse(mouse_event)) => {
+ let mouse_event = from_crossterm_mouse(mouse_event);
+ if send_input_instructions
+ .send(InputInstruction::MouseEvent(mouse_event))
+ .is_err()
+ {
+ break;
+ }
+ },
+ Ok(Event::Paste(text)) => {
+ let raw_bytes = text.as_bytes().to_vec();
+ let paste_event = termwiz::input::InputEvent::Paste(text);
+ if send_input_instructions
+ .send(InputInstruction::KeyEvent(paste_event, raw_bytes))
+ .is_err()
+ {
+ break;
+ }
+ },
+ Ok(Event::Resize(..)) => {
+ if let Some(ref tx) = resize_sender {
+ let _ = tx.send(());
+ }
+ },
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to read crossterm event: {}", e);
+ let _ = send_input_instructions.send(InputInstruction::Exit);
+ break;
+ },
+ }
+ }
+}
diff --git a/zellij-client/src/unit/terminal_loop_tests.rs b/zellij-client/src/unit/terminal_loop_tests.rs
index e43adb4b..d48da54e 100644
--- a/zellij-client/src/unit/terminal_loop_tests.rs
+++ b/zellij-client/src/unit/terminal_loop_tests.rs
@@ -112,7 +112,13 @@ impl ClientOsApi for TestClientOsApi {
None
}
- fn handle_signals(&self, _sigwinch_cb: Box<dyn Fn()>, _quit_cb: Box<dyn Fn()>) {}
+ fn handle_signals(
+ &self,
+ _sigwinch_cb: Box<dyn Fn()>,
+ _quit_cb: Box<dyn Fn()>,
+ _resize_receiver: Option<std::sync::mpsc::Receiver<()>>,
+ ) {
+ }
fn connect_to_server(&self, _path: &std::path::Path) {}
diff --git a/zellij-client/src/web_client/ipc_listener.rs b/zellij-client/src/web_client/ipc_listener.rs
index 62344ba8..0ba37002 100644
--- a/zellij-client/src/web_client/ipc_listener.rs
+++ b/zellij-client/src/web_client/ipc_listener.rs
@@ -1,8 +1,8 @@
use axum_server::Handle;
+use interprocess::local_socket::traits::tokio::Listener;
use std::net::IpAddr;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
-use tokio::net::{UnixListener, UnixStream};
-use zellij_utils::consts::WEBSERVER_SOCKET_PATH;
+use zellij_utils::consts::{ipc_bind_async, WEBSERVER_SOCKET_PATH};
use zellij_utils::prost::Message;
use zellij_utils::web_server_commands::{InstructionForWebServer, VersionInfo, WebServerResponse};
use zellij_utils::web_server_contract::web_server_contract::InstructionForWebServer as ProtoInstructionForWebServer;
@@ -10,7 +10,7 @@ use zellij_utils::web_server_contract::web_server_contract::WebServerResponse as
pub async fn create_webserver_receiver(
id: &str,
-) -> Result<UnixStream, Box<dyn std::error::Error + Send + Sync>> {
+) -> Result<interprocess::local_socket::tokio::Stream, Box<dyn std::error::Error + Send + Sync>> {
std::fs::create_dir_all(&WEBSERVER_SOCKET_PATH.as_path())?;
let socket_path = WEBSERVER_SOCKET_PATH.join(format!("{}", id));
@@ -18,13 +18,13 @@ pub async fn create_webserver_receiver(
tokio::fs::remove_file(&socket_path).await?;
}
- let listener = UnixListener::bind(&socket_path)?;
- let (stream, _) = listener.accept().await?;
+ let listener = ipc_bind_async(&socket_path)?;
+ let stream = listener.accept().await?;
Ok(stream)
}
pub async fn receive_webserver_instruction(
- receiver: &mut UnixStream,
+ receiver: &mut interprocess::local_socket::tokio::Stream,
) -> std::io::Result<InstructionForWebServer> {
// Read length prefix (4 bytes)
let mut len_bytes = [0u8; 4];
@@ -46,7 +46,7 @@ pub async fn receive_webserver_instruction(
}
pub async fn send_webserver_response(
- sender: &mut UnixStream,
+ sender: &mut interprocess::local_socket::tokio::Stream,
response: WebServerResponse,
) -> std::io::Result<()> {
let proto_response: ProtoWebServerResponse = response.into();
diff --git a/zellij-client/src/web_client/mod.rs b/zellij-client/src/web_client/mod.rs
index 958de439..19a1c422 100644
--- a/zellij-client/src/web_client/mod.rs
+++ b/zellij-client/src/web_client/mod.rs
@@ -54,6 +54,8 @@ use utils::should_use_https;
use uuid::Uuid;
use websocket_handlers::{ws_handler_control, ws_handler_terminal};
+const DEFAULT_SERVER_STARTUP_TIMEOUT_SECS: u64 = 10;
+
pub fn start_web_client(
config: Config,
config_options: Options,
@@ -63,6 +65,7 @@ pub fn start_web_client(
custom_port: Option<u16>,
custom_server_cert: Option<PathBuf>,
custom_server_key: Option<PathBuf>,
+ startup_timeout: Option<u64>,
) {
std::panic::set_hook({
Box::new(move |info| {
@@ -108,6 +111,8 @@ pub fn start_web_client(
web_server_port,
web_server_cert,
web_server_key,
+ config_file_path.clone(),
+ startup_timeout,
)
} else {
let runtime = Runtime::new().unwrap();
@@ -258,6 +263,8 @@ fn daemonize_web_server(
web_server_port: u16,
web_server_cert: Option<PathBuf>,
web_server_key: Option<PathBuf>,
+ _config_file_path: Option<PathBuf>,
+ _startup_timeout: Option<u64>,
) -> (Runtime, std::net::TcpListener, Option<RustlsConfig>) {
let (mut exit_message_tx, exit_message_rx) = pipe().unwrap();
let (mut exit_status_tx, mut exit_status_rx) = pipe().unwrap();
@@ -356,12 +363,72 @@ fn daemonize_web_server(
#[cfg(not(unix))]
fn daemonize_web_server(
- _web_server_ip: IpAddr,
- _web_server_port: u16,
- _web_server_cert: Option<PathBuf>,
- _web_server_key: Option<PathBuf>,
+ web_server_ip: IpAddr,
+ web_server_port: u16,
+ web_server_cert: Option<PathBuf>,
+ web_server_key: Option<PathBuf>,
+ config_file_path: Option<PathBuf>,
+ startup_timeout: Option<u64>,
) -> (Runtime, std::net::TcpListener, Option<RustlsConfig>) {
- unimplemented!("Non-Unix web server not yet implemented")
+ use std::env::current_exe;
+ use std::net::TcpStream;
+ use std::process::{exit, Command};
+ use std::time::{Duration, Instant};
+
+ let exe = current_exe().unwrap_or_else(|e| {
+ eprintln!("Failed to determine executable path: {}", e);
+ exit(2);
+ });
+
+ let mut cmd = Command::new(&exe);
+ if let Some(ref config_path) = config_file_path {
+ cmd.arg("--config").arg(config_path);
+ }
+ cmd.arg("web").arg("--start");
+ cmd.arg("--ip").arg(web_server_ip.to_string());
+ cmd.arg("--port").arg(web_server_port.to_string());
+ if let Some(ref cert) = web_server_cert {
+ cmd.arg("--cert").arg(cert);
+ }
+ if let Some(ref key) = web_server_key {
+ cmd.arg("--key").arg(key);
+ }
+
+ #[cfg(windows)]
+ {
+ use std::os::windows::process::CommandExt;
+ const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
+ const CREATE_NO_WINDOW: u32 = 0x08000000;
+ cmd.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP);
+ }
+
+ match cmd.spawn() {
+ Ok(_child) => {
+ let timeout_secs = startup_timeout.unwrap_or(DEFAULT_SERVER_STARTUP_TIMEOUT_SECS);
+ let deadline = Instant::now() + Duration::from_secs(timeout_secs);
+ let addr = format!("{}:{}", web_server_ip, web_server_port);
+ loop {
+ if TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_millis(200))
+ .is_ok()
+ {
+ println!(
+ "Web Server started on {} port {}",
+ web_server_ip, web_server_port
+ );
+ exit(0);
+ }
+ if Instant::now() > deadline {
+ eprintln!("Timed out waiting for web server to start on {}", addr);
+ exit(2);
+ }
+ std::thread::sleep(Duration::from_millis(100));
+ }
+ },
+ Err(e) => {
+ eprintln!("Failed to spawn web server: {}", e);
+ exit(2);
+ },
+ }
}
#[cfg(test)]
diff --git a/zellij-client/src/web_client/unit/web_client_tests.rs b/zellij-client/src/web_client/unit/web_client_tests.rs
index 91daa699..542a7865 100644
--- a/zellij-client/src/web_client/unit/web_client_tests.rs
+++ b/zellij-client/src/web_client/unit/web_client_tests.rs
@@ -2258,7 +2258,13 @@ impl ClientOsApi for MockClientOsApi {
}
msg
}
- fn handle_signals(&self, _sigwinch_cb: Box<dyn Fn()>, _quit_cb: Box<dyn Fn()>) {}
+ fn handle_signals(
+ &self,
+ _sigwinch_cb: Box<dyn Fn()>,
+ _quit_cb: Box<dyn Fn()>,
+ _resize_receiver: Option<std::sync::mpsc::Receiver<()>>,
+ ) {
+ }
fn connect_to_server(&self, _path: &std::path::Path) {}
fn load_palette(&self) -> Palette {
Palette::default()