<commit_before>use std::cmp::max;
use std::path::PathBuf;

use log::{self, error, LevelFilter};
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use structopt::StructOpt;

use alacritty_terminal::config::{Program, PtyConfig};

use crate::config::window::{Class, DEFAULT_NAME};
use crate::config::{serde_utils, UiConfig};

/// CLI options for the main Alacritty executable.
#[derive(StructOpt, Debug)]
#[structopt(author, about, version = env!("VERSION"))]
pub struct Options {
    /// Print all events to stdout.
    #[structopt(long)]
    pub print_events: bool,

    /// Generates ref test.
    #[structopt(long)]
    pub ref_test: bool,

    /// Defines the window title [default: Alacritty].
    #[structopt(short, long)]
    pub title: Option<String>,

    /// Defines window class/app_id on X11/Wayland [default: Alacritty].
    #[structopt(long, value_name = "instance> | <instance>,<general", parse(try_from_str = parse_class))]
    pub class: Option<Class>,

    /// Defines the X11 window ID (as a decimal integer) to embed Alacritty within.
    #[structopt(long)]
    pub embed: Option<String>,

    /// Specify alternative configuration file [default: $XDG_CONFIG_HOME/alacritty/alacritty.yml].
    #[cfg(not(any(target_os = "macos", windows)))]
    #[structopt(long)]
    pub config_file: Option<PathBuf>,

    /// Specify alternative configuration file [default: %APPDATA%\alacritty\alacritty.yml].
    #[cfg(windows)]
    #[structopt(long)]
    pub config_file: Option<PathBuf>,

    /// Specify alternative configuration file [default: $HOME/.config/alacritty/alacritty.yml].
    #[cfg(target_os = "macos")]
    #[structopt(long)]
    pub config_file: Option<PathBuf>,

    /// Path for IPC socket creation.
    #[cfg(unix)]
    #[structopt(long)]
    pub socket: Option<PathBuf>,

    /// Reduces the level of verbosity (the min level is -qq).
    #[structopt(short, conflicts_with("verbose"), parse(from_occurrences))]
    quiet: u8,

    /// Increases the level of verbosity (the max level is -vvv).
    #[structopt(short, conflicts_with("quiet"), parse(from_occurrences))]
    verbose: u8,

    /// Override configuration file options [example: cursor.style=Beam].
    #[structopt(short = "o", long)]
    option: Vec<String>,

    /// CLI options for config overrides.
    #[structopt(skip)]
    pub config_options: Value,

    /// Terminal options which could be passed via IPC.
    #[structopt(flatten)]
    pub terminal_options: TerminalOptions,

    /// Subcommand passed to the CLI.
    #[cfg(unix)]
    #[structopt(subcommand)]
    pub subcommands: Option<Subcommands>,
}

impl Options {
    pub fn new() -> Self {
        let mut options = Self::from_args();

        // Convert `--option` flags into serde `Value`.
        for option in &options.option {
            match option_as_value(option) {
                Ok(value) => {
                    options.config_options = serde_utils::merge(options.config_options, value);
                },
                Err(_) => eprintln!("Invalid CLI config option: {:?}", option),
            }
        }

        options
    }

    /// Override configuration file with options from the CLI.
    pub fn override_config(&self, config: &mut UiConfig) {
        if let Some(title) = self.title.clone() {
            config.window.title = title
        }
        if let Some(class) = &self.class {
            config.window.class = class.clone();
        }

        #[cfg(unix)]
        {
            config.ipc_socket |= self.socket.is_some();
        }

        config.window.dynamic_title &= self.title.is_none();
        config.window.embed = self.embed.as_ref().and_then(|embed| embed.parse().ok());
        config.debug.print_events |= self.print_events;
        config.debug.log_level = max(config.debug.log_level, self.log_level());
        config.debug.ref_test |= self.ref_test;

        if config.debug.print_events {
            config.debug.log_level = max(config.debug.log_level, LevelFilter::Info);
        }
    }

    /// Logging filter level.
    pub fn log_level(&self) -> LevelFilter {
        match (self.quiet, self.verbose) {
            // Force at least `Info` level for `--print-events`.
            (_, 0) if self.print_events => LevelFilter::Info,

            // Default.
            (0, 0) => LevelFilter::Warn,

            // Verbose.
            (_, 1) => LevelFilter::Info,
            (_, 2) => LevelFilter::Debug,
            (0, _) => LevelFilter::Trace,

            // Quiet.
            (1, _) => LevelFilter::Error,
            (..) => LevelFilter::Off,
        }
    }
}

/// Format an option in the format of `parent.field=value` to a serde Value.
fn option_as_value(option: &str) -> Result<Value, serde_yaml::Error> {
    let mut yaml_text = String::with_capacity(option.len());
    let mut closing_brackets = String::new();

    for (i, c) in option.chars().enumerate() {
        match c {
            '=' => {
                yaml_text.push_str(": ");
                yaml_text.push_str(&option[i + 1..]);
                break;
            },
            '.' => {
                yaml_text.push_str(": {");
                closing_brackets.push('}');
            },
            _ => yaml_text.push(c),
        }
    }

    yaml_text += &closing_brackets;

    serde_yaml::from_str(&yaml_text)
}

/// Parse the class CLI parameter.
fn parse_class(input: &str) -> Result<Class, String> {
    match input.find(',') {
        Some(position) => {
            let general = input[position + 1..].to_owned();

            // Warn the user if they've passed too many values.
            if general.contains(',') {
                return Err(String::from("Too many parameters"));
            }

            Ok(Class { instance: input[..position].into(), general })
        },
        None => Ok(Class { instance: input.into(), general: DEFAULT_NAME.into() }),
    }
}

/// Terminal specific cli options which can be passed to new windows via IPC.
#[derive(Serialize, Deserialize, StructOpt, Default, Debug, Clone, PartialEq)]
pub struct TerminalOptions {
    /// Start the shell in the specified working directory.
    #[structopt(long)]
    pub working_directory: Option<PathBuf>,

    /// Remain open after child process exit.
    #[structopt(long)]
    pub hold: bool,

    /// Command and args to execute (must be last argument).
    #[structopt(short = "e", long, allow_hyphen_values = true)]
    command: Vec<String>,
}

impl TerminalOptions {
    /// Shell override passed through the CLI.
    pub fn command(&self) -> Option<Program> {
        let (program, args) = self.command.split_first()?;
        Some(Program::WithArgs { program: program.clone(), args: args.to_vec() })
    }

    /// Override the [`PtyConfig`]'s fields with the [`TerminalOptions`].
    pub fn override_pty_config(&self, pty_config: &mut PtyConfig) {
        if let Some(working_directory) = &self.working_directory {
            if working_directory.is_dir() {
                pty_config.working_directory = Some(working_directory.to_owned());
            } else {
                error!("Invalid working directory: {:?}", working_directory);
            }
        }

        if let Some(command) = self.command() {
            pty_config.shell = Some(command);
        }

        pty_config.hold |= self.hold;
    }
}

impl From<TerminalOptions> for PtyConfig {
    fn from(mut options: TerminalOptions) -> Self {
        PtyConfig {
            working_directory: options.working_directory.take(),
            shell: options.command(),
            hold: options.hold,
        }
    }
}

/// Available CLI subcommands.
#[cfg(unix)]
#[derive(StructOpt, Debug)]
pub enum Subcommands {
    Msg(MessageOptions),
}

/// Send a message to the Alacritty socket.
#[cfg(unix)]
#[derive(StructOpt, Debug)]
pub struct MessageOptions {
    /// IPC socket connection path override.
    #[structopt(long, short)]
    pub socket: Option<PathBuf>,

    /// Message which should be sent.
    #[structopt(subcommand)]
    pub message: SocketMessage,
}

/// Available socket messages.
#[cfg(unix)]
#[derive(StructOpt, Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum SocketMessage {
    /// Create a new window in the same Alacritty process.
    CreateWindow(TerminalOptions),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(target_os = "linux")]
    use std::fs::File;
    #[cfg(target_os = "linux")]
    use std::io::Read;

    #[cfg(target_os = "linux")]
    use clap::Shell;
    use serde_yaml::mapping::Mapping;

    #[test]
    fn dynamic_title_ignoring_options_by_default() {
        let mut config = UiConfig::default();
        let old_dynamic_title = config.window.dynamic_title;

        Options::new().override_config(&mut config);

        assert_eq!(old_dynamic_title, config.window.dynamic_title);
    }

    #[test]
    fn dynamic_title_overridden_by_options() {
        let mut config = UiConfig::default();

        let options = Options { title: Some("foo".to_owned()), ..Options::new() };
        options.override_config(&mut config);

        assert!(!config.window.dynamic_title);
    }

    #[test]
    fn dynamic_title_not_overridden_by_config() {
        let mut config = UiConfig::default();

        config.window.title = "foo".to_owned();
        Options::new().override_config(&mut config);

        assert!(config.window.dynamic_title);
    }

    #[test]
    fn valid_option_as_value() {
        // Test with a single field.
        let value = option_as_value("field=true").unwrap();

        let mut mapping = Mapping::new();
        mapping.insert(Value::String(String::from("field")), Value::Bool(true));

        assert_eq!(value, Value::Mapping(mapping));

        // Test with nested fields
        let value = option_as_value("parent.field=true").unwrap();

        let mut parent_mapping = Mapping::new();
        parent_mapping.insert(Value::String(String::from("field")), Value::Bool(true));
        let mut mapping = Mapping::new();
        mapping.insert(Value::String(String::from("parent")), Value::Mapping(parent_mapping));

        assert_eq!(value, Value::Mapping(mapping));
    }

    #[test]
    fn invalid_option_as_value() {
        let value = option_as_value("}");
        assert!(value.is_err());
    }

    #[test]
    fn float_option_as_value() {
        let value = option_as_value("float=3.4").unwrap();

        let mut expected = Mapping::new();
        expected.insert(Value::String(String::from("float")), Value::Number(3.4.into()));

        assert_eq!(value, Value::Mapping(expected));
    }

    #[test]
    fn parse_instance_class() {
        let class = parse_class("one").unwrap();
        assert_eq!(class.instance, "one");
        assert_eq!(class.general, DEFAULT_NAME);
    }

    #[test]
    fn parse_general_class() {
        let class = parse_class("one,two").unwrap();
        assert_eq!(class.instance, "one");
        assert_eq!(class.general, "two");
    }

    #[test]
    fn parse_invalid_class() {
        let class = parse_class("one,two,three");
        assert!(class.is_err());
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn completions() {
        let mut clap = Options::clap();

        for (shell, file) in &[
            (Shell::Bash, "alacritty.bash"),
            (Shell::Fish, "alacritty.fish"),
            (Shell::Zsh, "_alacritty"),
        ] {
            let mut generated = Vec::new();
            clap.gen_completions_to("alacritty", *shell, &mut generated);
            let generated = String::from_utf8_lossy(&generated);

            let mut completion = String::new();
            let mut file = File::open(format!("../extra/completions/{}", file)).unwrap();
            file.read_to_string(&mut completion).unwrap();

            assert_eq!(generated, completion);
        }

        // NOTE: Use this to generate new completions.
        //
        // clap.gen_completions("alacritty", Shell::Bash, "../extra/completions/");
        // clap.gen_completions("alacritty", Shell::Fish, "../extra/completions/");
        // clap.gen_completions("alacritty", Shell::Zsh, "../extra/completions/");
    }
}
<commit_msg>Fix test failures due to CLI parameters<commit_after>use std::cmp::max;
use std::path::PathBuf;

