From 28902116a5078d1f8e0349c0aa570deb9e4fbff0 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin <win10@list.ru> Date: Wed, 6 May 2020 03:40:08 +0400 Subject: [PATCH] Added autcompletions. Description: - Added autocompletions for some shells. - Fixed styles. - Fixed possible bugs with clippy. Signed-off-by: Pavel Kirilin <win10@list.ru> --- Cargo.lock | 115 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/cli.rs | 101 +++++++++++++++++++++++++++++++++++++ src/config.rs | 18 +++---- src/initialization.rs | 10 ++-- src/main.rs | 17 ++++--- src/result.rs | 2 +- src/run_modes.rs | 48 +++++++----------- src/tty_stuff.rs | 97 +++++++++++++++++++---------------- 9 files changed, 315 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60fc49e..1ca0db6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" + [[package]] name = "atty" version = "0.2.14" @@ -35,12 +47,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + [[package]] name = "awatch" version = "0.3.1" dependencies = [ "alphanumeric-sort", "colored", + "dirs", "failure", "failure_derive", "lazy_static", @@ -75,12 +94,29 @@ dependencies = [ "libc", ] +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "blake2b_simd" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "cc" version = "1.0.50" @@ -119,6 +155,45 @@ dependencies = [ "winapi", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if", + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" +dependencies = [ + "cfg-if", + "libc", + "redox_users", + "winapi", +] + [[package]] name = "failure" version = "0.1.7" @@ -141,6 +216,17 @@ dependencies = [ "synstructure", ] +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "heck" version = "0.3.1" @@ -248,6 +334,17 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "redox_users" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" +dependencies = [ + "getrandom", + "redox_syscall", + "rust-argon2", +] + [[package]] name = "regex" version = "1.3.5" @@ -266,6 +363,18 @@ version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" +[[package]] +name = "rust-argon2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + [[package]] name = "rustc-demangle" version = "0.1.16" @@ -439,6 +548,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "winapi" version = "0.3.8" diff --git a/Cargo.toml b/Cargo.toml index a2eb793..3aadb10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ structopt = "0.3" # Used to build CLI. serde = "1.0" # A generic serialization/deserialization framework. serde_derive = "1.0.105" # Used to configure json config stucture. serde_json = "1.0" # Used to store config. +dirs = "2.0.2" # Dirs for locating home directory. 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. diff --git a/src/cli.rs b/src/cli.rs index 55e7eeb..6050449 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,6 +8,35 @@ pub struct Opt { pub mode: Option<RunMode> } +#[derive(StructOpt, Debug)] +pub enum ShellType { + #[structopt( + about = "Generates a .bash completion file for the Bourne Again SHell (BASH)", + name = "bash" + )] + Bash, + #[structopt( + about = "Generates a .fish completion file for the Friendly Interactive SHell (fish)", + name = "fish" + )] + Fish, + #[structopt( + about = "Generates a completion file for the Z SHell (ZSH)", + name = "zsh" + )] + Zsh, + #[structopt( + about = "Generates a completion file for PowerShell", + name = "ps" + )] + PowerShell, + #[structopt( + about = "Generates a completion file for Elvish", + name = "elvish" + )] + Elvish, +} + #[derive(StructOpt, Debug)] pub enum RunMode { #[structopt(name = "init", about = "Initialize config")] @@ -22,4 +51,76 @@ pub enum RunMode { Update, #[structopt(name = "reset", about = "Set episode to 0")] Reset, + #[structopt(name = "completion", about = "Generate autocompletion \ + for your shell. If no shell was specified, then it will try \ + to recognize it automatically.")] + Completion { + #[structopt(subcommand)] + shell: Option<ShellType> + }, +} + +impl From<ShellType> for Shell { + fn from(shell: ShellType) -> Self { + match shell { + ShellType::Bash => { Shell::Bash } + ShellType::Fish => { Shell::Fish } + ShellType::Zsh => { Shell::Zsh } + ShellType::PowerShell => { Shell::PowerShell } + ShellType::Elvish => { Shell::Elvish } + } + } +} + +fn recognize_shell() -> AppResult<Shell> { + println!("Started auto shell recognition."); + let shell_var = std::env::var("SHELL").map_err(|_| { + AppError::RuntimeError(String::from( + "$SHELL env variable not found please specify shell by yourself", + )) + })?; + let shell_split: Vec<_> = shell_var.split('/').collect(); + if shell_split.is_empty() { + return Err(AppError::RuntimeError(String::from( + "Can't recognize shell please specify it by yourself.", + ))); + } + let shell_name = shell_split.last().unwrap(); + Shell::from_str(shell_name).map_err(|_| AppError::RuntimeError(String::from("Unknown shell"))) +} + +pub fn generate_completion(shell: Option<ShellType>) -> AppResult<()> { + let shell = if let Some(shell_val) = shell { + Shell::from(shell_val) + } else { + let current_shell = recognize_shell()?; + println!("Recognized shell: {}", current_shell.to_string()); + current_shell + }; + let home_dir = dirs::home_dir(); + let mut target_dir = String::from("."); + if let Some(home_dir) = home_dir { + target_dir = match shell { + Shell::Bash => { + String::from("/etc/bash_completion.d") + } + Shell::Fish => { + let fish_dir = format!("{}/.config/fish/completions/", home_dir.display()); + fish_dir + } + Shell::Zsh => { + let zsh_dir = std::env::var("ZSH") + .map_err(|_| AppError::RuntimeError(String::from( + "Please install oh-my-zsh for rich autocompletions." + )))?; + format!("{}/completions", zsh_dir) + } + Shell::PowerShell => { String::from(".") } + Shell::Elvish => { String::from(".") } + }; + } + std::fs::create_dir_all(target_dir.as_str()).ok(); + Opt::clap().gen_completions(env!("CARGO_PKG_NAME"), shell, target_dir.as_str()); + println!("Completion file saved at {}", target_dir); + Ok(()) } \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 8cccdf7..6809df4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ -use crate::result::{AppResult, AppError}; +use crate::result::{AppError, AppResult}; +use crate::tty_stuff::{choose_command, choose_episode, choose_pattern}; use crate::CONFIG_PATH; -use std::io::{Write, Read}; -use crate::tty_stuff::{choose_pattern, choose_command, choose_episode}; +use std::io::{Read, Write}; #[derive(Serialize, Default, Clone, Deserialize)] pub struct Config { @@ -19,9 +19,9 @@ impl Config { pub fn new(pattern: String, maybe_command: String) -> AppResult<Self> { let mut command = maybe_command; if pattern.is_empty() { - return Err(AppError::RuntimeError( - String::from("Pattern can't be empty") - )); + return Err(AppError::RuntimeError(String::from( + "Pattern can't be empty", + ))); } if command.is_empty() { command = default_command(); @@ -43,9 +43,7 @@ impl Config { pub fn read() -> AppResult<Self> { let config_path = std::path::Path::new(CONFIG_PATH.as_str()); if !config_path.exists() { - return Err( - AppError::StdErr(String::from("Run 'awatch init' first.")) - ); + return Err(AppError::StdErr(String::from("Run 'awatch init' first."))); } let mut file = std::fs::File::open(CONFIG_PATH.as_str())?; let mut buffer = String::new(); @@ -100,4 +98,4 @@ pub fn get_matched_files(pattern: String) -> AppResult<Vec<String>> { } 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 3eca02e..2a085b8 100644 --- a/src/initialization.rs +++ b/src/initialization.rs @@ -1,11 +1,13 @@ -use crate::result::AppResult; -use crate::tty_stuff::{choose_pattern, choose_command}; use crate::config::Config; +use crate::result::AppResult; +use crate::tty_stuff::{choose_command, choose_pattern}; pub fn init_config() -> AppResult<()> { let pattern = choose_pattern(String::new())?; - let command = choose_command(String::from("mpv --fullscreen \"{}\""))?.trim().to_string(); + let command = choose_command(String::from("mpv --fullscreen \"{}\""))? + .trim() + .to_string(); let config = Config::new(pattern, command)?; config.save()?; Ok(()) -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 0c41364..ea862af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,24 +13,27 @@ lazy_static! { use structopt::StructOpt; +use crate::result::{AppError, AppResult}; use crate::run_modes::run; -use crate::result::AppResult; -use colored::{Colorize, Color}; +use colored::{Color, Colorize}; -pub mod result; pub mod config; +pub mod initialization; +pub mod result; pub mod run_modes; pub mod tty_stuff; -pub mod initialization; +use std::str::FromStr; +use structopt::clap::Shell; include!("cli.rs"); fn main() -> AppResult<()> { let opt: Opt = Opt::from_args(); if let Err(error) = run(opt) { - println!("{dashes} {title} {dashes}", - dashes = "#######".color(Color::BrightRed), - title= "Error".color(Color::BrightRed) + println!( + "{dashes} {title} {dashes}", + dashes = "#######".color(Color::BrightRed), + title = "Error".color(Color::BrightRed) ); println!("{}", error); } diff --git a/src/result.rs b/src/result.rs index c8aff76..3ed3dbe 100644 --- a/src/result.rs +++ b/src/result.rs @@ -26,4 +26,4 @@ impl From<regex::Error> for AppError { fn from(err: regex::Error) -> Self { Self::RuntimeError(err.to_string()) } -} \ No newline at end of file +} diff --git a/src/run_modes.rs b/src/run_modes.rs index a2d910a..ec956ce 100644 --- a/src/run_modes.rs +++ b/src/run_modes.rs @@ -1,30 +1,19 @@ -use crate::{Opt, RunMode}; -use crate::result::{AppResult, AppError}; +use crate::config::{update_config, update_episode, Config}; use crate::initialization::init_config; -use crate::config::{update_episode, update_config, Config}; +use crate::result::{AppError, AppResult}; +use crate::{generate_completion, Opt, RunMode}; use std::process::Command; pub fn run(opts: Opt) -> AppResult<()> { let mode = opts.mode.unwrap_or_else(|| RunMode::Play); match mode { - RunMode::Init => { - init_config() - } - RunMode::Play => { - play() - } - RunMode::Prev => { - update_episode(prev_episode) - } - RunMode::Next => { - update_episode(next_episode) - } - RunMode::Update => { - update_config() - } - RunMode::Reset => { - update_episode(|_| { Ok(0) }) - } + RunMode::Init => init_config(), + RunMode::Play => play(), + RunMode::Prev => update_episode(prev_episode), + RunMode::Next => update_episode(next_episode), + RunMode::Update => update_config(), + RunMode::Reset => update_episode(|_| Ok(0)), + RunMode::Completion { shell } => generate_completion(shell), } } @@ -32,9 +21,9 @@ pub fn prev_episode(current: usize) -> AppResult<usize> { if let Some(episode) = current.checked_sub(1) { Ok(episode) } else { - Err(AppError::RuntimeError( - String::from("Episode can't be less than zero.") - )) + Err(AppError::RuntimeError(String::from( + "Episode can't be less than zero.", + ))) } } @@ -42,9 +31,9 @@ pub fn next_episode(current: usize) -> AppResult<usize> { if let Some(episode) = current.checked_add(1) { Ok(episode) } else { - Err(AppError::RuntimeError( - String::from("Reached usize limit. Sorry.") - )) + Err(AppError::RuntimeError(String::from( + "Reached usize limit. Sorry.", + ))) } } @@ -58,7 +47,8 @@ fn add_leading_zero(n: usize) -> String { fn prepare_command(conf: Config) -> AppResult<String> { let index = conf.current_episode_count; - Ok(conf.command + Ok(conf + .command .replace("{}", conf.get_current_episode()?.as_str()) .replace("{n}", format!("{}", index).as_str()) .replace("{n+}", format!("{}", index + 1).as_str()) @@ -80,4 +70,4 @@ pub fn play() -> AppResult<()> { episode = conf.get_current_episode()?; } Ok(()) -} \ No newline at end of file +} diff --git a/src/tty_stuff.rs b/src/tty_stuff.rs index b89ead4..7d3f8d1 100644 --- a/src/tty_stuff.rs +++ b/src/tty_stuff.rs @@ -1,12 +1,12 @@ -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 crate::result::AppResult; +use std::io::{stdin, stdout, Stdout, Write}; use std::process::exit; use std::str::FromStr; +use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; +use termion::event::Key; +use termion::input::TermRead; +use termion::raw::{IntoRawMode, RawTerminal}; pub fn get_matched_files_grid(pattern: String, screen_width: u16) -> AppResult<String> { let mut grid = Grid::new(GridOptions { @@ -32,20 +32,24 @@ pub fn choose_pattern(current_pattern: String) -> AppResult<String> { current_pattern, |stdout, pattern| { if !pattern.is_empty() { - write!(stdout, "{}------Matched files------", termion::cursor::Goto(1, 3))?; + write!( + stdout, + "{}------Matched files------", + termion::cursor::Goto(1, 3) + )?; let (col, _) = termion::terminal_size()?; let grid = get_matched_files_grid(pattern, col)?; if grid.is_empty() { - write!(stdout, "{}No matches found", - termion::cursor::Goto(1, 4) - )?; + write!(stdout, "{}No matches found", termion::cursor::Goto(1, 4))?; } else { for line in grid.lines() { - write!(stdout, "{}{}{}{}", - termion::cursor::Down(1), - termion::clear::CurrentLine, - termion::cursor::Left(col), - line + write!( + stdout, + "{}{}{}{}", + termion::cursor::Down(1), + termion::clear::CurrentLine, + termion::cursor::Left(col), + line )?; } } @@ -63,7 +67,7 @@ pub fn choose_command(current_command: String) -> AppResult<String> { &mut stdout, "Command to execute files.", current_command, - |_, _| { Ok(()) }, + |_, _| Ok(()), ); stdout.suspend_raw_mode()?; res @@ -75,15 +79,13 @@ pub fn choose_episode(current_episode: usize) -> AppResult<usize> { &mut stdout, "Choose episode.", format!("{}", current_episode), - |_, _| { Ok(()) }, - ).map(|s| { - usize::from_str(s.as_str()).unwrap_or_else(|_| 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, @@ -93,25 +95,28 @@ pub fn read_tty_line( 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) + 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) + 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() { @@ -122,7 +127,7 @@ pub fn read_tty_line( } // Update pattern Key::Char(c) => { - buffer.insert(current_pos - 1, c.clone()); + buffer.insert(current_pos - 1, c); current_pos += 1; } Key::Backspace => { @@ -158,19 +163,25 @@ pub fn read_tty_line( _ => {} } // Clear the current line. - write!(stdout, "{}{}{}", - termion::cursor::Goto(1, 2), - termion::clear::AfterCursor, - buffer)?; + 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) - )?; + write!(stdout, "{}", termion::cursor::Goto(current_pos as u16, 2))?; stdout.flush()?; } - write!(stdout, "{}{}", termion::clear::All, termion::cursor::Goto(1, 1))?; + 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