From f2dcf743984158deb17e8a46c2fa7af48ae9e526 Mon Sep 17 00:00:00 2001 From: xscriptor Date: Sat, 7 Nov 2026 05:59:49 +0100 Subject: [PATCH] fix(banner-text): keep wide characters inside the box width --- CHANGELOG.md | 7 +++++++ Cargo.lock | 3 ++- index.json | 2 +- plugins/banner-text/Cargo.toml | 3 ++- plugins/banner-text/README.md | 4 ++++ plugins/banner-text/src/main.rs | 36 +++++++++++++++++++++++++++++++-- 6 files changed, 50 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4252a3..cf92895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2026-11-07 — banner-text 0.1.1 + +- `banner-text` 0.1.1: add invisible zero-width padding after wide characters + so the current box layouts keep the right border aligned. It is a + compatibility shim for the character-count width calculation; the rendered + text is unchanged. + ## 2026-11-06 — SDK 0.3.1 migration - Updated the workspace to `xfetch-plugin-api` 0.3.1 and diff --git a/Cargo.lock b/Cargo.lock index a8cc682..257ce9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -493,8 +493,9 @@ dependencies = [ [[package]] name = "xfetch-plugin-banner-text" -version = "0.1.0" +version = "0.1.1" dependencies = [ + "console", "serde", "xfetch-plugin-api", ] diff --git a/index.json b/index.json index 1ec668b..6a25931 100644 --- a/index.json +++ b/index.json @@ -23,7 +23,7 @@ { "name": "banner-text", "description": "Xfetch info plugin that emits text lines from a file or literal argument", - "version": "0.1.0", + "version": "0.1.1", "kind": "info_provider", "runtime": "native", "path": "plugins/banner-text", diff --git a/plugins/banner-text/Cargo.toml b/plugins/banner-text/Cargo.toml index b46b10d..c850c5d 100644 --- a/plugins/banner-text/Cargo.toml +++ b/plugins/banner-text/Cargo.toml @@ -1,10 +1,11 @@ [package] name = "xfetch-plugin-banner-text" -version = "0.1.0" +version = "0.1.1" edition = "2024" license = "MIT" description = "Xfetch info plugin that emits text lines from a file or literal argument" [dependencies] +console = "0.16.6" serde = { version = "1", features = ["derive"] } xfetch-plugin-api = { workspace = true } diff --git a/plugins/banner-text/README.md b/plugins/banner-text/README.md index 3f9b0b7..5d1d5ed 100644 --- a/plugins/banner-text/README.md +++ b/plugins/banner-text/README.md @@ -74,6 +74,10 @@ Each line is emitted separately. Trailing whitespace is removed and leading or trailing empty lines are discarded. Leading spaces on non-empty lines are preserved. +Wide characters (CJK, emoji) are followed by invisible zero-width spaces so the +current box layouts, which count characters instead of terminal columns, keep +the right border aligned. The rendered text is unchanged. + ## Output Given a file containing `永遠は朽ち、儚さが記憶を刻む`, the plugin returns: diff --git a/plugins/banner-text/src/main.rs b/plugins/banner-text/src/main.rs index 613f29d..b804355 100644 --- a/plugins/banner-text/src/main.rs +++ b/plugins/banner-text/src/main.rs @@ -55,7 +55,7 @@ fn resolve_text(args: &PluginArgs) -> String { fn normalize_lines(raw: &str, blank_line: bool) -> Vec { let mut lines: Vec = raw .lines() - .map(|line| line.trim_end().to_string()) + .map(|line| compensate_wide_chars(line.trim_end())) .collect(); while lines.first().is_some_and(|line| line.is_empty()) { lines.remove(0); @@ -69,6 +69,25 @@ fn normalize_lines(raw: &str, blank_line: bool) -> Vec { lines } +/// Adds zero-width spaces after wide characters. +/// +/// The current box layouts measure each row with `chars().count()`, so a CJK +/// character counts as one column even though terminals render it as two. The +/// invisible padding makes that character count match the visible width, which +/// keeps the right border aligned. It is harmless once the core measures the +/// display width instead. +fn compensate_wide_chars(line: &str) -> String { + let mut out = String::with_capacity(line.len()); + for ch in line.chars() { + out.push(ch); + let width = console::measure_text_width(&ch.to_string()); + for _ in 1..width { + out.push('\u{200b}'); + } + } + out +} + fn expand_path(path: &str) -> PathBuf { if path == "~" { return home_dir().unwrap_or_else(|| PathBuf::from(path)); @@ -91,7 +110,7 @@ fn home_dir() -> Option { #[cfg(test)] mod tests { - use super::normalize_lines; + use super::{compensate_wide_chars, normalize_lines}; #[test] fn trims_trailing_whitespace_and_edge_blank_lines() { @@ -109,4 +128,17 @@ mod tests { fn empty_text_returns_no_lines() { assert!(normalize_lines(" \n\t\n", true).is_empty()); } + + #[test] + fn wide_characters_get_zero_width_padding() { + let line = compensate_wide_chars("秘密の距離"); + assert_eq!(line.chars().count(), 10); + assert_eq!(line.matches('\u{200b}').count(), 5); + assert_eq!(console::measure_text_width(&line), 10); + } + + #[test] + fn ascii_is_not_padded() { + assert_eq!(compensate_wide_chars("banner"), "banner"); + } }