From 59b909efb8f5c3bb44c5dad4ab2ced0c5311ad38 Mon Sep 17 00:00:00 2001 From: Bjorn Blomberg Date: Tue, 14 Jul 2026 19:38:56 +0200 Subject: [PATCH] Cross-platform IPC via interprocess + Windows support fixes - Replace Unix-socket-only server/client with interprocess local sockets: unchanged unix domain socket on Linux (SOCKET_API compatible), named pipes on Windows - Run command lines with arguments via sh -c / cmd /C in the PTY - Wire RunProgram args into the spawned command - Make libc a unix-only dependency - Skip Kitty background image on Windows (protocol unsupported) - Drop dead pixel-size query code in kitty_gfx - Reject left/right panel positions at config parse instead of silently ignoring them Co-Authored-By: Claude Fable 5 --- Cargo.lock | 32 ++++++++++++++++++++++++++++++++ Cargo.toml | 3 +++ src/app.rs | 10 ++++++++-- src/client.rs | 12 +++++++----- src/config.rs | 2 -- src/ipc.rs | 26 ++++++++++++++++++++++++++ src/kitty_gfx.rs | 22 ---------------------- src/main.rs | 5 +++++ src/pty.rs | 26 +++++++++++++++++++++++++- src/server.rs | 31 ++++++++++++++++++++----------- 10 files changed, 126 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 532360e..4e2ea7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,6 +331,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "doctest-file" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2db04e74f0a9a93103b50e90b96024c9b2bdca8bce6a632ec71b88736d3d359" + [[package]] name = "document-features" version = "0.2.12" @@ -614,6 +620,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "interprocess" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069323743400cb7ab06a8fe5c1ed911d36b6919ec531661d034c89083629595b" +dependencies = [ + "doctest-file", + "libc", + "recvmsg", + "widestring", + "windows-sys", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1225,6 +1244,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1694,6 +1719,7 @@ dependencies = [ "anyhow", "crossterm", "image", + "interprocess", "libc", "portable-pty", "ratatui", @@ -1937,6 +1963,12 @@ dependencies = [ "wezterm-dynamic", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 83cd79c..7a3683e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,7 @@ portable-pty = "0.9" vt100 = "0.16" anyhow = "1" image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "bmp", "webp"] } +interprocess = "2.4" + +[target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/src/app.rs b/src/app.rs index 2575239..18a80c6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -477,7 +477,6 @@ impl App { bottom_used += 1; Rect::new(area.x, area.y + area.height.saturating_sub(bottom_used), area.width, 1) } - PanelPosition::Left | PanelPosition::Right => Rect::default(), }; let mut item_rects = Vec::new(); @@ -1257,7 +1256,14 @@ impl App { self.spawn_terminal(&shell); } MenuAction::RunScript { path } => { self.spawn_terminal(&path); } - MenuAction::RunProgram { command, .. } => { self.spawn_terminal(&command); } + MenuAction::RunProgram { command, args } => { + let cmdline = if args.is_empty() { + command + } else { + format!("{} {}", command, args.join(" ")) + }; + self.spawn_terminal(&cmdline); + } MenuAction::SpawnRunDialog => self.spawn_run_dialog(), MenuAction::Submenu { .. } => {} MenuAction::NoOp => {} diff --git a/src/client.rs b/src/client.rs index 0dd117b..f99e8dd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,22 +4,24 @@ use crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; +use interprocess::local_socket::traits::Stream as _; +use interprocess::local_socket::Stream; use std::io::{self, BufReader, BufWriter, Write}; -use std::os::unix::net::UnixStream; use std::sync::mpsc; use std::time::Duration; pub fn run(socket_path: &str) -> io::Result<()> { - let stream = UnixStream::connect(socket_path).map_err(|e| { + let name = ipc::socket_name(socket_path)?; + let stream = Stream::connect(name).map_err(|e| { io::Error::new( io::ErrorKind::ConnectionRefused, format!("Kunde inte ansluta till TUI-WM ({}): {}", socket_path, e), ) })?; - let stream_write = stream.try_clone()?; - let mut reader = BufReader::new(stream); - let mut writer = BufWriter::new(stream_write); + let (recv_half, send_half) = stream.split(); + let mut reader = BufReader::new(recv_half); + let mut writer = BufWriter::new(send_half); let (term_w, term_h) = crossterm::terminal::size()?; diff --git a/src/config.rs b/src/config.rs index 3b85c6a..8f940e7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -59,8 +59,6 @@ pub struct PanelConfig { pub enum PanelPosition { Top, Bottom, - Left, - Right, } #[derive(Deserialize, Debug, Clone)] diff --git a/src/ipc.rs b/src/ipc.rs index f395dd2..7218f07 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -6,6 +6,7 @@ pub const SOCKET_ENV: &str = "TUI_WM_SOCKET"; pub const WINDOW_ID_ENV: &str = "TUI_WM_WINDOW_ID"; pub const VERSION: &str = "0.1.0"; +#[cfg(unix)] pub fn default_socket_path() -> String { if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") { format!("{}/tui-wm.sock", runtime) @@ -16,6 +17,31 @@ pub fn default_socket_path() -> String { } } +#[cfg(windows)] +pub fn default_socket_path() -> String { + // Namespaced namn → named pipe (\\.\pipe\tui-wm-) + match std::env::var("USERNAME") { + Ok(user) => format!("tui-wm-{}", user), + Err(_) => "tui-wm".to_string(), + } +} + +/// Konverterar en socket-sträng till ett interprocess-namn. +/// Unix: filsystemssökväg (vanlig unix-socket — kompatibel med externa +/// klienter i t.ex. Python/Bash enligt SOCKET_API.md). +/// Windows: namespaced namn (named pipe). +#[cfg(unix)] +pub fn socket_name(path: &str) -> std::io::Result> { + use interprocess::local_socket::{GenericFilePath, ToFsName}; + path.to_fs_name::() +} + +#[cfg(windows)] +pub fn socket_name(path: &str) -> std::io::Result> { + use interprocess::local_socket::{GenericNamespaced, ToNsName}; + path.to_ns_name::() +} + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ClientMessage { diff --git a/src/kitty_gfx.rs b/src/kitty_gfx.rs index 65e39a7..4346d98 100644 --- a/src/kitty_gfx.rs +++ b/src/kitty_gfx.rs @@ -135,28 +135,6 @@ pub fn show_bg( }) } -/// Fråga terminalen om pixelstorlek via `\x1b[14t`. -/// Skickar frågan — svaret (`\x1b[4;;t`) läses av crossterm. -#[allow(dead_code)] -pub fn query_pixel_size(stdout: &mut impl Write) { - let _ = stdout.write_all(b"\x1b[14t"); - let _ = stdout.flush(); -} - -/// Försök tolka ett `\x1b[4;;t`-svar från stdin. -/// Returnerar `(width, height)` i pixlar. -#[allow(dead_code)] -pub fn parse_pixel_size_response(data: &[u8]) -> Option<(u16, u16)> { - // Format: ESC [ 4 ; ; t - let s = std::str::from_utf8(data).ok()?; - let s = s.strip_prefix("\x1b[4;")?; - let s = s.strip_suffix('t')?; - let mut parts = s.split(';'); - let h: u16 = parts.next()?.parse().ok()?; - let w: u16 = parts.next()?.parse().ok()?; - Some((w, h)) -} - /// Enkel base64-kodning. fn base64_encode(data: &[u8]) -> String { const ALPHABET: &[u8; 64] = diff --git a/src/main.rs b/src/main.rs index dc23511..23589e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -238,6 +238,11 @@ fn update_background( cols: u16, rows: u16, ) { + // Kitty graphics-protokollet stöds inte av Windows Terminal/conhost — + // att skriva APC-sekvenser dit ger bara skräptecken. + if cfg!(windows) { + return; + } let wanted_path = app.config.background_image.as_deref(); // Kolla om vi behöver göra något diff --git a/src/pty.rs b/src/pty.rs index 91f7b3f..c1c23e0 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -4,6 +4,30 @@ use std::io::Read; use std::sync::mpsc; use std::thread; +/// Bygger kommandot för PTY:n. En enkel programsökväg körs direkt; +/// en kommandorad med argument (t.ex. "htop -d 10") körs via shell +/// så att argument, pipes och citattecken fungerar. +fn build_command(shell: &str) -> CommandBuilder { + if shell.trim().contains(char::is_whitespace) { + #[cfg(unix)] + { + let mut c = CommandBuilder::new("/bin/sh"); + c.arg("-c"); + c.arg(shell); + c + } + #[cfg(windows)] + { + let mut c = CommandBuilder::new("cmd.exe"); + c.arg("/C"); + c.arg(shell); + c + } + } else { + CommandBuilder::new(shell) + } +} + pub struct PtyTerminal { pub master: Box, pub writer: Box, @@ -20,7 +44,7 @@ impl PtyTerminal { pixel_height: 0, })?; - let mut cmd = CommandBuilder::new(shell); + let mut cmd = build_command(shell); cmd.env("TERM", "xterm-256color"); cmd.env("COLORTERM", "truecolor"); for (key, val) in extra_env { diff --git a/src/server.rs b/src/server.rs index 88b9907..c56b48b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,6 +1,7 @@ use crate::ipc::{self, ClientMessage, ClientRole, ServerMessage}; +use interprocess::local_socket::traits::{ListenerExt as _, Stream as _}; +use interprocess::local_socket::{ListenerOptions, Stream}; use std::io::{BufReader, BufWriter}; -use std::os::unix::net::{UnixListener, UnixStream}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::mpsc; use std::thread; @@ -25,11 +26,21 @@ pub enum IpcEvent { } pub fn start(socket_path: &str, event_tx: mpsc::Sender) { + // På unix: städa bort en ev. kvarlämnad socket-fil från en tidigare körning. + #[cfg(unix)] let _ = std::fs::remove_file(socket_path); - let listener = match UnixListener::bind(socket_path) { + + let name = match ipc::socket_name(socket_path) { + Ok(n) => n, + Err(e) => { + eprintln!("Ogiltigt socket-namn {}: {}", socket_path, e); + return; + } + }; + let listener = match ListenerOptions::new().name(name).create_sync() { Ok(l) => l, Err(e) => { - eprintln!("Kunde inte binda Unix socket {}: {}", socket_path, e); + eprintln!("Kunde inte binda lokal socket {}: {}", socket_path, e); return; } }; @@ -45,7 +56,8 @@ pub fn start(socket_path: &str, event_tx: mpsc::Sender) { handle_client(client_id, stream, event_tx, sp); }); } - Err(_) => break, + // Transienta accept-fel (kan hända för named pipes) — fortsätt lyssna. + Err(_) => continue, } } }); @@ -53,20 +65,17 @@ pub fn start(socket_path: &str, event_tx: mpsc::Sender) { fn handle_client( client_id: usize, - stream: UnixStream, + stream: Stream, event_tx: mpsc::Sender, socket_path: String, ) { - let stream_write = match stream.try_clone() { - Ok(s) => s, - Err(_) => return, - }; - let mut reader = BufReader::new(stream); + let (recv_half, send_half) = stream.split(); + let mut reader = BufReader::new(recv_half); let (tx, rx) = mpsc::sync_channel::(64); // Writer thread thread::spawn(move || { - let mut writer = BufWriter::new(stream_write); + let mut writer = BufWriter::new(send_half); for msg in rx { if ipc::write_message(&mut writer, &msg).is_err() { break;