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",
|
"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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
10
src/app.rs
10
src/app.rs
@@ -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 => {}
|
||||||
|
|||||||
@@ -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()?;
|
||||||
|
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
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 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 {
|
||||||
|
|||||||
@@ -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] =
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
26
src/pty.rs
26
src/pty.rs
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user