diff --git a/build/linux/tui-fm b/build/linux/tui-fm index 86e8923..36bdc4d 100755 Binary files a/build/linux/tui-fm and b/build/linux/tui-fm differ diff --git a/src/file_ops.rs b/src/file_ops.rs index 41ea7ff..971ca82 100644 --- a/src/file_ops.rs +++ b/src/file_ops.rs @@ -30,19 +30,24 @@ impl FileEntry { .unwrap_or("") .to_lowercase(); match ext.as_str() { + // OBS: alla emoji-ikoner MÅSTE vara East-Asian-Width Wide + // (mäts som 2 kolumner av unicode-width). Tecken med + // EAW=Neutral/Ambiguous (t.ex. 🖼 U+1F5BC, ⚙ U+2699) + // mäts som 1 men ritas 2 breda av emoji-fonter → + // raden förskjuts åt höger. Se icon_widths-testet. "rs" => "🦀", - "png" | "jpg" | "jpeg" | "gif" | "bmp" | "svg" | "webp" | "ico" => "🖼", + "png" | "jpg" | "jpeg" | "gif" | "bmp" | "svg" | "webp" | "ico" => "📷", "mp4" | "mkv" | "avi" | "mov" | "webm" => "🎬", "mp3" | "flac" | "ogg" | "wav" | "aac" => "🎵", "zip" | "tar" | "gz" | "bz2" | "xz" | "7z" | "rar" => "📦", "pdf" => "📕", "txt" | "md" | "log" => "📄", - "json" | "yaml" | "yml" | "toml" | "xml" => "⚙", + "json" | "yaml" | "yml" | "toml" | "xml" => "🔩", "sh" | "bash" | "zsh" | "fish" => "🐚", "py" => "🐍", "js" | "ts" => "📜", "c" | "cpp" | "h" | "hpp" => "🔧", - "exe" | "bin" | "out" | "run" => "⚙", + "exe" | "bin" | "out" | "run" => "⚡", _ => "📎", } } @@ -294,3 +299,56 @@ pub fn get_root_dirs() -> Vec { drives } } + +#[cfg(test)] +mod tests { + use super::*; + use unicode_width::UnicodeWidthStr; + + fn entry(name: &str, ft: FileType) -> FileEntry { + FileEntry { + name: name.to_string(), + path: PathBuf::from(name), + file_type: ft, + size: 0, + is_hidden: false, + } + } + + /// Alla emoji-ikoner måste vara East-Asian-Width Wide (mäts som + /// 2 kolumner av unicode-width). En ikon med EAW=Neutral/Ambiguous + /// (t.ex. 🖼 U+1F5BC eller ⚙ U+2699) mäts som 1 men ritas 2 bred av + /// emoji-fonter — då förskjuts allt efter ikonen åt höger i UI:t. + #[test] + fn emoji_icons_are_wide() { + let mut samples: Vec = vec![ + entry("dir", FileType::Directory), + entry("link", FileType::Symlink), + ]; + for ext in [ + "rs", "png", "mp4", "mp3", "zip", "pdf", "txt", "json", "sh", + "py", "js", "c", "exe", "unknown", + ] { + samples.push(entry(&format!("f.{}", ext), FileType::File)); + } + for e in &samples { + let icon = e.icon(); + assert_eq!( + UnicodeWidthStr::width(icon), 2, + "ikonen {:?} (för {:?}) är inte EAW Wide — den kommer \ + förskjuta raden åt höger i terminaler med emoji-font", + icon, e.name + ); + } + } + + /// ASCII-fallback-ikonerna ska alltid vara exakt 3 tecken ([/], + /// [s], ...) så att kolumnerna ligger fast i ASCII-läge. + #[test] + fn ascii_icons_are_three_cols() { + let e = entry("f.rs", FileType::File); + assert_eq!(UnicodeWidthStr::width(e.icon_ascii()), 3); + let d = entry("dir", FileType::Directory); + assert_eq!(UnicodeWidthStr::width(d.icon_ascii()), 3); + } +} diff --git a/src/ui.rs b/src/ui.rs index 1693d9e..338cf9f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -156,9 +156,12 @@ fn draw_sidebar(f: &mut Frame, app: &App, area: Rect) { // ── Go-up button ────────────────────────────────────────────────────────── if app.unicode_support { + // ↑ U+2191 är ett rent textglyf (ingen emoji-presentation) → + // ritas alltid 1 kolumn brett, till skillnad från ⬆ U+2B06 som + // emoji-fonter ritar 2 brett fast unicode-width mäter 1. lines.push(Line::from(vec![ Span::raw(" "), - Span::styled("⬆", go_up_s), + Span::styled("↑", go_up_s), Span::styled(" Gå upp", go_up_s), ])); } else { @@ -496,8 +499,11 @@ fn draw_bottom_bar(f: &mut Frame, app: &App, area: Rect) { // Gear button at bottom-right let gear_x = inner.x + inner.width.saturating_sub(5); + // ≡ U+2261 är ett textglyf som alla monospace-fonter ritar 1 brett; + // ⚙ U+2699 (EAW=Neutral) ritas 2 brett av emoji-fonter och förskjuter + // raden åt höger. let (gear_label, gear_w) = if app.unicode_support { - (" \u{2699} ", 4u16) + (" \u{2261} ", 4u16) } else { (" [S] ", 5u16) };