Compare commits
4 Commits
c2a6272413
...
a604d62d44
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a604d62d44 | ||
|
|
2cbbfd21aa | ||
|
|
1c17d07497 | ||
|
|
ad15becd7a |
@@ -2,9 +2,14 @@
|
|||||||
[keybindings]
|
[keybindings]
|
||||||
|
|
||||||
enter_command_mode = [":", "ctrl+;"]
|
enter_command_mode = [":", "ctrl+;"]
|
||||||
next_buffer = ["space+b+n"]
|
next_buffer = ["ctrl+b+n"]
|
||||||
previous_buffer = ["space+b+p"]
|
previous_buffer = ["ctrl+b+p"]
|
||||||
close_buffer = ["space+b+d"]
|
close_buffer = ["ctrl+b+d"]
|
||||||
|
# SPACE NOT WORKING, NEEDS REDESIGN
|
||||||
|
# next_buffer = ["space+b+n"]
|
||||||
|
# previous_buffer = ["space+b+p"]
|
||||||
|
# close_buffer = ["space+b+d"]
|
||||||
|
# revert = ["space+b+r"]
|
||||||
|
|
||||||
[keybindings.general]
|
[keybindings.general]
|
||||||
up = ["k", "Up"]
|
up = ["k", "Up"]
|
||||||
@@ -27,7 +32,6 @@ move_up = ["Up"]
|
|||||||
move_down = ["Down"]
|
move_down = ["Down"]
|
||||||
toggle_sidebar = ["ctrl+t"]
|
toggle_sidebar = ["ctrl+t"]
|
||||||
toggle_buffer_list = ["ctrl+b"]
|
toggle_buffer_list = ["ctrl+b"]
|
||||||
revert = ["space+b+r"]
|
|
||||||
|
|
||||||
# MODE SPECIFIC
|
# MODE SPECIFIC
|
||||||
# READ ONLY MODE
|
# READ ONLY MODE
|
||||||
@@ -60,7 +64,7 @@ prev_field = ["Shift+Tab"]
|
|||||||
|
|
||||||
[keybindings.highlight]
|
[keybindings.highlight]
|
||||||
exit_highlight_mode = ["esc"]
|
exit_highlight_mode = ["esc"]
|
||||||
enter_highlight_mode_linewise = ["ctrl+v"]
|
enter_highlight_mode_linewise = ["shift+v"]
|
||||||
|
|
||||||
### AUTOGENERATED CANVAS CONFIG
|
### AUTOGENERATED CANVAS CONFIG
|
||||||
# Required
|
# Required
|
||||||
|
|||||||
@@ -250,28 +250,44 @@ impl Config {
|
|||||||
key: KeyCode,
|
key: KeyCode,
|
||||||
modifiers: KeyModifiers,
|
modifiers: KeyModifiers,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// Special handling for shift+character combinations
|
|
||||||
if binding.to_lowercase().starts_with("shift+") {
|
// Normalize binding once
|
||||||
let parts: Vec<&str> = binding.split('+').collect();
|
let binding_lc = binding.to_lowercase();
|
||||||
if parts.len() == 2 && parts[1].len() == 1 {
|
|
||||||
let expected_lowercase = parts[1].chars().next().unwrap().to_lowercase().next().unwrap();
|
// Robust handling for Shift+Tab
|
||||||
let expected_uppercase = expected_lowercase.to_uppercase().next().unwrap();
|
// Accept either BackTab (with or without SHIFT flagged) or Tab+SHIFT
|
||||||
if let KeyCode::Char(actual_char) = key {
|
if binding_lc == "shift+tab" || binding_lc == "backtab" {
|
||||||
if actual_char == expected_uppercase && modifiers.contains(KeyModifiers::SHIFT) {
|
return match key {
|
||||||
return true;
|
KeyCode::BackTab => true,
|
||||||
}
|
KeyCode::Tab => modifiers.contains(KeyModifiers::SHIFT),
|
||||||
}
|
_ => false,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Shift+Tab -> BackTab
|
// Robust handling for shift+<char> (letters)
|
||||||
if binding.to_lowercase() == "shift+tab" && key == KeyCode::BackTab && modifiers.is_empty() {
|
// Many terminals send uppercase Char without SHIFT bit.
|
||||||
|
if binding_lc.starts_with("shift+") {
|
||||||
|
let parts: Vec<&str> = binding.split('+').collect();
|
||||||
|
if parts.len() == 2 && parts[1].chars().count() == 1 {
|
||||||
|
let base = parts[1].chars().next().unwrap();
|
||||||
|
let upper = base.to_ascii_uppercase();
|
||||||
|
let lower = base.to_ascii_lowercase();
|
||||||
|
if let KeyCode::Char(actual) = key {
|
||||||
|
// Accept uppercase char regardless of SHIFT bit
|
||||||
|
if actual == upper {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Also accept lowercase char with SHIFT flagged (some terms do this)
|
||||||
|
if actual == lower && modifiers.contains(KeyModifiers::SHIFT) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle multi-character bindings (all standard keys without modifiers)
|
// Handle multi-character bindings (all standard keys without modifiers)
|
||||||
if binding.len() > 1 && !binding.contains('+') {
|
if binding.len() > 1 && !binding.contains('+') {
|
||||||
return match binding.to_lowercase().as_str() {
|
return match binding_lc.as_str() {
|
||||||
// Navigation keys
|
// Navigation keys
|
||||||
"left" => key == KeyCode::Left,
|
"left" => key == KeyCode::Left,
|
||||||
"right" => key == KeyCode::Right,
|
"right" => key == KeyCode::Right,
|
||||||
@@ -371,6 +387,7 @@ impl Config {
|
|||||||
let mut expected_key = None;
|
let mut expected_key = None;
|
||||||
|
|
||||||
for part in parts {
|
for part in parts {
|
||||||
|
let part_lc = part.to_lowercase();
|
||||||
match part.to_lowercase().as_str() {
|
match part.to_lowercase().as_str() {
|
||||||
// Modifiers
|
// Modifiers
|
||||||
"ctrl" | "control" => expected_modifiers |= KeyModifiers::CONTROL,
|
"ctrl" | "control" => expected_modifiers |= KeyModifiers::CONTROL,
|
||||||
@@ -789,12 +806,43 @@ impl Config {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize bindings for canvas consumption:
|
||||||
|
// - "shift+<char>" -> also add "<CHAR>"
|
||||||
|
// - "shift+tab" -> also add "backtab"
|
||||||
|
// This keeps your config human-friendly while making the canvas happy.
|
||||||
|
fn normalize_for_canvas(
|
||||||
|
map: &HashMap<String, Vec<String>>,
|
||||||
|
) -> HashMap<String, Vec<String>> {
|
||||||
|
let mut out: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
|
for (action, bindings) in map {
|
||||||
|
let mut new_list: Vec<String> = Vec::new();
|
||||||
|
for b in bindings {
|
||||||
|
new_list.push(b.clone());
|
||||||
|
let blc = b.to_lowercase();
|
||||||
|
if blc.starts_with("shift+") {
|
||||||
|
let parts: Vec<&str> = b.split('+').collect();
|
||||||
|
if parts.len() == 2 && parts[1].chars().count() == 1 {
|
||||||
|
let ch = parts[1].chars().next().unwrap();
|
||||||
|
new_list.push(ch.to_ascii_uppercase().to_string());
|
||||||
|
}
|
||||||
|
if blc == "shift+tab" {
|
||||||
|
new_list.push("backtab".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if blc == "shift+tab" {
|
||||||
|
new_list.push("backtab".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.insert(action.clone(), new_list);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_canvas_keymap(&self) -> CanvasKeyMap {
|
pub fn build_canvas_keymap(&self) -> CanvasKeyMap {
|
||||||
CanvasKeyMap::from_mode_maps(
|
let ro = Self::normalize_for_canvas(&self.keybindings.read_only);
|
||||||
&self.keybindings.read_only,
|
let ed = Self::normalize_for_canvas(&self.keybindings.edit);
|
||||||
&self.keybindings.edit,
|
let hl = Self::normalize_for_canvas(&self.keybindings.highlight);
|
||||||
&self.keybindings.highlight,
|
CanvasKeyMap::from_mode_maps(&ro, &ed, &hl)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
41
client/src/input/action.rs
Normal file
41
client/src/input/action.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// src/input/action.rs
|
||||||
|
use crate::movement::MovementAction;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum BufferAction {
|
||||||
|
Next,
|
||||||
|
Previous,
|
||||||
|
Close,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum CoreAction {
|
||||||
|
Save,
|
||||||
|
ForceQuit,
|
||||||
|
SaveAndQuit,
|
||||||
|
Revert,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum AppAction {
|
||||||
|
// Global/UI
|
||||||
|
ToggleSidebar,
|
||||||
|
ToggleBufferList,
|
||||||
|
OpenSearch,
|
||||||
|
FindFilePaletteToggle,
|
||||||
|
|
||||||
|
// Buffers
|
||||||
|
Buffer(BufferAction),
|
||||||
|
|
||||||
|
// Command mode
|
||||||
|
EnterCommandMode,
|
||||||
|
ExitCommandMode,
|
||||||
|
CommandExecute,
|
||||||
|
CommandBackspace,
|
||||||
|
|
||||||
|
// Navigation across UI
|
||||||
|
Navigate(MovementAction),
|
||||||
|
|
||||||
|
// Core actions
|
||||||
|
Core(CoreAction),
|
||||||
|
}
|
||||||
176
client/src/input/engine.rs
Normal file
176
client/src/input/engine.rs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
// src/input/engine.rs
|
||||||
|
use crate::config::binds::config::Config;
|
||||||
|
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||||
|
use crate::input::action::{AppAction, BufferAction, CoreAction};
|
||||||
|
use crate::movement::MovementAction;
|
||||||
|
use crate::modes::handlers::mode_manager::AppMode;
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct InputContext {
|
||||||
|
pub app_mode: AppMode,
|
||||||
|
pub overlay_active: bool,
|
||||||
|
pub allow_navigation_capture: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum InputOutcome {
|
||||||
|
Action(AppAction),
|
||||||
|
Pending, // sequence in progress
|
||||||
|
PassThrough, // let page/canvas handle it
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InputEngine {
|
||||||
|
seq: KeySequenceTracker,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputEngine {
|
||||||
|
pub fn new(timeout_ms: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
seq: KeySequenceTracker::new(timeout_ms),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_sequence(&mut self) {
|
||||||
|
self.seq.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_key(
|
||||||
|
&mut self,
|
||||||
|
key_event: KeyEvent,
|
||||||
|
ctx: &InputContext,
|
||||||
|
config: &Config,
|
||||||
|
) -> InputOutcome {
|
||||||
|
// Command mode keys are special (exit/execute/backspace) and typed chars
|
||||||
|
if ctx.app_mode == AppMode::Command {
|
||||||
|
if config.is_exit_command_mode(key_event.code, key_event.modifiers) {
|
||||||
|
self.seq.reset();
|
||||||
|
return InputOutcome::Action(AppAction::ExitCommandMode);
|
||||||
|
}
|
||||||
|
if config.is_command_execute(key_event.code, key_event.modifiers) {
|
||||||
|
self.seq.reset();
|
||||||
|
return InputOutcome::Action(AppAction::CommandExecute);
|
||||||
|
}
|
||||||
|
if config.is_command_backspace(key_event.code, key_event.modifiers) {
|
||||||
|
self.seq.reset();
|
||||||
|
return InputOutcome::Action(AppAction::CommandBackspace);
|
||||||
|
}
|
||||||
|
// Let command-line collect characters and other keys pass through
|
||||||
|
self.seq.reset();
|
||||||
|
return InputOutcome::PassThrough;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If overlays are active, do not intercept (palette, navigation, etc.)
|
||||||
|
if ctx.overlay_active {
|
||||||
|
self.seq.reset();
|
||||||
|
return InputOutcome::PassThrough;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Space-led multi-key sequences (leader = space)
|
||||||
|
if ctx.allow_navigation_capture {
|
||||||
|
let space = KeyCode::Char(' ');
|
||||||
|
let seq_active = !self.seq.current_sequence.is_empty()
|
||||||
|
&& self.seq.current_sequence[0] == space;
|
||||||
|
|
||||||
|
if seq_active {
|
||||||
|
self.seq.add_key(key_event.code);
|
||||||
|
let sequence = self.seq.get_sequence();
|
||||||
|
|
||||||
|
if let Some(action_str) = config.matches_key_sequence_generalized(&sequence) {
|
||||||
|
if let Some(app_action) = map_action_string(action_str, ctx) {
|
||||||
|
self.seq.reset();
|
||||||
|
return InputOutcome::Action(app_action);
|
||||||
|
}
|
||||||
|
// A non-app action sequence (canvas stuff) → pass-through
|
||||||
|
self.seq.reset();
|
||||||
|
return InputOutcome::PassThrough;
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.is_key_sequence_prefix(&sequence) {
|
||||||
|
return InputOutcome::Pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not matched and not a prefix → reset and continue to single key
|
||||||
|
self.seq.reset();
|
||||||
|
} else if key_event.code == space && config.is_key_sequence_prefix(&[space]) {
|
||||||
|
self.seq.reset();
|
||||||
|
self.seq.add_key(space);
|
||||||
|
return InputOutcome::Pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-key mapping: try general binds first (arrows, open_search, enter_command_mode)
|
||||||
|
if let Some(action_str) =
|
||||||
|
config.get_general_action(key_event.code, key_event.modifiers)
|
||||||
|
{
|
||||||
|
if let Some(app_action) = map_action_string(action_str, ctx) {
|
||||||
|
return InputOutcome::Action(app_action);
|
||||||
|
}
|
||||||
|
// Unknown to app layer (likely canvas movement etc.) → pass
|
||||||
|
return InputOutcome::PassThrough;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then app-level common/read-only/edit/highlight for UI toggles or core actions
|
||||||
|
if let Some(action_str) = config.get_app_action(key_event.code, key_event.modifiers) {
|
||||||
|
if let Some(app_action) = map_action_string(action_str, ctx) {
|
||||||
|
return InputOutcome::Action(app_action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InputOutcome::PassThrough
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a key sequence is currently active
|
||||||
|
pub fn has_active_sequence(&self) -> bool {
|
||||||
|
!self.seq.current_sequence.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn str_to_movement(s: &str) -> Option<MovementAction> {
|
||||||
|
match s {
|
||||||
|
"up" => Some(MovementAction::Up),
|
||||||
|
"down" => Some(MovementAction::Down),
|
||||||
|
"left" => Some(MovementAction::Left),
|
||||||
|
"right" => Some(MovementAction::Right),
|
||||||
|
"next" => Some(MovementAction::Next),
|
||||||
|
"previous" => Some(MovementAction::Previous),
|
||||||
|
"select" => Some(MovementAction::Select),
|
||||||
|
"esc" => Some(MovementAction::Esc),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_action_string(action: &str, ctx: &InputContext) -> Option<AppAction> {
|
||||||
|
match action {
|
||||||
|
// Global/UI
|
||||||
|
"toggle_sidebar" => Some(AppAction::ToggleSidebar),
|
||||||
|
"toggle_buffer_list" => Some(AppAction::ToggleBufferList),
|
||||||
|
"open_search" => Some(AppAction::OpenSearch),
|
||||||
|
"find_file_palette_toggle" => Some(AppAction::FindFilePaletteToggle),
|
||||||
|
|
||||||
|
// Buffers
|
||||||
|
"next_buffer" => Some(AppAction::Buffer(BufferAction::Next)),
|
||||||
|
"previous_buffer" => Some(AppAction::Buffer(BufferAction::Previous)),
|
||||||
|
"close_buffer" => Some(AppAction::Buffer(BufferAction::Close)),
|
||||||
|
|
||||||
|
// Command mode
|
||||||
|
"enter_command_mode" => Some(AppAction::EnterCommandMode),
|
||||||
|
"exit_command_mode" => Some(AppAction::ExitCommandMode),
|
||||||
|
"command_execute" => Some(AppAction::CommandExecute),
|
||||||
|
"command_backspace" => Some(AppAction::CommandBackspace),
|
||||||
|
|
||||||
|
// Navigation across UI (only if allowed)
|
||||||
|
s if str_to_movement(s).is_some() && ctx.allow_navigation_capture => {
|
||||||
|
Some(AppAction::Navigate(str_to_movement(s).unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core actions
|
||||||
|
"save" => Some(AppAction::Core(CoreAction::Save)),
|
||||||
|
"force_quit" => Some(AppAction::Core(CoreAction::ForceQuit)),
|
||||||
|
"save_and_quit" => Some(AppAction::Core(CoreAction::SaveAndQuit)),
|
||||||
|
"revert" => Some(AppAction::Core(CoreAction::Revert)),
|
||||||
|
|
||||||
|
// Unknown to app layer: ignore (canvas-specific actions, etc.)
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
3
client/src/input/mod.rs
Normal file
3
client/src/input/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// src/input/mod.rs
|
||||||
|
pub mod action;
|
||||||
|
pub mod engine;
|
||||||
@@ -14,6 +14,7 @@ pub mod search;
|
|||||||
pub mod bottom_panel;
|
pub mod bottom_panel;
|
||||||
pub mod pages;
|
pub mod pages;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
|
pub mod input;
|
||||||
|
|
||||||
pub use ui::run_ui;
|
pub use ui::run_ui;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// src/modes/handlers/event.rs
|
// src/modes/handlers/event.rs
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
use crate::input::engine::{InputContext, InputEngine, InputOutcome};
|
||||||
|
use crate::input::action::{AppAction, BufferAction, CoreAction};
|
||||||
use crate::buffer::{AppView, BufferState, switch_buffer, toggle_buffer_list};
|
use crate::buffer::{AppView, BufferState, switch_buffer, toggle_buffer_list};
|
||||||
use crate::sidebar::toggle_sidebar;
|
use crate::sidebar::toggle_sidebar;
|
||||||
use crate::search::event::handle_search_palette_event;
|
use crate::search::event::handle_search_palette_event;
|
||||||
@@ -76,7 +77,7 @@ pub struct EventHandler {
|
|||||||
pub command_message: String,
|
pub command_message: String,
|
||||||
pub edit_mode_cooldown: bool,
|
pub edit_mode_cooldown: bool,
|
||||||
pub ideal_cursor_column: usize,
|
pub ideal_cursor_column: usize,
|
||||||
pub key_sequence_tracker: KeySequenceTracker,
|
pub input_engine: InputEngine,
|
||||||
pub auth_client: AuthClient,
|
pub auth_client: AuthClient,
|
||||||
pub grpc_client: GrpcClient,
|
pub grpc_client: GrpcClient,
|
||||||
pub login_result_sender: mpsc::Sender<LoginResult>,
|
pub login_result_sender: mpsc::Sender<LoginResult>,
|
||||||
@@ -106,7 +107,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),
|
input_engine: InputEngine::new(1200),
|
||||||
auth_client: AuthClient::new().await?,
|
auth_client: AuthClient::new().await?,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
login_result_sender,
|
login_result_sender,
|
||||||
@@ -297,20 +298,81 @@ 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;
|
||||||
|
|
||||||
|
// Determine if canvas is in edit mode (we avoid capturing navigation then)
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Centralized key -> action resolution
|
||||||
|
let input_ctx = InputContext {
|
||||||
|
app_mode: current_mode,
|
||||||
|
overlay_active,
|
||||||
|
allow_navigation_capture: !in_form_edit_mode && !overlay_active,
|
||||||
|
};
|
||||||
|
match self.input_engine.process_key(key_event, &input_ctx, config) {
|
||||||
|
InputOutcome::Action(action) => {
|
||||||
|
if let Some(outcome) = self
|
||||||
|
.handle_app_action(
|
||||||
|
action,
|
||||||
|
key_event, // pass original key
|
||||||
|
config,
|
||||||
|
terminal,
|
||||||
|
command_handler,
|
||||||
|
auth_state,
|
||||||
|
buffer_state,
|
||||||
|
app_state,
|
||||||
|
router,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Ok(outcome);
|
||||||
|
}
|
||||||
|
// No early return on None (e.g., Navigate) — fall through
|
||||||
|
}
|
||||||
|
InputOutcome::Pending => {
|
||||||
|
// waiting for more keys in a sequence
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
InputOutcome::PassThrough => {
|
||||||
|
// fall through to page/canvas handlers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 =
|
||||||
login::event::handle_login_event(event, app_state, login_page)?;
|
login::event::handle_login_event(event.clone(), app_state, login_page)?;
|
||||||
// Only return if the login page actually consumed the key
|
// Only return if the login 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);
|
||||||
}
|
}
|
||||||
|
// Allow core actions via space-sequence even on Login page
|
||||||
|
if let Event::Key(k) = &event {
|
||||||
|
if let Some(sequence_action) = config.matches_key_sequence_generalized(&[k.code]) {
|
||||||
|
if matches!(sequence_action, "revert" | "save" | "force_quit" | "save_and_quit") {
|
||||||
|
let outcome = self.handle_core_action(
|
||||||
|
sequence_action,
|
||||||
|
auth_state,
|
||||||
|
terminal,
|
||||||
|
app_state,
|
||||||
|
router,
|
||||||
|
).await?;
|
||||||
|
return Ok(outcome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if let Page::Register(register_page) = &mut router.current {
|
} else if let Page::Register(register_page) = &mut router.current {
|
||||||
let outcome = crate::pages::register::event::handle_register_event(
|
let outcome = crate::pages::register::event::handle_register_event(
|
||||||
event,
|
event,
|
||||||
@@ -322,16 +384,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.input_engine.has_active_sequence() {
|
||||||
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) =
|
||||||
@@ -345,7 +412,7 @@ impl EventHandler {
|
|||||||
self.command_mode = true;
|
self.command_mode = true;
|
||||||
self.command_input.clear();
|
self.command_input.clear();
|
||||||
self.command_message.clear();
|
self.command_message.clear();
|
||||||
self.key_sequence_tracker.reset();
|
self.input_engine.reset_sequence();
|
||||||
self.set_focus_outside(router, true);
|
self.set_focus_outside(router, true);
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
@@ -391,7 +458,7 @@ impl EventHandler {
|
|||||||
self.command_mode = true;
|
self.command_mode = true;
|
||||||
self.command_input.clear();
|
self.command_input.clear();
|
||||||
self.command_message.clear();
|
self.command_message.clear();
|
||||||
self.key_sequence_tracker.reset();
|
self.input_engine.reset_sequence();
|
||||||
self.set_focus_outside(router, true);
|
self.set_focus_outside(router, true);
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
@@ -447,106 +514,11 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if toggle_sidebar(
|
|
||||||
&mut app_state.ui,
|
// Sidebar/buffer toggles now handled via AppAction in the engine
|
||||||
config,
|
|
||||||
key_code,
|
|
||||||
modifiers,
|
|
||||||
) {
|
|
||||||
let message = format!(
|
|
||||||
"Sidebar {}",
|
|
||||||
if app_state.ui.show_sidebar {
|
|
||||||
"shown"
|
|
||||||
} else {
|
|
||||||
"hidden"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return Ok(EventOutcome::Ok(message));
|
|
||||||
}
|
|
||||||
if toggle_buffer_list(
|
|
||||||
&mut app_state.ui,
|
|
||||||
config,
|
|
||||||
key_code,
|
|
||||||
modifiers,
|
|
||||||
) {
|
|
||||||
let message = format!(
|
|
||||||
"Buffer {}",
|
|
||||||
if app_state.ui.show_buffer_list {
|
|
||||||
"shown"
|
|
||||||
} else {
|
|
||||||
"hidden"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return Ok(EventOutcome::Ok(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_mode == AppMode::General {
|
if current_mode == AppMode::General {
|
||||||
if let Some(action) = config.get_action_for_key_in_mode(
|
// General mode specific key mapping now handled via AppAction
|
||||||
&config.keybindings.global,
|
|
||||||
key_code,
|
|
||||||
modifiers,
|
|
||||||
) {
|
|
||||||
match action {
|
|
||||||
"next_buffer" => {
|
|
||||||
if switch_buffer(buffer_state, true) {
|
|
||||||
return Ok(EventOutcome::Ok(
|
|
||||||
"Switched to next buffer".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"previous_buffer" => {
|
|
||||||
if switch_buffer(buffer_state, false) {
|
|
||||||
return Ok(EventOutcome::Ok(
|
|
||||||
"Switched to previous buffer".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"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,
|
|
||||||
);
|
|
||||||
return Ok(EventOutcome::Ok(message));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(action) = config.get_general_action(key_code, modifiers) {
|
|
||||||
if action == "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(EventOutcome::Ok(
|
|
||||||
"Search palette opened".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Allow ":" / ctrl+; to enter command mode only when outside canvas.
|
|
||||||
if action == "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();
|
|
||||||
// Keep focus outside so canvas won't receive keys
|
|
||||||
self.set_focus_outside(router, true);
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match current_mode {
|
match current_mode {
|
||||||
@@ -678,146 +650,20 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppMode::Command => {
|
AppMode::Command => {
|
||||||
if config.is_exit_command_mode(key_code, modifiers) {
|
// Command-mode keys already handled by the engine.
|
||||||
self.command_input.clear();
|
// Collect characters not handled (typed command input).
|
||||||
self.command_message.clear();
|
match key_code {
|
||||||
self.command_mode = false;
|
KeyCode::Char(c) => {
|
||||||
self.key_sequence_tracker.reset();
|
|
||||||
if let Page::Form(path) = &router.current {
|
|
||||||
if let Some(editor) = app_state.editor_for_path(path) {
|
|
||||||
editor.set_mode(CanvasMode::ReadOnly);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(EventOutcome::Ok(
|
|
||||||
"Exited command mode".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.is_command_execute(key_code, modifiers) {
|
|
||||||
let (mut current_position, total_count) =
|
|
||||||
if let Page::Form(path) = &router.current {
|
|
||||||
if let Some(fs) =
|
|
||||||
app_state.form_state_for_path_ref(path)
|
|
||||||
{
|
|
||||||
(fs.current_position, fs.total_count)
|
|
||||||
} else {
|
|
||||||
(1, 0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(1, 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
let outcome = command_mode::handle_command_event(
|
|
||||||
key_event,
|
|
||||||
config,
|
|
||||||
app_state,
|
|
||||||
router,
|
|
||||||
&mut self.command_input,
|
|
||||||
&mut self.command_message,
|
|
||||||
&mut self.grpc_client,
|
|
||||||
command_handler,
|
|
||||||
terminal,
|
|
||||||
&mut current_position,
|
|
||||||
total_count,
|
|
||||||
).await?;
|
|
||||||
if let Page::Form(path) = &router.current {
|
|
||||||
if let Some(fs) = app_state.form_state_for_path(path) {
|
|
||||||
fs.current_position = current_position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.command_mode = false;
|
|
||||||
self.key_sequence_tracker.reset();
|
|
||||||
let new_mode = ModeManager::derive_mode(
|
|
||||||
app_state,
|
|
||||||
self,
|
|
||||||
router,
|
|
||||||
);
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(outcome);
|
|
||||||
}
|
|
||||||
|
|
||||||
if key_code == KeyCode::Backspace {
|
|
||||||
self.command_input.pop();
|
|
||||||
self.key_sequence_tracker.reset();
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
|
|
||||||
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()));
|
||||||
}
|
}
|
||||||
|
_ => {
|
||||||
self.key_sequence_tracker.reset();
|
self.input_engine.reset_sequence();
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if let Event::Resize(_, _) = event {
|
} else if let Event::Resize(_, _) = event {
|
||||||
return Ok(EventOutcome::Ok("Resized".to_string()));
|
return Ok(EventOutcome::Ok("Resized".to_string()));
|
||||||
}
|
}
|
||||||
@@ -1003,4 +849,227 @@ impl EventHandler {
|
|||||||
_ => 0,
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn execute_command(
|
||||||
|
&mut self,
|
||||||
|
key_event: crossterm::event::KeyEvent,
|
||||||
|
config: &Config,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
|
command_handler: &mut CommandHandler,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
router: &mut Router,
|
||||||
|
) -> Result<EventOutcome> {
|
||||||
|
let (mut current_position, total_count) = if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(fs) = app_state.form_state_for_path_ref(path) {
|
||||||
|
(fs.current_position, fs.total_count)
|
||||||
|
} else {
|
||||||
|
(1, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(1, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let outcome = command_mode::handle_command_event(
|
||||||
|
key_event,
|
||||||
|
config,
|
||||||
|
app_state,
|
||||||
|
router,
|
||||||
|
&mut self.command_input,
|
||||||
|
&mut self.command_message,
|
||||||
|
&mut self.grpc_client,
|
||||||
|
command_handler,
|
||||||
|
terminal,
|
||||||
|
&mut current_position,
|
||||||
|
total_count,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(fs) = app_state.form_state_for_path(path) {
|
||||||
|
fs.current_position = current_position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.command_mode = false;
|
||||||
|
self.input_engine.reset_sequence();
|
||||||
|
let new_mode = ModeManager::derive_mode(app_state, self, router);
|
||||||
|
app_state.update_mode(new_mode);
|
||||||
|
Ok(outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn handle_app_action(
|
||||||
|
&mut self,
|
||||||
|
action: AppAction,
|
||||||
|
key_event: crossterm::event::KeyEvent,
|
||||||
|
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 {
|
||||||
|
AppAction::ToggleSidebar => {
|
||||||
|
app_state.ui.show_sidebar = !app_state.ui.show_sidebar;
|
||||||
|
let message = format!(
|
||||||
|
"Sidebar {}",
|
||||||
|
if app_state.ui.show_sidebar {
|
||||||
|
"shown"
|
||||||
|
} else {
|
||||||
|
"hidden"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(Some(EventOutcome::Ok(message)))
|
||||||
|
}
|
||||||
|
AppAction::ToggleBufferList => {
|
||||||
|
app_state.ui.show_buffer_list = !app_state.ui.show_buffer_list;
|
||||||
|
let message = format!(
|
||||||
|
"Buffer {}",
|
||||||
|
if app_state.ui.show_buffer_list {
|
||||||
|
"shown"
|
||||||
|
} else {
|
||||||
|
"hidden"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(Some(EventOutcome::Ok(message)))
|
||||||
|
}
|
||||||
|
AppAction::Buffer(BufferAction::Next) => {
|
||||||
|
if switch_buffer(buffer_state, true) {
|
||||||
|
return Ok(Some(EventOutcome::Ok(
|
||||||
|
"Switched to next buffer".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(Some(EventOutcome::Ok(String::new())))
|
||||||
|
}
|
||||||
|
AppAction::Buffer(BufferAction::Previous) => {
|
||||||
|
if switch_buffer(buffer_state, false) {
|
||||||
|
return Ok(Some(EventOutcome::Ok(
|
||||||
|
"Switched to previous buffer".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(Some(EventOutcome::Ok(String::new())))
|
||||||
|
}
|
||||||
|
AppAction::Buffer(BufferAction::Close) => {
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
AppAction::OpenSearch => {
|
||||||
|
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())))
|
||||||
|
}
|
||||||
|
AppAction::FindFilePaletteToggle => {
|
||||||
|
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.input_engine.reset_sequence();
|
||||||
|
return Ok(Some(EventOutcome::Ok(
|
||||||
|
"Table selection palette activated".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(Some(EventOutcome::Ok(String::new())))
|
||||||
|
}
|
||||||
|
AppAction::EnterCommandMode => {
|
||||||
|
if !self.is_in_form_edit_mode(router, app_state)
|
||||||
|
&& !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.input_engine.reset_sequence();
|
||||||
|
|
||||||
|
// Keep focus outside so canvas won’t consume keystrokes
|
||||||
|
self.set_focus_outside(router, true);
|
||||||
|
}
|
||||||
|
Ok(Some(EventOutcome::Ok(String::new())))
|
||||||
|
}
|
||||||
|
AppAction::ExitCommandMode => {
|
||||||
|
self.command_input.clear();
|
||||||
|
self.command_message.clear();
|
||||||
|
self.command_mode = false;
|
||||||
|
self.input_engine.reset_sequence();
|
||||||
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(editor) = app_state.editor_for_path(path) {
|
||||||
|
editor.set_mode(CanvasMode::ReadOnly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(EventOutcome::Ok(
|
||||||
|
"Exited command mode".to_string(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
AppAction::CommandExecute => {
|
||||||
|
// Execute using the actual configured key that triggered the action
|
||||||
|
let out = self
|
||||||
|
.execute_command(
|
||||||
|
key_event,
|
||||||
|
config,
|
||||||
|
terminal,
|
||||||
|
command_handler,
|
||||||
|
app_state,
|
||||||
|
router,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Some(out))
|
||||||
|
}
|
||||||
|
AppAction::CommandBackspace => {
|
||||||
|
self.command_input.pop();
|
||||||
|
self.input_engine.reset_sequence();
|
||||||
|
Ok(Some(EventOutcome::Ok(String::new())))
|
||||||
|
}
|
||||||
|
AppAction::Core(core) => {
|
||||||
|
let s = match core {
|
||||||
|
CoreAction::Save => "save",
|
||||||
|
CoreAction::ForceQuit => "force_quit",
|
||||||
|
CoreAction::SaveAndQuit => "save_and_quit",
|
||||||
|
CoreAction::Revert => "revert",
|
||||||
|
};
|
||||||
|
let out = self
|
||||||
|
.handle_core_action(s, auth_state, terminal, app_state, router)
|
||||||
|
.await?;
|
||||||
|
Ok(Some(out))
|
||||||
|
}
|
||||||
|
AppAction::Navigate(_ma) => {
|
||||||
|
// Movement is still handled by page/nav code paths that
|
||||||
|
// follow after PassThrough. We return None here to keep flow.
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_in_form_edit_mode(&self, router: &Router, app_state: &AppState) -> bool {
|
||||||
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(editor) = app_state.editor_for_path_ref(path) {
|
||||||
|
return editor.mode() == CanvasMode::Edit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::ui::handlers::context::DialogPurpose;
|
|||||||
use common::proto::komp_ac::auth::LoginResponse;
|
use common::proto::komp_ac::auth::LoginResponse;
|
||||||
use crate::pages::login::LoginFormState;
|
use crate::pages::login::LoginFormState;
|
||||||
use crate::state::pages::auth::UserRole;
|
use crate::state::pages::auth::UserRole;
|
||||||
|
use canvas::DataProvider;
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
@@ -108,7 +109,19 @@ pub async fn revert(
|
|||||||
login_state: &mut LoginFormState,
|
login_state: &mut LoginFormState,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
) -> String {
|
) -> String {
|
||||||
|
// Clear the underlying state
|
||||||
login_state.clear();
|
login_state.clear();
|
||||||
|
|
||||||
|
// Also clear values inside the editor’s data provider
|
||||||
|
{
|
||||||
|
let dp = login_state.editor.data_provider_mut();
|
||||||
|
dp.set_field_value(0, "".to_string());
|
||||||
|
dp.set_field_value(1, "".to_string());
|
||||||
|
dp.set_current_field(0);
|
||||||
|
dp.set_current_cursor_pos(0);
|
||||||
|
dp.set_has_unsaved_changes(false);
|
||||||
|
}
|
||||||
|
|
||||||
app_state.hide_dialog();
|
app_state.hide_dialog();
|
||||||
"Login reverted".to_string()
|
"Login reverted".to_string()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user