feat(config): make listsym_rejected configurable (P2)

The rejected/cancelled checkbox glyph was a hardcoded `const REJECTED = '-'`.
ListSyms now carries a `rejected` field (new_with_rejected); a per-wiki
`listsym_rejected` key (default `-`) is added to WikiConfig, and
WikiConfig::list_syms() builds the palette with it. Routed the three
config-driven ListSyms construction sites (parse path in lib.rs + the
toggle/cycle commands) through list_syms(), so a custom rejected glyph both
lexes and round-trips.

Tests: listsyms::custom_rejected_glyph_is_honoured + config round-trip.
README/doc/lua config comment updated. fmt clean; 0 Rust test failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 18:33:25 +00:00
parent 624bccbe50
commit 2d3db458fb
9 changed files with 76 additions and 17 deletions
+36 -8
View File
@@ -10,32 +10,47 @@
use crate::ast::CheckboxState;
/// Default rejected/cancelled marker (vimwiki's `listsym_rejected`).
const REJECTED: char = '-';
/// An ordered checkbox progress palette. Always holds at least two glyphs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListSyms {
progression: Vec<char>,
rejected: char,
}
impl Default for ListSyms {
fn default() -> Self {
Self {
progression: " .oOX".chars().collect(),
rejected: REJECTED,
}
}
}
impl ListSyms {
/// Build a palette from a glyph string. Falls back to the default when
/// fewer than two glyphs are supplied (a one-symbol palette can't express
/// both "empty" and "done").
/// Build a palette from a glyph string, with the default rejected marker.
/// Falls back to the default when fewer than two glyphs are supplied (a
/// one-symbol palette can't express both "empty" and "done").
pub fn new(s: &str) -> Self {
Self::new_with_rejected(s, REJECTED)
}
/// Like [`new`](Self::new) but with a custom rejected/cancelled glyph
/// (vimwiki's `listsym_rejected`).
pub fn new_with_rejected(s: &str, rejected: char) -> Self {
let progression: Vec<char> = s.chars().collect();
if progression.len() < 2 {
Self::default()
Self {
rejected,
..Self::default()
}
} else {
Self { progression }
Self {
progression,
rejected,
}
}
}
@@ -56,7 +71,7 @@ impl ListSyms {
}
pub fn rejected_glyph(&self) -> char {
REJECTED
self.rejected
}
pub fn glyph_at(&self, index: usize) -> char {
@@ -76,7 +91,7 @@ impl ListSyms {
/// Normalised five-bucket [`CheckboxState`] for a glyph, or `None` when the
/// glyph is not part of this palette (and is not the rejected marker).
pub fn state_of(&self, c: char) -> Option<CheckboxState> {
if c == REJECTED {
if c == self.rejected {
return Some(CheckboxState::Rejected);
}
let index = self.index_of(c)?;
@@ -96,7 +111,7 @@ impl ListSyms {
/// `ceil` rule, matching the stock five-symbol behaviour.
pub fn glyph_for_rate(&self, rate: i32) -> char {
if rate < 0 {
return REJECTED;
return self.rejected;
}
if rate <= 0 {
return self.empty_glyph();
@@ -157,6 +172,19 @@ mod tests {
assert_eq!(ListSyms::new("x"), ListSyms::default());
}
#[test]
fn custom_rejected_glyph_is_honoured() {
// `listsym_rejected = '✗'`: that glyph lexes/snaps as Rejected and the
// default `-` no longer does.
let s = ListSyms::new_with_rejected(" .oOX", '✗');
assert_eq!(s.rejected_glyph(), '✗');
assert_eq!(s.state_of('✗'), Some(CheckboxState::Rejected));
assert_eq!(s.glyph_for_rate(-1), '✗');
assert_eq!(s.state_of('-'), None); // no longer the rejected marker
// A short palette still keeps the custom rejected glyph.
assert_eq!(ListSyms::new_with_rejected("x", '✗').rejected_glyph(), '✗');
}
#[test]
fn custom_palette_buckets_to_nearest_state() {
// 3 glyphs → rates 0 / 50 / 100.