diff --git a/Cargo.lock b/Cargo.lock index 5dc4ca3..4dac50c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,6 +84,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -145,9 +151,9 @@ dependencies = [ [[package]] name = "arboard" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" +checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227" dependencies = [ "clipboard-win", "image", @@ -396,6 +402,21 @@ dependencies = [ "libc", ] +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clipboard-win" version = "5.4.0" @@ -949,6 +970,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "gdk-pixbuf-sys" version = "0.18.0" @@ -1228,6 +1258,30 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1406,7 +1460,9 @@ dependencies = [ name = "inkr" version = "1.0.0" dependencies = [ + "arboard", "base64 0.22.1", + "chrono", "eframe", "egui", "egui_extras", @@ -1417,6 +1473,7 @@ dependencies = [ "half", "insta", "log", + "notify", "rand 0.9.1", "rfd", "serde", @@ -1425,6 +1482,26 @@ dependencies = [ "zerocopy 0.8.25", ] +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.9.0", + "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 = "insta" version = "1.43.1" @@ -1532,6 +1609,26 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "kurbo" version = "0.9.5" @@ -1678,6 +1775,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + [[package]] name = "naga" version = "24.0.0" @@ -1745,6 +1854,30 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "notify" +version = "8.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97" +dependencies = [ + "bitflags 2.9.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "num-traits" version = "0.2.19" @@ -3480,6 +3613,12 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-result" version = "0.2.0" @@ -3526,6 +3665,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -3565,13 +3713,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -3590,6 +3754,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -3608,6 +3778,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -3626,12 +3802,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -3650,6 +3838,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -3668,6 +3862,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -3686,6 +3886,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -3704,6 +3910,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winit" version = "0.30.9" diff --git a/Cargo.toml b/Cargo.toml index 2dd36d6..af6c7b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,9 @@ eyre = "0.6.12" half = "2.6.0" zerocopy = { version = "0.8.25", features = ["derive", "std"] } base64 = "0.22.1" +chrono = { version = "0.4.41", features = ["serde"] } +notify = "8.1.0" +arboard = "3.6.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] env_logger = "0.11.8" diff --git a/src/app.rs b/src/app.rs index 28c46f1..d30c250 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,16 +1,23 @@ use std::{ fs, + io::Read, path::PathBuf, sync::{Arc, mpsc}, thread::JoinHandle, time::{Duration, Instant}, }; -use crate::{file_editor::FileEditor, folder::Folder, preferences::Preferences, util::GuiSender}; +use crate::{ + file_editor::{FileEditor, SaveStatus}, + folder::Folder, + preferences::Preferences, + util::{GuiSender, file_mtime, log_error}, +}; use egui::{ Align, Button, Context, FontData, FontDefinitions, Image, Key, Modifiers, PointerButton, RichText, ScrollArea, Widget, include_image, }; +use eyre::eyre; #[derive(serde::Deserialize, serde::Serialize)] #[serde(default)] @@ -40,7 +47,7 @@ pub struct Jobs { } impl Jobs { - fn start(&mut self, ctx: &Context, job: impl FnOnce() -> Option + Send + 'static) { + pub fn start(&mut self, ctx: &Context, job: impl FnOnce() -> Option + Send + 'static) { let ctx = ctx.clone(); let actions_tx = self.actions_tx.clone(); self.handles.push(std::thread::spawn(move || { @@ -73,9 +80,21 @@ impl Tab { } } - pub fn is_dirty(&self) -> bool { + pub fn notice_symbol(&self) -> Option<&'static str> { match self { - Tab::File(file_editor) => file_editor.is_dirty, + Tab::File(file_editor) => match file_editor.save_status() { + SaveStatus::Synced => None, + SaveStatus::NoFile => Some("?"), + SaveStatus::FileOutdated => Some("*"), + SaveStatus::BufferOutdated => Some("!"), + SaveStatus::Desynced => Some("!!"), + }, + } + } + + pub fn save(&mut self, ctx: &Context, jobs: &mut Jobs) { + match self { + Tab::File(file_editor) => file_editor.save(ctx, jobs), } } } @@ -268,11 +287,18 @@ impl eframe::App for App { self.jobs.start(ui.ctx(), move || { let file_path = rfd::FileDialog::new().pick_file()?; - let text = fs::read_to_string(&file_path) + let mut file = fs::File::open(&file_path) + .inspect_err(|e| log::error!("Failed to open {file_path:?}: {e}")) + .ok()?; + + let mtime = log_error(eyre!("file_path:?"), || file_mtime(&file))?; + + let mut text = String::new(); + file.read_to_string(&mut text) .inspect_err(|e| log::error!("Failed to read {file_path:?}: {e}")) .ok()?; - let editor = FileEditor::from_file(file_path, &text); + let editor = FileEditor::from_file(file_path, &text, mtime); Some(Action::OpenFile(editor)) }); } @@ -364,8 +390,8 @@ impl eframe::App for App { let selected = self.open_tab_index == Some(i); let mut button = Button::new(tab.title()).selected(selected); - if tab.is_dirty() { - button = button.right_text(RichText::new("*").strong()) + if let Some(symbol) = tab.notice_symbol() { + button = button.right_text(RichText::new(symbol).strong()) } let response = ui.add(button); @@ -396,13 +422,22 @@ impl eframe::App for App { if let Some(file_path) = response.open_file { let file_path = file_path.to_owned(); self.jobs.start(ui.ctx(), move || { - let text = fs::read_to_string(&file_path) + let mut file = fs::File::open(&file_path) + .inspect_err(|e| { + log::error!("Failed to open {file_path:?}: {e}") + }) + .ok()?; + + let mtime = log_error(eyre!("file_path:?"), || file_mtime(&file))?; + + let mut text = String::new(); + file.read_to_string(&mut text) .inspect_err(|e| { log::error!("Failed to read {file_path:?}: {e}") }) .ok()?; - let editor = FileEditor::from_file(file_path, &text); + let editor = FileEditor::from_file(file_path, &text, mtime); Some(Action::OpenFile(editor)) }); } @@ -448,29 +483,13 @@ impl App { } fn save_active_tab(&mut self, ctx: &Context) { - let open_file = self + let open_tab = self .open_tab_index .and_then(|i| self.tabs.get_mut(i)) - .map(|(id, tab)| match tab { - Tab::File(file_editor) => (*id, file_editor), - }) - .and_then(|(_, file_editor)| { - file_editor - .path() - .map(ToOwned::to_owned) - .zip(Some(file_editor)) - }); + .map(|(_id, tab)| tab); - if let Some((file_path, file_editor)) = open_file { - file_editor.is_dirty = false; - let text = file_editor.to_string(); - let file_path = file_path.to_owned(); - self.jobs.start(ctx, move || { - if let Err(e) = fs::write(file_path, text.as_bytes()) { - log::error!("{e}"); - }; - None - }); + if let Some(open_tab) = open_tab { + open_tab.save(ctx, &mut self.jobs); } } } diff --git a/src/file_editor.rs b/src/file_editor.rs index 251ca02..8a44b13 100644 --- a/src/file_editor.rs +++ b/src/file_editor.rs @@ -1,30 +1,77 @@ use std::{ cmp::Ordering, fmt::{self, Display}, + fs::{self, File}, + io::Write, ops::{Div as _, Sub as _}, path::{Path, PathBuf}, str::FromStr, + sync::mpsc, }; +use chrono::{DateTime, Local}; use egui::{ - Align, Button, DragAndDrop, Frame, Layout, ScrollArea, Ui, UiBuilder, Vec2, Widget as _, vec2, + Align, Button, Context, DragAndDrop, Frame, Layout, ScrollArea, Ui, UiBuilder, Vec2, + Widget as _, vec2, }; +use eyre::eyre; +use notify::{EventKind, Watcher}; use crate::{ + app::Jobs, custom_code_block::{MdItem, iter_lines_and_code_blocks}, handwriting::{self, Handwriting, HandwritingStyle}, preferences::Preferences, text_editor::MdTextEdit, + util::{file_mtime, log_error}, }; #[derive(serde::Deserialize, serde::Serialize)] pub struct FileEditor { title: String, - pub path: Option, + + path: Option, pub buffer: Vec, - /// Whether the file has been edited since it was laste saved to disk. - pub is_dirty: bool, + pub file_mtime: Option>, + pub buffer_mtime: DateTime, + + // TODO: instantiate these on load + #[serde(skip)] + inner: Option, + + /// Whether the file has been edited since it was last saved to disk. + is_dirty: bool, +} + +struct Inner { + file_events: mpsc::Receiver, + file_events_tx: mpsc::Sender, + _file_watcher: notify::RecommendedWatcher, +} + +#[derive(Debug)] +pub enum SaveStatus { + /// The contents of the buffer is the same as on disk. + Synced, + + /// The contents exits only in memory and has never been saved to disk. + NoFile, + + /// The buffer has been edited but not saved to disk. + FileOutdated, + + /// The contents on disk has changes that are newer than the buffer. + BufferOutdated, + + /// The contents on disk and in the buffer has diverged. + Desynced, +} + +#[derive(Debug)] +enum FileEvent { + NewFileMTime(DateTime), + NewBufferMTime(DateTime), } #[derive(serde::Deserialize, serde::Serialize)] @@ -40,18 +87,24 @@ impl FileEditor { title: title.into(), path: None, buffer, + file_mtime: None, + buffer_mtime: Local::now(), is_dirty: false, + inner: None, } } - pub fn from_file(file_path: PathBuf, contents: &str) -> Self { + pub fn from_file(file_path: PathBuf, contents: &str, mtime: DateTime) -> Self { let file_title = file_path .file_name() .map(|name| name.to_string_lossy().to_string()) .unwrap_or_else(|| String::from("untitled.md")); + Self { title: file_title, path: Some(file_path), + file_mtime: Some(mtime), + buffer_mtime: mtime, ..FileEditor::from(contents) } } @@ -64,7 +117,46 @@ impl FileEditor { self.path.as_deref() } + pub fn save_status(&self) -> SaveStatus { + let Some(file_mtime) = self.file_mtime else { + return SaveStatus::NoFile; + }; + + let buffer_is_newer = self.buffer_mtime > file_mtime; + let file_is_newer = self.buffer_mtime < file_mtime; + + if buffer_is_newer || (file_is_newer && self.is_dirty) { + SaveStatus::Desynced + } else if file_is_newer { + SaveStatus::BufferOutdated + } else if self.is_dirty { + SaveStatus::FileOutdated + } else { + SaveStatus::Synced + } + } + pub fn show(&mut self, ui: &mut Ui, preferences: &Preferences) { + if let Some(path) = &self.path + && self.inner.is_none() + { + self.inner = Some(spawn_file_watcher(path)); + } + + if let Some(inner) = &mut self.inner { + while let Ok(event) = inner.file_events.try_recv() { + match dbg!(event) { + FileEvent::NewFileMTime(mtime) => { + self.file_mtime = Some(mtime); + } + FileEvent::NewBufferMTime(mtime) => { + self.buffer_mtime = mtime; + self.file_mtime = Some(mtime); + } + } + } + } + ui.vertical_centered_justified(|ui| { ui.heading(&self.title); @@ -243,6 +335,38 @@ impl FileEditor { self.title = title.to_string_lossy().to_string(); self.path = Some(new_path); } + + pub fn set_dirty(&mut self, value: bool) { + self.is_dirty = value; + } + + pub fn save(&mut self, ctx: &Context, jobs: &mut Jobs) { + let Some(file_path) = self.path.clone() else { + log::info!("Can't save {}, no path set.", self.title); + return; + }; + + self.is_dirty = false; + let text = self.to_string(); + let inner = self + .inner + .get_or_insert_with(|| spawn_file_watcher(&file_path)); + let file_event_tx = inner.file_events_tx.clone(); + + jobs.start(ctx, move || { + log_error(eyre!("Failed to save file {file_path:?}"), || { + let mut file = fs::File::create(file_path)?; + file.write_all(text.as_bytes())?; + let mtime = file_mtime(&file)?; + + let _ = file_event_tx.send(FileEvent::NewBufferMTime(mtime)); + + Ok(()) + }); + + None + }); + } } impl Display for BufferItem { @@ -314,6 +438,40 @@ impl From<&str> for FileEditor { } } +fn spawn_file_watcher(p: &Path) -> Inner { + let (tx, rx) = mpsc::channel(); + + let path = p.to_owned(); + let events_tx = tx.clone(); + let mut watcher = notify::recommended_watcher(move |event: notify::Result| { + log_error(eyre!("watch {path:?} error"), || { + match event?.kind { + EventKind::Create(..) | EventKind::Modify(..) | EventKind::Remove(..) => {} + + EventKind::Access(..) | EventKind::Any | EventKind::Other => return Ok(()), + } + + let file = File::open(&path)?; + let mtime = file_mtime(&file)?; + + let _ = events_tx.send(FileEvent::NewFileMTime(mtime.into())); + + Ok(()) + }); + }) + .unwrap(); + + if let Err(e) = watcher.watch(p, notify::RecursiveMode::NonRecursive) { + log::error!("Failed to watch {p:?}: {e}"); + }; + + Inner { + file_events: rx, + file_events_tx: tx, + _file_watcher: watcher, + } +} + #[cfg(test)] mod test { diff --git a/src/handwriting/mod.rs b/src/handwriting/mod.rs index aa76915..5560937 100644 --- a/src/handwriting/mod.rs +++ b/src/handwriting/mod.rs @@ -8,6 +8,7 @@ use std::{ sync::Arc, }; +use arboard::Clipboard; use base64::{Engine, prelude::BASE64_STANDARD}; use canvas_rasterizer::CanvasRasterizer; use disk_format::{DiskFormat, RawStroke, RawStrokeHeader, f16_le}; @@ -163,6 +164,12 @@ impl Handwriting { } }); + if ui.button("copy").clicked() { + let text = self.to_string(); + // TODO: move to a job + let _ = Clipboard::new().unwrap().set_text(text); + } + let vertex_count: usize = self.e.mesh.indices.len() / 3; ui.label(format!("vertices: {vertex_count}")); }) diff --git a/src/markdown/tokenizer.rs b/src/markdown/tokenizer.rs index f2c6c73..d1c975c 100644 --- a/src/markdown/tokenizer.rs +++ b/src/markdown/tokenizer.rs @@ -31,7 +31,7 @@ pub enum TokenKind { Text, } -const TOKENS: &[(&'static str, TokenKind)] = &[ +const TOKENS: &[(&str, TokenKind)] = &[ ("\n", TokenKind::Newline), ("######", TokenKind::Heading(Heading::H6)), ("#####", TokenKind::Heading(Heading::H5)), diff --git a/src/rasterizer.rs b/src/rasterizer.rs index d6ff67e..cb2f39c 100644 --- a/src/rasterizer.rs +++ b/src/rasterizer.rs @@ -90,10 +90,10 @@ pub fn rasterize_onto<'a, Blend: BlendFn>( /// Rasterize a single triangles onto an image, /// /// Triangle positions must be in image-local point-coords. -pub fn rasterize_triangle_onto<'a, Blend: BlendFn>( +pub fn rasterize_triangle_onto( image: &mut ColorImage, point_to_pixel: TSTransform, - triangle: [&'a Vertex; 3], + triangle: [&Vertex; 3], ) { rasterize_onto::(image, point_to_pixel, [triangle].into_iter()); } diff --git a/src/util.rs b/src/util.rs index 01fefe3..aa08ce5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,8 @@ -use std::sync::mpsc; +use std::{fs::File, os::unix::fs::MetadataExt as _, sync::mpsc}; +use chrono::{DateTime, Local}; use egui::Id; +use eyre::{Context, ContextCompat}; use rand::{Rng, rng}; pub fn random_id() -> Id { @@ -28,3 +30,27 @@ impl GuiSender { Ok(()) } } + +#[track_caller] +pub fn log_error(chain: eyre::Report, f: impl FnOnce() -> eyre::Result) -> Option { + f().map_err(|e| e.wrap_err(chain)) + .inspect_err(|e| log::error!("{e}")) + .ok() +} + +pub fn file_mtime(file: &File) -> eyre::Result> { + (move || { + let meta = file.metadata().wrap_err("Failed to stat file")?; + + let sec = meta.mtime(); + let nsec = meta + .mtime_nsec() + .try_into() + .wrap_err("Nanoseconds overflowed")?; + + DateTime::from_timestamp(sec, nsec) + .wrap_err("Bad timestamp") + .map(Into::into) + })() + .wrap_err("Failed to get file mtime") +}