Compare commits

...

2 Commits

Author SHA1 Message Date
Julien Calixte
93f3a43b34 docs(prefs): document theme + auto_sync presets, override v0.5 deferral
Reference doc gains a theme key, an Options column, and the e-paper dark-mode
ghosting caveat. The v0.5 spec gets an amendment note superseding the two
reversed "decide-before-build" calls (auto_sync now a set-ahead palette
command; theme shipped early) without rewriting the verified-on-device
history; macroplan updated to match.
2026-07-12 09:02:21 +02:00
Julien Calixte
f37da21462 feat(editor): add theme and auto_sync preset prefs with rotate-on-Enter
Generalise the palette so Enter advances any pref to its next value and
wraps; a boolean toggle is the two-option case. Both string prefs share a
next_option(current, &OPTIONS) helper (off-list values snap to the head).

- theme (light/dark): rendered as a single whole-frame Frame::invert at the
  end of draw(), so text, selection, caret, panel and palette flip together.
- auto_sync: cycles 2m/5m/10m/15m/30m. Set-ahead only — still read by
  nothing until the v0.7 periodic push.

toggle_pref -> cycle_pref; palette hint "Enter toggle" -> "Enter change".
2026-07-12 09:02:15 +02:00
5 changed files with 245 additions and 51 deletions

View File

