Live theme reload

This commit is contained in:
mars 2023-04-12 17:13:20 -04:00
parent b7f9c8ba2e
commit 7d46cbcf9b
7 changed files with 371 additions and 36 deletions

201
Cargo.lock generated
View File

@ -76,8 +76,10 @@ name = "breed"
version = "0.1.0"
dependencies = [
"arg",
"crossbeam-channel",
"crossterm",
"libc",
"notify",
"once_cell",
"parking_lot",
"ropey",
@ -127,6 +129,25 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossterm"
version = "0.26.1"
@ -158,6 +179,18 @@ version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "filetime"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"windows-sys 0.48.0",
]
[[package]]
name = "flate2"
version = "1.0.25"
@ -174,6 +207,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]]
name = "glob"
version = "0.3.1"
@ -196,12 +238,52 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "itoa"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "kqueue"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587"
dependencies = [
"bitflags",
"libc",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -279,7 +361,7 @@ dependencies = [
"libc",
"log",
"wasi",
"windows-sys",
"windows-sys 0.45.0",
]
[[package]]
@ -292,6 +374,24 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "notify"
version = "5.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58ea850aa68a06e48fdb069c0ec44d0d64c8dbffa49bf3b6f7f0a901fdea1ba9"
dependencies = [
"bitflags",
"crossbeam-channel",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"mio",
"walkdir",
"windows-sys 0.42.0",
]
[[package]]
name = "once_cell"
version = "1.17.1"
@ -340,7 +440,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-sys",
"windows-sys 0.45.0",
]
[[package]]
@ -695,13 +795,37 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.0",
]
[[package]]
@ -710,13 +834,28 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
"windows_aarch64_gnullvm 0.48.0",
"windows_aarch64_msvc 0.48.0",
"windows_i686_gnu 0.48.0",
"windows_i686_msvc 0.48.0",
"windows_x86_64_gnu 0.48.0",
"windows_x86_64_gnullvm 0.48.0",
"windows_x86_64_msvc 0.48.0",
]
[[package]]
@ -725,42 +864,84 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.4.1"

View File

@ -7,7 +7,9 @@ license = "AGPL-3.0-or-later"
[dependencies]
arg = "0.4.1"
crossterm = "0.26"
crossbeam-channel = "0.5"
libc = "0.2.141"
notify = "5"
once_cell = "1.17"
parking_lot = "0.12"
ropey = "1.6"

View File

