Compare commits
3 Commits
9c633b4c72
...
cc1debc051
Author | SHA1 | Date |
---|---|---|
mars | cc1debc051 | |
mars | 31a1cf961e | |
mars | 1f9ba13620 |
|
@ -63,7 +63,9 @@ dependencies = [
|
|||
"arg",
|
||||
"crossterm",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"ropey",
|
||||
"toml",
|
||||
"yacexits",
|
||||
]
|
||||
|
||||
|
@ -130,6 +132,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"
|
||||
|
@ -310,6 +328,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"
|
||||
|
@ -378,6 +411,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"
|
||||
|
@ -495,6 +562,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"
|
||||
|
|
|
@ -8,5 +8,7 @@ license = "AGPL-3.0-or-later"
|
|||
arg = "0.4.1"
|
||||
crossterm = "0.26"
|
||||
libc = "0.2.141"
|
||||
once_cell = "1.17"
|
||||
ropey = "1.6"
|
||||
toml = "0.7"
|
||||
yacexits = "0.1.5"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
"ui.linenr" = "gray"
|
||||
|
||||
"error" = "red"
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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::io::Write;
|
||||
|
||||
use crossterm::{cursor, ExecutableCommand};
|
||||
use ropey::Rope;
|
||||
|
||||
use crate::theme::StyleStore;
|
||||
use crate::{Cursor, Direction};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Buffer {
|
||||
text: Rope,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn from_str(text: &str) -> Self {
|
||||
Self {
|
||||
text: Rope::from_str(text),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(
|
||||
&self,
|
||||
styles: &mut StyleStore,
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
scroll: Cursor,
|
||||
out: &mut (impl ExecutableCommand + Write),
|
||||
) -> crossterm::Result<u32> {
|
||||
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))?;
|
||||
|
||||
for (row, line) in (0..rows).zip(self.text.lines_at(scroll.line)) {
|
||||
// only the last line is empty and should be skipped
|
||||
if line.len_chars() == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let row = row as usize + scroll.line;
|
||||
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
|
||||
if lhs < width {
|
||||
let window = text_width.min(width - lhs);
|
||||
let rhs = lhs + window;
|
||||
write!(out, "{}", line.slice(lhs..rhs))?;
|
||||
}
|
||||
|
||||
out.execute(cursor::MoveToNextLine(1))?;
|
||||
}
|
||||
|
||||
Ok(gutter_width)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, cursor: Cursor) {
|
||||
let index = self.cursor_to_char(cursor);
|
||||
self.text.remove(index..=index);
|
||||
}
|
||||
|
||||
pub fn insert_char(&mut self, cursor: Cursor, c: char) {
|
||||
let index = self.cursor_to_char(cursor);
|
||||
self.text.insert_char(index, c);
|
||||
}
|
||||
|
||||
pub fn clamped_cursor(&self, cursor: Cursor) -> Cursor {
|
||||
Cursor {
|
||||
line: cursor.line,
|
||||
column: cursor
|
||||
.column
|
||||
.min(self.text.line(cursor.line).len_chars() - 1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cursor_to_char(&self, cursor: Cursor) -> usize {
|
||||
let cursor = self.clamped_cursor(cursor);
|
||||
self.text.line_to_char(cursor.line) + cursor.column
|
||||
}
|
||||
|
||||
pub fn move_cursor(&self, cursor: &mut Cursor, direction: Direction, enable_linewrap: bool) {
|
||||
*cursor = self.clamped_cursor(*cursor);
|
||||
match direction {
|
||||
Direction::Left => {
|
||||
if cursor.column > 0 {
|
||||
cursor.column -= 1;
|
||||
} else if enable_linewrap && cursor.line > 0 {
|
||||
cursor.line -= 1;
|
||||
let line = self.text.line(cursor.line);
|
||||
cursor.column = line.len_chars() - 1;
|
||||
}
|
||||
}
|
||||
Direction::Down => {
|
||||
if cursor.line + 2 < self.text.len_lines() {
|
||||
cursor.line += 1;
|
||||
}
|
||||
}
|
||||
Direction::Up => {
|
||||
if cursor.line > 0 {
|
||||
cursor.line -= 1;
|
||||
}
|
||||
}
|
||||
Direction::Right => {
|
||||
let line = self.text.line(cursor.line);
|
||||
if cursor.column + 2 > line.len_chars() {
|
||||
if enable_linewrap && cursor.line + 2 < self.text.len_lines() {
|
||||
cursor.line += 1;
|
||||
cursor.column = 0;
|
||||
}
|
||||
} else {
|
||||
cursor.column += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
160
src/main.rs
160
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,
|
||||
};
|
||||
|
||||
|
@ -34,106 +29,13 @@ use crossterm::{
|
|||
event::{read, Event, KeyCode, KeyEvent},
|
||||
terminal, ExecutableCommand, Result,
|
||||
};
|
||||
use ropey::Rope;
|
||||
use yacexits::{ exit, EX_DATAERR, EX_UNAVAILABLE };
|
||||
use yacexits::{exit, EX_DATAERR, EX_UNAVAILABLE};
|
||||
|
||||
struct Buffer {
|
||||
pub text: Rope,
|
||||
}
|
||||
mod buffer;
|
||||
mod theme;
|
||||
|
||||
impl Buffer {
|
||||
pub fn from_str(text: &str) -> Self {
|
||||
Self {
|
||||
text: Rope::from_str(text),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(
|
||||
&self,
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
scroll: Cursor,
|
||||
out: &mut (impl ExecutableCommand + Write),
|
||||
) -> Result<u32> {
|
||||
let lr_width = self.text.len_lines().ilog10() + 1;
|
||||
let gutter_width = lr_width + 1;
|
||||
let text_width = cols as usize - gutter_width as usize;
|
||||
|
||||
out.execute(cursor::MoveTo(0, 0))?;
|
||||
|
||||
for (row, line) in (0..rows).zip(self.text.lines_at(scroll.line)) {
|
||||
// only the last line is empty and should be skipped
|
||||
if line.len_chars() == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let row = row as usize + scroll.line;
|
||||
write!(out, "{:width$} ", row, width = lr_width as usize)?;
|
||||
|
||||
let lhs = scroll.column;
|
||||
let width = line.len_chars().saturating_sub(1); // lop off whitespace
|
||||
if lhs < width {
|
||||
let window = text_width.min(width - lhs);
|
||||
let rhs = lhs + window;
|
||||
write!(out, "{}", line.slice(lhs..rhs))?;
|
||||
}
|
||||
|
||||
out.execute(cursor::MoveToNextLine(1))?;
|
||||
}
|
||||
|
||||
Ok(gutter_width)
|
||||
}
|
||||
|
||||
pub fn clamped_cursor(&self, cursor: Cursor) -> Cursor {
|
||||
Cursor {
|
||||
line: cursor.line,
|
||||
column: cursor
|
||||
.column
|
||||
.min(self.text.line(cursor.line).len_chars() - 1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cursor_to_char(&self, cursor: Cursor) -> usize {
|
||||
let cursor = self.clamped_cursor(cursor);
|
||||
self.text.line_to_char(cursor.line) + cursor.column
|
||||
}
|
||||
|
||||
pub fn move_cursor(&self, cursor: &mut Cursor, direction: Direction, enable_linewrap: bool) {
|
||||
*cursor = self.clamped_cursor(*cursor);
|
||||
match direction {
|
||||
Direction::Left => {
|
||||
if cursor.column > 0 {
|
||||
cursor.column -= 1;
|
||||
} else if enable_linewrap && cursor.line > 0 {
|
||||
cursor.line -= 1;
|
||||
let line = self.text.line(cursor.line);
|
||||
cursor.column = line.len_chars() - 1;
|
||||
}
|
||||
}
|
||||
Direction::Down => {
|
||||
if cursor.line + 2 < self.text.len_lines() {
|
||||
cursor.line += 1;
|
||||
}
|
||||
}
|
||||
Direction::Up => {
|
||||
if cursor.line > 0 {
|
||||
cursor.line -= 1;
|
||||
}
|
||||
}
|
||||
Direction::Right => {
|
||||
let line = self.text.line(cursor.line);
|
||||
if cursor.column + 2 > line.len_chars() {
|
||||
if enable_linewrap && cursor.line + 2 < self.text.len_lines() {
|
||||
cursor.line += 1;
|
||||
cursor.column = 0;
|
||||
}
|
||||
} else {
|
||||
cursor.column += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
use buffer::Buffer;
|
||||
use theme::StyleStore;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
struct Cursor {
|
||||
|
@ -192,6 +94,7 @@ enum Direction {
|
|||
}
|
||||
|
||||
struct State {
|
||||
pub style_store: StyleStore,
|
||||
pub buffer: Buffer,
|
||||
pub cursor: Cursor,
|
||||
pub scroll: Cursor,
|
||||
|
@ -205,6 +108,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 +118,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 +138,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 +148,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(|| {
|
||||
|
@ -357,22 +264,18 @@ impl State {
|
|||
match event {
|
||||
Event::Key(KeyEvent { code, .. }) => match code {
|
||||
KeyCode::Char(c) => {
|
||||
let index = self.buffer.cursor_to_char(self.cursor);
|
||||
self.buffer.text.insert_char(index, c);
|
||||
self.buffer.insert_char(self.cursor, c);
|
||||
self.move_cursor(Direction::Right)
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
self.move_cursor(Direction::Left);
|
||||
let index = self.buffer.cursor_to_char(self.cursor);
|
||||
self.buffer.text.remove(index..=index);
|
||||
self.buffer.remove(self.cursor);
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
let index = self.buffer.cursor_to_char(self.cursor);
|
||||
self.buffer.text.remove(index..=index);
|
||||
self.buffer.remove(self.cursor);
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let index = self.buffer.cursor_to_char(self.cursor);
|
||||
self.buffer.text.insert_char(index, '\n');
|
||||
self.buffer.insert_char(self.cursor, '\n');
|
||||
self.cursor.line += 1;
|
||||
self.cursor.column = 0;
|
||||
}
|
||||
|
@ -455,23 +358,22 @@ fn main() -> Result<()> {
|
|||
let stdin = 0; // get stdin as a file descriptor
|
||||
if unsafe { libc::isatty(stdin) } == 0 {
|
||||
unsafe { File::from_raw_fd(stdin) }
|
||||
} else { File::open("/dev/null").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();
|
||||
} else {
|
||||
File::open("/dev/null").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);
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue