Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ if/when one lands — `index-engine.md` §5.)*
- **`b2-desktop`** — the Tauri host: the *second* dumb adapter, the GUI sibling of `b2-cli`. Each
`#[tauri::command]` is deserialize → one `Vault` call → serialize, reusing the CLI's `--json` view
types as the IPC contract; it also owns host-only infrastructure (the async cancellable reindex task,
the fs-watch `vault-changed` pulse, the OS folder dialog). Has its own `CLAUDE.md` with the
the fs-watch `vault-changed` pulse, the OS folder dialog, and the **declared menu bar** — `menu.rs`,
GH #119). Has its own `CLAUDE.md` with the
thin-adapter rules — read it before touching this crate.
- **`ui/`** (not a crate) — the desktop frontend: Vite + vanilla TS + CodeMirror 6, a separate npm
toolchain talking to the host over Tauri IPC (`ui/src/api.ts` is the seam). Rendering a note is a
Expand All @@ -269,8 +270,11 @@ if/when one lands — `index-engine.md` §5.)*
(`ui/src/shortcuts.ts`) that `?` jumps straight to. Chords themselves are declared once in
`ui/src/bindings.ts` — the keyboard registry — and the dispatcher, the editor's keymap and that
sheet all derive from it, so none of the three can drift; `conflicts()` fails the suite on two
commands sharing a keystroke in one scope, and `ui/src/editorkeys.ts` checks B2's chords against
CodeMirror's own ~100 stock bindings so an upgrade can't quietly take one. The four obligations a new surface owes are in
commands sharing a keystroke in one scope, `ui/src/editorkeys.ts` checks B2's chords against
CodeMirror's own ~100 stock bindings so an upgrade can't quietly take one, and `ui/src/menukeys.ts`
checks them against the **macOS menu bar's** — declared in `crates/b2-desktop/src/menu.rs` rather than
inherited from Tauri's default, since AppKit dispatches a menu accelerator before the webview sees the
key at all, which made it the one clash nothing could detect (GH #119). The four obligations a new surface owes are in
[`crates/b2-desktop/CLAUDE.md`](crates/b2-desktop/CLAUDE.md).

### The `Vault` façade (`b2-core/src/vault.rs`)
Expand Down
22 changes: 19 additions & 3 deletions crates/b2-desktop/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,13 @@ Every new surface owes all four. They are cheap while you're building it and exp
action lives in a menu, beside the menu item, which is where a keyboard user learns the shortcut
that lets them skip the menu next time.

Two things the registry will tell you before a user does. `conflicts()` fails the suite if your
Three things the registry will tell you before a user does. `conflicts()` fails the suite if your
chord already means something else in the same scope, so pick the scope honestly — it's what
separates "⏎ commits *this* dialog" from a clash. And `editorkeys.test.ts` compares B2's chords
separates "⏎ commits *this* dialog" from a clash. `editorkeys.test.ts` compares B2's chords
against CodeMirror's ~100 stock bindings, so if your chord needs to work while the note is being
edited, that check is what proves the editor isn't already using it.
edited, that check is what proves the editor isn't already using it. And `menukeys.test.ts`
compares them against the **menu bar's** (below) — the one clash no scope and no ordering can
win, because the keystroke never reaches the webview.

### Where the pieces live

Expand All @@ -161,6 +163,20 @@ Every new surface owes all four. They are cheap while you're building it and exp
- **`ui/src/shortcuts.ts`** — the one chord table, rendered as Settings' **Keyboard** section (`?`
opens the dialog there). Modifiers as macOS glyphs (⌘ ⇧ ⌫ ⏎); keys macOS spells out in its own
menus stay words (Esc, Tab, Space, Home/End).
- **`src/menu.rs` + `ui/src/menukeys.ts`** — the **menu bar**, and the app's third keyboard
([#119](https://github.com/AlteredCraft/B2/issues/119)). Set no menu and Tauri installs
`Menu::default()`, whose dozen accelerators (⌘Q ⌘W ⌘M ⌘H ⌥⌘H ⌘Z ⇧⌘Z ⌘X ⌘C ⌘V ⌘A ⌃⌘F) are live in
the window and enumerable by nobody — and AppKit dispatches a menu key equivalent inside
`NSApplication.sendEvent`, *before* the key window's responder chain, so they never reach the
webview's `keydown` and the registry cannot observe them either. So `menu.rs` declares the menu as
a table (the items stay `PredefinedMenuItem`s: the Edit menu is load-bearing — it is what routes
cut/copy/paste into the webview), the `menu_chords` command exports it, and `menukeys.ts` mirrors
it for the two jobs a runtime fetch can't do — the suite's gate, and the sheet's first paint. The
mirror is checked against the host at every boot (`menuDrift`), the way `WRITE_CONFLICT_MESSAGE`
and `VAULT_CHANGED_EVENT` are pinned across the same seam: **change the two together.** Note what
the gate is *not*: `conflicts()` asks a same-scope question, and scope buys nothing here — a menu
accelerator is taken before the webview is consulted, so `menuOverlaps` compares across every
scope.
- **`ui/src/settingstabs.ts`** — the Settings dialog's rail: the section list and its ARIA `tabs`
moves (↑↓ with wrap, Home/End; ⌃Tab cycles from anywhere in the dialog). Its own module for
treenav.ts's reason — the paint and the arrows must agree on order, so the order is defined once
Expand Down
13 changes: 13 additions & 0 deletions crates/b2-desktop/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,19 @@ pub fn embed_stats() -> Vec<EmbedStat> {
.collect()
}

/// Every chord the app's **menu bar** takes, in menu order (`menu.rs`, #119). The UI
/// folds these into its keyboard registry as reserved chords: the reference sheet lists
/// them, and the collision check can finally see the one set of chords it was blind to
/// — AppKit dispatches a menu key equivalent before the webview receives the key at all,
/// so no amount of watching `keydown` would have found them.
///
/// Static data, so infallible and vault-free — the shape of [`embed_device`], not of a
/// façade call.
#[tauri::command]
pub fn menu_chords() -> Vec<crate::menu::MenuChord> {
crate::menu::chords()
}

/// Releases the single-in-flight reindex slot on drop, so it is freed on **every**
/// exit path — normal return, an early `?` (e.g. model-not-provisioned), or a panic.
struct ReindexGuard<'a>(&'a AppState);
Expand Down
11 changes: 11 additions & 0 deletions crates/b2-desktop/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@
//! `project` — the model-free half of a reindex (index-engine.md) — opens the fake,
//! so the first tree paint never waits on a model load.
//! `B2_EMBEDDER=fake` forces the fake everywhere (offline/dev mode).
//!
//! And one it hands off: the **menu bar** is declared in [`menu`] rather than inherited
//! from `Menu::default()`, so its chords are B2's own data and the UI can list them
//! ([#119](https://github.com/AlteredCraft/B2/issues/119)).

// This binary is desktop-only (no mobile entry point), so a plain `main` suffices.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

mod commands;
mod error;
mod logging;
mod menu;
mod stats;
mod watch;

Expand Down Expand Up @@ -282,6 +287,11 @@ fn main() {
let _guard = logging::init_logging();
let state = AppState::new(resolve_root());
tauri::Builder::default()
// The menu bar, declared (#119). Without this call Tauri installs
// `Menu::default()`, whose dozen accelerators nothing in the app can enumerate
// — and AppKit dispatches them before the webview sees a key, so the keyboard
// registry can't observe them either. `menu::MENU` is that list, made data.
.menu(menu::build)
// The dialog plugin backs the native folder picker for `choose_vault`. It is
// driven host-side only; the webview gets no dialog permission (capabilities/
// default.json), so it can never open a dialog itself.
Expand Down Expand Up @@ -343,6 +353,7 @@ fn main() {
commands::models_dir,
commands::embed_device,
commands::embed_stats,
commands::menu_chords,
])
.run(tauri::generate_context!())
.expect("error while running the B2 desktop app");
Expand Down
Loading