use log::{self, error, LevelFilter};
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use structopt::StructOpt;

use alacritty_terminal::config::{Program, PtyConfig};

use crate::config::window::{Class, DEFAULT_NAME};
use crate::config::{serde_utils, UiConfig};

/// CLI options for the main Alacritty executable.
#[derive(StructOpt, Default, Debug)]
#[structopt(author, about, version = env!("VERSION"))]
pub struct Options {
    /// Print all events to stdout.
    #[structopt(long)]
    pub print_events: bool,

    /// Generates ref test.
    #[structopt(long)]
    pub ref_test: bool,

    /// Defines the window title [default: Alacritty].
    #[structopt(short, long)]
    pub title: Option<String>,

    /// Defines window class/app_id on X11/Wayland [default: Alacritty].
    #[structopt(long, value_name = "instance> | <instance>,<general", parse(try_from_str = parse_class))]
    pub class: Option<Class>,

    /// Defines the X11 window ID (as a decimal integer) to embed Alacritty within.
    #[structopt(long)]
    pub embed: Option<String>,

    /// Specify alternative configuration file [default: $XDG_CONFIG_HOME/alacritty/alacritty.yml].
    #[cfg(not(any(target_os = "macos", windows)))]
    #[structopt(long)]
    pub config_file: Option<PathBuf>,

    /// Specify alternative configuration file [default: %APPDATA%\alacritty\alacritty.yml].
    #[cfg(windows)]
    #[structopt(long)]
    pub config_file: Option<PathBuf>,

    /// Specify alternative configuration file [default: $HOME/.config/alacritty/alacritty.yml].
    #[cfg(target_os = "macos")]
    #[structopt(long)]
    pub config_file: Option<PathBuf>,

    /// Path for IPC socket creation.
    #[cfg(unix)]
    #[structopt(long)]
    pub socket: Option<PathBuf>,

    /// Reduces the level of verbosity (the min level is -qq).
    #[structopt(short, conflicts_with("verbose"), parse(from_occurrences))]
    quiet: u8,

    /// Increases the level of verbosity (the max level is -vvv).
    #[structopt(short, conflicts_with("quiet"), parse(from_occurrences))]
    verbose: u8,

    /// Override configuration file options [example: cursor.style=Beam].
    #[structopt(short = "o", long)]
    option: Vec<String>,

    /// CLI options for config overrides.
    #[structopt(skip)]
    pub config_options: Value,

    /// Terminal options which could be passed via IPC.
    #[structopt(flatten)]
    pub terminal_options: TerminalOptions,

    /// Subcommand passed to the CLI.
    #[cfg(unix)]
    #[structopt(subcommand)]
    pub subcommands: Option<Subcommands>,
}

impl Options {
    pub fn new() -> Self {
        let mut options = Self::from_args();

        // Convert `--option` flags into serde `Value`.
        for option in &options.option {
            match option_as_value(option) {
                Ok(value) => {
                    options.config_options = serde_utils::merge(options.config_options, value);
                },
                Err(_) => eprintln!("Invalid CLI config option: {:?}", option),
            }
        }

        options
    }

    /// Override configuration file with options from the CLI.
    pub fn override_config(&self, config: &mut UiConfig) {
        if let Some(title) = self.title.clone() {
            config.window.title = title
        }
        if let Some(class) = &self.class {
            config.window.class = class.clone();
        }

        #[cfg(unix)]
        {
            config.ipc_socket |= self.socket.is_some();
        }

        config.window.dynamic_title &= self.title.is_none();
        config.window.embed = self.embed.as_ref().and_then(|embed| embed.parse().ok());
        config.debug.print_events |= self.print_events;
        config.debug.log_level = max(config.debug.log_level, self.log_level());
        config.debug.ref_test |= self.ref_test;

        if config.debug.print_events {
            config.debug.log_level = max(config.debug.log_level, LevelFilter::Info);
        }
    }

    /// Logging filter level.
    pub fn log_level(&self) -> LevelFilter {
        match (self.quiet, self.verbose) {
            // Force at least `Info` level for `--print-events`.
            (_, 0) if self.print_events => LevelFilter::Info,

            // Default.
            (0, 0) => LevelFilter::Warn,

            // Verbose.
            (_, 1) => LevelFilter::Info,
            (_, 2) => LevelFilter::Debug,
            (0, _) => LevelFilter::Trace,

            // Quiet.
            (1, _) => LevelFilter::Error,
            (..) => LevelFilter::Off,
        }
    }
}

/// Format an option in the format of `parent.field=value` to a serde Value.
fn option_as_value(option: &str) -> Result<Value, serde_yaml::Error> {
    let mut yaml_text = String::with_capacity(option.len());
    let mut closing_brackets = String::new();

    for (i, c) in option.chars().enumerate() {
        match c {
            '=' => {
                yaml_text.push_str(": ");
                yaml_text.push_str(&option[i + 1..]);
                break;
            },
            '.' => {
                yaml_text.push_str(": {");
                closing_brackets.push('}');
            },
            _ => yaml_text.push(c),
        }
    }

    yaml_text += &closing_brackets;

    serde_yaml::from_str(&yaml_text)
}

/// Parse the class CLI parameter.
fn parse_class(input: &str) -> Result<Class, String> {
    match input.find(',') {
        Some(position) => {
            let general = input[position + 1..].to_owned();

            // Warn the user if they've passed too many values.
            if general.contains(',') {
                return Err(String::from("Too many parameters"));
            }

            Ok(Class { instance: input[..position].into(), general })
        },
        None => Ok(Class { instance: input.into(), general: DEFAULT_NAME.into() }),
    }
}

/// Terminal specific cli options which can be passed to new windows via IPC.
#[derive(Serialize, Deserialize, StructOpt, Default, Debug, Clone, PartialEq)]
pub struct TerminalOptions {
    /// Start the shell in the specified working directory.
    #[structopt(long)]
    pub working_directory: Option<PathBuf>,

    /// Remain open after child process exit.
    #[structopt(long)]
    pub hold: bool,

    /// Command and args to execute (must be last argument).
    #[structopt(short = "e", long, allow_hyphen_values = true)]
    command: Vec<String>,
}

impl TerminalOptions {
    /// Shell override passed through the CLI.
    pub fn command(&self) -> Option<Program> {
        let (program, args) = self.command.split_first()?;
        Some(Program::WithArgs { program: program.clone(), args: args.to_vec() })
    }

    /// Override the [`PtyConfig`]'s fields with the [`TerminalOptions`].
    pub fn override_pty_config(&self, pty_config: &mut PtyConfig) {
        if let Some(working_directory) = &self.working_directory {
            if working_directory.is_dir() {
                pty_config.working_directory = Some(working_directory.to_owned());
            } else {
                error!("Invalid working directory: {:?}", working_directory);
            }
        }

        if let Some(command) = self.command() {
            pty_config.shell = Some(command);
        }

        pty_config.hold |= self.hold;
    }
}

impl From<TerminalOptions> for PtyConfig {
    fn from(mut options: TerminalOptions) -> Self {
        PtyConfig {
            working_directory: options.working_directory.take(),
            shell: options.command(),
            hold: options.hold,
        }
    }
}

/// Available CLI subcommands.
#[cfg(unix)]
#[derive(StructOpt, Debug)]
pub enum Subcommands {
    Msg(MessageOptions),
}

/// Send a message to the Alacritty socket.
#[cfg(unix)]
#[derive(StructOpt, Debug)]
pub struct MessageOptions {
    /// IPC socket connection path override.
    #[structopt(long, short)]
    pub socket: Option<PathBuf>,

    /// Message which should be sent.
    #[structopt(subcommand)]
    pub message: SocketMessage,
}

/// Available socket messages.
#[cfg(unix)]
#[derive(StructOpt, Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum SocketMessage {
    /// Create a new window in the same Alacritty process.
    CreateWindow(TerminalOptions),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(target_os = "linux")]
    use std::fs::File;
    #[cfg(target_os = "linux")]
    use std::io::Read;

    #[cfg(target_os = "linux")]
    use clap::Shell;
    use serde_yaml::mapping::Mapping;

    #[test]
    fn dynamic_title_ignoring_options_by_default() {
        let mut config = UiConfig::default();
        let old_dynamic_title = config.window.dynamic_title;

        Options::default().override_config(&mut config);

        assert_eq!(old_dynamic_title, config.window.dynamic_title);
    }

    #[test]
    fn dynamic_title_overridden_by_options() {
        let mut config = UiConfig::default();

        let options = Options { title: Some("foo".to_owned()), ..Options::default() };
        options.override_config(&mut config);

        assert!(!config.window.dynamic_title);
    }

    #[test]
    fn dynamic_title_not_overridden_by_config() {
        let mut config = UiConfig::default();

        config.window.title = "foo".to_owned();
        Options::default().override_config(&mut config);

        assert!(config.window.dynamic_title);
    }

    #[test]
    fn valid_option_as_value() {
        // Test with a single field.
        let value = option_as_value("field=true").unwrap();

        let mut mapping = Mapping::new();
        mapping.insert(Value::String(String::from("field")), Value::Bool(true));

        assert_eq!(value, Value::Mapping(mapping));

        // Test with nested fields
        let value = option_as_value("parent.field=true").unwrap();

        let mut parent_mapping = Mapping::new();
        parent_mapping.insert(Value::String(String::from("field")), Value::Bool(true));
        let mut mapping = Mapping::new();
        mapping.insert(Value::String(String::from("parent")), Value::Mapping(parent_mapping));

        assert_eq!(value, Value::Mapping(mapping));
    }

    #[test]
    fn invalid_option_as_value() {
        let value = option_as_value("}");
        assert!(value.is_err());
    }

    #[test]
    fn float_option_as_value() {
        let value = option_as_value("float=3.4").unwrap();

        let mut expected = Mapping::new();
        expected.insert(Value::String(String::from("float")), Value::Number(3.4.into()));

        assert_eq!(value, Value::Mapping(expected));
    }

    #[test]
    fn parse_instance_class() {
        let class = parse_class("one").unwrap();
        assert_eq!(class.instance, "one");
        assert_eq!(class.general, DEFAULT_NAME);
    }

    #[test]
    fn parse_general_class() {
        let class = parse_class("one,two").unwrap();
        assert_eq!(class.instance, "one");
        assert_eq!(class.general, "two");
    }

    #[test]
    fn parse_invalid_class() {
        let class = parse_class("one,two,three");
        assert!(class.is_err());
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn completions() {
        let mut clap = Options::clap();

        for (shell, file) in &[
            (Shell::Bash, "alacritty.bash"),
            (Shell::Fish, "alacritty.fish"),
            (Shell::Zsh, "_alacritty"),
        ] {
            let mut generated = Vec::new();
            clap.gen_completions_to("alacritty", *shell, &mut generated);
            let generated = String::from_utf8_lossy(&generated);

            let mut completion = String::new();
            let mut file = File::open(format!("../extra/completions/{}", file)).unwrap();
            file.read_to_string(&mut completion).unwrap();

            assert_eq!(generated, completion);
        }

        // NOTE: Use this to generate new completions.
        //
        // clap.gen_completions("alacritty", Shell::Bash, "../extra/completions/");
        // clap.gen_completions("alacritty", Shell::Fish, "../extra/completions/");
        // clap.gen_completions("alacritty", Shell::Zsh, "../extra/completions/");
    }
}
<|endoftext|>


