File-association list view in settings + fix ghost glyphs after window moves
- New 'Filformat' row in the settings dialog opens a list of all [open_with] associations (.ext → command) plus the default editor; Delete removes an association (persisted to config.toml), Esc goes back - Ghost fix: some terminals leave the right half of wide glyphs (emoji) behind when the diff only rewrites part of them — windows being dragged/resized/closed/snapped now request one full terminal repaint on completion, clearing any leftovers. Verified the compositor itself is clean (headless frames have no ghosts) - Integration suite: 41/41 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
73
src/app.rs
73
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<CopyMode>,
|
||||
/// 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<Option<std::time::SystemTime>>,
|
||||
/// 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<String>,
|
||||
/// Associationsvyn är öppen: markerat index i listan
|
||||
assoc: Option<usize>,
|
||||
},
|
||||
/// "Ö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<String> = 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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<usize>,
|
||||
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!("{:<width$}", line, width = area.width as usize),
|
||||
style,
|
||||
)),
|
||||
Rect::new(area.x, y, area.width, 1),
|
||||
);
|
||||
y += 1;
|
||||
}
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(
|
||||
" ↑↓ välj Delete=ta bort Esc=tillbaka",
|
||||
Style::default().fg(th.hint),
|
||||
)),
|
||||
Rect::new(area.x, area.y + area.height.saturating_sub(1), area.width, 1),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let theme_name = app.config.theme.as_deref().unwrap_or("(inbyggt)");
|
||||
let panel_name = app
|
||||
.config
|
||||
@@ -547,6 +606,10 @@ fn render_settings_content(
|
||||
("Editor".to_string(), editor_val),
|
||||
("Skugga".to_string(), onoff(app.config.shadow).to_string()),
|
||||
("Scrollbar".to_string(), onoff(app.config.show_scrollbar).to_string()),
|
||||
(
|
||||
"Filformat".to_string(),
|
||||
format!("{} associationer (Enter=visa/ta bort)", app.config.open_with.len()),
|
||||
),
|
||||
];
|
||||
|
||||
let mut y = area.y + 1;
|
||||
|
||||
@@ -440,6 +440,27 @@ def main():
|
||||
app.send({"type": "close_window", "window_id": op2["id"]})
|
||||
time.sleep(0.3)
|
||||
|
||||
# ── 4j. Associationsvyn i inställningarna ────────────────────────
|
||||
disp.send(mouse_event({"Down": "Left"}, gx, 0))
|
||||
disp.send(mouse_event({"Up": "Left"}, gx, 0))
|
||||
text = disp.frame_text(timeout=5.0, contains="Filformat")
|
||||
check("inställningarna visar filformat-raden", "1 associationer" in text,
|
||||
f"frame: {text[:300]!r}")
|
||||
for _ in range(5):
|
||||
disp.send(key_event(code="Down"))
|
||||
disp.send(key_event(code="Enter"))
|
||||
text = disp.frame_text(timeout=5.0, contains=".txt")
|
||||
check("associationsvyn listar .txt → less",
|
||||
".txt" in text and "less" in text)
|
||||
disp.send(key_event(code="Delete"))
|
||||
time.sleep(0.5)
|
||||
cfg_now = open(os.path.join(cfgdir, "config.toml")).read()
|
||||
check("Delete tar bort associationen ur config",
|
||||
'txt = "less"' not in cfg_now)
|
||||
disp.send(key_event(code="Esc"))
|
||||
disp.send(key_event(code="Esc"))
|
||||
time.sleep(0.3)
|
||||
|
||||
# ── 5. Popup + popup_result via Enter ───────────────────────────
|
||||
app.send({
|
||||
"type": "spawn_popup", "message": "Integrationstest?",
|
||||
|
||||
Reference in New Issue
Block a user