Refactor file watcher feature and implement for gui
This commit is contained in:
parent
f57bccf017
commit
b32fc8a89c
@ -3,11 +3,12 @@
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, Stdout};
|
use std::io::{self, Stdout};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::mpsc::{self, TryRecvError};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser as ArgParser;
|
use clap::Parser as ArgParser;
|
||||||
use crossterm::event::{Event, KeyCode};
|
use crossterm::event::{Event, KeyCode, KeyEvent};
|
||||||
use crossterm::terminal::{
|
use crossterm::terminal::{
|
||||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
};
|
};
|
||||||
@ -17,6 +18,8 @@ use ratatui::backend::CrosstermBackend;
|
|||||||
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||||
use ratatui::widgets::{Block, Borders, Cell, Padding, Paragraph, Row, Table};
|
use ratatui::widgets::{Block, Borders, Cell, Padding, Paragraph, Row, Table};
|
||||||
|
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
|
use crate::watcher::FileWatcher;
|
||||||
use crate::Args as AppArgs;
|
use crate::Args as AppArgs;
|
||||||
|
|
||||||
|
|
||||||
@ -27,7 +30,23 @@ pub struct Args {
|
|||||||
|
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
current_file: Option<Savefile>,
|
current_path: PathBuf,
|
||||||
|
current_file: Savefile,
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
|
file_watcher: Option<FileWatcher>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
enum Message {
|
||||||
|
Exit,
|
||||||
|
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
|
ToggleFileWatch,
|
||||||
|
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
|
ReloadFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -42,7 +61,10 @@ pub(crate) fn execute(_app_args: &AppArgs, args: &Args) -> Result<()> {
|
|||||||
|
|
||||||
// TODO: prompt file path
|
// TODO: prompt file path
|
||||||
let state = State {
|
let state = State {
|
||||||
current_file: Some(savefile),
|
current_path: args.path.clone(),
|
||||||
|
current_file: savefile,
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
|
file_watcher: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
run(&mut terminal, state)?;
|
run(&mut terminal, state)?;
|
||||||
@ -52,6 +74,105 @@ pub(crate) fn execute(_app_args: &AppArgs, args: &Args) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
fn run(terminal: &mut Terminal, mut state: State) -> Result<()> {
|
||||||
|
let (mut evq_tx, evq_rx) = mpsc::channel::<Message>();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
terminal.draw(|frame| {
|
||||||
|
render(&state, frame);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
handle_events(&mut evq_tx)?;
|
||||||
|
|
||||||
|
let message = match evq_rx.try_recv() {
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(TryRecvError::Empty) => continue,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
match message {
|
||||||
|
Message::Exit => break,
|
||||||
|
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
|
Message::ToggleFileWatch => {
|
||||||
|
if state.file_watcher.is_none() {
|
||||||
|
let evq_tx = evq_tx.clone();
|
||||||
|
let callback = move || {
|
||||||
|
evq_tx.send(Message::ReloadFile).unwrap();
|
||||||
|
};
|
||||||
|
let file_watcher = FileWatcher::new(&state.current_path, callback);
|
||||||
|
state.file_watcher = Some(file_watcher);
|
||||||
|
} else {
|
||||||
|
state.file_watcher = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
|
Message::ReloadFile => {
|
||||||
|
let savefile = load_savefile(&state.current_path)?;
|
||||||
|
state.current_file = savefile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn handle_events(event_queue: &mut mpsc::Sender<Message>) -> Result<()> {
|
||||||
|
if !event::poll(Duration::from_millis(250))? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
match event::read()? {
|
||||||
|
Event::Key(key) => handle_keyboard_input(key, event_queue)?,
|
||||||
|
_ => return Ok(()),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn handle_keyboard_input(key: KeyEvent, event_queue: &mut mpsc::Sender<Message>) -> Result<()> {
|
||||||
|
let message = match key.code {
|
||||||
|
KeyCode::Char('q') => Message::Exit,
|
||||||
|
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
|
KeyCode::Char('w') => Message::ToggleFileWatch,
|
||||||
|
|
||||||
|
_ => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
event_queue.send(message)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn setup() -> Result<Terminal> {
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
|
||||||
|
enable_raw_mode()?;
|
||||||
|
|
||||||
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
|
|
||||||
|
Ok(Terminal::new(CrosstermBackend::new(stdout))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn reset(mut terminal: Terminal) -> Result<()> {
|
||||||
|
disable_raw_mode()?;
|
||||||
|
|
||||||
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||||
|
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fn load_savefile<P>(path: P) -> Result<Savefile>
|
fn load_savefile<P>(path: P) -> Result<Savefile>
|
||||||
where
|
where
|
||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
@ -63,11 +184,39 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn render(savefile: &Savefile, mut frame: &mut Frame) {
|
fn render(state: &State, mut frame: &mut Frame) {
|
||||||
|
let rows = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(40), Constraint::Length(2)])
|
||||||
|
.split(frame.size());
|
||||||
|
|
||||||
|
render_info(&state.current_file, &mut frame, rows[0]);
|
||||||
|
|
||||||
|
let status_block = Block::default().padding(Padding::horizontal(2));
|
||||||
|
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
|
let text = format!(
|
||||||
|
"{} file: {}",
|
||||||
|
if state.file_watcher.is_some() {
|
||||||
|
"Watching"
|
||||||
|
} else {
|
||||||
|
"Showing"
|
||||||
|
},
|
||||||
|
state.current_path.display()
|
||||||
|
);
|
||||||
|
#[cfg(not(feature = "watch"))]
|
||||||
|
let text = format!("Showing file: {}", state.current_path.display());
|
||||||
|
|
||||||
|
let status = Paragraph::new(text).block(status_block);
|
||||||
|
frame.render_widget(status, rows[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn render_info(savefile: &Savefile, mut frame: &mut Frame, area: Rect) {
|
||||||
let columns = Layout::default()
|
let columns = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
.split(frame.size());
|
.split(area);
|
||||||
|
|
||||||
let left_column = Layout::default()
|
let left_column = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
@ -83,6 +232,7 @@ fn render(savefile: &Savefile, mut frame: &mut Frame) {
|
|||||||
.constraints([Constraint::Ratio(10, 10)])
|
.constraints([Constraint::Ratio(10, 10)])
|
||||||
.split(columns[1]);
|
.split(columns[1]);
|
||||||
|
|
||||||
|
|
||||||
render_stats(&savefile, &mut frame, left_column[0]);
|
render_stats(&savefile, &mut frame, left_column[0]);
|
||||||
render_glyphs(&savefile, &mut frame, left_column[1]);
|
render_glyphs(&savefile, &mut frame, left_column[1]);
|
||||||
render_murals(&savefile, &mut frame, left_column[2]);
|
render_murals(&savefile, &mut frame, left_column[2]);
|
||||||
@ -98,7 +248,7 @@ fn render_stats<'a>(savefile: &Savefile, frame: &mut Frame, area: Rect) {
|
|||||||
|
|
||||||
let layout = Layout::default()
|
let layout = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)])
|
.constraints([Constraint::Percentage(25), Constraint::Percentage(75)])
|
||||||
.split(stats_section_block.inner(area));
|
.split(stats_section_block.inner(area));
|
||||||
|
|
||||||
let stats_block = Block::default().title("Stats");
|
let stats_block = Block::default().title("Stats");
|
||||||
@ -250,52 +400,3 @@ fn render_murals<'a>(savefile: &Savefile, frame: &mut Frame, area: Rect) {
|
|||||||
|
|
||||||
frame.render_widget(table, area);
|
frame.render_widget(table, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(terminal: &mut Terminal, state: State) -> Result<()> {
|
|
||||||
loop {
|
|
||||||
terminal.draw(|frame| {
|
|
||||||
if let Some(savefile) = &state.current_file {
|
|
||||||
render(savefile, frame);
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if quitting()? {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn quitting() -> Result<bool> {
|
|
||||||
if event::poll(Duration::from_millis(250))? {
|
|
||||||
if let Event::Key(key) = event::read()? {
|
|
||||||
return Ok(KeyCode::Char('q') == key.code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn setup() -> Result<Terminal> {
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
|
|
||||||
enable_raw_mode()?;
|
|
||||||
|
|
||||||
execute!(stdout, EnterAlternateScreen)?;
|
|
||||||
|
|
||||||
Ok(Terminal::new(CrosstermBackend::new(stdout))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn reset(mut terminal: Terminal) -> Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
|
|
||||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
||||||
|
|
||||||
terminal.show_cursor()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
mod edit;
|
mod edit;
|
||||||
mod gui;
|
mod gui;
|
||||||
mod show;
|
mod show;
|
||||||
|
mod watcher;
|
||||||
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
@ -33,6 +33,7 @@ pub(crate) fn execute(_app_args: &AppArgs, args: &Args) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
fn watch_all_info<P>(path: P) -> Result<()>
|
fn watch_all_info<P>(path: P) -> Result<()>
|
||||||
where
|
where
|
||||||
@ -40,25 +41,22 @@ where
|
|||||||
{
|
{
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
use notify::event::{AccessKind, AccessMode};
|
use crate::watcher::FileWatcher;
|
||||||
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
|
|
||||||
|
let _watcher = FileWatcher::new(path.as_ref(), move || {
|
||||||
|
tx.send(()).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
println!("Watching file for changes...");
|
println!("Watching file for changes...");
|
||||||
watcher.watch(path.as_ref(), RecursiveMode::NonRecursive)?;
|
|
||||||
|
|
||||||
let written_and_closed = EventKind::Access(AccessKind::Close(AccessMode::Write));
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let event = rx.recv()?;
|
let _ = rx.recv()?;
|
||||||
|
|
||||||
if event?.kind == written_and_closed {
|
|
||||||
show_all_info(&path).unwrap();
|
show_all_info(&path).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn show_all_info<P>(path: P) -> Result<()>
|
fn show_all_info<P>(path: P) -> Result<()>
|
||||||
@ -98,7 +96,7 @@ fn current_journey(savefile: &Savefile) {
|
|||||||
println!("Current Level: {:<10}", savefile.current_level_name());
|
println!("Current Level: {:<10}", savefile.current_level_name());
|
||||||
println!("Companions Met: {:<10}", savefile.companions_met);
|
println!("Companions Met: {:<10}", savefile.companions_met);
|
||||||
println!("Scarf Length: {:<10}", savefile.scarf_length);
|
println!("Scarf Length: {:<10}", savefile.scarf_length);
|
||||||
println!("Symbol Number: {:<10}", savefile.symbol);
|
println!("Symbol Number: {:<10}", savefile.symbol.id);
|
||||||
println!(
|
println!(
|
||||||
"Robe: {:<10}, Tier {}",
|
"Robe: {:<10}, Tier {}",
|
||||||
savefile.robe_color(),
|
savefile.robe_color(),
|
||||||
|
62
crates/wayfarer/src/watcher.rs
Normal file
62
crates/wayfarer/src/watcher.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
#![cfg(feature = "watch")]
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread::{self, spawn};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use notify::event::{AccessKind, AccessMode};
|
||||||
|
use notify::{Config as NotifyConfig, EventKind, RecommendedWatcher, Watcher};
|
||||||
|
|
||||||
|
|
||||||
|
pub struct FileWatcher {
|
||||||
|
exit_signal: mpsc::SyncSender<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileWatcher {
|
||||||
|
pub fn new<P, F>(path: P, callback: F) -> Self
|
||||||
|
where
|
||||||
|
P: Into<PathBuf>,
|
||||||
|
F: Fn() -> () + Send + 'static,
|
||||||
|
{
|
||||||
|
let (exit_signal, exit) = mpsc::sync_channel(0);
|
||||||
|
let (ev_tx, ev_rx) = mpsc::channel();
|
||||||
|
|
||||||
|
let path = path.into();
|
||||||
|
|
||||||
|
spawn(move || {
|
||||||
|
let mut watcher = RecommendedWatcher::new(ev_tx, NotifyConfig::default()).unwrap();
|
||||||
|
watcher
|
||||||
|
.watch(path.as_ref(), notify::RecursiveMode::NonRecursive)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let written_and_closed = EventKind::Access(AccessKind::Close(AccessMode::Write));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match ev_rx.try_recv() {
|
||||||
|
Ok(Ok(event)) => {
|
||||||
|
if event.kind == written_and_closed {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(mpsc::TryRecvError::Empty) => (),
|
||||||
|
Err(_) | Ok(Err(_)) => break,
|
||||||
|
}
|
||||||
|
|
||||||
|
if exit.try_recv().is_ok() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(500));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self { exit_signal }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for FileWatcher {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.exit_signal.send(());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user