Add simple keybinds system
This commit is contained in:
parent
18604f3ddd
commit
5f1e76e912
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Key, Keybind>;
|
||||
|
||||
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<Key, KeyMap>,
|
||||
pub map: KeyMap,
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for ModeKeys
|
||||
where
|
||||
T: IntoIterator<Item = &'a (Key, Action)>,
|
||||
{
|
||||
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<Self, String> {
|
||||
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<Value> for Keybind {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: Value) -> Result<Self, String> {
|
||||
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<Self, String> {
|
||||
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<KeyMap, String> {
|
||||
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<Key, String> {
|
||||
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)
|
||||
}
|
103
src/main.rs
103
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<Mutex<StyleStore>>,
|
||||
pub config: Config,
|
||||
pub buffer: Buffer,
|
||||
pub cursor: Cursor,
|
||||
pub file: Option<OsString>,
|
||||
|
@ -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::<Vec<&str>>();
|
||||
|
||||
let command = match command_parts.get(0) {
|
||||
|
|
Loading…
Reference in New Issue