use anyhow::{anyhow, Result};
use log::{debug, error, warn};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum KeyAction {
MoveUp,
MoveDown,
MoveLeft,
MoveRight,
PageUp,
PageDown,
Home,
End,
Accept,
Cancel,
Delete,
Backspace,
Clear,
NextSuggestion,
PrevSuggestion,
AcceptSuggestion,
ShowHelp,
ShowContext,
ShowStatus,
ShowTools,
ClearHistory,
ExecuteCommand(String),
InsertText(String),
ToggleFuzzySearch,
ToggleVimMode,
EnterCommandMode,
RecordMacro,
PlayMacro,
SaveSession,
LoadSession,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct KeyCombo {
pub key: String,
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub meta: bool,
}
impl KeyCombo {
pub fn new(key: &str) -> Self {
Self {
key: key.to_string(),
ctrl: false,
alt: false,
shift: false,
meta: false,
}
}
pub fn ctrl(mut self) -> Self {
self.ctrl = true;
self
}
pub fn alt(mut self) -> Self {
self.alt = true;
self
}
pub fn shift(mut self) -> Self {
self.shift = true;
self
}
pub fn meta(mut self) -> Self {
self.meta = true;
self
}
pub fn parse(input: &str) -> Result<Self> {
let parts: Vec<&str> = input.split('+').collect();
if parts.is_empty() {
return Err(anyhow!("Empty key combination"));
}
let mut combo = KeyCombo::new(parts.last().unwrap());
for modifier in &parts[..parts.len() - 1] {
match modifier.to_lowercase().as_str() {
"ctrl" | "control" => combo.ctrl = true,
"alt" | "option" => combo.alt = true,
"shift" => combo.shift = true,
"meta" | "super" | "cmd" => combo.meta = true,
_ => return Err(anyhow!("Unknown modifier: {}", modifier)),
}
}
Ok(combo)
}
pub fn format(&self) -> String {
let mut parts = Vec::new();
if self.ctrl {
parts.push("Ctrl");
}
if self.alt {
parts.push("Alt");
}
if self.shift {
parts.push("Shift");
}
if self.meta {
parts.push("Meta");
}
parts.push(&self.key);
parts.join("+")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeybindingConfig {
pub bindings: HashMap<KeyCombo, KeyAction>,
pub vim_mode: bool,
pub emacs_mode: bool,
pub custom_commands: HashMap<String, String>,
}
impl Default for KeybindingConfig {
fn default() -> Self {
let mut bindings = HashMap::new();
bindings.insert(KeyCombo::new("ArrowUp"), KeyAction::MoveUp);
bindings.insert(KeyCombo::new("ArrowDown"), KeyAction::MoveDown);
bindings.insert(KeyCombo::new("ArrowLeft"), KeyAction::MoveLeft);
bindings.insert(KeyCombo::new("ArrowRight"), KeyAction::MoveRight);
bindings.insert(KeyCombo::new("PageUp"), KeyAction::PageUp);
bindings.insert(KeyCombo::new("PageDown"), KeyAction::PageDown);
bindings.insert(KeyCombo::new("Home"), KeyAction::Home);
bindings.insert(KeyCombo::new("End"), KeyAction::End);
bindings.insert(KeyCombo::new("Enter"), KeyAction::Accept);
bindings.insert(KeyCombo::new("Escape"), KeyAction::Cancel);
bindings.insert(KeyCombo::new("Delete"), KeyAction::Delete);
bindings.insert(KeyCombo::new("Backspace"), KeyAction::Backspace);
bindings.insert(KeyCombo::new("Tab"), KeyAction::AcceptSuggestion);
bindings.insert(KeyCombo::new("h").ctrl(), KeyAction::ShowHelp);
bindings.insert(KeyCombo::new("c").ctrl(), KeyAction::Cancel);
bindings.insert(KeyCombo::new("l").ctrl(), KeyAction::Clear);
bindings.insert(KeyCombo::new("u").ctrl(), KeyAction::ClearHistory);
bindings.insert(KeyCombo::new("F1"), KeyAction::ShowHelp);
bindings.insert(KeyCombo::new("F2"), KeyAction::ShowContext);
bindings.insert(KeyCombo::new("F3"), KeyAction::ShowStatus);
bindings.insert(KeyCombo::new("F4"), KeyAction::ShowTools);
bindings.insert(KeyCombo::new("r").ctrl().shift(), KeyAction::RecordMacro);
bindings.insert(KeyCombo::new("p").ctrl().shift(), KeyAction::PlayMacro);
bindings.insert(KeyCombo::new("s").ctrl(), KeyAction::SaveSession);
bindings.insert(KeyCombo::new("o").ctrl(), KeyAction::LoadSession);
bindings.insert(
KeyCombo::new("f").ctrl().alt(),
KeyAction::ToggleFuzzySearch,
);
bindings.insert(KeyCombo::new("v").ctrl().alt(), KeyAction::ToggleVimMode);
Self {
bindings,
vim_mode: false,
emacs_mode: false,
custom_commands: HashMap::new(),
}
}
}
impl KeybindingConfig {
pub fn load() -> Result<Self> {
let config_path = Self::config_path()?;
if !config_path.exists() {
debug!("No keybinding config found, creating default");
let default_config = Self::default();
default_config.save()?;
return Ok(default_config);
}
let content = fs::read_to_string(&config_path)
.map_err(|e| anyhow!("Failed to read keybinding config: {}", e))?;
let config: Self = serde_json::from_str(&content)
.map_err(|e| anyhow!("Failed to parse keybinding config: {}", e))?;
debug!(
"Loaded keybinding config with {} bindings",
config.bindings.len()
);
Ok(config)
}
pub fn save(&self) -> Result<()> {
let config_path = Self::config_path()?;
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| anyhow!("Failed to create config directory: {}", e))?;
}
let content = serde_json::to_string_pretty(self)
.map_err(|e| anyhow!("Failed to serialize keybinding config: {}", e))?;
fs::write(&config_path, content)
.map_err(|e| anyhow!("Failed to write keybinding config: {}", e))?;
debug!("Saved keybinding config to {:?}", config_path);
Ok(())
}
fn config_path() -> Result<PathBuf> {
let home =
std::env::var("HOME").map_err(|_| anyhow!("HOME environment variable not set"))?;
Ok(PathBuf::from(home).join(".osvm").join("keybindings.json"))
}
pub fn get_action(&self, key_combo: &KeyCombo) -> Option<&KeyAction> {
self.bindings.get(key_combo)
}
pub fn set_binding(&mut self, key_combo: KeyCombo, action: KeyAction) {
self.bindings.insert(key_combo, action);
}
pub fn remove_binding(&mut self, key_combo: &KeyCombo) -> Option<KeyAction> {
self.bindings.remove(key_combo)
}
pub fn add_custom_command(&mut self, name: String, command: String) {
self.custom_commands.insert(name, command);
}
pub fn get_custom_command(&self, name: &str) -> Option<&String> {
self.custom_commands.get(name)
}
pub fn enable_vim_mode(&mut self) {
self.vim_mode = true;
self.emacs_mode = false;
self.add_vim_bindings();
}
pub fn enable_emacs_mode(&mut self) {
self.emacs_mode = true;
self.vim_mode = false;
self.add_emacs_bindings();
}
fn add_vim_bindings(&mut self) {
self.bindings
.insert(KeyCombo::new("h"), KeyAction::MoveLeft);
self.bindings
.insert(KeyCombo::new("j"), KeyAction::MoveDown);
self.bindings.insert(KeyCombo::new("k"), KeyAction::MoveUp);
self.bindings
.insert(KeyCombo::new("l"), KeyAction::MoveRight);
self.bindings.insert(KeyCombo::new("0"), KeyAction::Home);
self.bindings.insert(KeyCombo::new("$"), KeyAction::End);
self.bindings.insert(KeyCombo::new("x"), KeyAction::Delete);
self.bindings
.insert(KeyCombo::new("i"), KeyAction::EnterCommandMode);
self.bindings
.insert(KeyCombo::new("d").shift(), KeyAction::ClearHistory);
self.bindings
.insert(KeyCombo::new("y").shift(), KeyAction::SaveSession);
}
fn add_emacs_bindings(&mut self) {
self.bindings
.insert(KeyCombo::new("b").ctrl(), KeyAction::MoveLeft);
self.bindings
.insert(KeyCombo::new("f").ctrl(), KeyAction::MoveRight);
self.bindings
.insert(KeyCombo::new("p").ctrl(), KeyAction::MoveUp);
self.bindings
.insert(KeyCombo::new("n").ctrl(), KeyAction::MoveDown);
self.bindings
.insert(KeyCombo::new("a").ctrl(), KeyAction::Home);
self.bindings
.insert(KeyCombo::new("e").ctrl(), KeyAction::End);
self.bindings
.insert(KeyCombo::new("d").ctrl(), KeyAction::Delete);
self.bindings
.insert(KeyCombo::new("k").ctrl(), KeyAction::Clear);
self.bindings
.insert(KeyCombo::new("x").ctrl().then("h"), KeyAction::ShowHelp);
self.bindings
.insert(KeyCombo::new("x").ctrl().then("s"), KeyAction::SaveSession);
}
pub fn list_bindings(&self) -> Vec<(String, String)> {
self.bindings
.iter()
.map(|(combo, action)| (combo.format(), format!("{:?}", action)))
.collect()
}
pub fn export_readable(&self) -> String {
let mut output = String::from("# OSVM Agent Chat Keybindings\n\n");
if self.vim_mode {
output.push_str("Mode: Vim\n\n");
} else if self.emacs_mode {
output.push_str("Mode: Emacs\n\n");
} else {
output.push_str("Mode: Default\n\n");
}
output.push_str("## Navigation\n");
output.push_str("| Key | Action |\n");
output.push_str("|-----|--------|\n");
for (combo, action) in &self.bindings {
if matches!(
action,
KeyAction::MoveUp
| KeyAction::MoveDown
| KeyAction::MoveLeft
| KeyAction::MoveRight
| KeyAction::PageUp
| KeyAction::PageDown
| KeyAction::Home
| KeyAction::End
) {
output.push_str(&format!("| {} | {:?} |\n", combo.format(), action));
}
}
output.push_str("\n## Commands\n");
output.push_str("| Key | Action |\n");
output.push_str("|-----|--------|\n");
for (combo, action) in &self.bindings {
if !matches!(
action,
KeyAction::MoveUp
| KeyAction::MoveDown
| KeyAction::MoveLeft
| KeyAction::MoveRight
| KeyAction::PageUp
| KeyAction::PageDown
| KeyAction::Home
| KeyAction::End
) {
output.push_str(&format!("| {} | {:?} |\n", combo.format(), action));
}
}
if !self.custom_commands.is_empty() {
output.push_str("\n## Custom Commands\n");
output.push_str("| Name | Command |\n");
output.push_str("|------|----------|\n");
for (name, command) in &self.custom_commands {
output.push_str(&format!("| {} | {} |\n", name, command));
}
}
output
}
}
pub struct KeybindingManager {
config: KeybindingConfig,
recording_macro: bool,
macro_sequence: Vec<KeyCombo>,
last_macro: Vec<KeyCombo>,
}
impl KeybindingManager {
pub fn new() -> Result<Self> {
let config = KeybindingConfig::load().unwrap_or_else(|e| {
warn!("Failed to load keybinding config: {}, using defaults", e);
KeybindingConfig::default()
});
Ok(Self {
config,
recording_macro: false,
macro_sequence: Vec::new(),
last_macro: Vec::new(),
})
}
pub fn process_key(&mut self, key_combo: KeyCombo) -> Option<KeyAction> {
if self.recording_macro {
self.macro_sequence.push(key_combo.clone());
}
if let Some(action) = self.config.get_action(&key_combo) {
let action_clone = action.clone();
self.handle_special_actions(action_clone.clone());
Some(action_clone)
} else {
None
}
}
fn handle_special_actions(&mut self, action: KeyAction) {
match action {
KeyAction::RecordMacro => {
if self.recording_macro {
self.recording_macro = false;
self.last_macro = self.macro_sequence.clone();
self.macro_sequence.clear();
debug!(
"Stopped macro recording, {} keys recorded",
self.last_macro.len()
);
} else {
self.recording_macro = true;
self.macro_sequence.clear();
debug!("Started macro recording");
}
}
KeyAction::PlayMacro => {
if !self.last_macro.is_empty() {
debug!("Playing macro with {} keys", self.last_macro.len());
}
}
_ => {}
}
}
pub fn config(&self) -> &KeybindingConfig {
&self.config
}
pub fn config_mut(&mut self) -> &mut KeybindingConfig {
&mut self.config
}
pub fn save_config(&self) -> Result<()> {
self.config.save()
}
pub fn reload_config(&mut self) -> Result<()> {
self.config = KeybindingConfig::load()?;
Ok(())
}
pub fn is_recording_macro(&self) -> bool {
self.recording_macro
}
pub fn last_macro(&self) -> &[KeyCombo] {
&self.last_macro
}
}
trait KeyComboExt {
fn then(self, key: &str) -> Self;
}
impl KeyComboExt for KeyCombo {
fn then(mut self, key: &str) -> Self {
self.key = format!("{} {}", self.key, key);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_combo_parsing() {
let combo = KeyCombo::parse("Ctrl+C").unwrap();
assert_eq!(combo.key, "C");
assert!(combo.ctrl);
assert!(!combo.alt);
let combo = KeyCombo::parse("Alt+Shift+F1").unwrap();
assert_eq!(combo.key, "F1");
assert!(combo.alt);
assert!(combo.shift);
assert!(!combo.ctrl);
}
#[test]
fn test_key_combo_formatting() {
let combo = KeyCombo::new("C").ctrl().alt();
assert_eq!(combo.format(), "Ctrl+Alt+C");
}
#[test]
fn test_default_config() {
let config = KeybindingConfig::default();
assert!(!config.bindings.is_empty());
assert!(config.bindings.contains_key(&KeyCombo::new("Enter")));
}
#[test]
fn test_keybinding_manager() {
let mut manager = KeybindingManager::new().unwrap();
let action = manager.process_key(KeyCombo::new("Enter"));
assert!(matches!(action, Some(KeyAction::Accept)));
}
#[test]
fn test_macro_recording() {
let mut manager = KeybindingManager::new().unwrap();
manager.process_key(KeyCombo::new("r").ctrl().shift());
assert!(manager.is_recording_macro());
manager.process_key(KeyCombo::new("a"));
manager.process_key(KeyCombo::new("b"));
manager.process_key(KeyCombo::new("r").ctrl().shift());
assert!(!manager.is_recording_macro());
assert_eq!(manager.last_macro().len(), 3); }
}