Implement savefile persistence
This commit is contained in:
parent
c094c646f5
commit
0118dd0073
57
Cargo.lock
generated
57
Cargo.lock
generated
@ -278,6 +278,27 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.9.0"
|
||||
@ -326,6 +347,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
@ -438,6 +470,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.147"
|
||||
@ -511,6 +549,12 @@ version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "3.5.0"
|
||||
@ -597,6 +641,17 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"redox_syscall 0.2.16",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.4"
|
||||
@ -852,7 +907,9 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"crossterm 0.27.0",
|
||||
"directories",
|
||||
"jrny-save",
|
||||
"lazy_static",
|
||||
"notify",
|
||||
"ratatui",
|
||||
"tui-input",
|
||||
|
@ -7,6 +7,8 @@ license-file.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
directories = "5.0"
|
||||
lazy_static = "1.4"
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.3"
|
||||
|
@ -1,5 +1,6 @@
|
||||
mod edit;
|
||||
mod show;
|
||||
mod state;
|
||||
mod tui;
|
||||
mod watcher;
|
||||
|
||||
@ -32,7 +33,6 @@ pub(crate) enum CommandArgs {
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
|
||||
match &args.command {
|
||||
CommandArgs::Show(sub_args) => show::execute(&args, sub_args)?,
|
||||
|
||||
|
83
crates/wayfarer/src/state.rs
Normal file
83
crates/wayfarer/src/state.rs
Normal file
@ -0,0 +1,83 @@
|
||||
#![cfg(feature = "tui")]
|
||||
|
||||
use std::fs::{self, create_dir_all, read_to_string};
|
||||
use std::io::Write;
|
||||
use std::os::unix::prelude::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use directories::ProjectDirs;
|
||||
use jrny_save::Savefile;
|
||||
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref DIRS: ProjectDirs = {
|
||||
ProjectDirs::from("", "valeth", "wayfarer").unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PersistentState {
|
||||
pub savefile: Option<Savefile>,
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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,13 +1,14 @@
|
||||
#![cfg(feature = "tui")]
|
||||
|
||||
use std::io::{self, Stdout};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::{self, TryRecvError};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser as ArgParser;
|
||||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent};
|
||||
use crossterm::event::{
|
||||
DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers,
|
||||
};
|
||||
use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
};
|
||||
@ -15,23 +16,23 @@ use crossterm::{event, execute};
|
||||
use jrny_save::{Savefile, LEVEL_NAMES};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||
use ratatui::style::Style;
|
||||
use ratatui::widgets::{Block, Borders, Cell, Padding, Paragraph, Row, Table};
|
||||
use tui_input::backend::crossterm::EventHandler;
|
||||
use tui_input::Input;
|
||||
|
||||
use crate::state::PersistentState;
|
||||
#[cfg(feature = "watch")]
|
||||
use crate::watcher::FileWatcher;
|
||||
use crate::Args as AppArgs;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, ArgParser)]
|
||||
pub struct Args {
|
||||
path: PathBuf,
|
||||
}
|
||||
pub struct Args;
|
||||
|
||||
|
||||
struct State {
|
||||
current_file: Savefile,
|
||||
persistent: PersistentState,
|
||||
mode: Mode,
|
||||
file_select: Input,
|
||||
#[cfg(feature = "watch")]
|
||||
@ -44,8 +45,6 @@ struct State {
|
||||
enum Message {
|
||||
Exit,
|
||||
|
||||
ToggleFileSelect,
|
||||
|
||||
SetMode(Mode),
|
||||
|
||||
LoadFile,
|
||||
@ -58,11 +57,13 @@ enum Message {
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
Normal,
|
||||
|
||||
ShowError(String),
|
||||
|
||||
SelectFile,
|
||||
}
|
||||
|
||||
@ -71,14 +72,13 @@ type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
|
||||
type Frame<'a> = ratatui::Frame<'a, CrosstermBackend<Stdout>>;
|
||||
|
||||
|
||||
pub(crate) fn execute(_app_args: &AppArgs, args: &Args) -> Result<()> {
|
||||
pub(crate) fn execute(_app_args: &AppArgs, _args: &Args) -> Result<()> {
|
||||
let persistent = PersistentState::load()?;
|
||||
|
||||
let mut terminal = setup()?;
|
||||
|
||||
let savefile = Savefile::from_path(&args.path)?;
|
||||
|
||||
// TODO: prompt file path
|
||||
let state = State {
|
||||
current_file: savefile,
|
||||
persistent,
|
||||
mode: Mode::default(),
|
||||
file_select: Input::default(),
|
||||
#[cfg(feature = "watch")]
|
||||
@ -93,56 +93,67 @@ pub(crate) fn execute(_app_args: &AppArgs, args: &Args) -> Result<()> {
|
||||
}
|
||||
|
||||
|
||||
#[allow(unused_mut)]
|
||||
#[cfg_attr(not(feature = "watch"), allow(unused_mut))]
|
||||
fn run(terminal: &mut Terminal, mut state: State) -> Result<()> {
|
||||
let (mut evq_tx, evq_rx) = mpsc::channel::<Message>();
|
||||
let (mut msg_tx, msg_rx) = mpsc::channel::<Message>();
|
||||
|
||||
loop {
|
||||
terminal.draw(|frame| {
|
||||
render(&state, frame);
|
||||
render(&mut state, frame);
|
||||
})?;
|
||||
|
||||
handle_events(&mut evq_tx, &mut state)?;
|
||||
handle_events(&mut msg_tx, &mut state)?;
|
||||
|
||||
let message = match evq_rx.try_recv() {
|
||||
Ok(msg) => msg,
|
||||
Err(TryRecvError::Empty) => continue,
|
||||
match msg_rx.try_recv() {
|
||||
Ok(Message::Exit) => break,
|
||||
Ok(message) => {
|
||||
if let Err(err) = handle_message(&mut state, &mut msg_tx, message) {
|
||||
state.mode = Mode::ShowError(format!("{}", err));
|
||||
}
|
||||
}
|
||||
Err(TryRecvError::Empty) => (),
|
||||
Err(_) => break,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[cfg_attr(not(feature = "watch"), allow(unused_variables))]
|
||||
fn handle_message(
|
||||
state: &mut State,
|
||||
msg_tx: &mut mpsc::Sender<Message>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
match message {
|
||||
Message::Exit => break,
|
||||
|
||||
Message::SetMode(mode) => {
|
||||
state.mode = mode;
|
||||
}
|
||||
|
||||
Message::LoadFile => {
|
||||
let path = PathBuf::from(state.file_select.value());
|
||||
|
||||
state.current_file = Savefile::from_path(&path)?;
|
||||
state
|
||||
.persistent
|
||||
.set_active_savefile_path(state.file_select.value())?;
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
if state.file_watcher.is_some() {
|
||||
state.file_watcher = None;
|
||||
}
|
||||
}
|
||||
|
||||
Message::ToggleFileSelect => {
|
||||
state.mode = match state.mode {
|
||||
Mode::SelectFile => Mode::Normal,
|
||||
_ => Mode::SelectFile,
|
||||
};
|
||||
msg_tx.send(Message::SetMode(Mode::Normal))?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
Message::ToggleFileWatch => {
|
||||
Message::ToggleFileWatch if state.persistent.savefile.is_some() => {
|
||||
let savefile = state.persistent.savefile.as_ref().unwrap();
|
||||
|
||||
if state.file_watcher.is_none() {
|
||||
let evq_tx = evq_tx.clone();
|
||||
let evq_tx = msg_tx.clone();
|
||||
let callback = move || {
|
||||
evq_tx.send(Message::ReloadFile).unwrap();
|
||||
};
|
||||
let file_watcher = FileWatcher::new(&state.current_file.path, callback);
|
||||
let file_watcher = FileWatcher::new(&savefile.path, callback);
|
||||
state.file_watcher = Some(file_watcher);
|
||||
} else {
|
||||
state.file_watcher = None;
|
||||
@ -150,11 +161,11 @@ fn run(terminal: &mut Terminal, mut state: State) -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
Message::ReloadFile => {
|
||||
let savefile = Savefile::from_path(&state.current_file.path)?;
|
||||
state.current_file = savefile;
|
||||
}
|
||||
Message::ReloadFile if state.persistent.savefile.is_some() => {
|
||||
state.persistent.reload_active_savefile()?;
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -177,17 +188,24 @@ fn handle_events(event_queue: &mut mpsc::Sender<Message>, state: &mut State) ->
|
||||
|
||||
fn handle_keyboard_input(
|
||||
key: KeyEvent,
|
||||
event_queue: &mut mpsc::Sender<Message>,
|
||||
msg_tx: &mut mpsc::Sender<Message>,
|
||||
state: &mut State,
|
||||
) -> Result<()> {
|
||||
match (state.mode, key.code) {
|
||||
match (&state.mode, key.code) {
|
||||
(_, KeyCode::Char('q')) if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
msg_tx.send(Message::Exit)?;
|
||||
}
|
||||
|
||||
(_, KeyCode::Esc) => {
|
||||
event_queue.send(Message::SetMode(Mode::Normal))?;
|
||||
msg_tx.send(Message::SetMode(Mode::Normal))?;
|
||||
}
|
||||
|
||||
(Mode::ShowError(_), _) => {
|
||||
msg_tx.send(Message::SetMode(Mode::Normal))?;
|
||||
}
|
||||
|
||||
(Mode::SelectFile, KeyCode::Enter) => {
|
||||
event_queue.send(Message::LoadFile)?;
|
||||
event_queue.send(Message::ToggleFileSelect)?;
|
||||
msg_tx.send(Message::LoadFile)?;
|
||||
}
|
||||
|
||||
(Mode::SelectFile, _) => {
|
||||
@ -195,16 +213,16 @@ fn handle_keyboard_input(
|
||||
}
|
||||
|
||||
(Mode::Normal, KeyCode::Char('q')) => {
|
||||
event_queue.send(Message::Exit)?;
|
||||
msg_tx.send(Message::Exit)?;
|
||||
}
|
||||
|
||||
(Mode::Normal, KeyCode::Char('o')) => {
|
||||
event_queue.send(Message::ToggleFileSelect)?;
|
||||
msg_tx.send(Message::SetMode(Mode::SelectFile))?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
(Mode::Normal, KeyCode::Char('w')) => {
|
||||
event_queue.send(Message::ToggleFileWatch)?;
|
||||
msg_tx.send(Message::ToggleFileWatch)?;
|
||||
}
|
||||
|
||||
_ => (),
|
||||
@ -240,42 +258,78 @@ fn reset(mut terminal: Terminal) -> Result<()> {
|
||||
}
|
||||
|
||||
|
||||
fn render(state: &State, mut frame: &mut Frame) {
|
||||
fn render(state: &mut 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]);
|
||||
match &state.persistent.savefile {
|
||||
Some(savefile) => render_info(&savefile, &mut frame, rows[0]),
|
||||
None => render_no_active_file(&mut frame, rows[0]),
|
||||
}
|
||||
|
||||
render_status_bar(&state, &mut frame, rows[1]);
|
||||
}
|
||||
|
||||
|
||||
fn render_no_active_file(frame: &mut Frame, area: Rect) {
|
||||
let info_block = Block::default()
|
||||
.padding(Padding::horizontal(2))
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let info = Paragraph::new("No active file.\nPress 'o' to open a file, or 'q' to quit.")
|
||||
.block(info_block);
|
||||
|
||||
frame.render_widget(info, area);
|
||||
}
|
||||
|
||||
|
||||
fn render_status_bar(state: &State, mut frame: &mut Frame, area: Rect) {
|
||||
let status_block = Block::default().padding(Padding::horizontal(2));
|
||||
|
||||
match state.mode {
|
||||
#[cfg(feature = "watch")]
|
||||
Mode::Normal if state.file_watcher.is_some() => {
|
||||
let text = format!("Watching file: {}", state.current_file.path.display());
|
||||
let status = Paragraph::new(text).block(status_block);
|
||||
frame.render_widget(status, rows[1]);
|
||||
match &state.mode {
|
||||
Mode::ShowError(error_msg) => {
|
||||
let error_msg = Paragraph::new(error_msg.clone())
|
||||
.style(Style::default().fg(ratatui::style::Color::LightRed))
|
||||
.block(status_block);
|
||||
frame.render_widget(error_msg, area);
|
||||
}
|
||||
|
||||
#[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.persistent.savefile.is_none() => (),
|
||||
|
||||
Mode::Normal => {
|
||||
let text = format!("Showing file: {}", state.current_file.path.display());
|
||||
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, rows[1]);
|
||||
frame.render_widget(status, area);
|
||||
}
|
||||
|
||||
Mode::SelectFile => {
|
||||
let scroll = state.file_select.visual_scroll(rows[1].width as usize);
|
||||
let input = Paragraph::new(state.file_select.value())
|
||||
Mode::SelectFile => render_file_select(&state, &mut frame, status_block, area),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_file_select(state: &State, frame: &mut Frame, block: Block, area: Rect) {
|
||||
const PROMPT: &str = "Open file:";
|
||||
const PADDING: usize = 2;
|
||||
|
||||
let scroll = state.file_select.visual_scroll(area.width as usize);
|
||||
let input = Paragraph::new(format!("{} {}", PROMPT, state.file_select.value()))
|
||||
.scroll((0, scroll as u16))
|
||||
.block(status_block);
|
||||
frame.render_widget(input, rows[1]);
|
||||
.block(block);
|
||||
frame.render_widget(input, area);
|
||||
frame.set_cursor(
|
||||
rows[1].x + (state.file_select.visual_cursor() as u16) + 2,
|
||||
rows[1].y,
|
||||
area.x + (state.file_select.visual_cursor() + PROMPT.len() + 1 + PADDING) as u16,
|
||||
area.y,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user