diff --git a/build/linux/tui-fm b/build/linux/tui-fm index 36bdc4d..c7733c4 100755 Binary files a/build/linux/tui-fm and b/build/linux/tui-fm differ diff --git a/src/app.rs b/src/app.rs index 527e185..dea0e40 100644 --- a/src/app.rs +++ b/src/app.rs @@ -24,6 +24,8 @@ pub struct ClipboardEntry { #[derive(Debug, Clone, PartialEq)] pub enum ContextMenuItem { + /// Öppna filen (via TUI-WM om möjligt) eller gå in i katalogen + Open, Copy, Cut, Paste, @@ -112,6 +114,9 @@ pub struct App { /// terminalen, vilket gör hit-testing korrekt även i nästlade /// terminaler (t.ex. inne i TUI-WM). pub term_size: (u16, u16), + /// Editor-kommando + fil som ska köras genom att TUI:t tillfälligt + /// suspenderas (fallback när TUI-WM inte kör). Hanteras i run_app. + pub pending_spawn: Option<(String, PathBuf)>, } // Which field inside the settings editor is being edited @@ -187,6 +192,31 @@ impl App { settings_hover_button: None, unicode_support: detect_unicode_support(), term_size: crossterm::terminal::size().unwrap_or((80, 24)), + pending_spawn: None, + } + } + + /// Öppna en fil: via TUI-WM:s socket om vi kör i ett TUI-WM-fönster, + /// annars med den konfigurerade editorn (TUI:t suspenderas under tiden). + pub fn open_path(&mut self, path: &PathBuf) { + if path.is_dir() { + self.navigate_to(path.clone()); + return; + } + if crate::tui_wm::available() { + match crate::tui_wm::open_file(path) { + Ok(()) => self.set_status(format!("Öppnar {} via TUI-WM", path.display())), + Err(e) => self.set_status(format!("TUI-WM-öppning misslyckades: {}", e)), + } + return; + } + let editor = self.config.text_editor.trim().to_string(); + if editor.is_empty() { + self.set_status( + "Ingen editor konfigurerad (kugghjulet ≡) och TUI-WM kör inte".to_string(), + ); + } else { + self.pending_spawn = Some((editor, path.clone())); } } @@ -449,6 +479,7 @@ impl App { } } else { if has_selection { + items.push(ContextMenuItem::Open); items.push(ContextMenuItem::Copy); items.push(ContextMenuItem::Cut); } diff --git a/src/events.rs b/src/events.rs index baba877..7567b32 100644 --- a/src/events.rs +++ b/src/events.rs @@ -12,6 +12,30 @@ use crate::ui; pub fn run_app>(terminal: &mut Terminal, app: &mut App) -> io::Result<()> { loop { + // Fallback-öppning utanför TUI-WM: suspendera TUI:t, kör editorn + // på samma terminal, återuppta och rita om. + if let Some((editor, path)) = app.pending_spawn.take() { + use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; + use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, + }; + disable_raw_mode()?; + crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?; + let status = std::process::Command::new("sh") + .arg("-c") + .arg(format!("{} '{}'", editor, path.display().to_string().replace('\'', r"'\''"))) + .status(); + enable_raw_mode()?; + crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?; + terminal.clear()?; + match status { + Ok(s) if s.success() => {} + Ok(s) => app.set_status(format!("Editorn avslutades med {}", s)), + Err(e) => app.set_status(format!("Kunde inte starta editorn: {}", e)), + } + app.refresh(); + } + terminal.draw(|f| ui::draw(f, app))?; if event::poll(Duration::from_millis(50))? { @@ -381,6 +405,12 @@ fn execute_context_menu(app: &mut App) { app.context_menu = None; if let Some(item) = item { match item { + ContextMenuItem::Open => { + if let Some(&i) = app.selected_indices.first() + && let Some(e) = app.entries.get(i) { + app.open_path(&e.path.clone()); + } + } ContextMenuItem::Copy => app.copy_selected(), ContextMenuItem::Cut => app.cut_selected(), ContextMenuItem::Paste => { @@ -875,10 +905,10 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) { if is_double { app.last_click_entry = None; - if let Some(e) = app.entries.get(idx).cloned() - && e.is_dir() { - app.navigate_to(e.path.clone()); - } + if let Some(e) = app.entries.get(idx).cloned() { + // Kataloger navigeras; filer öppnas via TUI-WM/editor + app.open_path(&e.path.clone()); + } } else { app.select_only(idx); } diff --git a/src/main.rs b/src/main.rs index d9b268e..6e46345 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod app; mod config; mod file_ops; mod theme; +mod tui_wm; mod ui; mod events; diff --git a/src/tui_wm.rs b/src/tui_wm.rs new file mode 100644 index 0000000..cbbcabf --- /dev/null +++ b/src/tui_wm.rs @@ -0,0 +1,69 @@ +//! Integration med TUI-WM:s socket-API. +//! +//! När TUI-FM körs i ett TUI-WM-fönster sätts miljövariabeln +//! `TUI_WM_SOCKET`. Via den kan filer öppnas i egna fönster med +//! användarens filassociationer/standard-editor (TUI-WM visar en +//! "Öppna med"-dialog för okända format). + +use std::path::Path; + +/// Kör TUI-FM inne i TUI-WM? +pub fn available() -> bool { + std::env::var("TUI_WM_SOCKET").is_ok() +} + +/// Be TUI-WM öppna en fil. Blockerar bara för handskakningen. +pub fn open_file(path: &Path) -> Result<(), String> { + let sock = std::env::var("TUI_WM_SOCKET").map_err(|_| "TUI_WM_SOCKET saknas".to_string())?; + open_via_socket(&sock, path) +} + +#[cfg(unix)] +fn open_via_socket(sock: &str, path: &Path) -> Result<(), String> { + use std::io::{Read, Write}; + use std::os::unix::net::UnixStream; + + let mut stream = UnixStream::connect(sock).map_err(|e| format!("anslutning: {}", e))?; + stream + .set_read_timeout(Some(std::time::Duration::from_secs(2))) + .ok(); + + let write_msg = |s: &mut UnixStream, v: serde_json::Value| -> Result<(), String> { + let data = serde_json::to_vec(&v).map_err(|e| e.to_string())?; + s.write_all(&(data.len() as u32).to_be_bytes()) + .and_then(|_| s.write_all(&data)) + .map_err(|e| e.to_string()) + }; + let read_msg = |s: &mut UnixStream| -> Result { + let mut len = [0u8; 4]; + s.read_exact(&mut len).map_err(|e| e.to_string())?; + let mut buf = vec![0u8; u32::from_be_bytes(len) as usize]; + s.read_exact(&mut buf).map_err(|e| e.to_string())?; + serde_json::from_slice(&buf).map_err(|e| e.to_string()) + }; + + write_msg( + &mut stream, + serde_json::json!({"type": "hello", "role": "app", "width": 0, "height": 0}), + )?; + let hello = read_msg(&mut stream)?; + if hello.get("type").and_then(|t| t.as_str()) != Some("hello_ok") { + return Err("oväntat svar från TUI-WM".to_string()); + } + write_msg( + &mut stream, + serde_json::json!({ + "type": "open_file", + "path": path.to_string_lossy(), + "request_id": "tui-fm-open", + }), + )?; + // Vänta in kvittensen så meddelandet hinner behandlas + let _ = read_msg(&mut stream); + Ok(()) +} + +#[cfg(not(unix))] +fn open_via_socket(_sock: &str, _path: &Path) -> Result<(), String> { + Err("socket-öppning stöds inte på denna plattform ännu".to_string()) +} diff --git a/src/ui.rs b/src/ui.rs index b74ff89..11f374d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -533,6 +533,8 @@ fn draw_context_menu(f: &mut Frame, app: &App, menu: crate::app::ContextMenu) { // Use 2-char ASCII icon codes — emoji (U+1F000+) are 2-wide in terminals and // cause ghost characters and column drift when partially overwritten. let (icon, label, item_style): (&str, &str, Style) = match item { + ContextMenuItem::Open => + ("O ", "Öppna", Style::default().add_modifier(Modifier::BOLD)), ContextMenuItem::Copy => ("C ", "Kopiera", Style::default()), ContextMenuItem::Cut =>