Read config.toml, keys.toml and themes, and open them with :config (stage 8b)
The config folder now sets everything: config.toml the settings (now also `theme` and `editor`), keys.toml the keys, and themes/<name>.toml the theme, laid over the built-in one, so a theme only needs what it changes. Each file is applied again a second after it's saved; a mistake only drops its own line, and the status bar says what's wrong, naming the file. :config lists the three files with their folder; Enter hands the terminal to your editor (the editor setting, $VISUAL, $EDITOR, else notepad or vi), writing the file first if it's missing, with every option at its default behind a #, explained (keys.toml lists every command by name). :theme switches themes until cumulus quits, like :set. The built-in theme gets the named styles the user picked, each also put on by its tag: question (yellow, dashed), important (red, thick) and done (gray, its title faded); and link styles blocks and supports, whose label shows on a link without its own. Themes can also set [modes], [node], [root], [branch] and [link]. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f6dc4564aa
commit
abe3dc1d84
26 files changed
+1683
-129
No files matched your search
+175
-3
@@ -1,6 +1,178 @@
|
||||
//! Where cumulus keeps its files ([`paths`]), and every setting it has
|
||||
//! ([`settings`]). Reading config.toml, keys.toml and theme files comes with
|
||||
//! part 8b of the build.
|
||||
//! Where cumulus keeps its files ([`paths`]), and the files in its config
|
||||
//! folder: config.toml ([`settings`]), keys.toml ([`keys_file`]) and themes
|
||||
//! ([`theme_file`]). Each one only states what it changes; a missing file
|
||||
//! changes nothing, and a mistake only drops the line it's on, with a
|
||||
//! message saying what's wrong. cumulus applies a file again when it's
|
||||
//! saved.
|
||||
|
||||
pub mod keys_file;
|
||||
pub mod paths;
|
||||
pub mod settings;
|
||||
pub mod theme_file;
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use paths::Paths;
|
||||
use settings::Settings;
|
||||
|
||||
use crate::input::keymap::{DEFAULT_LEADER, Keymap};
|
||||
use crate::ui::theme::Theme;
|
||||
|
||||
/// One of the config folder's files.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConfigFile {
|
||||
/// config.toml: the settings.
|
||||
Settings,
|
||||
/// keys.toml: your keys.
|
||||
Keys,
|
||||
/// The theme's file, `themes/<name>.toml`.
|
||||
Theme,
|
||||
}
|
||||
|
||||
impl ConfigFile {
|
||||
pub const ALL: [ConfigFile; 3] = [ConfigFile::Settings, ConfigFile::Keys, ConfigFile::Theme];
|
||||
|
||||
/// Where it is, for the theme named `theme`.
|
||||
pub fn path(self, paths: &Paths, theme: &str) -> PathBuf {
|
||||
match self {
|
||||
ConfigFile::Settings => paths.config.join("config.toml"),
|
||||
ConfigFile::Keys => paths.config.join("keys.toml"),
|
||||
ConfigFile::Theme => theme_file::path(paths, theme),
|
||||
}
|
||||
}
|
||||
|
||||
/// What's in it, in a few words.
|
||||
pub fn about(self) -> &'static str {
|
||||
match self {
|
||||
ConfigFile::Settings => "the settings",
|
||||
ConfigFile::Keys => "your keys",
|
||||
ConfigFile::Theme => "the theme",
|
||||
}
|
||||
}
|
||||
|
||||
/// The file as `:config` first writes it: everything it can hold, each
|
||||
/// at its default, commented out.
|
||||
pub fn template(self) -> String {
|
||||
match self {
|
||||
ConfigFile::Settings => Settings::template(),
|
||||
ConfigFile::Keys => keys_file::template(),
|
||||
ConfigFile::Theme => theme_file::template(&Theme::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything the config folder sets.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub settings: Settings,
|
||||
pub keymap: Keymap,
|
||||
pub theme: Theme,
|
||||
/// What was wrong in the files, in words.
|
||||
pub problems: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
/// No files: everything as cumulus has it.
|
||||
fn default() -> Config {
|
||||
Config {
|
||||
settings: Settings::default(),
|
||||
keymap: Keymap::new(DEFAULT_LEADER),
|
||||
theme: Theme::default(),
|
||||
problems: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Reads the config folder: config.toml, keys.toml, and the theme
|
||||
/// config.toml names.
|
||||
pub fn load(paths: &Paths) -> Config {
|
||||
let (settings, mut problems) = read(&ConfigFile::Settings.path(paths, ""))
|
||||
.map_or_else(|| (Settings::default(), Vec::new()), |text| Settings::from_toml(&text));
|
||||
let (keymap, keys_problems) = keys(paths);
|
||||
problems.extend(keys_problems);
|
||||
let (theme, theme_problems) = theme_file::load(paths, &settings.theme, settings.glyphs);
|
||||
problems.extend(theme_problems);
|
||||
Config { settings, keymap, theme, problems }
|
||||
}
|
||||
}
|
||||
|
||||
/// The keys: keys.toml on top of the defaults.
|
||||
pub fn keys(paths: &Paths) -> (Keymap, Vec<String>) {
|
||||
match read(&ConfigFile::Keys.path(paths, "")) {
|
||||
Some(text) => {
|
||||
let (keymap, problems) = Keymap::from_toml(&text);
|
||||
(keymap, problems.into_iter().map(|problem| format!("keys.toml: {problem}")).collect())
|
||||
}
|
||||
None => (Keymap::new(DEFAULT_LEADER), Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A config file's text, when there's a file.
|
||||
fn read(path: &std::path::Path) -> Option<String> {
|
||||
fs::read_to_string(path).ok()
|
||||
}
|
||||
|
||||
/// Text as comment lines for a config file: `# ` before each, and at most
|
||||
/// 76 columns.
|
||||
pub(crate) fn comment(text: &str) -> String {
|
||||
let mut lines: Vec<String> = Vec::new();
|
||||
for word in text.split_whitespace() {
|
||||
match lines.last_mut() {
|
||||
Some(line) if line.len() + 1 + word.len() <= 76 => {
|
||||
line.push(' ');
|
||||
line.push_str(word);
|
||||
}
|
||||
_ => lines.push(format!("# {word}")),
|
||||
}
|
||||
}
|
||||
lines.iter().map(|line| format!("{line}\n")).collect()
|
||||
}
|
||||
|
||||
/// Text that doesn't read as TOML, in one line: where it went wrong, and
|
||||
/// what's wrong there.
|
||||
pub fn toml_problem(text: &str, error: &toml::de::Error) -> String {
|
||||
match error.span() {
|
||||
Some(span) => {
|
||||
let before = text.char_indices().take_while(|&(at, _)| at < span.start);
|
||||
let line = before.filter(|&(_, c)| c == '\n').count() + 1;
|
||||
format!("line {line}: {}", error.message().trim())
|
||||
}
|
||||
None => error.message().trim().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::storage::test_support::TempDir;
|
||||
use crate::ui::glyphs::Glyphs;
|
||||
|
||||
#[test]
|
||||
fn the_folder_sets_settings_keys_and_theme() {
|
||||
let folder = TempDir::new();
|
||||
let paths = Paths::inside(folder.path());
|
||||
let config = Config::load(&paths);
|
||||
assert!(config.problems.is_empty());
|
||||
assert_eq!(config.settings, Settings::default());
|
||||
|
||||
fs::create_dir_all(paths.config.join("themes")).unwrap();
|
||||
fs::write(paths.config.join("config.toml"), "theme = \"night\"\nglyphs = \"ascii\"\n").unwrap();
|
||||
fs::write(paths.config.join("keys.toml"), "[normal]\n\"Q\" = \"close\"\n\"x\" = \"frobnicate\"\n")
|
||||
.unwrap();
|
||||
fs::write(paths.config.join("themes").join("night.toml"), "accent = \"magenta\"\n").unwrap();
|
||||
let config = Config::load(&paths);
|
||||
assert_eq!(config.theme.accent, ratatui::style::Color::Magenta);
|
||||
assert_eq!(config.theme.glyphs, Glyphs::Ascii, "the theme draws with the glyphs setting");
|
||||
assert_eq!(config.problems.len(), 1, "{:?}", config.problems);
|
||||
assert!(config.problems[0].contains("frobnicate"), "{:?}", config.problems);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comments_wrap_at_76_columns() {
|
||||
let text = comment(&"word ".repeat(30));
|
||||
assert!(text.lines().all(|line| line.starts_with("# ") && line.len() <= 76), "{text}");
|
||||
assert_eq!(text.split_whitespace().filter(|word| *word == "word").count(), 30);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user