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
60 changes: 52 additions & 8 deletions docs/example-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -321,21 +321,28 @@ literal = false
# the help overlay's scroll keys and its own `/` filter, the shared `/`
# list-filter editing sub-state (annotation list / staging / accepted panel
# / switcher, spec 12), Compose, the commit-message modal, the diff-view `/`
# search input, the fuzzy file finder, and the two Project Search focuses) —
# search input, the fuzzy file finder, the two Project Search focuses, the
# imported-thread overlay, the submit-review modal, the post-submit result
# view, and the finished-review cleanup modal) —
# has its own `[keys.<mode>]` table, with the
# identical grammar and merge semantics as `[keys.diff]`/`[keys.panel]`
# above: `action-name = "<key>"` or `= ["<key>", ...]`, an override
# replaces that action's default keys entirely, an unlisted action keeps
# its defaults, `= []` unbinds it, and a same-table collision is won by
# the override (with a warning).
#
# Free-text modes (Compose, the commit-message modal, Search, Finder, and
# both Project Search focuses) type printable characters into a buffer or
# query — that character insertion is never an action and can't be bound;
# only the *control* keys listed below (Enter/Esc/arrows/Backspace/...)
# are actions.
#
# The fourteen mode names and their complete action lists, each showing its
# Free-text modes (Compose, the commit-message modal, Search, Finder, both
# Project Search focuses, and the submit-review modal's summary field) type
# printable characters into a buffer or query — that character insertion is
# never an action and can't be bound; only the *control* keys listed below
# (Enter/Esc/arrows/Backspace/...) are actions. The table is consulted
# before the character-insert fallback, so binding a control action of one
# of those modes to a bare printable key is accepted but takes that
# character away from typing — e.g. `[keys.submit-forge] cancel = "q"` means
# you can no longer type `q` into the review summary. Prefer a modified key
# (`ctrl-`/`alt-`) or a named key in a free-text mode.
#
# The eighteen mode names and their complete action lists, each showing its
# default key(s):
#
# [keys.list] # Annotation list panel (`a`)
Expand Down Expand Up @@ -404,6 +411,10 @@ literal = false
# jump-to-bottom = ["G", "end"] # Jump to bottom
# confirm = "enter" # Confirm the highlighted row
# enter-filter = "/" # Filter (fuzzy, narrows the active tab)
# toggle-all-commits = "a" # Commits tab: ahead-of-base <-> full recent-HEAD log
# cleanup-finished-reviews = "X" # Pull Requests tab: clean up finished reviews
# refresh = "r" # Pull Requests tab: re-fetch the PR list
# pr-details = "i" # Pull Requests tab: read the PR description
# close = "esc" # Close
#
# [keys.help] # Help overlay scroll/close/filter-open keys
Expand Down Expand Up @@ -501,6 +512,39 @@ literal = false
# toggle-whole-word = "alt-w" # Toggle whole-word matching
# toggle-literal = "alt-r" # Toggle regex / literal matching
#
# [keys.thread-view] # Imported PR comment thread overlay (spec 13)
# scroll-down = ["j", "down"] # Scroll conversation down
# scroll-up = ["k", "up"] # Scroll conversation up
# reply = "r" # Reply to this thread
# close = ["q", "esc"] # Close the thread overlay
#
# [keys.submit-forge] # Submit-review modal (`U` in a PR review, spec 13)
# confirm = "enter" # Submit the review (publishes to the forge)
# cancel = "esc" # Cancel — close this modal, send nothing
# verdict-next = "tab" # Next verdict (comment / approve / request changes)
# verdict-prev = "shift-tab" # Previous verdict
# scroll-down = "down" # Scroll the batch preview down
# scroll-up = "up" # Scroll the batch preview up
# page-down = "pagedown" # Scroll the batch preview down a page
# page-up = "pageup" # Scroll the batch preview up a page
# delete-char = "backspace" # Delete summary character
# compose-summary = "ctrl-e" # Edit the summary in the composer (multi-line)
#
# [keys.submit-result] # Post-submit result view (shown when a submit stops early)
# retry = "U" # Submit again — retry everything that didn't land
# dismiss = ["enter", "esc", "q"] # Dismiss the result view
# scroll-down = ["j", "down"] # Scroll the outcome list down
# scroll-up = ["k", "up"] # Scroll the outcome list up
# page-down = "pagedown" # Scroll the outcome list down a page
# page-up = "pageup" # Scroll the outcome list up a page
#
# [keys.cleanup-reviews] # Finished-review cleanup confirm modal (launcher `X`)
# move-down = ["j", "down"] # Move the highlight down
# move-up = ["k", "up"] # Move the highlight up
# toggle = "space" # Toggle the highlighted entry's selection
# confirm = ["enter", "y"] # Delete the selected finished reviews (worktree, branch, saved state)
# cancel = ["esc", "n"] # Cancel — close this modal, delete nothing
#
# Example: remap the staging panel's unstage key to `x` and the switcher's
# confirm key to `l` (also still one of ToggleTab's defaults, so this would
# collide in a real config — shown separately here for two independent,
Expand Down
6 changes: 5 additions & 1 deletion src/config/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ const MODAL_MODE_NAMES: &[&str] = &[
"finder",
"project-search-input",
"project-search-results",
"thread-view",
"submit-forge",
"submit-result",
"cleanup-reviews",
"filter-edit",
];

