airmux 0.1.0

Just another tmux session manager
Documentation
extern crate airmux;
use airmux::config::Config;
use airmux::*;

use clap::{
    crate_description, crate_name, crate_version, App, AppSettings, Arg, ArgMatches, SubCommand,
};
use main_error::MainError;

use std::error::Error;

pub const APP_NAME: &str = crate_name!();
pub const APP_AUTHOR: &str = "airmux";
pub const APP_VERSION: &str = crate_version!();
pub const APP_DESCRIPTION: &str = crate_description!();

fn main() -> Result<(), MainError> {
    let app = App::new("airmux")
        .name(APP_NAME)
        .version(APP_VERSION)
        .about(APP_DESCRIPTION)
        .settings(&[
            AppSettings::SubcommandRequired,
            AppSettings::VersionlessSubcommands,
            AppSettings::InferSubcommands,
        ])
        .arg(
            Arg::with_name("config_dir")
                .global(true)
                .help("configuration directory to use")
                .short("c")
                .long("config-dir")
                .value_name("DIR")
                .env("AIRMUX_CONFIG"),
        )
        .subcommands(vec![
            SubCommand::with_name("list")
                .about("List all configured projects")
                .alias("ls"),
            SubCommand::with_name("start")
                .about("Start a project as a tmux session")
                .args(&[
                    Arg::with_name("project_name")
                        .help("name of the project")
                        .value_name("PROJECT_NAME")
                        .index(1),
                    Arg::with_name("attach")
                        .help("force attach the session")
                        .short("a")
                        .long("attach")
                        .conflicts_with("no_attach"),
                    Arg::with_name("no_attach")
                        .help("don't automatically attach the session")
                        .short("d")
                        .long("no-attach"),
                    Arg::with_name("verbose")
                        .help("print a message if the session was created or updated")
                        .short("V")
                        .long("verbose"),
                    Arg::with_name("args")
                        .help("arguments to be passed as variables to the yaml file")
                        .value_name("ARGUMENT")
                        .multiple(true),
                    Arg::with_name("tmux_command")
                        .global(true)
                        .help("tmux command to use")
                        .short("t")
                        .long("command")
                        .value_name("COMMAND")
                        .env("AIRMUX_COMMAND"),
                ]),
            SubCommand::with_name("debug")
                .about("Print tmux source without actually running tmux")
                .args(&[
                    Arg::with_name("project_name")
                        .help("name of the project")
                        .value_name("PROJECT_NAME")
                        .index(1),
                    Arg::with_name("attach")
                        .help("force attach the session (ignored)")
                        .short("a")
                        .long("attach")
                        .conflicts_with("no_attach"),
                    Arg::with_name("no_attach")
                        .help("don't automatically attach the session (ignored)")
                        .short("d")
                        .long("no-attach"),
                    Arg::with_name("verbose")
                        .help("print a message if the session was created or updated")
                        .short("V")
                        .long("verbose"),
                    Arg::with_name("args")
                        .help("arguments to be passed as variables to the yaml file")
                        .value_name("ARGUMENT")
                        .multiple(true),
                    Arg::with_name("tmux_command")
                        .global(true)
                        .help("tmux command to use")
                        .short("t")
                        .long("command")
                        .value_name("COMMAND")
                        .env("AIRMUX_COMMAND"),
                ]),
            SubCommand::with_name("kill")
                .about("Kill tmux session that matches the project")
                .args(&[
                    Arg::with_name("project_name")
                        .help("name of the project")
                        .value_name("PROJECT_NAME")
                        .index(1),
                    Arg::with_name("args")
                        .help("arguments to be passed as variables to the yaml file")
                        .value_name("ARGUMENT")
                        .multiple(true),
                    Arg::with_name("tmux_command")
                        .global(true)
                        .help("tmux command to use")
                        .short("t")
                        .long("command")
                        .value_name("COMMAND")
                        .env("AIRMUX_COMMAND"),
                ]),
            SubCommand::with_name("edit")
                .about("Create or edit a project")
                .alias("new")
                .args(&[
                    Arg::with_name("project_name")
                        .help("name of the project")
                        .value_name("PROJECT_NAME")
                        .index(1),
                    Arg::with_name("extension")
                        .help("the extension to use for the project file (yml|yaml|json)")
                        .short("e")
                        .long("ext")
                        .value_name("FILE_EXT")
                        .possible_values(&["yml", "yaml", "json"])
                        .case_insensitive(true),
                    Arg::with_name("editor")
                        .help("the editor to use")
                        .short("E")
                        .long("editor")
                        .required(true)
                        .value_name("EDITOR")
                        .env("EDITOR"),
                    Arg::with_name("no_check")
                        .help("do not check the project file")
                        .short("C")
                        .long("no-check"),
                    Arg::with_name("args")
                        .help("arguments to be passed as variables to the yaml file when checking")
                        .value_name("ARGUMENT")
                        .multiple(true),
                    Arg::with_name("tmux_command")
                        .global(true)
                        .help("tmux command to use")
                        .short("t")
                        .long("command")
                        .value_name("COMMAND")
                        .env("AIRMUX_COMMAND"),
                ]),
            SubCommand::with_name("remove")
                .about("Remove a project (does not affect loaded tmux sessions)")
                .aliases(&["rm", "delete"])
                .args(&[
                    Arg::with_name("project_name")
                        .help("name of the project")
                        .value_name("PROJECT_NAME")
                        .index(1),
                    Arg::with_name("no_input")
                        .help("do not prompt for confirmation")
                        .short("y")
                        .long("no-input"),
                ]),
            SubCommand::with_name("freeze")
                .about("Save current tmux session as a project file (commands not included)")
                .args(&[
                    Arg::with_name("stdout")
                        .help("print the project file to stdout instead")
                        .short("s")
                        .long("stdout")
                        .conflicts_with_all(&[
                            "project_name",
                            "editor",
                            "no_input",
                            "no_check",
                            "args",
                        ]),
                    Arg::with_name("project_name")
                        .help("name of the project")
                        .value_name("PROJECT_NAME")
                        .index(1),
                    Arg::with_name("extension")
                        .help("the extension to use for the project file (yml|yaml|json)")
                        .short("e")
                        .long("ext")
                        .value_name("FILE_EXT")
                        .possible_values(&["yml", "yaml", "json"])
                        .case_insensitive(true),
                    Arg::with_name("no_input")
                        .help("do not prompt for confirmation")
                        .short("y")
                        .long("no-input"),
                    Arg::with_name("editor")
                        .help("the editor to use")
                        .short("E")
                        .long("editor")
                        .required(true)
                        .value_name("EDITOR")
                        .env("EDITOR"),
                    Arg::with_name("no_check")
                        .help("do not check the project file")
                        .short("C")
                        .long("no-check"),
                    Arg::with_name("args")
                        .help("arguments to be passed as variables to the yaml file when checking")
                        .value_name("ARGUMENT")
                        .multiple(true),
                    Arg::with_name("tmux_command")
                        .global(true)
                        .help("tmux command to use")
                        .short("t")
                        .long("command")
                        .value_name("COMMAND")
                        .env("AIRMUX_COMMAND"),
                ]),
        ]);

    let matches = app.get_matches();
    match matches.subcommand() {
        ("start", Some(sub_matches)) => command_start(sub_matches),
        ("debug", Some(sub_matches)) => command_debug(sub_matches),
        ("kill", Some(sub_matches)) => command_kill(sub_matches),
        ("edit", Some(sub_matches)) => command_edit(sub_matches),
        ("remove", Some(sub_matches)) => command_remove(sub_matches),
        ("list", Some(sub_matches)) => command_list(sub_matches),
        ("freeze", Some(sub_matches)) => command_freeze(sub_matches),
        _ => panic!(),
    }
    .map_err(|x| x.into())
}

