Added tty interactive input.

Signed-off-by: Pavel Kirilin <win10@list.ru>
This commit is contained in:
2020-04-01 04:56:02 +04:00
parent 822fd0d43b
commit 02344c86f9
8 changed files with 254 additions and 70 deletions

44
Cargo.lock generated
View File

@ -48,6 +48,8 @@ dependencies = [
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"structopt", "structopt",
"term_grid",
"termion",
] ]
[[package]] [[package]]
@ -169,6 +171,12 @@ version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
[[package]]
name = "numtoa"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
[[package]] [[package]]
name = "proc-macro-error" name = "proc-macro-error"
version = "0.4.12" version = "0.4.12"
@ -213,6 +221,21 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "regex" name = "regex"
version = "1.3.5" version = "1.3.5"
@ -335,6 +358,27 @@ dependencies = [
"unicode-xid", "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]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.11.0" version = "0.11.0"

View File

@ -16,3 +16,5 @@ failure_derive = "0.1.7" # Used to create new error type.
lazy_static = "1.4" # Define lazy static vars. lazy_static = "1.4" # Define lazy static vars.
alphanumeric-sort = "1.0.12" # Used to search for videos. alphanumeric-sort = "1.0.12" # Used to search for videos.
regex = "1" # Regular expressions. regex = "1" # Regular expressions.
termion = "1.5.5" # For interacting with terminal.
term_grid = "0.1.7" # For showing matched files while initialization

View File

@ -19,30 +19,5 @@ pub enum RunMode {
#[structopt(name = "next", about = "Increase episode counter by one")] #[structopt(name = "next", about = "Increase episode counter by one")]
Next, Next,
#[structopt(name = "update", about = "Update saved config")] #[structopt(name = "update", about = "Update saved config")]
Update(UpdateOptions), Update,
}
#[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>,
} }

View File

@ -1,6 +1,7 @@
use crate::result::{AppResult, AppError}; use crate::result::{AppResult, AppError};
use crate::{CONFIG_PATH, UpdateOptions}; use crate::CONFIG_PATH;
use std::io::{Write, Read}; use std::io::{Write, Read};
use crate::tty_stuff::{choose_pattern, choose_command, choose_episode};
#[derive(Serialize, Default, Deserialize)] #[derive(Serialize, Default, Deserialize)]
pub struct Config { pub struct Config {
@ -54,10 +55,34 @@ impl Config {
} }
pub fn get_current_episode(&self) -> AppResult<String> { pub fn get_current_episode(&self) -> AppResult<String> {
let names = get_matched_files(self.pattern.clone())?;
if let Some(episode) = names.get(self.current_episode_count) {
Ok(episode.clone())
} else {
Err(AppError::RuntimeError(String::from("This is the end.")))
}
}
}
pub fn update_episode(episode_func: fn(usize) -> AppResult<usize>) -> AppResult<()> {
let mut conf = Config::read()?;
conf.current_episode_count = episode_func(conf.current_episode_count)?;
conf.save()
}
pub fn update_config() -> AppResult<()> {
let mut conf = Config::read()?;
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 current_dir = std::env::current_dir()?;
let mut names = Vec::new(); let mut names = Vec::new();
let episode_regex = regex::Regex::new(self.pattern.as_str())?; let episode_regex = regex::Regex::new(pattern.as_str())?;
for entry in std::fs::read_dir(current_dir)? { for entry in std::fs::read_dir(current_dir)? {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
@ -74,30 +99,5 @@ impl Config {
} }
} }
alphanumeric_sort::sort_str_slice(names.as_mut_slice()); alphanumeric_sort::sort_str_slice(names.as_mut_slice());
if let Some(episode) = names.get(self.current_episode_count) { Ok(names)
Ok(episode.clone())
} else {
Err(AppError::RuntimeError(String::from("This is the end.")))
}
}
}
pub fn update_episode(episode_func: fn(usize) -> AppResult<usize>) -> AppResult<()> {
let mut conf = Config::read()?;
conf.current_episode_count = episode_func(conf.current_episode_count)?;
conf.save()
}
pub fn update_config(options: UpdateOptions) -> 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.save()
} }

View File

@ -1,16 +1,10 @@
use crate::result::AppResult; use crate::result::AppResult;
use crate::tty_stuff::{choose_pattern, choose_command};
use crate::config::Config; use crate::config::Config;
pub fn init_config() -> AppResult<()> { pub fn init_config() -> AppResult<()> {
let stdin = std::io::stdin(); let pattern = choose_pattern(String::new())?;
println!("How can we recognize anime files? Enter filename regex."); let command = choose_command(String::from("mpv --fullscreen \"{}\""))?.trim().to_string();
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 config = Config::new(pattern, command)?; let config = Config::new(pattern, command)?;
config.save()?; config.save()?;
Ok(()) Ok(())

View File

@ -19,6 +19,7 @@ use crate::result::AppResult;
pub mod result; pub mod result;
pub mod config; pub mod config;
pub mod run_modes; pub mod run_modes;
pub mod tty_stuff;
pub mod initialization; pub mod initialization;
include!("cli.rs"); include!("cli.rs");

View File

@ -2,7 +2,7 @@ use crate::{Opt, RunMode};
use crate::result::{AppResult, AppError}; use crate::result::{AppResult, AppError};
use crate::initialization::init_config; use crate::initialization::init_config;
use crate::config::{update_episode, update_config, Config}; use crate::config::{update_episode, update_config, Config};
use std::process::Command; use std::process::{Command};
pub fn run(opts: Opt) -> AppResult<()> { pub fn run(opts: Opt) -> AppResult<()> {
let mode = opts.mode.unwrap_or_else(|| RunMode::Play); let mode = opts.mode.unwrap_or_else(|| RunMode::Play);
@ -19,8 +19,8 @@ pub fn run(opts: Opt) -> AppResult<()> {
RunMode::Next => { RunMode::Next => {
update_episode(next_episode) update_episode(next_episode)
} }
RunMode::Update(options) => { RunMode::Update => {
update_config(options) update_config()
} }
} }
} }

168
src/tty_stuff.rs Normal file
View File

@ -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)
}