use crate::os_input_output::SignalEvent; use crate::stdin_handler_windows::restore_vt_input; 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}; /// Whether zellij should use the VT byte path on Windows (raw stdin via /// `ReadFile` + termwiz/kitty parsing) instead of the native-console path /// (crossterm `INPUT_RECORD`s). /// /// True when either of these environment variables is set: /// /// * `TERM` — set automatically by terminal emulators that expose a Unix-style /// environment (Alacritty, WezTerm, WSL sessions inheriting bash's env), or /// set manually by users who want zellij on the VT path (e.g. adding /// `TERM=xterm-256color` to a Windows Terminal / PowerShell profile). /// * `WT_SESSION` — set per-session by Windows Terminal itself. Detecting it /// means default WT installs land on the VT path without the user having to /// set `TERM`, so they get bracketed paste, kitty keyboard, SGR mouse, and /// the other features that depend on raw VT input. /// /// Every VT-path-conditional code site (stdin handler, mouse mode setup, /// future input-related code) should call this so the gates can't drift — /// when they did drift, mouse setup using `crossterm::EnableMouseCapture` /// clobbered the `ENABLE_VIRTUAL_TERMINAL_INPUT` flag the stdin path had /// just set and broke all input from WT. pub(crate) fn use_vt_path() -> bool { std::env::var("TERM").is_ok() || std::env::var("WT_SESSION").is_ok() } /// 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 { 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 { 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. /// /// 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>, } 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(resize_receiver: Option>) -> io::Result { 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, }) } } impl Iterator for BlockingSignalIterator { type Item = SignalEvent; fn next(&mut self) -> Option { 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, IpcReceiverWithContext, ) { 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 use_vt_path() { 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(()) } /// Restore the console input mode to its pre-Zellij state. /// /// On the VT path, `enable_vt_input()` sets ENABLE_MOUSE_INPUT and /// ENABLE_VIRTUAL_TERMINAL_INPUT on the console handle, but crossterm's /// `disable_raw_mode()` never clears them. This function restores the /// original console mode saved before those flags were set. pub(crate) fn restore_console_mode() { restore_vt_input(); } /// 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 use_vt_path() { 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(()) }