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 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:38:56 +02:00
parent b8d6a18a2e
commit 59b909efb8
10 changed files with 126 additions and 43 deletions

32
Cargo.lock generated
View File

@@ -331,6 +331,12 @@ dependencies = [
"crypto-common", "crypto-common",
] ]
[[package]]
name = "doctest-file"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2db04e74f0a9a93103b50e90b96024c9b2bdca8bce6a632ec71b88736d3d359"
[[package]] [[package]]
name = "document-features" name = "document-features"
version = "0.2.12" version = "0.2.12"
@@ -614,6 +620,19 @@ dependencies = [
"syn 2.0.117", "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]] [[package]]
name = "itertools" name = "itertools"
version = "0.14.0" version = "0.14.0"
@@ -1225,6 +1244,12 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "recvmsg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -1694,6 +1719,7 @@ dependencies = [
"anyhow", "anyhow",
"crossterm", "crossterm",
"image", "image",
"interprocess",
"libc", "libc",
"portable-pty", "portable-pty",
"ratatui", "ratatui",
@@ -1937,6 +1963,12 @@ dependencies = [
"wezterm-dynamic", "wezterm-dynamic",
] ]
[[package]]
name = "widestring"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@@ -18,4 +18,7 @@ portable-pty = "0.9"
vt100 = "0.16" vt100 = "0.16"
anyhow = "1" anyhow = "1"
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "bmp", "webp"] } image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "bmp", "webp"] }
interprocess = "2.4"
[target.'cfg(unix)'.dependencies]
libc = "0.2" libc = "0.2"

View File

@@ -477,7 +477,6 @@ impl App {
bottom_used += 1; bottom_used += 1;
Rect::new(area.x, area.y + area.height.saturating_sub(bottom_used), area.width, 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(); let mut item_rects = Vec::new();
@@ -1257,7 +1256,14 @@ impl App {
self.spawn_terminal(&shell); self.spawn_terminal(&shell);
} }
MenuAction::RunScript { path } => { self.spawn_terminal(&path); } 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::SpawnRunDialog => self.spawn_run_dialog(),
MenuAction::Submenu { .. } => {} MenuAction::Submenu { .. } => {}
MenuAction::NoOp => {} MenuAction::NoOp => {}

View File

@@ -4,22 +4,24 @@ use crossterm::{
execute, execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 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::io::{self, BufReader, BufWriter, Write};
use std::os::unix::net::UnixStream;
use std::sync::mpsc; use std::sync::mpsc;
use std::time::Duration; use std::time::Duration;
pub fn run(socket_path: &str) -> io::Result<()> { 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::Error::new(
io::ErrorKind::ConnectionRefused, io::ErrorKind::ConnectionRefused,
format!("Kunde inte ansluta till TUI-WM ({}): {}", socket_path, e), format!("Kunde inte ansluta till TUI-WM ({}): {}", socket_path, e),
) )
})?; })?;
let stream_write = stream.try_clone()?; let (recv_half, send_half) = stream.split();
let mut reader = BufReader::new(stream); let mut reader = BufReader::new(recv_half);
let mut writer = BufWriter::new(stream_write); let mut writer = BufWriter::new(send_half);
let (term_w, term_h) = crossterm::terminal::size()?; let (term_w, term_h) = crossterm::terminal::size()?;

View File

