forked from mars/breed
Add basic theming
This commit is contained in:
parent
d7f8ccad2a
commit
1f9ba13620
76
Cargo.lock
generated
76
Cargo.lock
generated
|
@ -62,7 +62,9 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arg",
|
"arg",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
"once_cell",
|
||||||
"ropey",
|
"ropey",
|
||||||
|
"toml",
|
||||||
"yacexits",
|
"yacexits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -129,6 +131,22 @@ version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
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]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -309,6 +327,21 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
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]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -377,6 +410,40 @@ dependencies = [
|
||||||
"unicode-width",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.8"
|
version = "1.0.8"
|
||||||
|
@ -494,6 +561,15 @@ version = "0.42.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yacexits"
|
name = "yacexits"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
|
|
@ -7,5 +7,7 @@ license = "AGPL-3.0-or-later"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
arg = "0.4.1"
|
arg = "0.4.1"
|
||||||
crossterm = "0.26"
|
crossterm = "0.26"
|
||||||
|
once_cell = "1.17"
|
||||||
ropey = "1.6"
|
ropey = "1.6"
|
||||||
|
toml = "0.7"
|
||||||
yacexits = "0.1.5"
|
yacexits = "0.1.5"
|
||||||
|
|
3
default_theme.toml
Normal file
3
default_theme.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
"ui.linenr" = "gray"
|
||||||
|
|
||||||
|
"error" = "red"
|
54
src/main.rs
54
src/main.rs
|
@ -20,12 +20,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
env::args,
|
env::args,
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{
|
io::{stdout, Read, Stdout, Write},
|
||||||
Read,
|
|
||||||
stdout,
|
|
||||||
Stdout,
|
|
||||||
Write,
|
|
||||||
},
|
|
||||||
os::fd::FromRawFd,
|
os::fd::FromRawFd,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -35,7 +30,11 @@ use crossterm::{
|
||||||
terminal, ExecutableCommand, Result,
|
terminal, ExecutableCommand, Result,
|
||||||
};
|
};
|
||||||
use ropey::Rope;
|
use ropey::Rope;
|
||||||
use yacexits::{ exit, EX_DATAERR, EX_UNAVAILABLE };
|
use yacexits::{exit, EX_DATAERR, EX_UNAVAILABLE};
|
||||||
|
|
||||||
|
mod theme;
|
||||||
|
|
||||||
|
use theme::StyleStore;
|
||||||
|
|
||||||
struct Buffer {
|
struct Buffer {
|
||||||
pub text: Rope,
|
pub text: Rope,
|
||||||
|
@ -50,13 +49,15 @@ impl Buffer {
|
||||||
|
|
||||||
pub fn draw(
|
pub fn draw(
|
||||||
&self,
|
&self,
|
||||||
|
styles: &mut StyleStore,
|
||||||
cols: u16,
|
cols: u16,
|
||||||
rows: u16,
|
rows: u16,
|
||||||
scroll: Cursor,
|
scroll: Cursor,
|
||||||
out: &mut (impl ExecutableCommand + Write),
|
out: &mut (impl ExecutableCommand + Write),
|
||||||
) -> Result<u32> {
|
) -> Result<u32> {
|
||||||
let lr_width = self.text.len_lines().ilog10() + 1;
|
let linenr_style = styles.get_scope("ui.linenr");
|
||||||
let gutter_width = lr_width + 1;
|
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;
|
let text_width = cols as usize - gutter_width as usize;
|
||||||
|
|
||||||
out.execute(cursor::MoveTo(0, 0))?;
|
out.execute(cursor::MoveTo(0, 0))?;
|
||||||
|
@ -68,7 +69,8 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
let row = row as usize + scroll.line;
|
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 lhs = scroll.column;
|
||||||
let width = line.len_chars().saturating_sub(1); // lop off whitespace
|
let width = line.len_chars().saturating_sub(1); // lop off whitespace
|
||||||
|
@ -192,6 +194,7 @@ enum Direction {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
|
pub style_store: StyleStore,
|
||||||
pub buffer: Buffer,
|
pub buffer: Buffer,
|
||||||
pub cursor: Cursor,
|
pub cursor: Cursor,
|
||||||
pub scroll: Cursor,
|
pub scroll: Cursor,
|
||||||
|
@ -205,6 +208,7 @@ impl State {
|
||||||
let (cols, rows) = terminal::size()?;
|
let (cols, rows) = terminal::size()?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
style_store: StyleStore::default(),
|
||||||
buffer: Buffer::from_str(text),
|
buffer: Buffer::from_str(text),
|
||||||
cursor: Cursor::default(),
|
cursor: Cursor::default(),
|
||||||
scroll: 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
|
// begin update
|
||||||
let (cols, rows) = terminal::size()?;
|
let (cols, rows) = terminal::size()?;
|
||||||
out.execute(terminal::BeginSynchronizedUpdate)?;
|
out.execute(terminal::BeginSynchronizedUpdate)?;
|
||||||
|
@ -234,8 +238,9 @@ impl State {
|
||||||
show_status_bar = true;
|
show_status_bar = true;
|
||||||
}
|
}
|
||||||
Mode::Normal(NormalState { error: Some(error) }) => {
|
Mode::Normal(NormalState { error: Some(error) }) => {
|
||||||
|
let error_style = self.style_store.get_scope("error");
|
||||||
out.execute(cursor::MoveTo(0, rows - 1))?;
|
out.execute(cursor::MoveTo(0, rows - 1))?;
|
||||||
write!(out, "{}", error)?;
|
error_style.print_styled(out, error)?;
|
||||||
show_status_bar = true;
|
show_status_bar = true;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -243,7 +248,9 @@ impl State {
|
||||||
|
|
||||||
// draw buffer
|
// draw buffer
|
||||||
let buffer_rows = if show_status_bar { rows - 1 } else { rows };
|
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
|
// draw cursor
|
||||||
let cursor_pos = set_cursor_pos.unwrap_or_else(|| {
|
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()) {
|
match argv.get(1).map(|s| s.as_str()) {
|
||||||
Some("-") | None => unsafe { File::from_raw_fd(0) }, // stdin as a file
|
Some("-") | None => unsafe { File::from_raw_fd(0) }, // stdin as a file
|
||||||
Some(path) => {
|
Some(path) => std::fs::File::open(path).unwrap_or_else(|_| {
|
||||||
std::fs::File::open(path).unwrap_or_else(|_| {
|
eprintln!("{}: {}: No such file or directory.", argv[0], argv[1]);
|
||||||
eprintln!(
|
exit(EX_UNAVAILABLE);
|
||||||
"{}: {}: No such file or directory.",
|
}),
|
||||||
argv[0],
|
}
|
||||||
argv[1]
|
.read_to_end(&mut buf)
|
||||||
);
|
.unwrap();
|
||||||
exit(EX_UNAVAILABLE);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}.read_to_end(&mut buf).unwrap();
|
|
||||||
|
|
||||||
let text = String::from_utf8(buf).unwrap_or_else(|_| {
|
let text = String::from_utf8(buf).unwrap_or_else(|_| {
|
||||||
eprintln!(
|
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);
|
exit(EX_DATAERR);
|
||||||
});
|
});
|
||||||
|
|
311
src/theme.rs
Normal file
311
src/theme.rs
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Marceline Cramer
|
||||||
|
* Copyright (c) 2023 Emma Tebibyte <emma@tebibyte.media>
|
||||||
|
* 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<Theme> = 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<String, usize>,
|
||||||
|
|
||||||
|
/// The style data store.
|
||||||
|
styles: Vec<Style>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StyleStore {
|
||||||
|
fn default() -> Self {
|
||||||
|
let theme = Lazy::force(&DEFAULT_THEME).to_owned();
|
||||||
|
Self::new(theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyleStore {
|
||||||
|
/// Creates a new style store with a given theme.
|
||||||
|
pub fn new(theme: Theme) -> Self {
|
||||||
|
Self {
|
||||||
|
theme,
|
||||||
|
generation: 0,
|
||||||
|
scopes: HashMap::new(),
|
||||||
|
styles: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a scope to this store and returns its index. Reuses indices for existing scopes.
|
||||||
|
pub fn add_scope(&mut self, scope: &str) -> usize {
|
||||||
|
let style = self.theme.get_scope_style(scope);
|
||||||
|
|
||||||
|
if let Some(style_idx) = self.scopes.get(scope).copied() {
|
||||||
|
let old_style = self
|
||||||
|
.styles
|
||||||
|
.get_mut(style_idx)
|
||||||
|
.expect("StyleStore index is out-of-bounds");
|
||||||
|
let _ = std::mem::replace(old_style, style);
|
||||||
|
style_idx
|
||||||
|
} else {
|
||||||
|
let style_idx = self.styles.len();
|
||||||
|
self.styles.push(style);
|
||||||
|
self.scopes.insert(scope.to_string(), style_idx);
|
||||||
|
style_idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the style for a scope by name.
|
||||||
|
pub fn get_scope(&mut self, scope: &str) -> Style {
|
||||||
|
let idx = self.add_scope(scope);
|
||||||
|
self.get_style(idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the style for a scope index. Panics if out-of-bounds.
|
||||||
|
pub fn get_style(&self, index: usize) -> Style {
|
||||||
|
self.styles
|
||||||
|
.get(index)
|
||||||
|
.expect("StyleStore is out-of-bounds")
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current generation of the store. Increments every update.
|
||||||
|
pub fn generation(&self) -> usize {
|
||||||
|
self.generation
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies a theme to this store and updates the generation.
|
||||||
|
pub fn apply(&mut self, theme: Theme) {
|
||||||
|
self.generation += 1;
|
||||||
|
self.theme = theme;
|
||||||
|
|
||||||
|
for (scope, style_idx) in self.scopes.iter() {
|
||||||
|
let new_style = self.theme.get_scope_style(scope);
|
||||||
|
let old_style = self.styles.get_mut(*style_idx).unwrap();
|
||||||
|
let _ = std::mem::replace(old_style, new_style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
pub struct Style {
|
||||||
|
pub fg: Option<Color>,
|
||||||
|
pub bg: Option<Color>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Style {
|
||||||
|
pub fn print_styled(
|
||||||
|
&self,
|
||||||
|
out: &mut (impl ExecutableCommand + Write),
|
||||||
|
content: &str,
|
||||||
|
) -> crossterm::Result<()> {
|
||||||
|
use crossterm::style::{ResetColor, SetBackgroundColor, SetForegroundColor};
|
||||||
|
|
||||||
|
if let Some(fg) = self.fg {
|
||||||
|
out.execute(SetForegroundColor(fg))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bg) = self.bg {
|
||||||
|
out.execute(SetBackgroundColor(bg))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(out, "{}", content)?;
|
||||||
|
|
||||||
|
out.execute(ResetColor)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Theme {
|
||||||
|
pub name: String,
|
||||||
|
pub styles: HashMap<String, Style>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Theme {
|
||||||
|
pub fn from_value(name: &str, value: Value) -> Result<Self, String> {
|
||||||
|
if let Value::Table(table) = value {
|
||||||
|
Self::from_map(name, table)
|
||||||
|
} else {
|
||||||
|
panic!("Expected theme TOML value to be a table, found {:?}", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_map(name: &str, mut values: Map<String, Value>) -> Result<Self, String> {
|
||||||
|
let palette = values
|
||||||
|
.remove("palette")
|
||||||
|
.map(Palette::try_from)
|
||||||
|
.unwrap_or_else(|| Ok(Palette::default()))?;
|
||||||
|
|
||||||
|
let mut styles = HashMap::with_capacity(values.len());
|
||||||
|
|
||||||
|
for (name, style) in values {
|
||||||
|
let style = palette.parse_style(style)?;
|
||||||
|
styles.insert(name.to_string(), style);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
styles,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a style for a scope, matching the longest available scope.
|
||||||
|
///
|
||||||
|
/// If no match is found, defaults to the default [Style].
|
||||||
|
pub fn get_scope_style(&self, scope: &str) -> Style {
|
||||||
|
let mut longest = Style::default();
|
||||||
|
let mut longest_len = 0;
|
||||||
|
|
||||||
|
for (target, style) in self.styles.iter() {
|
||||||
|
if target.len() > longest_len && scope.starts_with(target) {
|
||||||
|
longest = *style;
|
||||||
|
longest_len = target.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
longest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Palette {
|
||||||
|
pub palette: HashMap<String, Color>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Palette {
|
||||||
|
fn default() -> Self {
|
||||||
|
use Color::*;
|
||||||
|
let pairs = &[
|
||||||
|
("black", Black),
|
||||||
|
("red", DarkRed),
|
||||||
|
("green", DarkGreen),
|
||||||
|
("yellow", DarkYellow),
|
||||||
|
("blue", DarkBlue),
|
||||||
|
("magenta", DarkMagenta),
|
||||||
|
("cyan", DarkCyan),
|
||||||
|
("gray", DarkGrey),
|
||||||
|
("grey", DarkGrey),
|
||||||
|
("light-red", Red),
|
||||||
|
("light-green", Green),
|
||||||
|
("light-yellow", Yellow),
|
||||||
|
("light-blue", Blue),
|
||||||
|
("light-magenta", Magenta),
|
||||||
|
("light-cyan", Cyan),
|
||||||
|
("light-gray", Grey),
|
||||||
|
("light-grey", Grey),
|
||||||
|
("white", White),
|
||||||
|
];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
palette: HashMap::from_iter(
|
||||||
|
pairs
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, color)| (name.to_string(), *color)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Value> for Palette {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(value: Value) -> Result<Self, String> {
|
||||||
|
let values = value.as_table().ok_or(format!(
|
||||||
|
"Theme: expected table for palette but got: {:?}",
|
||||||
|
value
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let default = Self::default();
|
||||||
|
let mut palette = HashMap::with_capacity(values.len());
|
||||||
|
|
||||||
|
for (name, value) in values {
|
||||||
|
let color = default.parse_color(value)?;
|
||||||
|
palette.insert(name.to_string(), color);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { palette })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Palette {
|
||||||
|
pub fn hex_string_to_rgb(s: &str) -> Result<Color, String> {
|
||||||
|
if s.starts_with('#') && s.len() == 7 {
|
||||||
|
if let (Ok(r), Ok(g), Ok(b)) = (
|
||||||
|
u8::from_str_radix(&s[1..3], 16),
|
||||||
|
u8::from_str_radix(&s[3..5], 16),
|
||||||
|
u8::from_str_radix(&s[5..7], 16),
|
||||||
|
) {
|
||||||
|
return Ok(Color::Rgb { r, g, b });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!("Theme: malformed hexcode: {}", s))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_value_as_str(value: &Value) -> Result<&str, String> {
|
||||||
|
value
|
||||||
|
.as_str()
|
||||||
|
.ok_or(format!("Theme: expected string but got: {:?}", value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_color(&self, value: &Value) -> Result<Color, String> {
|
||||||
|
let str = Self::parse_value_as_str(&value)?;
|
||||||
|
|
||||||
|
self.palette
|
||||||
|
.get(str)
|
||||||
|
.copied()
|
||||||
|
.ok_or("")
|
||||||
|
.or_else(|_| Self::hex_string_to_rgb(&str))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_style_map(&self, entries: Map<String, Value>) -> Result<Style, String> {
|
||||||
|
let mut style = Style::default();
|
||||||
|
for (name, value) in entries {
|
||||||
|
match name.as_str() {
|
||||||
|
"fg" => style.fg = Some(self.parse_color(&value)?),
|
||||||
|
"bg" => style.bg = Some(self.parse_color(&value)?),
|
||||||
|
_ => return Err(format!("Theme: invalid style attribute: {}", name)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_style(&self, value: Value) -> Result<Style, String> {
|
||||||
|
if let Value::Table(entries) = value {
|
||||||
|
self.parse_style_map(entries)
|
||||||
|
} else {
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.fg = Some(self.parse_color(&value)?);
|
||||||
|
Ok(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user