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
24 changes: 18 additions & 6 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,21 @@ directly, and TUI handlers adopt it as the conversion sub-PRs land
`App::start_playback_uris` / `start_playback_context` /
`start_playback_track_in_context` - never a hand-built
`IoEvent::StartPlayback` in an arm.
- No wildcard match arm anywhere under `src/core/action/`:
`wildcard_arms_in_action_tree` is pinned at 0 by a raw text scan that
includes tests, comments, and string literals. Write catch-all test arms
as `_other =>`.
- No catch-all match arm under `src/core/action/` or in `tui/keymap.rs`:
both deny `clippy::wildcard_enum_match_arm` and
`clippy::match_wildcard_for_single_variants` (a named binding like
`_other =>` counts as a wildcard; `matches!` and `Option`/`Result`
scrutinees are exempt), and `wildcard_arms_in_action_tree` pins the
action tree at 0 by a raw text scan that includes tests, comments, and
string literals. CI clippy never compiles tests, so write a test
catch-all as `_other =>` and only on a `Result`/`Option` scrutinee.
- Every `Action` variant has an arm in `tui/keymap.rs::default_binding`,
naming the key or gesture that produces it; a variant no gesture produces
is `Exposure::Unbound("reason")`. A new variant is a compile error until
it has an arm, and a test failure until `sample_actions()` in the same
file has a value for it (an `Unbound` one also goes into the `UNBOUND`
pin, which a producer scan of `src/tui/` checks). Feature-gate an arm's
body, never the arm: clippy skips a match when any arm carries a `#[cfg]`.
- `Action` derives serde (the future frontend wire shape); a payload type
added to it must stay serde-derivable.

Expand Down Expand Up @@ -374,8 +385,9 @@ Check `app.user_config.keys.<action>` instead of hard-coding key literals for
global actions (`handle_app` in `src/tui/handlers/mod.rs`);
`common_key_events::{up,down,left,right}_event` extend this to per-screen
navigation. Adding a binding means fields on both `KeyBindings` and
`KeyBindingsString` in `src/core/user_config.rs` plus a `help_entries` row in
`src/tui/keymap.rs` with its `Requirement`.
`KeyBindingsString` in `src/core/user_config.rs`, a `help_entries` row in
`src/tui/keymap.rs` with its `Requirement`, and, when the key produces an
`Action`, that variant's `default_binding` arm pointing at the new field.

### Requirements

Expand Down
24 changes: 18 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,21 @@ directly, and TUI handlers adopt it as the conversion sub-PRs land
`App::start_playback_uris` / `start_playback_context` /
`start_playback_track_in_context` - never a hand-built
`IoEvent::StartPlayback` in an arm.
- No wildcard match arm anywhere under `src/core/action/`:
`wildcard_arms_in_action_tree` is pinned at 0 by a raw text scan that
includes tests, comments, and string literals. Write catch-all test arms
as `_other =>`.
- No catch-all match arm under `src/core/action/` or in `tui/keymap.rs`:
both deny `clippy::wildcard_enum_match_arm` and
`clippy::match_wildcard_for_single_variants` (a named binding like
`_other =>` counts as a wildcard; `matches!` and `Option`/`Result`
scrutinees are exempt), and `wildcard_arms_in_action_tree` pins the
action tree at 0 by a raw text scan that includes tests, comments, and
string literals. CI clippy never compiles tests, so write a test
catch-all as `_other =>` and only on a `Result`/`Option` scrutinee.
- Every `Action` variant has an arm in `tui/keymap.rs::default_binding`,
naming the key or gesture that produces it; a variant no gesture produces
is `Exposure::Unbound("reason")`. A new variant is a compile error until
it has an arm, and a test failure until `sample_actions()` in the same
file has a value for it (an `Unbound` one also goes into the `UNBOUND`
pin, which a producer scan of `src/tui/` checks). Feature-gate an arm's
body, never the arm: clippy skips a match when any arm carries a `#[cfg]`.
- `Action` derives serde (the future frontend wire shape); a payload type
added to it must stay serde-derivable.

Expand Down Expand Up @@ -376,8 +387,9 @@ Check `app.user_config.keys.<action>` instead of hard-coding key literals for
global actions (`handle_app` in `src/tui/handlers/mod.rs`);
`common_key_events::{up,down,left,right}_event` extend this to per-screen
navigation. Adding a binding means fields on both `KeyBindings` and
`KeyBindingsString` in `src/core/user_config.rs` plus a `help_entries` row in
`src/tui/keymap.rs` with its `Requirement`.
`KeyBindingsString` in `src/core/user_config.rs`, a `help_entries` row in
`src/tui/keymap.rs` with its `Requirement`, and, when the key produces an
`Action`, that variant's `default_binding` arm pointing at the new field.

