Replace ambiguous-width icons that shift sidebar/file rows right

⬆ (U+2B06), 🖼 (U+1F5BC) and ⚙ (U+2699) are East-Asian-Width
Neutral: unicode-width measures 1 column but emoji fonts draw 2, so
every row containing them shifted right by one column. Replaced with
真 Wide emoji (📷, 🔩, ) or pure text glyphs (↑, ≡), plus a
regression test asserting every emoji icon measures 2 columns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 09:33:07 +02:00
parent 91c520c86a
commit fde2087274
3 changed files with 69 additions and 5 deletions

Binary file not shown.

View File

@@ -30,19 +30,24 @@ impl FileEntry {
.unwrap_or("") .unwrap_or("")
.to_lowercase(); .to_lowercase();
match ext.as_str() { 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" => "🦀", "rs" => "🦀",
"png" | "jpg" | "jpeg" | "gif" | "bmp" | "svg" | "webp" | "ico" => "🖼", "png" | "jpg" | "jpeg" | "gif" | "bmp" | "svg" | "webp" | "ico" => "📷",
"mp4" | "mkv" | "avi" | "mov" | "webm" => "🎬", "mp4" | "mkv" | "avi" | "mov" | "webm" => "🎬",
"mp3" | "flac" | "ogg" | "wav" | "aac" => "🎵", "mp3" | "flac" | "ogg" | "wav" | "aac" => "🎵",
"zip" | "tar" | "gz" | "bz2" | "xz" | "7z" | "rar" => "📦", "zip" | "tar" | "gz" | "bz2" | "xz" | "7z" | "rar" => "📦",
"pdf" => "📕", "pdf" => "📕",
"txt" | "md" | "log" => "📄", "txt" | "md" | "log" => "📄",
"json" | "yaml" | "yml" | "toml" | "xml" => "", "json" | "yaml" | "yml" | "toml" | "xml" => "🔩",
"sh" | "bash" | "zsh" | "fish" => "🐚", "sh" | "bash" | "zsh" | "fish" => "🐚",
"py" => "🐍", "py" => "🐍",
"js" | "ts" => "📜", "js" | "ts" => "📜",
"c" | "cpp" | "h" | "hpp" => "🔧", "c" | "cpp" | "h" | "hpp" => "🔧",
"exe" | "bin" | "out" | "run" => "", "exe" | "bin" | "out" | "run" => "",
_ => "📎", _ => "📎",
} }
} }
@@ -294,3 +299,56 @@ pub fn get_root_dirs() -> Vec<PathBuf> {
drives 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<FileEntry> = 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);
}
}

View File

@@ -156,9 +156,12 @@ fn draw_sidebar(f: &mut Frame, app: &App, area: Rect) {
// ── Go-up button ────────────────────────────────────────────────────────── // ── Go-up button ──────────────────────────────────────────────────────────
if app.unicode_support { 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![ lines.push(Line::from(vec![
Span::raw(" "), Span::raw(" "),
Span::styled("", go_up_s), Span::styled("", go_up_s),
Span::styled(" Gå upp", go_up_s), Span::styled(" Gå upp", go_up_s),
])); ]));
} else { } else {
@@ -496,8 +499,11 @@ fn draw_bottom_bar(f: &mut Frame, app: &App, area: Rect) {
// Gear button at bottom-right // Gear button at bottom-right
let gear_x = inner.x + inner.width.saturating_sub(5); 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 { let (gear_label, gear_w) = if app.unicode_support {
(" \u{2699} ", 4u16) (" \u{2261} ", 4u16)
} else { } else {
(" [S] ", 5u16) (" [S] ", 5u16)
}; };