From 02344c86f97a9d976c494391c1b184842d859fe3 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin <win10@list.ru> Date: Wed, 1 Apr 2020 04:56:02 +0400 Subject: [PATCH] Added tty interactive input. Signed-off-by: Pavel Kirilin <win10@list.ru> --- Cargo.lock | 44 +++++++++++ Cargo.toml | 4 +- src/cli.rs | 27 +------ src/config.rs | 62 ++++++++-------- src/initialization.rs | 12 +-- src/main.rs | 1 + src/run_modes.rs | 6 +- src/tty_stuff.rs | 168 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 254 insertions(+), 70 deletions(-) create mode 100644 src/tty_stuff.rs diff --git a/Cargo.lock b/Cargo.lock index d3069c7..ffcf14a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,6 +48,8 @@ dependencies = [ "serde_derive", "serde_json", "structopt", + "term_grid", + "termion", ] [[package]] @@ -169,6 +171,12 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + [[package]] name = "proc-macro-error" version = "0.4.12" @@ -213,6 +221,21 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" + +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +dependencies = [ + "redox_syscall", +] + [[package]] name = "regex" version = "1.3.5" @@ -335,6 +358,27 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "term_grid" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230d3e804faaed5a39b08319efb797783df2fd9671b39b7596490cb486d702cf" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "termion" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] + [[package]] name = "textwrap" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index f7a9c3d..d3a965d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,6 @@ failure = "0.1.7" # Experimental error handling abstraction. failure_derive = "0.1.7" # Used to create new error type. lazy_static = "1.4" # Define lazy static vars. alphanumeric-sort = "1.0.12" # Used to search for videos. -regex = "1" # Regular expressions. \ No newline at end of file +regex = "1" # Regular expressions. +termion = "1.5.5" # For interacting with terminal. +term_grid = "0.1.7" # For showing matched files while initialization \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index a97dfe7..2b834bc 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -19,30 +19,5 @@ pub enum RunMode { #[structopt(name = "next", about = "Increase episode counter by one")] Next, #[structopt(name = "update", about = "Update saved config")] - Update(UpdateOptions), -} - -#[derive(StructOpt, Debug)] -pub struct UpdateOptions { - #[structopt( - short, - long, - name = "episode", - about = "Update saved last episode" - )] - pub last_episode: Option<usize>, - #[structopt( - short, - long, - name = "command", - about = "Update saved command to show the next episode" - )] - pub command: Option<String>, - #[structopt( - short, - long, - name = "pattern", - about = "Update pattern for filenames" - )] - pub pattern: Option<String>, + Update, } \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index e6e8b55..d5c32b0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use crate::result::{AppResult, AppError}; -use crate::{CONFIG_PATH, UpdateOptions}; +use crate::CONFIG_PATH; use std::io::{Write, Read}; +use crate::tty_stuff::{choose_pattern, choose_command, choose_episode}; #[derive(Serialize, Default, Deserialize)] pub struct Config { @@ -54,26 +55,7 @@ impl Config { } pub fn get_current_episode(&self) -> AppResult<String> { - let current_dir = std::env::current_dir()?; - let mut names = Vec::new(); - - let episode_regex = regex::Regex::new(self.pattern.as_str())?; - for entry in std::fs::read_dir(current_dir)? { - let entry = entry?; - let path = entry.path(); - let meta = std::fs::metadata(&path)?; - if meta.is_dir() { - continue; - } - let name = entry.file_name(); - let name_str = name.into_string(); - if let Ok(name) = name_str { - if episode_regex.is_match(name.as_str()) { - names.push(name); - } - } - } - alphanumeric_sort::sort_str_slice(names.as_mut_slice()); + let names = get_matched_files(self.pattern.clone())?; if let Some(episode) = names.get(self.current_episode_count) { Ok(episode.clone()) } else { @@ -88,16 +70,34 @@ pub fn update_episode(episode_func: fn(usize) -> AppResult<usize>) -> AppResult< conf.save() } -pub fn update_config(options: UpdateOptions) -> AppResult<()> { +pub fn update_config() -> AppResult<()> { let mut conf = Config::read()?; - if let Some(pattern) = options.pattern { - conf.pattern = pattern; - } - if let Some(command) = options.command { - conf.command = command; - } - if let Some(episode) = options.last_episode { - conf.current_episode_count = episode; - } + conf.pattern = choose_pattern(conf.pattern)?; + conf.command = choose_command(conf.command)?; + conf.current_episode_count = choose_episode(conf.current_episode_count)?; conf.save() +} + +pub fn get_matched_files(pattern: String) -> AppResult<Vec<String>> { + let current_dir = std::env::current_dir()?; + let mut names = Vec::new(); + + let episode_regex = regex::Regex::new(pattern.as_str())?; + for entry in std::fs::read_dir(current_dir)? { + let entry = entry?; + let path = entry.path(); + let meta = std::fs::metadata(&path)?; + if meta.is_dir() { + continue; + } + let name = entry.file_name(); + let name_str = name.into_string(); + if let Ok(name) = name_str { + if episode_regex.is_match(name.as_str()) { + names.push(name); + } + } + } + alphanumeric_sort::sort_str_slice(names.as_mut_slice()); + Ok(names) } \ No newline at end of file diff --git a/src/initialization.rs b/src/initialization.rs index 4a3525c..3eca02e 100644 --- a/src/initialization.rs +++ b/src/initialization.rs @@ -1,16 +1,10 @@ use crate::result::AppResult; +use crate::tty_stuff::{choose_pattern, choose_command}; use crate::config::Config; pub fn init_config() -> AppResult<()> { - let stdin = std::io::stdin(); - println!("How can we recognize anime files? Enter filename regex."); - let mut pattern = String::new(); - stdin.read_line(&mut pattern)?; - pattern = pattern.trim().to_string(); - println!("How can we show you the next episode? If 'mpv --fullscreen \"{{}}\"' is correct, leave it blank."); - let mut command = String::new(); - stdin.read_line(&mut command)?; - command = command.trim().to_string(); + let pattern = choose_pattern(String::new())?; + let command = choose_command(String::from("mpv --fullscreen \"{}\""))?.trim().to_string(); let config = Config::new(pattern, command)?; config.save()?; Ok(()) diff --git a/src/main.rs b/src/main.rs index 3d16a7c..6dab836 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ use crate::result::AppResult; pub mod result; pub mod config; pub mod run_modes; +pub mod tty_stuff; pub mod initialization; include!("cli.rs"); diff --git a/src/run_modes.rs b/src/run_modes.rs index bc3da6e..eb58c25 100644 --- a/src/run_modes.rs +++ b/src/run_modes.rs @@ -2,7 +2,7 @@ use crate::{Opt, RunMode}; use crate::result::{AppResult, AppError}; use crate::initialization::init_config; use crate::config::{update_episode, update_config, Config}; -use std::process::Command; +use std::process::{Command}; pub fn run(opts: Opt) -> AppResult<()> { let mode = opts.mode.unwrap_or_else(|| RunMode::Play); @@ -19,8 +19,8 @@ pub fn run(opts: Opt) -> AppResult<()> { RunMode::Next => { update_episode(next_episode) } - RunMode::Update(options) => { - update_config(options) + RunMode::Update => { + update_config() } } } diff --git a/src/tty_stuff.rs b/src/tty_stuff.rs new file mode 100644 index 0000000..e3c4c39 --- /dev/null +++ b/src/tty_stuff.rs @@ -0,0 +1,168 @@ +use crate::result::AppResult; +use crate::config::get_matched_files; +use termion::input::TermRead; +use std::io::{Write, stdout, stdin, Stdout}; +use termion::event::Key; +use termion::raw::{IntoRawMode, RawTerminal}; +use term_grid::{Grid, GridOptions, Filling, Direction, Cell}; +use std::process::exit; +use std::str::FromStr; + +pub fn get_matched_files_grid(pattern: String) -> AppResult<String> { + let mut grid = Grid::new(GridOptions { + direction: Direction::LeftToRight, + filling: Filling::Spaces(2), + }); + let filenames = get_matched_files(pattern).unwrap_or_else(|_| Vec::new()); + for filename in filenames { + grid.add(Cell::from(filename)) + } + Ok(format!("{}", grid.fit_into_columns(6))) +} + +pub fn choose_pattern(current_pattern: String) -> AppResult<String> { + let mut stdout = stdout().into_raw_mode()?; + let res = read_tty_line( + &mut stdout, + "How can we recognize files? Enter filename regex.", + current_pattern, + |stdout, pattern| { + if !pattern.is_empty() { + write!(stdout, "{}------Matched files------", termion::cursor::Goto(1, 3))?; + let grid = get_matched_files_grid(pattern)?; + if grid.is_empty() { + write!(stdout, "{}No matches found", + termion::cursor::Goto(1, 4) + )?; + } else { + write!(stdout, "{}{}", + termion::cursor::Goto(1, 4), + grid + )?; + } + } + Ok(()) + }, + ); + stdout.suspend_raw_mode()?; + res +} + +pub fn choose_command(current_command: String) -> AppResult<String> { + let mut stdout = stdout().into_raw_mode()?; + let res = read_tty_line( + &mut stdout, + "Command to execute files.", + current_command, + |_, _| { Ok(()) }, + ); + stdout.suspend_raw_mode()?; + res +} + +pub fn choose_episode(current_episode: usize) -> AppResult<usize> { + let mut stdout = stdout().into_raw_mode()?; + let res = read_tty_line( + &mut stdout, + "Choose episode.", + format!("{}", current_episode), + |_, _| { Ok(()) }, + ).map(|s| { + usize::from_str(s.as_str()).unwrap_or_else(|_| current_episode) + }); + stdout.suspend_raw_mode()?; + res +} + + +pub fn read_tty_line( + stdout: &mut RawTerminal<Stdout>, + prompt: &str, + current_value: String, + after_key_press: fn(&mut RawTerminal<Stdout>, String) -> AppResult<()>, +) -> AppResult<String> { + let stdin = stdin(); + // Get the standard output stream and go to raw mode. + + write!(stdout, "{}{}{}{}", + termion::clear::All, + termion::cursor::Goto(1, 1), + prompt, + termion::cursor::Goto(1, 2) + )?; + // Flush stdout (i.e. make the output appear). + stdout.flush()?; + let mut buffer = current_value; + let mut current_pos = buffer.len() + 1; + if !buffer.is_empty() { + write!(stdout, "{}{}{}", + termion::cursor::Goto(1, 2), + termion::clear::AfterCursor, + buffer)?; + after_key_press(stdout, buffer.clone())?; + write!(stdout, "{}", + termion::cursor::Goto(current_pos as u16, 2) + )?; + stdout.flush()?; + } + for c in stdin.keys() { + match c? { + // Exit if \n. + Key::Char('\n') => { + break; + } + // Update pattern + Key::Char(c) => { + buffer.insert(current_pos - 1, c.clone()); + current_pos += 1; + println!("{:#?}", c); + } + Key::Backspace => { + if let Some(pos) = current_pos.checked_sub(2) { + current_pos = pos + 1; + buffer.remove(pos); + } + } + Key::Delete => { + if current_pos <= buffer.len() { + buffer.remove(current_pos - 1); + } + } + Key::Right => { + if current_pos <= buffer.len() { + current_pos += 1; + } + } + Key::Left => { + if let Some(pos) = current_pos.checked_sub(1) { + current_pos = pos; + } + } + Key::Ctrl('c') => { + write!(stdout, "{}", termion::cursor::Show)?; + stdout.suspend_raw_mode()?; + exit(0); + } + Key::Ctrl('u') => { + buffer.clear(); + current_pos = 1; + } + _ => {} + } + // Clear the current line. + write!(stdout, "{}{}{}", + termion::cursor::Goto(1, 2), + termion::clear::AfterCursor, + buffer)?; + // Print matched files + after_key_press(stdout, buffer.clone())?; + write!(stdout, "{}", + termion::cursor::Goto(current_pos as u16, 2) + )?; + stdout.flush()?; + } + write!(stdout, "{}{}", termion::clear::All, termion::cursor::Goto(1, 1))?; + stdout.flush()?; + // Show the cursor again before we exit. + Ok(buffer) +} \ No newline at end of file -- GitLab