feat(keymap): add configurable leader and Emacs enhancements
This commit is contained in:
@@ -17,7 +17,7 @@ use std::time::Duration;
|
|||||||
pub const DEFAULT_CONFIG_PATH: &str = "~/.config/owlen/config.toml";
|
pub const DEFAULT_CONFIG_PATH: &str = "~/.config/owlen/config.toml";
|
||||||
|
|
||||||
/// Current schema version written to `config.toml`.
|
/// Current schema version written to `config.toml`.
|
||||||
pub const CONFIG_SCHEMA_VERSION: &str = "1.7.0";
|
pub const CONFIG_SCHEMA_VERSION: &str = "1.8.0";
|
||||||
|
|
||||||
/// Provider config key for forcing Ollama provider mode.
|
/// Provider config key for forcing Ollama provider mode.
|
||||||
pub const OLLAMA_MODE_KEY: &str = "ollama_mode";
|
pub const OLLAMA_MODE_KEY: &str = "ollama_mode";
|
||||||
@@ -1812,6 +1812,8 @@ pub struct UiSettings {
|
|||||||
pub icon_mode: IconMode,
|
pub icon_mode: IconMode,
|
||||||
#[serde(default = "UiSettings::default_keymap_profile")]
|
#[serde(default = "UiSettings::default_keymap_profile")]
|
||||||
pub keymap_profile: Option<String>,
|
pub keymap_profile: Option<String>,
|
||||||
|
#[serde(default = "UiSettings::default_keymap_leader")]
|
||||||
|
pub keymap_leader: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub keymap_path: Option<String>,
|
pub keymap_path: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -1915,6 +1917,10 @@ impl UiSettings {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_keymap_leader() -> String {
|
||||||
|
"Space".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
fn deserialize_role_label_mode<'de, D>(
|
fn deserialize_role_label_mode<'de, D>(
|
||||||
deserializer: D,
|
deserializer: D,
|
||||||
) -> std::result::Result<RoleLabelDisplay, D::Error>
|
) -> std::result::Result<RoleLabelDisplay, D::Error>
|
||||||
@@ -1985,6 +1991,7 @@ impl Default for UiSettings {
|
|||||||
show_timestamps: Self::default_show_timestamps(),
|
show_timestamps: Self::default_show_timestamps(),
|
||||||
icon_mode: Self::default_icon_mode(),
|
icon_mode: Self::default_icon_mode(),
|
||||||
keymap_profile: Self::default_keymap_profile(),
|
keymap_profile: Self::default_keymap_profile(),
|
||||||
|
keymap_leader: Self::default_keymap_leader(),
|
||||||
keymap_path: None,
|
keymap_path: None,
|
||||||
accessibility: AccessibilitySettings::default(),
|
accessibility: AccessibilitySettings::default(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,47 @@
|
|||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["m"]
|
|
||||||
command = "model.open_all"
|
command = "model.open_all"
|
||||||
|
sequences = [
|
||||||
|
["m"],
|
||||||
|
["<leader>", "m"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+Shift+L"]
|
|
||||||
command = "model.open_local"
|
command = "model.open_local"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+Shift+L"],
|
||||||
|
["<leader>", "m", "l"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+Shift+C"]
|
|
||||||
command = "model.open_cloud"
|
command = "model.open_cloud"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+Shift+C"],
|
||||||
|
["<leader>", "m", "c"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+Shift+P"]
|
|
||||||
command = "model.open_available"
|
command = "model.open_available"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+Shift+P"],
|
||||||
|
["<leader>", "m", "a"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal", "editing"]
|
modes = ["normal", "editing"]
|
||||||
sequence = ["Ctrl+P"]
|
|
||||||
command = "palette.open"
|
command = "palette.open"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+P"],
|
||||||
|
["<leader>", "t"]
|
||||||
|
]
|
||||||
|
|
||||||
|
[[binding]]
|
||||||
|
modes = ["normal"]
|
||||||
|
command = "provider.switch"
|
||||||
|
sequence = ["<leader>", "p"]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
@@ -35,38 +55,56 @@ command = "focus.prev"
|
|||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+1"]
|
|
||||||
command = "focus.files"
|
command = "focus.files"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+1"],
|
||||||
|
["<leader>", "f", "1"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+2"]
|
|
||||||
command = "focus.chat"
|
command = "focus.chat"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+2"],
|
||||||
|
["<leader>", "f", "2"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+3"]
|
|
||||||
command = "focus.code"
|
command = "focus.code"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+3"],
|
||||||
|
["<leader>", "f", "3"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+4"]
|
|
||||||
command = "focus.thinking"
|
command = "focus.thinking"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+4"],
|
||||||
|
["<leader>", "f", "4"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+5"]
|
|
||||||
command = "focus.input"
|
command = "focus.input"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+5"],
|
||||||
|
["<leader>", "f", "5"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["editing"]
|
modes = ["editing"]
|
||||||
sequence = ["Enter"]
|
|
||||||
command = "composer.submit"
|
command = "composer.submit"
|
||||||
|
sequence = ["Enter"]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+;"]
|
|
||||||
command = "mode.command"
|
command = "mode.command"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+;"],
|
||||||
|
["<leader>", ":"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal", "editing", "visual", "command", "help"]
|
modes = ["normal", "editing", "visual", "command", "help"]
|
||||||
@@ -75,78 +113,98 @@ command = "debug.toggle"
|
|||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["g", "g"]
|
|
||||||
command = "navigate.top"
|
command = "navigate.top"
|
||||||
|
sequences = [
|
||||||
|
["g", "g"],
|
||||||
|
["<leader>", "g", "t"]
|
||||||
|
]
|
||||||
|
|
||||||
|
[[binding]]
|
||||||
|
modes = ["normal"]
|
||||||
|
command = "navigate.bottom"
|
||||||
|
sequences = [
|
||||||
|
["Shift+G"],
|
||||||
|
["<leader>", "g", "b"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["g", "t"]
|
|
||||||
command = "files.focus_expand"
|
command = "files.focus_expand"
|
||||||
|
sequences = [
|
||||||
|
["g", "t"],
|
||||||
|
["g", "T"],
|
||||||
|
["<leader>", "f", "e"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["g", "T"]
|
|
||||||
command = "files.focus_expand"
|
|
||||||
|
|
||||||
[[binding]]
|
|
||||||
modes = ["normal"]
|
|
||||||
sequence = ["g", "h"]
|
|
||||||
command = "files.toggle_hidden"
|
command = "files.toggle_hidden"
|
||||||
|
sequences = [
|
||||||
|
["g", "h"],
|
||||||
|
["g", "H"],
|
||||||
|
["<leader>", "f", "h"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["g", "H"]
|
|
||||||
command = "files.toggle_hidden"
|
|
||||||
|
|
||||||
[[binding]]
|
|
||||||
modes = ["normal"]
|
|
||||||
sequence = ["d", "d"]
|
|
||||||
command = "input.clear"
|
command = "input.clear"
|
||||||
|
sequences = [
|
||||||
|
["d", "d"],
|
||||||
|
["<leader>", "m", "d"]
|
||||||
|
]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+W", "s"]
|
|
||||||
command = "workspace.split_horizontal"
|
command = "workspace.split_horizontal"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+W", "s"],
|
||||||
|
["Ctrl+W", "S"],
|
||||||
|
["<leader>", "l", "s"]
|
||||||
|
]
|
||||||
timeout_ms = 1200
|
timeout_ms = 1200
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+W", "S"]
|
|
||||||
command = "workspace.split_horizontal"
|
|
||||||
timeout_ms = 1200
|
|
||||||
|
|
||||||
[[binding]]
|
|
||||||
modes = ["normal"]
|
|
||||||
sequence = ["Ctrl+W", "v"]
|
|
||||||
command = "workspace.split_vertical"
|
command = "workspace.split_vertical"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+W", "v"],
|
||||||
|
["Ctrl+W", "V"],
|
||||||
|
["<leader>", "l", "v"]
|
||||||
|
]
|
||||||
timeout_ms = 1200
|
timeout_ms = 1200
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+W", "V"]
|
|
||||||
command = "workspace.split_vertical"
|
|
||||||
timeout_ms = 1200
|
|
||||||
|
|
||||||
[[binding]]
|
|
||||||
modes = ["normal"]
|
|
||||||
sequence = ["Ctrl+K", "Left"]
|
|
||||||
command = "workspace.focus_left"
|
command = "workspace.focus_left"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+K", "Left"],
|
||||||
|
["<leader>", "l", "h"]
|
||||||
|
]
|
||||||
timeout_ms = 1200
|
timeout_ms = 1200
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+K", "Right"]
|
|
||||||
command = "workspace.focus_right"
|
command = "workspace.focus_right"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+K", "Right"],
|
||||||
|
["<leader>", "l", "l"]
|
||||||
|
]
|
||||||
timeout_ms = 1200
|
timeout_ms = 1200
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+K", "Up"]
|
|
||||||
command = "workspace.focus_up"
|
command = "workspace.focus_up"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+K", "Up"],
|
||||||
|
["<leader>", "l", "k"]
|
||||||
|
]
|
||||||
timeout_ms = 1200
|
timeout_ms = 1200
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
modes = ["normal"]
|
modes = ["normal"]
|
||||||
sequence = ["Ctrl+K", "Down"]
|
|
||||||
command = "workspace.focus_down"
|
command = "workspace.focus_down"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+K", "Down"],
|
||||||
|
["<leader>", "l", "j"]
|
||||||
|
]
|
||||||
timeout_ms = 1200
|
timeout_ms = 1200
|
||||||
|
|||||||
@@ -1,79 +1,159 @@
|
|||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "normal"
|
mode = "normal"
|
||||||
keys = ["Alt+M"]
|
|
||||||
command = "model.open_all"
|
command = "model.open_all"
|
||||||
|
sequences = [
|
||||||
|
["Alt+M"],
|
||||||
|
["Ctrl+X", "m"]
|
||||||
|
]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "normal"
|
mode = "normal"
|
||||||
keys = ["Ctrl+Alt+L"]
|
|
||||||
command = "model.open_local"
|
command = "model.open_local"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+Alt+L"],
|
||||||
|
["Ctrl+X", "l"]
|
||||||
|
]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "normal"
|
mode = "normal"
|
||||||
keys = ["Ctrl+Alt+C"]
|
|
||||||
command = "model.open_cloud"
|
command = "model.open_cloud"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+Alt+C"],
|
||||||
|
["Ctrl+X", "c"]
|
||||||
|
]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "normal"
|
mode = "normal"
|
||||||
keys = ["Ctrl+Alt+A"]
|
|
||||||
command = "model.open_available"
|
command = "model.open_available"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+Alt+A"],
|
||||||
|
["Ctrl+X", "a"]
|
||||||
|
]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "normal"
|
modes = ["normal", "editing"]
|
||||||
keys = ["Alt+x"]
|
sequence = ["Ctrl+Space"]
|
||||||
command = "mode.command"
|
|
||||||
|
|
||||||
[[binding]]
|
|
||||||
mode = "editing"
|
|
||||||
keys = ["Alt+x"]
|
|
||||||
command = "mode.command"
|
|
||||||
|
|
||||||
[[binding]]
|
|
||||||
mode = "normal"
|
|
||||||
keys = ["Ctrl+Space"]
|
|
||||||
command = "palette.open"
|
command = "palette.open"
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "normal"
|
mode = "normal"
|
||||||
keys = ["Alt+O"]
|
command = "provider.switch"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+X", "Ctrl+P"]
|
||||||
|
]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|
||||||
|
[[binding]]
|
||||||
|
mode = "normal"
|
||||||
|
sequence = ["Alt+O"]
|
||||||
command = "focus.next"
|
command = "focus.next"
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "normal"
|
mode = "normal"
|
||||||
keys = ["Alt+Shift+O"]
|
sequence = ["Alt+Shift+O"]
|
||||||
command = "focus.prev"
|
command = "focus.prev"
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "normal"
|
mode = "normal"
|
||||||
keys = ["Alt+1"]
|
|
||||||
command = "focus.files"
|
command = "focus.files"
|
||||||
|
sequence = ["Alt+1"]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "normal"
|
mode = "normal"
|
||||||
keys = ["Alt+2"]
|
|
||||||
command = "focus.chat"
|
command = "focus.chat"
|
||||||
|
sequence = ["Alt+2"]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "normal"
|
mode = "normal"
|
||||||
keys = ["Alt+3"]
|
|
||||||
command = "focus.code"
|
command = "focus.code"
|
||||||
|
sequence = ["Alt+3"]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "normal"
|
mode = "normal"
|
||||||
keys = ["Alt+4"]
|
|
||||||
command = "focus.thinking"
|
command = "focus.thinking"
|
||||||
|
sequence = ["Alt+4"]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "normal"
|
mode = "normal"
|
||||||
keys = ["Alt+5"]
|
|
||||||
command = "focus.input"
|
command = "focus.input"
|
||||||
|
sequence = ["Alt+5"]
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "editing"
|
mode = "editing"
|
||||||
keys = ["Ctrl+Enter"]
|
|
||||||
command = "composer.submit"
|
command = "composer.submit"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+Enter"],
|
||||||
|
["Ctrl+X", "Ctrl+S"]
|
||||||
|
]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|
||||||
[[binding]]
|
[[binding]]
|
||||||
mode = "normal"
|
mode = "normal"
|
||||||
keys = ["Ctrl+Alt+D"]
|
command = "mode.command"
|
||||||
|
sequence = ["Alt+x"]
|
||||||
|
|
||||||
|
[[binding]]
|
||||||
|
modes = ["normal", "editing", "visual", "command", "help"]
|
||||||
|
sequence = ["F12"]
|
||||||
command = "debug.toggle"
|
command = "debug.toggle"
|
||||||
|
|
||||||
|
[[binding]]
|
||||||
|
mode = "normal"
|
||||||
|
command = "files.focus_expand"
|
||||||
|
sequences = [
|
||||||
|
["Ctrl+X", "Ctrl+F"]
|
||||||
|
]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|
||||||
|
[[binding]]
|
||||||
|
mode = "normal"
|
||||||
|
command = "input.clear"
|
||||||
|
sequences = [
|
||||||
|
["Alt+Backspace"],
|
||||||
|
["Ctrl+X", "k"]
|
||||||
|
]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|
||||||
|
[[binding]]
|
||||||
|
mode = "normal"
|
||||||
|
command = "workspace.split_horizontal"
|
||||||
|
sequence = ["Ctrl+X", "2"]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|
||||||
|
[[binding]]
|
||||||
|
mode = "normal"
|
||||||
|
command = "workspace.split_vertical"
|
||||||
|
sequence = ["Ctrl+X", "3"]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|
||||||
|
[[binding]]
|
||||||
|
mode = "normal"
|
||||||
|
command = "workspace.focus_right"
|
||||||
|
sequence = ["Ctrl+X", "o"]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|
||||||
|
[[binding]]
|
||||||
|
mode = "normal"
|
||||||
|
command = "workspace.focus_left"
|
||||||
|
sequence = ["Ctrl+X", "Shift+O"]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|
||||||
|
[[binding]]
|
||||||
|
mode = "normal"
|
||||||
|
command = "navigate.top"
|
||||||
|
sequences = [
|
||||||
|
["Alt+G", "g"]
|
||||||
|
]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|
||||||
|
[[binding]]
|
||||||
|
mode = "normal"
|
||||||
|
command = "navigate.bottom"
|
||||||
|
sequences = [
|
||||||
|
["Alt+G", "Shift+G"]
|
||||||
|
]
|
||||||
|
timeout_ms = 1200
|
||||||
|
|||||||
@@ -56,10 +56,10 @@ use crate::model_info_panel::ModelInfoPanel;
|
|||||||
use crate::slash::{self, McpSlashCommand, SlashCommand};
|
use crate::slash::{self, McpSlashCommand, SlashCommand};
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
CodeWorkspace, CommandPalette, DebugLogEntry, DebugLogState, FileFilterMode, FileIconResolver,
|
CodeWorkspace, CommandPalette, DebugLogEntry, DebugLogState, FileFilterMode, FileIconResolver,
|
||||||
FileNode, FileTreeState, Keymap, KeymapEventResult, KeymapProfile, KeymapState,
|
FileNode, FileTreeState, Keymap, KeymapEventResult, KeymapOverrides, KeymapProfile,
|
||||||
ModelPaletteEntry, PaletteSuggestion, PaneDirection, PaneRestoreRequest, RepoSearchMessage,
|
KeymapState, ModelPaletteEntry, PaletteSuggestion, PaneDirection, PaneRestoreRequest,
|
||||||
RepoSearchState, SplitAxis, SymbolSearchMessage, SymbolSearchState, WorkspaceSnapshot,
|
RepoSearchMessage, RepoSearchState, SplitAxis, SymbolSearchMessage, SymbolSearchState,
|
||||||
install_global_logger, spawn_repo_search_task, spawn_symbol_search_task,
|
WorkspaceSnapshot, install_global_logger, spawn_repo_search_task, spawn_symbol_search_task,
|
||||||
};
|
};
|
||||||
use crate::toast::{Toast, ToastLevel, ToastManager};
|
use crate::toast::{Toast, ToastLevel, ToastManager};
|
||||||
use crate::ui::{format_token_short, format_tool_output};
|
use crate::ui::{format_token_short, format_tool_output};
|
||||||
@@ -581,6 +581,7 @@ pub struct ChatApp {
|
|||||||
mvu_model: AppModel,
|
mvu_model: AppModel,
|
||||||
keymap: Keymap,
|
keymap: Keymap,
|
||||||
current_keymap_profile: KeymapProfile,
|
current_keymap_profile: KeymapProfile,
|
||||||
|
keymap_leader: String,
|
||||||
keymap_state: KeymapState,
|
keymap_state: KeymapState,
|
||||||
controller_event_rx: mpsc::UnboundedReceiver<ControllerEvent>,
|
controller_event_rx: mpsc::UnboundedReceiver<ControllerEvent>,
|
||||||
pending_llm_request: bool, // Flag to indicate LLM request needs to be processed
|
pending_llm_request: bool, // Flag to indicate LLM request needs to be processed
|
||||||
@@ -794,13 +795,21 @@ impl ChatApp {
|
|||||||
let icon_mode = config_guard.ui.icon_mode;
|
let icon_mode = config_guard.ui.icon_mode;
|
||||||
let keymap_path = config_guard.ui.keymap_path.clone();
|
let keymap_path = config_guard.ui.keymap_path.clone();
|
||||||
let keymap_profile = config_guard.ui.keymap_profile.clone();
|
let keymap_profile = config_guard.ui.keymap_profile.clone();
|
||||||
|
let keymap_leader_raw = config_guard.ui.keymap_leader.clone();
|
||||||
let accessibility = config_guard.ui.accessibility.clone();
|
let accessibility = config_guard.ui.accessibility.clone();
|
||||||
drop(config_guard);
|
drop(config_guard);
|
||||||
|
let keymap_overrides = KeymapOverrides::new(keymap_leader_raw);
|
||||||
let keymap = {
|
let keymap = {
|
||||||
let registry = CommandRegistry::default();
|
let registry = CommandRegistry::default();
|
||||||
Keymap::load(keymap_path.as_deref(), keymap_profile.as_deref(), ®istry)
|
Keymap::load(
|
||||||
|
keymap_path.as_deref(),
|
||||||
|
keymap_profile.as_deref(),
|
||||||
|
®istry,
|
||||||
|
keymap_overrides.clone(),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let current_keymap_profile = keymap.profile();
|
let current_keymap_profile = keymap.profile();
|
||||||
|
let keymap_leader = keymap_overrides.leader().to_string();
|
||||||
let base_theme_name = theme_name.clone();
|
let base_theme_name = theme_name.clone();
|
||||||
let mut theme = owlen_core::theme::get_theme(&base_theme_name).unwrap_or_else(|| {
|
let mut theme = owlen_core::theme::get_theme(&base_theme_name).unwrap_or_else(|| {
|
||||||
eprintln!("Warning: Theme '{}' not found, using default", theme_name);
|
eprintln!("Warning: Theme '{}' not found, using default", theme_name);
|
||||||
@@ -867,6 +876,7 @@ impl ChatApp {
|
|||||||
mvu_model: AppModel::default(),
|
mvu_model: AppModel::default(),
|
||||||
keymap,
|
keymap,
|
||||||
current_keymap_profile,
|
current_keymap_profile,
|
||||||
|
keymap_leader,
|
||||||
keymap_state: KeymapState::default(),
|
keymap_state: KeymapState::default(),
|
||||||
controller_event_rx,
|
controller_event_rx,
|
||||||
pending_llm_request: false,
|
pending_llm_request: false,
|
||||||
@@ -2145,15 +2155,27 @@ impl ChatApp {
|
|||||||
self.current_keymap_profile
|
self.current_keymap_profile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn keymap_leader(&self) -> &str {
|
||||||
|
&self.keymap_leader
|
||||||
|
}
|
||||||
|
|
||||||
fn reload_keymap_from_config(&mut self) -> Result<()> {
|
fn reload_keymap_from_config(&mut self) -> Result<()> {
|
||||||
let registry = CommandRegistry::default();
|
let registry = CommandRegistry::default();
|
||||||
let config = self.controller.config();
|
let config = self.controller.config();
|
||||||
let keymap_path = config.ui.keymap_path.clone();
|
let keymap_path = config.ui.keymap_path.clone();
|
||||||
let keymap_profile = config.ui.keymap_profile.clone();
|
let keymap_profile = config.ui.keymap_profile.clone();
|
||||||
|
let keymap_leader_raw = config.ui.keymap_leader.clone();
|
||||||
drop(config);
|
drop(config);
|
||||||
|
|
||||||
self.keymap = Keymap::load(keymap_path.as_deref(), keymap_profile.as_deref(), ®istry);
|
let overrides = KeymapOverrides::new(keymap_leader_raw);
|
||||||
|
self.keymap = Keymap::load(
|
||||||
|
keymap_path.as_deref(),
|
||||||
|
keymap_profile.as_deref(),
|
||||||
|
®istry,
|
||||||
|
overrides.clone(),
|
||||||
|
);
|
||||||
self.current_keymap_profile = self.keymap.profile();
|
self.current_keymap_profile = self.keymap.profile();
|
||||||
|
self.keymap_leader = overrides.leader().to_string();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3837,6 +3859,125 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_emacs_editing_key(&mut self, key: &KeyEvent) -> bool {
|
||||||
|
if !matches!(self.focused_panel, FocusedPanel::Input) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
|
||||||
|
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||||
|
let alt = key.modifiers.contains(KeyModifiers::ALT);
|
||||||
|
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char(ch) => match (ch.to_ascii_lowercase(), ctrl, alt) {
|
||||||
|
('y', true, false) => self.emacs_yank(),
|
||||||
|
('w', true, false) => self.emacs_kill_word_back(),
|
||||||
|
('w', false, true) => self.emacs_copy_word_back(),
|
||||||
|
('k', true, false) => self.emacs_kill_line(),
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emacs_yank(&mut self) -> bool {
|
||||||
|
if self.clipboard.is_empty() {
|
||||||
|
self.status = "Kill ring is empty".to_string();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.textarea.insert_str(&self.clipboard) {
|
||||||
|
self.sync_textarea_to_buffer();
|
||||||
|
self.status = format!("Yanked {} chars", self.clipboard.len());
|
||||||
|
self.error = None;
|
||||||
|
} else {
|
||||||
|
self.status = "Unable to insert clipboard contents".to_string();
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emacs_kill_word_back(&mut self) -> bool {
|
||||||
|
self.textarea.cancel_selection();
|
||||||
|
self.textarea.start_selection();
|
||||||
|
self.textarea.move_cursor(CursorMove::WordBack);
|
||||||
|
|
||||||
|
let killed = if self.textarea.cut() {
|
||||||
|
let grabbed = self.textarea.yank_text();
|
||||||
|
if grabbed.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(grabbed)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(grabbed) = killed {
|
||||||
|
self.clipboard = grabbed;
|
||||||
|
self.status = format!("Killed {} chars", self.clipboard.len());
|
||||||
|
self.error = None;
|
||||||
|
self.sync_textarea_to_buffer();
|
||||||
|
} else {
|
||||||
|
self.status = "Nothing to kill".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.textarea.cancel_selection();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emacs_copy_word_back(&mut self) -> bool {
|
||||||
|
self.textarea.cancel_selection();
|
||||||
|
self.textarea.start_selection();
|
||||||
|
self.textarea.move_cursor(CursorMove::WordBack);
|
||||||
|
self.textarea.copy();
|
||||||
|
let copied = self.textarea.yank_text();
|
||||||
|
self.textarea.cancel_selection();
|
||||||
|
self.textarea.move_cursor(CursorMove::WordForward);
|
||||||
|
|
||||||
|
if copied.is_empty() {
|
||||||
|
self.status = "Nothing to copy".to_string();
|
||||||
|
} else {
|
||||||
|
self.clipboard = copied;
|
||||||
|
self.status = format!("Copied {} chars", self.clipboard.len());
|
||||||
|
self.error = None;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emacs_kill_line(&mut self) -> bool {
|
||||||
|
self.textarea.cancel_selection();
|
||||||
|
let start_cursor = self.textarea.cursor();
|
||||||
|
self.textarea.start_selection();
|
||||||
|
self.textarea.move_cursor(CursorMove::End);
|
||||||
|
if self.textarea.cursor() == start_cursor {
|
||||||
|
self.textarea.move_cursor(CursorMove::Forward);
|
||||||
|
}
|
||||||
|
|
||||||
|
let killed = if self.textarea.cut() {
|
||||||
|
let grabbed = self.textarea.yank_text();
|
||||||
|
if grabbed.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(grabbed)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(grabbed) = killed {
|
||||||
|
self.clipboard = grabbed;
|
||||||
|
self.status = format!("Killed {} chars", self.clipboard.len());
|
||||||
|
self.error = None;
|
||||||
|
self.sync_textarea_to_buffer();
|
||||||
|
} else {
|
||||||
|
self.status = "Nothing to kill".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.textarea.cancel_selection();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
async fn execute_command(&mut self, command: AppCommand) -> Result<bool> {
|
async fn execute_command(&mut self, command: AppCommand) -> Result<bool> {
|
||||||
match command {
|
match command {
|
||||||
AppCommand::OpenModelPicker(filter) => {
|
AppCommand::OpenModelPicker(filter) => {
|
||||||
@@ -3862,6 +4003,18 @@ impl ChatApp {
|
|||||||
self.error = None;
|
self.error = None;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
AppCommand::OpenProviderSwitcher => {
|
||||||
|
if !matches!(self.mode, InputMode::Normal) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
if let Err(err) = self.show_model_picker(None).await {
|
||||||
|
self.error = Some(err.to_string());
|
||||||
|
} else if matches!(self.mode, InputMode::ProviderSelection) {
|
||||||
|
self.status = "Select a provider to activate".to_string();
|
||||||
|
self.error = None;
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
AppCommand::CycleFocusForward => {
|
AppCommand::CycleFocusForward => {
|
||||||
if !matches!(self.mode, InputMode::Normal) {
|
if !matches!(self.mode, InputMode::Normal) {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
@@ -3931,6 +4084,17 @@ impl ChatApp {
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
self.jump_to_top();
|
self.jump_to_top();
|
||||||
|
self.status = "Jumped to top".to_string();
|
||||||
|
self.error = None;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
AppCommand::JumpToBottom => {
|
||||||
|
if !matches!(self.mode, InputMode::Normal) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
self.jump_to_bottom();
|
||||||
|
self.status = "Jumped to bottom".to_string();
|
||||||
|
self.error = None;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
AppCommand::ExpandFilePanel => {
|
AppCommand::ExpandFilePanel => {
|
||||||
@@ -6441,90 +6605,98 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
InputMode::Editing => match (key.code, key.modifiers) {
|
InputMode::Editing => {
|
||||||
(KeyCode::Char('p'), modifiers)
|
if self.current_keymap_profile == KeymapProfile::Emacs
|
||||||
if modifiers.contains(KeyModifiers::CONTROL) =>
|
&& self.handle_emacs_editing_key(&key)
|
||||||
{
|
{
|
||||||
self.sync_textarea_to_buffer();
|
|
||||||
self.set_input_mode(InputMode::Command);
|
|
||||||
self.command_palette.clear();
|
|
||||||
self.command_palette.ensure_suggestions();
|
|
||||||
self.status = ":".to_string();
|
|
||||||
}
|
|
||||||
(KeyCode::Char('c'), modifiers)
|
|
||||||
if modifiers.contains(KeyModifiers::CONTROL) =>
|
|
||||||
{
|
|
||||||
let _ = self.cancel_active_generation()?;
|
|
||||||
self.sync_textarea_to_buffer();
|
|
||||||
self.set_input_mode(InputMode::Normal);
|
|
||||||
self.reset_status();
|
|
||||||
}
|
|
||||||
(KeyCode::Esc, KeyModifiers::NONE) => {
|
|
||||||
// Sync textarea content to input buffer before leaving edit mode
|
|
||||||
self.sync_textarea_to_buffer();
|
|
||||||
self.set_input_mode(InputMode::Normal);
|
|
||||||
self.reset_status();
|
|
||||||
}
|
|
||||||
(KeyCode::Char('['), modifiers)
|
|
||||||
if modifiers.contains(KeyModifiers::CONTROL) =>
|
|
||||||
{
|
|
||||||
self.sync_textarea_to_buffer();
|
|
||||||
self.set_input_mode(InputMode::Normal);
|
|
||||||
self.reset_status();
|
|
||||||
}
|
|
||||||
(KeyCode::Char('j' | 'J'), m) if m.contains(KeyModifiers::CONTROL) => {
|
|
||||||
self.textarea.insert_newline();
|
|
||||||
}
|
|
||||||
(KeyCode::Enter, KeyModifiers::NONE) => {
|
|
||||||
self.sync_textarea_to_buffer();
|
|
||||||
let effects =
|
|
||||||
self.apply_app_event(AppEvent::Composer(ComposerEvent::Submit));
|
|
||||||
self.handle_app_effects(effects).await?;
|
|
||||||
return Ok(AppState::Running);
|
return Ok(AppState::Running);
|
||||||
}
|
}
|
||||||
(KeyCode::Enter, _) => {
|
|
||||||
// Any Enter with modifiers keeps editing and inserts a newline via tui-textarea
|
match (key.code, key.modifiers) {
|
||||||
self.textarea.input(Input::from(key));
|
(KeyCode::Char('p'), modifiers)
|
||||||
}
|
if modifiers.contains(KeyModifiers::CONTROL) =>
|
||||||
// History navigation
|
{
|
||||||
(KeyCode::Up, m) if m.contains(KeyModifiers::CONTROL) => {
|
self.sync_textarea_to_buffer();
|
||||||
self.input_buffer_mut().history_previous();
|
self.set_input_mode(InputMode::Command);
|
||||||
self.sync_buffer_to_textarea();
|
self.command_palette.clear();
|
||||||
}
|
self.command_palette.ensure_suggestions();
|
||||||
(KeyCode::Down, m) if m.contains(KeyModifiers::CONTROL) => {
|
self.status = ":".to_string();
|
||||||
self.input_buffer_mut().history_next();
|
}
|
||||||
self.sync_buffer_to_textarea();
|
(KeyCode::Char('c'), modifiers)
|
||||||
}
|
if modifiers.contains(KeyModifiers::CONTROL) =>
|
||||||
// Vim-style navigation with Ctrl
|
{
|
||||||
(KeyCode::Char('a'), m) if m.contains(KeyModifiers::CONTROL) => {
|
let _ = self.cancel_active_generation()?;
|
||||||
self.textarea.move_cursor(tui_textarea::CursorMove::Head);
|
self.sync_textarea_to_buffer();
|
||||||
}
|
self.set_input_mode(InputMode::Normal);
|
||||||
(KeyCode::Char('e'), m) if m.contains(KeyModifiers::CONTROL) => {
|
self.reset_status();
|
||||||
self.textarea.move_cursor(tui_textarea::CursorMove::End);
|
}
|
||||||
}
|
(KeyCode::Esc, KeyModifiers::NONE) => {
|
||||||
(KeyCode::Char('w'), m) if m.contains(KeyModifiers::CONTROL) => {
|
// Sync textarea content to input buffer before leaving edit mode
|
||||||
self.textarea
|
self.sync_textarea_to_buffer();
|
||||||
.move_cursor(tui_textarea::CursorMove::WordForward);
|
self.set_input_mode(InputMode::Normal);
|
||||||
}
|
self.reset_status();
|
||||||
(KeyCode::Char('b'), m) if m.contains(KeyModifiers::CONTROL) => {
|
}
|
||||||
self.textarea
|
(KeyCode::Char('['), modifiers)
|
||||||
.move_cursor(tui_textarea::CursorMove::WordBack);
|
if modifiers.contains(KeyModifiers::CONTROL) =>
|
||||||
}
|
{
|
||||||
(KeyCode::Tab, m) if m.is_empty() => {
|
self.sync_textarea_to_buffer();
|
||||||
if !self.complete_resource_reference() {
|
self.set_input_mode(InputMode::Normal);
|
||||||
|
self.reset_status();
|
||||||
|
}
|
||||||
|
(KeyCode::Char('j' | 'J'), m) if m.contains(KeyModifiers::CONTROL) => {
|
||||||
|
self.textarea.insert_newline();
|
||||||
|
}
|
||||||
|
(KeyCode::Enter, KeyModifiers::NONE) => {
|
||||||
|
self.sync_textarea_to_buffer();
|
||||||
|
let effects =
|
||||||
|
self.apply_app_event(AppEvent::Composer(ComposerEvent::Submit));
|
||||||
|
self.handle_app_effects(effects).await?;
|
||||||
|
return Ok(AppState::Running);
|
||||||
|
}
|
||||||
|
(KeyCode::Enter, _) => {
|
||||||
|
// Any Enter with modifiers keeps editing and inserts a newline via tui-textarea
|
||||||
|
self.textarea.input(Input::from(key));
|
||||||
|
}
|
||||||
|
// History navigation
|
||||||
|
(KeyCode::Up, m) if m.contains(KeyModifiers::CONTROL) => {
|
||||||
|
self.input_buffer_mut().history_previous();
|
||||||
|
self.sync_buffer_to_textarea();
|
||||||
|
}
|
||||||
|
(KeyCode::Down, m) if m.contains(KeyModifiers::CONTROL) => {
|
||||||
|
self.input_buffer_mut().history_next();
|
||||||
|
self.sync_buffer_to_textarea();
|
||||||
|
}
|
||||||
|
// Vim-style navigation with Ctrl
|
||||||
|
(KeyCode::Char('a'), m) if m.contains(KeyModifiers::CONTROL) => {
|
||||||
|
self.textarea.move_cursor(tui_textarea::CursorMove::Head);
|
||||||
|
}
|
||||||
|
(KeyCode::Char('e'), m) if m.contains(KeyModifiers::CONTROL) => {
|
||||||
|
self.textarea.move_cursor(tui_textarea::CursorMove::End);
|
||||||
|
}
|
||||||
|
(KeyCode::Char('w'), m) if m.contains(KeyModifiers::CONTROL) => {
|
||||||
|
self.textarea
|
||||||
|
.move_cursor(tui_textarea::CursorMove::WordForward);
|
||||||
|
}
|
||||||
|
(KeyCode::Char('b'), m) if m.contains(KeyModifiers::CONTROL) => {
|
||||||
|
self.textarea
|
||||||
|
.move_cursor(tui_textarea::CursorMove::WordBack);
|
||||||
|
}
|
||||||
|
(KeyCode::Tab, m) if m.is_empty() => {
|
||||||
|
if !self.complete_resource_reference() {
|
||||||
|
self.textarea.input(Input::from(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(KeyCode::Char('r'), m) if m.contains(KeyModifiers::CONTROL) => {
|
||||||
|
// Redo - history next
|
||||||
|
self.input_buffer_mut().history_next();
|
||||||
|
self.sync_buffer_to_textarea();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Let tui-textarea handle all other input
|
||||||
self.textarea.input(Input::from(key));
|
self.textarea.input(Input::from(key));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(KeyCode::Char('r'), m) if m.contains(KeyModifiers::CONTROL) => {
|
}
|
||||||
// Redo - history next
|
|
||||||
self.input_buffer_mut().history_next();
|
|
||||||
self.sync_buffer_to_textarea();
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Let tui-textarea handle all other input
|
|
||||||
self.textarea.input(Input::from(key));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
InputMode::Visual => match (key.code, key.modifiers) {
|
InputMode::Visual => match (key.code, key.modifiers) {
|
||||||
(KeyCode::Esc, _) | (KeyCode::Char('v'), KeyModifiers::NONE) => {
|
(KeyCode::Esc, _) | (KeyCode::Char('v'), KeyModifiers::NONE) => {
|
||||||
// Cancel selection and return to normal mode
|
// Cancel selection and return to normal mode
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use crate::{
|
|||||||
pub enum AppCommand {
|
pub enum AppCommand {
|
||||||
OpenModelPicker(Option<FilterMode>),
|
OpenModelPicker(Option<FilterMode>),
|
||||||
OpenCommandPalette,
|
OpenCommandPalette,
|
||||||
|
OpenProviderSwitcher,
|
||||||
CycleFocusForward,
|
CycleFocusForward,
|
||||||
CycleFocusBackward,
|
CycleFocusBackward,
|
||||||
FocusPanel(FocusedPanel),
|
FocusPanel(FocusedPanel),
|
||||||
@@ -19,6 +20,7 @@ pub enum AppCommand {
|
|||||||
ToggleDebugLog,
|
ToggleDebugLog,
|
||||||
SetKeymap(KeymapProfile),
|
SetKeymap(KeymapProfile),
|
||||||
JumpToTop,
|
JumpToTop,
|
||||||
|
JumpToBottom,
|
||||||
ExpandFilePanel,
|
ExpandFilePanel,
|
||||||
ToggleHiddenFiles,
|
ToggleHiddenFiles,
|
||||||
SplitPaneHorizontal,
|
SplitPaneHorizontal,
|
||||||
@@ -53,6 +55,10 @@ impl CommandRegistry {
|
|||||||
AppCommand::OpenModelPicker(Some(FilterMode::Available)),
|
AppCommand::OpenModelPicker(Some(FilterMode::Available)),
|
||||||
);
|
);
|
||||||
commands.insert("palette.open".to_string(), AppCommand::OpenCommandPalette);
|
commands.insert("palette.open".to_string(), AppCommand::OpenCommandPalette);
|
||||||
|
commands.insert(
|
||||||
|
"provider.switch".to_string(),
|
||||||
|
AppCommand::OpenProviderSwitcher,
|
||||||
|
);
|
||||||
commands.insert("focus.next".to_string(), AppCommand::CycleFocusForward);
|
commands.insert("focus.next".to_string(), AppCommand::CycleFocusForward);
|
||||||
commands.insert("focus.prev".to_string(), AppCommand::CycleFocusBackward);
|
commands.insert("focus.prev".to_string(), AppCommand::CycleFocusBackward);
|
||||||
commands.insert(
|
commands.insert(
|
||||||
@@ -87,6 +93,7 @@ impl CommandRegistry {
|
|||||||
AppCommand::SetKeymap(KeymapProfile::Emacs),
|
AppCommand::SetKeymap(KeymapProfile::Emacs),
|
||||||
);
|
);
|
||||||
commands.insert("navigate.top".to_string(), AppCommand::JumpToTop);
|
commands.insert("navigate.top".to_string(), AppCommand::JumpToTop);
|
||||||
|
commands.insert("navigate.bottom".to_string(), AppCommand::JumpToBottom);
|
||||||
commands.insert(
|
commands.insert(
|
||||||
"files.focus_expand".to_string(),
|
"files.focus_expand".to_string(),
|
||||||
AppCommand::ExpandFilePanel,
|
AppCommand::ExpandFilePanel,
|
||||||
|
|||||||
@@ -15,6 +15,37 @@ use crate::commands::registry::{AppCommand, CommandRegistry};
|
|||||||
const DEFAULT_KEYMAP: &str = include_str!("../../keymap.toml");
|
const DEFAULT_KEYMAP: &str = include_str!("../../keymap.toml");
|
||||||
const EMACS_KEYMAP: &str = include_str!("../../keymap_emacs.toml");
|
const EMACS_KEYMAP: &str = include_str!("../../keymap_emacs.toml");
|
||||||
const DEFAULT_SEQUENCE_TIMEOUT: Duration = Duration::from_millis(1200);
|
const DEFAULT_SEQUENCE_TIMEOUT: Duration = Duration::from_millis(1200);
|
||||||
|
const DEFAULT_LEADER_KEY: &str = "Space";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct KeymapOverrides {
|
||||||
|
leader: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeymapOverrides {
|
||||||
|
pub fn new(leader: impl Into<String>) -> Self {
|
||||||
|
let raw = leader.into();
|
||||||
|
let trimmed = raw.trim();
|
||||||
|
let leader = if trimmed.is_empty() {
|
||||||
|
DEFAULT_LEADER_KEY.to_string()
|
||||||
|
} else {
|
||||||
|
trimmed.to_string()
|
||||||
|
};
|
||||||
|
Self { leader }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leader(&self) -> &str {
|
||||||
|
&self.leader
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KeymapOverrides {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
leader: DEFAULT_LEADER_KEY.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Keymap {
|
pub struct Keymap {
|
||||||
@@ -22,6 +53,7 @@ pub struct Keymap {
|
|||||||
trees: HashMap<InputMode, KeymapNode>,
|
trees: HashMap<InputMode, KeymapNode>,
|
||||||
default_timeout: Duration,
|
default_timeout: Duration,
|
||||||
bindings: Vec<ResolvedBinding>,
|
bindings: Vec<ResolvedBinding>,
|
||||||
|
overrides: KeymapOverrides,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
@@ -43,6 +75,7 @@ impl Keymap {
|
|||||||
custom_path: Option<&str>,
|
custom_path: Option<&str>,
|
||||||
preferred_profile: Option<&str>,
|
preferred_profile: Option<&str>,
|
||||||
registry: &CommandRegistry,
|
registry: &CommandRegistry,
|
||||||
|
overrides: KeymapOverrides,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut loader = KeymapLoader::new(preferred_profile);
|
let mut loader = KeymapLoader::new(preferred_profile);
|
||||||
if let Some(path) = custom_path.and_then(expand_path) {
|
if let Some(path) = custom_path.and_then(expand_path) {
|
||||||
@@ -86,7 +119,11 @@ impl Keymap {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let sequences = entry.resolve_sequences();
|
let sequences = entry
|
||||||
|
.resolve_sequences()
|
||||||
|
.into_iter()
|
||||||
|
.map(|sequence| apply_overrides(sequence, &overrides))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
if sequences.is_empty() {
|
if sequences.is_empty() {
|
||||||
warn!(
|
warn!(
|
||||||
"No key sequence defined for command '{}' (modes: {:?})",
|
"No key sequence defined for command '{}' (modes: {:?})",
|
||||||
@@ -134,9 +171,14 @@ impl Keymap {
|
|||||||
trees,
|
trees,
|
||||||
default_timeout: DEFAULT_SEQUENCE_TIMEOUT,
|
default_timeout: DEFAULT_SEQUENCE_TIMEOUT,
|
||||||
bindings,
|
bindings,
|
||||||
|
overrides,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn leader(&self) -> &str {
|
||||||
|
self.overrides.leader()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn profile(&self) -> KeymapProfile {
|
pub fn profile(&self) -> KeymapProfile {
|
||||||
self.profile
|
self.profile
|
||||||
}
|
}
|
||||||
@@ -342,7 +384,7 @@ impl KeyPattern {
|
|||||||
for token in tokens[..tokens.len().saturating_sub(1)].iter() {
|
for token in tokens[..tokens.len().saturating_sub(1)].iter() {
|
||||||
match token.to_ascii_lowercase().as_str() {
|
match token.to_ascii_lowercase().as_str() {
|
||||||
"ctrl" | "control" => modifiers.insert(KeyModifiers::CONTROL),
|
"ctrl" | "control" => modifiers.insert(KeyModifiers::CONTROL),
|
||||||
"alt" | "option" => modifiers.insert(KeyModifiers::ALT),
|
"alt" | "option" | "meta" => modifiers.insert(KeyModifiers::ALT),
|
||||||
"shift" => modifiers.insert(KeyModifiers::SHIFT),
|
"shift" => modifiers.insert(KeyModifiers::SHIFT),
|
||||||
other => warn!("Unknown modifier '{other}' in key binding '{spec}'"),
|
other => warn!("Unknown modifier '{other}' in key binding '{spec}'"),
|
||||||
}
|
}
|
||||||
@@ -369,6 +411,7 @@ impl KeyPattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let key = match self.code {
|
let key = match self.code {
|
||||||
|
KeyCodeKind::Char(' ') => "Space".to_string(),
|
||||||
KeyCodeKind::Char(c) => c.to_string(),
|
KeyCodeKind::Char(c) => c.to_string(),
|
||||||
KeyCodeKind::Enter => "Enter".to_string(),
|
KeyCodeKind::Enter => "Enter".to_string(),
|
||||||
KeyCodeKind::Tab => "Tab".to_string(),
|
KeyCodeKind::Tab => "Tab".to_string(),
|
||||||
@@ -477,6 +520,19 @@ impl KeyList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_overrides(sequence: Vec<String>, overrides: &KeymapOverrides) -> Vec<String> {
|
||||||
|
sequence
|
||||||
|
.into_iter()
|
||||||
|
.map(|token| {
|
||||||
|
if token.trim().eq_ignore_ascii_case("<leader>") {
|
||||||
|
overrides.leader().to_string()
|
||||||
|
} else {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn insert_sequence(
|
fn insert_sequence(
|
||||||
root: &mut KeymapNode,
|
root: &mut KeymapNode,
|
||||||
sequence: &[KeyPattern],
|
sequence: &[KeyPattern],
|
||||||
@@ -718,7 +774,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn resolve_binding_from_default_keymap() {
|
fn resolve_binding_from_default_keymap() {
|
||||||
let registry = CommandRegistry::new();
|
let registry = CommandRegistry::new();
|
||||||
let keymap = Keymap::load(None, None, ®istry);
|
let keymap = Keymap::load(None, None, ®istry, KeymapOverrides::default());
|
||||||
let mut state = KeymapState::default();
|
let mut state = KeymapState::default();
|
||||||
|
|
||||||
let event = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::NONE);
|
let event = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::NONE);
|
||||||
@@ -732,7 +788,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn resolves_multi_key_sequence() {
|
fn resolves_multi_key_sequence() {
|
||||||
let registry = CommandRegistry::new();
|
let registry = CommandRegistry::new();
|
||||||
let keymap = Keymap::load(None, None, ®istry);
|
let keymap = Keymap::load(None, None, ®istry, KeymapOverrides::default());
|
||||||
let mut state = KeymapState::default();
|
let mut state = KeymapState::default();
|
||||||
|
|
||||||
let first = keymap.step(
|
let first = keymap.step(
|
||||||
@@ -755,7 +811,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn emacs_profile_loads_builtin() {
|
fn emacs_profile_loads_builtin() {
|
||||||
let registry = CommandRegistry::new();
|
let registry = CommandRegistry::new();
|
||||||
let keymap = Keymap::load(None, Some("emacs"), ®istry);
|
let keymap = Keymap::load(None, Some("emacs"), ®istry, KeymapOverrides::default());
|
||||||
assert_eq!(keymap.profile(), KeymapProfile::Emacs);
|
assert_eq!(keymap.profile(), KeymapProfile::Emacs);
|
||||||
let mut state = KeymapState::default();
|
let mut state = KeymapState::default();
|
||||||
let result = keymap.step(
|
let result = keymap.step(
|
||||||
@@ -768,4 +824,27 @@ mod tests {
|
|||||||
KeymapEventResult::Matched(AppCommand::EnterCommandMode)
|
KeymapEventResult::Matched(AppCommand::EnterCommandMode)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leader_override_substitutes_placeholder_tokens() {
|
||||||
|
let registry = CommandRegistry::new();
|
||||||
|
let overrides = KeymapOverrides::new("Ctrl+Space");
|
||||||
|
let keymap = Keymap::load(None, None, ®istry, overrides.clone());
|
||||||
|
assert_eq!(keymap.leader(), overrides.leader());
|
||||||
|
|
||||||
|
let mut found_leader_binding = false;
|
||||||
|
for binding in keymap.describe_bindings() {
|
||||||
|
if binding.command == "model.open_all"
|
||||||
|
&& binding.sequence.first().map(String::as_str) == Some("Ctrl+Space")
|
||||||
|
{
|
||||||
|
found_leader_binding = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
found_leader_binding,
|
||||||
|
"expected leader substitution to produce Ctrl+Space prefix for model.open_all"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ pub use file_icons::{FileIconResolver, FileIconSet, IconDetection};
|
|||||||
pub use file_tree::{
|
pub use file_tree::{
|
||||||
FileNode, FileTreeState, FilterMode as FileFilterMode, GitDecoration, VisibleFileEntry,
|
FileNode, FileTreeState, FilterMode as FileFilterMode, GitDecoration, VisibleFileEntry,
|
||||||
};
|
};
|
||||||
pub use keymap::{Keymap, KeymapBindingDescription, KeymapEventResult, KeymapProfile, KeymapState};
|
pub use keymap::{
|
||||||
|
Keymap, KeymapBindingDescription, KeymapEventResult, KeymapOverrides, KeymapProfile,
|
||||||
|
KeymapState,
|
||||||
|
};
|
||||||
pub use search::{
|
pub use search::{
|
||||||
RepoSearchFile, RepoSearchMatch, RepoSearchMessage, RepoSearchRow, RepoSearchRowKind,
|
RepoSearchFile, RepoSearchMatch, RepoSearchMessage, RepoSearchRow, RepoSearchRowKind,
|
||||||
RepoSearchState, SymbolEntry, SymbolKind, SymbolSearchMessage, SymbolSearchState,
|
RepoSearchState, SymbolEntry, SymbolKind, SymbolSearchMessage, SymbolSearchState,
|
||||||
|
|||||||
@@ -3981,6 +3981,7 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
let reduced = app.is_reduced_chrome();
|
let reduced = app.is_reduced_chrome();
|
||||||
let palette = GlassPalette::for_theme_with_mode(theme, reduced);
|
let palette = GlassPalette::for_theme_with_mode(theme, reduced);
|
||||||
let profile = app.current_keymap_profile();
|
let profile = app.current_keymap_profile();
|
||||||
|
let leader = app.keymap_leader();
|
||||||
let area = centered_rect(75, 70, frame.area());
|
let area = centered_rect(75, 70, frame.area());
|
||||||
if area.width == 0 || area.height == 0 {
|
if area.width == 0 || area.height == 0 {
|
||||||
return;
|
return;
|
||||||
@@ -4077,6 +4078,24 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
" Alt+O → cycle panels forward (Tab also works)",
|
" Alt+O → cycle panels forward (Tab also works)",
|
||||||
));
|
));
|
||||||
lines.push(Line::from(" Alt+Shift+O → cycle panels backward"));
|
lines.push(Line::from(" Alt+Shift+O → cycle panels backward"));
|
||||||
|
lines.push(Line::from(" Ctrl+X Ctrl+F → expand Files panel"));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.push(Line::from(vec![Span::styled(
|
||||||
|
"WINDOW & PROVIDER",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD).fg(theme.info),
|
||||||
|
)]));
|
||||||
|
lines.push(Line::from(" Ctrl+X 2 → split horizontal"));
|
||||||
|
lines.push(Line::from(" Ctrl+X 3 → split vertical"));
|
||||||
|
lines.push(Line::from(" Ctrl+X o → focus next window"));
|
||||||
|
lines.push(Line::from(" Ctrl+X Shift+O → focus previous window"));
|
||||||
|
lines.push(Line::from(" Ctrl+X Ctrl+P → open provider switcher"));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.push(Line::from(vec![Span::styled(
|
||||||
|
"JUMPS",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD).fg(theme.info),
|
||||||
|
)]));
|
||||||
|
lines.push(Line::from(" Alt+G g → jump to top"));
|
||||||
|
lines.push(Line::from(" Alt+G Shift+G → jump to bottom"));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
lines.push(Line::from(
|
lines.push(Line::from(
|
||||||
@@ -4090,6 +4109,33 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
" Ctrl/Alt+4 → focus Thinking / Agent Actions",
|
" Ctrl/Alt+4 → focus Thinking / Agent Actions",
|
||||||
));
|
));
|
||||||
lines.push(Line::from(" Ctrl/Alt+5 → focus Input editor"));
|
lines.push(Line::from(" Ctrl/Alt+5 → focus Input editor"));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.push(Line::from(vec![Span::styled(
|
||||||
|
format!("LEADER ({leader})"),
|
||||||
|
Style::default().add_modifier(Modifier::BOLD).fg(theme.info),
|
||||||
|
)]));
|
||||||
|
lines.push(Line::from(format!(
|
||||||
|
" {leader} m → open model picker"
|
||||||
|
)));
|
||||||
|
lines.push(Line::from(format!(
|
||||||
|
" {leader} m l → focus local models"
|
||||||
|
)));
|
||||||
|
lines.push(Line::from(format!(
|
||||||
|
" {leader} m c → focus cloud models"
|
||||||
|
)));
|
||||||
|
lines.push(Line::from(format!(
|
||||||
|
" {leader} p → provider switcher"
|
||||||
|
)));
|
||||||
|
lines.push(Line::from(format!(
|
||||||
|
" {leader} t → open command palette"
|
||||||
|
)));
|
||||||
|
lines.push(Line::from(format!(
|
||||||
|
" {leader} l s → split horizontal"
|
||||||
|
)));
|
||||||
|
lines.push(Line::from(format!(" {leader} l v → split vertical")));
|
||||||
|
lines.push(Line::from(format!(
|
||||||
|
" {leader} l h/j/k/l → focus left/down/up/right"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4167,7 +4213,7 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
1 => {
|
1 => {
|
||||||
let send_line = match profile {
|
let send_line = match profile {
|
||||||
KeymapProfile::Emacs => {
|
KeymapProfile::Emacs => {
|
||||||
" Ctrl+Enter → send message (Enter inserts newline first)"
|
" Ctrl+Enter or Ctrl+X Ctrl+S → send message (Enter inserts newline first)"
|
||||||
}
|
}
|
||||||
_ => " Enter → send message (slash commands run before send)",
|
_ => " Enter → send message (slash commands run before send)",
|
||||||
};
|
};
|
||||||
@@ -4181,14 +4227,16 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
KeymapProfile::Emacs => " Shift+Enter → insert newline (alternate)",
|
KeymapProfile::Emacs => " Shift+Enter → insert newline (alternate)",
|
||||||
_ => " Ctrl+J → insert newline (multiline compose)",
|
_ => " Ctrl+J → insert newline (multiline compose)",
|
||||||
};
|
};
|
||||||
let palette_binding = match profile {
|
let palette_binding: String = match profile {
|
||||||
KeymapProfile::Emacs => {
|
KeymapProfile::Emacs => {
|
||||||
" Alt+x → open command palette (also works in Normal)"
|
" Ctrl+Space → open command palette (also works in Normal)".to_string()
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
format!(" Ctrl+P / {leader} t → open command palette (also works in Normal)")
|
||||||
}
|
}
|
||||||
_ => " Ctrl+P → open command palette (also works in Normal)",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let lines = vec![
|
let mut lines = vec![
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from(vec![Span::styled(
|
Line::from(vec![Span::styled(
|
||||||
"ENTERING EDIT MODE",
|
"ENTERING EDIT MODE",
|
||||||
@@ -4217,29 +4265,44 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
Line::from(" Ctrl+↑ / Ctrl+↓ → cycle input history"),
|
Line::from(" Ctrl+↑ / Ctrl+↓ → cycle input history"),
|
||||||
Line::from(" Ctrl+A / Ctrl+E → jump to start / end of line"),
|
Line::from(" Ctrl+A / Ctrl+E → jump to start / end of line"),
|
||||||
Line::from(" Ctrl+W / Ctrl+B → move cursor by words forward/back"),
|
Line::from(" Ctrl+W / Ctrl+B → move cursor by words forward/back"),
|
||||||
Line::from(palette_binding),
|
Line::from(palette_binding.clone()),
|
||||||
Line::from(" Ctrl+C → cancel streaming response and exit editing"),
|
Line::from(" Ctrl+C → cancel streaming response and exit editing"),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from(vec![Span::styled(
|
|
||||||
"NORMAL MODE SHORTCUTS",
|
|
||||||
Style::default().add_modifier(Modifier::BOLD).fg(theme.info),
|
|
||||||
)]),
|
|
||||||
Line::from(" dd → clear input buffer"),
|
|
||||||
Line::from(" p → paste clipboard into input"),
|
|
||||||
Line::from(palette_binding),
|
|
||||||
Line::from(""),
|
|
||||||
Line::from(vec![Span::styled(
|
|
||||||
"TIPS",
|
|
||||||
Style::default()
|
|
||||||
.add_modifier(Modifier::BOLD)
|
|
||||||
.fg(theme.user_message_role),
|
|
||||||
)]),
|
|
||||||
Line::from(" • Slash commands (e.g. :clear, :open) are parsed before sending"),
|
|
||||||
Line::from(
|
|
||||||
" • After sending, focus returns to Normal—press i or Ctrl/Alt+5 to edit",
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if matches!(profile, KeymapProfile::Emacs) {
|
||||||
|
lines.push(Line::from(vec![Span::styled(
|
||||||
|
"KILL RING",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD).fg(theme.info),
|
||||||
|
)]));
|
||||||
|
lines.push(Line::from(" Ctrl+Y → yank last kill into buffer"));
|
||||||
|
lines.push(Line::from(" Alt+W → copy previous word"));
|
||||||
|
lines.push(Line::from(" Ctrl+W → kill previous word"));
|
||||||
|
lines.push(Line::from(" Ctrl+K → kill to end of line"));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(Line::from(vec![Span::styled(
|
||||||
|
"NORMAL MODE SHORTCUTS",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD).fg(theme.info),
|
||||||
|
)]));
|
||||||
|
lines.push(Line::from(" dd → clear input buffer"));
|
||||||
|
lines.push(Line::from(" p → paste clipboard into input"));
|
||||||
|
lines.push(Line::from(palette_binding.clone()));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.push(Line::from(vec![Span::styled(
|
||||||
|
"TIPS",
|
||||||
|
Style::default()
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
.fg(theme.user_message_role),
|
||||||
|
)]));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" • Slash commands (e.g. :clear, :open) are parsed before sending",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" • After sending, focus returns to Normal—press i or Ctrl/Alt+5 to edit",
|
||||||
|
));
|
||||||
|
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
2 => vec![
|
2 => vec![
|
||||||
@@ -4283,112 +4346,177 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
Line::from(" • Yanked text is available for paste with 'p' in normal mode"),
|
Line::from(" • Yanked text is available for paste with 'p' in normal mode"),
|
||||||
Line::from(" • Read-only panels (Chat/Thinking) always keep data intact; yank copies"),
|
Line::from(" • Read-only panels (Chat/Thinking) always keep data intact; yank copies"),
|
||||||
],
|
],
|
||||||
3 => vec![
|
3 => {
|
||||||
// Commands
|
let mut lines = vec![
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from(vec![Span::styled(
|
Line::from(vec![Span::styled(
|
||||||
"COMMAND MODE",
|
"COMMAND MODE",
|
||||||
Style::default().add_modifier(Modifier::BOLD).fg(theme.info),
|
Style::default().add_modifier(Modifier::BOLD).fg(theme.info),
|
||||||
)]),
|
)]),
|
||||||
Line::from(" Press ':' to enter command mode, then type one of:"),
|
Line::from(" Press ':' to enter command mode, then type one of:"),
|
||||||
Line::from(""),
|
];
|
||||||
Line::from(" :keymap [vim|emacs] → switch keymap profile"),
|
|
||||||
Line::from(" :keymap → display the active keymap"),
|
if matches!(profile, KeymapProfile::Emacs) {
|
||||||
Line::from(""),
|
lines.push(Line::from(
|
||||||
Line::from(vec![Span::styled(
|
" Alt+x → enter command mode (Emacs M-x)",
|
||||||
"KEYBINDINGS",
|
));
|
||||||
Style::default()
|
}
|
||||||
.add_modifier(Modifier::BOLD)
|
|
||||||
.fg(theme.user_message_role),
|
lines.extend([
|
||||||
)]),
|
Line::from(""),
|
||||||
Line::from(" Enter → execute command"),
|
Line::from(" :keymap [vim|emacs] → switch keymap profile"),
|
||||||
Line::from(" Esc → exit command mode"),
|
Line::from(" :keymap → display the active keymap"),
|
||||||
Line::from(" Tab → autocomplete suggestion"),
|
Line::from(""),
|
||||||
Line::from(" ↑/↓ → navigate suggestions"),
|
Line::from(vec![Span::styled(
|
||||||
Line::from(" Backspace → delete character"),
|
"KEYBINDINGS",
|
||||||
Line::from(" Ctrl+P → open command palette"),
|
Style::default()
|
||||||
Line::from(""),
|
.add_modifier(Modifier::BOLD)
|
||||||
Line::from(vec![Span::styled(
|
.fg(theme.user_message_role),
|
||||||
|
)]),
|
||||||
|
]);
|
||||||
|
lines.push(Line::from(" Enter → execute command"));
|
||||||
|
lines.push(Line::from(" Esc → exit command mode"));
|
||||||
|
lines.push(Line::from(" Tab → autocomplete suggestion"));
|
||||||
|
lines.push(Line::from(" ↑/↓ → navigate suggestions"));
|
||||||
|
lines.push(Line::from(" Backspace → delete character"));
|
||||||
|
lines.push(Line::from(" Ctrl+P → open command palette"));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.push(Line::from(vec![Span::styled(
|
||||||
"GENERAL",
|
"GENERAL",
|
||||||
Style::default()
|
Style::default()
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
.fg(theme.user_message_role),
|
.fg(theme.user_message_role),
|
||||||
)]),
|
)]));
|
||||||
Line::from(" :h, :help → show this help"),
|
lines.push(Line::from(" :h, :help → show this help"));
|
||||||
Line::from(" F1 or ? → toggle help overlay"),
|
lines.push(Line::from(" F1 or ? → toggle help overlay"));
|
||||||
Line::from(" F12 → toggle debug log panel"),
|
lines.push(Line::from(" F12 → toggle debug log panel"));
|
||||||
Line::from(" :files, :explorer → toggle files panel"),
|
lines.push(Line::from(" :files, :explorer → toggle files panel"));
|
||||||
Line::from(" :markdown [on|off] → toggle markdown rendering"),
|
lines.push(Line::from(
|
||||||
Line::from(" Ctrl+←/→ → resize files panel"),
|
" :markdown [on|off] → toggle markdown rendering",
|
||||||
Line::from(" Ctrl+↑/↓ → resize chat/thinking split"),
|
));
|
||||||
Line::from(" :quit → quit application"),
|
lines.push(Line::from(" Ctrl+←/→ → resize files panel"));
|
||||||
Line::from(" Ctrl+C twice → quit application"),
|
lines.push(Line::from(
|
||||||
Line::from(" :reload → reload configuration and themes"),
|
" Ctrl+↑/↓ → resize chat/thinking split",
|
||||||
Line::from(" :layout save/load → persist or restore pane layout"),
|
));
|
||||||
Line::from(""),
|
lines.push(Line::from(" :quit → quit application"));
|
||||||
Line::from(vec![Span::styled(
|
lines.push(Line::from(" Ctrl+C twice → quit application"));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" :reload → reload configuration and themes",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" :layout save/load → persist or restore pane layout",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.push(Line::from(vec![Span::styled(
|
||||||
"CONVERSATION",
|
"CONVERSATION",
|
||||||
Style::default()
|
Style::default()
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
.fg(theme.user_message_role),
|
.fg(theme.user_message_role),
|
||||||
)]),
|
)]));
|
||||||
Line::from(" :n, :new → start new conversation"),
|
lines.push(Line::from(" :n, :new → start new conversation"));
|
||||||
Line::from(" :c, :clear → clear current conversation"),
|
lines.push(Line::from(
|
||||||
Line::from(""),
|
" :c, :clear → clear current conversation",
|
||||||
Line::from(vec![Span::styled(
|
));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.push(Line::from(vec![Span::styled(
|
||||||
"MODEL & THEME",
|
"MODEL & THEME",
|
||||||
Style::default()
|
Style::default()
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
.fg(theme.user_message_role),
|
.fg(theme.user_message_role),
|
||||||
)]),
|
)]));
|
||||||
Line::from(" :m, :model → open model selector"),
|
lines.push(Line::from(" :m, :model → open model selector"));
|
||||||
Line::from(" :themes → open theme selector"),
|
lines.push(Line::from(" :themes → open theme selector"));
|
||||||
Line::from(" :theme <name> → switch to a specific theme"),
|
lines.push(Line::from(
|
||||||
Line::from(" :provider <name> [auto|local|cloud] → switch provider or set mode"),
|
" :theme <name> → switch to a specific theme",
|
||||||
Line::from(" :models --local | --cloud → focus models by scope"),
|
));
|
||||||
Line::from(" :cloud setup [--force-cloud-base-url] → configure Ollama Cloud"),
|
lines.push(Line::from(
|
||||||
Line::from(" :web on|off|status → manage web_search availability"),
|
" :provider <name> [auto|local|cloud] → switch provider or set mode",
|
||||||
Line::from(" :limits → show hourly/weekly usage totals"),
|
));
|
||||||
Line::from(""),
|
lines.push(Line::from(
|
||||||
Line::from(vec![Span::styled(
|
" :models --local | --cloud → focus models by scope",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" :cloud setup [--force-cloud-base-url] → configure Ollama Cloud",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" :web on|off|status → manage web_search availability",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" :limits → show hourly/weekly usage totals",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.push(Line::from(vec![Span::styled(
|
||||||
"SESSION MANAGEMENT",
|
"SESSION MANAGEMENT",
|
||||||
Style::default()
|
Style::default()
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
.fg(theme.user_message_role),
|
.fg(theme.user_message_role),
|
||||||
)]),
|
)]));
|
||||||
Line::from(" :session save [name] → save current session (optional name)"),
|
lines.push(Line::from(
|
||||||
Line::from(" :load, :o → browse and load saved sessions"),
|
" :session save [name] → save current session (optional name)",
|
||||||
Line::from(" :sessions, :ls → browse saved sessions"),
|
));
|
||||||
Line::from(""),
|
lines.push(Line::from(
|
||||||
Line::from(vec![Span::styled(
|
" :load, :o → browse and load saved sessions",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(" :sessions, :ls → browse saved sessions"));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.push(Line::from(vec![Span::styled(
|
||||||
"AGENT",
|
"AGENT",
|
||||||
Style::default()
|
Style::default()
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
.fg(theme.user_message_role),
|
.fg(theme.user_message_role),
|
||||||
)]),
|
)]));
|
||||||
Line::from(" :agent start → arm the agent for the next request"),
|
lines.push(Line::from(
|
||||||
Line::from(" :agent stop → stop or disarm the agent"),
|
" :agent start → arm the agent for the next request",
|
||||||
Line::from(" :agent status → show current agent state"),
|
));
|
||||||
Line::from(""),
|
lines.push(Line::from(
|
||||||
Line::from(vec![Span::styled(
|
" :agent stop → stop or disarm the agent",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" :agent status → show current agent state",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.push(Line::from(vec![Span::styled(
|
||||||
"CODE VIEW",
|
"CODE VIEW",
|
||||||
Style::default()
|
Style::default()
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
.fg(theme.user_message_role),
|
.fg(theme.user_message_role),
|
||||||
)]),
|
)]));
|
||||||
Line::from(" :open <path> → open file in code side panel"),
|
lines.push(Line::from(
|
||||||
Line::from(" :create <path> → create file (makes parent directories)"),
|
" :open <path> → open file in code side panel",
|
||||||
Line::from(" :close → close the code side panel"),
|
));
|
||||||
Line::from(" :w[!] [path] → write active file (optionally to path)"),
|
lines.push(Line::from(
|
||||||
Line::from(" :q[!] → close active file (append ! to discard)"),
|
" :create <path> → create file (makes parent directories)",
|
||||||
Line::from(" :wq[!] [path] → save then close active file"),
|
));
|
||||||
// New mode and tool commands added in phases 0‑5
|
lines.push(Line::from(
|
||||||
Line::from(" :code → switch to code mode (CLI: owlen --code)"),
|
" :close → close the code side panel",
|
||||||
Line::from(" :mode <chat|code> → change current mode explicitly"),
|
));
|
||||||
Line::from(" :tools install/audit → manage MCP tool presets"),
|
lines.push(Line::from(
|
||||||
Line::from(" :agent status → show agent configuration and iteration info"),
|
" :w[!] [path] → write active file (optionally to path)",
|
||||||
Line::from(" :stop-agent → abort a running ReAct agent loop"),
|
));
|
||||||
],
|
lines.push(Line::from(
|
||||||
|
" :q[!] → close active file (append ! to discard)",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" :wq[!] [path] → save then close active file",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" :code → switch to code mode (CLI: owlen --code)",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" :mode <chat|code> → change current mode explicitly",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" :tools install/audit → manage MCP tool presets",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" :agent status → show agent configuration and iteration info",
|
||||||
|
));
|
||||||
|
lines.push(Line::from(
|
||||||
|
" :stop-agent → abort a running ReAct agent loop",
|
||||||
|
));
|
||||||
|
|
||||||
|
lines
|
||||||
|
}
|
||||||
4 => vec![
|
4 => vec![
|
||||||
// Sessions
|
// Sessions
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
|
|||||||
Reference in New Issue
Block a user