diff --git a/Cargo.lock b/Cargo.lock index cab7774..37a34b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,9 @@ version = "0.1.0" dependencies = [ "arg", "crossterm", + "once_cell", "ropey", + "toml", "yacexits", ] @@ -129,6 +131,22 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -309,6 +327,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "serde" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" + +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde", +] + [[package]] name = "shlex" version = "1.1.0" @@ -377,6 +410,40 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "toml" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.8" @@ -494,6 +561,15 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "winnow" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] + [[package]] name = "yacexits" version = "0.1.5" diff --git a/Cargo.toml b/Cargo.toml index 9dc3429..45438d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,7 @@ license = "AGPL-3.0-or-later" [dependencies] arg = "0.4.1" crossterm = "0.26" +once_cell = "1.17" ropey = "1.6" +toml = "0.7" yacexits = "0.1.5" diff --git a/default_theme.toml b/default_theme.toml new file mode 100644 index 0000000..be3ce26 --- /dev/null +++ b/default_theme.toml @@ -0,0 +1,3 @@ +"ui.linenr" = "gray" + +"error" = "red" diff --git a/src/main.rs b/src/main.rs index ea9a540..c814eaa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,12 +20,7 @@ use std::{ env::args, fs::File, - io::{ - Read, - stdout, - Stdout, - Write, - }, + io::{stdout, Read, Stdout, Write}, os::fd::FromRawFd, }; @@ -35,7 +30,11 @@ use crossterm::{ terminal, ExecutableCommand, Result, }; use ropey::Rope; -use yacexits::{ exit, EX_DATAERR, EX_UNAVAILABLE }; +use yacexits::{exit, EX_DATAERR, EX_UNAVAILABLE}; + +mod theme; + +use theme::StyleStore; struct Buffer { pub text: Rope, @@ -50,13 +49,15 @@ impl Buffer { pub fn draw( &self, + styles: &mut StyleStore, cols: u16, rows: u16, scroll: Cursor, out: &mut (impl ExecutableCommand + Write), ) -> Result { - let lr_width = self.text.len_lines().ilog10() + 1; - let gutter_width = lr_width + 1; + let linenr_style = styles.get_scope("ui.linenr"); + let linenr_width = self.text.len_lines().ilog10() + 1; + let gutter_width = linenr_width + 1; let text_width = cols as usize - gutter_width as usize; out.execute(cursor::MoveTo(0, 0))?; @@ -68,7 +69,8 @@ impl Buffer { } let row = row as usize + scroll.line; - write!(out, "{:width$} ", row, width = lr_width as usize)?; + let linenr = format!("{:width$} ", row, width = linenr_width as usize); + linenr_style.print_styled(out, &linenr)?; let lhs = scroll.column; let width = line.len_chars().saturating_sub(1); // lop off whitespace @@ -192,6 +194,7 @@ enum Direction { } struct State { + pub style_store: StyleStore, pub buffer: Buffer, pub cursor: Cursor, pub scroll: Cursor, @@ -205,6 +208,7 @@ impl State { let (cols, rows) = terminal::size()?; Ok(Self { + style_store: StyleStore::default(), buffer: Buffer::from_str(text), cursor: Cursor::default(), scroll: Cursor::default(), @@ -214,7 +218,7 @@ impl State { }) } - pub fn draw(&self, out: &mut impl Write) -> Result<()> { + pub fn draw(&mut self, out: &mut impl Write) -> Result<()> { // begin update let (cols, rows) = terminal::size()?; out.execute(terminal::BeginSynchronizedUpdate)?; @@ -234,8 +238,9 @@ impl State { show_status_bar = true; } Mode::Normal(NormalState { error: Some(error) }) => { + let error_style = self.style_store.get_scope("error"); out.execute(cursor::MoveTo(0, rows - 1))?; - write!(out, "{}", error)?; + error_style.print_styled(out, error)?; show_status_bar = true; } _ => {} @@ -243,7 +248,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, out)?; + let lr_width = + self.buffer + .draw(&mut self.style_store, cols, buffer_rows, self.scroll, out)?; // draw cursor let cursor_pos = set_cursor_pos.unwrap_or_else(|| { @@ -452,21 +459,18 @@ fn main() -> Result<()> { match argv.get(1).map(|s| s.as_str()) { Some("-") | None => unsafe { File::from_raw_fd(0) }, // stdin as a file - Some(path) => { - std::fs::File::open(path).unwrap_or_else(|_| { - eprintln!( - "{}: {}: No such file or directory.", - argv[0], - argv[1] - ); - exit(EX_UNAVAILABLE); - }) - }, - }.read_to_end(&mut buf).unwrap(); + Some(path) => std::fs::File::open(path).unwrap_or_else(|_| { + eprintln!("{}: {}: No such file or directory.", argv[0], argv[1]); + exit(EX_UNAVAILABLE); + }), + } + .read_to_end(&mut buf) + .unwrap(); let text = String::from_utf8(buf).unwrap_or_else(|_| { eprintln!( - "{}: {}: File contents are not valid UTF-8.", argv[0], argv[1] + "{}: {}: File contents are not valid UTF-8.", + argv[0], argv[1] ); exit(EX_DATAERR); }); diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..5235321 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2023 Marceline Cramer + * Copyright (c) 2023 Emma Tebibyte + * 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 std::io::Write; + +use crossterm::{style::Color, ExecutableCommand}; +use once_cell::sync::Lazy; +use toml::{map::Map, Value}; + +pub static DEFAULT_THEME: Lazy = Lazy::new(|| { + let text = include_str!("../default_theme.toml"); + let value = toml::from_str(text).expect("Failed to parse default theme"); + Theme::from_value("default", value).expect("Failed to load default theme") +}); + +#[derive(Debug)] +pub struct StyleStore { + /// The current theme. + theme: Theme, + + /// The current generation of the store. Increments every update. + generation: usize, + + /// Maps named scopes to indices. + scopes: HashMap, + + /// The style data store. + styles: Vec