Expand All @@ -195,7 +199,7 @@ const MODAL_MODE_NAMES: &[&str] = &[
/// `diff`/`panel`/`global`, keyed by mode name (one of [`MODAL_MODE_NAMES`])
/// — a single map rather than one field per mode, since
/// `crate::ui::modal_keys_config` (the edge module resolving these) already
/// needs one generic merge function reusable across all thirteen modes.
/// needs one generic merge function reusable across every mode.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct KeysConfig {
pub diff: BTreeMap<String, Vec<KeySeqSpec>>,
Expand Down
46 changes: 46 additions & 0 deletions src/ui/footer_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,52 @@ fn switcher_mode_hints() {
);
}

/// End-to-end for the `[keys.<mode>]` -> footer path: the strip is built
/// from the *effective* tables, so a remapped modal key reaches the footer
/// with no per-mode wiring. Uses `Mode::ThreadView` as the representative
/// PR-flow modal — a strip built from the compiled-in default table instead
/// would still print `r`.
#[test]
fn a_remapped_modal_key_shows_up_in_that_modes_hint_strip() {
let mut keys = crate::config::KeysConfig::default();
let mut table = std::collections::BTreeMap::new();
table.insert(
"reply".to_string(),
vec![crate::config::keys::KeySeqSpec::One(
crate::config::keys::ChordSpec {
code: KeyCode::Char('a'),
mods: KeyModifiers::NONE,
},
)],
);
keys.modal.insert("thread-view".to_string(), table);
let (modal_keys, warnings) = crate::ui::modal_keys_config::effective_modal_keys(&keys);
assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");

let km = Keymap::default_map();
let entries = build_hints(
Mode::ThreadView,
FooterFlags {
staging_allowed: true,
code_intel_allowed: true,
push_publishes: false,
viewing_commit: false,
help_open: false,
project_search_focus: SearchFocus::Input,
review_session: true,
web_target: None,
},
None,
&km,
&modal_keys,
);
let reply = entries
.iter()
.find(|e| e.label == "reply")
.expect("the thread overlay's reply hint");
assert_eq!(reply.key, "a");
}

#[test]
fn search_mode_has_no_hint_strip() {
let km = Keymap::default_map();
Expand Down
33 changes: 33 additions & 0 deletions src/ui/forge_submit_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,39 @@ fn typing_a_summary_clears_the_hint_and_lets_request_changes_confirm() {
assert!(app.submit_forge.is_none());
}

/// The accepted trade for making `[keys.submit-forge]` remappable: the table
/// is consulted before the char-insert fallback, so a control action bound to
/// a bare printable key takes that character away from summary typing. Pins
/// the ordering — flipping it would silently un-remap every letter-keyed
/// submit-forge override.
#[test]
fn a_submit_forge_action_remapped_onto_a_letter_shadows_summary_typing() {
let mut keys = crate::config::KeysConfig::default();
let mut table = std::collections::BTreeMap::new();
table.insert(
"cancel".to_string(),
vec![crate::config::keys::KeySeqSpec::One(
crate::config::keys::ChordSpec {
code: KeyCode::Char('q'),
mods: KeyModifiers::NONE,
},
)],
);
keys.modal.insert("submit-forge".to_string(), table);
let (modal_keys, warnings) = crate::ui::modal_keys_config::effective_modal_keys(&keys);
assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");

let mut app = github_review_app(&["src/a.rs"]);
app.modal_keys = modal_keys;
app.open_submit_forge();
handle_submit_forge_key(
&mut app,
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE),
);
assert_eq!(app.mode, Mode::Normal, "`q` must cancel, not type");
assert!(app.submit_forge.is_none());
}

// -- confirm on the fake path sends nothing (no live backend) ----------------

// -- scrollable preview + overflow markers -----------------------------------
Expand Down
Loading
Loading