Added tty interactive input.
Signed-off-by: Pavel Kirilin <win10@list.ru>
This commit is contained in:
44
Cargo.lock
generated
44
Cargo.lock
generated
@ -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"
|
||||
|
@ -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.
|
||||
regex = "1" # Regular expressions.
|
||||
termion = "1.5.5" # For interacting with terminal.
|
||||
term_grid = "0.1.7" # For showing matched files while initialization
|
27
src/cli.rs
27
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,
|
||||
}
|
@ -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)
|
||||
}
|
@ -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(())
|
||||
|
@ -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");
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
168
src/tty_stuff.rs
Normal file
168
src/tty_stuff.rs
Normal 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)
|
||||
}
|
Reference in New Issue
Block a user