use crate::data::Backend;
use crate::effects::StackEffect;
use crate::output::{NoiseLevel, OutputFormat};
use clap::{Args, Parser, Subcommand, ValueEnum};
use std::str::FromStr;
use std::{error, fmt};
mod interact;
use interact::*;
pub const SIGI_VERSION: &str = std::env!("CARGO_PKG_VERSION");
const DEFAULT_STACK_NAME: &str = "sigi";
const DEFAULT_FORMAT: OutputFormat = OutputFormat::Human(NoiseLevel::Normal);
const DEFAULT_BACKEND: Backend = Backend::HomeDir;
const DEFAULT_SHORT_LIST_LIMIT: usize = 10;
const COMPLETE_TERMS: [&str; 4] = ["complete", "done", "finish", "fulfill"];
const COUNT_TERMS: [&str; 3] = ["count", "size", "length"];
const DELETE_TERMS: [&str; 5] = ["delete", "pop", "remove", "cancel", "drop"];
const DELETE_ALL_TERMS: [&str; 6] = [
"delete-all",
"purge",
"pop-all",
"remove-all",
"cancel-all",
"drop-all",
];
const HEAD_TERMS: [&str; 3] = ["head", "top", "first"];
const IS_EMPTY_TERMS: [&str; 2] = ["is-empty", "empty"];
const LIST_TERMS: [&str; 4] = ["list", "ls", "snoop", "all"];
const LIST_STACKS_TERMS: [&str; 2] = ["list-stacks", "stacks"];
const MOVE_TERMS: [&str; 1] = ["move"];
const MOVE_ALL_TERMS: [&str; 1] = ["move-all"];
const NEXT_TERMS: [&str; 4] = ["next", "later", "cycle", "bury"];
const PEEK_TERMS: [&str; 2] = ["peek", "show"];
const PICK_TERMS: [&str; 1] = ["pick"];
const PUSH_TERMS: [&str; 6] = ["push", "create", "add", "do", "start", "new"];
const ROT_TERMS: [&str; 2] = ["rot", "rotate"];
const SWAP_TERMS: [&str; 1] = ["swap"];
const TAIL_TERMS: [&str; 3] = ["tail", "bottom", "last"];
pub fn run() {
let args = Cli::parse();
let stack = args.stack.unwrap_or_else(|| DEFAULT_STACK_NAME.into());
match args.mode {
None => {
let output = args.fc.into_output_format().unwrap_or(DEFAULT_FORMAT);
let peek = StackEffect::Peek { stack };
peek.run(&DEFAULT_BACKEND, &output);
}
Some(Mode::Command(command)) => {
let (effect, effect_fc) = command.into_effect_and_fc(stack);
let output = args.fc.into_fallback_for(effect_fc);
effect.run(&DEFAULT_BACKEND, &output);
}
Some(Mode::Interactive { fc }) => {
let output = args.fc.into_fallback_for(fc);
interact(stack, output);
}
Some(Mode::ReadStdin) => interact(stack, OutputFormat::TerseText),
};
}
#[derive(Parser)]
#[command(name = "sigi", version = SIGI_VERSION, after_help = INTERACT_INSTRUCTIONS, after_long_help = INTERACT_LONG_INSTRUCTIONS)]
struct Cli {
#[command(flatten)]
fc: FormatConfig,
#[arg(short='t', long, visible_aliases = &["topic", "about", "namespace"])]
stack: Option<String>,
#[command(subcommand)]
mode: Option<Mode>,
}
#[derive(Subcommand)]
enum Mode {
#[command(visible_alias = "i")]
Interactive {
#[command(flatten)]
fc: FormatConfig,
},
#[command(name = "-")]
ReadStdin,
#[command(flatten)]
Command(Command),
}
#[derive(Subcommand)]
enum Command {
#[command(visible_aliases = &COMPLETE_TERMS[1..])]
Complete {
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &COUNT_TERMS[1..])]
Count {
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &DELETE_TERMS[1..])]
Delete {
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &DELETE_ALL_TERMS[1..])]
DeleteAll {
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &HEAD_TERMS[1..])]
Head {
n: Option<usize>,
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &IS_EMPTY_TERMS[1..])]
IsEmpty {
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &LIST_TERMS[1..])]
List {
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &LIST_STACKS_TERMS[1..])]
ListStacks {
#[command(flatten)]
fc: FormatConfig,
},
#[command(arg_required_else_help = true, visible_aliases = &MOVE_TERMS[1..])]
Move {
#[arg(name = "destination")]
dest: String,
#[command(flatten)]
fc: FormatConfig,
},
#[command(arg_required_else_help = true, visible_aliases = &MOVE_ALL_TERMS[1..])]
MoveAll {
#[arg(name = "destination")]
dest: String,
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &NEXT_TERMS[1..])]
Next {
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &PEEK_TERMS[1..])]
Peek {
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &PICK_TERMS[1..])]
Pick {
ns: Vec<usize>,
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &PUSH_TERMS[1..])]
Push {
content: Vec<String>,
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &ROT_TERMS[1..])]
Rot {
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &SWAP_TERMS[1..])]
Swap {
#[command(flatten)]
fc: FormatConfig,
},
#[command(visible_aliases = &TAIL_TERMS[1..])]
Tail {
n: Option<usize>,
#[command(flatten)]
fc: FormatConfig,
},
}
impl Command {
fn into_effect_and_fc(self, stack: String) -> (StackEffect, FormatConfig) {
use StackEffect::*;
match self {
Command::Complete { fc } => (Complete { stack }, fc),
Command::Count { fc } => (Count { stack }, fc),
Command::Delete { fc } => (Delete { stack }, fc),
Command::DeleteAll { fc } => (DeleteAll { stack }, fc),
Command::Head { n, fc } => {
let n = n.unwrap_or(DEFAULT_SHORT_LIST_LIMIT);
(Head { n, stack }, fc)
}
Command::IsEmpty { fc } => (IsEmpty { stack }, fc),
Command::List { fc } => (ListAll { stack }, fc),
Command::ListStacks { fc } => (ListStacks, fc),
Command::Move { dest, fc } => (Move { stack, dest }, fc),
Command::MoveAll { dest, fc } => (MoveAll { stack, dest }, fc),
Command::Next { fc } => (Next { stack }, fc),
Command::Peek { fc } => (Peek { stack }, fc),
Command::Pick { ns, fc } => (Pick { stack, indices: ns }, fc),
Command::Push { content, fc } => {
let content = content.join(" ");
(Push { stack, content }, fc)
}
Command::Rot { fc } => (Rot { stack }, fc),
Command::Swap { fc } => (Swap { stack }, fc),
Command::Tail { n, fc } => {
let n = n.unwrap_or(DEFAULT_SHORT_LIST_LIMIT);
(Tail { n, stack }, fc)
}
}
}
}
#[derive(Args)]
struct FormatConfig {
#[arg(short, long)]
quiet: bool,
#[arg(short, long)]
silent: bool,
#[arg(short, long, visible_alias = "noisy")]
verbose: bool,
#[arg(short, long)]
format: Option<ProgrammaticFormat>,
}
impl FormatConfig {
fn into_output_format(self) -> Option<OutputFormat> {
let FormatConfig {
verbose,
silent,
quiet,
format,
} = self;
use NoiseLevel::*;
use OutputFormat::*;
format
.map(|format| match format {
ProgrammaticFormat::Csv => Csv,
ProgrammaticFormat::Json => Json,
ProgrammaticFormat::JsonCompact => JsonCompact,
ProgrammaticFormat::Tsv => Tsv,
})
.or(if verbose {
Some(Human(Verbose))
} else if silent {
Some(Silent)
} else if quiet {
Some(Human(Quiet))
} else {
None
})
}
fn into_fallback_for(self, fc: FormatConfig) -> OutputFormat {
fc.into_output_format()
.or_else(|| self.into_output_format())
.unwrap_or(DEFAULT_FORMAT)
}
}
#[derive(ValueEnum, Clone)]
enum ProgrammaticFormat {
Csv,
Json,
JsonCompact,
Tsv,
}
impl FromStr for ProgrammaticFormat {
type Err = UnknownFormat;
fn from_str(format: &str) -> Result<Self, Self::Err> {
use ProgrammaticFormat::*;
let format = format.to_ascii_lowercase();
match format.as_str() {
"csv" => Ok(Csv),
"json" => Ok(Json),
"json-compact" => Ok(JsonCompact),
"tsv" => Ok(Tsv),
_ => Err(UnknownFormat { format }),
}
}
}
#[derive(Debug)]
struct UnknownFormat {
format: String,
}
impl error::Error for UnknownFormat {}
impl fmt::Display for UnknownFormat {
fn fmt(&self, out: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(out, "Unknown format: {}", self.format)
}
}