<commit_before>mod err;

use anime::local::{EpisodeMap, EpisodeMatcher};
use anime::remote::anilist::AniList;
use anime::remote::{RemoteService, SeriesID, SeriesInfo};
use clap::{clap_app, ArgMatches};
use err::Result;
use snafu::{ensure, OptionExt, ResultExt};
use std::fs;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use std::thread;
use std::time::Duration;

fn main() {
    let args = clap_app!(anisplit =>
        (version: env!("CARGO_PKG_VERSION"))
        (author: env!("CARGO_PKG_AUTHORS"))
        (about: "This is a tool to split up an anime series that has multiple \
                 seasons merged together.")
        (@arg path: +takes_value +required "The path pointing to the series to split")
        (@arg out_dir: -o --out +takes_value "The path to create the split seasons in. If this is not specified, the parent directory of the series path will be used")
        (@arg series_id: -i --id +takes_value "The anime series ID. Use if the program doesn't detect the right series automatically")
        (@arg name_format: -f --format +takes_value "The format to rename the files as. Must contain \"{title}\" and \"{episode}\"")
        (@arg matcher: -m --matcher +takes_value "The custom pattern to match episode files with")
        (@group method =>
            (@arg symlink: --symlink "Create symbolic links that point to the original episode files. This is the default method")
            (@arg hardlink: --hardlink "Create hard links that point to the original episode files")
            (@arg move: --move "Move the original episode files and rename them")
        )
    )
    .get_matches();

    if let Err(err) = run(&args) {
        err::display_error(err);
        std::process::exit(1);
    }
}

fn run(args: &ArgMatches) -> Result<()> {
    let remote = AniList::unauthenticated();

    let path = PathBuf::from(args.value_of("path").unwrap())
        .canonicalize()
        .context(err::IO)?;

    let name_format = match args.value_of("name_format") {
        Some(format) => NameFormat::new(format)?,
        None => NameFormat::new("{title} - {episode}.mkv")?,
    };

    let matcher = match args.value_of("matcher") {
        Some(pattern) => {
            let pattern = pattern
                .replace("{title}", "(?P<title>.+)")
                .replace("{episode}", r"(?P<episode>\d+)");
            EpisodeMatcher::from_pattern(pattern)?
        }
        None => EpisodeMatcher::new(),
    };

    let out_dir = match args.value_of("out_dir") {
        Some(out_dir) => PathBuf::from(out_dir),
        None => path.parent().context(err::NoDirParent)?.into(),
    };

    let data = SeriesData {
        episodes: EpisodeMap::parse(&path, &matcher)?,
        name_format,
        link_method: LinkMethod::from_args(args),
        path,
        out_dir,
    };

    let series = {
        let title = parse_path_title(&data.path)?;
        find_series_info(args, title, &remote)?
    };

    format_sequels(&data, series, &remote)
}

struct SeriesData {
    episodes: EpisodeMap,
    name_format: NameFormat,
    link_method: LinkMethod,
    path: PathBuf,
    out_dir: PathBuf,
}

struct NameFormat(String);

impl NameFormat {
    fn new<S>(format: S) -> Result<NameFormat>
    where
        S: Into<String>,
    {
        let format = format.into();

        ensure!(
            format.contains("{title}"),
            err::MissingFormatGroup { group: "title" }
        );

        ensure!(
            format.contains("{episode}"),
            err::MissingFormatGroup { group: "episode" }
        );

        Ok(NameFormat(format))
    }

    fn process<S>(&self, name: S, episode: u32) -> String
    where
        S: AsRef<str>,
    {
        self.0
            .replace("{title}", name.as_ref())
            .replace("{episode}", &format!("{:02}", episode))
    }
}

enum LinkMethod {
    Symlink,
    Hardlink,
    Move,
}

impl LinkMethod {
    fn from_args(args: &ArgMatches) -> LinkMethod {
        if args.is_present("symlink") {
            LinkMethod::Symlink
        } else if args.is_present("hardlink") {
            LinkMethod::Hardlink
        } else if args.is_present("move") {
            LinkMethod::Move
        } else {
            LinkMethod::default()
        }
    }

    fn execute<P>(&self, from: P, to: P) -> Result<()>
    where
        P: AsRef<Path>,
    {
        let from = from.as_ref();
        let to = to.as_ref();

        let result = match self {
            LinkMethod::Symlink => symlink(from, to),
            LinkMethod::Hardlink => fs::hard_link(from, to),
            LinkMethod::Move => fs::rename(from, to),
        };

        result.context(err::LinkIO { from, to })
    }
}

impl Default for LinkMethod {
    fn default() -> LinkMethod {
        LinkMethod::Symlink
    }
}

fn format_sequels(data: &SeriesData, mut info: SeriesInfo, remote: &AniList) -> Result<()> {
    let mut episode_offset = 0;

    while let Some(sequel) = info.sequel {
        info = remote.search_info_by_id(sequel)?;
        episode_offset += info.episodes;

        format_series(&data, &info, episode_offset)?;

        // We don't need to sleep if there isn't another sequel
        if info.sequel.is_none() {
            break;
        }

        thread::sleep(Duration::from_millis(500));
    }

    Ok(())
}

fn format_series(data: &SeriesData, info: &SeriesInfo, episode_offset: u32) -> Result<()> {
    let out_dir = data.out_dir.join(&info.title.preferred);
    let mut out_dir_exists = out_dir.exists();

    let mut num_links_created = 0;

    for real_ep_num in (1 + episode_offset)..=(episode_offset + info.episodes) {
        let original_filename = match data.episodes.get(&real_ep_num) {
            Some(filename) => filename,
            None => continue,
        };

        // We only want to create the directory for the season if we have any episodes
        // from it
        if !out_dir_exists {
            fs::create_dir_all(&out_dir).context(err::FileIO { path: &out_dir })?;
            out_dir_exists = true;
        }

        let episode_path = data.path.join(original_filename);

        let new_filename = data
            .name_format
            .process(&info.title.preferred, real_ep_num - episode_offset);

        let link_path = out_dir.join(new_filename);

        match data.link_method.execute(&episode_path, &link_path) {
            Ok(()) => num_links_created += 1,
            Err(err) => eprintln!("{}", err),
        }
    }

    println!(
        "created {} links for {}",
        num_links_created, info.title.preferred
    );

    Ok(())
}

fn parse_path_title<P>(path: P) -> Result<String>
where
    P: AsRef<Path>,
{
    let path = path.as_ref();

    ensure!(path.is_dir(), err::NotADirectory);

    let title = path
        .file_name()
        .context(err::NoDirName)?
        .to_string_lossy()
        .into_owned();

    Ok(title)
}

