From 77f545b32ee155bef3e5a3e3a478bdbac42876aa Mon Sep 17 00:00:00 2001 From: Ty Date: Thu, 29 Feb 2024 21:11:46 -0700 Subject: [PATCH] Add profiles edit command This took too much effort, someone better appreciate it --- src/commands/profiles/edit.rs | 308 ++++++++++++++++++++++++++++++++++ src/commands/profiles/mod.rs | 2 + src/main.rs | 1 + 3 files changed, 311 insertions(+) create mode 100644 src/commands/profiles/edit.rs diff --git a/src/commands/profiles/edit.rs b/src/commands/profiles/edit.rs new file mode 100644 index 0000000..29068d4 --- /dev/null +++ b/src/commands/profiles/edit.rs @@ -0,0 +1,308 @@ +use anstyle::{AnsiColor, Reset, Style as Anstyle}; +use anyhow::{bail, Context}; +use asp::profiles::{Email, HexColor, Url}; +use clap::Parser; +use dialoguer::{theme::ColorfulTheme, Input, Select}; +use indoc::writedoc; +use sea_orm::{ActiveModelTrait, ActiveValue, EntityTrait, IntoActiveModel, IntoActiveValue}; + +use std::{io::Write, str::FromStr}; + +use crate::{ + commands::AspmSubcommand, + entities::{claims::ActiveModel as ClaimActiveModel, prelude::*}, +}; + +/// Edits an existing profile +#[derive(Parser, Debug)] +pub struct ProfilesEditCommand { + /// The profile to edit. If not provided, it will be queried for interactively. + profile: Option, +} + +#[async_trait::async_trait] +impl AspmSubcommand for ProfilesEditCommand { + async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> { + let (profile, claims) = match &self.profile { + Some(fingerprint) => { + let mut profiles = Profiles::find_by_id(fingerprint) + .find_with_related(Claims) + .all(&state.db) + .await + .context("Unable to query database for a profile")?; + if profiles.is_empty() { + // Impossible to take element out of vec without panicing on OOB, and sea-orm removed SelectTwoMany::one() + bail!("No profile found for the specified fingerprint") + } else { + profiles.swap_remove(0) + } + } + None => { + let mut profiles = Profiles::find() + .find_with_related(Claims) + .all(&state.db) + .await + .context("Unable to query database for profiles")?; + if profiles.is_empty() { + bail!("No profiles found to edit") + } + let choice = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Please select the profile to edit") + .items( + profiles + .iter() + .map(|(profile, _)| &profile.key) + .collect::>() + .as_slice(), + ) + .default(0) + .interact() + .context("Unable to prompt on stderr")?; + + profiles.swap_remove(choice) + } + }; + let (mut profile, mut claims) = ( + profile.into_active_model(), + claims + .into_iter() + .map(|claim| claim.into_active_model()) + .collect::>(), + ); + let mut removed_claims = Vec::::new(); + + loop { + let operation = Select::with_theme(&ColorfulTheme::default()) + .default(0) + .with_prompt("Please select what to do next (or exit without saving with Esc/q)") + .items(&[ + "Edit alias", + "Edit name", + "Edit email", + "Edit description", + "Edit avatar URL", + "Edit color", + "Edit claims", + "Preview", + "Save & exit", + "Exit without saving", + ]) + .interact_opt() + .context("Unable to prompt on stderr")?; + + match operation { + Some(0) => { + profile.alias = ActiveValue::set( + Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Please enter the new profile alias") + .allow_empty(false) + .interact() + .context("Unable to prompt on stderr")?, + ) + } + Some(1) => { + profile.name = ActiveValue::set( + Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Please enter the new profile name") + .allow_empty(false) + .interact() + .context("Unable to prompt on stderr")?, + ) + } + Some(2) => { + profile.email = ActiveValue::set( + Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Please enter the new profile email") + .allow_empty(true) + .validate_with(|input: &String| -> Result<(), &str> { + Email::from_str(input.as_str()) + .map(|_| ()) + .or(Err("Invalid email")) + }) + .interact() + .map(|res| if res.is_empty() { None } else { Some(res) }) + .context("Unable to prompt on stderr")?, + ) + } + Some(3) => { + profile.description = ActiveValue::set( + Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Please enter the new profile description") + .allow_empty(true) + .interact() + .map(|res: String| if res.is_empty() { None } else { Some(res) }) + .context("Unable to prompt on stderr")?, + ) + } + Some(4) => { + profile.avatar_url = ActiveValue::set( + Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Please enter the new profile avatar URL") + .allow_empty(true) + .validate_with(|input: &String| -> Result<(), &str> { + Url::from_str(input.as_str()) + .map(|_| ()) + .or(Err("Invalid URL")) + }) + .interact() + .map(|res| if res.is_empty() { None } else { Some(res) }) + .context("Unable to prompt on stderr")?, + ) + } + Some(5) => { + profile.color = ActiveValue::set( + Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Please enter the new profile color") + .allow_empty(true) + .validate_with(|input: &String| -> Result<(), &str> { + HexColor::from_str(input.as_str()) + .map(|_| ()) + .or(Err("Invalid color")) + }) + .interact() + .map(|res| if res.is_empty() { None } else { Some(res) }) + .context("Unable to prompt on stderr")?, + ) + } + Some(6) => loop { + match Select::with_theme(&ColorfulTheme::default()) + .with_prompt("How would you like to edit the claims?") + .default(0) + .items(&[ + "Add a new claim", + "Edit an existing claim", + "Remove a claim", + "Stop editing claims", + ]) + .interact_opt() + .context("Unable to prompt on stderr")? + { + Some(0) => claims.push(ClaimActiveModel { + profile: profile.key.clone(), + uri: Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Please enter the URL of the claim") + .allow_empty(false) + .validate_with(|input: &String| -> Result<(), &str> { + Url::from_str(input.as_str()) + .map(|_| ()) + .or(Err("Invalid URL")) + }) + .interact() + .context("Unable to prompt on stderr")? + .into_active_value(), + ..Default::default() + }), + Some(1) => { + let index = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Which claim would you like to edit?") + .default(0) + .items( + claims + .iter() + .map(|claim| claim.uri.as_ref()) + .collect::>() + .as_slice(), + ) + .interact() + .context("Unable to prompt on stderr")?; + claims[index].uri = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Please enter the URL of the claim") + .allow_empty(false) + .validate_with(|input: &String| -> Result<(), &str> { + Url::from_str(input.as_str()) + .map(|_| ()) + .or(Err("Invalid URL")) + }) + .interact() + .context("Unable to prompt on stderr")? + .into_active_value() + } + Some(2) => removed_claims.push( + claims.remove( + Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Which claim would you like to edit?") + .default(0) + .items( + claims + .iter() + .map(|claim| claim.uri.as_ref()) + .collect::>() + .as_slice(), + ) + .interact() + .context("Unable to prompt on stderr")?, + ), + ), + Some(3) | None => break, + _ => unreachable!(), + } + }, + Some(7) => { + let reset = Reset.render(); + let alias_style = Anstyle::new() + .underline() + .fg_color(Some(anstyle::Color::Ansi(AnsiColor::BrightCyan))) + .render(); + let key_style = Anstyle::new() + .fg_color(Some(anstyle::Color::Ansi(AnsiColor::BrightGreen))) + .render(); + let value_style = Anstyle::new() + .fg_color(Some(anstyle::Color::Ansi(AnsiColor::BrightYellow))) + .render(); + let _ = writedoc! { + std::io::stdout(), + " + {alias_style}{alias}{reset}: + {key_style}Fingerprint{reset} {value_style}{fingerprint}{reset} + {key_style}Name{reset} {value_style}{name}{reset} + {key_style}Email{reset} {value_style}{email}{reset} + {key_style}Description{reset} {value_style}{description}{reset} + {key_style}Avatar URL{reset} {value_style}{avatar_url}{reset} + {key_style}Color{reset} {value_style}{color}{reset} + {key_style}Claims{reset} + {claims} + ", + alias = profile.alias.as_ref(), + fingerprint = profile.key.as_ref(), + name = profile.name.as_ref(), + description = profile.description.as_ref().clone().unwrap_or("None".to_string()), + avatar_url = profile.avatar_url.as_ref().clone().unwrap_or("None".to_string()), + email = profile.email.as_ref().clone().unwrap_or("None".to_string()), + color = profile.color.as_ref().clone().unwrap_or("None".to_string()), + claims = claims.iter().map(|claim| format!("\t\t- {value_style}{uri}{reset}", uri = claim.uri.as_ref())).collect::>().join("\n"), + }; + } + Some(8) => { + // Update profile + profile + .update(&state.db) + .await + .context("Unable to save profile to database")?; + // Update and insert all claims + for claim in claims { + claim + .save(&state.db) + .await + .context("Unable to save claim to database")?; + } + // Remove all deleted claims + for claim in removed_claims { + claim + .delete(&state.db) + .await + .context("Unable to remove claim from database")?; + } + println!("All changes saved"); + break; + } + None | Some(9) => { + println!("Changes discarded"); + break; + } + _ => unreachable!(), + } + } + + Ok(()) + } +} diff --git a/src/commands/profiles/mod.rs b/src/commands/profiles/mod.rs index ee2c971..f0d21d4 100644 --- a/src/commands/profiles/mod.rs +++ b/src/commands/profiles/mod.rs @@ -1,6 +1,7 @@ use clap::{Parser, Subcommand}; pub mod create; +pub mod edit; pub mod export; pub mod list; @@ -16,4 +17,5 @@ pub enum ProfilesSubcommands { Create(create::ProfilesCreateCommand), Export(export::ProfilesExportCommand), List(list::ProfilesListCommand), + Edit(edit::ProfilesEditCommand), } diff --git a/src/main.rs b/src/main.rs index a042aaa..ade5088 100644 --- a/src/main.rs +++ b/src/main.rs @@ -160,6 +160,7 @@ fn cli(parsed: AspmCommand) -> Result<(), anyhow::Error> { ProfilesSubcommands::Create(subcommand) => subcommand.execute_sync(state, runtime), ProfilesSubcommands::Export(subcommand) => subcommand.execute_sync(state, runtime), ProfilesSubcommands::List(subcommand) => subcommand.execute_sync(state, runtime), + ProfilesSubcommands::Edit(subcommand) => subcommand.execute_sync(state, runtime), }, } }