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:
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
10
src/app.rs
10
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 => {}
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
|
||||
@@ -59,8 +59,6 @@ pub struct PanelConfig {
|
||||
pub enum PanelPosition {
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
|
||||
26
src/ipc.rs
26
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-<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)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ClientMessage {
|
||||
|
||||
@@ -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.
|
||||
fn base64_encode(data: &[u8]) -> String {
|
||||
const ALPHABET: &[u8; 64] =
|
||||
|
||||
@@ -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
|
||||
|
||||
26
src/pty.rs
26
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<dyn portable_pty::MasterPty + Send>,
|
||||
pub writer: Box<dyn std::io::Write + Send>,
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<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 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<IpcEvent>) {
|
||||
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(
|
||||
client_id: usize,
|
||||
stream: UnixStream,
|
||||
stream: Stream,
|
||||
event_tx: mpsc::Sender<IpcEvent>,
|
||||
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::<ServerMessage>(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;
|
||||
|
||||
Reference in New Issue
Block a user