diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index ca23f19..3ed3bf2 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -51,12 +51,30 @@ jobs: run: | set -e TAG="latest" - BODY="Rullande bygge från senaste master. Commit: ${{ github.sha }}. Arkitekturer: linux x64 + arm64 (Pi5)." + BASE="https://gitea.brasse-pc.eu/$REPO/releases/download/$TAG" + BODY=$(cat < *keys, @@ -2068,13 +2068,27 @@ impl App { KeyCode::Enter => { let buf = editing.take().unwrap_or_default(); let val = buf.trim().to_string(); - self.config.default_editor = - if val.is_empty() { None } else { Some(val) }; - let _ = Config::save_top_level_str( - "config.toml", - "default_editor", - self.config.default_editor.as_deref(), - ); + let row = match self.settings_content(id) { + Some(WindowContent::Settings { selected, .. }) => *selected, + _ => 2, + }; + if row == 3 { + self.config.screenshot_dir = + if val.is_empty() { None } else { Some(val) }; + let _ = Config::save_top_level_str( + "config.toml", + "screenshot_dir", + self.config.screenshot_dir.as_deref(), + ); + } else { + self.config.default_editor = + if val.is_empty() { None } else { Some(val) }; + let _ = Config::save_top_level_str( + "config.toml", + "default_editor", + self.config.default_editor.as_deref(), + ); + } } KeyCode::Esc => { *editing = None; @@ -2135,6 +2149,7 @@ 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 current_shotdir = self.config.screenshot_dir.clone().unwrap_or_default(); let Some(WindowContent::Settings { selected, editing, .. }) = self.settings_content(id) else { return }; let row = *selected; if row == 2 { @@ -2142,14 +2157,19 @@ impl App { *editing = Some(current_editor); return; } - if row == 5 { + if row == 3 { + // Skärmbildskatalog: börja textredigera värdet + *editing = Some(current_shotdir); + return; + } + if row == 6 { // Associationer: öppna listvyn if let Some(WindowContent::Settings { assoc, .. }) = self.settings_content(id) { *assoc = Some(0); } return; } - if row == 6 { + if row == 7 { // Tangenter: öppna keybind-vyn if let Some(WindowContent::Settings { keys, .. }) = self.settings_content(id) { *keys = Some((0, false)); @@ -2190,11 +2210,11 @@ impl App { let _ = Config::save_panel_file("config.toml", 0, &options[next]); // panel-innehållet läses om av hot-reloaden } - 3 => { + 4 => { self.config.shadow = !self.config.shadow; let _ = Config::save_top_level_bool("config.toml", "shadow", self.config.shadow); } - 4 => { + 5 => { self.config.show_scrollbar = !self.config.show_scrollbar; let _ = Config::save_top_level_bool("config.toml", "show_scrollbar", self.config.show_scrollbar); } diff --git a/src/backend.rs b/src/backend.rs index 9a48268..0a0c0f3 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -21,11 +21,20 @@ use std::io::{self, Write}; pub struct SafeBackend { writer: W, + /// Positioner där vi senast skrev ett brett (2-kolumns) tecken. + /// Behövs för att expandera ratatuis diff: när ett brett tecken + /// ersätts av ett smalt innehåller diffen bara vänstercellen — + /// högercellen är "space" i båda ratatui-buffertarna och skrivs + /// aldrig om, så terminalen kan lämna kvar glyfens högra halva. + wide_cells: std::collections::HashSet<(u16, u16)>, } impl SafeBackend { pub fn new(writer: W) -> Self { - SafeBackend { writer } + SafeBackend { + writer, + wide_cells: std::collections::HashSet::new(), + } } } @@ -69,11 +78,15 @@ impl Backend for SafeBackend { I: Iterator, { use std::fmt::Write as _; + let cells: Vec<(u16, u16, &Cell)> = content.collect(); + let in_diff: std::collections::HashSet<(u16, u16)> = + cells.iter().map(|(x, y, _)| (*x, *y)).collect(); + let mut buf = String::new(); let mut last_fg = None; let mut last_bg = None; let mut last_mod = None; - for (x, y, cell) in content { + for (x, y, cell) in cells { // Explicit position för varje cell — se modulkommentaren. let _ = write!(buf, "\x1b[{};{}H", y + 1, x + 1); if last_mod != Some(cell.modifier) { @@ -91,6 +104,23 @@ impl Backend for SafeBackend { last_bg = Some(cell.bg); } buf.push_str(cell.symbol()); + + let width = unicode_width::UnicodeWidthStr::width(cell.symbol()); + let was_wide = self.wide_cells.contains(&(x, y)); + if width > 1 { + self.wide_cells.insert((x, y)); + } else { + self.wide_cells.remove(&(x, y)); + // Skade-expansion: ett brett tecken ersattes av ett smalt. + // Ratatui-diffen innehåller inte högercellen (space i båda + // buffertarna) men terminalen kan ha kvar glyfens högra + // halva där — skriv explicit över den med ett space i den + // nya cellens bakgrundsfärg. + if was_wide && !in_diff.contains(&(x + 1, y)) { + let _ = write!(buf, "\x1b[{};{}H ", y + 1, x + 2); + self.wide_cells.remove(&(x + 1, y)); + } + } } buf.push_str("\x1b[0m"); self.writer.write_all(buf.as_bytes()) @@ -117,6 +147,7 @@ impl Backend for SafeBackend { } fn clear(&mut self) -> io::Result<()> { + self.wide_cells.clear(); self.writer.write_all(b"\x1b[2J\x1b[H") } diff --git a/src/render.rs b/src/render.rs index 39dc3c5..ffa0a82 100644 --- a/src/render.rs +++ b/src/render.rs @@ -647,19 +647,22 @@ fn render_settings_content( .first() .and_then(|p| p.file.as_deref()) .unwrap_or("(inline)"); - let editor_val = match editing { - Some(buf) => format!("{}▏ (Enter=spara, Esc=avbryt)", buf), - None => app - .config - .default_editor - .clone() - .unwrap_or_else(|| "(ingen — Enter för att ange)".to_string()), + let edit_val = |row: usize, current: Option, hint: &str| -> String { + if selected == row { + if let Some(buf) = editing { + return format!("{}▏ (Enter=spara, Esc=avbryt)", buf); + } + } + current.unwrap_or_else(|| hint.to_string()) }; + let editor_val = edit_val(2, app.config.default_editor.clone(), "(ingen — Enter för att ange)"); + let shotdir_val = edit_val(3, app.config.screenshot_dir.clone(), "skärmbilder (standard)"); let onoff = |b: bool| if b { "[x] på " } else { "[ ] av " }; let rows: Vec<(String, String)> = vec![ ("Tema".to_string(), format!("‹ {} ›", theme_name)), ("Panel".to_string(), format!("‹ {} ›", panel_name)), ("Editor".to_string(), editor_val), + ("Skärmbilder".to_string(), shotdir_val), ("Skugga".to_string(), onoff(app.config.shadow).to_string()), ("Scrollbar".to_string(), onoff(app.config.show_scrollbar).to_string()), ( diff --git a/tests/integration.py b/tests/integration.py index 6ffed4a..73513ce 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -396,10 +396,10 @@ def main(): text = disp.frame_text(timeout=5.0, contains="Tema:") check("inställningsdialogen öppnas från panelen", "Tema:" in text and "Skugga" in text and "config.toml" in text) - # Ned tre rader till Skugga (0=Tema, 1=Panel, 2=Editor, 3=Skugga) - disp.send(key_event(code="Down")) - disp.send(key_event(code="Down")) - disp.send(key_event(code="Down")) + # Ned fyra rader till Skugga + # (0=Tema, 1=Panel, 2=Editor, 3=Skärmbilder, 4=Skugga) + for _ in range(4): + disp.send(key_event(code="Down")) disp.send(key_event(code="Enter")) time.sleep(0.5) cfg_now = open(os.path.join(cfgdir, "config.toml")).read() @@ -446,7 +446,7 @@ def main(): 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): + for _ in range(6): disp.send(key_event(code="Down")) disp.send(key_event(code="Enter")) text = disp.frame_text(timeout=5.0, contains=".txt") @@ -480,11 +480,35 @@ def main(): check("PNG-skärmbilden har giltig header", magic == b"\x89PNG\r\n\x1a\n", f"magic: {magic!r}") + # ── 4k2. Skärmbildskatalog via inställningarna ─────────────────── + disp.send(mouse_event({"Down": "Left"}, gx, 0)) + disp.send(mouse_event({"Up": "Left"}, gx, 0)) + disp.frame_text(timeout=5.0, contains="Skärmbilder") + for _ in range(3): + disp.send(key_event(code="Down")) + disp.send(key_event(code="Enter")) + for ch in "bilder": + disp.send(key_event(ch=ch)) + disp.send(key_event(code="Enter")) + time.sleep(0.5) + cfg_now = open(os.path.join(cfgdir, "config.toml")).read() + check("skärmbildskatalogen sparas till config", + 'screenshot_dir = "bilder"' in cfg_now) + disp.send(key_event(code="Esc")) + time.sleep(0.3) + disp.send(key_event(ch="p", modifiers="ALT")) + disp.frame_text(timeout=8.0, contains="Skärmbild sparad") + disp.send(key_event(code="Enter")) + time.sleep(0.3) + check("skärmbilden hamnar i den valda katalogen", + os.path.isdir(os.path.join(cfgdir, "bilder")) + and len(os.listdir(os.path.join(cfgdir, "bilder"))) >= 1) + # ── 4l. Tangent-fliken i inställningarna ───────────────────────── disp.send(mouse_event({"Down": "Left"}, gx, 0)) disp.send(mouse_event({"Up": "Left"}, gx, 0)) disp.frame_text(timeout=5.0, contains="Tangenter") - for _ in range(6): + for _ in range(7): disp.send(key_event(code="Down")) disp.send(key_event(code="Enter")) text = disp.frame_text(timeout=5.0, contains="Fönsterväxlare")