From 5993ef0b4b14514ea261c7f859d9ae4624730c41 Mon Sep 17 00:00:00 2001 From: xscriptor Date: Fri, 6 Nov 2026 09:09:05 +0100 Subject: [PATCH 1/5] chore: migrate plugins to SDK 0.3.0 Update the workspace to xfetch-plugin-api 0.3.0 and xfetch-guest-api 0.3.0. The JSON protocol and WIT are unchanged; plugin code, manifests and catalog versions remain as they were. --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 23 +++++++++++++++++------ Cargo.toml | 4 ++-- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 877cc03..c313a51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2026-11-06 — SDK 0.3.0 migration + +- Updated the workspace to `xfetch-plugin-api` 0.3.0 and + `xfetch-guest-api` 0.3.0. +- The JSON protocol remains version 1 and the WIT remains + `xfetch:runtime@0.1.0`. +- Plugin code, manifests and catalog versions are unchanged; this is a + dependency alignment with the core 0.3.0 SDK. + ## 2026-11-03 — v1.0.2 Release train v1.0.2 (catalog schema 1, catalog_version 1.0.2). diff --git a/Cargo.lock b/Cargo.lock index d772adb..221fc97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "base64" -version = "0.22.1" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "bitflags" @@ -419,11 +419,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "xfetch-api-common" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7a35b6c4f442aea26e73bb821978a637bd31261d80d4e97c11185876dd684c" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "xfetch-guest-api" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af1966a017fadc5f8412d7e8e28ea4774d436004caaaeb2a5174a581a790e2" +checksum = "a48b8ca8b0a8f1218f09dbd93570cef036cd9bd19f9b3763e25b912ad1805ab3" dependencies = [ "base64", "serde", @@ -441,12 +451,13 @@ dependencies = [ [[package]] name = "xfetch-plugin-api" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54bc785c4c4bb18d4041edff98baeff88a9ff5595b52212a3a01c7d56e74fb8d" +checksum = "0455e40808edcf2da4ced9c032eaee0c89b130ab76aa214257b8d90d1a5f5df5" dependencies = [ "serde", "serde_json", + "xfetch-api-common", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 379fb22..2490def 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,5 +21,5 @@ members = [ resolver = "2" [workspace.dependencies] -xfetch-plugin-api = { version = "0.2.0", package = "xfetch-plugin-api" } -xfetch-guest-api = { version = "0.2.0", package = "xfetch-guest-api" } +xfetch-plugin-api = { version = "0.3.0", package = "xfetch-plugin-api" } +xfetch-guest-api = { version = "0.3.0", package = "xfetch-guest-api" } From 8d83ea6f8168fab0e7a4ed37a5f3dd74a4e07088 Mon Sep 17 00:00:00 2001 From: xscriptor Date: Fri, 6 Nov 2026 09:53:32 +0100 Subject: [PATCH 2/5] fix(banner): keep proverbs inside the logo width Measure logo and banner lines with terminal display width so ANSI colour codes are ignored, and wrap long banner text to the logo width instead of widening the logo block. Banner 0.2.1; catalog regenerated. --- CHANGELOG.md | 6 ++ Cargo.lock | 27 ++++++++- index.json | 2 +- plugins/banner/Cargo.toml | 3 +- plugins/banner/README.md | 6 +- plugins/banner/src/main.rs | 121 +++++++++++++++++++++++++++++-------- 6 files changed, 134 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c313a51..2b05b28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2026-11-06 — Banner width fix + +- `banner` 0.2.1: centre the proverb against the visible logo width using + Unicode terminal width instead of counting ANSI escape bytes. Coloured logos + no longer widen the logo block or push the box layout to the right. + ## 2026-11-06 — SDK 0.3.0 migration - Updated the workspace to `xfetch-plugin-api` 0.3.0 and diff --git a/Cargo.lock b/Cargo.lock index 221fc97..f08d2d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,18 @@ version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +[[package]] +name = "console" +version = "0.16.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys", +] + [[package]] name = "dirs" version = "7.0.0" @@ -45,6 +57,12 @@ dependencies = [ "objc2", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "itoa" version = "1.0.18" @@ -278,6 +296,12 @@ version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" +[[package]] +name = "unicode-width" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557" + [[package]] name = "winapi" version = "0.3.9" @@ -462,8 +486,9 @@ dependencies = [ [[package]] name = "xfetch-plugin-banner" -version = "0.2.0" +version = "0.2.1" dependencies = [ + "console", "xfetch-plugin-api", ] diff --git a/index.json b/index.json index cd61989..1ec668b 100644 --- a/index.json +++ b/index.json @@ -14,7 +14,7 @@ { "name": "banner", "description": "Xfetch logo-animation plugin that prints a Chinese/Japanese proverb banner under (or above) the logo", - "version": "0.2.0", + "version": "0.2.1", "kind": "logo_animation", "runtime": "native", "path": "plugins/banner", diff --git a/plugins/banner/Cargo.toml b/plugins/banner/Cargo.toml index 8bdd1ef..f88464a 100644 --- a/plugins/banner/Cargo.toml +++ b/plugins/banner/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "xfetch-plugin-banner" -version = "0.2.0" +version = "0.2.1" edition = "2024" license = "MIT" description = "Xfetch logo-animation plugin that prints a Chinese/Japanese proverb banner under (or above) the logo" [dependencies] +console = "0.16.6" xfetch-plugin-api = { workspace = true } diff --git a/plugins/banner/README.md b/plugins/banner/README.md index 612ac2c..43dbc7a 100644 --- a/plugins/banner/README.md +++ b/plugins/banner/README.md @@ -28,8 +28,10 @@ without modifying the logo file. Set `frames_path` to a text file to use its first `===`-separated frame as the banner text instead of the built-in list. -The banner is centred to the logo width, counting CJK characters as two -columns. Requires a terminal with a CJK font (see the repo notes) and a TTY. +The banner is centred to the logo width, counting terminal display width so +ANSI colour codes are ignored. Longer banners wrap to the logo width instead +of widening the logo block, which keeps box layouts aligned. Requires a +terminal with a CJK font (see the repo notes) and a TTY. ## Proverb list diff --git a/plugins/banner/src/main.rs b/plugins/banner/src/main.rs index 526fcf8..d812dc8 100644 --- a/plugins/banner/src/main.rs +++ b/plugins/banner/src/main.rs @@ -61,7 +61,11 @@ fn build_frame(request: &LogoAnimationRequest) -> AnimationFrame { let above = style.contains("above"); let text = banner_text(request, style); let width = logo_width(&request.lines); - let banner: Vec = text.iter().map(|line| center(line, width)).collect(); + let banner: Vec = text + .iter() + .flat_map(|line| wrap(line, width)) + .map(|line| center(&line, width)) + .collect(); let mut lines = Vec::new(); if above { @@ -111,6 +115,38 @@ fn logo_width(lines: &[String]) -> usize { .unwrap_or(0) } +/// Splits a banner line into chunks that fit the logo width. +/// +/// CJK text has no spaces, so wrapping is per character and each chunk is +/// measured with the same terminal-width rules used for centering. This keeps +/// the logo block from growing horizontally and pushing a box layout right. +fn wrap(text: &str, width: usize) -> Vec { + if width == 0 { + return vec![text.to_string()]; + } + + let mut lines = Vec::new(); + let mut current = String::new(); + let mut current_width = 0; + + for ch in text.chars() { + let ch_width = display_width(&ch.to_string()); + if current_width + ch_width > width && !current.is_empty() { + lines.push(std::mem::take(&mut current)); + current_width = 0; + } + current.push(ch); + current_width += ch_width; + } + if !current.is_empty() { + lines.push(current); + } + if lines.is_empty() { + lines.push(String::new()); + } + lines +} + fn center(text: &str, width: usize) -> String { let text_width = display_width(text); if text_width >= width { @@ -121,27 +157,14 @@ fn center(text: &str, width: usize) -> String { format!("{}{}{}", " ".repeat(left), text, " ".repeat(right)) } -/// Display width: East Asian wide characters count as two columns. +/// Display width in terminal columns. +/// +/// `console` skips ANSI sequences and uses Unicode display width, so the +/// banner is centred against the logo the user actually sees. The previous +/// character-count implementation treated the escape bytes of coloured logo +/// lines as visible columns and widened the whole logo block. fn display_width(text: &str) -> usize { - text.chars().map(|ch| if is_wide(ch) { 2 } else { 1 }).sum() -} - -fn is_wide(ch: char) -> bool { - let c = ch as u32; - (0x1100..=0x115F).contains(&c) - || (0x2E80..=0x303E).contains(&c) - || (0x3041..=0x33FF).contains(&c) - || (0x3400..=0x4DBF).contains(&c) - || (0x4E00..=0x9FFF).contains(&c) - || (0xA000..=0xA4CF).contains(&c) - || (0xAC00..=0xD7A3).contains(&c) - || (0xF900..=0xFAFF).contains(&c) - || (0xFE30..=0xFE4F).contains(&c) - || (0xFF00..=0xFF60).contains(&c) - || (0xFFE0..=0xFFE6).contains(&c) - || (0x1F300..=0x1F64F).contains(&c) - || (0x1F900..=0x1F9FF).contains(&c) - || (0x20000..=0x3FFFD).contains(&c) + console::measure_text_width(text) } #[cfg(test)] @@ -163,17 +186,27 @@ mod tests { #[test] fn banner_goes_below_the_logo() { let frame = build_frame(&request("banner-static")); - assert_eq!(frame.lines.len(), 4); assert_eq!(frame.lines[0], " ██ "); - assert!(frame.lines[3].contains(PROVERBS[0])); + assert_eq!(frame.lines[1], " ████ "); + assert_eq!(frame.lines[2], ""); + let banner: String = frame.lines[3..].iter().map(|line| line.trim()).collect(); + assert!(banner.contains(PROVERBS[0])); } #[test] fn banner_above_puts_it_first() { let frame = build_frame(&request("banner-above")); - assert!(frame.lines[0].contains(PROVERBS[0])); - assert_eq!(frame.lines[1], ""); - assert_eq!(frame.lines[2], " ██ "); + let blank = frame + .lines + .iter() + .position(String::is_empty) + .expect("blank separator"); + let banner: String = frame.lines[..blank] + .iter() + .map(|line| line.trim()) + .collect(); + assert!(banner.contains(PROVERBS[0])); + assert_eq!(frame.lines[blank + 1], " ██ "); } #[test] @@ -191,6 +224,42 @@ mod tests { assert_eq!(display_width(banner), display_width(" ██ ")); } + #[test] + fn wraps_a_banner_wider_than_the_logo() { + let req = LogoAnimationRequest::new( + vec![" ██ ".to_string()], + Some(vec![vec!["千里之行".to_string()]]), + LogoAnimationArgs { + style: Some("banner-static".to_string()), + ..LogoAnimationArgs::default() + }, + ); + let frame = build_frame(&req); + let banner: Vec<&String> = frame.lines.iter().skip(2).collect(); + assert!(banner.len() > 1, "long banner must wrap"); + for line in banner { + assert!( + display_width(line) <= display_width(" ██ "), + "banner line wider than logo: {line:?}" + ); + } + } + + #[test] + fn centered_line_ignores_ansi_codes() { + let req = LogoAnimationRequest::new( + vec!["\x1b[32m ██ \x1b[0m".to_string()], + Some(vec![vec!["X".to_string()]]), + LogoAnimationArgs { + style: Some("banner".to_string()), + ..LogoAnimationArgs::default() + }, + ); + let frame = build_frame(&req); + let banner = frame.lines.last().expect("banner line"); + assert_eq!(display_width(banner), display_width(" ██ ")); + } + #[test] fn frames_path_overrides_the_list() { let req = LogoAnimationRequest::new( From 9e242084c392cf0f764ecb2bff65d0cece0c7781 Mon Sep 17 00:00:00 2001 From: xscriptor Date: Fri, 6 Nov 2026 10:20:50 +0100 Subject: [PATCH 3/5] docs: record SDK 0.3.0 compatibility --- docs/compatibility.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/compatibility.md b/docs/compatibility.md index 64fa6a6..1faece7 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -1,5 +1,15 @@ # Platform Compatibility +

