388 lines
11 KiB
Rust
388 lines
11 KiB
Rust
use core::fmt;
|
|
use std::fs::{self, create_dir_all, read_to_string, OpenOptions};
|
|
use std::io::Write;
|
|
use std::os::unix::prelude::OsStrExt;
|
|
use std::path::Path;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use anyhow::{bail, Context, Result};
|
|
use jrny_save::Savefile;
|
|
use ratatui::widgets::TableState;
|
|
use tracing::{debug, error};
|
|
use tui_input::Input;
|
|
|
|
use super::view::info::glyphs::TABLE_RANGE as GLYPHS_TABLE_RANGE;
|
|
use super::view::info::murals::TABLE_RANGE as MURALS_TABLE_RANGE;
|
|
use super::view::info::stats::TABLE_RANGE as STATS_TABLE_RANGE;
|
|
use super::Direction;
|
|
#[cfg(feature = "watch")]
|
|
use crate::watcher::FileWatcher;
|
|
use crate::DIRS;
|
|
|
|
|
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
|
pub enum Mode {
|
|
#[default]
|
|
Normal,
|
|
|
|
Edit,
|
|
|
|
Insert,
|
|
|
|
SelectFile,
|
|
}
|
|
|
|
impl Mode {
|
|
pub fn is_editing(&self) -> bool {
|
|
self == &Self::Edit || self == &Self::Insert
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Section {
|
|
#[default]
|
|
General,
|
|
Glyphs,
|
|
Murals,
|
|
Companions,
|
|
}
|
|
|
|
|
|
#[derive(Default)]
|
|
pub struct State {
|
|
pub savefile: Option<Savefile>,
|
|
pub original_file: Option<Savefile>,
|
|
pub active_section: Section,
|
|
pub stats_table: TableState,
|
|
pub glyphs_table: TableState,
|
|
pub murals_table: TableState,
|
|
pub error_msg: Option<(Instant, String)>,
|
|
pub mode: Mode,
|
|
pub prompt_save: bool,
|
|
pub edit_input: Option<Input>,
|
|
pub file_select: Input,
|
|
#[cfg(feature = "watch")]
|
|
file_watcher: Option<FileWatcher>,
|
|
}
|
|
|
|
|
|
impl State {
|
|
const ERROR_MSG_DURATION: Duration = Duration::new(3, 0);
|
|
|
|
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,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
pub fn show_error_message<S>(&mut self, msg: S)
|
|
where
|
|
S: fmt::Display,
|
|
{
|
|
let until = Instant::now() + Self::ERROR_MSG_DURATION;
|
|
error!(%msg);
|
|
self.error_msg = Some((until, msg.to_string()));
|
|
}
|
|
|
|
pub fn clear_expired_error_message(&mut self) {
|
|
if let Some((until, _)) = self.error_msg {
|
|
if Instant::now() >= until {
|
|
self.error_msg = None;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn clear_error_message(&mut self) {
|
|
self.error_msg.take();
|
|
}
|
|
|
|
pub fn set_savefile_from_path<P>(&mut self, path: P) -> Result<()>
|
|
where
|
|
P: AsRef<Path>,
|
|
{
|
|
let savefile = Savefile::from_path(path)?;
|
|
self.savefile = Some(savefile);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
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(())
|
|
}
|
|
|
|
pub fn edit_current_file(&mut self) {
|
|
if !self.mode.is_editing() {
|
|
self.original_file = self.savefile.clone();
|
|
self.select_section(self.active_section);
|
|
self.mode = Mode::Edit;
|
|
}
|
|
}
|
|
|
|
pub fn start_editing_entry(&mut self) {
|
|
self.mode = Mode::Insert;
|
|
}
|
|
|
|
#[tracing::instrument(skip_all)]
|
|
pub fn commit_entry_edit(&mut self) -> Result<()> {
|
|
debug!(section = ?self.active_section);
|
|
|
|
match self.active_section {
|
|
Section::General => self.edit_stats_section()?,
|
|
_ => (),
|
|
}
|
|
|
|
self.mode = Mode::Edit;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn edit_stats_section(&mut self) -> Result<()> {
|
|
let Some(savefile) = &mut self.savefile else {
|
|
bail!("No savefile loaded");
|
|
};
|
|
|
|
let input = self.edit_input.take().context("no edit input")?;
|
|
let value = input.value();
|
|
|
|
match self.stats_table.selected().context("no selection")? {
|
|
0 => savefile.journey_count = value.parse()?,
|
|
1 => savefile.total_companions_met = value.parse()?,
|
|
2 => savefile.total_collected_symbols = value.parse()?,
|
|
3 => savefile.current_level.set_by_name(value)?,
|
|
4 => savefile.companions_met = value.parse()?,
|
|
5 => savefile.scarf_length.set_length(value.parse()?)?,
|
|
6 => savefile.symbol.set_by_id(value.parse()?)?,
|
|
7 => savefile.robe.set_color(value.parse()?),
|
|
8 => savefile.robe.set_tier(value.parse()?)?,
|
|
9 => {}
|
|
idx => debug!("unknown index {:?}", idx),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn next_entry_value(&mut self) -> Result<()> {
|
|
match self.active_section {
|
|
Section::General => self.next_stats_entry_value()?,
|
|
_ => (),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn next_stats_entry_value(&mut self) -> Result<()> {
|
|
let Some(savefile) = &mut self.savefile else {
|
|
bail!("No savefile loaded");
|
|
};
|
|
|
|
match self.stats_table.selected().context("no selection")? {
|
|
0 => savefile.journey_count += 1,
|
|
1 => savefile.total_companions_met += 1,
|
|
2 => savefile.total_collected_symbols += 1,
|
|
3 => savefile.current_level = savefile.current_level.wrapping_next(),
|
|
4 => savefile.companions_met += 1,
|
|
5 => savefile.scarf_length.increase_length()?,
|
|
6 => savefile.symbol = savefile.symbol.wrapping_next(),
|
|
7 => savefile.robe.swap_colors(),
|
|
8 => savefile.robe.increase_tier(),
|
|
9 => {}
|
|
idx => debug!("unknown index {:?}", idx),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn previous_entry_value(&mut self) -> Result<()> {
|
|
match self.active_section {
|
|
Section::General => self.previous_stats_entry_value()?,
|
|
_ => (),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn previous_stats_entry_value(&mut self) -> Result<()> {
|
|
let Some(savefile) = &mut self.savefile else {
|
|
bail!("No savefile loaded");
|
|
};
|
|
|
|
match self.stats_table.selected().context("no selection")? {
|
|
0 => savefile.journey_count = savefile.journey_count.saturating_sub(1),
|
|
1 => savefile.total_companions_met = savefile.total_companions_met.saturating_sub(1),
|
|
2 => {
|
|
savefile.total_collected_symbols =
|
|
savefile.total_collected_symbols.saturating_sub(1)
|
|
}
|
|
3 => savefile.current_level = savefile.current_level.wrapping_previous(),
|
|
4 => savefile.companions_met = savefile.companions_met.saturating_sub(1),
|
|
5 => savefile.scarf_length.decrease_length()?,
|
|
6 => savefile.symbol = savefile.symbol.wrapping_previous(),
|
|
7 => savefile.robe.swap_colors(),
|
|
8 => savefile.robe.decrease_tier(),
|
|
9 => {}
|
|
idx => debug!("unknown index {:?}", idx),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn cancel_editing_entry(&mut self) {
|
|
if self.mode == Mode::Insert {
|
|
self.edit_input = None;
|
|
self.mode = Mode::Edit;
|
|
}
|
|
}
|
|
|
|
pub fn is_savefile_loaded(&self) -> bool {
|
|
self.savefile.is_some()
|
|
}
|
|
|
|
#[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(())
|
|
}
|
|
|
|
pub fn save_edited_file(&mut self) -> Result<()> {
|
|
let path = self.file_select.value();
|
|
|
|
let savefile = self.savefile.as_ref().context("no active savefile")?;
|
|
|
|
let outfile = OpenOptions::new()
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(true)
|
|
.open(path)?;
|
|
|
|
savefile.write(outfile)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn move_section(&mut self, direction: Direction) {
|
|
let next_section = match (direction, self.active_section) {
|
|
(Direction::Left, Section::Companions) => Section::General,
|
|
(Direction::Left, _) => Section::Companions,
|
|
(Direction::Right, Section::Companions) => Section::General,
|
|
(Direction::Right, _) => Section::Companions,
|
|
(Direction::Down, Section::General) => Section::Glyphs,
|
|
(Direction::Down, Section::Glyphs) => Section::Murals,
|
|
(Direction::Down, Section::Murals) => Section::General,
|
|
(Direction::Down, section) => section,
|
|
(Direction::Up, Section::General) => Section::Murals,
|
|
(Direction::Up, Section::Murals) => Section::Glyphs,
|
|
(Direction::Up, Section::Glyphs) => Section::General,
|
|
(Direction::Up, section) => section,
|
|
};
|
|
|
|
self.select_section(next_section);
|
|
self.active_section = next_section;
|
|
}
|
|
|
|
fn select_section(&mut self, section: Section) {
|
|
let table = match section {
|
|
Section::General => &mut self.stats_table,
|
|
Section::Glyphs => &mut self.glyphs_table,
|
|
Section::Murals => &mut self.murals_table,
|
|
_ => return,
|
|
};
|
|
|
|
if table.selected().is_none() {
|
|
table.select(Some(0));
|
|
}
|
|
}
|
|
|
|
pub fn move_in_current_section(&mut self, direction: Direction) {
|
|
match self.active_section {
|
|
Section::General => {
|
|
select_row_in_range(&mut self.stats_table, direction, STATS_TABLE_RANGE)
|
|
}
|
|
Section::Glyphs => {
|
|
select_row_in_range(&mut self.glyphs_table, direction, GLYPHS_TABLE_RANGE)
|
|
}
|
|
Section::Murals => {
|
|
select_row_in_range(&mut self.murals_table, direction, MURALS_TABLE_RANGE)
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
fn select_row_in_range(table: &mut TableState, direction: Direction, (min, max): (usize, usize)) {
|
|
match (direction, table.selected()) {
|
|
(Direction::Up, Some(i)) if i <= min => (),
|
|
(Direction::Up, Some(i)) => {
|
|
table.select(Some(i - 1));
|
|
}
|
|
(Direction::Down, Some(i)) if i >= max => (),
|
|
(Direction::Down, Some(i)) => {
|
|
table.select(Some(i + 1));
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
|
|
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))
|
|
}
|