fn find_series_info<S>(args: &ArgMatches, title: S, remote: &AniList) -> Result<SeriesInfo>
where
    S: AsRef<str>,
{
    match args.value_of("series_id") {
        Some(id) => {
            let id = id.parse::<SeriesID>().context(err::InvalidSeriesID)?;
            let series = remote.search_info_by_id(id)?;
            Ok(series)
        }
        None => {
            let title = title.as_ref();
            let mut results = remote.search_info_by_name(title)?;
            let best_result = detect::best_matching_info(title, &results)
                .context(err::UnableToDetectSeries { title })?;

            let series = results.swap_remove(best_result);
            Ok(series)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn name_format_detect_no_group() {
        NameFormat::new("useless").unwrap();
    }

    #[test]
    fn name_format_detect_no_title_group() {
        let result = NameFormat::new("missing_title - {episode}.mkv");

        match result {
            Err(err::Error::MissingFormatGroup { group }) if group == "title" => (),
            Ok(_) => panic!("expected missing title group error"),
            Err(err) => panic!("expected missing title group error, got: {:?}", err),
        }
    }

    #[test]
    fn name_format_detect_no_episode_group() {
        let result = NameFormat::new("{title} - missing_episode.mkv");

        match result {
            Err(err::Error::MissingFormatGroup { group }) if group == "episode" => (),
            Ok(_) => panic!("expected missing episode group error"),
            Err(err) => panic!("expected missing episode group error, got: {:?}", err),
        }
    }
}
<commit_msg>anisplit: add preview link method<commit_after>mod err;

use anime::local::{EpisodeMap, EpisodeMatcher};
use anime::remote::anilist::AniList;
use anime::remote::{RemoteService, SeriesID, SeriesInfo};
use clap::{clap_app, ArgMatches};
use err::Result;
use snafu::{ensure, OptionExt, ResultExt};
use std::fs;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use std::thread;
use std::time::Duration;

fn main() {
    let args = clap_app!(anisplit =>
        (version: env!("CARGO_PKG_VERSION"))
        (author: env!("CARGO_PKG_AUTHORS"))
        (about: "This is a tool to split up an anime series that has multiple \
                 seasons merged together.")
        (@arg path: +takes_value +required "The path pointing to the series to split")
        (@arg out_dir: -o --out +takes_value "The path to create the split seasons in. If this is not specified, the parent directory of the series path will be used")
        (@arg series_id: -i --id +takes_value "The anime series ID. Use if the program doesn't detect the right series automatically")
        (@arg name_format: -f --format +takes_value "The format to rename the files as. Must contain \"{title}\" and \"{episode}\"")
        (@arg matcher: -m --matcher +takes_value "The custom pattern to match episode files with")
        (@group method =>
            (@arg symlink: --symlink "Create symbolic links that point to the original episode files. This is the default method")
            (@arg hardlink: --hardlink "Create hard links that point to the original episode files")
            (@arg move: --move "Move the original episode files and rename them")
            (@arg preview: --preview "Show the changes to files that will be made")
        )
    )
    .get_matches();

    if let Err(err) = run(&args) {
        err::display_error(err);
        std::process::exit(1);
    }
}

fn run(args: &ArgMatches) -> Result<()> {
    let remote = AniList::unauthenticated();

    let path = PathBuf::from(args.value_of("path").unwrap())
        .canonicalize()
        .context(err::IO)?;

    let name_format = match args.value_of("name_format") {
        Some(format) => NameFormat::new(format)?,
        None => NameFormat::new("{title} - {episode}.mkv")?,
    };

    let matcher = match args.value_of("matcher") {
        Some(pattern) => {
            let pattern = pattern
                .replace("{title}", "(?P<title>.+)")
                .replace("{episode}", r"(?P<episode>\d+)");
            EpisodeMatcher::from_pattern(pattern)?
        }
        None => EpisodeMatcher::new(),
    };

    let out_dir = match args.value_of("out_dir") {
        Some(out_dir) => PathBuf::from(out_dir),
        None => path.parent().context(err::NoDirParent)?.into(),
    };

    let data = SeriesData {
        episodes: EpisodeMap::parse(&path, &matcher)?,
        name_format,
        link_method: LinkMethod::from_args(args),
        path,
        out_dir,
    };

    let series = {
        let title = parse_path_title(&data.path)?;
        find_series_info(args, title, &remote)?
    };

    format_sequels(&data, series, &remote)
}

struct SeriesData {
    episodes: EpisodeMap,
    name_format: NameFormat,
    link_method: LinkMethod,
    path: PathBuf,
    out_dir: PathBuf,
}

struct NameFormat(String);

impl NameFormat {
    fn new<S>(format: S) -> Result<NameFormat>
    where
        S: Into<String>,
    {
        let format = format.into();

        ensure!(
            format.contains("{title}"),
            err::MissingFormatGroup { group: "title" }
        );

        ensure!(
            format.contains("{episode}"),
            err::MissingFormatGroup { group: "episode" }
        );

        Ok(NameFormat(format))
    }

    fn process<S>(&self, name: S, episode: u32) -> String
    where
        S: AsRef<str>,
    {
        self.0
            .replace("{title}", name.as_ref())
            .replace("{episode}", &format!("{:02}", episode))
    }
}

enum LinkMethod {
    Symlink,
    Hardlink,
    Move,
    Preview,
}

impl LinkMethod {
    fn from_args(args: &ArgMatches) -> LinkMethod {
        if args.is_present("symlink") {
            LinkMethod::Symlink
        } else if args.is_present("hardlink") {
            LinkMethod::Hardlink
        } else if args.is_present("move") {
            LinkMethod::Move
        } else if args.is_present("preview") {
            LinkMethod::Preview
        } else {
            LinkMethod::default()
        }
    }

    fn execute<P>(&self, from: P, to: P) -> Result<()>
    where
        P: AsRef<Path>,
    {
        let from = from.as_ref();
        let to = to.as_ref();

        let result = match self {
            LinkMethod::Symlink => symlink(from, to),
            LinkMethod::Hardlink => fs::hard_link(from, to),
            LinkMethod::Move => fs::rename(from, to),
            LinkMethod::Preview => {
                println!(
                    "preview: {} -> {}\n",
                    from.to_string_lossy(),
                    to.to_string_lossy()
                );
                Ok(())
            }
        };

        result.context(err::LinkIO { from, to })
    }
}

impl Default for LinkMethod {
    fn default() -> LinkMethod {
        LinkMethod::Symlink
    }
}

fn format_sequels(data: &SeriesData, mut info: SeriesInfo, remote: &AniList) -> Result<()> {
    let mut episode_offset = 0;

    while let Some(sequel) = info.sequel {
        info = remote.search_info_by_id(sequel)?;
        episode_offset += info.episodes;

        format_series(&data, &info, episode_offset)?;

        // We don't need to sleep if there isn't another sequel
        if info.sequel.is_none() {
            break;
        }

        thread::sleep(Duration::from_millis(500));
    }

    Ok(())
}

fn format_series(data: &SeriesData, info: &SeriesInfo, episode_offset: u32) -> Result<()> {
    let out_dir = data.out_dir.join(&info.title.preferred);
    let mut out_dir_exists = out_dir.exists();

    let mut num_links_created = 0;

    for real_ep_num in (1 + episode_offset)..=(episode_offset + info.episodes) {
        let original_filename = match data.episodes.get(&real_ep_num) {
            Some(filename) => filename,
            None => continue,
        };

        // We only want to create the directory for the season if we have any episodes
        // from it
        if !out_dir_exists {
            fs::create_dir_all(&out_dir).context(err::FileIO { path: &out_dir })?;
            out_dir_exists = true;
        }

        let episode_path = data.path.join(original_filename);

        let new_filename = data
            .name_format
            .process(&info.title.preferred, real_ep_num - episode_offset);

        let link_path = out_dir.join(new_filename);

        match data.link_method.execute(&episode_path, &link_path) {
            Ok(()) => num_links_created += 1,
            Err(err) => eprintln!("{}", err),
        }
    }

    println!(
        "created {} links for {}",
        num_links_created, info.title.preferred
    );

    Ok(())
}

fn parse_path_title<P>(path: P) -> Result<String>
where
    P: AsRef<Path>,
{
    let path = path.as_ref();

    ensure!(path.is_dir(), err::NotADirectory);

    let title = path
        .file_name()
        .context(err::NoDirName)?
        .to_string_lossy()
        .into_owned();

    Ok(title)
}

fn find_series_info<S>(args: &ArgMatches, title: S, remote: &AniList) -> Result<SeriesInfo>
where
    S: AsRef<str>,
{
    match args.value_of("series_id") {
        Some(id) => {
            let id = id.parse::<SeriesID>().context(err::InvalidSeriesID)?;
            let series = remote.search_info_by_id(id)?;
            Ok(series)
        }
        None => {
            let title = title.as_ref();
            let mut results = remote.search_info_by_name(title)?;
            let best_result = detect::best_matching_info(title, &results)
                .context(err::UnableToDetectSeries { title })?;

            let series = results.swap_remove(best_result);
            Ok(series)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn name_format_detect_no_group() {
        NameFormat::new("useless").unwrap();
    }

    #[test]
    fn name_format_detect_no_title_group() {
        let result = NameFormat::new("missing_title - {episode}.mkv");

        match result {
            Err(err::Error::MissingFormatGroup { group }) if group == "title" => (),
            Ok(_) => panic!("expected missing title group error"),
            Err(err) => panic!("expected missing title group error, got: {:?}", err),
        }
    }

    #[test]
    fn name_format_detect_no_episode_group() {
        let result = NameFormat::new("{title} - missing_episode.mkv");

        match result {
            Err(err::Error::MissingFormatGroup { group }) if group == "episode" => (),
            Ok(_) => panic!("expected missing episode group error"),
            Err(err) => panic!("expected missing episode group error, got: {:?}", err),
        }
    }
}
<|endoftext|>


<commit_before>use crate::common::util::*;

#[test]
fn test_rm_one_file() {
    let (at, mut ucmd) = at_and_ucmd!();
    let file = "test_rm_one_file";

    at.touch(file);

    ucmd.arg(file).succeeds().no_stderr();

    assert!(!at.file_exists(file));
}

#[test]
fn test_rm_failed() {
    let (_at, mut ucmd) = at_and_ucmd!();
    let file = "test_rm_one_file"; // Doesn't exist

    ucmd.arg(file).fails().stderr_contains(&format!(
        "cannot remove '{}': No such file or directory",
        file
    ));
}

#[test]
fn test_rm_multiple_files() {
    let (at, mut ucmd) = at_and_ucmd!();
    let file_a = "test_rm_multiple_file_a";
    let file_b = "test_rm_multiple_file_b";

    at.touch(file_a);
    at.touch(file_b);

    ucmd.arg(file_a).arg(file_b).succeeds().no_stderr();

    assert!(!at.file_exists(file_a));
    assert!(!at.file_exists(file_b));
}

#[test]
fn test_rm_interactive() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    let file_a = "test_rm_interactive_file_a";
    let file_b = "test_rm_interactive_file_b";

    at.touch(file_a);
    at.touch(file_b);

    scene
        .ucmd()
        .arg("-i")
        .arg(file_a)
        .arg(file_b)
        .pipe_in("n")
        .succeeds();

    assert!(at.file_exists(file_a));
    assert!(at.file_exists(file_b));

    scene
        .ucmd()
        .arg("-i")
        .arg(file_a)
        .arg(file_b)
        .pipe_in("Yesh") // spell-checker:disable-line
        .succeeds();

    assert!(!at.file_exists(file_a));
    assert!(at.file_exists(file_b));
}

#[test]
fn test_rm_force() {
    let (at, mut ucmd) = at_and_ucmd!();
    let file_a = "test_rm_force_a";
    let file_b = "test_rm_force_b";

    ucmd.arg("-f")
        .arg(file_a)
        .arg(file_b)
        .succeeds()
        .no_stderr();

    assert!(!at.file_exists(file_a));
    assert!(!at.file_exists(file_b));
}

#[test]
fn test_rm_force_multiple() {
    let (at, mut ucmd) = at_and_ucmd!();
    let file_a = "test_rm_force_a";
    let file_b = "test_rm_force_b";

    ucmd.arg("-f")
        .arg("-f")
        .arg("-f")
        .arg(file_a)
        .arg(file_b)
        .succeeds()
        .no_stderr();

    assert!(!at.file_exists(file_a));
    assert!(!at.file_exists(file_b));
}

#[test]
fn test_rm_empty_directory() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_empty_directory";

    at.mkdir(dir);

    ucmd.arg("-d").arg(dir).succeeds().no_stderr();

    assert!(!at.dir_exists(dir));
}

#[test]
fn test_rm_empty_directory_verbose() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_empty_directory_verbose";

    at.mkdir(dir);

    ucmd.arg("-d")
        .arg("-v")
        .arg(dir)
        .succeeds()
        .stdout_only(format!("removed directory '{}'\n", dir));

    assert!(!at.dir_exists(dir));
}

#[test]
fn test_rm_non_empty_directory() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_non_empty_dir";
    let file_a = &format!("{}/test_rm_non_empty_file_a", dir);

    at.mkdir(dir);
    at.touch(file_a);

    ucmd.arg("-d")
        .arg(dir)
        .fails()
        .stderr_contains(&format!("cannot remove '{}': Directory not empty", dir));
    assert!(at.file_exists(file_a));
    assert!(at.dir_exists(dir));
}