@ -36,6 +36,8 @@ pub struct Buffer {
text: Rope,
syntax_set: Arc<SyntaxSet>,
parser: ParseState,
style_dirty: bool,
style_generation: usize,
styled: Vec<Vec<(Style, Range<usize>)>>,
}
@ -51,6 +53,8 @@ impl Buffer {
text: Rope::from_str(text),
syntax_set: Arc::new(SyntaxSet::load_defaults_newlines()),
parser,
style_dirty: true,
style_generation: 0,
styled: Vec::new(),
};
@ -60,13 +64,15 @@ impl Buffer {
}
pub fn draw(
&self,
styles: &mut StyleStore,
&mut self,
cols: u16,
rows: u16,
scroll: Cursor,
out: &mut (impl ExecutableCommand + Write),
) -> crossterm::Result<u32> {
self.parse();
let mut styles = self.styles.lock();
let linenr_style = styles.get_scope("ui.linenr");
let linenr_width = self.text.len_lines().ilog10() + 1;
let gutter_width = linenr_width + 1;
@ -114,19 +120,23 @@ impl Buffer {
pub fn remove(&mut self, cursor: Cursor) {
let index = self.cursor_to_char(cursor);
self.text.remove(index..=index);
self.parse();
self.style_dirty = true;
}
pub fn insert_char(&mut self, cursor: Cursor, c: char) {
let index = self.cursor_to_char(cursor);
self.text.insert_char(index, c);
self.parse();
self.style_dirty = true;
}
/// Parses the whole file from scratch.
fn parse(&mut self) {
use std::fmt::Write;
let mut styles = self.styles.lock();
if styles.generation() == self.style_generation && !self.style_dirty {
return;
}
self.styled.clear();
let mut parser = self.parser.clone();
let mut line_buf = String::new();
@ -163,6 +173,9 @@ impl Buffer {
self.styled.push(line);
}
self.style_dirty = true;
self.style_generation = styles.generation();
}
pub fn clamped_cursor(&self, cursor: Cursor) -> Cursor {

143
src/config.rs Normal file
View File

@ -0,0 +1,143 @@
/*
* 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::path::{Path, PathBuf};
use std::sync::Arc;
use crossbeam_channel::{unbounded, Receiver, RecvError, Sender};
use notify::{Event, EventKind, RecommendedWatcher, Watcher};
use parking_lot::Mutex;
use crate::theme::{StyleStore, Theme};
pub struct Config {
/// Whether or not to wrap lines when moving the cursor left and right.
pub move_linewrap: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
move_linewrap: true,
}
}
}
/// Gets the configuration directory. Panics if unavailable.
pub fn get_config_dir() -> PathBuf {
std::env::var_os("HOME")
.map(PathBuf::try_from)
.expect("$HOME is unset")
.unwrap()
.join(".config/breed")
}
/// Watches a theme file and automatically reloads it into a [StyleStore].
pub struct ThemeWatcher {
styles: Arc<Mutex<StyleStore>>,
watcher: RecommendedWatcher,
current_path: PathBuf,
fs_rx: Receiver<notify::Result<Event>>,
command_rx: Receiver<PathBuf>,
quit: bool,
}
impl ThemeWatcher {
pub fn spawn(styles: Arc<Mutex<StyleStore>>) -> Sender<PathBuf> {
let themes_dir = get_config_dir().join("themes");
let default_path = themes_dir.join("default.toml");
let (fs_tx, fs_rx) = unbounded();
let (command_tx, command_rx) = unbounded();
let watcher = notify::recommended_watcher(fs_tx).unwrap();
let mut watcher = Self {
styles,
current_path: PathBuf::new(),
watcher,
fs_rx,
command_rx,
quit: false,
};
watcher.watch_theme(&default_path).unwrap();
std::thread::spawn(move || watcher.run());
command_tx
}
fn load_theme(&mut self, path: &Path) -> Result<(), String> {
let name = path.file_stem().unwrap_or_default().to_string_lossy();
let contents = std::fs::read_to_string(path)
.map_err(|err| format!("Failed to read theme file: {:?}", err))?;
let value =
toml::from_str(&contents).map_err(|err| format!("Failed to parse theme: {:?}", err))?;
let theme = Theme::from_value(&name, value)?;
self.styles.lock().apply(theme);
Ok(())
}
fn watch_theme(&mut self, path: &Path) -> notify::Result<()> {
let old = self.current_path.clone();
let mode = notify::RecursiveMode::NonRecursive;
self.watcher.watch(&path, mode)?;
self.current_path = path.to_path_buf();
if old != PathBuf::new() {
self.watcher.unwatch(&old)?;
}
self.load_theme(path).map_err(|err| notify::Error {
kind: notify::ErrorKind::Generic(err),
paths: vec![path.to_owned()],
})
}
fn on_fs(&mut self, event: Result<notify::Result<Event>, crossbeam_channel::RecvError>) {
match event {
Err(RecvError) => self.quit = true,
Ok(Ok(event)) => match event.kind {
EventKind::Modify(_) => {
for path in event.paths {
self.load_theme(&path).unwrap();
}
}
_ => {}
},
_ => {} // TODO display errors?
}
}
fn on_command(&mut self, path: Result<PathBuf, RecvError>) {
match path {
Err(RecvError) => self.quit = true,
Ok(path) => {
self.watch_theme(&path).unwrap();
}
}
}
fn run(mut self) {
while !self.quit {
crossbeam_channel::select! {
recv(self.fs_rx) -> event => self.on_fs(event),
recv(self.command_rx) -> path => self.on_command(path),
}
}
}
}

View File

@ -22,12 +22,14 @@ use std::{
fs::File,
io::{stdout, Read, Stdout, Write},
os::fd::FromRawFd,
path::PathBuf,
sync::Arc,
};
use crossbeam_channel::Sender;
use crossterm::{
cursor,
event::{read, Event, KeyCode, KeyEvent},
event::{Event, KeyCode, KeyEvent},
terminal, ExecutableCommand, Result,
};
use parking_lot::Mutex;
@ -35,6 +37,7 @@ use yacexits::{exit, EX_DATAERR, EX_UNAVAILABLE};
mod actions;
mod buffer;
mod config;
mod theme;
use buffer::Buffer;
@ -104,6 +107,7 @@ pub struct State {
pub size: (usize, usize),
pub mode: Mode,
pub quit: bool,
pub theme_tx: Sender<PathBuf>,
}
impl State {
@ -111,6 +115,7 @@ impl State {
let styles = Arc::new(Mutex::new(StyleStore::default()));
let buffer = Buffer::from_str(styles.clone(), text);
let (cols, rows) = terminal::size()?;
let theme_tx = config::ThemeWatcher::spawn(styles.clone());
Ok(Self {
styles,
@ -120,6 +125,7 @@ impl State {
size: (cols as usize, rows as usize),
mode: Mode::default(),
quit: false,
theme_tx,
})
}
@ -152,11 +158,12 @@ impl State {
_ => {}
}
// done with styles
drop(styles);
// draw buffer
let buffer_rows = if show_status_bar { rows - 1 } else { rows };
let lr_width = self
.buffer
.draw(&mut styles, cols, buffer_rows, self.scroll, out)?;
let lr_width = self.buffer.draw(cols, buffer_rows, self.scroll, out)?;
// draw cursor
let cursor_pos = set_cursor_pos.unwrap_or_else(|| {
@ -341,10 +348,13 @@ impl State {
}
fn screen_main(stdout: &mut Stdout, mut state: State) -> Result<()> {
let poll_timeout = std::time::Duration::from_millis(100);
while !state.quit {
state.draw(stdout)?;
let event = read()?;
state.on_event(event);
if let true = crossterm::event::poll(poll_timeout)? {
let event = crossterm::event::read()?;
state.on_event(event);
}
}
Ok(())

View File

@ -24,13 +24,7 @@ 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)]
#[derive(Debug, Default)]
pub struct StyleStore {
/// The current theme.
theme: Theme,
@ -45,13 +39,6 @@ pub struct StyleStore {
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 {

View File

@ -9,7 +9,6 @@
"constructor" = "foam"
"comment" = { fg = "muted", modifiers = ["italic"]}
"constant" = "foam"
"constant.builtin" = "rose"