fn command_start(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    let config = Config::from_args(APP_NAME, APP_AUTHOR, matches).check()?;

    let project_name = matches.value_of_lossy("project_name");
    let attach = matches.is_present("attach");
    let no_attach = matches.is_present("no_attach");
    let verbose = matches.is_present("verbose");
    let args = matches.values_of_lossy("args").unwrap_or_default();
    let args: Vec<&str> = args.iter().map(AsRef::as_ref).collect();

    let force_attach = if attach {
        Some(true)
    } else if no_attach {
        Some(false)
    } else {
        None
    };

    actions::start_project(
        &config,
        project_name.as_deref(),
        force_attach,
        false,
        verbose,
        &args,
    )
}

fn command_debug(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    let config = Config::from_args(APP_NAME, APP_AUTHOR, matches).check()?;

    let project_name = matches.value_of_lossy("project_name");
    let attach = matches.is_present("attach");
    let no_attach = matches.is_present("no_attach");
    let verbose = matches.is_present("verbose");
    let args = matches.values_of_lossy("args").unwrap_or_default();
    let args: Vec<&str> = args.iter().map(AsRef::as_ref).collect();

    let force_attach = if attach {
        Some(true)
    } else if no_attach {
        Some(false)
    } else {
        None
    };

    actions::start_project(
        &config,
        project_name.as_deref(),
        force_attach,
        true,
        verbose,
        &args,
    )
}

fn command_kill(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    let config = Config::from_args(APP_NAME, APP_AUTHOR, matches).check()?;

    let project_name = matches.value_of_lossy("project_name");
    let args = matches.values_of_lossy("args").unwrap_or_default();
    let args: Vec<&str> = args.iter().map(AsRef::as_ref).collect();

    actions::kill_project(&config, project_name.as_deref(), &args)
}

fn command_edit(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    let config = Config::from_args(APP_NAME, APP_AUTHOR, matches).check()?;

    let project_name = matches.value_of_lossy("project_name");
    let extension = matches.value_of_lossy("extension");
    let editor = matches.value_of_lossy("editor").unwrap();
    let no_check = matches.is_present("no_check");
    let args = matches.values_of_lossy("args").unwrap_or_default();
    let args: Vec<&str> = args.iter().map(AsRef::as_ref).collect();

    actions::edit_project(
        &config,
        project_name.as_deref(),
        extension.as_deref(),
        &editor,
        no_check,
        &args,
    )
}

fn command_remove(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    let config = Config::from_args(APP_NAME, APP_AUTHOR, matches).check()?;

    let project_name = matches.value_of_lossy("project_name");
    let no_input = matches.is_present("no_input");

    actions::remove_project(&config, project_name.as_deref(), no_input)
}

fn command_list(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    let config = Config::from_args(APP_NAME, APP_AUTHOR, matches).check()?;

    actions::list_projects(&config)
}

fn command_freeze(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    let config = Config::from_args(APP_NAME, APP_AUTHOR, matches).check()?;

    let stdout = matches.is_present("stdout");
    let project_name = matches.value_of_lossy("project_name");
    let extension = matches.value_of_lossy("extension");
    let no_input = matches.is_present("no_input");
    let editor = matches.value_of_lossy("editor").unwrap();
    let no_check = matches.is_present("no_check");
    let args = matches.values_of_lossy("args").unwrap_or_default();
    let args: Vec<&str> = args.iter().map(AsRef::as_ref).collect();

    actions::freeze_project(
        &config,
        stdout,
        project_name.as_deref(),
        extension.as_deref(),
        &editor,
        no_input,
        no_check,
        &args,
    )
}