#[test]
fn test_rm_recursive() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_recursive_directory";
    let file_a = "test_rm_recursive_directory/test_rm_recursive_file_a";
    let file_b = "test_rm_recursive_directory/test_rm_recursive_file_b";

    at.mkdir(dir);
    at.touch(file_a);
    at.touch(file_b);

    ucmd.arg("-r").arg(dir).succeeds().no_stderr();

    assert!(!at.dir_exists(dir));
    assert!(!at.file_exists(file_a));
    assert!(!at.file_exists(file_b));
}

#[test]
fn test_rm_recursive_multiple() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_recursive_directory";
    let file_a = "test_rm_recursive_directory/test_rm_recursive_file_a";
    let file_b = "test_rm_recursive_directory/test_rm_recursive_file_b";

    at.mkdir(dir);
    at.touch(file_a);
    at.touch(file_b);

    ucmd.arg("-r")
        .arg("-r")
        .arg("-r")
        .arg(dir)
        .succeeds()
        .no_stderr();

    assert!(!at.dir_exists(dir));
    assert!(!at.file_exists(file_a));
    assert!(!at.file_exists(file_b));
}

#[test]
fn test_rm_directory_without_flag() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_directory_without_flag_dir";

    at.mkdir(dir);

    ucmd.arg(dir)
        .fails()
        .stderr_contains(&format!("cannot remove '{}': Is a directory", dir));
}

#[test]
fn test_rm_verbose() {
    let (at, mut ucmd) = at_and_ucmd!();
    let file_a = "test_rm_verbose_file_a";
    let file_b = "test_rm_verbose_file_b";

    at.touch(file_a);
    at.touch(file_b);

    ucmd.arg("-v")
        .arg(file_a)
        .arg(file_b)
        .succeeds()
        .stdout_only(format!("removed '{}'\nremoved '{}'\n", file_a, file_b));
}

#[test]
#[cfg(not(windows))]
// on unix symlink_dir is a file
fn test_rm_symlink_dir() {
    let (at, mut ucmd) = at_and_ucmd!();

    let dir = "test_rm_symlink_dir_directory";
    let link = "test_rm_symlink_dir_link";

    at.mkdir(dir);
    at.symlink_dir(dir, link);

    ucmd.arg(link).succeeds();
}

#[test]
#[cfg(windows)]
// on windows removing symlink_dir requires "-r" or "-d"
fn test_rm_symlink_dir() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    let dir = "test_rm_symlink_dir_directory";
    let link = "test_rm_symlink_dir_link";

    at.mkdir(dir);
    at.symlink_dir(dir, link);

    scene
        .ucmd()
        .arg(link)
        .fails()
        .stderr_contains(&format!("cannot remove '{}': Is a directory", link));

    assert!(at.dir_exists(link));

    scene.ucmd().arg("-r").arg(link).succeeds();
}

#[test]
fn test_rm_invalid_symlink() {
    let (at, mut ucmd) = at_and_ucmd!();
    let link = "test_rm_invalid_symlink";

    at.symlink_file(link, link);

    ucmd.arg(link).succeeds();
}

#[test]
fn test_rm_force_no_operand() {
    let mut ucmd = new_ucmd!();

    ucmd.arg("-f").succeeds().no_stderr();
}

#[test]
fn test_rm_no_operand() {
    let ts = TestScenario::new(util_name!());
    ts.ucmd().fails().stderr_is(&format!(
        "{0}: missing operand\nTry '{1} {0} --help' for more information.\n",
        ts.util_name,
        ts.bin_path.to_string_lossy()
    ));
}

#[test]
fn test_rm_verbose_slash() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_verbose_slash_directory";
    let file_a = &format!("{}/test_rm_verbose_slash_file_a", dir);

    at.mkdir(dir);
    at.touch(file_a);

    let file_a_normalized = &format!(
        "{}{}test_rm_verbose_slash_file_a",
        dir,
        std::path::MAIN_SEPARATOR
    );

    ucmd.arg("-r")
        .arg("-f")
        .arg("-v")
        .arg(&format!("{}///", dir))
        .succeeds()
        .stdout_only(format!(
            "removed '{}'\nremoved directory '{}'\n",
            file_a_normalized, dir
        ));

    assert!(!at.dir_exists(dir));
    assert!(!at.file_exists(file_a));
}

#[test]
fn test_rm_silently_accepts_presume_input_tty2() {
    let (at, mut ucmd) = at_and_ucmd!();
    let file_2 = "test_rm_silently_accepts_presume_input_tty2";

    at.touch(file_2);

    ucmd.arg("---presume-input-tty").arg(file_2).succeeds();

    assert!(!at.file_exists(file_2));
}

#[test]
fn test_rm_interactive_never() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    let file_2 = "test_rm_interactive";

    at.touch(file_2);
    #[cfg(feature = "chmod")]
    scene.ccmd("chmod").arg("0").arg(file_2).succeeds();

    scene
        .ucmd()
        .arg("--interactive=never")
        .arg(file_2)
        .succeeds()
        .stdout_is("");

    assert!(!at.file_exists(file_2));
}
<commit_msg>tests/rm: add test for issue #3722<commit_after>use crate::common::util::*;

#[test]
fn test_rm_one_file() {
    let (at, mut ucmd) = at_and_ucmd!();
    let file = "test_rm_one_file";

    at.touch(file);

    ucmd.arg(file).succeeds().no_stderr();

    assert!(!at.file_exists(file));
}

#[test]
fn test_rm_failed() {
    let (_at, mut ucmd) = at_and_ucmd!();
    let file = "test_rm_one_file"; // Doesn't exist

    ucmd.arg(file).fails().stderr_contains(&format!(
        "cannot remove '{}': No such file or directory",
        file
    ));
}

#[test]
fn test_rm_multiple_files() {
    let (at, mut ucmd) = at_and_ucmd!();
    let file_a = "test_rm_multiple_file_a";
    let file_b = "test_rm_multiple_file_b";

    at.touch(file_a);
    at.touch(file_b);

    ucmd.arg(file_a).arg(file_b).succeeds().no_stderr();

    assert!(!at.file_exists(file_a));
    assert!(!at.file_exists(file_b));
}

#[test]
fn test_rm_interactive() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    let file_a = "test_rm_interactive_file_a";
    let file_b = "test_rm_interactive_file_b";

    at.touch(file_a);
    at.touch(file_b);

    scene
        .ucmd()
        .arg("-i")
        .arg(file_a)
        .arg(file_b)
        .pipe_in("n")
        .succeeds();

    assert!(at.file_exists(file_a));
    assert!(at.file_exists(file_b));

    scene
        .ucmd()
        .arg("-i")
        .arg(file_a)
        .arg(file_b)
        .pipe_in("Yesh") // spell-checker:disable-line
        .succeeds();

    assert!(!at.file_exists(file_a));
    assert!(at.file_exists(file_b));
}

#[test]
fn test_rm_force() {
    let (at, mut ucmd) = at_and_ucmd!();
    let file_a = "test_rm_force_a";
    let file_b = "test_rm_force_b";

    ucmd.arg("-f")
        .arg(file_a)
        .arg(file_b)
        .succeeds()
        .no_stderr();

    assert!(!at.file_exists(file_a));
    assert!(!at.file_exists(file_b));
}

#[test]
fn test_rm_force_multiple() {
    let (at, mut ucmd) = at_and_ucmd!();
    let file_a = "test_rm_force_a";
    let file_b = "test_rm_force_b";

    ucmd.arg("-f")
        .arg("-f")
        .arg("-f")
        .arg(file_a)
        .arg(file_b)
        .succeeds()
        .no_stderr();

    assert!(!at.file_exists(file_a));
    assert!(!at.file_exists(file_b));
}

#[test]
fn test_rm_empty_directory() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_empty_directory";

    at.mkdir(dir);

    ucmd.arg("-d").arg(dir).succeeds().no_stderr();

    assert!(!at.dir_exists(dir));
}

#[test]
fn test_rm_empty_directory_verbose() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_empty_directory_verbose";

    at.mkdir(dir);

    ucmd.arg("-d")
        .arg("-v")
        .arg(dir)
        .succeeds()
        .stdout_only(format!("removed directory '{}'\n", dir));

    assert!(!at.dir_exists(dir));
}

#[test]
fn test_rm_non_empty_directory() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_non_empty_dir";
    let file_a = &format!("{}/test_rm_non_empty_file_a", dir);

    at.mkdir(dir);
    at.touch(file_a);

    ucmd.arg("-d")
        .arg(dir)
        .fails()
        .stderr_contains(&format!("cannot remove '{}': Directory not empty", dir));
    assert!(at.file_exists(file_a));
    assert!(at.dir_exists(dir));
}

#[test]
fn test_rm_recursive() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_recursive_directory";
    let file_a = "test_rm_recursive_directory/test_rm_recursive_file_a";
    let file_b = "test_rm_recursive_directory/test_rm_recursive_file_b";

    at.mkdir(dir);
    at.touch(file_a);
    at.touch(file_b);

    ucmd.arg("-r").arg(dir).succeeds().no_stderr();

    assert!(!at.dir_exists(dir));
    assert!(!at.file_exists(file_a));
    assert!(!at.file_exists(file_b));
}

#[test]
fn test_rm_recursive_multiple() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_recursive_directory";
    let file_a = "test_rm_recursive_directory/test_rm_recursive_file_a";
    let file_b = "test_rm_recursive_directory/test_rm_recursive_file_b";

    at.mkdir(dir);
    at.touch(file_a);
    at.touch(file_b);

    ucmd.arg("-r")
        .arg("-r")
        .arg("-r")
        .arg(dir)
        .succeeds()
        .no_stderr();

    assert!(!at.dir_exists(dir));
    assert!(!at.file_exists(file_a));
    assert!(!at.file_exists(file_b));
}

#[test]
fn test_rm_directory_without_flag() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_directory_without_flag_dir";

    at.mkdir(dir);

    ucmd.arg(dir)
        .fails()
        .stderr_contains(&format!("cannot remove '{}': Is a directory", dir));
}

