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