@@ -59,8 +59,6 @@ pub struct PanelConfig {
pub enum PanelPosition { pub enum PanelPosition {
Top, Top,
Bottom, Bottom,
Left,
Right,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]

View File

@@ -6,6 +6,7 @@ pub const SOCKET_ENV: &str = "TUI_WM_SOCKET";
pub const WINDOW_ID_ENV: &str = "TUI_WM_WINDOW_ID"; pub const WINDOW_ID_ENV: &str = "TUI_WM_WINDOW_ID";
pub const VERSION: &str = "0.1.0"; pub const VERSION: &str = "0.1.0";
#[cfg(unix)]
pub fn default_socket_path() -> String { pub fn default_socket_path() -> String {
if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") { if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
format!("{}/tui-wm.sock", runtime) 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-<user>)
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<interprocess::local_socket::Name<'_>> {
use interprocess::local_socket::{GenericFilePath, ToFsName};
path.to_fs_name::<GenericFilePath>()
}
#[cfg(windows)]
pub fn socket_name(path: &str) -> std::io::Result<interprocess::local_socket::Name<'_>> {
use interprocess::local_socket::{GenericNamespaced, ToNsName};
path.to_ns_name::<GenericNamespaced>()
}
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientMessage { pub enum ClientMessage {

View File

@@ -135,28 +135,6 @@ pub fn show_bg(
}) })
} }
/// Fråga terminalen om pixelstorlek via `\x1b[14t`.
/// Skickar frågan — svaret (`\x1b[4;<h>;<w>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;<h>;<w>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 ; <height> ; <width> 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. /// Enkel base64-kodning.
fn base64_encode(data: &[u8]) -> String { fn base64_encode(data: &[u8]) -> String {
const ALPHABET: &[u8; 64] = const ALPHABET: &[u8; 64] =

View File

@@ -238,6 +238,11 @@ fn update_background(
cols: u16, cols: u16,
rows: 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(); let wanted_path = app.config.background_image.as_deref();
// Kolla om vi behöver göra något // Kolla om vi behöver göra något

View File

@@ -4,6 +4,30 @@ use std::io::Read;
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; 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 struct PtyTerminal {
pub master: Box<dyn portable_pty::MasterPty + Send>, pub master: Box<dyn portable_pty::MasterPty + Send>,
pub writer: Box<dyn std::io::Write + Send>, pub writer: Box<dyn std::io::Write + Send>,
@@ -20,7 +44,7 @@ impl PtyTerminal {
pixel_height: 0, pixel_height: 0,
})?; })?;
let mut cmd = CommandBuilder::new(shell); let mut cmd = build_command(shell);
cmd.env("TERM", "xterm-256color"); cmd.env("TERM", "xterm-256color");
cmd.env("COLORTERM", "truecolor"); cmd.env("COLORTERM", "truecolor");
for (key, val) in extra_env { for (key, val) in extra_env {

View File

@@ -1,6 +1,7 @@
use crate::ipc::{self, ClientMessage, ClientRole, ServerMessage}; 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::io::{BufReader, BufWriter};
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;
@@ -25,11 +26,21 @@ pub enum IpcEvent {
} }
pub fn start(socket_path: &str, event_tx: mpsc::Sender<IpcEvent>) { pub fn start(socket_path: &str, event_tx: mpsc::Sender<IpcEvent>) {
// 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 _ = 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, Ok(l) => l,
Err(e) => { Err(e) => {
eprintln!("Kunde inte binda Unix socket {}: {}", socket_path, e); eprintln!("Kunde inte binda lokal socket {}: {}", socket_path, e);
return; return;
} }
}; };
@@ -45,7 +56,8 @@ pub fn start(socket_path: &str, event_tx: mpsc::Sender<IpcEvent>) {
handle_client(client_id, stream, event_tx, sp); 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<IpcEvent>) {
fn handle_client( fn handle_client(
client_id: usize, client_id: usize,
stream: UnixStream, stream: Stream,
event_tx: mpsc::Sender<IpcEvent>, event_tx: mpsc::Sender<IpcEvent>,
socket_path: String, socket_path: String,
) { ) {
let stream_write = match stream.try_clone() { let (recv_half, send_half) = stream.split();
Ok(s) => s, let mut reader = BufReader::new(recv_half);
Err(_) => return,
};
let mut reader = BufReader::new(stream);
let (tx, rx) = mpsc::sync_channel::<ServerMessage>(64); let (tx, rx) = mpsc::sync_channel::<ServerMessage>(64);
// Writer thread // Writer thread
thread::spawn(move || { thread::spawn(move || {
let mut writer = BufWriter::new(stream_write); let mut writer = BufWriter::new(send_half);
for msg in rx { for msg in rx {
if ipc::write_message(&mut writer, &msg).is_err() { if ipc::write_message(&mut writer, &msg).is_err() {
break; break;