SDK compatibility

+ +

+ The plugins in this repository target xfetch-plugin-api 0.3.0 + and xfetch-guest-api 0.3.0. The JSON protocol remains + 1 and the WIT package remains + xfetch:runtime@0.1.0. Plugins built against the previous + 0.2.0 SDK remain runtime-compatible. +

+

Platform support per plugin. ✓ = works, ~ = works with caveats (see notes), ✗ = not supported (the plugin From 1caee4de43ea85dca081e3362f219fedeb8ee237 Mon Sep 17 00:00:00 2001 From: xscriptor Date: Fri, 6 Nov 2026 10:29:09 +0100 Subject: [PATCH 4/5] ci: add tests workflow Run fmt, clippy, workspace tests and the catalog check on pull requests and on main after merge. No build or release artifacts. --- .github/workflows/tests.yml | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ccb6478 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,52 @@ +name: Tests + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: tests-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: cargo test + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install stable Rust toolchain + uses: dtolnay/rust-toolchain@18f0487477426dce244608110aa2660c2893deb7 # stable + with: + components: rustfmt, clippy + + - name: Check formatting + run: cargo fmt --all --check + + - name: Run clippy + run: cargo clippy --locked --all-targets -- -D warnings + + - name: Run workspace tests + run: cargo test --workspace --locked --no-fail-fast + + catalog: + name: catalog metadata + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Validate catalog + run: python3 scripts/generate_catalog.py --check From bd95934aef5fde9489b9bbde0eb9481c6c0a42bc Mon Sep 17 00:00:00 2001 From: xscriptor Date: Fri, 6 Nov 2026 13:54:03 +0100 Subject: [PATCH 5/5] docs: record CI and wasm validation --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b05b28..cd7e44d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ Unicode terminal width instead of counting ANSI escape bytes. Coloured logos no longer widen the logo block or push the box layout to the right. +## 2026-11-06 — CI and wasm validation + +- Added a `Tests` workflow: fmt, clippy, workspace tests and catalog + verification on pull requests and on main after merge. It does not build or + publish release artifacts. +- Validated the wasm guests with their native toolchains: Rust + (`wasm-crypto`), componentize-py (`wasm-ip-geo`), Go (`wasm-pacman`) and + clang/wasm-ld (`wasm-proc`), including runtime checks against xfetch 1.0.3. + ## 2026-11-06 — SDK 0.3.0 migration - Updated the workspace to `xfetch-plugin-api` 0.3.0 and