Open files through TUI-WM: context-menu Open, double-click, editor fallback
- Double-clicking a file or choosing the new 'Öppna' context-menu item asks TUI-WM to open it via $TUI_WM_SOCKET (open_file), inheriting the user's file associations and default editor - Outside TUI-WM the configured text_editor is used instead: the TUI suspends, runs the editor on the same terminal and resumes - Directories keep navigating as before Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
31
src/app.rs
31
src/app.rs
@@ -24,6 +24,8 @@ pub struct ClipboardEntry {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum ContextMenuItem {
|
pub enum ContextMenuItem {
|
||||||
|
/// Öppna filen (via TUI-WM om möjligt) eller gå in i katalogen
|
||||||
|
Open,
|
||||||
Copy,
|
Copy,
|
||||||
Cut,
|
Cut,
|
||||||
Paste,
|
Paste,
|
||||||
@@ -112,6 +114,9 @@ pub struct App {
|
|||||||
/// terminalen, vilket gör hit-testing korrekt även i nästlade
|
/// terminalen, vilket gör hit-testing korrekt även i nästlade
|
||||||
/// terminaler (t.ex. inne i TUI-WM).
|
/// terminaler (t.ex. inne i TUI-WM).
|
||||||
pub term_size: (u16, u16),
|
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
|
// Which field inside the settings editor is being edited
|
||||||
@@ -187,6 +192,31 @@ impl App {
|
|||||||
settings_hover_button: None,
|
settings_hover_button: None,
|
||||||
unicode_support: detect_unicode_support(),
|
unicode_support: detect_unicode_support(),
|
||||||
term_size: crossterm::terminal::size().unwrap_or((80, 24)),
|
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 {
|
} else {
|
||||||
if has_selection {
|
if has_selection {
|
||||||
|
items.push(ContextMenuItem::Open);
|
||||||
items.push(ContextMenuItem::Copy);
|
items.push(ContextMenuItem::Copy);
|
||||||
items.push(ContextMenuItem::Cut);
|
items.push(ContextMenuItem::Cut);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,30 @@ use crate::ui;
|
|||||||
|
|
||||||
pub fn run_app<B: Backend<Error = io::Error>>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
|
pub fn run_app<B: Backend<Error = io::Error>>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
|
||||||
loop {
|
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))?;
|
terminal.draw(|f| ui::draw(f, app))?;
|
||||||
|
|
||||||
if event::poll(Duration::from_millis(50))? {
|
if event::poll(Duration::from_millis(50))? {
|
||||||
@@ -381,6 +405,12 @@ fn execute_context_menu(app: &mut App) {
|
|||||||
app.context_menu = None;
|
app.context_menu = None;
|
||||||
if let Some(item) = item {
|
if let Some(item) = item {
|
||||||
match 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::Copy => app.copy_selected(),
|
||||||
ContextMenuItem::Cut => app.cut_selected(),
|
ContextMenuItem::Cut => app.cut_selected(),
|
||||||
ContextMenuItem::Paste => {
|
ContextMenuItem::Paste => {
|
||||||
@@ -875,10 +905,10 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) {
|
|||||||
|
|
||||||
if is_double {
|
if is_double {
|
||||||
app.last_click_entry = None;
|
app.last_click_entry = None;
|
||||||
if let Some(e) = app.entries.get(idx).cloned()
|
if let Some(e) = app.entries.get(idx).cloned() {
|
||||||
&& e.is_dir() {
|
// Kataloger navigeras; filer öppnas via TUI-WM/editor
|
||||||
app.navigate_to(e.path.clone());
|
app.open_path(&e.path.clone());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
app.select_only(idx);
|
app.select_only(idx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod app;
|
|||||||
mod config;
|
mod config;
|
||||||
mod file_ops;
|
mod file_ops;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
mod tui_wm;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod events;
|
mod events;
|
||||||
|
|
||||||
|
|||||||
69
src/tui_wm.rs
Normal file
69
src/tui_wm.rs
Normal file
@@ -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<serde_json::Value, String> {
|
||||||
|
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())
|
||||||
|
}
|
||||||
@@ -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
|
// Use 2-char ASCII icon codes — emoji (U+1F000+) are 2-wide in terminals and
|
||||||
// cause ghost characters and column drift when partially overwritten.
|
// cause ghost characters and column drift when partially overwritten.
|
||||||
let (icon, label, item_style): (&str, &str, Style) = match item {
|
let (icon, label, item_style): (&str, &str, Style) = match item {
|
||||||
|
ContextMenuItem::Open =>
|
||||||
|
("O ", "Öppna", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
ContextMenuItem::Copy =>
|
ContextMenuItem::Copy =>
|
||||||
("C ", "Kopiera", Style::default()),
|
("C ", "Kopiera", Style::default()),
|
||||||
ContextMenuItem::Cut =>
|
ContextMenuItem::Cut =>
|
||||||
|
|||||||
Reference in New Issue
Block a user