diff --git a/Cargo.lock b/Cargo.lock index 8b7cb6f..64d4041 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -708,6 +708,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "tui-input" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e785f863a3af4c800a2a669d0b64c879b538738e352607e2624d03f868dc01" +dependencies = [ + "crossterm 0.27.0", + "unicode-width", +] + [[package]] name = "unicode-ident" version = "1.0.11" @@ -824,6 +834,7 @@ dependencies = [ "jrny-save", "notify", "ratatui", + "tui-input", ] [[package]] diff --git a/crates/wayfarer/Cargo.toml b/crates/wayfarer/Cargo.toml index ebfaf9a..b97a5ae 100644 --- a/crates/wayfarer/Cargo.toml +++ b/crates/wayfarer/Cargo.toml @@ -24,6 +24,10 @@ optional = true version = "0.22" optional = true +[dependencies.tui-input] +version = "0.8" +optional = true + [dependencies.crossterm] version = "0.27" optional = true @@ -32,4 +36,4 @@ optional = true [features] default = ["watch", "tui"] watch = ["dep:notify"] -tui = ["dep:ratatui", "dep:crossterm"] +tui = ["dep:ratatui", "dep:tui-input", "dep:crossterm"] diff --git a/crates/wayfarer/src/tui.rs b/crates/wayfarer/src/tui.rs index 221e08b..4f6990c 100644 --- a/crates/wayfarer/src/tui.rs +++ b/crates/wayfarer/src/tui.rs @@ -8,7 +8,7 @@ use std::time::Duration; use anyhow::Result; use clap::Parser as ArgParser; -use crossterm::event::{Event, KeyCode, KeyEvent}; +use crossterm::event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent}; use crossterm::terminal::{ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, }; @@ -17,6 +17,8 @@ use jrny_save::{Savefile, LEVEL_NAMES}; use ratatui::backend::CrosstermBackend; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::widgets::{Block, Borders, Cell, Padding, Paragraph, Row, Table}; +use tui_input::backend::crossterm::EventHandler; +use tui_input::Input; #[cfg(feature = "watch")] use crate::watcher::FileWatcher; @@ -32,6 +34,8 @@ pub struct Args { struct State { current_path: PathBuf, current_file: Savefile, + mode: Mode, + file_select: Input, #[cfg(feature = "watch")] file_watcher: Option, } @@ -42,6 +46,12 @@ struct State { enum Message { Exit, + ToggleFileSelect, + + SetMode(Mode), + + LoadFile, + #[cfg(feature = "watch")] ToggleFileWatch, @@ -50,6 +60,15 @@ enum Message { } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +enum Mode { + #[default] + Normal, + + SelectFile, +} + + type Terminal = ratatui::Terminal>; type Frame<'a> = ratatui::Frame<'a, CrosstermBackend>; @@ -63,6 +82,8 @@ pub(crate) fn execute(_app_args: &AppArgs, args: &Args) -> Result<()> { let state = State { current_path: args.path.clone(), current_file: savefile, + mode: Mode::default(), + file_select: Input::default(), #[cfg(feature = "watch")] file_watcher: None, }; @@ -84,7 +105,7 @@ fn run(terminal: &mut Terminal, mut state: State) -> Result<()> { render(&state, frame); })?; - handle_events(&mut evq_tx)?; + handle_events(&mut evq_tx, &mut state)?; let message = match evq_rx.try_recv() { Ok(msg) => msg, @@ -95,6 +116,29 @@ fn run(terminal: &mut Terminal, mut state: State) -> 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 = load_savefile(&path)?; + state.current_path = path; + + #[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, + }; + } + #[cfg(feature = "watch")] Message::ToggleFileWatch => { if state.file_watcher.is_none() { @@ -121,13 +165,13 @@ fn run(terminal: &mut Terminal, mut state: State) -> Result<()> { } -fn handle_events(event_queue: &mut mpsc::Sender) -> Result<()> { +fn handle_events(event_queue: &mut mpsc::Sender, state: &mut State) -> Result<()> { if !event::poll(Duration::from_millis(250))? { return Ok(()); } match event::read()? { - Event::Key(key) => handle_keyboard_input(key, event_queue)?, + Event::Key(key) => handle_keyboard_input(key, event_queue, state)?, _ => return Ok(()), } @@ -135,18 +179,41 @@ fn handle_events(event_queue: &mut mpsc::Sender) -> Result<()> { } -fn handle_keyboard_input(key: KeyEvent, event_queue: &mut mpsc::Sender) -> Result<()> { - let message = match key.code { - KeyCode::Char('q') => Message::Exit, +fn handle_keyboard_input( + key: KeyEvent, + event_queue: &mut mpsc::Sender, + state: &mut State, +) -> Result<()> { + match (state.mode, key.code) { + (_, KeyCode::Esc) => { + event_queue.send(Message::SetMode(Mode::Normal))?; + } + + (Mode::SelectFile, KeyCode::Enter) => { + event_queue.send(Message::LoadFile)?; + event_queue.send(Message::ToggleFileSelect)?; + } + + (Mode::SelectFile, _) => { + state.file_select.handle_event(&Event::Key(key)); + } + + (Mode::Normal, KeyCode::Char('q')) => { + event_queue.send(Message::Exit)?; + } + + (Mode::Normal, KeyCode::Char('o')) => { + event_queue.send(Message::ToggleFileSelect)?; + } #[cfg(feature = "watch")] - KeyCode::Char('w') => Message::ToggleFileWatch, + (Mode::Normal, KeyCode::Char('w')) => { + event_queue.send(Message::ToggleFileWatch)?; + } - _ => return Ok(()), + _ => (), }; - event_queue.send(message)?; - Ok(()) } @@ -156,7 +223,7 @@ fn setup() -> Result { enable_raw_mode()?; - execute!(stdout, EnterAlternateScreen)?; + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; Ok(Terminal::new(CrosstermBackend::new(stdout))?) } @@ -165,7 +232,11 @@ fn setup() -> Result { fn reset(mut terminal: Terminal) -> Result<()> { disable_raw_mode()?; - execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; terminal.show_cursor()?; @@ -194,21 +265,32 @@ fn render(state: &State, mut frame: &mut Frame) { let status_block = Block::default().padding(Padding::horizontal(2)); - #[cfg(feature = "watch")] - let text = format!( - "{} file: {}", - if state.file_watcher.is_some() { - "Watching" - } else { - "Showing" - }, - state.current_path.display() - ); - #[cfg(not(feature = "watch"))] - let text = format!("Showing file: {}", state.current_path.display()); + match state.mode { + #[cfg(feature = "watch")] + Mode::Normal if state.file_watcher.is_some() => { + let text = format!("Watching file: {}", state.current_path.display()); + let status = Paragraph::new(text).block(status_block); + frame.render_widget(status, rows[1]); + } - let status = Paragraph::new(text).block(status_block); - frame.render_widget(status, rows[1]) + Mode::Normal => { + let text = format!("Showing file: {}", state.current_path.display()); + let status = Paragraph::new(text).block(status_block); + frame.render_widget(status, rows[1]); + } + + Mode::SelectFile => { + let scroll = state.file_select.visual_scroll(rows[1].width as usize); + let input = Paragraph::new(state.file_select.value()) + .scroll((0, scroll as u16)) + .block(status_block); + frame.render_widget(input, rows[1]); + frame.set_cursor( + rows[1].x + (state.file_select.visual_cursor() as u16) + 2, + rows[1].y, + ); + } + } }