pub(crate) mod commands;
pub(crate) mod invocation;
pub(crate) mod pipeline;
pub(crate) mod rows;
use crate::config::{ConfigLayer, RuntimeLoadOptions};
use clap::{Args, Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
use crate::ui::UiPresentation;
pub use pipeline::{
ParsedCommandLine, is_cli_help_stage, parse_command_text_with_aliases,
parse_command_tokens_with_aliases, validate_cli_dsl_stages,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum PresentationArg {
Expressive,
Compact,
#[value(alias = "gammel-og-bitter")]
Austere,
}
impl From<PresentationArg> for UiPresentation {
fn from(value: PresentationArg) -> Self {
match value {
PresentationArg::Expressive => UiPresentation::Expressive,
PresentationArg::Compact => UiPresentation::Compact,
PresentationArg::Austere => UiPresentation::Austere,
}
}
}
#[derive(Debug, Parser)]
#[command(
name = "osp",
version = env!("CARGO_PKG_VERSION"),
about = "OSP CLI",
after_help = "Use `osp plugins commands` to list plugin-provided commands."
)]
pub struct Cli {
#[arg(short = 'u', long = "user")]
pub user: Option<String>,
#[arg(short = 'i', long = "incognito", global = true)]
pub incognito: bool,
#[arg(long = "profile", global = true)]
pub profile: Option<String>,
#[arg(long = "no-env", global = true)]
pub no_env: bool,
#[arg(long = "no-config-file", alias = "no-config", global = true)]
pub no_config_file: bool,
#[arg(long = "defaults-only", global = true)]
pub defaults_only: bool,
#[arg(long = "plugin-dir", global = true)]
pub plugin_dirs: Vec<PathBuf>,
#[arg(long = "theme", global = true)]
pub theme: Option<String>,
#[arg(long = "presentation", alias = "app-style", global = true)]
presentation: Option<PresentationArg>,
#[arg(
long = "gammel-og-bitter",
conflicts_with = "presentation",
global = true
)]
gammel_og_bitter: bool,
#[command(subcommand)]
pub command: Option<Commands>,
}
impl Cli {
pub fn runtime_load_options(&self) -> RuntimeLoadOptions {
runtime_load_options_from_flags(self.no_env, self.no_config_file, self.defaults_only)
}
}
#[derive(Debug, Subcommand)]
pub enum Commands {
Plugins(PluginsArgs),
Doctor(DoctorArgs),
Theme(ThemeArgs),
Config(ConfigArgs),
History(HistoryArgs),
#[command(hide = true)]
Intro(IntroArgs),
#[command(hide = true)]
Repl(ReplArgs),
#[command(external_subcommand)]
External(Vec<String>),
}
#[derive(Debug, Parser)]
#[command(name = "osp", no_binary_name = true)]
pub struct InlineCommandCli {
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Debug, Args)]
pub struct ReplArgs {
#[command(subcommand)]
pub command: ReplCommands,
}
#[derive(Debug, Subcommand)]
pub enum ReplCommands {
#[command(name = "debug-complete", hide = true)]
DebugComplete(DebugCompleteArgs),
#[command(name = "debug-highlight", hide = true)]
DebugHighlight(DebugHighlightArgs),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum DebugMenuArg {
Completion,
History,
}
#[derive(Debug, Args)]
pub struct DebugCompleteArgs {
#[arg(long)]
pub line: String,
#[arg(long = "menu", value_enum, default_value_t = DebugMenuArg::Completion)]
pub menu: DebugMenuArg,
#[arg(long)]
pub cursor: Option<usize>,
#[arg(long, default_value_t = 80)]
pub width: u16,
#[arg(long, default_value_t = 24)]
pub height: u16,
#[arg(long = "step")]
pub steps: Vec<String>,
#[arg(long = "menu-ansi", default_value_t = false)]
pub menu_ansi: bool,
#[arg(long = "menu-unicode", default_value_t = false)]
pub menu_unicode: bool,
}
#[derive(Debug, Args)]
pub struct DebugHighlightArgs {
#[arg(long)]
pub line: String,
}
#[derive(Debug, Args)]
pub struct PluginsArgs {
#[command(subcommand)]
pub command: PluginsCommands,
}
#[derive(Debug, Args)]
pub struct DoctorArgs {
#[command(subcommand)]
pub command: Option<DoctorCommands>,
}
#[derive(Debug, Subcommand)]
pub enum DoctorCommands {
All,
Config,
Last,
Plugins,
Theme,
}
#[derive(Debug, Subcommand)]
pub enum PluginsCommands {
List,
Commands,
Config(PluginConfigArgs),
Refresh,
Enable(PluginCommandStateArgs),
Disable(PluginCommandStateArgs),
ClearState(PluginCommandClearArgs),
SelectProvider(PluginProviderSelectArgs),
ClearProvider(PluginProviderClearArgs),
Doctor,
}
#[derive(Debug, Args)]
pub struct ThemeArgs {
#[command(subcommand)]
pub command: ThemeCommands,
}
#[derive(Debug, Subcommand)]
pub enum ThemeCommands {
List,
Show(ThemeShowArgs),
Use(ThemeUseArgs),
}
#[derive(Debug, Args)]
pub struct ThemeShowArgs {
pub name: Option<String>,
}
#[derive(Debug, Args)]
pub struct ThemeUseArgs {
pub name: String,
}
#[derive(Debug, Args, Clone, Default)]
pub struct PluginScopeArgs {
#[arg(long = "global", conflicts_with = "profile")]
pub global: bool,
#[arg(long = "profile")]
pub profile: Option<String>,
#[arg(
long = "terminal",
num_args = 0..=1,
default_missing_value = "__current__"
)]
pub terminal: Option<String>,
}
#[derive(Debug, Args, Clone, Default)]
pub struct ConfigScopeArgs {
#[arg(long = "global", conflicts_with_all = ["profile", "profile_all"])]
pub global: bool,
#[arg(long = "profile", conflicts_with = "profile_all")]
pub profile: Option<String>,
#[arg(long = "profile-all", conflicts_with = "profile")]
pub profile_all: bool,
#[arg(
long = "terminal",
num_args = 0..=1,
default_missing_value = "__current__"
)]
pub terminal: Option<String>,
}
#[derive(Debug, Args, Clone, Default)]
pub struct ConfigStoreArgs {
#[arg(long = "session", conflicts_with_all = ["config_store", "secrets", "save"])]
pub session: bool,
#[arg(long = "config", conflicts_with_all = ["session", "secrets"])]
pub config_store: bool,
#[arg(long = "secrets", conflicts_with_all = ["session", "config_store"])]
pub secrets: bool,
#[arg(long = "save", conflicts_with_all = ["session", "config_store", "secrets"])]
pub save: bool,
}
#[derive(Debug, Args, Clone)]
pub struct PluginCommandTargetArgs {
pub command: String,
#[command(flatten)]
pub scope: PluginScopeArgs,
}
#[derive(Debug, Args, Clone, Default)]
pub struct ConfigReadOutputArgs {
#[arg(long = "sources")]
pub sources: bool,
#[arg(long = "raw")]
pub raw: bool,
}
#[derive(Debug, Args)]
pub struct PluginCommandStateArgs {
#[command(flatten)]
pub target: PluginCommandTargetArgs,
}
#[derive(Debug, Args)]
pub struct PluginCommandClearArgs {
#[command(flatten)]
pub target: PluginCommandTargetArgs,
}
#[derive(Debug, Args)]
pub struct PluginProviderSelectArgs {
#[command(flatten)]
pub target: PluginCommandTargetArgs,
pub plugin_id: String,
}
#[derive(Debug, Args)]
pub struct PluginProviderClearArgs {
#[command(flatten)]
pub target: PluginCommandTargetArgs,
}
#[derive(Debug, Args)]
pub struct PluginConfigArgs {
pub plugin_id: String,
}
#[derive(Debug, Args)]
pub struct ConfigArgs {
#[command(subcommand)]
pub command: ConfigCommands,
}
#[derive(Debug, Args)]
pub struct HistoryArgs {
#[command(subcommand)]
pub command: HistoryCommands,
}
#[derive(Debug, Args, Clone, Default)]
pub struct IntroArgs {}
#[derive(Debug, Subcommand)]
pub enum HistoryCommands {
List,
Prune(HistoryPruneArgs),
Clear,
}
#[derive(Debug, Args)]
pub struct HistoryPruneArgs {
pub keep: usize,
}
#[derive(Debug, Subcommand)]
pub enum ConfigCommands {
Show(ConfigShowArgs),
Get(ConfigGetArgs),
Explain(ConfigExplainArgs),
Set(ConfigSetArgs),
Unset(ConfigUnsetArgs),
#[command(alias = "diagnostics")]
Doctor,
}
#[derive(Debug, Args)]
pub struct ConfigShowArgs {
#[command(flatten)]
pub output: ConfigReadOutputArgs,
}
#[derive(Debug, Args)]
pub struct ConfigGetArgs {
pub key: String,
#[command(flatten)]
pub output: ConfigReadOutputArgs,
}
#[derive(Debug, Args)]
pub struct ConfigExplainArgs {
pub key: String,
#[arg(long = "show-secrets")]
pub show_secrets: bool,
}
#[derive(Debug, Args)]
pub struct ConfigSetArgs {
pub key: String,
pub value: String,
#[command(flatten)]
pub scope: ConfigScopeArgs,
#[command(flatten)]
pub store: ConfigStoreArgs,
#[arg(long = "dry-run")]
pub dry_run: bool,
#[arg(long = "yes")]
pub yes: bool,
#[arg(long = "explain")]
pub explain: bool,
}
#[derive(Debug, Args)]
pub struct ConfigUnsetArgs {
pub key: String,
#[command(flatten)]
pub scope: ConfigScopeArgs,
#[command(flatten)]
pub store: ConfigStoreArgs,
#[arg(long = "dry-run")]
pub dry_run: bool,
}
impl Cli {
pub(crate) fn append_static_session_overrides(&self, layer: &mut ConfigLayer) {
if let Some(user) = self
.user
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
layer.set("user.name", user);
}
if self.incognito {
layer.set("repl.history.enabled", false);
}
append_appearance_overrides(
layer,
self.theme.as_deref(),
if self.gammel_og_bitter {
Some(UiPresentation::Austere)
} else {
self.presentation.map(UiPresentation::from)
},
);
}
}
pub(crate) fn append_appearance_overrides(
layer: &mut ConfigLayer,
theme: Option<&str>,
presentation: Option<UiPresentation>,
) {
if let Some(theme) = theme.map(str::trim).filter(|value| !value.is_empty()) {
layer.set("theme.name", theme);
}
if let Some(presentation) = presentation {
layer.set("ui.presentation", presentation.as_config_value());
}
}
pub(crate) fn runtime_load_options_from_flags(
no_env: bool,
no_config_file: bool,
defaults_only: bool,
) -> RuntimeLoadOptions {
if defaults_only {
RuntimeLoadOptions::defaults_only()
} else {
RuntimeLoadOptions::new()
.with_env(!no_env)
.with_config_file(!no_config_file)
}
}
pub fn parse_inline_command_tokens(tokens: &[String]) -> Result<Option<Commands>, clap::Error> {
InlineCommandCli::try_parse_from(tokens.iter().map(String::as_str)).map(|parsed| parsed.command)
}
#[cfg(test)]
mod tests {
use super::{
Cli, Commands, ConfigCommands, InlineCommandCli, RuntimeLoadOptions,
append_appearance_overrides, parse_inline_command_tokens,
};
use crate::config::{ConfigLayer, ConfigValue};
use clap::Parser;
#[test]
fn parse_inline_command_tokens_accepts_builtin_and_external_commands_unit() {
let builtin = parse_inline_command_tokens(&["config".to_string(), "doctor".to_string()])
.expect("builtin command should parse");
assert!(matches!(
builtin,
Some(Commands::Config(args)) if matches!(args.command, ConfigCommands::Doctor)
));
let external = parse_inline_command_tokens(&["ldap".to_string(), "user".to_string()])
.expect("external command should parse");
assert!(
matches!(external, Some(Commands::External(tokens)) if tokens == vec!["ldap", "user"])
);
}
#[test]
fn cli_runtime_load_options_and_inline_parser_follow_disable_flags_unit() {
let cli = Cli::parse_from(["osp", "--no-env", "--no-config-file", "theme", "list"]);
assert_eq!(
cli.runtime_load_options(),
RuntimeLoadOptions::new()
.with_env(false)
.with_config_file(false)
);
let cli = Cli::parse_from(["osp", "--defaults-only", "theme", "list"]);
assert_eq!(
cli.runtime_load_options(),
RuntimeLoadOptions::defaults_only()
);
let inline = InlineCommandCli::try_parse_from(["theme", "list"])
.expect("inline command should parse");
assert!(matches!(inline.command, Some(Commands::Theme(_))));
}
#[test]
fn app_style_alias_maps_to_presentation_unit() {
let cli = Cli::parse_from(["osp", "--app-style", "austere"]);
let mut layer = ConfigLayer::default();
cli.append_static_session_overrides(&mut layer);
assert_eq!(
layer
.entries()
.iter()
.find(|entry| entry.key == "ui.presentation")
.map(|entry| &entry.value),
Some(&ConfigValue::from("austere"))
);
}
#[test]
fn appearance_overrides_trim_theme_and_apply_presentation_unit() {
let mut layer = ConfigLayer::default();
append_appearance_overrides(
&mut layer,
Some(" nord "),
Some(crate::ui::UiPresentation::Compact),
);
assert_eq!(
layer
.entries()
.iter()
.find(|entry| entry.key == "theme.name")
.map(|entry| &entry.value),
Some(&ConfigValue::from("nord"))
);
assert_eq!(
layer
.entries()
.iter()
.find(|entry| entry.key == "ui.presentation")
.map(|entry| &entry.value),
Some(&ConfigValue::from("compact"))
);
}
}