From ab0fd6950327e8b61dd36e917d24d2c194855f41 Mon Sep 17 00:00:00 2001 From: Overlord Date: Fri, 14 Aug 2026 00:54:27 +0200 Subject: [PATCH] Show [name] for optional positionals, allow filtering import by name too The custom --help renderer showed every positional as regardless of whether it was actually required, which was misleading for open and transfer (both take an optional profile name). positional_args() now checks Arg::is_required_set() and renders [name] for an optional one. transfer's name positional previously only worked with --export and was rejected outright when combined with --import. It now filters --import the same way: 'porthole transfer -i file.toml' imports just that one profile out of the file instead of everything in it, erroring with a new TransferProfileNotFound if the file doesn't contain it. Co-Authored-By: Claude Sonnet 5 --- src/cli.rs | 2 +- src/commands/transfer.rs | 25 ++++++++++++++++++------- src/error.rs | 4 ++-- src/main.rs | 9 +++++++-- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 2737d7a..65d364a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -188,7 +188,7 @@ pub struct WipeArgs { #[derive(Args)] pub struct TransferArgs { - /// Name of the profile to export; every saved profile if omitted. Not used for import. + /// Restrict export/import to just this profile; every profile in scope if omitted. pub name: Option, /// Export saved profiles to a file. diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs index 953ed5c..271fa46 100644 --- a/src/commands/transfer.rs +++ b/src/commands/transfer.rs @@ -19,9 +19,8 @@ pub fn run(args: TransferArgs) -> Result<()> { match (&args.export, &args.import) { (Some(_), Some(_)) => Err(PortholeError::TransferConflictingMode), (None, None) => Err(PortholeError::TransferNoMode), - (None, Some(_)) if args.name.is_some() => Err(PortholeError::TransferNameWithImport), (Some(path), None) => export(path, args.name.as_deref()), - (None, Some(path)) => import(path), + (None, Some(path)) => import(path, args.name.as_deref()), } } @@ -48,22 +47,34 @@ fn export(path: &str, name: Option<&str>) -> Result<()> { Ok(()) } -fn import(path: &str) -> Result<()> { +fn import(path: &str, name: Option<&str>) -> Result<()> { let text = std::fs::read_to_string(path)?; let file: TransferFile = toml::from_str(&text)?; - for p in &file.profiles { + let selected = match name { + Some(n) => { + let n = profile::normalize(n); + let found = file.profiles.into_iter().find(|p| p.name == n); + vec![found.ok_or(PortholeError::TransferProfileNotFound(n))?] + } + None => file.profiles, + }; + + for p in &selected { profile::require_valid_name(&p.name)?; if profile::exists(&p.name) { return Err(PortholeError::AlreadyExists(p.name.clone())); } } - for p in &file.profiles { + for p in &selected { profile::save(p)?; } - ui::ok(&format!("Imported {} profile(s) from '{path}'.", file.profiles.len())); - warn_about_missing_identities(&file.profiles); + match name { + Some(n) => ui::ok(&format!("Imported profile '{n}' from '{path}'.")), + None => ui::ok(&format!("Imported {} profile(s) from '{path}'.", selected.len())), + } + warn_about_missing_identities(&selected); Ok(()) } diff --git a/src/error.rs b/src/error.rs index 19bce56..3bdcf0a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -35,8 +35,8 @@ pub enum PortholeError { #[error("only one of -i/--import, -e/--export may be given")] TransferConflictingMode, - #[error("a profile name only applies to -e/--export, not -i/--import")] - TransferNameWithImport, + #[error("'{0}' not found in the transfer file")] + TransferProfileNotFound(String), #[error("nothing to do: {0}")] NothingToDo(String), diff --git a/src/main.rs b/src/main.rs index 22d9f43..9053b77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -139,10 +139,15 @@ fn print_full_help() { } } -/// `` for each positional arg of `cmd`, space-joined and colored cyan. +/// `` for each required positional arg of `cmd`, `[name]` for an +/// optional one, space-joined and colored cyan. fn positional_args(cmd: &clap::Command) -> String { cmd.get_positionals() - .map(|a| ui::cyan(&format!("<{}>", a.get_id().as_str()))) + .map(|a| { + let id = a.get_id().as_str(); + let text = if a.is_required_set() { format!("<{id}>") } else { format!("[{id}]") }; + ui::cyan(&text) + }) .collect::>() .join(" ") }