Compare commits
3 Commits
8c5be0c371
...
5258447579
Author | SHA1 | Date | |
---|---|---|---|
5258447579 | |||
337a60cef5 | |||
6f0ab738f0 |
@ -1,19 +1,27 @@
|
||||
mod edit;
|
||||
mod show;
|
||||
mod state;
|
||||
mod tui;
|
||||
mod watcher;
|
||||
|
||||
|
||||
use std::fs::create_dir_all;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser as ArgParser;
|
||||
use directories::ProjectDirs;
|
||||
use tracing::Level as TracingLevel;
|
||||
use tracing_appender::rolling;
|
||||
use tracing_subscriber::filter::Targets;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
use crate::state::logs_dir;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref DIRS: ProjectDirs = {
|
||||
ProjectDirs::from("", "valeth", "wayfarer").unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, ArgParser)]
|
||||
@ -80,3 +88,17 @@ fn tracing_setup() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn logs_dir() -> Result<PathBuf> {
|
||||
let log_root_path = DIRS
|
||||
.state_dir()
|
||||
.unwrap_or_else(|| DIRS.cache_dir())
|
||||
.join("logs");
|
||||
|
||||
if !log_root_path.exists() {
|
||||
create_dir_all(&log_root_path)?;
|
||||
}
|
||||
|
||||
Ok(log_root_path)
|
||||
}
|
||||
|
@ -1,104 +0,0 @@
|
||||
use std::fs::create_dir_all;
|
||||
use std::path::PathBuf;
|
||||
#[cfg(feature = "tui")]
|
||||
use std::{
|
||||
fs::{self, read_to_string},
|
||||
io::Write,
|
||||
os::unix::prelude::OsStrExt,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use directories::ProjectDirs;
|
||||
#[cfg(feature = "tui")]
|
||||
use jrny_save::Savefile;
|
||||
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref DIRS: ProjectDirs = {
|
||||
ProjectDirs::from("", "valeth", "wayfarer").unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
pub fn logs_dir() -> Result<PathBuf> {
|
||||
let log_root_path = DIRS
|
||||
.state_dir()
|
||||
.unwrap_or_else(|| DIRS.cache_dir())
|
||||
.join("logs");
|
||||
|
||||
if !log_root_path.exists() {
|
||||
create_dir_all(&log_root_path)?;
|
||||
}
|
||||
|
||||
Ok(log_root_path)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PersistentState {
|
||||
pub savefile: Option<Savefile>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
impl PersistentState {
|
||||
pub fn load() -> Result<Self> {
|
||||
let data_dir = DIRS.data_local_dir();
|
||||
|
||||
if !data_dir.exists() {
|
||||
create_dir_all(&data_dir)?;
|
||||
}
|
||||
|
||||
let savefile = load_last_active_savefile()?;
|
||||
|
||||
Ok(Self { savefile })
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
pub fn reload_active_savefile(&mut self) -> Result<()> {
|
||||
if let Some(cur_savefile) = &self.savefile {
|
||||
let new_savefile = Savefile::from_path(&cur_savefile.path)?;
|
||||
self.savefile = Some(new_savefile);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_active_savefile_path<P>(&mut self, path: P) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let savefile = Savefile::from_path(&path)?;
|
||||
|
||||
let state_path = DIRS.data_local_dir().join("active_savefile");
|
||||
let mut state_file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(state_path)?;
|
||||
|
||||
let active_savefile = savefile.path.as_os_str().as_bytes();
|
||||
|
||||
state_file.write_all(active_savefile)?;
|
||||
self.savefile = Some(savefile);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
fn load_last_active_savefile() -> Result<Option<Savefile>> {
|
||||
let state_path = DIRS.data_local_dir().join("active_savefile");
|
||||
|
||||
if !state_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let path = read_to_string(&state_path).unwrap();
|
||||
|
||||
let savefile = Savefile::from_path(path.trim_end())?;
|
||||
|
||||
Ok(Some(savefile))
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
#![cfg(feature = "tui")]
|
||||
|
||||
mod events;
|
||||
mod state;
|
||||
mod view;
|
||||
|
||||
|
||||
@ -9,18 +10,17 @@ use std::sync::mpsc::{self, TryRecvError};
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser as ArgParser;
|
||||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
||||
use crossterm::event::{
|
||||
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
|
||||
};
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use tracing::{debug, error, info};
|
||||
use tui_input::Input;
|
||||
|
||||
use crate::state::PersistentState;
|
||||
#[cfg(feature = "watch")]
|
||||
use crate::watcher::FileWatcher;
|
||||
use self::state::{Mode, State};
|
||||
use crate::Args as AppArgs;
|
||||
|
||||
|
||||
@ -31,15 +31,6 @@ type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
|
||||
pub struct Args;
|
||||
|
||||
|
||||
pub struct State {
|
||||
persistent: PersistentState,
|
||||
mode: Mode,
|
||||
file_select: Input,
|
||||
#[cfg(feature = "watch")]
|
||||
file_watcher: Option<FileWatcher>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum Message {
|
||||
@ -52,35 +43,15 @@ pub enum Message {
|
||||
#[cfg(feature = "watch")]
|
||||
ToggleFileWatch,
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
ReloadFile,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Normal,
|
||||
|
||||
ShowError(String),
|
||||
|
||||
SelectFile,
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn execute(_app_args: &AppArgs, _args: &Args) -> Result<()> {
|
||||
let persistent = PersistentState::load()?;
|
||||
let state = State::load()?;
|
||||
|
||||
let mut terminal = setup()?;
|
||||
|
||||
let state = State {
|
||||
persistent,
|
||||
mode: Mode::default(),
|
||||
file_select: Input::default(),
|
||||
#[cfg(feature = "watch")]
|
||||
file_watcher: None,
|
||||
};
|
||||
|
||||
run(&mut terminal, state)?;
|
||||
|
||||
reset(terminal)?;
|
||||
@ -137,45 +108,38 @@ fn handle_message(
|
||||
|
||||
Message::LoadFile => {
|
||||
let file_path = state.file_select.value();
|
||||
|
||||
info!("Loading file {}", file_path);
|
||||
|
||||
state.persistent.set_active_savefile_path(file_path)?;
|
||||
state.set_selected_as_active_savefile()?;
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
if state.file_watcher.is_some() {
|
||||
state.file_watcher = None;
|
||||
if state.is_watching_file() {
|
||||
state.reset_file_watcher();
|
||||
}
|
||||
|
||||
msg_tx.send(Message::SetMode(Mode::Normal))?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
Message::ToggleFileWatch if state.persistent.savefile.is_some() => {
|
||||
let savefile = state.persistent.savefile.as_ref().unwrap();
|
||||
Message::ToggleFileWatch => {
|
||||
if let Some(savefile) = state.savefile() {
|
||||
if state.is_watching_file() {
|
||||
let evq_tx = msg_tx.clone();
|
||||
let callback = move || {
|
||||
evq_tx.send(Message::ReloadFile).unwrap();
|
||||
};
|
||||
|
||||
if state.file_watcher.is_none() {
|
||||
let evq_tx = msg_tx.clone();
|
||||
let callback = move || {
|
||||
evq_tx.send(Message::ReloadFile).unwrap();
|
||||
};
|
||||
|
||||
info!("Starting file watcher on {}", savefile.path.display());
|
||||
|
||||
let file_watcher = FileWatcher::new(&savefile.path, callback);
|
||||
state.file_watcher = Some(file_watcher);
|
||||
} else {
|
||||
info!("Stopped file watcher on {}", savefile.path.display());
|
||||
|
||||
state.file_watcher = None;
|
||||
info!("Starting file watcher on {}", savefile.path.display());
|
||||
state.enable_file_watcher(callback);
|
||||
} else {
|
||||
info!("Stopped file watcher on {}", savefile.path.display());
|
||||
state.reset_file_watcher();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
Message::ReloadFile if state.persistent.savefile.is_some() => {
|
||||
debug!("Reloading file");
|
||||
|
||||
state.persistent.reload_active_savefile()?;
|
||||
Message::ReloadFile => {
|
||||
state.reload_active_savefile()?;
|
||||
}
|
||||
|
||||
_ => (),
|
||||
@ -191,7 +155,12 @@ fn setup() -> Result<Terminal> {
|
||||
debug!("Enabling terminal raw mode");
|
||||
enable_raw_mode()?;
|
||||
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
execute!(
|
||||
stdout,
|
||||
EnterAlternateScreen,
|
||||
EnableMouseCapture,
|
||||
EnableBracketedPaste
|
||||
)?;
|
||||
|
||||
Ok(Terminal::new(CrosstermBackend::new(stdout))?)
|
||||
}
|
||||
@ -204,7 +173,8 @@ fn reset(mut terminal: Terminal) -> Result<()> {
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
DisableMouseCapture,
|
||||
DisableBracketedPaste
|
||||
)?;
|
||||
|
||||
terminal.show_cursor()?;
|
||||
|
@ -3,7 +3,9 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use tracing::debug;
|
||||
use tui_input::backend::crossterm::EventHandler;
|
||||
use tui_input::Input;
|
||||
|
||||
use super::{Message, Mode, State};
|
||||
|
||||
@ -15,6 +17,7 @@ pub fn handle(event_queue: &mut mpsc::Sender<Message>, state: &mut State) -> Res
|
||||
|
||||
match event::read()? {
|
||||
Event::Key(key) => handle_keyboard_input(key, event_queue, state)?,
|
||||
Event::Paste(val) => handle_paste(val, state)?,
|
||||
_ => return Ok(()),
|
||||
}
|
||||
|
||||
@ -22,6 +25,19 @@ pub fn handle(event_queue: &mut mpsc::Sender<Message>, state: &mut State) -> Res
|
||||
}
|
||||
|
||||
|
||||
fn handle_paste(value: String, state: &mut State) -> Result<()> {
|
||||
match &state.mode {
|
||||
Mode::SelectFile => {
|
||||
debug!("Received pasted content: {:?}", value);
|
||||
let combined = format!("{}{}", state.file_select.value(), value);
|
||||
state.file_select = Input::new(combined);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[tracing::instrument(skip(msg_tx, state))]
|
||||
fn handle_keyboard_input(
|
||||
key: KeyEvent,
|
||||
@ -57,6 +73,10 @@ fn handle_keyboard_input(
|
||||
msg_tx.send(Message::SetMode(Mode::SelectFile))?;
|
||||
}
|
||||
|
||||
(Mode::Normal, KeyCode::Char('r')) => {
|
||||
msg_tx.send(Message::ReloadFile)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
(Mode::Normal, KeyCode::Char('w')) => {
|
||||
msg_tx.send(Message::ToggleFileWatch)?;
|
||||
|
120
crates/wayfarer/src/tui/state.rs
Normal file
120
crates/wayfarer/src/tui/state.rs
Normal file
@ -0,0 +1,120 @@
|
||||
use std::fs::{self, create_dir_all, read_to_string};
|
||||
use std::io::Write;
|
||||
use std::os::unix::prelude::OsStrExt;
|
||||
|
||||
use anyhow::Result;
|
||||
use jrny_save::Savefile;
|
||||
use tracing::debug;
|
||||
use tui_input::Input;
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
use crate::watcher::FileWatcher;
|
||||
use crate::DIRS;
|
||||
|
||||
|
||||
pub struct State {
|
||||
savefile: Option<Savefile>,
|
||||
pub mode: Mode,
|
||||
pub file_select: Input,
|
||||
#[cfg(feature = "watch")]
|
||||
file_watcher: Option<FileWatcher>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Normal,
|
||||
|
||||
ShowError(String),
|
||||
|
||||
SelectFile,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn load() -> Result<Self> {
|
||||
let data_dir = DIRS.data_local_dir();
|
||||
|
||||
if !data_dir.exists() {
|
||||
create_dir_all(&data_dir)?;
|
||||
}
|
||||
|
||||
let savefile = load_last_active_savefile()?;
|
||||
|
||||
Ok(Self {
|
||||
savefile,
|
||||
mode: Mode::default(),
|
||||
file_select: Input::default(),
|
||||
#[cfg(feature = "watch")]
|
||||
file_watcher: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn savefile(&self) -> Option<&Savefile> {
|
||||
self.savefile.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_selected_as_active_savefile(&mut self) -> Result<()> {
|
||||
let savefile = Savefile::from_path(&self.file_select.value())?;
|
||||
|
||||
let state_path = DIRS.data_local_dir().join("active_savefile");
|
||||
let mut state_file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(state_path)?;
|
||||
|
||||
let active_savefile = savefile.path.as_os_str().as_bytes();
|
||||
|
||||
state_file.write_all(active_savefile)?;
|
||||
self.savefile = Some(savefile);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
pub fn is_watching_file(&self) -> bool {
|
||||
self.file_watcher.is_some()
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
pub fn enable_file_watcher<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn() + Send + 'static,
|
||||
{
|
||||
if let Some(savefile) = &self.savefile {
|
||||
let file_watcher = FileWatcher::new(&savefile.path, callback);
|
||||
self.file_watcher = Some(file_watcher);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
pub fn reset_file_watcher(&mut self) {
|
||||
self.file_watcher = None;
|
||||
}
|
||||
|
||||
pub fn reload_active_savefile(&mut self) -> Result<()> {
|
||||
if let Some(cur_savefile) = &self.savefile {
|
||||
debug!("Reloading file");
|
||||
let new_savefile = Savefile::from_path(&cur_savefile.path)?;
|
||||
self.savefile = Some(new_savefile);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn load_last_active_savefile() -> Result<Option<Savefile>> {
|
||||
let state_path = DIRS.data_local_dir().join("active_savefile");
|
||||
|
||||
if !state_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let path = read_to_string(&state_path)?;
|
||||
|
||||
let savefile = Savefile::from_path(path.trim_end())?;
|
||||
|
||||
Ok(Some(savefile))
|
||||
}
|
@ -7,7 +7,7 @@ use crate::tui::State;
|
||||
|
||||
|
||||
pub fn render(state: &mut State, frame: &mut Frame, area: Rect) {
|
||||
match &state.persistent.savefile {
|
||||
match state.savefile() {
|
||||
Some(savefile) => render_info(savefile, frame, area),
|
||||
None => render_no_active_file(frame, area),
|
||||
}
|
||||
|
@ -18,20 +18,20 @@ pub fn render(state: &State, mut frame: &mut Frame, area: Rect) {
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
Mode::Normal if state.file_watcher.is_some() && state.persistent.savefile.is_some() => {
|
||||
let savefile = state.persistent.savefile.as_ref().unwrap();
|
||||
let text = format!("Watching file: {}", savefile.path.display());
|
||||
let status = Paragraph::new(text).block(status_block);
|
||||
frame.render_widget(status, area);
|
||||
Mode::Normal if state.is_watching_file() => {
|
||||
if let Some(savefile) = state.savefile() {
|
||||
let text = format!("Watching file: {}", savefile.path.display());
|
||||
let status = Paragraph::new(text).block(status_block);
|
||||
frame.render_widget(status, area);
|
||||
}
|
||||
}
|
||||
|
||||
Mode::Normal if state.persistent.savefile.is_none() => (),
|
||||
|
||||
Mode::Normal => {
|
||||
let savefile = state.persistent.savefile.as_ref().unwrap();
|
||||
let text = format!("Showing file: {}", savefile.path.display());
|
||||
let status = Paragraph::new(text).block(status_block);
|
||||
frame.render_widget(status, area);
|
||||
if let Some(savefile) = state.savefile() {
|
||||
let text = format!("Showing file: {}", savefile.path.display());
|
||||
let status = Paragraph::new(text).block(status_block);
|
||||
frame.render_widget(status, area);
|
||||
}
|
||||
}
|
||||
|
||||
Mode::SelectFile => render_file_select(&state, &mut frame, status_block, area),
|
||||
|
Loading…
Reference in New Issue
Block a user