aboutsummaryrefslogtreecommitdiff
path: root/src/app/input
diff options
context:
space:
mode:
authorsf-jin-ku <jin.ku@sendbird.com>2026-06-03 09:13:01 -0700
committerGitHub <noreply@github.com>2026-06-03 19:13:01 +0300
commitf5e66b251fa8ec24989bf25861bcf4e38a561470 (patch)
tree3244ffd1af0054334f8fe65ddbb0ff6f5a809cd3 /src/app/input
parent03f68b7275f07c98769bf699d2aa14805b921f8d (diff)
feat: switch input source during prefix mode (#434)
On macOS, opt-in under [experimental], temporarily switch the host input source to the current ASCII-capable layout while prefix mode is active so prefix commands register even when a CJK IME is composing, then restore the previous input source when prefix mode exits or is cancelled. No-op when the current source is already ASCII-capable, on non-macOS platforms, or when any input source call fails. Pane text input is unaffected. Also pin next_resize_poll in next_loop_deadline_includes_selection_autoscroll_deadline so the selection autoscroll deadline stays the earliest; the default resize poll could otherwise beat it and flake the assertion. refs #400 Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/app/input')
-rw-r--r--src/app/input/mod.rs3
-rw-r--r--src/app/input/settings.rs82
2 files changed, 77 insertions, 8 deletions
diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs
index cfcc3039..1da647e8 100644
--- a/src/app/input/mod.rs
+++ b/src/app/input/mod.rs
@@ -219,6 +219,9 @@ impl App {
SettingsAction::SavePaneHistory(enabled) => {
self.save_pane_history_persistence(enabled)
}
+ SettingsAction::SaveSwitchAsciiInputSourceInPrefix(enabled) => {
+ self.save_switch_ascii_input_source_in_prefix(enabled)
+ }
SettingsAction::InstallRecommendedIntegrations => {
self.install_recommended_integrations()
}
diff --git a/src/app/input/settings.rs b/src/app/input/settings.rs
index 986f59e2..66ec3226 100644
--- a/src/app/input/settings.rs
+++ b/src/app/input/settings.rs
@@ -3,7 +3,7 @@ use ratatui::layout::Rect;
use crate::{
app::{
- state::{AppState, SettingsSection, THEME_NAMES},
+ state::{AppState, ExperimentSetting, SettingsSection, THEME_NAMES},
App, Mode,
},
config::ToastDelivery,
@@ -18,9 +18,24 @@ pub(super) enum SettingsAction {
SaveToastDelivery(ToastDelivery),
SaveAgentBorderLabels(bool),
SavePaneHistory(bool),
+ SaveSwitchAsciiInputSourceInPrefix(bool),
InstallRecommendedIntegrations,
}
+/// Map an Experiments row index to the toggle action that flips it.
+fn experiment_toggle_action(state: &AppState, idx: usize) -> Option<SettingsAction> {
+ match ExperimentSetting::ALL.get(idx).copied()? {
+ ExperimentSetting::PaneHistory => Some(SettingsAction::SavePaneHistory(
+ !ExperimentSetting::PaneHistory.enabled(state),
+ )),
+ ExperimentSetting::SwitchAsciiInputSourceInPrefix => {
+ Some(SettingsAction::SaveSwitchAsciiInputSourceInPrefix(
+ !ExperimentSetting::SwitchAsciiInputSourceInPrefix.enabled(state),
+ ))
+ }
+ }
+}
+
impl App {
pub(crate) fn handle_settings_key(&mut self, key: KeyEvent) {
let previous_section = self.state.settings.section;
@@ -35,6 +50,9 @@ impl App {
SettingsAction::SavePaneHistory(enabled) => {
self.save_pane_history_persistence(enabled)
}
+ SettingsAction::SaveSwitchAsciiInputSourceInPrefix(enabled) => {
+ self.save_switch_ascii_input_source_in_prefix(enabled)
+ }
SettingsAction::InstallRecommendedIntegrations => {
self.install_recommended_integrations()
}
@@ -228,10 +246,12 @@ pub(super) fn update_settings_state(state: &mut AppState, key: KeyEvent) -> Opti
}
},
SettingsSection::Experiments => match key.code {
+ KeyCode::Up | KeyCode::Char('k') => state.settings.list.move_prev(),
+ KeyCode::Down | KeyCode::Char('j') => {
+ state.settings.list.move_next(ExperimentSetting::ALL.len())
+ }
KeyCode::Enter | KeyCode::Char(' ') => {
- return Some(SettingsAction::SavePaneHistory(
- !state.pane_history_persistence_enabled(),
- ));
+ return experiment_toggle_action(state, state.settings.list.selected);
}
KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => {
state.settings.section = SettingsSection::Integrations;
@@ -377,7 +397,11 @@ impl AppState {
}
SettingsSection::Experiments => {
let list_y = area.y + 3;
- (row == list_y).then_some(0)
+ if row >= list_y && row < list_y + ExperimentSetting::ALL.len() as u16 {
+ Some((row - list_y) as usize)
+ } else {
+ None
+ }
}
SettingsSection::Integrations => None,
}
@@ -419,9 +443,7 @@ impl AppState {
let enabled = idx == 0;
Some(SettingsAction::SaveAgentBorderLabels(enabled))
}
- SettingsSection::Experiments => Some(SettingsAction::SavePaneHistory(
- !self.pane_history_persistence_enabled(),
- )),
+ SettingsSection::Experiments => experiment_toggle_action(self, idx),
SettingsSection::Integrations => None,
};
}
@@ -524,6 +546,30 @@ mod tests {
}
#[test]
+ fn settings_experiments_down_then_toggle_switches_ascii_input_source() {
+ let mut state = state_with_workspaces(&["test"]);
+ state.switch_ascii_input_source_in_prefix = false;
+ open_settings_at(&mut state, SettingsSection::Experiments);
+
+ update_settings_state(
+ &mut state,
+ KeyEvent::new(KeyCode::Down, KeyModifiers::empty()),
+ );
+ assert_eq!(state.settings.list.selected, 1);
+
+ let action = update_settings_state(
+ &mut state,
+ KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
+ );
+
+ assert_eq!(
+ action,
+ Some(SettingsAction::SaveSwitchAsciiInputSourceInPrefix(true))
+ );
+ assert_eq!(state.mode, Mode::Settings);
+ }
+
+ #[test]
fn settings_tab_cycle_places_experiments_last() {
let mut state = state_with_workspaces(&["test"]);
open_settings_at(&mut state, SettingsSection::PaneLabels);
@@ -607,6 +653,26 @@ mod tests {
}
#[test]
+ fn settings_mouse_click_toggles_switch_ascii_input_source_row() {
+ let mut app = app_for_mouse_test();
+ app.state.switch_ascii_input_source_in_prefix = false;
+ open_settings_at(&mut app.state, SettingsSection::Experiments);
+
+ let area = app.state.settings_content_rect();
+ let action = app.state.handle_settings_mouse(mouse(
+ MouseEventKind::Down(crossterm::event::MouseButton::Left),
+ area.x + 2,
+ area.y + 4,
+ ));
+
+ assert_eq!(
+ action,
+ Some(SettingsAction::SaveSwitchAsciiInputSourceInPrefix(true))
+ );
+ assert_eq!(app.state.settings.list.selected, 1);
+ }
+
+ #[test]
fn integration_update_badge_only_tracks_outdated_recommendations() {
let mut state = state_with_workspaces(&["test"]);
state.integration_recommendations = vec![integration_recommendation(