parent
552aa8ca22
commit
b708f85e7d
|
|
@ -49,7 +49,9 @@ pub(crate) use self::{
|
|||
handle_global_menu_key, handle_keybind_help_key, handle_navigator_key,
|
||||
insert_navigator_search_text, insert_rename_input_text,
|
||||
},
|
||||
navigate::terminal_direct_navigation_action,
|
||||
navigate::{
|
||||
terminal_direct_indexed_navigation_action, terminal_direct_non_indexed_navigation_action,
|
||||
},
|
||||
settings::open_settings_at,
|
||||
};
|
||||
use self::{
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ use crate::{
|
|||
terminal::TerminalRuntimeRegistry,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn terminal_direct_navigation_action(
|
||||
state: &AppState,
|
||||
key: TerminalKey,
|
||||
|
|
@ -28,6 +29,20 @@ pub(crate) fn terminal_direct_navigation_action(
|
|||
action_for_key(state, key, BindingDispatch::Direct)
|
||||
}
|
||||
|
||||
pub(crate) fn terminal_direct_non_indexed_navigation_action(
|
||||
state: &AppState,
|
||||
key: TerminalKey,
|
||||
) -> Option<NavigateAction> {
|
||||
non_indexed_action_for_key(state, key, BindingDispatch::Direct)
|
||||
}
|
||||
|
||||
pub(crate) fn terminal_direct_indexed_navigation_action(
|
||||
state: &AppState,
|
||||
key: TerminalKey,
|
||||
) -> Option<NavigateAction> {
|
||||
indexed_navigation_action(state, key, BindingDispatch::Direct)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ActionContext {
|
||||
Direct,
|
||||
|
|
@ -61,25 +76,10 @@ impl App {
|
|||
return;
|
||||
}
|
||||
|
||||
if let Some(action) = action_for_key(&self.state, raw_key, BindingDispatch::Prefix) {
|
||||
if action == NavigateAction::EditScrollback {
|
||||
let previous_mode = self.state.mode;
|
||||
self.cancel_copy_mode_if_active();
|
||||
self.launch_focused_scrollback_editor();
|
||||
finish_action_context(&mut self.state, ActionContext::Prefix, previous_mode);
|
||||
} else if action == NavigateAction::CopyMode {
|
||||
self.cancel_copy_mode_if_active();
|
||||
self.execute_tui_navigate_action(action, ActionContext::Prefix);
|
||||
} else if copy_mode_survives_prefix_action(action) {
|
||||
self.execute_tui_navigate_action(action, ActionContext::Prefix);
|
||||
if self.state.copy_mode.is_some() {
|
||||
self.state.sync_copy_mode_with_focus();
|
||||
}
|
||||
} else {
|
||||
self.cancel_copy_mode_if_active();
|
||||
self.execute_tui_navigate_action(action, ActionContext::Prefix);
|
||||
}
|
||||
self.selection_autoscroll_deadline = None;
|
||||
if let Some(action) =
|
||||
non_indexed_action_for_key(&self.state, raw_key, BindingDispatch::Prefix)
|
||||
{
|
||||
self.execute_prefix_key_action(action);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -89,9 +89,37 @@ impl App {
|
|||
return;
|
||||
}
|
||||
|
||||
if let Some(action) =
|
||||
indexed_navigation_action(&self.state, raw_key, BindingDispatch::Prefix)
|
||||
{
|
||||
self.execute_prefix_key_action(action);
|
||||
return;
|
||||
}
|
||||
|
||||
leave_command_mode(&mut self.state);
|
||||
}
|
||||
|
||||
fn execute_prefix_key_action(&mut self, action: NavigateAction) {
|
||||
if action == NavigateAction::EditScrollback {
|
||||
let previous_mode = self.state.mode;
|
||||
self.cancel_copy_mode_if_active();
|
||||
self.launch_focused_scrollback_editor();
|
||||
finish_action_context(&mut self.state, ActionContext::Prefix, previous_mode);
|
||||
} else if action == NavigateAction::CopyMode {
|
||||
self.cancel_copy_mode_if_active();
|
||||
self.execute_tui_navigate_action(action, ActionContext::Prefix);
|
||||
} else if copy_mode_survives_prefix_action(action) {
|
||||
self.execute_tui_navigate_action(action, ActionContext::Prefix);
|
||||
if self.state.copy_mode.is_some() {
|
||||
self.state.sync_copy_mode_with_focus();
|
||||
}
|
||||
} else {
|
||||
self.cancel_copy_mode_if_active();
|
||||
self.execute_tui_navigate_action(action, ActionContext::Prefix);
|
||||
}
|
||||
self.selection_autoscroll_deadline = None;
|
||||
}
|
||||
|
||||
pub(crate) fn handle_navigate_key(&mut self, raw_key: TerminalKey) {
|
||||
let key = raw_key.as_key_event();
|
||||
self.state.update_dismissed = true;
|
||||
|
|
@ -127,7 +155,7 @@ impl App {
|
|||
return;
|
||||
}
|
||||
|
||||
if let Some(action) = navigate_mode_action_for_key(&self.state, raw_key) {
|
||||
if let Some(action) = navigate_mode_non_indexed_action_for_key(&self.state, raw_key) {
|
||||
if action == NavigateAction::EditScrollback {
|
||||
self.launch_focused_scrollback_editor();
|
||||
} else {
|
||||
|
|
@ -139,6 +167,12 @@ impl App {
|
|||
|
||||
if let Some(binding) = command_for_key(&self.state, raw_key, BindingDispatch::Prefix) {
|
||||
self.launch_custom_command(binding, ActionContext::Navigate);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(action) = navigate_mode_indexed_action_for_key(&self.state, raw_key) {
|
||||
self.execute_tui_navigate_action(action, ActionContext::Navigate);
|
||||
self.selection_autoscroll_deadline = None;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1360,15 +1394,21 @@ fn action_matches(
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn action_for_key(
|
||||
state: &AppState,
|
||||
key: TerminalKey,
|
||||
dispatch: BindingDispatch,
|
||||
) -> Option<NavigateAction> {
|
||||
if let Some(action) = indexed_navigation_action(state, key, dispatch) {
|
||||
return Some(action);
|
||||
}
|
||||
non_indexed_action_for_key(state, key, dispatch)
|
||||
.or_else(|| indexed_navigation_action(state, key, dispatch))
|
||||
}
|
||||
|
||||
fn non_indexed_action_for_key(
|
||||
state: &AppState,
|
||||
key: TerminalKey,
|
||||
dispatch: BindingDispatch,
|
||||
) -> Option<NavigateAction> {
|
||||
let kb = &state.keybinds;
|
||||
for (bindings, action) in [
|
||||
(&kb.help, NavigateAction::Help),
|
||||
|
|
@ -1424,6 +1464,7 @@ fn action_for_key(
|
|||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn navigate_mode_action_for_key(state: &AppState, key: TerminalKey) -> Option<NavigateAction> {
|
||||
let action = action_for_key(state, key, BindingDispatch::Prefix)?;
|
||||
if matches!(
|
||||
|
|
@ -1438,6 +1479,30 @@ fn navigate_mode_action_for_key(state: &AppState, key: TerminalKey) -> Option<Na
|
|||
Some(action)
|
||||
}
|
||||
|
||||
fn navigate_mode_non_indexed_action_for_key(
|
||||
state: &AppState,
|
||||
key: TerminalKey,
|
||||
) -> Option<NavigateAction> {
|
||||
let action = non_indexed_action_for_key(state, key, BindingDispatch::Prefix)?;
|
||||
if matches!(
|
||||
action,
|
||||
NavigateAction::FocusPaneLeft
|
||||
| NavigateAction::FocusPaneDown
|
||||
| NavigateAction::FocusPaneUp
|
||||
| NavigateAction::FocusPaneRight
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
Some(action)
|
||||
}
|
||||
|
||||
fn navigate_mode_indexed_action_for_key(
|
||||
state: &AppState,
|
||||
key: TerminalKey,
|
||||
) -> Option<NavigateAction> {
|
||||
indexed_navigation_action(state, key, BindingDispatch::Prefix)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn execute_navigate_action(state: &mut AppState, action: NavigateAction) {
|
||||
let mut terminal_runtimes = TerminalRuntimeRegistry::new();
|
||||
|
|
@ -2383,6 +2448,113 @@ last_pane = "prefix+tab"
|
|||
assert_eq!(action, Some(NavigateAction::SwitchTab(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_shift_indexed_workspace_shortcut_maps_shifted_symbol_key() {
|
||||
let mut state = state_with_workspaces(&["one", "two"]);
|
||||
let config: Config =
|
||||
toml::from_str("[keys]\nswitch_workspace = \"prefix+shift+1..9\"\n").unwrap();
|
||||
state.keybinds.switch_workspace = config.keybinds().switch_workspace;
|
||||
|
||||
let action = action_for_key(
|
||||
&state,
|
||||
TerminalKey::new(KeyCode::Char('@'), KeyModifiers::empty()),
|
||||
BindingDispatch::Prefix,
|
||||
);
|
||||
|
||||
assert_eq!(action, Some(NavigateAction::SwitchWorkspace(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_symbol_binding_takes_precedence_over_shifted_indexed_alias() {
|
||||
let mut state = state_with_workspaces(&["one", "two"]);
|
||||
let config: Config = toml::from_str(
|
||||
r#"
|
||||
[keys]
|
||||
help = "prefix+!"
|
||||
switch_workspace = "prefix+shift+1..9"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
state.keybinds = config.keybinds();
|
||||
|
||||
let action = action_for_key(
|
||||
&state,
|
||||
TerminalKey::new(KeyCode::Char('!'), KeyModifiers::empty()),
|
||||
BindingDispatch::Prefix,
|
||||
);
|
||||
|
||||
assert_eq!(action, Some(NavigateAction::Help));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_symbol_custom_command_is_visible_before_shifted_indexed_alias() {
|
||||
let mut state = state_with_workspaces(&["one", "two"]);
|
||||
let config: Config = toml::from_str(
|
||||
r#"
|
||||
[keys]
|
||||
switch_workspace = "prefix+shift+1..9"
|
||||
|
||||
[[keys.command]]
|
||||
key = "prefix+!"
|
||||
command = "echo literal"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
state.keybinds = config.keybinds();
|
||||
|
||||
let key = TerminalKey::new(KeyCode::Char('!'), KeyModifiers::empty());
|
||||
assert!(command_for_key(&state, key, BindingDispatch::Prefix).is_some());
|
||||
assert_eq!(
|
||||
indexed_navigation_action(&state, key, BindingDispatch::Prefix),
|
||||
Some(NavigateAction::SwitchWorkspace(0))
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn literal_symbol_custom_command_runs_before_shifted_indexed_alias() {
|
||||
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let mut app = App::new(
|
||||
&Config::default(),
|
||||
true,
|
||||
None,
|
||||
api_rx,
|
||||
crate::api::EventHub::default(),
|
||||
);
|
||||
app.state.workspaces = vec![Workspace::test_new("one"), Workspace::test_new("two")];
|
||||
app.state.active = Some(1);
|
||||
app.state.selected = 1;
|
||||
app.state.mode = Mode::Terminal;
|
||||
|
||||
let output_path = unique_temp_path("literal-symbol-custom-command");
|
||||
let config: Config = toml::from_str(&format!(
|
||||
r#"
|
||||
[keys]
|
||||
switch_workspace = "prefix+shift+1..9"
|
||||
|
||||
[[keys.command]]
|
||||
key = "prefix+!"
|
||||
command = "printf literal > '{}'"
|
||||
"#,
|
||||
output_path.display()
|
||||
))
|
||||
.unwrap();
|
||||
app.state.keybinds = config.keybinds();
|
||||
|
||||
app.handle_key(TerminalKey::new(
|
||||
app.state.prefix_code,
|
||||
app.state.prefix_mods,
|
||||
))
|
||||
.await;
|
||||
app.handle_key(TerminalKey::new(KeyCode::Char('!'), KeyModifiers::empty()))
|
||||
.await;
|
||||
|
||||
assert_eq!(wait_for_file(&output_path), "literal");
|
||||
assert_eq!(app.state.active, Some(1));
|
||||
assert_eq!(app.state.mode, Mode::Terminal);
|
||||
let _ = std::fs::remove_file(output_path);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn navigate_mode_runs_prefix_action_rhs_without_pressing_prefix_again() {
|
||||
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ impl App {
|
|||
|
||||
let key_event = key.as_key_event();
|
||||
|
||||
if let Some(action) = super::terminal_direct_navigation_action(&self.state, key) {
|
||||
if let Some(action) = super::terminal_direct_non_indexed_navigation_action(&self.state, key)
|
||||
{
|
||||
debug!(
|
||||
code = ?key_event.code,
|
||||
modifiers = ?key_event.modifiers,
|
||||
|
|
@ -66,6 +67,18 @@ impl App {
|
|||
return None;
|
||||
}
|
||||
|
||||
if let Some(action) = super::terminal_direct_indexed_navigation_action(&self.state, key) {
|
||||
debug!(
|
||||
code = ?key_event.code,
|
||||
modifiers = ?key_event.modifiers,
|
||||
kind = ?key_event.kind,
|
||||
action = ?action,
|
||||
"intercepted terminal direct indexed keybinding before forwarding to pane"
|
||||
);
|
||||
self.execute_tui_navigate_action(action, super::navigate::ActionContext::Direct);
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.state.is_prefix_key(key) {
|
||||
self.state.mode = Mode::Prefix;
|
||||
return None;
|
||||
|
|
|
|||
|
|
@ -255,11 +255,21 @@ pub struct IndexedKeybind {
|
|||
|
||||
impl IndexedKeybind {
|
||||
pub fn matched_index(&self, key: TerminalKey) -> Option<usize> {
|
||||
let KeyCode::Char(c @ '1'..='9') = key.code else {
|
||||
return None;
|
||||
let key_number = match key.code {
|
||||
KeyCode::Char(c @ '1'..='9') => c,
|
||||
KeyCode::Char(c) => {
|
||||
let number = shifted_number_symbol(c)?;
|
||||
if !indexed_shifted_number_matches(key, self.trigger.combo(), number) {
|
||||
return None;
|
||||
}
|
||||
number
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
if terminal_key_matches_combo(key, self.trigger.combo()) {
|
||||
Some((c as usize) - ('1' as usize))
|
||||
let legacy_shifted_number =
|
||||
matches!(key.code, KeyCode::Char(c) if shifted_number_symbol(c) == Some(key_number));
|
||||
if terminal_key_matches_combo(key, self.trigger.combo()) || legacy_shifted_number {
|
||||
Some((key_number as usize) - ('1' as usize))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -1355,6 +1365,31 @@ fn legacy_shifted_ascii_letter_matches(
|
|||
&& actual_modifiers | KeyModifiers::SHIFT == expected_modifiers
|
||||
}
|
||||
|
||||
const SHIFTED_NUMBER_SYMBOLS: [(char, char); 9] = [
|
||||
('1', '!'),
|
||||
('2', '@'),
|
||||
('3', '#'),
|
||||
('4', '$'),
|
||||
('5', '%'),
|
||||
('6', '^'),
|
||||
('7', '&'),
|
||||
('8', '*'),
|
||||
('9', '('),
|
||||
];
|
||||
|
||||
fn shifted_number_symbol(ch: char) -> Option<char> {
|
||||
SHIFTED_NUMBER_SYMBOLS
|
||||
.iter()
|
||||
.find_map(|(number, symbol)| (*symbol == ch).then_some(*number))
|
||||
}
|
||||
|
||||
fn indexed_shifted_number_matches(key: TerminalKey, combo: KeyCombo, number: char) -> bool {
|
||||
let (expected_code, expected_modifiers) = normalize_key_combo(combo);
|
||||
matches!(expected_code, KeyCode::Char(expected) if expected == number)
|
||||
&& expected_modifiers.contains(KeyModifiers::SHIFT)
|
||||
&& key.modifiers == expected_modifiers.difference(KeyModifiers::SHIFT)
|
||||
}
|
||||
|
||||
fn shifted_char_matches_expected(
|
||||
actual_code: KeyCode,
|
||||
shifted_codepoint: Option<u32>,
|
||||
|
|
|
|||
Loading…
Reference in New Issue