diff --git a/src/config.rs b/src/config.rs index aee666e..dcdba06 100644 --- a/src/config.rs +++ b/src/config.rs @@ -24,17 +24,22 @@ use crossbeam_channel::{unbounded, Receiver, RecvError, Sender}; use notify::{Event, EventKind, RecommendedWatcher, Watcher}; use parking_lot::Mutex; +use crate::keybinds::Keybinds; use crate::theme::{StyleStore, Theme}; pub struct Config { /// Whether or not to wrap lines when moving the cursor left and right. pub move_linewrap: bool, + + /// Key bindings. + pub keybinds: Keybinds, } impl Default for Config { fn default() -> Self { Self { move_linewrap: true, + keybinds: Default::default(), } } } diff --git a/src/keybinds.rs b/src/keybinds.rs new file mode 100644 index 0000000..e53e219 --- /dev/null +++ b/src/keybinds.rs @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2023 Marceline Cramer + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +use std::collections::HashMap; + +use crossterm::event::KeyCode; +use toml::{Table, Value}; + +use crate::actions::{load_actions, Action}; + +pub type Key = KeyCode; + +pub type KeyMap = HashMap; + +pub struct Keybinds { + pub normal: ModeKeys, + pub insert: ModeKeys, + pub visual: ModeKeys, +} + +impl Default for Keybinds { + fn default() -> Self { + use crate::actions::*; + use KeyCode::*; + + let basic_nav = &[ + (Esc, normal_mode as Action), + (Left, move_char_left), + (Down, move_line_down), + (Up, move_line_up), + (Right, move_char_right), + (PageUp, page_up), + (PageDown, page_down), + (Home, goto_line_start), + (End, goto_line_end_newline), + ]; + + let normalish_keys = &[ + (Char(':'), command_mode as Action), + (Char('h'), move_char_left), + (Char('j'), move_line_down), + (Char('k'), move_line_up), + (Char('l'), move_char_right), + (Char('i'), insert_mode), + (Char('a'), append_mode), + (Char('I'), insert_at_line_start), + (Char('A'), insert_at_line_end), + (Char('o'), open_below), + (Char('O'), open_above), + (Char('u'), undo), + (Char('U'), redo), + ] + .iter() + .chain(basic_nav); + + let insert_keys = &[ + (Backspace, delete_char_backward as Action), + (Delete, delete_char_forward), + (Enter, insert_newline), + ] + .iter() + .chain(basic_nav); + + Self { + normal: normalish_keys.clone().into(), + insert: insert_keys.clone().into(), + visual: normalish_keys.clone().into(), + } + } +} + +#[derive(Clone, Default)] +pub struct ModeKeys { + pub minor_modes: HashMap, + pub map: KeyMap, +} + +impl<'a, T> From for ModeKeys +where + T: IntoIterator, +{ + fn from(iter: T) -> Self { + let minor_modes = HashMap::new(); + let mut map = KeyMap::new(); + for (key, action) in iter { + map.insert(*key, Keybind::Action(*action)); + } + + Self { minor_modes, map } + } +} + +impl ModeKeys { + pub fn apply_table(values: Table) -> Result { + let mut keys = Self::default(); + + for (key, value) in values { + let key = parse_key(&key)?; + match value { + Value::Table(table) => { + let map = parse_key_map(table)?; + keys.minor_modes.insert(key, map); + } + Value::String(keybind) => { + let bind = Keybind::try_from(keybind.as_str())?; + keys.map.insert(key, bind); + } + value => return Err(format!("Expected string or table but got {:?}", value)), + } + } + + Ok(keys) + } +} + +#[derive(Clone)] +pub enum Keybind { + Action(Action), + Command(String), +} + +impl TryFrom for Keybind { + type Error = String; + + fn try_from(value: Value) -> Result { + value + .as_str() + .ok_or_else(|| format!("Expected string but got {:?}", value))? + .try_into() + } +} + +impl TryFrom<&str> for Keybind { + type Error = String; + + fn try_from(keybind: &str) -> Result { + if keybind.starts_with(':') { + Ok(Keybind::Command(keybind[1..].to_string())) + } else { + // TODO static once cell + let actions = load_actions(); + actions + .get(keybind) + .map(|cb| Keybind::Action(cb.clone())) + .ok_or_else(|| format!("Couldn't find action {:?}", keybind)) + } + } +} + +pub fn parse_key_map(values: Table) -> Result { + let mut map = KeyMap::with_capacity(values.len()); + + for (key, value) in values { + let key = parse_key(&key)?; + let bind = Keybind::try_from(value)?; + map.insert(key, bind); + } + + Ok(map) +} + +pub fn parse_key(key: &str) -> Result { + use KeyCode::*; + let key = match key { + key if key.len() == 1 => Char(key.chars().nth(0).unwrap()), + "backspace" => Backspace, + "space" => Char(' '), + "ret" => Enter, + "minus" => Char('-'), + "left" => Left, + "right" => Right, + "up" => Up, + "down" => Down, + "home" => Home, + "end" => End, + "pageup" => PageUp, + "pagedown" => PageDown, + "tab" => Tab, + "del" => Delete, + "ins" => Insert, + "null" => Null, + "esc" => Esc, + key => return Err(format!("Unrecognized key name {:?}", key)), + }; + + Ok(key) +} diff --git a/src/main.rs b/src/main.rs index 8dad9b4..b5cc612 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,7 @@ use crossterm::{ event::{Event, KeyCode, KeyEvent}, terminal, QueueableCommand, Result, }; +use keybinds::Keybind; use parking_lot::Mutex; use ropey::Rope; use yacexits::{exit, EX_DATAERR, EX_UNAVAILABLE}; @@ -40,9 +41,11 @@ use yacexits::{exit, EX_DATAERR, EX_UNAVAILABLE}; mod actions; mod buffer; mod config; +mod keybinds; mod theme; use buffer::Buffer; +use config::Config; use theme::StyleStore; #[derive(Copy, Clone, Debug, Default)] @@ -103,6 +106,7 @@ pub enum Direction { pub struct State { pub styles: Arc>, + pub config: Config, pub buffer: Buffer, pub cursor: Cursor, pub file: Option, @@ -124,6 +128,7 @@ impl State { Ok(Self { styles, + config: Default::default(), buffer, cursor: Cursor::default(), file: file_name, @@ -170,7 +175,9 @@ impl State { // draw buffer let buffer_rows = if show_status_bar { rows - 1 } else { rows }; - let lr_width = self.buffer.draw(cols, buffer_rows, self.scroll, self.cursor, out)?; + let lr_width = self + .buffer + .draw(cols, buffer_rows, self.scroll, self.cursor, out)?; // draw cursor let cursor_pos = set_cursor_pos.unwrap_or_else(|| { @@ -211,28 +218,16 @@ impl State { fn on_normal_event(&mut self, event: Event, mut state: NormalState) { // reset the error from the last event state.error = None; + self.mode = Mode::Normal(state); match event { - Event::Key(KeyEvent { code, .. }) => match code { - KeyCode::Char(':') => { - self.mode = Mode::Command(Default::default()); + Event::Key(KeyEvent { code, .. }) => { + if let Some(keybind) = self.config.keybinds.visual.map.get(&code) { + self.execute_keybind(keybind.clone()); } - KeyCode::Char('v') => { - self.mode = Mode::Visual; - } - KeyCode::Char('i') => actions::insert_mode(self), - KeyCode::Char('a') => actions::append_mode(self), - KeyCode::Char('o') => actions::open_below(self), - KeyCode::Char('O') => actions::open_above(self), - code => self.on_any_key(code), - }, + } event => self.on_any_event(event), } - - match self.mode { - Mode::Normal(_) => self.mode = Mode::Normal(state), - _ => {} - } } fn on_command_event(&mut self, event: Event, mut state: CommandState) { @@ -264,7 +259,11 @@ impl State { self.mode = Mode::Normal(NormalState { error }); return; } - code => return self.on_any_key(code), + KeyCode::Esc => { + self.mode = Mode::default(); + return; + } + _ => {} }, event => return self.on_any_event(event), } @@ -274,43 +273,25 @@ impl State { fn on_visual_event(&mut self, event: Event) { match event { - Event::Key(KeyEvent { code, .. }) => match code { - KeyCode::Esc => { - self.mode = Mode::default(); + Event::Key(KeyEvent { code, .. }) => { + if let Some(keybind) = self.config.keybinds.visual.map.get(&code) { + self.execute_keybind(keybind.clone()); } - code => self.on_any_key(code), - }, + } event => self.on_any_event(event), } } - fn on_insert_event(&mut self, event: Event, state: InsertState) { + fn on_insert_event(&mut self, event: Event, _state: InsertState) { match event { - Event::Key(KeyEvent { code, .. }) => match code { - KeyCode::Char(c) => { - self.buffer.insert_char(self.cursor, c); - self.move_cursor(Direction::Right) - } - KeyCode::Backspace => { - self.move_cursor(Direction::Left); - self.buffer.remove(self.cursor); - } - KeyCode::Delete => { - self.buffer.remove(self.cursor); - } - KeyCode::Enter => { - self.buffer.insert_char(self.cursor, '\n'); - self.cursor.line += 1; - self.cursor.column = 0; - } - KeyCode::Esc => { - if state.append { - self.move_cursor(Direction::Left); + Event::Key(KeyEvent { code, .. }) => match self.config.keybinds.insert.map.get(&code) { + Some(keybind) => self.execute_keybind(keybind.clone()), + None => { + if let KeyCode::Char(c) = code { + self.buffer.insert_char(self.cursor, c); + self.move_cursor(Direction::Right); } - - self.mode = Mode::default(); } - code => self.on_any_key(code), }, event => self.on_any_event(event), } @@ -321,18 +302,6 @@ impl State { Event::Resize(cols, rows) => { self.size = (cols as usize, rows as usize); } - Event::Key(KeyEvent { code, .. }) => self.on_any_key(code), - _ => {} - } - } - - fn on_any_key(&mut self, code: KeyCode) { - match code { - KeyCode::Esc => self.mode = Mode::default(), - KeyCode::Char('h') | KeyCode::Left => actions::move_char_left(self), - KeyCode::Char('j') | KeyCode::Down => actions::move_line_down(self), - KeyCode::Char('k') | KeyCode::Up => actions::move_line_up(self), - KeyCode::Char('l') | KeyCode::Right => actions::move_char_right(self), _ => {} } } @@ -359,7 +328,19 @@ impl State { self.write_buffer(handle).map_err(|err| format!("{}", err)) } - fn execute_command(mut self, command: &str) -> std::result::Result<(), String> { + fn execute_keybind(&mut self, keybind: Keybind) { + match keybind { + Keybind::Action(action) => action(self), + Keybind::Command(command) => { + let result = self.execute_command(&command); + let error = result.err(); + self.mode = Mode::Normal(NormalState { error }); + return; + } + } + } + + fn execute_command(&mut self, command: &str) -> std::result::Result<(), String> { let command_parts = command.split(' ').collect::>(); let command = match command_parts.get(0) {