392 lines
11 KiB
Rust
392 lines
11 KiB
Rust
/*
|
|
* 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, fmt::Display, ops::Range};
|
|
|
|
use crossterm::style::Color;
|
|
use crossterm::QueueableCommand;
|
|
use syntect::parsing::Scope;
|
|
use toml::{map::Map, Value};
|
|
|
|
#[derive(Debug, Default)]
|
|
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>,
|
|
|
|
/// Maps syntect scopes to indices.
|
|
syntect_scopes: HashMap<Scope, usize>,
|
|
|
|
/// The style data store.
|
|
styles: Vec<Style>,
|
|
}
|
|
|
|
impl StyleStore {
|
|
/// Creates a new style store with a given theme.
|
|
pub fn new(theme: Theme) -> Self {
|
|
Self {
|
|
theme,
|
|
generation: 0,
|
|
scopes: HashMap::new(),
|
|
syntect_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 {
|
|
if let Some(style_idx) = self.scopes.get(scope) {
|
|
*style_idx
|
|
} else {
|
|
let style_idx = self.styles.len();
|
|
let style = self.theme.get_scope_style(scope);
|
|
self.styles.push(style);
|
|
self.scopes.insert(scope.to_string(), style_idx);
|
|
|
|
if let Ok(scope) = Scope::new(scope) {
|
|
self.syntect_scopes.insert(scope, 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)
|
|
}
|
|
|
|
/// Adds a Syntect [Scope] and returns its index. Resues indices for existing scopes.
|
|
pub fn add_syntect_scope(&mut self, scope: &Scope) -> usize {
|
|
if let Some(style_idx) = self.syntect_scopes.get(scope) {
|
|
*style_idx
|
|
} else {
|
|
let style_idx = self.styles.len();
|
|
let scope_str = scope.build_string();
|
|
let style = self.theme.get_scope_style(&scope_str);
|
|
self.styles.push(style);
|
|
self.scopes.insert(scope_str, style_idx);
|
|
self.syntect_scopes.insert(*scope, style_idx);
|
|
style_idx
|
|
}
|
|
}
|
|
|
|
/// Gets the style for a Syntect [Scope].
|
|
pub fn get_syntect_scope(&mut self, scope: &Scope) -> &Style {
|
|
let idx = self.add_syntect_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")
|
|
}
|
|
|
|
/// 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, Default, PartialEq, Eq)]
|
|
pub struct StyledString {
|
|
pub content: String,
|
|
pub styles: Vec<(usize, Style)>,
|
|
}
|
|
|
|
impl StyledString {
|
|
pub fn new_unstyled(content: String) -> Self {
|
|
Self {
|
|
styles: vec![(content.len(), Style::default())],
|
|
content,
|
|
}
|
|
}
|
|
|
|
pub fn print(&self, out: &mut impl QueueableCommand, base: Style) -> crossterm::Result<()> {
|
|
let mut cursor = 0;
|
|
for (size, style) in self.styles.iter() {
|
|
let end = cursor + size;
|
|
let range = cursor..end;
|
|
let sub = &self.content[range];
|
|
let mut style = style.clone();
|
|
style.apply(&base);
|
|
style.print_styled(out, sub)?;
|
|
cursor = end;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn print_sub(
|
|
&self,
|
|
out: &mut impl QueueableCommand,
|
|
clip: Range<usize>,
|
|
base: Style,
|
|
) -> crossterm::Result<usize> {
|
|
let mut remaining = clip.end - clip.start;
|
|
let mut cursor = 0;
|
|
for (size, style) in self.styles.iter() {
|
|
let end = cursor + size;
|
|
let clipped = (cursor.max(clip.start))..(end.min(clip.end));
|
|
cursor = end;
|
|
|
|
if clipped.start >= clipped.end {
|
|
continue;
|
|
}
|
|
|
|
remaining -= clipped.end - clipped.start;
|
|
|
|
let sub = &self.content[clipped];
|
|
let mut style = style.clone();
|
|
style.apply(&base);
|
|
style.print_styled(out, sub)?;
|
|
|
|
if cursor >= clip.end {
|
|
break;
|
|
}
|
|
}
|
|
|
|
Ok(remaining)
|
|
}
|
|
}
|
|
|
|
#[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 QueueableCommand,
|
|
content: impl Display,
|
|
) -> crossterm::Result<()> {
|
|
use crossterm::style::{Print, ResetColor, SetBackgroundColor, SetForegroundColor};
|
|
|
|
if let Some(fg) = self.fg {
|
|
out.queue(SetForegroundColor(fg))?;
|
|
}
|
|
|
|
if let Some(bg) = self.bg {
|
|
out.queue(SetBackgroundColor(bg))?;
|
|
}
|
|
|
|
out.queue(Print(content))?;
|
|
out.queue(ResetColor)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Applies another [Style] on top of this one.
|
|
pub fn apply(&mut self, other: &Style) {
|
|
if other.fg.is_some() {
|
|
self.fg = other.fg;
|
|
}
|
|
|
|
if other.bg.is_some() {
|
|
self.bg = other.bg;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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 mut default = Self::default();
|
|
for (name, value) in values {
|
|
let color = default.parse_color(value)?;
|
|
default.palette.insert(name.to_string(), color);
|
|
}
|
|
|
|
Ok(default)
|
|
}
|
|
}
|
|
|
|
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)?),
|
|
"modifiers" => {} // ignore for now
|
|
"underline" => {} // ignore for now
|
|
_ => 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)
|
|
}
|
|
}
|
|
}
|