diff --git a/README.md b/README.md index ed8054f..0514e72 100644 --- a/README.md +++ b/README.md @@ -30,4 +30,6 @@ wayfarer --path | q | Normal | Quits the application | | o | Normal | Open a new file | | w | Normal | Toggle file watcher mode (requires "watch" feature) | +| e | Normal | Enter edit mode | | H,J,K,L | Edit | Move between sections | +| h,j,k,l | Edit | Move inside the current section | diff --git a/crates/wayfarer/src/tui.rs b/crates/wayfarer/src/tui.rs index 674c429..0ab017d 100644 --- a/crates/wayfarer/src/tui.rs +++ b/crates/wayfarer/src/tui.rs @@ -19,7 +19,7 @@ use crossterm::terminal::{ use ratatui::backend::CrosstermBackend; use tracing::{debug, error, info}; -use self::state::{Mode, Section, State}; +use self::state::{Mode, State}; type Terminal = ratatui::Terminal>; @@ -33,6 +33,15 @@ pub struct Args { } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Direction { + Left, + Right, + Up, + Down, +} + + #[derive(Debug, Clone)] #[non_exhaustive] pub enum Message { @@ -47,13 +56,9 @@ pub enum Message { ReloadFile, - MoveSectionLeft, + MoveSection(Direction), - MoveSectionDown, - - MoveSectionUp, - - MoveSectionRight, + MoveCur(Direction), } @@ -117,6 +122,10 @@ fn handle_message( message: Message, ) -> Result<()> { match message { + Message::SetMode(Mode::Edit) => { + state.edit_current_file(); + } + Message::SetMode(mode) => { debug!("Setting mode to {:?}", mode); @@ -159,36 +168,12 @@ fn handle_message( state.reload_active_savefile()?; } - Message::MoveSectionLeft => { - state.active_section = match state.active_section { - Section::Companions => Section::General, - _ => Section::Companions, - }; + Message::MoveSection(direction) => { + state.move_section(direction); } - Message::MoveSectionRight => { - state.active_section = match state.active_section { - Section::Companions => Section::General, - _ => Section::Companions, - } - } - - Message::MoveSectionDown => { - state.active_section = match state.active_section { - Section::General => Section::Glyphs, - Section::Glyphs => Section::Murals, - Section::Murals => Section::General, - section => section, - }; - } - - Message::MoveSectionUp => { - state.active_section = match state.active_section { - Section::General => Section::Murals, - Section::Glyphs => Section::General, - Section::Murals => Section::Glyphs, - section => section, - } + Message::MoveCur(direction) => { + state.move_in_current_section(direction); } _ => (), diff --git a/crates/wayfarer/src/tui/events.rs b/crates/wayfarer/src/tui/events.rs index ee0ee04..488c3ae 100644 --- a/crates/wayfarer/src/tui/events.rs +++ b/crates/wayfarer/src/tui/events.rs @@ -7,7 +7,7 @@ use tracing::debug; use tui_input::backend::crossterm::EventHandler; use tui_input::Input; -use super::{Message, Mode, State}; +use super::{Direction, Message, Mode, State}; pub fn handle(event_queue: &mut mpsc::Sender, state: &mut State) -> Result<()> { @@ -78,19 +78,35 @@ fn handle_keyboard_input( } (Mode::Edit, KeyCode::Char('H')) => { - msg_tx.send(Message::MoveSectionLeft)?; + msg_tx.send(Message::MoveSection(Direction::Left))?; } (Mode::Edit, KeyCode::Char('J')) => { - msg_tx.send(Message::MoveSectionDown)?; + msg_tx.send(Message::MoveSection(Direction::Down))?; } (Mode::Edit, KeyCode::Char('K')) => { - msg_tx.send(Message::MoveSectionUp)?; + msg_tx.send(Message::MoveSection(Direction::Up))?; } (Mode::Edit, KeyCode::Char('L')) => { - msg_tx.send(Message::MoveSectionRight)?; + msg_tx.send(Message::MoveSection(Direction::Right))?; + } + + (Mode::Edit, KeyCode::Char('h')) => { + msg_tx.send(Message::MoveCur(Direction::Left))?; + } + + (Mode::Edit, KeyCode::Char('j')) => { + msg_tx.send(Message::MoveCur(Direction::Down))?; + } + + (Mode::Edit, KeyCode::Char('k')) => { + msg_tx.send(Message::MoveCur(Direction::Up))?; + } + + (Mode::Edit, KeyCode::Char('l')) => { + msg_tx.send(Message::MoveCur(Direction::Right))?; } (Mode::Normal, KeyCode::Char('e')) => { diff --git a/crates/wayfarer/src/tui/state.rs b/crates/wayfarer/src/tui/state.rs index b59e094..9e304df 100644 --- a/crates/wayfarer/src/tui/state.rs +++ b/crates/wayfarer/src/tui/state.rs @@ -5,9 +5,12 @@ use std::path::Path; use anyhow::Result; use jrny_save::Savefile; +use ratatui::widgets::TableState; use tracing::debug; use tui_input::Input; +use super::view::info::{GLYPHS_TABLE_RANGE, MURALS_TABLE_RANGE, STATS_TABLE_RANGE}; +use super::Direction; #[cfg(feature = "watch")] use crate::watcher::FileWatcher; use crate::DIRS; @@ -39,7 +42,11 @@ pub enum Section { #[derive(Default)] pub struct State { savefile: Option, + original_file: Option, pub active_section: Section, + pub stats_table: TableState, + pub glyphs_table: TableState, + pub murals_table: TableState, pub mode: Mode, pub file_select: Input, #[cfg(feature = "watch")] @@ -95,6 +102,16 @@ impl State { Ok(()) } + pub fn edit_current_file(&mut self) { + self.original_file = self.savefile.clone(); + self.select_section(self.active_section); + 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() @@ -125,6 +142,69 @@ impl State { 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)); + } + _ => (), + } } diff --git a/crates/wayfarer/src/tui/view.rs b/crates/wayfarer/src/tui/view.rs index 1fca915..6276c2d 100644 --- a/crates/wayfarer/src/tui/view.rs +++ b/crates/wayfarer/src/tui/view.rs @@ -1,5 +1,5 @@ -mod info; -mod status_bar; +pub mod info; +pub mod status_bar; use std::io::Stdout; diff --git a/crates/wayfarer/src/tui/view/info.rs b/crates/wayfarer/src/tui/view/info.rs index 4d277a8..8f1ab7c 100644 --- a/crates/wayfarer/src/tui/view/info.rs +++ b/crates/wayfarer/src/tui/view/info.rs @@ -1,6 +1,6 @@ -use jrny_save::{Savefile, LEVEL_NAMES}; +use jrny_save::LEVEL_NAMES; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; -use ratatui::style::Style; +use ratatui::style::{Color, Style}; use ratatui::widgets::{Block, Borders, Cell, Padding, Paragraph, Row, Table}; use crate::tui::state::{Mode, Section}; @@ -8,10 +8,16 @@ use crate::tui::view::Frame; use crate::tui::State; +pub const STATS_TABLE_RANGE: (usize, usize) = (0, 9); +pub const GLYPHS_TABLE_RANGE: (usize, usize) = (0, 5); +pub const MURALS_TABLE_RANGE: (usize, usize) = (0, 6); + + pub fn render(state: &mut State, frame: &mut Frame, area: Rect) { - match state.savefile() { - Some(savefile) => render_info(savefile, state, frame, area), - None => render_no_active_file(frame, area), + if state.is_savefile_loaded() { + render_info(state, frame, area); + } else { + render_no_active_file(frame, area); } } @@ -28,7 +34,7 @@ fn render_no_active_file(frame: &mut Frame, area: Rect) { } -fn render_info(savefile: &Savefile, state: &State, mut frame: &mut Frame, area: Rect) { +fn render_info(state: &mut State, mut frame: &mut Frame, area: Rect) { let columns = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) @@ -48,16 +54,22 @@ fn render_info(savefile: &Savefile, state: &State, mut frame: &mut Frame, area: .constraints([Constraint::Ratio(10, 10)]) .split(columns[1]); - render_stats(&savefile, state, &mut frame, left_column[0]); - render_glyphs(&savefile, state, &mut frame, left_column[1]); - render_murals(&savefile, state, &mut frame, left_column[2]); - render_companions(&savefile, state, &mut frame, right_column[0]); + render_stats(state, &mut frame, left_column[0]); + render_glyphs(state, &mut frame, left_column[1]); + render_murals(state, &mut frame, left_column[2]); + render_companions(state, &mut frame, right_column[0]); } -fn render_stats<'a>(savefile: &Savefile, state: &State, frame: &mut Frame, area: Rect) { - let border_style = if state.active_section == Section::General && state.mode == Mode::Edit { - Style::default().fg(ratatui::style::Color::Blue) +fn render_stats<'a>(state: &mut State, frame: &mut Frame, area: Rect) { + let Some(savefile) = state.savefile() else { + return + }; + + let is_selected = state.active_section == Section::General && state.mode == Mode::Edit; + + let border_style = if is_selected { + Style::default().fg(Color::Blue) } else { Style::default() }; @@ -74,6 +86,12 @@ fn render_stats<'a>(savefile: &Savefile, state: &State, frame: &mut Frame, area: let stats_block = Block::default().title("Stats"); + let table_highlight = if is_selected { + Style::default().fg(Color::Blue) + } else { + Style::default() + }; + let table = Table::new([ Row::new([ "Journeys Completed".to_string(), @@ -101,6 +119,7 @@ fn render_stats<'a>(savefile: &Savefile, state: &State, frame: &mut Frame, area: 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); @@ -110,12 +129,18 @@ fn render_stats<'a>(savefile: &Savefile, state: &State, frame: &mut Frame, area: frame.render_widget(stats_section_block, area); frame.render_widget(cur_symbol, layout[0]); - frame.render_widget(table, layout[1]); + frame.render_stateful_widget(table, layout[1], &mut state.stats_table); } -fn render_companions<'a>(savefile: &Savefile, state: &State, frame: &mut Frame, area: Rect) { - let border_style = if state.active_section == Section::Companions && state.mode == Mode::Edit { +fn render_companions<'a>(state: &State, frame: &mut Frame, area: Rect) { + let Some(savefile) = state.savefile() else { + return + }; + + let is_selected = state.active_section == Section::Companions && state.mode == Mode::Edit; + + let border_style = if is_selected { Style::default().fg(ratatui::style::Color::Blue) } else { Style::default() @@ -164,12 +189,19 @@ fn render_companions<'a>(savefile: &Savefile, state: &State, frame: &mut Frame, } -fn render_glyphs<'a>(savefile: &Savefile, state: &State, frame: &mut Frame, area: Rect) { +fn render_glyphs<'a>(state: &mut State, frame: &mut Frame, area: Rect) { const FOUND_SIGN: &str = "◆"; const NOT_FOUND_SIGN: &str = "◇"; - let border_style = if state.active_section == Section::Glyphs && state.mode == Mode::Edit { - Style::default().fg(ratatui::style::Color::Blue) + let Some(savefile) = state.savefile() else { + return + }; + + let is_selected = state.active_section == Section::Glyphs && state.mode == Mode::Edit; + + + let border_style = if is_selected { + Style::default().fg(Color::Blue) } else { Style::default() }; @@ -180,6 +212,12 @@ fn render_glyphs<'a>(savefile: &Savefile, state: &State, frame: &mut Frame, area .borders(Borders::ALL) .padding(Padding::new(2, 2, 1, 1)); + let table_highlight = if is_selected { + Style::default().fg(Color::Blue) + } else { + Style::default() + }; + let table = Table::new(savefile.glyphs.all().map(|(level_number, status)| { let status = status .iter() @@ -198,18 +236,25 @@ fn render_glyphs<'a>(savefile: &Savefile, state: &State, frame: &mut Frame, area Constraint::Length(3), ]) .column_spacing(1) + .highlight_style(table_highlight) .block(block); - frame.render_widget(table, area); + frame.render_stateful_widget(table, area, &mut state.glyphs_table); } -fn render_murals<'a>(savefile: &Savefile, state: &State, frame: &mut Frame, area: Rect) { +fn render_murals<'a>(state: &mut State, frame: &mut Frame, area: Rect) { const FOUND_SIGN: &str = "▾"; const NOT_FOUND_SIGN: &str = "▿"; - let border_style = if state.active_section == Section::Murals && state.mode == Mode::Edit { - Style::default().fg(ratatui::style::Color::Blue) + let Some(savefile) = state.savefile() else { + return + }; + + let is_selected = state.active_section == Section::Murals && state.mode == Mode::Edit; + + let border_style = if is_selected { + Style::default().fg(Color::Blue) } else { Style::default() }; @@ -220,6 +265,12 @@ fn render_murals<'a>(savefile: &Savefile, state: &State, frame: &mut Frame, area .borders(Borders::ALL) .padding(Padding::new(2, 2, 1, 1)); + let table_highlight = if is_selected { + Style::default().fg(Color::Blue) + } else { + Style::default() + }; + let table = Table::new(savefile.murals.all().map(|(level_number, status)| { let status = status .iter() @@ -238,7 +289,8 @@ fn render_murals<'a>(savefile: &Savefile, state: &State, frame: &mut Frame, area Constraint::Length(3), ]) .column_spacing(1) + .highlight_style(table_highlight) .block(block); - frame.render_widget(table, area); + frame.render_stateful_widget(table, area, &mut state.murals_table); }