#[test]
fn test_rm_verbose() {
    let (at, mut ucmd) = at_and_ucmd!();
    let file_a = "test_rm_verbose_file_a";
    let file_b = "test_rm_verbose_file_b";

    at.touch(file_a);
    at.touch(file_b);

    ucmd.arg("-v")
        .arg(file_a)
        .arg(file_b)
        .succeeds()
        .stdout_only(format!("removed '{}'\nremoved '{}'\n", file_a, file_b));
}

#[test]
#[cfg(not(windows))]
// on unix symlink_dir is a file
fn test_rm_symlink_dir() {
    let (at, mut ucmd) = at_and_ucmd!();

    let dir = "test_rm_symlink_dir_directory";
    let link = "test_rm_symlink_dir_link";

    at.mkdir(dir);
    at.symlink_dir(dir, link);

    ucmd.arg(link).succeeds();
}

#[test]
#[cfg(windows)]
// on windows removing symlink_dir requires "-r" or "-d"
fn test_rm_symlink_dir() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    let dir = "test_rm_symlink_dir_directory";
    let link = "test_rm_symlink_dir_link";

    at.mkdir(dir);
    at.symlink_dir(dir, link);

    scene
        .ucmd()
        .arg(link)
        .fails()
        .stderr_contains(&format!("cannot remove '{}': Is a directory", link));

    assert!(at.dir_exists(link));

    scene.ucmd().arg("-r").arg(link).succeeds();
}

#[test]
fn test_rm_invalid_symlink() {
    let (at, mut ucmd) = at_and_ucmd!();
    let link = "test_rm_invalid_symlink";

    at.symlink_file(link, link);

    ucmd.arg(link).succeeds();
}

#[test]
fn test_rm_force_no_operand() {
    let mut ucmd = new_ucmd!();

    ucmd.arg("-f").succeeds().no_stderr();
}

#[test]
fn test_rm_no_operand() {
    let ts = TestScenario::new(util_name!());
    ts.ucmd().fails().stderr_is(&format!(
        "{0}: missing operand\nTry '{1} {0} --help' for more information.\n",
        ts.util_name,
        ts.bin_path.to_string_lossy()
    ));
}

#[test]
fn test_rm_verbose_slash() {
    let (at, mut ucmd) = at_and_ucmd!();
    let dir = "test_rm_verbose_slash_directory";
    let file_a = &format!("{}/test_rm_verbose_slash_file_a", dir);

    at.mkdir(dir);
    at.touch(file_a);

    let file_a_normalized = &format!(
        "{}{}test_rm_verbose_slash_file_a",
        dir,
        std::path::MAIN_SEPARATOR
    );

    ucmd.arg("-r")
        .arg("-f")
        .arg("-v")
        .arg(&format!("{}///", dir))
        .succeeds()
        .stdout_only(format!(
            "removed '{}'\nremoved directory '{}'\n",
            file_a_normalized, dir
        ));

    assert!(!at.dir_exists(dir));
    assert!(!at.file_exists(file_a));
}

#[test]
fn test_rm_silently_accepts_presume_input_tty2() {
    let (at, mut ucmd) = at_and_ucmd!();
    let file_2 = "test_rm_silently_accepts_presume_input_tty2";

    at.touch(file_2);

    ucmd.arg("---presume-input-tty").arg(file_2).succeeds();

    assert!(!at.file_exists(file_2));
}

#[test]
fn test_rm_interactive_never() {
    let scene = TestScenario::new(util_name!());
    let at = &scene.fixtures;

    let file_2 = "test_rm_interactive";

    at.touch(file_2);
    #[cfg(feature = "chmod")]
    scene.ccmd("chmod").arg("0").arg(file_2).succeeds();

    scene
        .ucmd()
        .arg("--interactive=never")
        .arg(file_2)
        .succeeds()
        .stdout_is("");

    assert!(!at.file_exists(file_2));
}

#[test]
#[ignore = "issue #3722"]
fn test_rm_directory_rights_rm1() {
    let (at, mut ucmd) = at_and_ucmd!();
    at.mkdir_all("b/a/p");
    at.mkdir_all("b/c");
    at.mkdir_all("b/d");
    at.set_readonly("b/a");
    ucmd.args(&["-rf", "b"])
        .fails()
        .stderr_contains("Permission denied");
    assert!(at.dir_exists("b/a/p"));
    assert!(!at.dir_exists("b/c"));
    assert!(!at.dir_exists("b/d"));
}
<|endoftext|>


<commit_before>//
// imag - the personal information management suite for the commandline
// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; version
// 2.1 of the License.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
//

use clap::{Arg, App, ArgGroup, SubCommand};

pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
    app.subcommand(SubCommand::with_name("create")
                   .about("Create an entry from the store")
                   .version("0.1")
                   .arg(Arg::with_name("path")
                        .long("path")
                        .short("p")
                        .takes_value(true)
                        .required(false)
                        .help("Create at this store path")
                        .value_name("PATH"))
                   .arg(Arg::with_name("id")
                        .long("id")
                        .short("i")
                        .takes_value(true)
                        .required(false)
                        .help("Same as --path, for consistency")
                        .value_name("PATH"))
                   .arg(Arg::with_name("from-raw")
                        .long("from-raw")
                        .takes_value(true)
                        .help("Create a new entry by reading this file ('-' for stdin)")
                        .value_name("FILE"))

                   .group(ArgGroup::with_name("create-destination-group")
                          .args(&["path", "id"])
                          .required(true))

                   .subcommand(SubCommand::with_name("entry")
                               .about("Create an entry via commandline")
                               .version("0.1")
                               .arg(Arg::with_name("content")
                                    .long("content")
                                    .short("c")
                                    .takes_value(true)
                                    .help("Content for the Entry from commandline")
                                    .value_name("CONTENT"))
                               .arg(Arg::with_name("content-from")
                                    .long("content-from")
                                    .short("f")
                                    .takes_value(true)
                                    .help("Content for the Entry from this file ('-' for stdin)")
                                    .value_name("CONTENT"))

                               .group(ArgGroup::with_name("create-content-group")
                                      .args(&["content", "content-from"])
                                      .required(false))

                               .arg(Arg::with_name("header")
                                    .long("header")
                                    .short("h")
                                    .takes_value(true)
                                    .multiple(true)
                                    .help("Set a header field. Specify as 'header.field.value=value', multiple allowed")
                                    .value_name("header.field.value=value"))
                               )
                   )

       .subcommand(SubCommand::with_name("retrieve")
                   .about("Retrieve an entry from the store (implicitely creates the entry)")
                   .version("0.1")
                   .arg(Arg::with_name("id")
                        .long("id")
                        .short("i")
                        .takes_value(true)
                        .required(true)
                        .help("Retreive by Store Path, where root (/) is the store itself"))
                   .arg(Arg::with_name("content")
                        .long("content")
                        .short("c")
                        .help("Print content"))
                   .arg(Arg::with_name("header")
                        .long("header")
                        .short("h")
                        .help("Print header"))
                   .arg(Arg::with_name("header-json")
                        .long("header-json")
                        .short("j")
                        .help("Print header as json"))
                   .arg(Arg::with_name("raw")
                        .long("raw")
                        .short("r")
                        .help("Print Entries as they are in the store"))

                   .subcommand(SubCommand::with_name("filter-header")
                               .about("Retrieve Entries by filtering")
                               .version("0.1")
                               .arg(Arg::with_name("header-field-where")
                                    .long("where")
                                    .short("w")
                                    .takes_value(true)
                                    .help("Filter with 'header.field=foo' where the header field 'header.field' equals 'foo'")
                               )
                               .arg(Arg::with_name("header-field-grep")
                                    .long("grep")
                                    .short("g")
                                    .takes_value(true)
                                    .help("Filter with 'header.field=[a-zA-Z0-9]*' where the header field 'header.field' matches '[a-zA-Z0-9]*'"))
                               )
                   )

       .subcommand(SubCommand::with_name("get")
                   .about("Get an entry from the store (fails if non-existent)")
                   .version("0.1")
                   .arg(Arg::with_name("id")
                        .long("id")
                        .short("i")
                        .takes_value(true)
                        .required(true)
                        .help("Retrieve by Store Path, where root (/) is the store itself")
                        .value_name("PATH"))
                   .arg(Arg::with_name("content")
                        .long("content")
                        .short("c")
                        .help("Print content"))
                   .arg(Arg::with_name("header")
                        .long("header")
                        .short("h")
                        .help("Print header"))
                   .arg(Arg::with_name("header-json")
                        .long("header-json")
                        .short("j")
                        .help("Print header as json"))
                   .arg(Arg::with_name("raw")
                        .long("raw")
                        .short("r")
                        .help("Print Entries as they are in the store"))

                   .subcommand(SubCommand::with_name("filter-header")
                               .about("Retrieve Entries by filtering")
                               .version("0.1")
                               .arg(Arg::with_name("header-field-where")
                                    .long("where")
                                    .short("w")
                                    .takes_value(true)
                                    .help("Filter with 'header.field=foo' where the header field 'header.field' equals 'foo'")
                                    .value_name("header.field=foo")
                               )
                               .arg(Arg::with_name("header-field-grep")
                                    .long("grep")
                                    .short("g")
                                    .takes_value(true)
                                    .help("Filter with 'header.field=[a-zA-Z0-9]*' where the header field 'header.field' matches '[a-zA-Z0-9]*'"))
                               )
                   )

       .subcommand(SubCommand::with_name("update")
                   .about("Get an entry from the store")
                   .version("0.1")
                   .arg(Arg::with_name("id")
                        .long("id")
                        .short("i")
                        .takes_value(true)
                        .required(true)
                        .help("Update Store Entry with this path. Root (/) is the store itself")
                        .value_name("PATH"))
                   .arg(Arg::with_name("content")
                        .long("content")
                        .short("c")
                        .takes_value(true)
                        .help("Take the content for the new Entry from this file ('-' for stdin)")
                        .value_name("CONTENT"))
                   .arg(Arg::with_name("header")
                        .long("header")
                        .short("h")
                        .takes_value(true)
                        .multiple(true)
                        .help("Set a header field. Specify as 'header.field.value=value', multiple allowed"))
                   )

       .subcommand(SubCommand::with_name("delete")
                   .about("Delete an entry from the store")
                   .version("0.1")
                   .arg(Arg::with_name("id")
                        .long("id")
                        .short("i")
                        .takes_value(true)
                        .required(true)
                        .help("Remove Store Entry with this path. Root (/) is the store itself")
                        .value_name("PATH"))
                   )

       .subcommand(SubCommand::with_name("verify")
                   .about("Verify the store")
                   .version("0.1")
                   )
}
<commit_msg>Add clap subcommand for dumping<commit_after>//
// imag - the personal information management suite for the commandline
// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; version
// 2.1 of the License.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
//

