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

View File

@@ -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 => {}

View File

@@ -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()?;

View File

@@ -59,8 +59,6 @@ pub struct PanelConfig {
pub enum PanelPosition {
Top,
Bottom,
Left,
Right,
}
#[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 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 {

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.
fn base64_encode(data: &[u8]) -> String {
const ALPHABET: &[u8; 64] =

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;