Compare commits

...

2 Commits

Author SHA1 Message Date
Julien Calixte
ad047cc717 docs: reconcile screen-layout docs with the panel changes
Mode now sits at the side panel's bottom-left (not its top), and the
words-this-session field is gone. Update the CONTEXT.md field list and
the product-doc mock + prose to match; note the keyboard flag renders
as NO KBD (Latin-9 has no glyph for the mock's symbols).
2026-07-08 21:48:32 +02:00
Julien Calixte
ba4a8bfad8 feat(editor): put the mode indicator at the side-panel bottom-left
Move the mode + pending-command echo from the panel's top to its
bottom-left (vim `-- INSERT --` style); word count takes the top slot
and the NO KBD flag sits just above the mode. Also drop the
words-this-session field (not wanted) and its session_base_words state.
2026-07-08 21:48:21 +02:00
3 changed files with 63 additions and 68 deletions

View File

@@ -67,8 +67,9 @@ full-width text region before the side panel carved out its right edge).
**Side panel**:
The right region (~150 px / ~20 cols, full height) holding all metadata:
filename + dirty dot, mode, word count, session, clock, Wi-Fi,
keyboard-disconnect flag, publish state. Sits entirely in the master half
filename + dirty dot, word count, elapsed time, clock, Wi-Fi,
keyboard-disconnect flag, publish state, and the mode indicator at its
bottom-left. Sits entirely in the master half
(right of the `x = 396` seam). Every field is static, event-driven, or
throttled — never per-keystroke.
_Avoid_: header, status line, status bar (retired — the old top header band and

View File

@@ -101,17 +101,17 @@ context," not a full page. Hardware rationale: [ADR-003](adr.md#adr-003-display-
```
┌───────────────────────────────────────────────────────┬─────────────────────┐
│ Lorem ipsum dolor sit amet, consectetur adipiscing │ notes.md ● │
│ elit. Sed do eiusmod tempor incididunt ut labore et │ INSERT
│ elit. Sed do eiusmod tempor incididunt ut labore et │
│ dolore magna aliqua. Ut enim ad minim veniam, quis │ │
│ nostrud exercitation ullamco laboris nisi ut aliquip │ 1 240 words │
│ ex ea commodo consequat. │ +318 this session
│ ex ea commodo consequat. │
│ │ 12:07 elapsed │
│ Duis aute irure dolor in reprehenderit in voluptate │ │
│ velit esse cillum dolore eu fugiat nulla pariatur. │ │
│ Excepteur sint occaecat cupidatat non proident. │ │
│ │ 14:02 │
│ The quick brown fox jumps over the lazy▎ │ Wi-Fi — │
│ │ ✓ pushed abc1234
│ │ -- INSERT --
└───────────────────────────────────────────────────────┴─────────────────────┘
writing column (~60 cols, full height) side panel (~20 ch)
```
@@ -122,12 +122,13 @@ context," not a full page. Hardware rationale: [ADR-003](adr.md#adr-003-display-
This is the only region that repaints as you type.
- **Side panel** (right, ~150 px / ~20 cols, full height): all metadata lives
here, so the writing column keeps the full height. Top — **filename** + dirty
dot (●/○) and **mode** (always `INSERT` in v0.1). Middle — **word count** and
the **session** (words written + elapsed time), both refreshed on a short
typing pause, *not* per keystroke. Bottom — ambient state: **clock** (if SNTP
has set the time), **Wi-Fi** (`—` = off/on-demand, `✓` = connected,
`✗` = failed), a **keyboard-disconnect** flag (`⌨ ✗`, shown *only* while the
keyboard is dropped; blank when healthy — no permanent `⌨ ✓`, per
dot (●/○). Middle — **word count** and **elapsed time**, refreshed on a short
typing pause, *not* per keystroke. Bottom-left — the **mode** indicator
(always `INSERT` in v0.1) with any pending count/operator echo. Bottom —
ambient state: **clock** (if SNTP has set the time), **Wi-Fi** (`—` =
off/on-demand, `✓` = connected, `✗` = failed), a **keyboard-disconnect** flag
(rendered `NO KBD` — Latin-9 has no `⌨`/`✗` glyph — shown *only* while the
keyboard is dropped; blank when healthy, per
[`CONTEXT.md`](../CONTEXT.md) "No state the user didn't ask for"), and
**publish state**. `Ctrl-S` briefly flashes "saved HH:MM";
`Ctrl-G` transitions through `✓ committed abc1234 · pushing…` at ~0.2 s — the

View File

@@ -84,9 +84,6 @@ pub struct Editor {
/// not a live count, so ordinary typing never repaints the panel row — it is
/// refreshed on a typing pause / non-Insert action via `refresh_stats`.
shown_words: usize,
/// Word count captured at the session start (buffer load / boot), so the
/// panel can show words *added* this session. Empty boot buffer → 0.
session_base_words: usize,
/// Whether a USB keyboard is attached; drives the panel disconnect flag.
/// Fed from `usb_kbd::keyboard_present()` by the main loop.
keyboard_present: bool,
@@ -111,7 +108,6 @@ impl Editor {
pending_g: false,
cmdline: String::new(),
shown_words: 0,
session_base_words: 0,
keyboard_present: false,
}
}
@@ -923,12 +919,13 @@ impl Editor {
f
}
/// Draw the side panel: a full-height rule, then the mode and any pending
/// command state, in the small 6×10 font. This is the metadata surface every
/// later panel field (filename, word count, clock, Wi-Fi, publish state)
/// will add to; v0.1 shows the mode plus the transient count/operator echo.
/// Everything here is event-driven (mode/command state), never per-keystroke,
/// so it adds no typing-time ghosting.
/// Draw the side panel: a full-height rule, word count at the top, and the
/// mode indicator + pending-command echo at the bottom-left, with a
/// keyboard-disconnect flag just above the mode while the keyboard is
/// dropped. Small 6×10 font. This is the surface every later field
/// (filename, clock, Wi-Fi, publish state) will add to. Word count is a
/// throttled snapshot and the rest is event-driven, so the panel never
/// repaints per keystroke.
fn draw_panel(&self, f: &mut Frame) {
// The rule dividing writing column from panel, full panel height.
Rectangle::new(Point::new(DIVIDER_X, 0), Size::new(1, epd::HEIGHT as u32))
@@ -937,59 +934,55 @@ impl Editor {
.unwrap();
let style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);
let name = match self.mode {
Mode::Normal => "NORMAL",
Mode::Insert => "INSERT",
Mode::View => "VIEW",
Mode::Command => "COMMAND",
};
Text::with_baseline(name, Point::new(PANEL_X, 2), style, Baseline::Top)
.draw(f)
.unwrap();
// Pending count / operator / text-object / g, one line below the mode —
// transient feedback while a multi-key command is mid-entry.
let mut pend = String::new();
if self.count > 0 {
pend.push_str(&self.count.to_string());
}
match self.pending_op {
Some(Op::Delete) => pend.push('d'),
Some(Op::Change) => pend.push('c'),
None => {}
}
match self.pending_obj {
Some(false) => pend.push('i'),
Some(true) => pend.push('a'),
None => {}
}
if self.pending_g {
pend.push('g');
}
if !pend.is_empty() {
Text::with_baseline(&pend, Point::new(PANEL_X, 14), style, Baseline::Top)
.draw(f)
.unwrap();
}
// Word count + words added this session, from the throttled snapshot.
// Word count, from the throttled snapshot (never per keystroke).
let words = format!("{} words", self.shown_words);
Text::with_baseline(&words, Point::new(PANEL_X, 40), style, Baseline::Top)
.draw(f)
.unwrap();
let session = format!(
"+{} this session",
self.shown_words.saturating_sub(self.session_base_words)
);
Text::with_baseline(&session, Point::new(PANEL_X, 52), style, Baseline::Top)
Text::with_baseline(&words, Point::new(PANEL_X, 2), style, Baseline::Top)
.draw(f)
.unwrap();
// Keyboard-disconnect flag at the panel foot, shown only while the
// keyboard is dropped. Latin-9 has no ⌨/✗ glyph, so plain text.
// Keyboard-disconnect flag, just above the mode line, shown only while
// the keyboard is dropped. Latin-9 has no ⌨/✗ glyph, so plain text.
if !self.keyboard_present {
Text::with_baseline(
"NO KBD",
Point::new(PANEL_X, epd::HEIGHT as i32 - 34),
style,
Baseline::Top,
)
.draw(f)
.unwrap();
}
// Mode indicator + pending count/operator echo at the panel's bottom-
// left. In Command mode the ':' line (bottom strip) takes over instead.
// All event-driven — never repaints per keystroke.
if self.mode != Mode::Command {
let name = match self.mode {
Mode::Normal => "NORMAL",
Mode::Insert => "INSERT",
Mode::View => "VIEW",
Mode::Command => unreachable!(),
};
let mut s = format!("-- {name} --");
if self.count > 0 {
s.push_str(&format!(" {}", self.count));
}
match self.pending_op {
Some(Op::Delete) => s.push('d'),
Some(Op::Change) => s.push('c'),
None => {}
}
match self.pending_obj {
Some(false) => s.push('i'),
Some(true) => s.push('a'),
None => {}
}
if self.pending_g {
s.push('g');
}
Text::with_baseline(
&s,
Point::new(PANEL_X, epd::HEIGHT as i32 - 22),
style,
Baseline::Top,
@@ -999,8 +992,8 @@ impl Editor {
}
}
/// The transient `:` command line, in the bottom 12 px strip below the
/// writing column (vim-style). Shown only while composing a command.
/// The transient `:` command line, in the bottom strip below the writing
/// column (vim-style). Shown only while composing a command.
fn draw_cmdline(&self, f: &mut Frame) {
if self.mode != Mode::Command {
return;