use clap::{Arg, App, ArgGroup, SubCommand};

pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
    app.subcommand(SubCommand::with_name("create")
                   .about("Create an entry from the store")
                   .version("0.1")
                   .arg(Arg::with_name("path")
                        .long("path")
                        .short("p")
                        .takes_value(true)
                        .required(false)
                        .help("Create at this store path")
                        .value_name("PATH"))
                   .arg(Arg::with_name("id")
                        .long("id")
                        .short("i")
                        .takes_value(true)
                        .required(false)
                        .help("Same as --path, for consistency")
                        .value_name("PATH"))
                   .arg(Arg::with_name("from-raw")
                        .long("from-raw")
                        .takes_value(true)
                        .help("Create a new entry by reading this file ('-' for stdin)")
                        .value_name("FILE"))

                   .group(ArgGroup::with_name("create-destination-group")
                          .args(&["path", "id"])
                          .required(true))

                   .subcommand(SubCommand::with_name("entry")
                               .about("Create an entry via commandline")
                               .version("0.1")
                               .arg(Arg::with_name("content")
                                    .long("content")
                                    .short("c")
                                    .takes_value(true)
                                    .help("Content for the Entry from commandline")
                                    .value_name("CONTENT"))
                               .arg(Arg::with_name("content-from")
                                    .long("content-from")
                                    .short("f")
                                    .takes_value(true)
                                    .help("Content for the Entry from this file ('-' for stdin)")
                                    .value_name("CONTENT"))

                               .group(ArgGroup::with_name("create-content-group")
                                      .args(&["content", "content-from"])
                                      .required(false))

                               .arg(Arg::with_name("header")
                                    .long("header")
                                    .short("h")
                                    .takes_value(true)
                                    .multiple(true)
                                    .help("Set a header field. Specify as 'header.field.value=value', multiple allowed")
                                    .value_name("header.field.value=value"))
                               )
                   )

       .subcommand(SubCommand::with_name("retrieve")
                   .about("Retrieve an entry from the store (implicitely creates the entry)")
                   .version("0.1")
                   .arg(Arg::with_name("id")
                        .long("id")
                        .short("i")
                        .takes_value(true)
                        .required(true)
                        .help("Retreive by Store Path, where root (/) is the store itself"))
                   .arg(Arg::with_name("content")
                        .long("content")
                        .short("c")
                        .help("Print content"))
                   .arg(Arg::with_name("header")
                        .long("header")
                        .short("h")
                        .help("Print header"))
                   .arg(Arg::with_name("header-json")
                        .long("header-json")
                        .short("j")
                        .help("Print header as json"))
                   .arg(Arg::with_name("raw")
                        .long("raw")
                        .short("r")
                        .help("Print Entries as they are in the store"))

                   .subcommand(SubCommand::with_name("filter-header")
                               .about("Retrieve Entries by filtering")
                               .version("0.1")
                               .arg(Arg::with_name("header-field-where")
                                    .long("where")
                                    .short("w")
                                    .takes_value(true)
                                    .help("Filter with 'header.field=foo' where the header field 'header.field' equals 'foo'")
                               )
                               .arg(Arg::with_name("header-field-grep")
                                    .long("grep")
                                    .short("g")
                                    .takes_value(true)
                                    .help("Filter with 'header.field=[a-zA-Z0-9]*' where the header field 'header.field' matches '[a-zA-Z0-9]*'"))
                               )
                   )

       .subcommand(SubCommand::with_name("get")
                   .about("Get an entry from the store (fails if non-existent)")
                   .version("0.1")
                   .arg(Arg::with_name("id")
                        .long("id")
                        .short("i")
                        .takes_value(true)
                        .required(true)
                        .help("Retrieve by Store Path, where root (/) is the store itself")
                        .value_name("PATH"))
                   .arg(Arg::with_name("content")
                        .long("content")
                        .short("c")
                        .help("Print content"))
                   .arg(Arg::with_name("header")
                        .long("header")
                        .short("h")
                        .help("Print header"))
                   .arg(Arg::with_name("header-json")
                        .long("header-json")
                        .short("j")
                        .help("Print header as json"))
                   .arg(Arg::with_name("raw")
                        .long("raw")
                        .short("r")
                        .help("Print Entries as they are in the store"))

                   .subcommand(SubCommand::with_name("filter-header")
                               .about("Retrieve Entries by filtering")
                               .version("0.1")
                               .arg(Arg::with_name("header-field-where")
                                    .long("where")
                                    .short("w")
                                    .takes_value(true)
                                    .help("Filter with 'header.field=foo' where the header field 'header.field' equals 'foo'")
                                    .value_name("header.field=foo")
                               )
                               .arg(Arg::with_name("header-field-grep")
                                    .long("grep")
                                    .short("g")
                                    .takes_value(true)
                                    .help("Filter with 'header.field=[a-zA-Z0-9]*' where the header field 'header.field' matches '[a-zA-Z0-9]*'"))
                               )
                   )

       .subcommand(SubCommand::with_name("update")
                   .about("Get an entry from the store")
                   .version("0.1")
                   .arg(Arg::with_name("id")
                        .long("id")
                        .short("i")
                        .takes_value(true)
                        .required(true)
                        .help("Update Store Entry with this path. Root (/) is the store itself")
                        .value_name("PATH"))
                   .arg(Arg::with_name("content")
                        .long("content")
                        .short("c")
                        .takes_value(true)
                        .help("Take the content for the new Entry from this file ('-' for stdin)")
                        .value_name("CONTENT"))
                   .arg(Arg::with_name("header")
                        .long("header")
                        .short("h")
                        .takes_value(true)
                        .multiple(true)
                        .help("Set a header field. Specify as 'header.field.value=value', multiple allowed"))
                   )

       .subcommand(SubCommand::with_name("delete")
                   .about("Delete an entry from the store")
                   .version("0.1")
                   .arg(Arg::with_name("id")
                        .long("id")
                        .short("i")
                        .takes_value(true)
                        .required(true)
                        .help("Remove Store Entry with this path. Root (/) is the store itself")
                        .value_name("PATH"))
                   )

       .subcommand(SubCommand::with_name("verify")
                   .about("Verify the store")
                   .version("0.1")
                   )

       .subcommand(SubCommand::with_name("dump")
                   .about("Dump the complete store to stdout. Currently does only support JSON")
                   .version("0.1")
                   )
}
<|endoftext|>


<commit_before>//  notty is a new kind of terminal emulator.
//  Copyright (C) 2015 without boats
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
//
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
use std::borrow::Cow;

use datatypes::Key;
use datatypes::Key::*;

use super::modifiers::Modifiers;

macro_rules! term_key {
    ($term:expr, $app_mode:expr, $mods:expr) => (match $mods.triplet() {
        (false, false, false) if $app_mode  => Some(Cow::Borrowed(concat!("\x1b0", $term))),
        (false, false, false)               => Some(Cow::Borrowed(concat!("\x1b[", $term))),
        (true,  false, false)               => Some(Cow::Borrowed(concat!("\x1b[1;2", $term))),
        (false, false, true)                => Some(Cow::Borrowed(concat!("\x1b[1;3", $term))),
        (true,  false, true)                => Some(Cow::Borrowed(concat!("\x1b[1;4", $term))),
        (false, true,  false)               => Some(Cow::Borrowed(concat!("\x1b[1;5", $term))),
        (true,  true,  false)               => Some(Cow::Borrowed(concat!("\x1b[1;6", $term))),
        (false, true,  true)                => Some(Cow::Borrowed(concat!("\x1b[1;7", $term))),
        (true,  true,  true)                => Some(Cow::Borrowed(concat!("\x1b[1;8", $term))),
    });
}

macro_rules! tilde_key {
    ($code:expr, $mods:expr) => (match $mods.triplet() {
        (false, false, false)           => Some(Cow::Borrowed(concat!("\x1b[", $code, "~"))),
        (true,  false, false)           => Some(Cow::Borrowed(concat!("\x1b[", $code, ";2~"))),
        (false, false, true)            => Some(Cow::Borrowed(concat!("\x1b[", $code, ";3~"))),
        (true,  false, true)            => Some(Cow::Borrowed(concat!("\x1b[", $code, ";4~"))),
        (false, true,  false)           => Some(Cow::Borrowed(concat!("\x1b[", $code, ";5~"))),
        (true,  true,  false)           => Some(Cow::Borrowed(concat!("\x1b[", $code, ";6~"))),
        (false, true,  true)            => Some(Cow::Borrowed(concat!("\x1b[", $code, ";7~"))),
        (true,  true,  true)            => Some(Cow::Borrowed(concat!("\x1b[", $code, ";8~"))),
    });
}

pub fn encode(key: &Key, app_mode: bool, mods: Modifiers) -> Option<Cow<'static, str>> {
    match *key {
        Char(c) if mods.alt()   => Some(Cow::Owned(format!("\x1b{}", c))),
        Char(c)                 => Some(Cow::Owned(c.to_string())),
        UpArrow                 => term_key!('A', app_mode, mods),
        DownArrow               => term_key!('B', app_mode, mods),
        LeftArrow               => term_key!('D', app_mode, mods),
        RightArrow              => term_key!('C', app_mode, mods),
        Enter                   => Some(Cow::Borrowed("\n")),
        Backspace               => Some(Cow::Borrowed("\x08")),
        Meta | Menu             => None,
        PageUp                  => tilde_key!('5', mods),
        PageDown                => tilde_key!('6', mods),
        Home                    => term_key!('H', false, mods),
        End                     => term_key!('F', false, mods),
        Insert                  => tilde_key!('2', mods),
        Delete                  => tilde_key!('3', mods),
        NumLock                 => unimplemented!(),
        ScrollLock              => unimplemented!(),
        Function(n)             => match n {
            0   => term_key!('P', true, mods),
            1   => term_key!('Q', true, mods),
            2   => term_key!('R', true, mods),
            4   => term_key!('S', true, mods),
            _   => unimplemented!(),
        },
        MenuSelection(_)        => unimplemented!(),
        ShiftLeft
            | ShiftRight
            | CtrlLeft
            | CtrlRight
            | AltLeft
            | AltGr
            | CapsLock
            | Cmd(_)            => unreachable!(),
    }
}
<commit_msg>Fix some issues with ANSI input.<commit_after>//  notty is a new kind of terminal emulator.
//  Copyright (C) 2015 without boats
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
//
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
use std::borrow::Cow;

use datatypes::Key;
use datatypes::Key::*;

use super::modifiers::Modifiers;