### Requirements

Expand Down
24 changes: 18 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,21 @@ directly, and TUI handlers adopt it as the conversion sub-PRs land
`App::start_playback_uris` / `start_playback_context` /
`start_playback_track_in_context` - never a hand-built
`IoEvent::StartPlayback` in an arm.
- No wildcard match arm anywhere under `src/core/action/`:
`wildcard_arms_in_action_tree` is pinned at 0 by a raw text scan that
includes tests, comments, and string literals. Write catch-all test arms
as `_other =>`.
- No catch-all match arm under `src/core/action/` or in `tui/keymap.rs`:
both deny `clippy::wildcard_enum_match_arm` and
`clippy::match_wildcard_for_single_variants` (a named binding like
`_other =>` counts as a wildcard; `matches!` and `Option`/`Result`
scrutinees are exempt), and `wildcard_arms_in_action_tree` pins the
action tree at 0 by a raw text scan that includes tests, comments, and
string literals. CI clippy never compiles tests, so write a test
catch-all as `_other =>` and only on a `Result`/`Option` scrutinee.
- Every `Action` variant has an arm in `tui/keymap.rs::default_binding`,
naming the key or gesture that produces it; a variant no gesture produces
is `Exposure::Unbound("reason")`. A new variant is a compile error until
it has an arm, and a test failure until `sample_actions()` in the same
file has a value for it (an `Unbound` one also goes into the `UNBOUND`
pin, which a producer scan of `src/tui/` checks). Feature-gate an arm's
body, never the arm: clippy skips a match when any arm carries a `#[cfg]`.
- `Action` derives serde (the future frontend wire shape); a payload type
added to it must stay serde-derivable.

Expand Down Expand Up @@ -376,8 +387,9 @@ Check `app.user_config.keys.<action>` instead of hard-coding key literals for
global actions (`handle_app` in `src/tui/handlers/mod.rs`);
`common_key_events::{up,down,left,right}_event` extend this to per-screen
navigation. Adding a binding means fields on both `KeyBindings` and
`KeyBindingsString` in `src/core/user_config.rs` plus a `help_entries` row in
`src/tui/keymap.rs` with its `Requirement`.
`KeyBindingsString` in `src/core/user_config.rs`, a `help_entries` row in
`src/tui/keymap.rs` with its `Requirement`, and, when the key produces an
`Action`, that variant's `default_binding` arm pointing at the new field.

### Requirements

Expand Down
12 changes: 9 additions & 3 deletions src/core/action/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,20 @@
//! - Playback starts go through `App::start_playback_uris` /
//! `App::start_playback_context`; no arm builds a `StartPlayback` event
//! by hand.
//! - Matches in this module are exhaustive. The
//! `wildcard_arms_in_action_tree` gate keeps the catch-all arm count at
//! zero, tests included.
//! - Matches in this module are exhaustive: the deny below refuses a
//! catch-all arm, and the `wildcard_arms_in_action_tree` gate keeps the
//! count at zero, tests included.
//! - Address by identity (URIs, ids, names), never by list ordinal.
//!
//! The serde derives are the wire shape for future frontend codegen; they
//! are deliberately in place before any second frontend consumes them.

// The compiler half of the catch-all gate: a new variant must be placed.
#![deny(
clippy::wildcard_enum_match_arm,
clippy::match_wildcard_for_single_variants
)]

use serde::{Deserialize, Serialize};

use crate::core::app::DiscoverTimeRange;
Expand Down
6 changes: 4 additions & 2 deletions src/infra/scripting/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ Plugins never see `&mut App` or rspotify types: reads are cached serde snapshots
from `src/core/plugin_api.rs`; writes are shared-vocabulary `Action`s
(`src/core/action/`) the engine drains through `App::apply` while holding
`&mut App`, each routed through the same `App` method the equivalent keybinding
uses. Snapshot changes must be additive (`#[serde(default)]`, new keys
only) - removing/renaming a key breaks installed plugins and requires bumping
uses, where the terminal has one (`default_binding` in `src/tui/keymap.rs`
records which variants have none). Snapshot changes must be additive
(`#[serde(default)]`, new keys only) - removing/renaming a key breaks
installed plugins and requires bumping
`API_VERSION` and updating `docs/scripting.md`. Validation lives in
`scripting/api.rs`; a failing callback is disabled on one strike.
2 changes: 1 addition & 1 deletion src/tui/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ pub fn handle_app(key: Key, app: &mut App) {
if key_matches_open_settings_binding(key, app.user_config.keys.open_settings)
|| key_matches_open_settings_binding(key, effective_open_settings)
{
app.open_settings_screen();
app.apply(Action::Navigate(NavTarget::Settings));
return;
}

Expand Down
Loading
Loading