Implement simple file opener
This commit is contained in:
parent
8fca0f2f6a
commit
2534cbf32c
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -708,6 +708,16 @@ dependencies = [
|
|||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
@ -824,6 +834,7 @@ dependencies = [
|
|||||||
"jrny-save",
|
"jrny-save",
|
||||||
"notify",
|
"notify",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
"tui-input",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -24,6 +24,10 @@ optional = true
|
|||||||
version = "0.22"
|
version = "0.22"
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.tui-input]
|
||||||
|
version = "0.8"
|
||||||
|
optional = true
|
||||||
|
|
||||||
[dependencies.crossterm]
|
[dependencies.crossterm]
|
||||||
version = "0.27"
|
version = "0.27"
|
||||||
optional = true
|
optional = true
|
||||||
@ -32,4 +36,4 @@ optional = true
|
|||||||
[features]
|
[features]
|
||||||
default = ["watch", "tui"]
|
default = ["watch", "tui"]
|
||||||
watch = ["dep:notify"]
|
watch = ["dep:notify"]
|
||||||
tui = ["dep:ratatui", "dep:crossterm"]
|
tui = ["dep:ratatui", "dep:tui-input", "dep:crossterm"]
|
||||||
|
@ -8,7 +8,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser as ArgParser;
|
use clap::Parser as ArgParser;
|
||||||
use crossterm::event::{Event, KeyCode, KeyEvent};
|
use crossterm::event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent};
|
||||||
use crossterm::terminal::{
|
use crossterm::terminal::{
|
||||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
};
|
};
|
||||||
@ -17,6 +17,8 @@ use jrny_save::{Savefile, LEVEL_NAMES};
|
|||||||
use ratatui::backend::CrosstermBackend;
|
use ratatui::backend::CrosstermBackend;
|
||||||
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||||
use ratatui::widgets::{Block, Borders, Cell, Padding, Paragraph, Row, Table};
|
use ratatui::widgets::{Block, Borders, Cell, Padding, Paragraph, Row, Table};
|
||||||
|
use tui_input::backend::crossterm::EventHandler;
|
||||||
|
use tui_input::Input;
|
||||||
|
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
use crate::watcher::FileWatcher;
|
use crate::watcher::FileWatcher;
|
||||||
@ -32,6 +34,8 @@ pub struct Args {
|
|||||||
struct State {
|
struct State {
|
||||||
current_path: PathBuf,
|
current_path: PathBuf,
|
||||||
current_file: Savefile,
|
current_file: Savefile,
|
||||||
|
mode: Mode,
|
||||||
|
file_select: Input,
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
file_watcher: Option<FileWatcher>,
|
file_watcher: Option<FileWatcher>,
|
||||||
}
|
}
|
||||||
@ -42,6 +46,12 @@ struct State {
|
|||||||
enum Message {
|
enum Message {
|
||||||
Exit,
|
Exit,
|
||||||
|
|
||||||
|
ToggleFileSelect,
|
||||||
|
|
||||||
|
SetMode(Mode),
|
||||||
|
|
||||||
|
LoadFile,
|
||||||
|
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
ToggleFileWatch,
|
ToggleFileWatch,
|
||||||
|
|
||||||
@ -50,6 +60,15 @@ enum Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Mode {
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
|
||||||
|
SelectFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
|
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
|
||||||
type Frame<'a> = ratatui::Frame<'a, CrosstermBackend<Stdout>>;
|
type Frame<'a> = ratatui::Frame<'a, CrosstermBackend<Stdout>>;
|
||||||
|
|
||||||
@ -63,6 +82,8 @@ pub(crate) fn execute(_app_args: &AppArgs, args: &Args) -> Result<()> {
|
|||||||
let state = State {
|
let state = State {
|
||||||
current_path: args.path.clone(),
|
current_path: args.path.clone(),
|
||||||
current_file: savefile,
|
current_file: savefile,
|
||||||
|
mode: Mode::default(),
|
||||||
|
file_select: Input::default(),
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
file_watcher: None,
|
file_watcher: None,
|
||||||
};
|
};
|
||||||
@ -84,7 +105,7 @@ fn run(terminal: &mut Terminal, mut state: State) -> Result<()> {
|
|||||||
render(&state, frame);
|
render(&state, frame);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
handle_events(&mut evq_tx)?;
|
handle_events(&mut evq_tx, &mut state)?;
|
||||||
|
|
||||||
let message = match evq_rx.try_recv() {
|
let message = match evq_rx.try_recv() {
|
||||||
Ok(msg) => msg,
|
Ok(msg) => msg,
|
||||||
@ -95,6 +116,29 @@ fn run(terminal: &mut Terminal, mut state: State) -> Result<()> {
|
|||||||
match message {
|
match message {
|
||||||
Message::Exit => break,
|
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")]
|
#[cfg(feature = "watch")]
|
||||||
Message::ToggleFileWatch => {
|
Message::ToggleFileWatch => {
|
||||||
if state.file_watcher.is_none() {
|
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<Message>) -> Result<()> {
|
fn handle_events(event_queue: &mut mpsc::Sender<Message>, state: &mut State) -> Result<()> {
|
||||||
if !event::poll(Duration::from_millis(250))? {
|
if !event::poll(Duration::from_millis(250))? {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
match event::read()? {
|
match event::read()? {
|
||||||
Event::Key(key) => handle_keyboard_input(key, event_queue)?,
|
Event::Key(key) => handle_keyboard_input(key, event_queue, state)?,
|
||||||
_ => return Ok(()),
|
_ => return Ok(()),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,18 +179,41 @@ fn handle_events(event_queue: &mut mpsc::Sender<Message>) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn handle_keyboard_input(key: KeyEvent, event_queue: &mut mpsc::Sender<Message>) -> Result<()> {
|
fn handle_keyboard_input(
|
||||||
let message = match key.code {
|
key: KeyEvent,
|
||||||
KeyCode::Char('q') => Message::Exit,
|
event_queue: &mut mpsc::Sender<Message>,
|
||||||
|
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")]
|
#[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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +223,7 @@ fn setup() -> Result<Terminal> {
|
|||||||
|
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
||||||
execute!(stdout, EnterAlternateScreen)?;
|
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
|
|
||||||
Ok(Terminal::new(CrosstermBackend::new(stdout))?)
|
Ok(Terminal::new(CrosstermBackend::new(stdout))?)
|
||||||
}
|
}
|
||||||
@ -165,7 +232,11 @@ fn setup() -> Result<Terminal> {
|
|||||||
fn reset(mut terminal: Terminal) -> Result<()> {
|
fn reset(mut terminal: Terminal) -> Result<()> {
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
|
|
||||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
execute!(
|
||||||
|
terminal.backend_mut(),
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
DisableMouseCapture
|
||||||
|
)?;
|
||||||
|
|
||||||
terminal.show_cursor()?;
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
@ -194,21 +265,32 @@ fn render(state: &State, mut frame: &mut Frame) {
|
|||||||
|
|
||||||
let status_block = Block::default().padding(Padding::horizontal(2));
|
let status_block = Block::default().padding(Padding::horizontal(2));
|
||||||
|
|
||||||
#[cfg(feature = "watch")]
|
match state.mode {
|
||||||
let text = format!(
|
#[cfg(feature = "watch")]
|
||||||
"{} file: {}",
|
Mode::Normal if state.file_watcher.is_some() => {
|
||||||
if state.file_watcher.is_some() {
|
let text = format!("Watching file: {}", state.current_path.display());
|
||||||
"Watching"
|
let status = Paragraph::new(text).block(status_block);
|
||||||
} else {
|
frame.render_widget(status, rows[1]);
|
||||||
"Showing"
|
}
|
||||||
},
|
|
||||||
state.current_path.display()
|
|
||||||
);
|
|
||||||
#[cfg(not(feature = "watch"))]
|
|
||||||
let text = format!("Showing file: {}", state.current_path.display());
|
|
||||||
|
|
||||||
let status = Paragraph::new(text).block(status_block);
|
Mode::Normal => {
|
||||||
frame.render_widget(status, rows[1])
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user