Add tab reload on external change

This commit is contained in:
Jeremy Soller 2023-11-21 11:57:11 -07:00
parent 68675b1a99
commit f207db0e84
No known key found for this signature in database
GPG key ID: DCFCA852D3906975
4 changed files with 242 additions and 6 deletions

View file

@ -5,7 +5,9 @@ use cosmic::{
cosmic_config::{self, CosmicConfigEntry},
cosmic_theme, executor,
iced::{
clipboard, event, keyboard, subscription,
clipboard, event,
futures::{self, SinkExt},
keyboard, subscription,
widget::{row, text},
window, Alignment, Length, Point,
},
@ -20,6 +22,7 @@ use std::{
process,
sync::Mutex,
};
use tokio::time;
use config::{Action, AppTheme, Config, CONFIG_VERSION};
mod config;
@ -104,6 +107,23 @@ pub struct Flags {
config: Config,
}
#[derive(Debug)]
pub struct WatcherWrapper {
watcher_opt: Option<notify::RecommendedWatcher>,
}
impl Clone for WatcherWrapper {
fn clone(&self) -> Self {
Self { watcher_opt: None }
}
}
impl PartialEq for WatcherWrapper {
fn eq(&self, _other: &Self) -> bool {
false
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq)]
pub enum Message {
@ -118,6 +138,8 @@ pub enum Message {
Key(keyboard::Modifiers, keyboard::KeyCode),
NewFile,
NewWindow,
NotifyEvent(notify::Event),
NotifyWatcher(WatcherWrapper),
OpenFileDialog,
OpenFile(PathBuf),
OpenProjectDialog,
@ -171,6 +193,7 @@ pub struct App {
font_sizes: Vec<u16>,
theme_names: Vec<String>,
context_page: ContextPage,
watcher_opt: Option<notify::RecommendedWatcher>,
}
impl App {
@ -304,6 +327,7 @@ impl App {
let mut tab = Tab::new(&self.config);
tab.open(canonical);
tab.watch(&mut self.watcher_opt);
tab
}
None => Tab::new(&self.config),
@ -337,9 +361,7 @@ impl App {
log::error!("failed to save config: {}", err);
}
},
None => {
//TODO: log that there is no handler?
}
None => {}
}
self.update_config()
}
@ -485,6 +507,7 @@ impl Application for App {
font_sizes,
theme_names,
context_page: ContextPage::Settings,
watcher_opt: None,
};
for arg in env::args().skip(1) {
@ -681,6 +704,57 @@ impl Application for App {
}
}
}
Message::NotifyEvent(event) => {
let mut needs_reload = Vec::new();
for entity in self.tab_model.iter() {
match self.tab_model.data::<Tab>(entity) {
Some(tab) => {
if let Some(path) = &tab.path_opt {
if event.paths.contains(&path) {
if tab.changed() {
log::warn!(
"file changed externally before being saved: {:?}",
path
);
} else {
needs_reload.push(entity);
}
}
}
}
None => {}
}
}
for entity in needs_reload {
match self.tab_model.data_mut::<Tab>(entity) {
Some(tab) => {
tab.reload();
}
None => {
log::warn!("failed to find tab {:?} that needs reload", entity);
}
}
}
}
Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take()
{
Some(watcher) => {
self.watcher_opt = Some(watcher);
for entity in self.tab_model.iter() {
match self.tab_model.data::<Tab>(entity) {
Some(tab) => {
tab.watch(&mut self.watcher_opt);
}
None => {}
}
}
}
None => {
log::warn!("message did not contain notify watcher");
}
},
Message::OpenFileDialog => {
#[cfg(feature = "rfd")]
return Command::perform(
@ -1113,6 +1187,63 @@ impl Application for App {
}) => Some(Message::Key(modifiers, key_code)),
_ => None,
}),
subscription::channel(0, 100, |mut output| async move {
let watcher_res = {
let mut output = output.clone();
notify::recommended_watcher(
move |event_res: Result<notify::Event, notify::Error>| match event_res {
Ok(event) => {
match &event.kind {
notify::EventKind::Access(_)
| notify::EventKind::Modify(
notify::event::ModifyKind::Metadata(_),
) => {
// Data not mutated
return;
}
_ => {}
}
match futures::executor::block_on(async {
output.send(Message::NotifyEvent(event)).await
}) {
Ok(()) => {}
Err(err) => {
log::warn!("failed to send notify event: {:?}", err);
}
}
}
Err(err) => {
log::warn!("failed to watch files: {:?}", err);
}
},
)
};
match watcher_res {
Ok(watcher) => {
match output
.send(Message::NotifyWatcher(WatcherWrapper {
watcher_opt: Some(watcher),
}))
.await
{
Ok(()) => {}
Err(err) => {
log::warn!("failed to send notify watcher: {:?}", err);
}
}
}
Err(err) => {
log::warn!("failed to create file watcher: {:?}", err);
}
}
//TODO: how to properly kill this task?
loop {
time::sleep(time::Duration::new(1, 0)).await;
}
}),
cosmic_config::config_subscription(0, Self::APP_ID.into(), CONFIG_VERSION).map(
|(_, res)| match res {
Ok(config) => Message::Config(config),

View file

@ -5,6 +5,7 @@ use cosmic::{
widget::{icon, Icon},
};
use cosmic_text::{Attrs, Buffer, Edit, Shaping, SyntaxEditor, ViEditor, Wrap};
use notify::Watcher;
use std::{fs, path::PathBuf, sync::Mutex};
use crate::{fl, mime_icon, Config, FALLBACK_MIME_ICON, FONT_SYSTEM, SYNTAX_SYSTEM};
@ -82,6 +83,31 @@ impl Tab {
}
}
pub fn reload(&mut self) {
let mut editor = self.editor.lock().unwrap();
let mut font_system = FONT_SYSTEM.lock().unwrap();
let mut editor = editor.borrow_with(&mut font_system);
if let Some(path) = &self.path_opt {
// Save scroll
let scroll = editor.buffer().scroll();
//TODO: save/restore more?
match editor.load_text(path, self.attrs) {
Ok(()) => {
log::info!("reloaded {:?}", path);
}
Err(err) => {
log::error!("failed to reload {:?}: {}", path, err);
}
}
// Restore scroll
editor.buffer_mut().set_scroll(scroll);
} else {
log::warn!("tried to reload with no path");
}
}
pub fn save(&mut self) {
if let Some(path) = &self.path_opt {
let mut editor = self.editor.lock().unwrap();
@ -104,6 +130,21 @@ impl Tab {
}
}
pub fn watch(&self, watcher_opt: &mut Option<notify::RecommendedWatcher>) {
if let Some(path) = &self.path_opt {
if let Some(watcher) = watcher_opt {
match watcher.watch(&path, notify::RecursiveMode::NonRecursive) {
Ok(()) => {
log::info!("watching {:?} for changes", path);
}
Err(err) => {
log::warn!("failed to watch {:?} for changes: {:?}", path, err);
}
}
}
}
}
pub fn changed(&self) -> bool {
let editor = self.editor.lock().unwrap();
editor.changed()