| |
usage: r#"
|
| |
Usage
|
| |
|
| - |
rad profile new <name>
|
| - |
rad profile switch <name> [--print-env]
|
| + |
rad profile new <name> [--from <name>] [--force]
|
| + |
rad profile switch <name|default> [--print-env]
|
| |
rad profile list
|
| |
rad profile current
|
| |
rad profile remove <name> [-y]
|
| |
|
| |
Options
|
| |
|
| + |
--from <name> Create from existing profile (for: new)
|
| + |
--force, -f Overwrite existing profile or reserved name (for: new)
|
| |
--print-env Print shell export for RAD_PROFILE (for: switch)
|
| |
--yes, -y Confirm removal (for: remove)
|
| |
--help Print help
|
| + |
|
| + |
Notes
|
| + |
|
| + |
• 'default' is the pseudo-profile representing the root (~/.radicle).
|
| + |
'rad profile switch default' persists the current root into the active profile,
|
| + |
clears the root, and unsets the active profile marker.
|
| |
"#,
|
| |
};
|
| |
|
| - |
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
| - |
enum OpTag {
|
| - |
#[default]
|
| - |
List,
|
| + |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| + |
enum OperationName {
|
| |
New,
|
| |
Switch,
|
| + |
List,
|
| |
Current,
|
| |
Remove,
|
| |
}
|
| |
|
| |
#[derive(Debug)]
|
| - |
enum Op {
|
| - |
New { name: String },
|
| - |
Switch { name: String, print_env: bool },
|
| - |
List,
|
| - |
Current,
|
| - |
Remove { name: String, yes: bool },
|
| + |
struct Operation {
|
| + |
name: OperationName,
|
| + |
target: Option<String>,
|
| + |
from: Option<String>,
|
| + |
force: bool,
|
| + |
print_env: bool,
|
| + |
yes: bool,
|
| |
}
|
| |
|
| |
#[derive(Debug)]
|
| - |
pub struct Options {
|
| - |
op: Op,
|
| + |
pub struct Options(Operation);
|
| + |
|
| + |
#[derive(Error, Debug)]
|
| + |
pub enum ProfileError {
|
| + |
#[error("home directory not available")]
|
| + |
Home,
|
| + |
#[error("profiles root not found: {0}")]
|
| + |
ProfilesRoot(PathBuf),
|
| + |
#[error("active profile marker error: {0}")]
|
| + |
ActiveMarker(PathBuf),
|
| + |
#[error("profile not found: {0}")]
|
| + |
NotFound(String),
|
| + |
#[error("profile already exists: {0}")]
|
| + |
AlreadyExists(String),
|
| + |
#[error("cannot remove active profile: {0}")]
|
| + |
RemoveActive(String),
|
| + |
#[error("cannot remove pseudo-profile 'default'")]
|
| + |
RemoveDefault,
|
| + |
#[error("confirmation required, use -y/--yes")]
|
| + |
ConfirmationRequired,
|
| + |
#[error("io error: {0}")]
|
| + |
Io(#[from] io::Error),
|
| + |
#[error("invalid state: {0}")]
|
| + |
InvalidState(&'static str),
|
| |
}
|
| |
|
| + |
type Result<T> = std::result::Result<T, ProfileError>;
|
| + |
|
| |
impl Args for Options {
|
| |
fn from_args(v: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
|
| - |
let mut p = lexopt::Parser::from_args(v);
|
| - |
let mut tag: Option<OpTag> = None;
|
| - |
let mut name: Option<String> = None;
|
| + |
let mut p = Parser::from_args(v);
|
| + |
let mut op = None;
|
| + |
let mut target = None;
|
| + |
let mut from = None;
|
| + |
let mut force = false;
|
| |
let mut print_env = false;
|
| |
let mut yes = false;
|
| |
|
| |
while let Some(arg) = p.next()? {
|
| |
match arg {
|
| - |
Long("help") | Short('h') => return Err(args::Error::Help.into()),
|
| - |
Long("print-env") => print_env = true,
|
| - |
Long("yes") | Short('y') => yes = true,
|
| - |
Value(val) if tag.is_none() => match val.to_string_lossy().as_ref() {
|
| - |
"new" => tag = Some(OpTag::New),
|
| - |
"switch" | "use" => tag = Some(OpTag::Switch),
|
| - |
"list" | "ls" => tag = Some(OpTag::List),
|
| - |
"current" | "cur" => tag = Some(OpTag::Current),
|
| - |
"remove" | "rm" => tag = Some(OpTag::Remove),
|
| - |
u => anyhow::bail!("unknown operation '{}'", u),
|
| - |
},
|
| - |
Value(val) if name.is_none() => name = Some(args::string(&val)),
|
| - |
_ => return Err(anyhow!(arg.unexpected())),
|
| + |
Value(val) if op.is_none() => {
|
| + |
let s = term::args::string(&val);
|
| + |
op = Some(match s.as_str() {
|
| + |
"new" => OperationName::New,
|
| + |
"switch" => OperationName::Switch,
|
| + |
"list" => OperationName::List,
|
| + |
"current" => OperationName::Current,
|
| + |
"remove" => OperationName::Remove,
|
| + |
other => anyhow::bail!("unknown subcommand: {other}"),
|
| + |
});
|
| + |
}
|
| + |
Value(val) => {
|
| + |
target = Some(term::args::string(&val));
|
| + |
}
|
| + |
Long("from") => {
|
| + |
from = Some(term::args::string(&p.value()?));
|
| + |
}
|
| + |
Long("force") | Short('f') => {
|
| + |
force = true;
|
| + |
}
|
| + |
Long("print-env") => {
|
| + |
print_env = true;
|
| + |
}
|
| + |
Long("yes") | Short('y') => {
|
| + |
yes = true;
|
| + |
}
|
| + |
Long("help") | Short('h') => return Err(term::args::Error::Help.into()),
|
| + |
_ => anyhow::bail!("unexpected argument"),
|
| |
}
|
| |
}
|
| |
|
| - |
let op = match tag.unwrap_or_default() {
|
| - |
OpTag::New => Op::New {
|
| - |
name: name.ok_or(anyhow!("name required, see `rad profile new --help`"))?,
|
| - |
},
|
| - |
OpTag::Switch => Op::Switch {
|
| - |
name: name.ok_or(anyhow!("name required, see `rad profile switch --help`"))?,
|
| + |
let name = op.ok_or_else(|| anyhow::anyhow!("missing subcommand"))?;
|
| + |
if matches!(name, OperationName::New | OperationName::Switch | OperationName::Remove)
|
| + |
&& target.is_none()
|
| + |
{
|
| + |
anyhow::bail!("missing <name>");
|
| + |
}
|
| + |
|
| + |
Ok((
|
| + |
Options(Operation {
|
| + |
name,
|
| + |
target,
|
| + |
from,
|
| + |
force,
|
| |
print_env,
|
| - |
},
|
| - |
OpTag::List => Op::List,
|
| - |
OpTag::Current => Op::Current,
|
| - |
OpTag::Remove => Op::Remove {
|
| - |
name: name.ok_or(anyhow!("name required, see `rad profile remove --help`"))?,
|
| |
yes,
|
| - |
},
|
| - |
};
|
| - |
|
| - |
Ok((Options { op }, vec![]))
|
| + |
}),
|
| + |
vec![],
|
| + |
))
|
| |
}
|
| |
}
|
| |
|
| - |
pub fn run(options: Options, _ctx: impl Context) -> anyhow::Result<()> {
|
| - |
ensure_roots()?;
|
| - |
match options.op {
|
| - |
Op::New { name } => {
|
| - |
create_profile(&name)?;
|
| - |
set_active(&name)?;
|
| - |
clear_root()?;
|
| - |
term::success!("Profile {} created and activated; root cleared", term::format::tertiary(&name));
|
| - |
}
|
| - |
Op::Switch { name, print_env } => {
|
| - |
let cur = active()?;
|
| - |
if cur != name {
|
| - |
ensure_profile_dir(&cur)?;
|
| - |
save_root_into(&cur)?;
|
| - |
apply_profile_to_root(&name)?;
|
| - |
set_active(&name)?;
|
| - |
} else {
|
| - |
apply_profile_to_root(&name)?;
|
| + |
pub fn run(opts: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
| + |
run_inner(opts, ctx).map_err(|e| anyhow::anyhow!(e))
|
| + |
}
|
| + |
|
| + |
fn run_inner(opts: Options, ctx: impl term::Context) -> Result<()> {
|
| + |
let home = resolve_home(ctx)?;
|
| + |
|
| + |
match opts.0.name {
|
| + |
OperationName::New => {
|
| + |
let name = opts.0.target.as_ref().unwrap();
|
| + |
if name == "default" && !opts.0.force {
|
| + |
return Err(ProfileError::AlreadyExists("default".into()));
|
| |
}
|
| - |
if print_env {
|
| - |
println!("export RAD_PROFILE={}", name);
|
| + |
create_profile(&home, name, opts.0.from.as_deref(), opts.0.force)?;
|
| + |
write_active(&home, name)?;
|
| + |
clear_root_for_auth(&home)?;
|
| + |
term::success!("Profile {} created", term::format::tertiary(name));
|
| + |
}
|
| + |
OperationName::Switch => {
|
| + |
let name = opts.0.target.as_ref().unwrap();
|
| + |
if name == "default" {
|
| + |
if let Some(current) = read_active(&home)? {
|
| + |
persist_root_into_profile(&home, ¤t)?;
|
| + |
}
|
| + |
clear_root_for_auth(&home)?;
|
| + |
clear_active(&home)?;
|
| + |
if opts.0.print_env {
|
| + |
println!("unset RAD_PROFILE");
|
| + |
} else {
|
| + |
term::success!("Switched to {}", term::format::tertiary("default"));
|
| + |
}
|
| |
} else {
|
| - |
term::success!("Switched to {}", term::format::tertiary(&name));
|
| + |
let active = read_active(&home)?;
|
| + |
if let Some(current) = active.as_deref() {
|
| + |
persist_root_into_profile(&home, current)?;
|
| + |
}
|
| + |
copy_profile_into_root(&home, name)?;
|
| + |
write_active(&home, name)?;
|
| + |
if opts.0.print_env {
|
| + |
println!("export RAD_PROFILE={name}");
|
| + |
} else {
|
| + |
term::success!("Switched to {}", term::format::tertiary(name));
|
| + |
}
|
| |
}
|
| |
}
|
| - |
Op::List => list_profiles()?,
|
| - |
Op::Current => {
|
| - |
println!("{}", active()?);
|
| + |
OperationName::List => {
|
| + |
list_profiles(&home)?;
|
| |
}
|
| - |
Op::Remove { name, yes } => {
|
| - |
remove_profile(&name, yes)?;
|
| - |
term::success!("Profile {} removed", term::format::tertiary(&name));
|
| + |
OperationName::Current => {
|
| + |
let cur = read_active(&home)?.unwrap_or_else(|| "default".to_string());
|
| + |
println!("{cur}");
|
| + |
}
|
| + |
OperationName::Remove => {
|
| + |
let name = opts.0.target.as_ref().unwrap();
|
| + |
if name == "default" {
|
| + |
return Err(ProfileError::RemoveDefault);
|
| + |
}
|
| + |
let cur = read_active(&home)?;
|
| + |
if cur.as_deref() == Some(name) {
|
| + |
return Err(ProfileError::RemoveActive(name.clone()));
|
| + |
}
|
| + |
if !opts.0.yes {
|
| + |
return Err(ProfileError::ConfirmationRequired);
|
| + |
}
|
| + |
remove_profile(&home, name)?;
|
| + |
term::success!("Profile {} removed", term::format::tertiary(name));
|
| |
}
|
| |
}
|
| |
Ok(())
|
| |
}
|
| |
|
| - |
fn rad_home() -> anyhow::Result<PathBuf> {
|
| - |
Ok(profile::home()?.path().to_path_buf())
|
| - |
}
|
| |
|
| - |
fn profiles_root() -> anyhow::Result<PathBuf> {
|
| - |
Ok(rad_home()?.join("profiles"))
|
| + |
fn resolve_home(ctx: impl term::Context) -> Result<PathBuf> {
|
| + |
if let Ok(p) = ctx.profile() {
|
| + |
return Ok(p.home().path().to_path_buf());
|
| + |
}
|
| + |
if let Ok(rad_home) = env::var("RAD_HOME") {
|
| + |
return Ok(PathBuf::from(rad_home));
|
| + |
}
|
| + |
if let Ok(home) = env::var("HOME") {
|
| + |
return Ok(PathBuf::from(home).join(".radicle"));
|
| + |
}
|
| + |
Err(ProfileError::Home)
|
| |
}
|
| |
|
| - |
fn active_file() -> anyhow::Result<PathBuf> {
|
| - |
Ok(rad_home()?.join(".active_profile"))
|
| + |
fn profiles_root(home: &Path) -> PathBuf {
|
| + |
home.join("profiles")
|
| |
}
|
| |
|
| - |
fn cfg_root() -> anyhow::Result<PathBuf> {
|
| - |
Ok(rad_home()?.join("config.json"))
|
| + |
fn active_file(home: &Path) -> PathBuf {
|
| + |
home.join(".active_profile")
|
| |
}
|
| |
|
| - |
fn keys_root() -> anyhow::Result<PathBuf> {
|
| - |
Ok(rad_home()?.join("keys"))
|
| + |
fn root_config(home: &Path) -> PathBuf {
|
| + |
home.join("config.json")
|
| |
}
|
| |
|
| - |
fn profile_dir(name: &str) -> anyhow::Result<PathBuf> {
|
| - |
Ok(profiles_root()?.join(name))
|
| + |
fn root_keys(home: &Path) -> PathBuf {
|
| + |
home.join("keys")
|
| |
}
|
| |
|
| - |
fn profile_cfg(name: &str) -> anyhow::Result<PathBuf> {
|
| - |
Ok(profile_dir(name)?.join("config.json"))
|
| + |
fn profile_dir(home: &Path, name: &str) -> PathBuf {
|
| + |
profiles_root(home).join(name)
|
| |
}
|
| |
|
| - |
fn profile_keys(name: &str) -> anyhow::Result<PathBuf> {
|
| - |
Ok(profile_dir(name)?.join("keys"))
|
| + |
fn profile_config(home: &Path, name: &str) -> PathBuf {
|
| + |
profile_dir(home, name).join("config.json")
|
| |
}
|
| |
|
| - |
fn exists(p: &Path) -> bool {
|
| - |
fs::symlink_metadata(p).is_ok()
|
| + |
fn profile_keys(home: &Path, name: &str) -> PathBuf {
|
| + |
profile_dir(home, name).join("keys")
|
| |
}
|
| |
|
| - |
fn ensure_roots() -> anyhow::Result<()> {
|
| - |
let pr = profiles_root()?;
|
| - |
if !pr.exists() {
|
| - |
fs::create_dir_all(&pr)?;
|
| - |
}
|
| + |
fn ensure_profiles_root(home: &Path) -> Result<()> {
|
| + |
fs::create_dir_all(profiles_root(home))?;
|
| |
Ok(())
|
| |
}
|
| |
|
| - |
fn ensure_profile_dir(name: &str) -> anyhow::Result<()> {
|
| - |
let dir = profile_dir(name)?;
|
| - |
if !dir.exists() {
|
| - |
fs::create_dir_all(profile_keys(name)?)?;
|
| - |
} else {
|
| - |
fs::create_dir_all(profile_keys(name)?)?;
|
| + |
fn create_profile(home: &Path, name: &str, from: Option<&str>, force: bool) -> Result<()> {
|
| + |
ensure_profiles_root(home)?;
|
| + |
let dst_dir = profile_dir(home, name);
|
| + |
if dst_dir.exists() && !force {
|
| + |
return Err(ProfileError::AlreadyExists(name.to_string()));
|
| |
}
|
| - |
Ok(())
|
| - |
}
|
| - |
|
| - |
fn create_profile(name: &str) -> anyhow::Result<()> {
|
| - |
let dir = profile_dir(name)?;
|
| - |
if dir.exists() {
|
| - |
anyhow::bail!("profile already exists");
|
| + |
fs::create_dir_all(&dst_dir)?;
|
| + |
fs::create_dir_all(profile_keys(home, name))?;
|
| + |
if let Some(src) = from {
|
| + |
let src_cfg = profile_config(home, src);
|
| + |
let src_keys = profile_keys(home, src);
|
| + |
let dst_cfg = profile_config(home, name);
|
| + |
let dst_keys = profile_keys(home, name);
|
| + |
if src_cfg.exists() {
|
| + |
if let Some(parent) = dst_cfg.parent() {
|
| + |
fs::create_dir_all(parent)?;
|
| + |
}
|
| + |
fs::copy(src_cfg, dst_cfg)?;
|
| + |
}
|
| + |
if src_keys.exists() {
|
| + |
copy_dir(&src_keys, &dst_keys)?;
|
| + |
}
|
| |
}
|
| - |
fs::create_dir_all(profile_keys(name)?)?;
|
| |
Ok(())
|
| |
}
|
| |
|
| - |
fn set_active(name: &str) -> anyhow::Result<()> {
|
| - |
fs::write(active_file()?, format!("{}\n", name))?;
|
| + |
fn remove_profile(home: &Path, name: &str) -> Result<()> {
|
| + |
let dir = profile_dir(home, name);
|
| + |
if !dir.exists() {
|
| + |
return Err(ProfileError::NotFound(name.to_string()));
|
| + |
}
|
| + |
fs::remove_dir_all(dir)?;
|
| |
Ok(())
|
| |
}
|
| |
|
| - |
fn active() -> anyhow::Result<String> {
|
| - |
if let Ok(v) = std::env::var("RAD_PROFILE") {
|
| - |
return Ok(v);
|
| - |
}
|
| - |
let f = active_file()?;
|
| - |
if exists(&f) {
|
| - |
let s = fs::read_to_string(f)?;
|
| - |
let s = s.trim();
|
| - |
if !s.is_empty() {
|
| - |
return Ok(s.to_owned());
|
| - |
}
|
| + |
fn read_active(home: &Path) -> Result<Option<String>> {
|
| + |
let path = active_file(home);
|
| + |
match fs::read_to_string(&path) {
|
| + |
Ok(s) => Ok(Some(s.trim().to_string())),
|
| + |
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
|
| + |
Err(_) => Err(ProfileError::ActiveMarker(path)),
|
| |
}
|
| - |
Ok("default".to_owned())
|
| |
}
|
| |
|
| - |
fn clear_root() -> anyhow::Result<()> {
|
| - |
let c = cfg_root()?;
|
| - |
if exists(&c) {
|
| - |
fs::remove_file(&c).ok();
|
| - |
}
|
| - |
let k = keys_root()?;
|
| - |
if exists(&k) {
|
| - |
fs::remove_dir_all(&k).ok();
|
| + |
fn write_active(home: &Path, name: &str) -> Result<()> {
|
| + |
let path = active_file(home);
|
| + |
if let Some(parent) = path.parent() {
|
| + |
fs::create_dir_all(parent)?;
|
| |
}
|
| + |
fs::write(path, format!("{name}\n"))?;
|
| |
Ok(())
|
| |
}
|
| |
|
| - |
fn copy_file_atomic(src: &Path, dst: &Path) -> anyhow::Result<()> {
|
| - |
if !exists(src) {
|
| - |
anyhow::bail!("missing {}", src.display());
|
| + |
fn clear_active(home: &Path) -> Result<()> {
|
| + |
let path = active_file(home);
|
| + |
if path.exists() {
|
| + |
fs::remove_file(path)?;
|
| |
}
|
| - |
if let Some(p) = dst.parent() {
|
| - |
fs::create_dir_all(p)?;
|
| - |
}
|
| - |
let tmp = dst.with_extension("tmp");
|
| - |
fs::copy(src, &tmp).with_context(|| format!("copy {} -> {}", src.display(), tmp.display()))?;
|
| - |
if exists(dst) {
|
| - |
fs::remove_file(dst).ok();
|
| - |
}
|
| - |
fs::rename(&tmp, dst)?;
|
| |
Ok(())
|
| |
}
|
| |
|
| - |
fn copy_dir_replace(src: &Path, dst: &Path) -> anyhow::Result<()> {
|
| - |
if !exists(src) {
|
| - |
anyhow::bail!("missing {}", src.display());
|
| + |
fn persist_root_into_profile(home: &Path, name: &str) -> Result<()> {
|
| + |
let src_cfg = root_config(home);
|
| + |
let src_keys = root_keys(home);
|
| + |
let dst_cfg = profile_config(home, name);
|
| + |
let dst_keys = profile_keys(home, name);
|
| + |
|
| + |
if let Some(parent) = dst_cfg.parent() {
|
| + |
fs::create_dir_all(parent)?;
|
| |
}
|
| - |
if exists(dst) {
|
| - |
fs::remove_dir_all(dst)?;
|
| + |
fs::create_dir_all(&dst_keys)?;
|
| + |
|
| + |
if src_cfg.exists() {
|
| + |
fs::copy(src_cfg, dst_cfg)?;
|
| |
}
|
| - |
fs::create_dir_all(dst)?;
|
| - |
for e in fs::read_dir(src)? {
|
| - |
let e = e?;
|
| - |
let ft = e.file_type()?;
|
| - |
let sp = e.path();
|
| - |
let dp = dst.join(e.file_name());
|
| - |
if ft.is_dir() {
|
| - |
copy_dir_replace(&sp, &dp)?;
|
| - |
} else if ft.is_file() {
|
| - |
copy_file_atomic(&sp, &dp)?;
|
| - |
}
|
| + |
if src_keys.exists() {
|
| + |
copy_dir(&src_keys, &dst_keys)?;
|
| |
}
|
| |
Ok(())
|
| |
}
|
| |
|
| - |
fn save_root_into(name: &str) -> anyhow::Result<()> {
|
| - |
let csrc = cfg_root()?;
|
| - |
let ksrc = keys_root()?;
|
| - |
if !exists(&csrc) && !exists(&ksrc) {
|
| - |
return Ok(());
|
| + |
fn copy_profile_into_root(home: &Path, name: &str) -> Result<()> {
|
| + |
let src_cfg = profile_config(home, name);
|
| + |
let src_keys = profile_keys(home, name);
|
| + |
let dst_cfg = root_config(home);
|
| + |
let dst_keys = root_keys(home);
|
| + |
|
| + |
if dst_cfg.exists() {
|
| + |
fs::remove_file(&dst_cfg)?;
|
| |
}
|
| - |
ensure_profile_dir(name)?;
|
| - |
let cdst = profile_cfg(name)?;
|
| - |
let kdst = profile_keys(name)?;
|
| - |
if exists(&csrc) {
|
| - |
copy_file_atomic(&csrc, &cdst)?;
|
| + |
if dst_keys.exists() {
|
| + |
fs::remove_dir_all(&dst_keys)?;
|
| |
}
|
| - |
if exists(&ksrc) {
|
| - |
copy_dir_replace(&ksrc, &kdst)?;
|
| + |
if src_cfg.exists() {
|
| + |
if let Some(parent) = dst_cfg.parent() {
|
| + |
fs::create_dir_all(parent)?;
|
| + |
}
|
| + |
fs::copy(src_cfg, dst_cfg)?;
|
| + |
}
|
| + |
if src_keys.exists() {
|
| + |
fs::create_dir_all(&dst_keys)?;
|
| + |
copy_dir(&src_keys, &dst_keys)?;
|
| |
}
|
| |
Ok(())
|
| |
}
|
| |
|
| - |
fn apply_profile_to_root(name: &str) -> anyhow::Result<()> {
|
| - |
let pd = profile_dir(name)?;
|
| - |
if !pd.exists() {
|
| - |
anyhow::bail!("profile '{}' not found", name);
|
| + |
fn clear_root_for_auth(home: &Path) -> Result<()> {
|
| + |
let cfg = root_config(home);
|
| + |
let keys = root_keys(home);
|
| + |
if cfg.exists() {
|
| + |
fs::remove_file(cfg)?;
|
| |
}
|
| - |
let pc = profile_cfg(name)?;
|
| - |
let pk = profile_keys(name)?;
|
| - |
clear_root()?;
|
| - |
if exists(&pc) {
|
| - |
copy_file_atomic(&pc, &cfg_root()?)?;
|
| - |
}
|
| - |
if exists(&pk) {
|
| - |
copy_dir_replace(&pk, &keys_root()?)?;
|
| + |
if keys.exists() {
|
| + |
fs::remove_dir_all(keys)?;
|
| |
}
|
| |
Ok(())
|
| |
}
|
| |
|
| - |
fn list_profiles() -> anyhow::Result<()> {
|
| - |
let cur = active()?;
|
| - |
let mut v = Vec::new();
|
| - |
for e in fs::read_dir(profiles_root()?)? {
|
| - |
let e = e?;
|
| + |
fn list_profiles(home: &Path) -> Result<()> {
|
| + |
ensure_profiles_root(home)?;
|
| + |
let active = read_active(home)?;
|
| + |
let mut names = Vec::<String>::new();
|
| + |
for entry in fs::read_dir(profiles_root(home))? {
|
| + |
let e = entry?;
|
| |
if e.file_type()?.is_dir() {
|
| - |
v.push(e.file_name().to_string_lossy().into_owned());
|
| + |
if let Some(n) = e.file_name().to_str() {
|
| + |
names.push(n.to_string());
|
| + |
}
|
| |
}
|
| |
}
|
| - |
v.sort();
|
| - |
for n in v {
|
| - |
if n == cur {
|
| - |
println!("* {}", n);
|
| + |
names.sort();
|
| + |
|
| + |
|
| + |
if active.is_none() {
|
| + |
println!("* default");
|
| + |
} else {
|
| + |
println!(" default");
|
| + |
}
|
| + |
|
| + |
for n in names {
|
| + |
if active.as_deref() == Some(n.as_str()) {
|
| + |
println!("* {n}");
|
| |
} else {
|
| - |
println!(" {}", n);
|
| + |
println!(" {n}");
|
| |
}
|
| |
}
|
| |
Ok(())
|
| |
}
|
| |
|
| - |
fn remove_profile(name: &str, yes: bool) -> anyhow::Result<()> {
|
| - |
let cur = active().unwrap_or_else(|_| "default".to_owned());
|
| - |
if name == cur {
|
| - |
anyhow::bail!("cannot remove the active profile");
|
| - |
}
|
| - |
let dir = profile_dir(name)?;
|
| - |
if !dir.exists() {
|
| - |
anyhow::bail!("profile '{}' not found", name);
|
| - |
}
|
| - |
if !yes {
|
| - |
anyhow::bail!("refusing to remove without -y");
|
| + |
fn copy_dir(src: &Path, dst: &Path) -> Result<()> {
|
| + |
fs::create_dir_all(dst)?;
|
| + |
for entry in fs::read_dir(src)? {
|
| + |
let e = entry?;
|
| + |
let ty = e.file_type()?;
|
| + |
let to = dst.join(e.file_name());
|
| + |
if ty.is_dir() {
|
| + |
copy_dir(&e.path(), &to)?;
|
| + |
} else {
|
| + |
fs::copy(e.path(), &to)?;
|
| + |
}
|
| |
}
|
| - |
fs::remove_dir_all(dir)?;
|
| |
Ok(())
|
| |
}
|
| |
\ No newline at end of file
|