Implement edit functionality for general stats
This commit is contained in:
parent
13e02fa883
commit
f17d128518
@ -54,6 +54,16 @@ pub enum Message {
|
||||
#[cfg(feature = "watch")]
|
||||
ToggleFileWatch,
|
||||
|
||||
StartEditEntry,
|
||||
|
||||
CommitEditEntry,
|
||||
|
||||
CancelEditEntry,
|
||||
|
||||
NextEntryValue,
|
||||
|
||||
PreviousEntryValue,
|
||||
|
||||
ReloadFile,
|
||||
|
||||
MoveSection(Direction),
|
||||
@ -122,16 +132,12 @@ fn handle_message(
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
match message {
|
||||
Message::SetMode(Mode::Edit) => {
|
||||
state.edit_current_file();
|
||||
}
|
||||
|
||||
Message::SetMode(Mode::Edit) => state.edit_current_file(),
|
||||
Message::SetMode(mode) => {
|
||||
debug!("Setting mode to {:?}", mode);
|
||||
|
||||
state.mode = mode;
|
||||
}
|
||||
|
||||
Message::LoadFile => {
|
||||
let file_path = state.file_select.value();
|
||||
info!("Loading file {}", file_path);
|
||||
@ -145,10 +151,9 @@ fn handle_message(
|
||||
|
||||
msg_tx.send(Message::SetMode(Mode::Normal))?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
Message::ToggleFileWatch => {
|
||||
if let Some(savefile) = state.savefile() {
|
||||
if let Some(savefile) = &state.savefile {
|
||||
if state.is_watching_file() {
|
||||
let evq_tx = msg_tx.clone();
|
||||
let callback = move || {
|
||||
@ -163,19 +168,26 @@ fn handle_message(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message::ReloadFile => {
|
||||
state.reload_active_savefile()?;
|
||||
Message::ReloadFile => state.reload_active_savefile()?,
|
||||
Message::MoveSection(direction) => state.move_section(direction),
|
||||
Message::MoveCur(direction) => state.move_in_current_section(direction),
|
||||
Message::StartEditEntry => state.start_editing_entry(),
|
||||
Message::CommitEditEntry => {
|
||||
if let Err(err) = state.commit_entry_edit() {
|
||||
error!(%err);
|
||||
}
|
||||
}
|
||||
|
||||
Message::MoveSection(direction) => {
|
||||
state.move_section(direction);
|
||||
Message::CancelEditEntry => state.cancel_editing_entry(),
|
||||
Message::NextEntryValue => {
|
||||
if let Err(err) = state.next_entry_value() {
|
||||
error!(%err);
|
||||
}
|
||||
}
|
||||
|
||||
Message::MoveCur(direction) => {
|
||||
state.move_in_current_section(direction);
|
||||
Message::PreviousEntryValue => {
|
||||
if let Err(err) = state.previous_entry_value() {
|
||||
error!(%err);
|
||||
}
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
|
||||
|
@ -49,6 +49,10 @@ fn handle_keyboard_input(
|
||||
msg_tx.send(Message::Exit)?;
|
||||
}
|
||||
|
||||
(Mode::Insert, KeyCode::Esc) => {
|
||||
msg_tx.send(Message::CancelEditEntry)?;
|
||||
}
|
||||
|
||||
(_, KeyCode::Esc) => {
|
||||
msg_tx.send(Message::SetMode(Mode::Normal))?;
|
||||
}
|
||||
@ -109,6 +113,28 @@ fn handle_keyboard_input(
|
||||
msg_tx.send(Message::MoveCur(Direction::Right))?;
|
||||
}
|
||||
|
||||
(Mode::Edit, KeyCode::Enter) => {
|
||||
msg_tx.send(Message::StartEditEntry)?;
|
||||
}
|
||||
|
||||
(Mode::Edit, KeyCode::Char('n')) => {
|
||||
msg_tx.send(Message::NextEntryValue)?;
|
||||
}
|
||||
|
||||
(Mode::Edit, KeyCode::Char('p')) => {
|
||||
msg_tx.send(Message::PreviousEntryValue)?;
|
||||
}
|
||||
|
||||
(Mode::Insert, KeyCode::Enter) => {
|
||||
msg_tx.send(Message::CommitEditEntry)?;
|
||||
}
|
||||
|
||||
(Mode::Insert, _) => {
|
||||
if let Some(input) = &mut state.edit_input {
|
||||
input.handle_event(&Event::Key(key));
|
||||
}
|
||||
}
|
||||
|
||||
(Mode::Normal, KeyCode::Char('e')) => {
|
||||
msg_tx.send(Message::SetMode(Mode::Edit))?;
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ use std::io::Write;
|
||||
use std::os::unix::prelude::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use jrny_save::Savefile;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use jrny_save::{RobeColor, Savefile, LEVEL_NAMES};
|
||||
use ratatui::widgets::TableState;
|
||||
use tracing::debug;
|
||||
use tui_input::Input;
|
||||
@ -25,11 +25,19 @@ pub enum Mode {
|
||||
|
||||
Edit,
|
||||
|
||||
Insert,
|
||||
|
||||
ShowError(String),
|
||||
|
||||
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 {
|
||||
@ -43,13 +51,14 @@ pub enum Section {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
savefile: Option<Savefile>,
|
||||
pub savefile: Option<Savefile>,
|
||||
original_file: Option<Savefile>,
|
||||
pub active_section: Section,
|
||||
pub stats_table: TableState,
|
||||
pub glyphs_table: TableState,
|
||||
pub murals_table: TableState,
|
||||
pub mode: Mode,
|
||||
pub edit_input: Option<Input>,
|
||||
pub file_select: Input,
|
||||
#[cfg(feature = "watch")]
|
||||
file_watcher: Option<FileWatcher>,
|
||||
@ -72,10 +81,6 @@ impl State {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn savefile(&self) -> Option<&Savefile> {
|
||||
self.savefile.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_savefile_from_path<P>(&mut self, path: P) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
@ -105,13 +110,209 @@ impl State {
|
||||
}
|
||||
|
||||
pub fn edit_current_file(&mut self) {
|
||||
self.original_file = self.savefile.clone();
|
||||
self.select_section(self.active_section);
|
||||
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 => {
|
||||
let level_id = LEVEL_NAMES
|
||||
.iter()
|
||||
.position(|&v| v == value.trim_end())
|
||||
.context("invalid level name")?;
|
||||
savefile.current_level = level_id as u64;
|
||||
}
|
||||
4 => savefile.companions_met = value.parse()?,
|
||||
5 => {
|
||||
let new_length = value.parse()?;
|
||||
if new_length > 30 {
|
||||
bail!("Max length exceeded");
|
||||
}
|
||||
|
||||
savefile.scarf_length = new_length;
|
||||
}
|
||||
6 => {
|
||||
let next_symbol_id = value.parse()?;
|
||||
|
||||
if next_symbol_id > 20 {
|
||||
bail!("Symbol id out of range");
|
||||
}
|
||||
|
||||
savefile.symbol.id = next_symbol_id;
|
||||
}
|
||||
7 => {
|
||||
let new_color = match value {
|
||||
"Red" | "red" => RobeColor::Red,
|
||||
"White" | "white" => RobeColor::White,
|
||||
_ => bail!("invalid robe color"),
|
||||
};
|
||||
savefile.set_robe_color(new_color);
|
||||
}
|
||||
8 => savefile.set_robe_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 => {
|
||||
let next_level = savefile.current_level + 1;
|
||||
savefile.current_level = if next_level >= LEVEL_NAMES.len() as u64 {
|
||||
0
|
||||
} else {
|
||||
savefile.current_level + 1
|
||||
};
|
||||
}
|
||||
4 => savefile.companions_met += 1,
|
||||
5 => {
|
||||
if savefile.scarf_length < 30 {
|
||||
savefile.scarf_length += 1;
|
||||
}
|
||||
}
|
||||
6 => {
|
||||
let next_symbol = savefile.symbol.id + 1;
|
||||
savefile.symbol.id = if next_symbol > 20 {
|
||||
0
|
||||
} else {
|
||||
savefile.symbol.id + 1
|
||||
};
|
||||
}
|
||||
7 => {
|
||||
savefile.set_robe_color(match savefile.robe_color() {
|
||||
RobeColor::Red => RobeColor::White,
|
||||
RobeColor::White => RobeColor::Red,
|
||||
});
|
||||
}
|
||||
8 => {
|
||||
let next_tier = savefile.robe_tier() + 1;
|
||||
savefile.set_robe_tier(next_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 => {
|
||||
let next_level: i64 = savefile.current_level as i64 - 1;
|
||||
savefile.current_level = if next_level < 0 {
|
||||
LEVEL_NAMES.len() as u64 - 1
|
||||
} else {
|
||||
next_level as u64
|
||||
};
|
||||
}
|
||||
4 => savefile.companions_met = savefile.companions_met.saturating_sub(1),
|
||||
5 => {
|
||||
if savefile.scarf_length > 0 {
|
||||
savefile.scarf_length = savefile.scarf_length.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
6 => {
|
||||
let next_symbol = savefile.symbol.id as i32 - 1;
|
||||
savefile.symbol.id = if next_symbol < 0 {
|
||||
20
|
||||
} else {
|
||||
next_symbol as u32
|
||||
};
|
||||
}
|
||||
7 => {
|
||||
savefile.set_robe_color(match savefile.robe_color() {
|
||||
RobeColor::Red => RobeColor::White,
|
||||
RobeColor::White => RobeColor::Red,
|
||||
});
|
||||
}
|
||||
8 => {
|
||||
let next_tier = savefile.robe_tier().saturating_sub(1);
|
||||
savefile.set_robe_tier(next_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()
|
||||
self.savefile.is_some()
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
|
@ -8,7 +8,7 @@ use crate::tui::State;
|
||||
|
||||
|
||||
pub(super) fn render<'a>(state: &State, frame: &mut Frame, area: Rect) {
|
||||
let Some(savefile) = state.savefile() else {
|
||||
let Some(savefile) = &state.savefile else {
|
||||
return
|
||||
};
|
||||
|
||||
|
@ -15,7 +15,7 @@ pub(super) fn render<'a>(state: &mut State, frame: &mut Frame, area: Rect) {
|
||||
const FOUND_SIGN: &str = "◆";
|
||||
const NOT_FOUND_SIGN: &str = "◇";
|
||||
|
||||
let Some(savefile) = state.savefile() else {
|
||||
let Some(savefile) = &state.savefile else {
|
||||
return
|
||||
};
|
||||
|
||||
|
@ -15,7 +15,7 @@ pub(super) fn render<'a>(state: &mut State, frame: &mut Frame, area: Rect) {
|
||||
const FOUND_SIGN: &str = "▾";
|
||||
const NOT_FOUND_SIGN: &str = "▿";
|
||||
|
||||
let Some(savefile) = state.savefile() else {
|
||||
let Some(savefile) = &state.savefile else {
|
||||
return
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::{Block, Borders, Padding, Paragraph, Row, Table};
|
||||
use ratatui::widgets::{Block, Borders, Cell, Padding, Paragraph, Row, Table};
|
||||
use tui_input::Input;
|
||||
|
||||
use crate::tui::state::{Mode, Section};
|
||||
use crate::tui::view::Frame;
|
||||
@ -11,11 +12,11 @@ pub const TABLE_RANGE: (usize, usize) = (0, 9);
|
||||
|
||||
|
||||
pub(super) fn render<'a>(state: &mut State, frame: &mut Frame, area: Rect) {
|
||||
let Some(savefile) = state.savefile() else {
|
||||
let Some(savefile) = &state.savefile else {
|
||||
return
|
||||
};
|
||||
|
||||
let is_selected = state.active_section == Section::General && state.mode == Mode::Edit;
|
||||
let is_selected = state.active_section == Section::General && state.mode.is_editing();
|
||||
|
||||
let border_style = if is_selected {
|
||||
Style::default().fg(Color::Blue)
|
||||
@ -41,36 +42,54 @@ pub(super) fn render<'a>(state: &mut State, frame: &mut Frame, area: Rect) {
|
||||
Style::default()
|
||||
};
|
||||
|
||||
let table = Table::new([
|
||||
Row::new([
|
||||
"Journeys Completed".to_string(),
|
||||
savefile.journey_count.to_string(),
|
||||
]),
|
||||
Row::new([
|
||||
"Total Companions Met".to_string(),
|
||||
let rows = [
|
||||
("Journeys Completed", savefile.journey_count.to_string()),
|
||||
(
|
||||
"Total Companions Met",
|
||||
savefile.total_companions_met.to_string(),
|
||||
]),
|
||||
Row::new([
|
||||
"Total Symbols Collected".to_string(),
|
||||
),
|
||||
(
|
||||
"Total Symbols Collected",
|
||||
savefile.total_collected_symbols.to_string(),
|
||||
]),
|
||||
Row::new(["Current Level", savefile.current_level_name()]),
|
||||
Row::new([
|
||||
"Companions Met".to_string(),
|
||||
savefile.companions_met.to_string(),
|
||||
]),
|
||||
Row::new([
|
||||
"Scarf Length".to_string(),
|
||||
savefile.scarf_length.to_string(),
|
||||
]),
|
||||
Row::new(["Symbol Number".to_string(), savefile.symbol.id.to_string()]),
|
||||
Row::new(["Robe Color".to_string(), savefile.robe_color().to_string()]),
|
||||
Row::new(["Robe Tier".to_string(), savefile.robe_tier().to_string()]),
|
||||
Row::new(["Last Played".to_string(), savefile.last_played.to_string()]),
|
||||
])
|
||||
.highlight_style(table_highlight)
|
||||
.widths(&[Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
|
||||
.block(stats_block);
|
||||
),
|
||||
("Current Level", savefile.current_level_name().to_string()),
|
||||
("Companions Met", savefile.companions_met.to_string()),
|
||||
("Scarf Length", savefile.scarf_length.to_string()),
|
||||
("Symbol Number", savefile.symbol.id.to_string()),
|
||||
("Robe Color", savefile.robe_color().to_string()),
|
||||
("Robe Tier", savefile.robe_tier().to_string()),
|
||||
("Last Played", savefile.last_played.to_string()),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, (title, value))| {
|
||||
let value = match state.stats_table.selected() {
|
||||
Some(sel) if sel == idx => {
|
||||
let value = match state.mode {
|
||||
Mode::Insert => {
|
||||
if state.edit_input.is_none() {
|
||||
state.edit_input.replace(Input::new(value));
|
||||
}
|
||||
|
||||
let input = state.edit_input.as_ref().unwrap();
|
||||
input.value().to_string()
|
||||
}
|
||||
Mode::Edit => format!("< {} >", value),
|
||||
_ => value.to_string(),
|
||||
};
|
||||
|
||||
Cell::from(value)
|
||||
}
|
||||
_ => Cell::from(value.to_string()),
|
||||
};
|
||||
|
||||
Row::new([Cell::from(title), value])
|
||||
});
|
||||
|
||||
let table = Table::new(rows)
|
||||
.highlight_style(table_highlight)
|
||||
.widths(&[Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
|
||||
.block(stats_block);
|
||||
|
||||
let cur_symbol_block = Block::default();
|
||||
|
||||
@ -79,4 +98,15 @@ pub(super) fn render<'a>(state: &mut State, frame: &mut Frame, area: Rect) {
|
||||
frame.render_widget(stats_section_block, area);
|
||||
frame.render_widget(cur_symbol, layout[0]);
|
||||
frame.render_stateful_widget(table, layout[1], &mut state.stats_table);
|
||||
|
||||
if state.mode == Mode::Insert {
|
||||
if let Some(idx) = state.stats_table.selected() {
|
||||
if let Some(input) = &state.edit_input {
|
||||
frame.set_cursor(
|
||||
layout[1].x + layout[1].width / 3 + (input.visual_cursor() + 1) as u16,
|
||||
layout[1].y + (idx + 1 - state.stats_table.offset()) as u16,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +17,8 @@ pub fn render(state: &State, mut frame: &mut Frame, area: Rect) {
|
||||
frame.render_widget(error_msg, area);
|
||||
}
|
||||
|
||||
Mode::Edit => {
|
||||
if let Some(savefile) = state.savefile() {
|
||||
Mode::Edit | Mode::Insert => {
|
||||
if let Some(savefile) = &state.savefile {
|
||||
let text = format!("Editing file: {}", savefile.path.display());
|
||||
let status = Paragraph::new(text).block(status_block);
|
||||
frame.render_widget(status, area);
|
||||
@ -27,22 +27,22 @@ pub fn render(state: &State, mut frame: &mut Frame, area: Rect) {
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
Mode::Normal if state.is_watching_file() => {
|
||||
if let Some(savefile) = state.savefile() {
|
||||
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 let Some(savefile) = state.savefile() {
|
||||
Mode::SelectFile => render_file_select(&state, &mut frame, status_block, 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