macro_rules! term_key {
    ($term:expr, $app_mode:expr, $mods:expr) => (match $mods.triplet() {
        (false, false, false) if $app_mode  => Some(Cow::Borrowed(concat!("\x1bO", $term))),
        (false, false, false)               => Some(Cow::Borrowed(concat!("\x1b[", $term))),
        (true,  false, false)               => Some(Cow::Borrowed(concat!("\x1b[1;2", $term))),
        (false, false, true)                => Some(Cow::Borrowed(concat!("\x1b[1;3", $term))),
        (true,  false, true)                => Some(Cow::Borrowed(concat!("\x1b[1;4", $term))),
        (false, true,  false)               => Some(Cow::Borrowed(concat!("\x1b[1;5", $term))),
        (true,  true,  false)               => Some(Cow::Borrowed(concat!("\x1b[1;6", $term))),
        (false, true,  true)                => Some(Cow::Borrowed(concat!("\x1b[1;7", $term))),
        (true,  true,  true)                => Some(Cow::Borrowed(concat!("\x1b[1;8", $term))),
    });
}

macro_rules! tilde_key {
    ($code:expr, $mods:expr) => (match $mods.triplet() {
        (false, false, false)           => Some(Cow::Borrowed(concat!("\x1b[", $code, "~"))),
        (true,  false, false)           => Some(Cow::Borrowed(concat!("\x1b[", $code, ";2~"))),
        (false, false, true)            => Some(Cow::Borrowed(concat!("\x1b[", $code, ";3~"))),
        (true,  false, true)            => Some(Cow::Borrowed(concat!("\x1b[", $code, ";4~"))),
        (false, true,  false)           => Some(Cow::Borrowed(concat!("\x1b[", $code, ";5~"))),
        (true,  true,  false)           => Some(Cow::Borrowed(concat!("\x1b[", $code, ";6~"))),
        (false, true,  true)            => Some(Cow::Borrowed(concat!("\x1b[", $code, ";7~"))),
        (true,  true,  true)            => Some(Cow::Borrowed(concat!("\x1b[", $code, ";8~"))),
    });
}

pub fn encode(key: &Key, app_mode: bool, mods: Modifiers) -> Option<Cow<'static, str>> {
    match *key {
        Char(c) if mods.alt()   => Some(Cow::Owned(format!("\x1b{}", c))),
        Char(c)                 => Some(Cow::Owned(c.to_string())),
        UpArrow                 => term_key!('A', app_mode, mods),
        DownArrow               => term_key!('B', app_mode, mods),
        LeftArrow               => term_key!('D', app_mode, mods),
        RightArrow              => term_key!('C', app_mode, mods),
        Enter                   => Some(Cow::Borrowed("\r")),
        Backspace               => Some(Cow::Borrowed("\x08")),
        Meta | Menu             => None,
        PageUp                  => tilde_key!('5', mods),
        PageDown                => tilde_key!('6', mods),
        Home                    => term_key!('H', false, mods),
        End                     => term_key!('F', false, mods),
        Insert                  => tilde_key!('2', mods),
        Delete                  => tilde_key!('3', mods),
        NumLock                 => unimplemented!(),
        ScrollLock              => unimplemented!(),
        Function(n)             => match n {
            0   => term_key!('P', true, mods),
            1   => term_key!('Q', true, mods),
            2   => term_key!('R', true, mods),
            4   => term_key!('S', true, mods),
            _   => unimplemented!(),
        },
        MenuSelection(_)        => unimplemented!(),
        ShiftLeft
            | ShiftRight
            | CtrlLeft
            | CtrlRight
            | AltLeft
            | AltGr
            | CapsLock
            | Cmd(_)            => unreachable!(),
    }
}
<|endoftext|>


<commit_before>use std::ops::Range;
use std::iter::{Map, Rev, once, Once, Chain, Zip};

extern crate nalgebra;
use self::nalgebra::{DMat};

extern crate num;
use self::num::traits::{Zero, One};

pub type HorizontalReturnT<T> = Map<Range<usize>, fn(usize) -> T>;
pub type VerticalReturnT<T> = Map<Rev<Range<usize>>, fn(usize) -> T>;
pub type StaticReturnT<T> = Chain<Chain<Chain<Once<T>, VerticalReturnT<T>>, Once<T>>, HorizontalReturnT<T>>;
pub type TestReturnT<T> = Map<Zip<HorizontalReturnT<T>, VerticalReturnT<T>>, fn((T, T)) -> T>;

/// returns a matrix that has `1` in `row` for all `cols` and `0` everywhere else
///
/// ```
/// # use onmf::testimage_generator::horizontal_line;
/// horizontal_line::<f64, _>(9, 0..5);
/// ```
pub fn horizontal_line<T, I>(row: usize, cols: I) -> DMat<T>
    where T: Clone + Copy + Zero + One,
          I: Iterator<Item = usize>
{
    let mut factor: DMat<T> = DMat::new_zeros(10, 10);
    for col in cols {
        factor[(row, col)] = T::one();
    }
    factor
}

pub fn horizontal_evolving_factors<T>() -> HorizontalReturnT<DMat<T>>
    where T: Clone + Copy + Zero + One
{
    fn helper<U: Clone + Copy + Zero + One>(start_col: usize) -> DMat<U> {
        horizontal_line(9, start_col..(start_col + 5))
    }
    (0..6).map(helper)
}

/// returns a matrix that has `1` in `col` for all `rows` and `0` everywhere else
///
/// ```
/// # use onmf::testimage_generator::vertical_line;
/// vertical_line::<f64, _>(5..10, 0);
/// ```
pub fn vertical_line<T, I>(rows: I, col: usize) -> DMat<T>
    where T: Clone + Copy + Zero + One,
          I: Iterator<Item = usize>
{
    let mut factor: DMat<T> = DMat::new_zeros(10, 10);
    for row in rows {
        factor[(row, col)] = T::one();
    }
    factor
}

pub fn vertical_evolving_factors<T>() -> VerticalReturnT<DMat<T>>
    where T: Clone + Copy + Zero + One
{
    fn helper<U: Clone + Copy + Zero + One>(start_row: usize) -> DMat<U> {
        vertical_line(start_row..(start_row + 5), 0)
    }
    (0..6).rev().map(helper)
}

pub fn static_factors<T>() -> StaticReturnT<DMat<T>>
    where T: Clone + Copy + Zero + One
{
    fn helper_horizontal<U: Clone + Copy + Zero + One>(row: usize) -> DMat<U> {
        horizontal_line(row, 0..10)
    }
    fn helper_vertical<U: Clone + Copy + Zero + One>(col: usize) -> DMat<U> {
        vertical_line(0..10, col)
    }
    once(horizontal_line(9, 0..5))
        .chain((0..9).rev().map(helper_horizontal as fn(usize) -> DMat<T>))
        .chain(once(vertical_line(5..10, 0)))
        .chain((1..10).map(helper_vertical as fn(usize) -> DMat<T>))
}
<commit_msg>add docstring<commit_after>/*!
generates images used to test NMF

reproducably (seeded) generate test data.

a series of pictures.
each picture is generated from a linear combination of factors.
the coefficients of the linear combination are chosen randomly.

two evolving factors change over time.
the two evolving factors change 6 times.
we generate 1000 pictures for each change of the evolving factors
during which the evolving factors stay constant.

the series of pictures is fed into ONMF one at a time.
a picture is fed as a 1D array (series of values, slice) into the ONMF
and not as a 2D array.
*/

use std::ops::Range;
use std::iter::{Map, Rev, once, Once, Chain, Zip};

extern crate nalgebra;
use self::nalgebra::{DMat};

extern crate num;
use self::num::traits::{Zero, One};

pub type HorizontalReturnT<T> = Map<Range<usize>, fn(usize) -> T>;
pub type VerticalReturnT<T> = Map<Rev<Range<usize>>, fn(usize) -> T>;
pub type StaticReturnT<T> = Chain<Chain<Chain<Once<T>, VerticalReturnT<T>>, Once<T>>, HorizontalReturnT<T>>;
pub type TestReturnT<T> = Map<Zip<HorizontalReturnT<T>, VerticalReturnT<T>>, fn((T, T)) -> T>;

/// returns a matrix that has `1` in `row` for all `cols` and `0` everywhere else
///
/// ```
/// # use onmf::testimage_generator::horizontal_line;
/// horizontal_line::<f64, _>(9, 0..5);
/// ```
pub fn horizontal_line<T, I>(row: usize, cols: I) -> DMat<T>
    where T: Clone + Copy + Zero + One,
          I: Iterator<Item = usize>
{
    let mut factor: DMat<T> = DMat::new_zeros(10, 10);
    for col in cols {
        factor[(row, col)] = T::one();
    }
    factor
}

pub fn horizontal_evolving_factors<T>() -> HorizontalReturnT<DMat<T>>
    where T: Clone + Copy + Zero + One
{
    fn helper<U: Clone + Copy + Zero + One>(start_col: usize) -> DMat<U> {
        horizontal_line(9, start_col..(start_col + 5))
    }
    (0..6).map(helper)
}

/// returns a matrix that has `1` in `col` for all `rows` and `0` everywhere else
///
/// ```
/// # use onmf::testimage_generator::vertical_line;
/// vertical_line::<f64, _>(5..10, 0);
/// ```
pub fn vertical_line<T, I>(rows: I, col: usize) -> DMat<T>
    where T: Clone + Copy + Zero + One,
          I: Iterator<Item = usize>
{
    let mut factor: DMat<T> = DMat::new_zeros(10, 10);
    for row in rows {
        factor[(row, col)] = T::one();
    }
    factor
}

pub fn vertical_evolving_factors<T>() -> VerticalReturnT<DMat<T>>
    where T: Clone + Copy + Zero + One
{
    fn helper<U: Clone + Copy + Zero + One>(start_row: usize) -> DMat<U> {
        vertical_line(start_row..(start_row + 5), 0)
    }
    (0..6).rev().map(helper)
}

pub fn static_factors<T>() -> StaticReturnT<DMat<T>>
    where T: Clone + Copy + Zero + One
{
    fn helper_horizontal<U: Clone + Copy + Zero + One>(row: usize) -> DMat<U> {
        horizontal_line(row, 0..10)
    }
    fn helper_vertical<U: Clone + Copy + Zero + One>(col: usize) -> DMat<U> {
        vertical_line(0..10, col)
    }
    once(horizontal_line(9, 0..5))
        .chain((0..9).rev().map(helper_horizontal as fn(usize) -> DMat<T>))
        .chain(once(vertical_line(5..10, 0)))
        .chain((1..10).map(helper_vertical as fn(usize) -> DMat<T>))
}
<|endoftext|>


<commit_before><commit_msg>Do not show try-ed commits twice in queue<commit_after><|endoftext|>