@@ -76,6 +76,16 @@ impl Frame {
pub fn bytes(&self) -> &[u8] {
&self.buf
}
/// Flip every pixel black↔white across the whole framebuffer. The editor
/// draws its native black-ink-on-white-paper frame, then calls this once at
/// the end for the dark theme — so text, selection, caret, panel and palette
/// all invert together and each stays legible against the flipped ground.
pub fn invert(&mut self) {
for b in &mut self.buf {
*b = !*b;
}
}
}
impl OriginDimensions for Frame {

View File

@@ -55,7 +55,7 @@ name = "v0.5 palette + multi-file"
start = 2026-09-07
original = 2026-09-28
delivered = 2026-07-12
learning = "Delivered 2026-07-12, well ahead of the 2026-09-28 baseline, and fully on-device confirmed. Four slices: the drained Effect queue + parked-buffer LRU foundation; the Cmd-P fuzzy file palette (Spike 11 — no ghosting on the transient panel); :enew + file delete (Spike 14 caught that add_all alone doesn't stage a deletion on this libgit2 — fixed with update_all, i.e. git add -A); and the git-tracked .typoena.toml prefs with a stay-open palette `>` command mode + :settings. Both directions of the prefs loop are proven on hardware — boot-read (byte-exact parse) and on-device palette edit (a device publish flipped line_numbers on origin). Three decide-before-build calls: the idle auto-save is unformatted, and both the per-device auto_sync override and the `> auto sync` command are deferred to v0.7 where auto_sync gains behaviour. Descoped from v0.5 (not the four slices): explicit buffer close, the grey-Publish-in-Local panel cue, and the multi-file publish count."
learning = "Delivered 2026-07-12, well ahead of the 2026-09-28 baseline, and fully on-device confirmed. Four slices: the drained Effect queue + parked-buffer LRU foundation; the Cmd-P fuzzy file palette (Spike 11 — no ghosting on the transient panel); :enew + file delete (Spike 14 caught that add_all alone doesn't stage a deletion on this libgit2 — fixed with update_all, i.e. git add -A); and the git-tracked .typoena.toml prefs with a stay-open palette `>` command mode + :settings. Both directions of the prefs loop are proven on hardware — boot-read (byte-exact parse) and on-device palette edit (a device publish flipped line_numbers on origin). Three decide-before-build calls: the idle auto-save is unformatted, and both the per-device auto_sync override and the `> auto sync` command are deferred to v0.7 where auto_sync gains behaviour. Amended 2026-07-12: a light/dark `theme` key and a set-ahead `> auto sync` preset command (2m/5m/10m/15m/30m) were added on top — the palette generalised so Enter rotates any pref to its next value (a bool is the two-option case); auto_sync is still read by nothing until v0.7. Descoped from v0.5 (not the four slices): explicit buffer close, the grey-Publish-in-Local panel cue, and the multi-file publish count."
[[feature]]
name = "v0.6 markdown"
@@ -202,8 +202,9 @@ reconnect, and on-device provisioning (the first release usable by a non-author)
## v1.0 — Polish — [ ]
≤ 3 s boot, runtime-switchable fonts and light/dark theme, enclosure files, and
a user guide. **Not started.** Detail: [v1.0-polish.md](v1.0-polish.md).
≤ 3 s boot, runtime-switchable fonts, enclosure files, and a user guide
(light/dark theme landed early, in v0.5). **Not started.** Detail:
[v1.0-polish.md](v1.0-polish.md).
## v1.x — Stretch / nice-to-have

View File

@@ -1,8 +1,9 @@
# `.typoena.toml` — editor preferences
> The git-tracked file that controls how the editor behaves — auto-save,
> format-on-save, and the line-number gutter. Hand-editable, or toggled live
> from the `Cmd-P` palette. Landed in **v0.5** (see
> format-on-save, the line-number gutter, and the panel theme. Hand-editable, or
> changed live from the `Cmd-P` palette (booleans flip; the theme and auto-sync
> interval rotate through preset options on **Enter**). Landed in **v0.5** (see
> [`macroplan.md`](macroplan.md)).
>
> **Not to be confused with `/sd/typoena.conf`** — that holds the device
@@ -20,8 +21,8 @@ It lives inside the Tracked repo (`/sd/repo`), so it is **committed and pushed**
like any note — which means the preferences **sync to every device** that clones
the repo. That is deliberate: your editor behaviour follows you. (A per-device
override for the one genuinely device-specific key, `auto_sync`, may layer on top
later via `typoena.conf`deferred until `auto_sync` actually does something in
v0.7. See the [auto_sync](#auto_sync) note.)
later via `typoena.conf`worth it only once `auto_sync` actually does something
in v0.7. See the [auto_sync](#auto_sync) note.)
The file is read **once at boot**, before the first screen is drawn (so
`line_numbers` shapes the opening frame). A **missing, empty, or partial file is
@@ -30,21 +31,27 @@ works with no config present.
## Keys
| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `save_on_idle` | bool | `true` | Auto-save the current buffer on the idle typing-pause, so `:w` is optional. |
| `format_on_save` | bool | `true` | Run `:fmt` on the buffer before an explicit `:w`/`:sync`. |
| `line_numbers` | bool | `true` | Show the absolute line-number gutter. Off reclaims its columns for text. |
| `auto_sync` | string | `"10m"` | Max-staleness cap for opportunistic auto-publish. **Schema only in v0.5 — no behaviour yet.** |
| Key | Type | Default | Options | Effect |
| --- | --- | --- | --- | --- |
| `save_on_idle` | bool | `true` | `true` / `false` | Auto-save the current buffer on the idle typing-pause, so `:w` is optional. |
| `format_on_save` | bool | `true` | `true` / `false` | Run `:fmt` on the buffer before an explicit `:w`/`:sync`. |
| `line_numbers` | bool | `true` | `true` / `false` | Show the absolute line-number gutter. Off reclaims its columns for text. |
| `theme` | string | `"light"` | `light` / `dark` | Panel colour polarity. `dark` inverts the whole frame to white-on-black. |
| `auto_sync` | string | `"10m"` | `2m` / `5m` / `10m` / `15m` / `30m` | Max-staleness cap for opportunistic auto-publish. **Value only — no behaviour yet** (rides v0.7). |
The **Options** column is what the palette rotates through on **Enter**; a
boolean is just the two-option case. Hand-editing a string key can still set any
value — the palette only cycles the presets.
### Example
```toml
# Typoena editor preferences — hand-editable, git-tracked.
# Edit here, or toggle live from the Cmd-P palette (type `>`).
# Edit here, or change live from the Cmd-P palette (type `>`).
save_on_idle = true
format_on_save = true
line_numbers = true
theme = "light"
auto_sync = "10m"
```
@@ -82,14 +89,29 @@ returns the gutter's columns to the text, so prose gets the full writing width.
Applied **live** — toggling it from the palette redraws immediately with (or
without) the gutter.
### `theme`
Panel colour polarity: `light` (the native black-ink-on-white-paper) or `dark`
(white-on-black). On the 1-bit e-paper panel this is not a palette swap but a
**whole-frame invert** applied at the very end of the render, so text, selection,
caret, side panel and command palette all flip together and each stays legible.
Any value other than `dark` reads as light. Applied **live** — cycling it from
the palette repaints inverted at once.
> **On e-paper, `dark` is not free.** Partial refreshes over a mostly-black field
> ghost more than over white, and the panel is tuned for white-background reading.
> It works, but expect a slightly muddier refresh than `light` — verify on-device.
### `auto_sync`
A duration string (`"10m"`, `"2m"`, `"0"`/empty to disable) that will one day cap
how stale the published copy is allowed to get — an *opportunistic, rate-limited*
push, not a wall-clock timer. **In v0.5 this is schema + default only:** the value
is parsed, preserved through a round-trip, and shown nowhere editable **nothing
reads it yet.** The periodic push itself rides the better-git work in v0.7 and
must interact with sleep in v0.8. Rationale for the `"10m"` default:
A duration string that will one day cap how stale the published copy is allowed
to get — an *opportunistic, rate-limited* push, not a wall-clock timer. The
palette rotates it through the presets `2m` / `5m` / `10m` / `15m` / `30m`
(hand-editing can still set any string, e.g. `"0"`/empty to disable). **The value
is only stored and displayed in v0.5 — nothing reads it yet:** the periodic push
rides the better-git work in v0.7 and must interact with sleep in v0.8, so
cycling the interval today changes what will be honoured *then*, not now.
Rationale for the `"10m"` default:
[`tradeoff-curves/wifi-auto-sync.md`](tradeoff-curves/wifi-auto-sync.md).
## Editing it
@@ -104,22 +126,27 @@ Two ways, both landing in the same file:
- **`Cmd-P`** then type **`>`** — switches the file palette to the command
list (VS Code semantics).
The three boolean prefs appear as toggles carrying their current state:
Every pref appears carrying its current state:
```
> save on idle: on
format on save: on
line numbers: on
theme: light
auto sync: 10m
```
`Ctrl-N`/`Ctrl-P` move the selection; **Enter** flips the selected pref,
applies it at once, writes the change back to `.typoena.toml`, and confirms
the new state on the snackbar (e.g. `line numbers: off - saved`). **The list
stays open** so you can flip several prefs in a row; **Esc** (or `Cmd-P`)
closes it. Each change rides the next `:sync` to your other devices.
`Ctrl-N`/`Ctrl-P` move the selection; **Enter** advances the selected pref to
its next value, applies it at once, writes the change back to `.typoena.toml`,
and confirms the new state on the snackbar (e.g. `theme: dark - saved`). A
boolean flips; the theme and auto-sync interval **rotate through their preset
options and wrap** — same key, so the palette is uniformly "press Enter to
change". **The list stays open** so you can change several prefs in a row;
**Esc** (or `Cmd-P`) closes it. Each change rides the next `:sync` to your
other devices.
`auto_sync` is **not** a palette command in v0.5 (it has no behaviour to
drive yet); it returns as a value command in v0.7.
`auto_sync` is a value command now, but has no behaviour to drive until v0.7 —
cycling it sets the interval that the future periodic push will honour.
## Parsing

View File

@@ -108,6 +108,25 @@ true via a *device*-authored publish (`3c79f38`), proving toggle → `SavePrefs`
atomic write → `git add -A` → push — and the `save_on_idle` autosave works on
device too. **v0.5 slice 4 fully DONE + on-device confirmed.**
**Amendment 2026-07-12 — non-boolean prefs (`theme`, `auto_sync`).** Two of the
"decide before build" calls above are **superseded**: `auto_sync` is now a live
palette command, and a new `theme` (`light`/`dark`) key ships. The generalising
idea is that a boolean toggle is just the two-option case of *rotate through a
preset list on Enter* — so the palette gains one uniform gesture: **Enter
advances the selected pref to its next value and wraps** (a bool flips; a string
pref cycles its options). `theme` rotates `light``dark` and is applied by a
single whole-frame invert at the end of the render ([`Frame::invert`]), so text,
selection, caret, panel and palette all flip together; `auto_sync` rotates
`2m`/`5m`/`10m`/`15m`/`30m`. Decision (3) — "a value control that changes nothing
readable would be a dead switch" — is knowingly overridden: cycling `auto_sync`
persists and displays the interval, but **still drives no behaviour until v0.7**;
we accept a set-ahead control so the surface is ready and the value syncs now.
Decision (2) (per-device `typoena.conf` override) stays deferred. Kept simple: no
enum machinery — both string prefs share a `next_option(current, &OPTIONS)`
helper, and hand-editing the TOML can still set any value (the palette only
cycles presets; an off-list value snaps to the head on the next Enter). Editor
tests cover the rotate/wrap/snap, the live theme invert, and the round-trip.
**Trailing-newline handling — a saved note ends with a *visible* blank line
(commit `d14d9e7`, 2026-07-12; host-tested, on-device gate open).** Two
adjustments to how the buffer meets the file. First, format-on-save no longer
@@ -186,6 +205,12 @@ one.
gutter (built always-on in v0.2). Off reclaims the gutter's columns for
text (`gutter_cols()` → 0); the palette `> line numbers: on/off` command
toggles it live. **Done 2026-07-12.**
- [x] `theme` (string, default `"light"`; options `light`/`dark`) — panel
colour polarity. `dark` is a single whole-frame invert at the end of the
render (`Frame::invert`), so everything flips together and stays legible.
Palette `> theme` rotates `light``dark` live. **Added 2026-07-12
(amendment); host-tested, on-device check pending.** Caveat: `dark`
partial-refreshes ghost more on e-paper than `light` — verify on panel.
- [ ] `auto_sync` (duration string, default `"10m"`; `"0"` / omitted
disables; **min clamp ~`"2m"`** so a palette typo can't drain the
battery) — a *max-staleness cap*, not a wall-clock timer:
@@ -200,6 +225,9 @@ one.
through `Prefs`; **nothing reads the value yet** — the periodic side
rides the better-git work (v0.7) and must interact with light / deep
sleep (v0.8). Marked `[~]`: parsed and preserved, no behaviour.
**Amended 2026-07-12:** now a palette preset command — Enter rotates it
through `2m`/`5m`/`10m`/`15m`/`30m` (the `~2m` min is baked into the
preset list). Set-ahead only: still read by nothing until v0.7.
- [x] Open question RESOLVED (2026-07-12): the per-device sync cadence override
(a card-local `typoena.conf` layer over the committed prefs) is
**deferred**`auto_sync` is inert in v0.5, so there is nothing yet to
@@ -213,6 +241,9 @@ one.
**The list stays open after a toggle** (flip several, Esc/`Cmd-P` closes),
and **`:settings` opens it directly** — both added 2026-07-12 as the "change
config from the device" surface (chosen over a separate settings modal).
This command list is the discoverable surface later actions (`:fmt`, theme,
font) also register into. **`> auto sync: <dur>` deferred to v0.7** — a
value control that changes nothing readable would be a dead switch.
This command list is the discoverable surface later actions (`:fmt`, font)
also register into. **Amended 2026-07-12:** the palette now also carries the
non-boolean prefs `> theme` (`light`/`dark`, live whole-frame invert) and
`> auto sync` (`2m`..`30m`), both cycled by the same Enter-rotates-to-next
gesture. `auto_sync` is exposed **set-ahead** (no behaviour until v0.7),
knowingly overriding the earlier "dead switch" call.

View File

@@ -181,10 +181,17 @@ pub struct Prefs {
/// Show the absolute line-number gutter (built always-on in v0.2). Off
/// reclaims the gutter's columns for text — applied live by [`gutter_cols`].
pub line_numbers: bool,
/// Max-staleness cap for opportunistic auto-publish, as a duration string
/// (`"10m"`, `"0"`/empty disables). **Schema + default only in v0.5** — the
/// periodic push itself rides v0.7/v0.8, so nothing reads this yet; it is
/// parsed and preserved through a round-trip but has no behaviour here.
/// Panel colour polarity: `"light"` (native black-ink-on-white-paper) or
/// `"dark"` (white-on-black). On the 1-bit panel this is a whole-frame invert
/// applied at the end of [`draw`](Editor::draw) via [`Frame::invert`], so any
/// value other than `"dark"` reads as light. The palette rotates it through
/// [`THEME_OPTIONS`]; a hand-typed value still round-trips.
pub theme: String,
/// Max-staleness cap for opportunistic auto-publish, as a duration string.
/// The palette rotates it through [`AUTO_SYNC_OPTIONS`] (`"2m"`..`"30m"`);
/// hand-editing can still set any string. **Persisted-but-inert in v0.5** —
/// the periodic push that reads it rides v0.7/v0.8, so cycling it changes the
/// stored/displayed value but triggers nothing yet.
pub auto_sync: String,
}
@@ -194,6 +201,7 @@ impl Default for Prefs {
save_on_idle: true,
format_on_save: true,
line_numbers: true,
theme: "light".into(),
auto_sync: "10m".into(),
}
}
@@ -231,6 +239,7 @@ impl Prefs {
p.line_numbers = b;
}
}
"theme" => p.theme = val.trim_matches('"').to_string(),
"auto_sync" => p.auto_sync = val.trim_matches('"').to_string(),
_ => {}
}
@@ -245,12 +254,17 @@ impl Prefs {
pub fn to_toml(&self) -> String {
format!(
"# Typoena editor preferences — hand-editable, git-tracked.\n\
# Edit here, or toggle live from the Cmd-P palette (type `>`).\n\
# Edit here, or change live from the Cmd-P palette (type `>`).\n\
save_on_idle = {}\n\
format_on_save = {}\n\
line_numbers = {}\n\
theme = \"{}\"\n\
auto_sync = \"{}\"\n",
self.save_on_idle, self.format_on_save, self.line_numbers, self.auto_sync,
self.save_on_idle,
self.format_on_save,
self.line_numbers,
self.theme,
self.auto_sync,
)
}
}
@@ -265,6 +279,25 @@ fn parse_bool(v: &str) -> Option<bool> {
}
}
/// The panel-polarity presets the palette rotates [`Prefs::theme`] through.
const THEME_OPTIONS: [&str; 2] = ["light", "dark"];
/// The auto-publish intervals the palette rotates [`Prefs::auto_sync`] through.
/// Hand-editing the TOML can still set any duration string; these are just the
/// values the `>` palette cycles.
const AUTO_SYNC_OPTIONS: [&str; 5] = ["2m", "5m", "10m", "15m", "30m"];
/// The option after `current` in `options`, wrapping past the end — the
/// rotate-on-Enter for a preset string pref. A `current` that isn't in the list
/// (e.g. hand-typed into the TOML) snaps to the first option, so one Enter
/// always lands on a known value.
fn next_option<'a>(current: &str, options: &[&'a str]) -> &'a str {
match options.iter().position(|&o| o == current) {
Some(i) => options[(i + 1) % options.len()],
None => options[0],
}
}
/// Resolve a `:e`/`:enew` argument (or palette pick) to an absolute path +
/// [`Scope`]. Everything the writer can reach lives on the card under `/sd`, so
/// the `/sd` prefix is **optional**: `/sd/repo/x`, `/repo/x`, and `repo/x` all
@@ -386,21 +419,27 @@ fn palette_label(path: &str) -> &str {
/// A palette command (the `>` mode). v0.5 exposes the three boolean prefs as
/// live toggles; each command's *label* carries the pref's current state
/// ([`Editor::command_label`]), so the list doubles as a settings readout. This
/// registry is the discoverable surface later features (`:fmt`, theme, font)
/// grow into. `auto_sync` is deliberately absent until v0.7 gives it behaviour —
/// a value control that changes nothing would be a dead switch.
/// registry is the discoverable surface later features (`:fmt`, font) grow into.
/// A boolean flips on Enter; a preset like [`Theme`](PaletteCmd::Theme) or
/// [`AutoSync`](PaletteCmd::AutoSync) rotates to its next option and wraps.
/// `auto_sync` has no behaviour yet (v0.7), so cycling it only changes the
/// stored/displayed value — see [`Prefs::auto_sync`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PaletteCmd {
SaveOnIdle,
FormatOnSave,
LineNumbers,
Theme,
AutoSync,
}
/// The palette command list, in display order (empty `>` query shows them all).
const PALETTE_CMDS: [PaletteCmd; 3] = [
const PALETTE_CMDS: [PaletteCmd; 5] = [
PaletteCmd::SaveOnIdle,
PaletteCmd::FormatOnSave,
PaletteCmd::LineNumbers,
PaletteCmd::Theme,
PaletteCmd::AutoSync,
];
/// A pending operator awaiting a motion or text object (`d`elete / `c`hange /
@@ -1638,6 +1677,8 @@ impl Editor {
PaletteCmd::SaveOnIdle => format!("save on idle: {}", on(self.prefs.save_on_idle)),
PaletteCmd::FormatOnSave => format!("format on save: {}", on(self.prefs.format_on_save)),
PaletteCmd::LineNumbers => format!("line numbers: {}", on(self.prefs.line_numbers)),
PaletteCmd::Theme => format!("theme: {}", self.prefs.theme),
PaletteCmd::AutoSync => format!("auto sync: {}", self.prefs.auto_sync),
}
}
@@ -1663,25 +1704,34 @@ impl Editor {
/// which closes: opening a file switches away, toggling a pref does not.
fn palette_run_command(&mut self) {
if let Some(&ci) = self.palette_command_matches().get(self.palette_sel) {
self.toggle_pref(PALETTE_CMDS[ci]);
self.cycle_pref(PALETTE_CMDS[ci]);
}
}
/// Flip the boolean pref a command targets, apply it live (the next
/// [`draw`](Self::draw) reflects it — line numbers appear/vanish at once),
/// queue the prefs-file write ([`Effect::SavePrefs`]), and confirm the new
/// state on the snackbar. The queued `SavePrefs` is what makes the change
/// durable and lets it ride the next `:sync` to other devices.
fn toggle_pref(&mut self, cmd: PaletteCmd) {
/// Advance the pref a command targets to its next value, apply it live (the
/// next [`draw`](Self::draw) reflects it — line numbers appear/vanish, the
/// theme flips at once), queue the prefs-file write ([`Effect::SavePrefs`]),
/// and confirm the new state on the snackbar. A boolean flips; a preset
/// string ([`Theme`](PaletteCmd::Theme), [`AutoSync`](PaletteCmd::AutoSync))
/// rotates to its next option and wraps — so from the palette every setting
/// is "press Enter to change". The queued `SavePrefs` is what makes the
/// change durable and lets it ride the next `:sync` to other devices.
fn cycle_pref(&mut self, cmd: PaletteCmd) {
match cmd {
PaletteCmd::SaveOnIdle => self.prefs.save_on_idle = !self.prefs.save_on_idle,
PaletteCmd::FormatOnSave => self.prefs.format_on_save = !self.prefs.format_on_save,
PaletteCmd::LineNumbers => self.prefs.line_numbers = !self.prefs.line_numbers,
PaletteCmd::Theme => {
self.prefs.theme = next_option(&self.prefs.theme, &THEME_OPTIONS).to_string()
}
PaletteCmd::AutoSync => {
self.prefs.auto_sync = next_option(&self.prefs.auto_sync, &AUTO_SYNC_OPTIONS).to_string()
}
}
self.requests.push(Effect::SavePrefs {
contents: self.prefs.to_toml(),
});
// The label already reflects the just-flipped state (e.g. "line numbers: off").
// The label already reflects the just-changed state (e.g. "theme: dark").
self.set_notice(format!("{} - saved", self.command_label(cmd)));
}
@@ -2703,6 +2753,12 @@ impl Editor {
if self.mode == Mode::Palette {
self.draw_palette(&mut f);
}
// Dark theme: flip the whole frame in one pass, after everything else is
// painted, so text, selection, caret, panel and palette all invert
// together. Any value but "dark" stays native black-on-white.
if self.prefs.theme == "dark" {
f.invert();
}
f
}
@@ -2928,7 +2984,7 @@ impl Editor {
}
let hint = if command_mode {
"^N/^P move Enter toggle Esc close"
"^N/^P move Enter change Esc close"
} else {
"^N/^P move Enter open Esc close"
};
@@ -4646,6 +4702,7 @@ mod tests {
assert!(p.save_on_idle);
assert!(p.format_on_save);
assert!(p.line_numbers);
assert_eq!(p.theme, "light");
assert_eq!(p.auto_sync, "10m");
}
@@ -4689,11 +4746,19 @@ mod tests {
save_on_idle: false,
format_on_save: true,
line_numbers: false,
theme: "dark".into(),
auto_sync: "5m".into(),
};
assert_eq!(Prefs::parse(&p.to_toml()), p);
}
#[test]
fn prefs_parse_reads_theme_and_auto_sync_strings() {
let p = Prefs::parse("theme = \"dark\"\nauto_sync = \"15m\"\n");
assert_eq!(p.theme, "dark");
assert_eq!(p.auto_sync, "15m");
}
#[test]
fn empty_prefs_file_yields_defaults() {
assert_eq!(Prefs::parse(""), Prefs::default());
@@ -4734,7 +4799,7 @@ mod tests {
fn leading_gt_switches_the_palette_to_command_mode() {
let e = palette_type(&["/sd/repo/notes.md"], ">");
assert!(e.palette_command_mode());
// All three prefs commands are offered on a bare `>`.
// Every prefs command is offered on a bare `>`.
assert_eq!(e.palette_command_matches().len(), PALETTE_CMDS.len());
}
@@ -4811,6 +4876,66 @@ mod tests {
assert_eq!(e.notice.as_deref(), Some("save on idle: off - saved"));
}
// ---- Preset (non-boolean) prefs: rotate through options on Enter ----
#[test]
fn next_option_rotates_and_wraps() {
assert_eq!(next_option("light", &THEME_OPTIONS), "dark");
assert_eq!(next_option("dark", &THEME_OPTIONS), "light"); // wraps
assert_eq!(next_option("10m", &AUTO_SYNC_OPTIONS), "15m");
assert_eq!(next_option("30m", &AUTO_SYNC_OPTIONS), "2m"); // wraps
}
#[test]
fn next_option_snaps_an_unknown_value_to_the_head() {
// A hand-typed value outside the preset list lands on the first option.
assert_eq!(next_option("sepia", &THEME_OPTIONS), "light");
assert_eq!(next_option("7m", &AUTO_SYNC_OPTIONS), "2m");
}
#[test]
fn theme_command_label_reflects_the_current_value() {
let e = palette_editor(&["/sd/repo/notes.md"]);
assert_eq!(e.command_label(PaletteCmd::Theme), "theme: light");
assert_eq!(e.command_label(PaletteCmd::AutoSync), "auto sync: 10m");
}
#[test]
fn running_the_theme_command_rotates_the_preset_and_queues_a_save_prefs() {
// >theme<Enter> flips light -> dark, in-core, and asks the host to persist.
let mut e = palette_type(&["/sd/repo/notes.md"], ">theme");
assert_eq!(e.prefs().theme, "light");
e.handle(Key::Enter);
assert_eq!(e.prefs().theme, "dark"); // applied live
assert_eq!(e.mode(), Mode::Palette); // stays open for more changes
assert_eq!(kinds(&e.take_effects()), vec![Kind::SavePrefs]);
e.handle(Key::Enter);
assert_eq!(e.prefs().theme, "light"); // rotates back, wrapping
}
#[test]
fn running_the_auto_sync_command_walks_the_interval_presets() {
// Default 10m; Enter rotates 10m -> 15m -> 30m -> 2m (wrap).
let mut e = palette_type(&["/sd/repo/notes.md"], ">auto");
assert_eq!(e.prefs().auto_sync, "10m");
for expected in ["15m", "30m", "2m"] {
e.handle(Key::Enter);
assert_eq!(e.prefs().auto_sync, expected);
}
}
#[test]
fn dark_theme_inverts_the_whole_frame() {
// The dark frame is the exact bitwise inverse of the light one.
let mut e = Editor::with_text("hello world".into());
e.caret = 0;
let light = e.draw(true).bytes().to_vec();
e.prefs.theme = "dark".into();
let dark = e.draw(true).bytes().to_vec();
assert_eq!(light.len(), dark.len());
assert!(light.iter().zip(&dark).all(|(l, d)| *l == !*d));
}
#[test]
fn a_no_match_command_query_runs_nothing() {
let mut e = palette_type(&["/sd/repo/notes.md"], ">zzzzz");