diff --git a/src/app.rs b/src/app.rs index bd8f647..2598369 100644 --- a/src/app.rs +++ b/src/app.rs @@ -111,6 +111,9 @@ pub struct App { last_title_click: Option<(usize, std::time::Instant)>, /// Copy-mode: bläddra/söka i fokuserat fönsters historik med tangentbordet pub copy_mode: Option, + /// Begär full omritning av värdterminalen (rensar spök-tecken som + /// vissa terminaler lämnar kvar när breda glyfer delvis skrivs över) + pub needs_full_redraw: bool, /// mtime-signatur för bevakade config-filer (hot-reload) watch_sig: Vec>, /// Senaste hot-reload-kollen @@ -310,10 +313,13 @@ pub enum WindowContent { }, /// Inbyggd inställningsdialog (tema, panel, editor, skugga, scrollbar) Settings { - /// Markerad rad (0=tema, 1=panel, 2=editor, 3=skugga, 4=scrollbar) + /// Markerad rad (0=tema, 1=panel, 2=editor, 3=skugga, + /// 4=scrollbar, 5=associationer) selected: usize, /// Textredigering pågår för editor-raden (buffern som skrivs i) editing: Option, + /// Associationsvyn är öppen: markerat index i listan + assoc: Option, }, /// "Öppna med"-dialog: fråga vilken app som ska öppna en fil vars /// format saknar association. @@ -531,6 +537,7 @@ impl App { self.status_states = Self::build_status_states(&config); self.watch_sig = Self::config_signature(&Self::watch_paths(&config)); self.config = config; + self.needs_full_redraw = true; crate::log::log("config hot-reload: laddade om config/tema/paneler"); } @@ -565,6 +572,7 @@ impl App { switcher: None, last_title_click: None, copy_mode: None, + needs_full_redraw: false, watch_sig, last_watch_check: None, hovered_tui_btn: false, @@ -1135,6 +1143,11 @@ impl App { MouseEventKind::Up(btn) => { // Släpptes ett fönster-drag vid skärmkanten? → snap till halva let was_dragging = self.windows.iter().find(|w| w.dragging.is_some()).map(|w| w.id); + let was_resizing = self.windows.iter().any(|w| w.resizing.is_some()); + if was_dragging.is_some() || was_resizing { + // Fönstret har flyttats/ändrats — rensa ev. spökglyfer + self.needs_full_redraw = true; + } for w in &mut self.windows { w.dragging = None; w.resizing = None; @@ -1341,7 +1354,7 @@ impl App { self.focus_window(id); // rad 1..=4 i innehållet motsvarar inställningsrad 0..=3 let rel = row.saturating_sub(cr.y); - if (1..=5).contains(&rel) { + if (1..=6).contains(&rel) { let target = (rel - 1) as usize; let mut activate = false; if let Some(WindowContent::Settings { selected, .. }) = self.settings_content(id) { @@ -1866,7 +1879,7 @@ impl App { y, width: w, height: h, - content: WindowContent::Settings { selected: 0, editing: None }, + content: WindowContent::Settings { selected: 0, editing: None, assoc: None }, dragging: None, resizing: None, resizable: false, @@ -1884,7 +1897,47 @@ impl App { } fn handle_settings_key(&mut self, id: usize, key: KeyEvent) { - const ROWS: usize = 5; + const ROWS: usize = 6; + // Associationsvyn är öppen? + let assoc_open = matches!( + self.settings_content(id), + Some(WindowContent::Settings { assoc: Some(_), .. }) + ); + if assoc_open { + let mut exts: Vec = self.config.open_with.keys().cloned().collect(); + exts.sort(); + let len = exts.len(); + let Some(WindowContent::Settings { assoc, .. }) = self.settings_content(id) else { return }; + match key.code { + KeyCode::Esc | KeyCode::Char('q') => { + *assoc = None; + } + KeyCode::Up => { + if let Some(i) = assoc.as_mut() { + *i = i.saturating_sub(1); + } + } + KeyCode::Down => { + if let Some(i) = assoc.as_mut() { + *i = (*i + 1).min(len.saturating_sub(1)); + } + } + KeyCode::Delete | KeyCode::Backspace => { + let idx = assoc.unwrap_or(0); + if let Some(ext) = exts.get(idx).cloned() { + if let Some(WindowContent::Settings { assoc, .. }) = self.settings_content(id) { + if let Some(i) = assoc.as_mut() { + *i = i.saturating_sub(if idx + 1 == len { 1 } else { 0 }); + } + } + self.config.open_with.remove(&ext); + let _ = Config::remove_open_with("config.toml", &ext); + } + } + _ => {} + } + return; + } // Textredigering av editor-raden pågår? if let Some(WindowContent::Settings { editing, .. }) = self.settings_content(id) { if editing.is_some() { @@ -1959,13 +2012,20 @@ impl App { /// direkt, men tema/flaggor appliceras även omedelbart i minnet. fn settings_activate(&mut self, id: usize, forward: bool) { let current_editor = self.config.default_editor.clone().unwrap_or_default(); - let Some(WindowContent::Settings { selected, editing }) = self.settings_content(id) else { return }; + let Some(WindowContent::Settings { selected, editing, .. }) = self.settings_content(id) else { return }; let row = *selected; if row == 2 { // Editor: börja textredigera värdet *editing = Some(current_editor); return; } + if row == 5 { + // Associationer: öppna listvyn + if let Some(WindowContent::Settings { assoc, .. }) = self.settings_content(id) { + *assoc = Some(0); + } + return; + } match row { 0 => { // Tema: cykla inbyggt + themes/*.toml @@ -2179,6 +2239,7 @@ impl App { if w.prev_geom.is_none() { w.prev_geom = Some((w.x, w.y, w.width, w.height)); } + self.needs_full_redraw = true; w.x = if left { ca.x as i32 } else { (ca.x + half_w) as i32 }; w.y = ca.y as i32; w.width = if left { half_w } else { ca.width - half_w }; @@ -2195,6 +2256,7 @@ impl App { if !w.resizable { return; } + self.needs_full_redraw = true; let is_maximized = w.x == ca.x as i32 && w.y == ca.y as i32 && w.width == ca.width @@ -2221,6 +2283,7 @@ impl App { } fn close_window(&mut self, id: usize) { + self.needs_full_redraw = true; self.windows.retain(|w| w.id != id); if self.focused_id == Some(id) { self.focused_id = self.windows.last().map(|w| w.id); diff --git a/src/config.rs b/src/config.rs index 4523ccc..a0a0465 100644 --- a/src/config.rs +++ b/src/config.rs @@ -337,6 +337,17 @@ impl Config { Ok(()) } + /// Ta bort en filassociation ur [open_with] i config-filen. + pub fn remove_open_with(path: &str, ext: &str) -> anyhow::Result<()> { + let content = fs::read_to_string(path).unwrap_or_default(); + let mut doc: toml_edit::DocumentMut = content.parse().unwrap_or_default(); + if let Some(tbl) = doc.get_mut("open_with").and_then(|t| t.as_table_mut()) { + tbl.remove(ext); + } + fs::write(path, doc.to_string())?; + Ok(()) + } + /// Sätt fil-referensen för panel nummer `index` i config-filen. pub fn save_panel_file(path: &str, index: usize, file: &str) -> anyhow::Result<()> { let content = fs::read_to_string(path).unwrap_or_default(); diff --git a/src/main.rs b/src/main.rs index b18e992..27cd2e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -262,6 +262,12 @@ fn run_standalone_loop( // ── Kitty bakgrundsbild ────────────────────────────────────────── update_background(terminal, app, &mut bg_state, size.width, size.height); + // Full omritning efter fönsterflytt/-stängning: rensar halva + // emoji-glyfer ("spöken") som terminal-diffen annars lämnar kvar. + if app.needs_full_redraw { + app.needs_full_redraw = false; + terminal.clear()?; + } terminal.draw(|frame| { let area = frame.area(); app.update_layout(area); diff --git a/src/render.rs b/src/render.rs index 113ab74..ca172da 100644 --- a/src/render.rs +++ b/src/render.rs @@ -390,8 +390,8 @@ fn render_window(frame: &mut Frame, window: &FloatingWindow, app: &App) { WindowContent::PopupDialog { .. } => { render_popup_dialog(frame, window, focused_id, th); } - WindowContent::Settings { selected, editing } => { - render_settings_content(frame, cr, *selected, editing.as_deref(), app, th); + WindowContent::Settings { selected, editing, assoc } => { + render_settings_content(frame, cr, *selected, editing.as_deref(), *assoc, app, th); } WindowContent::OpenWith { path, input, save_default } => { render_open_with_content(frame, cr, path, input, *save_default, th); @@ -522,9 +522,68 @@ fn render_settings_content( area: Rect, selected: usize, editing: Option<&str>, + assoc: Option, app: &App, th: &crate::theme::Theme, ) { + // Associationsvyn: lista filformat → kommandon + if let Some(sel) = assoc { + let mut exts: Vec<&String> = app.config.open_with.keys().collect(); + exts.sort(); + let mut y = area.y; + let editor = app.config.default_editor.as_deref().unwrap_or("(ingen)"); + frame.render_widget( + Paragraph::new(Span::styled( + format!(" Textfiler → {} (default_editor)", editor), + Style::default().fg(th.hint), + )), + Rect::new(area.x, y, area.width, 1), + ); + y += 2; + if exts.is_empty() { + frame.render_widget( + Paragraph::new(Span::styled( + " (inga associationer — spara via Öppna med-dialogen)", + Style::default().fg(th.hint), + )), + Rect::new(area.x, y, area.width, 1), + ); + y += 1; + } + let list_bottom = area.y + area.height.saturating_sub(2); + for (i, ext) in exts.iter().enumerate() { + if y >= list_bottom { + frame.render_widget( + Paragraph::new(Span::styled(" …fler", Style::default().fg(th.hint))), + Rect::new(area.x, y, area.width, 1), + ); + break; + } + let cmd = app.config.open_with.get(*ext).map(|s| s.as_str()).unwrap_or(""); + let style = if i == sel { + th.hover_highlight() + } else { + Style::default().fg(th.text) + }; + let line = format!(" .{:<8} → {}", ext, cmd); + frame.render_widget( + Paragraph::new(Span::styled( + format!("{: