From 7d46cbcf9b8959d9e5060d472d1ab47537b6799d Mon Sep 17 00:00:00 2001 From: mars Date: Wed, 12 Apr 2023 17:13:20 -0400 Subject: [PATCH] Live theme reload --- Cargo.lock | 201 ++++++++++++++++++++-- Cargo.toml | 2 + src/buffer.rs | 23 ++- src/config.rs | 143 +++++++++++++++ src/main.rs | 22 ++- src/theme.rs | 15 +- default_theme.toml => themes/default.toml | 1 - 7 files changed, 371 insertions(+), 36 deletions(-) create mode 100644 src/config.rs rename default_theme.toml => themes/default.toml (98%) diff --git a/Cargo.lock b/Cargo.lock index 2ee1316..691869e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index ebd31f7..94d6c83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/buffer.rs b/src/buffer.rs index 47957b3..70d3b54 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -36,6 +36,8 @@ pub struct Buffer { text: Rope, syntax_set: Arc, parser: ParseState, + style_dirty: bool, + style_generation: usize, styled: Vec)>>, } @@ -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 { + 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 { diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..aee666e --- /dev/null +++ b/src/config.rs @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2023 Marceline Cramer + * Copyright (c) 2023 Emma Tebibyte + * 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>, + watcher: RecommendedWatcher, + current_path: PathBuf, + fs_rx: Receiver>, + command_rx: Receiver, + quit: bool, +} + +impl ThemeWatcher { + pub fn spawn(styles: Arc>) -> Sender { + 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, 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) { + 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), + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 8311ea1..7c7f8d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, } 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(()) diff --git a/src/theme.rs b/src/theme.rs index 72c6768..3723a10 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -24,13 +24,7 @@ use crossterm::{style::Color, ExecutableCommand}; use once_cell::sync::Lazy; use toml::{map::Map, Value}; -pub static DEFAULT_THEME: Lazy = 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