forked from mars/breed
resolve merge conflict
This commit is contained in:
commit
36bbb3b77b
76
Cargo.lock
generated
76
Cargo.lock
generated
|
@ -63,7 +63,9 @@ dependencies = [
|
||||||
"arg",
|
"arg",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"libc",
|
"libc",
|
||||||
|
"once_cell",
|
||||||
"ropey",
|
"ropey",
|
||||||
|
"toml",
|
||||||
"yacexits",
|
"yacexits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -130,6 +132,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"
|
||||||
|
@ -310,6 +328,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"
|
||||||
|
@ -378,6 +411,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"
|
||||||
|
@ -495,6 +562,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"
|
||||||
|
|
|
@ -8,5 +8,7 @@ license = "AGPL-3.0-or-later"
|
||||||
arg = "0.4.1"
|
arg = "0.4.1"
|
||||||
crossterm = "0.26"
|
crossterm = "0.26"
|
||||||
libc = "0.2.141"
|
libc = "0.2.141"
|
||||||
|
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"
|
138
src/buffer.rs
Normal file
138
src/buffer.rs
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
128
src/main.rs
128
src/main.rs
|
@ -29,109 +29,16 @@ use crossterm::{
|
||||||
event::{read, Event, KeyCode, KeyEvent},
|
event::{read, Event, KeyCode, KeyEvent},
|
||||||
terminal, ExecutableCommand, Result,
|
terminal, ExecutableCommand, Result,
|
||||||
};
|
};
|
||||||
use ropey::Rope;
|
|
||||||
use yacexits::{exit, EX_DATAERR, EX_UNAVAILABLE};
|
use yacexits::{exit, EX_DATAERR, EX_UNAVAILABLE};
|
||||||
|
|
||||||
struct Buffer {
|
mod buffer;
|
||||||
pub text: Rope,
|
mod theme;
|
||||||
}
|
|
||||||
|
|
||||||
impl Buffer {
|
use buffer::Buffer;
|
||||||
pub fn from_str(text: &str) -> Self {
|
use theme::StyleStore;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default)]
|
#[derive(Copy, Clone, Debug, Default)]
|
||||||
struct Cursor {
|
pub struct Cursor {
|
||||||
pub column: usize,
|
pub column: usize,
|
||||||
pub line: usize,
|
pub line: usize,
|
||||||
}
|
}
|
||||||
|
@ -179,7 +86,7 @@ impl Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
enum Direction {
|
pub enum Direction {
|
||||||
Left,
|
Left,
|
||||||
Down,
|
Down,
|
||||||
Up,
|
Up,
|
||||||
|
@ -187,6 +94,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,
|
||||||
|
@ -200,6 +108,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(),
|
||||||
|
@ -209,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
|
// begin update
|
||||||
let (cols, rows) = terminal::size()?;
|
let (cols, rows) = terminal::size()?;
|
||||||
out.execute(terminal::BeginSynchronizedUpdate)?;
|
out.execute(terminal::BeginSynchronizedUpdate)?;
|
||||||
|
@ -229,8 +138,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;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -238,7 +148,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(|| {
|
||||||
|
@ -360,22 +272,18 @@ impl State {
|
||||||
match event {
|
match event {
|
||||||
Event::Key(KeyEvent { code, .. }) => match code {
|
Event::Key(KeyEvent { code, .. }) => match code {
|
||||||
KeyCode::Char(c) => {
|
KeyCode::Char(c) => {
|
||||||
let index = self.buffer.cursor_to_char(self.cursor);
|
self.buffer.insert_char(self.cursor, c);
|
||||||
self.buffer.text.insert_char(index, c);
|
|
||||||
self.move_cursor(Direction::Right)
|
self.move_cursor(Direction::Right)
|
||||||
}
|
}
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
self.move_cursor(Direction::Left);
|
self.move_cursor(Direction::Left);
|
||||||
let index = self.buffer.cursor_to_char(self.cursor);
|
self.buffer.remove(self.cursor);
|
||||||
self.buffer.text.remove(index..=index);
|
|
||||||
}
|
}
|
||||||
KeyCode::Delete => {
|
KeyCode::Delete => {
|
||||||
let index = self.buffer.cursor_to_char(self.cursor);
|
self.buffer.remove(self.cursor);
|
||||||
self.buffer.text.remove(index..=index);
|
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let index = self.buffer.cursor_to_char(self.cursor);
|
self.buffer.insert_char(self.cursor, '\n');
|
||||||
self.buffer.text.insert_char(index, '\n');
|
|
||||||
self.cursor.line += 1;
|
self.cursor.line += 1;
|
||||||
self.cursor.column = 0;
|
self.cursor.column = 0;
|
||||||
}
|
}
|
||||||
|
|
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