doing key sequencing via space
This commit is contained in:
@@ -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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,15 +381,20 @@ impl EventHandler {
|
|||||||
return Ok(outcome);
|
return Ok(outcome);
|
||||||
}
|
}
|
||||||
} else if let Page::Form(path) = &router.current {
|
} else if let Page::Form(path) = &router.current {
|
||||||
let outcome = forms::event::handle_form_event(
|
// If a space-led sequence is in progress or has just begun, do not forward to editor
|
||||||
event,
|
if self.key_sequence_tracker.current_sequence.is_empty() {
|
||||||
app_state,
|
let outcome = forms::event::handle_form_event(
|
||||||
path,
|
event,
|
||||||
&mut self.ideal_cursor_column,
|
app_state,
|
||||||
)?;
|
path,
|
||||||
// Only return if the form page actually consumed the key
|
&mut self.ideal_cursor_column,
|
||||||
if !outcome.get_message_if_ok().is_empty() {
|
)?;
|
||||||
return Ok(outcome);
|
if !outcome.get_message_if_ok().is_empty() {
|
||||||
|
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
|
||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user