diff --git a/Cargo.lock b/Cargo.lock index 92b15aa..0991c97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -496,6 +496,41 @@ dependencies = [ "thiserror", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.3.11" @@ -505,6 +540,29 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive-getters" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_setters" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8ef033054e131169b8f0f9a7af8f5533a9436fadf3c500ed547f730f07090d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -516,6 +574,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + [[package]] name = "either" version = "1.13.0" @@ -548,6 +615,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.3" @@ -743,6 +816,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "2.6.0" @@ -878,6 +957,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lock_api" version = "0.4.12" @@ -1401,6 +1486,7 @@ dependencies = [ "ratatui", "slice-utils", "thiserror", + "tui-popup", "tui-prompts", "tui-textarea", ] @@ -1583,6 +1669,18 @@ dependencies = [ "winnow", ] +[[package]] +name = "tui-popup" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9ee3d08800c83ba0a2efaec44d225bcc3f885f30e2b520a17e2cd962b7da6ab" +dependencies = [ + "derive-getters", + "derive_setters", + "document-features", + "ratatui", +] + [[package]] name = "tui-prompts" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index d3b0e4e..426a38b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ thiserror = "1.0.65" tui-textarea = "0.7.0" tui-prompts = "0.5.0" slice-utils = { git = "https://dev.zaphar.net/zaphar/slice-cursor-rs.git", ref = "main" } +tui-popup = "0.6.0" diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c5395e1..d671f02 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,7 +5,7 @@ use std::{path::PathBuf, process::ExitCode}; use crate::book::Book; use anyhow::Result; -use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; +use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use ratatui::{ self, buffer::Buffer, @@ -15,6 +15,7 @@ use ratatui::{ widgets::{Block, Cell, Paragraph, Row, Table, TableState, Widget}, Frame, }; +use tui_popup::Popup; use tui_prompts::{State, Status, TextPrompt, TextState}; use tui_textarea::{CursorMove, TextArea}; @@ -24,22 +25,44 @@ mod test; use cmd::Cmd; -#[derive(Default, Debug, PartialEq)] +#[derive(Default, Debug, PartialEq, Clone)] pub enum Modality { #[default] Navigate, CellEdit, Command, // TODO(zaphar): Command Mode? + Dialog, } -#[derive(Default, Debug)] +#[derive(Debug)] pub struct AppState<'ws> { - pub modality: Modality, + pub modality_stack: Vec, pub table_state: TableState, pub command_state: TextState<'ws>, } +impl<'ws> Default for AppState<'ws> { + fn default() -> Self { + AppState { + modality_stack: vec![Modality::default()], + table_state: Default::default(), + command_state: Default::default() + } + } +} +impl<'ws> AppState<'ws> { + pub fn modality(&'ws self) -> &'ws Modality { + self.modality_stack.last().unwrap() + } + + pub fn pop_modality(&mut self) { + if self.modality_stack.len() > 1 { + self.modality_stack.pop(); + } + } +} + // TODO(jwall): This should probably move to a different module. /// The Address in a Table. #[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)] @@ -70,6 +93,7 @@ pub struct Workspace<'ws> { text_area: TextArea<'ws>, dirty: bool, show_help: bool, + popup: String } impl<'ws> Workspace<'ws> { @@ -81,6 +105,7 @@ impl<'ws> Workspace<'ws> { text_area: reset_text_area("".to_owned()), dirty: false, show_help: false, + popup: String::new(), }; ws.handle_movement_change(); ws @@ -140,10 +165,11 @@ impl<'ws> Workspace<'ws> { pub fn handle_input(&mut self) -> Result> { if let Event::Key(key) = event::read()? { - let result = match self.state.modality { + let result = match self.state.modality() { Modality::Navigate => self.handle_navigation_input(key)?, Modality::CellEdit => self.handle_edit_input(key)?, Modality::Command => self.handle_command_input(key)?, + Modality::Dialog => self.handle_dialog_input(key)?, }; return Ok(result); } @@ -152,7 +178,7 @@ impl<'ws> Workspace<'ws> { fn render_help_text(&self) -> impl Widget { let info_block = Block::bordered().title("Help"); - Paragraph::new(match self.state.modality { + Paragraph::new(match self.state.modality() { Modality::Navigate => Text::from(vec![ "Navigate Mode:".into(), "* e: Enter edit mode for current cell".into(), @@ -171,6 +197,10 @@ impl<'ws> Workspace<'ws> { "Command Mode:".into(), "* ESC: Exit command mode".into(), ]), + Modality::Dialog => Text::from(vec![ + "Dialog Mode:".into(), + "* ESC: Exit dialog".into(), + ]), }) .block(info_block) } @@ -188,6 +218,18 @@ impl<'ws> Workspace<'ws> { Ok(None) } + fn handle_dialog_input(&mut self, key: event::KeyEvent) -> Result> { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => self.exit_dialog_mode()?, + _ => { + // NOOP + } + } + } + Ok(None) + } + fn handle_edit_input(&mut self, key: event::KeyEvent) -> Result> { if key.kind == KeyEventKind::Press { match key.code { @@ -220,7 +262,7 @@ impl<'ws> Workspace<'ws> { Ok(true) } Ok(Some(Cmd::Help(_maybe_topic))) => { - // TODO(jeremy): Modal dialogs? + self.enter_dialog_mode("TODO help topic".to_owned()); Ok(true) } Ok(Some(Cmd::Write(maybe_path))) => { @@ -245,9 +287,12 @@ impl<'ws> Workspace<'ws> { // TODO(zaphar): We probably need to do better than this std::process::exit(0); }, - Ok(None) => Ok(false), - Err(_msg) => { - // TODO(jeremy): Modal dialogs? + Ok(None) => { + self.enter_dialog_mode(format!("Unrecognized commmand {}", cmd_text)); + Ok(false) + }, + Err(msg) => { + self.enter_dialog_mode(msg.to_owned()); Ok(false) } } @@ -328,18 +373,23 @@ impl<'ws> Workspace<'ws> { } fn enter_navigation_mode(&mut self) { - self.state.modality = Modality::Navigate; + self.state.modality_stack.push(Modality::Navigate); } fn enter_command_mode(&mut self) { - self.state.modality = Modality::Command; + self.state.modality_stack.push(Modality::Command); self.state.command_state.truncate(); *self.state.command_state.status_mut() = Status::Pending; self.state.command_state.focus(); } + fn enter_dialog_mode(&mut self, msg: String) { + self.popup = msg; + self.state.modality_stack.push(Modality::Dialog); + } + fn enter_edit_mode(&mut self) { - self.state.modality = Modality::CellEdit; + self.state.modality_stack.push(Modality::CellEdit); self.text_area .set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED)); self.text_area @@ -352,11 +402,16 @@ impl<'ws> Workspace<'ws> { let cmd = self.state.command_state.value().to_owned(); self.state.command_state.blur(); *self.state.command_state.status_mut() = Status::Done; + self.state.pop_modality(); self.handle_command(cmd)?; - self.enter_navigation_mode(); Ok(()) } + fn exit_dialog_mode(&mut self) -> Result<()> { + self.state.pop_modality(); + Ok(()) + } + fn exit_edit_mode(&mut self) -> Result<()> { self.text_area.set_cursor_line_style(Style::default()); self.text_area.set_cursor_style(Style::default()); @@ -418,7 +473,7 @@ impl<'ws> Workspace<'ws> { info_para.render(rect, buf); })); } - if self.state.modality == Modality::Command { + if self.state.modality() == &Modality::Command { cs.push(Constraint::Max(1)); rs.push(Box::new(|rect: Rect, buf: &mut Buffer, ws: &mut Self| { StatefulWidget::render( @@ -474,10 +529,11 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> { .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|| String::from("Unknown")), )) - .title_bottom(match &self.state.modality { + .title_bottom(match self.state.modality() { Modality::Navigate => "navigate", Modality::CellEdit => "edit", Modality::Command => "command", + Modality::Dialog => "", }) .title_bottom( Line::from(format!( @@ -492,6 +548,11 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> { } outer_block.render(area, buf); + + if self.state.modality() == &Modality::Dialog { + let popup = Popup::new(Text::from(self.popup.clone())); + popup.render(area, buf); + } } }