doing key sequencing via space

This commit is contained in:
Priec
2025-09-08 12:56:03 +02:00
parent c2a6272413
commit ad15becd7a
2 changed files with 189 additions and 80 deletions

View File

@@ -67,6 +67,7 @@ impl KeySequenceTracker {
// Helper function to convert any KeyCode to a string representation // Helper function to convert any KeyCode to a string representation
pub fn key_to_string(key: &KeyCode) -> String { pub fn key_to_string(key: &KeyCode) -> String {
match key { match key {
KeyCode::Char(' ') => "space".to_string(),
KeyCode::Char(c) => c.to_string(), KeyCode::Char(c) => c.to_string(),
KeyCode::Left => "left".to_string(), KeyCode::Left => "left".to_string(),
KeyCode::Right => "right".to_string(), KeyCode::Right => "right".to_string(),
@@ -90,6 +91,7 @@ pub fn key_to_string(key: &KeyCode) -> String {
// Helper function to convert a string to a KeyCode // Helper function to convert a string to a KeyCode
pub fn string_to_keycode(s: &str) -> Option<KeyCode> { pub fn string_to_keycode(s: &str) -> Option<KeyCode> {
match s.to_lowercase().as_str() { match s.to_lowercase().as_str() {
"space" => Some(KeyCode::Char(' ')),
"left" => Some(KeyCode::Left), "left" => Some(KeyCode::Left),
"right" => Some(KeyCode::Right), "right" => Some(KeyCode::Right),
"up" => Some(KeyCode::Up), "up" => Some(KeyCode::Up),
@@ -140,7 +142,7 @@ fn is_compound_key(part: &str) -> bool {
matches!(part.to_lowercase().as_str(), matches!(part.to_lowercase().as_str(),
"esc" | "up" | "down" | "left" | "right" | "enter" | "esc" | "up" | "down" | "left" | "right" | "enter" |
"backspace" | "delete" | "tab" | "backtab" | "home" | "backspace" | "delete" | "tab" | "backtab" | "home" |
"end" | "pageup" | "pagedown" | "insert" "end" | "pageup" | "pagedown" | "insert" | "space"
) )
} }

View File

@@ -106,7 +106,7 @@ impl EventHandler {
command_message: String::new(), command_message: String::new(),
edit_mode_cooldown: false, edit_mode_cooldown: false,
ideal_cursor_column: 0, ideal_cursor_column: 0,
key_sequence_tracker: KeySequenceTracker::new(400), key_sequence_tracker: KeySequenceTracker::new(1200),
auth_client: AuthClient::new().await?, auth_client: AuthClient::new().await?,
grpc_client, grpc_client,
login_result_sender, login_result_sender,
@@ -297,12 +297,71 @@ impl EventHandler {
let key_code = key_event.code; let key_code = key_event.code;
let modifiers = key_event.modifiers; let modifiers = key_event.modifiers;
// LOGIN: canvas <-> buttons focus handoff
// Do not let Login canvas receive keys when overlays/palettes are active
let overlay_active = self.command_mode let overlay_active = self.command_mode
|| app_state.ui.show_search_palette || app_state.ui.show_search_palette
|| self.navigation_state.active; || self.navigation_state.active;
// --- SPACE-LED SEQUENCES INTERCEPTION (all modes except Form Edit) ---
if !overlay_active {
// Detect if we are in a Form and the canvas is in Edit mode (then we don't intercept)
let in_form_edit_mode = matches!(
&router.current,
Page::Form(path) if {
if let Some(editor) = app_state.editor_for_path_ref(path) {
editor.mode() == CanvasMode::Edit
} else { false }
}
);
if !in_form_edit_mode {
let space = KeyCode::Char(' ');
let seq_active = !self.key_sequence_tracker.current_sequence.is_empty()
&& self.key_sequence_tracker.current_sequence[0] == space;
if seq_active {
self.key_sequence_tracker.add_key(key_code);
let sequence = self.key_sequence_tracker.get_sequence();
if let Some(action) = config.matches_key_sequence_generalized(&sequence) {
self.key_sequence_tracker.reset();
if let Some(outcome) = self
.dispatch_space_sequence_action(
action,
config,
terminal,
command_handler,
auth_state,
buffer_state,
app_state,
router,
)
.await?
{
return Ok(outcome);
} else {
return Ok(EventOutcome::Ok(String::new()));
}
}
if config.is_key_sequence_prefix(&sequence) {
return Ok(EventOutcome::Ok(String::new()));
}
// Not matched and not prefix → reset and fall through
self.key_sequence_tracker.reset();
} else if key_code == space && config.is_key_sequence_prefix(&[space]) {
// Start new space-led sequence and do not let the page consume the space
self.key_sequence_tracker.reset();
self.key_sequence_tracker.add_key(space);
return Ok(EventOutcome::Ok(String::new()));
}
}
}
// --- END SPACE-LED SEQUENCES ---
// LOGIN: canvas <-> buttons focus handoff
// Do not let Login canvas receive keys when overlays/palettes are active
if !overlay_active { if !overlay_active {
if let Page::Login(login_page) = &mut router.current { if let Page::Login(login_page) = &mut router.current {
let outcome = let outcome =
@@ -322,16 +381,21 @@ impl EventHandler {
return Ok(outcome); return Ok(outcome);
} }
} else if let Page::Form(path) = &router.current { } else if let Page::Form(path) = &router.current {
// If a space-led sequence is in progress or has just begun, do not forward to editor
if self.key_sequence_tracker.current_sequence.is_empty() {
let outcome = forms::event::handle_form_event( let outcome = forms::event::handle_form_event(
event, event,
app_state, app_state,
path, path,
&mut self.ideal_cursor_column, &mut self.ideal_cursor_column,
)?; )?;
// Only return if the form page actually consumed the key
if !outcome.get_message_if_ok().is_empty() { if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome); return Ok(outcome);
} }
} else {
// Sequence is active; we already handled or are waiting for more keys
return Ok(EventOutcome::Ok(String::new()));
}
} else if let Page::AddLogic(add_logic_page) = &mut router.current { } else if let Page::AddLogic(add_logic_page) = &mut router.current {
// Allow ":" (enter_command_mode) even when inside AddLogic canvas // Allow ":" (enter_command_mode) even when inside AddLogic canvas
if let Some(action) = if let Some(action) =
@@ -742,74 +806,9 @@ impl EventHandler {
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
// Default command-mode handling: we do not treat Space as a leader here.
// Space-led sequences are handled only in General mode above.
if let KeyCode::Char(c) = key_code { if let KeyCode::Char(c) = key_code {
if c == 'f' {
self.key_sequence_tracker.add_key(key_code);
let sequence =
self.key_sequence_tracker.get_sequence();
if config.matches_key_sequence_generalized(
&sequence,
) == Some("find_file_palette_toggle")
{
if matches!(&router.current, Page::Form(_) | Page::Intro(_)) {
let mut all_table_paths: Vec<String> =
app_state
.profile_tree
.profiles
.iter()
.flat_map(|profile| {
profile.tables.iter().map(
move |table| {
format!(
"{}/{}",
profile.name,
table.name
)
},
)
})
.collect();
all_table_paths.sort();
self.navigation_state
.activate_find_file(all_table_paths);
self.command_mode = false;
self.command_input.clear();
self.command_message.clear();
self.key_sequence_tracker.reset();
return Ok(EventOutcome::Ok(
"Table selection palette activated"
.to_string(),
));
} else {
self.key_sequence_tracker.reset();
self.command_input.push('f');
if sequence.len() > 1
&& sequence[0] == KeyCode::Char('f')
{
self.command_input.push('f');
}
self.command_message = "Find File not available in this view."
.to_string();
return Ok(EventOutcome::Ok(
self.command_message.clone(),
));
}
}
if config.is_key_sequence_prefix(&sequence) {
return Ok(EventOutcome::Ok(String::new()));
}
}
if c != 'f'
&& !self.key_sequence_tracker.current_sequence.is_empty()
{
self.key_sequence_tracker.reset();
}
self.command_input.push(c); self.command_input.push(c);
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
@@ -1004,3 +1003,111 @@ impl EventHandler {
} }
} }
} }
impl EventHandler {
#[allow(clippy::too_many_arguments)]
async fn dispatch_space_sequence_action(
&mut self,
action: &str,
config: &Config,
terminal: &mut TerminalCore,
command_handler: &mut CommandHandler,
auth_state: &mut AuthState,
buffer_state: &mut BufferState,
app_state: &mut AppState,
router: &mut Router,
) -> Result<Option<EventOutcome>> {
match action {
// Reuse existing behavior
"next_buffer" => {
if switch_buffer(buffer_state, true) {
return Ok(Some(EventOutcome::Ok(
"Switched to next buffer".to_string(),
)));
}
Ok(Some(EventOutcome::Ok(String::new())))
}
"previous_buffer" => {
if switch_buffer(buffer_state, false) {
return Ok(Some(EventOutcome::Ok(
"Switched to previous buffer".to_string(),
)));
}
Ok(Some(EventOutcome::Ok(String::new())))
}
"close_buffer" => {
let current_table_name = app_state.current_view_table_name.as_deref();
let message =
buffer_state.close_buffer_with_intro_fallback(current_table_name);
Ok(Some(EventOutcome::Ok(message)))
}
"open_search" => {
if let Page::Form(_) = &router.current {
if let Some(table_name) = app_state.current_view_table_name.clone() {
app_state.ui.show_search_palette = true;
app_state.search_state = Some(SearchState::new(table_name));
self.set_focus_outside(router, true);
return Ok(Some(EventOutcome::Ok(
"Search palette opened".to_string(),
)));
}
}
Ok(Some(EventOutcome::Ok(String::new())))
}
"enter_command_mode" => {
if self.is_focus_outside(router)
&& !self.command_mode
&& !app_state.ui.show_search_palette
&& !self.navigation_state.active
{
self.command_mode = true;
self.command_input.clear();
self.command_message.clear();
self.key_sequence_tracker.reset();
self.set_focus_outside(router, true);
return Ok(Some(EventOutcome::Ok(String::new())));
}
Ok(Some(EventOutcome::Ok(String::new())))
}
"find_file_palette_toggle" => {
if matches!(&router.current, Page::Form(_) | Page::Intro(_)) {
let mut all_table_paths: Vec<String> = app_state
.profile_tree
.profiles
.iter()
.flat_map(|profile| {
profile.tables.iter().map(move |table| {
format!("{}/{}", profile.name, table.name)
})
})
.collect();
all_table_paths.sort();
self.navigation_state.activate_find_file(all_table_paths);
self.command_mode = false;
self.command_input.clear();
self.command_message.clear();
self.key_sequence_tracker.reset();
return Ok(Some(EventOutcome::Ok(
"Table selection palette activated".to_string(),
)));
}
Ok(Some(EventOutcome::Ok(String::new())))
}
// Core actions that already have a handler
"save" | "force_quit" | "save_and_quit" | "revert" => {
let outcome = self
.handle_core_action(
action,
auth_state,
terminal,
app_state,
router,
)
.await?;
Ok(Some(outcome))
}
_ => Ok(None),
}
}
}