mod command;
mod cursor;
mod editor;
mod keymap;
mod keys;
mod languages;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, anyhow, bail};
use crossterm::event::KeyCode;
use serde::Deserialize;
pub use command::{COMMAND_BINDS, CommandBind};
pub use cursor::{CursorShape, CursorShapes};
pub use editor::{EditorConfig, EditorToml};
pub use keymap::{
GOTO_BINDINGS, KeySig, Keymap, LEADER_DEFAULTS, OBJECT_BINDINGS, OP_PENDING_BINDINGS,
Z_BINDINGS,
};
pub use languages::{
FormatterConfig, Language, LanguageConfig, LanguageRegistry, LspConfig, LspToml,
};
use cursor::{CursorConfig, resolve_cursor_shapes};
use keymap::LEADER;
use keys::{action_to_token, parse_sequence};
pub struct Config {
pub keymap: Keymap,
pub cursor_shapes: CursorShapes,
pub languages: LanguageRegistry,
pub editor: EditorConfig,
pub grammar_dir: PathBuf,
pub query_dir: PathBuf,
}
impl Config {
pub fn load(path: Option<&Path>) -> Result<Self> {
let toml = Toml::load(path)?;
Self::resolve(toml)
}
fn resolve(toml: Toml) -> Result<Self> {
let mut keymap = Keymap::vim_default();
for (i, b) in toml.bind.iter().enumerate() {
install_binding(&mut keymap, &b.keys, &b.action)
.with_context(|| format!("bind[{}] ({} → {})", i, b.keys, b.action))?;
}
let cursor_shapes = resolve_cursor_shapes(&toml.cursor)?;
let editor = EditorConfig::default().overlay(&toml.editor);
let languages = LanguageRegistry::build(toml.languages, toml.lsp)?;
let grammar_dir = toml
.grammar_dir
.map(PathBuf::from)
.unwrap_or_else(|| default_subdir("grammars"));
let query_dir = toml
.query_dir
.map(PathBuf::from)
.unwrap_or_else(|| default_subdir("queries"));
Ok(Self {
keymap,
cursor_shapes,
languages,
editor,
grammar_dir,
query_dir,
})
}
}
#[derive(Debug, Default, Deserialize)]
struct Toml {
#[serde(default)]
bind: Vec<BindEntry>,
#[serde(default)]
cursor: CursorConfig,
#[serde(default)]
editor: EditorToml,
#[serde(default)]
languages: std::collections::HashMap<String, LanguageConfig>,
#[serde(default)]
lsp: std::collections::HashMap<String, LspToml>,
grammar_dir: Option<String>,
query_dir: Option<String>,
}
impl Toml {
fn load(path: Option<&Path>) -> Result<Self> {
let Some(path) = path else {
return Ok(Self::default());
};
if !path.exists() {
return Ok(Self::default());
}
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading config {}", path.display()))?;
toml::from_str(&text).with_context(|| format!("parsing config {}", path.display()))
}
}
#[derive(Debug, Deserialize)]
struct BindEntry {
keys: String,
action: String,
}
pub fn default_path() -> Option<PathBuf> {
if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") {
let p = PathBuf::from(xdg).join("vorto/config.toml");
if p.exists() {
return Some(p);
}
}
let home = std::env::var_os("HOME")?;
let p = PathBuf::from(home).join(".config/vorto/config.toml");
Some(p)
}
fn default_subdir(name: &str) -> PathBuf {
if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") {
return PathBuf::from(xdg).join("vorto").join(name);
}
if let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home).join(".config/vorto").join(name);
}
PathBuf::from(name)
}
fn install_binding(keymap: &mut Keymap, keys: &str, action: &str) -> Result<()> {
let sequence = parse_sequence(keys)?;
let token = action_to_token(action).ok_or_else(|| anyhow!("unknown action: {}", action))?;
match sequence.as_slice() {
[k] => {
keymap.bind_initial(*k, token);
}
[first, second] if first.code == KeyCode::Char(LEADER) && first.modifiers.is_empty() => {
keymap.bind_leader(*second, token);
}
[_, _] => bail!(
"only `<space>X` two-key sequences are supported; got: {}",
keys
),
_ => bail!(
"sequences of more than 2 keys aren't supported yet; got: {}",
keys
),
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::action::{DirectKind, Token};
use crossterm::event::KeyModifiers;
#[test]
fn install_leader_binding() {
let mut km = Keymap::vim_default();
install_binding(&mut km, "<space>w", "save").unwrap();
let sig = KeySig::new(KeyCode::Char('w'), KeyModifiers::NONE);
assert_eq!(km.leader.get(&sig), Some(&Token::Direct(DirectKind::Save)));
}
#[test]
fn install_overrides_existing() {
let mut km = Keymap::vim_default();
install_binding(&mut km, "u", "quit").unwrap();
let sig = KeySig::new(KeyCode::Char('u'), KeyModifiers::NONE);
assert_eq!(km.initial.get(&sig), Some(&Token::Direct(DirectKind::Quit)));
}
#[test]
fn parse_inline_array_form() {
let text = r#"
bind = [
{ keys = "<C-s>", action = "save" },
{ keys = "<space>w", action = "save" },
]
"#;
let toml: Toml = toml::from_str(text).unwrap();
assert_eq!(toml.bind.len(), 2);
assert_eq!(toml.bind[0].keys, "<C-s>");
assert_eq!(toml.bind[1].action, "save");
}
#[test]
fn cursor_defaults_when_unset() {
let toml: Toml = toml::from_str("").unwrap();
let shapes = resolve_cursor_shapes(&toml.cursor).unwrap();
assert!(matches!(shapes.normal, CursorShape::Block));
assert!(matches!(shapes.insert, CursorShape::Bar));
assert!(matches!(shapes.visual, CursorShape::Underbar));
}
#[test]
fn cursor_overrides() {
let text = r#"
[cursor]
normal = "bar"
insert = "underbar"
visual = "block"
"#;
let toml: Toml = toml::from_str(text).unwrap();
let shapes = resolve_cursor_shapes(&toml.cursor).unwrap();
assert!(matches!(shapes.normal, CursorShape::Bar));
assert!(matches!(shapes.insert, CursorShape::Underbar));
assert!(matches!(shapes.visual, CursorShape::Block));
}
#[test]
fn cursor_unknown_shape() {
let text = r#"
[cursor]
normal = "diamond"
"#;
let toml: Toml = toml::from_str(text).unwrap();
assert!(resolve_cursor_shapes(&toml.cursor).is_err());
}
#[test]
fn parse_languages_table() {
let text = r#"
[languages.rust]
extensions = ["rs", "rlib"]
[languages.fish]
extensions = ["fish"]
grammar = "fish-shell"
"#;
let toml: Toml = toml::from_str(text).unwrap();
assert_eq!(toml.languages.len(), 2);
assert_eq!(
toml.languages["rust"].extensions.as_deref(),
Some(&["rs".to_string(), "rlib".to_string()][..])
);
assert_eq!(
toml.languages["fish"].grammar.as_deref(),
Some("fish-shell")
);
}
#[test]
fn parse_table_array_form() {
let text = r#"
[[bind]]
keys = "<C-s>"
action = "save"
[[bind]]
keys = "<space>w"
action = "save"
"#;
let toml: Toml = toml::from_str(text).unwrap();
assert_eq!(toml.bind.len(), 2);
assert_eq!(toml.bind[0].keys, "<C-s>");
}
}