breed/src/theme.rs

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)
}
}
}