diff --git a/rust/agama-cli/src/config.rs b/rust/agama-cli/src/config.rs index 1f97056616..538d05b886 100644 --- a/rust/agama-cli/src/config.rs +++ b/rust/agama-cli/src/config.rs @@ -1,17 +1,23 @@ -use std::io::{self, Read}; - -use crate::{ - error::CliError, - printers::{print, Format}, +use std::{ + io::{self, Read}, + path::PathBuf, + process::Command, }; + +use crate::show_progress; use agama_lib::{ auth::AuthToken, connection, install_settings::InstallSettings, Store as SettingsStore, }; +use anyhow::anyhow; use clap::Subcommand; +use std::io::Write; +use tempfile::Builder; + +const DEFAULT_EDITOR: &str = "/usr/bin/vi"; #[derive(Subcommand, Debug)] pub enum ConfigCommands { - /// Generates an installation profile with the current settings. + /// Generate an installation profile with the current settings. /// /// It is possible that many configuration settings do not have a value. Those settings /// are not included in the output. @@ -19,16 +25,23 @@ pub enum ConfigCommands { /// The output of command can be used as input for the "agama config load". Show, - /// Reads and loads a profile from the standard input. + /// Read and load a profile from the standard input. Load, -} -pub enum ConfigAction { - Show, - Load, + /// Edit and update installation option using an external editor. + /// + /// The changes are not applied if the editor exits with an error code. + /// + /// If an editor is not specified, it honors the EDITOR environment variable. It falls back to + /// `/usr/bin/vi` as a last resort. + Edit { + /// Editor command (including additional arguments if needed) + #[arg(short, long)] + editor: Option, + }, } -pub async fn run(subcommand: ConfigCommands, format: Format) -> anyhow::Result<()> { +pub async fn run(subcommand: ConfigCommands) -> anyhow::Result<()> { let Some(token) = AuthToken::find() else { println!("You need to login for generating a valid token"); return Ok(()); @@ -37,26 +50,68 @@ pub async fn run(subcommand: ConfigCommands, format: Format) -> anyhow::Result<( let client = agama_lib::http_client(token.as_str())?; let store = SettingsStore::new(connection().await?, client).await?; - let command = parse_config_command(subcommand)?; - match command { - ConfigAction::Show => { + match subcommand { + ConfigCommands::Show => { let model = store.load().await?; - print(model, std::io::stdout(), format)?; + let json = serde_json::to_string_pretty(&model)?; + println!("{}", json); Ok(()) } - ConfigAction::Load => { + ConfigCommands::Load => { let mut stdin = io::stdin(); let mut contents = String::new(); stdin.read_to_string(&mut contents)?; let result: InstallSettings = serde_json::from_str(&contents)?; Ok(store.store(&result).await?) } + ConfigCommands::Edit { editor } => { + let model = store.load().await?; + let editor = editor + .or_else(|| std::env::var("EDITOR").ok()) + .unwrap_or(DEFAULT_EDITOR.to_string()); + let result = edit(&model, &editor)?; + tokio::spawn(async move { + show_progress().await.unwrap(); + }); + store.store(&result).await?; + Ok(()) + } } } -fn parse_config_command(subcommand: ConfigCommands) -> Result { - match subcommand { - ConfigCommands::Show => Ok(ConfigAction::Show), - ConfigCommands::Load => Ok(ConfigAction::Load), +/// Edit the installation settings using an external editor. +/// +/// If the editor does not return a successful error code, it returns an error. +/// +/// * `model`: current installation settings. +/// * `editor`: editor command. +fn edit(model: &InstallSettings, editor: &str) -> anyhow::Result { + let content = serde_json::to_string_pretty(model)?; + let mut file = Builder::new().suffix(".json").tempfile()?; + let path = PathBuf::from(file.path()); + write!(file, "{}", content)?; + + let mut command = editor_command(&editor); + let status = command.arg(path.as_os_str()).status()?; + if status.success() { + return Ok(InstallSettings::from_file(path)?); } + + Err(anyhow!( + "Ignoring the changes becase the editor was closed with an error code." + )) +} + +/// Return the Command to run the editor. +/// +/// Separate the program and the arguments and build a Command struct. +/// +/// * `command`: command to run as editor. +fn editor_command(command: &str) -> Command { + let mut parts = command.split_whitespace(); + let program = parts.next().unwrap_or(DEFAULT_EDITOR); + + let mut command = Command::new(program); + command.args(parts.collect::>()); + command } diff --git a/rust/agama-cli/src/main.rs b/rust/agama-cli/src/main.rs index 87cf6ba4bc..e3bde3f913 100644 --- a/rust/agama-cli/src/main.rs +++ b/rust/agama-cli/src/main.rs @@ -5,7 +5,6 @@ mod commands; mod config; mod error; mod logs; -mod printers; mod profile; mod progress; mod questions; @@ -18,7 +17,6 @@ use auth::run as run_auth_cmd; use commands::Commands; use config::run as run_config_cmd; use logs::run as run_logs_cmd; -use printers::Format; use profile::run as run_profile_cmd; use progress::InstallerProgress; use questions::run as run_questions_cmd; @@ -39,10 +37,6 @@ use std::{ struct Cli { #[command(subcommand)] pub command: Commands, - - /// Format output - #[arg(value_enum, short, long, default_value_t = Format::Json)] - pub format: Format, } async fn probe() -> anyhow::Result<()> { @@ -129,7 +123,7 @@ async fn run_command(cli: Cli) -> anyhow::Result<()> { Commands::Config(subcommand) => { let manager = build_manager().await?; wait_for_services(&manager).await?; - run_config_cmd(subcommand, cli.format).await + run_config_cmd(subcommand).await } Commands::Probe => { let manager = build_manager().await?; diff --git a/rust/agama-cli/src/printers.rs b/rust/agama-cli/src/printers.rs deleted file mode 100644 index 4fe28e9c8a..0000000000 --- a/rust/agama-cli/src/printers.rs +++ /dev/null @@ -1,74 +0,0 @@ -use serde::Serialize; -use std::fmt::Debug; -use std::io::Write; - -/// Prints the content using the given format -/// -/// # Example -/// -///```rust -/// use agama_lib::users; -/// use agama_cli::printers::{print, Format}; -/// use std::io; -/// -/// let user = users::User { login: "jane doe".to_string() }; -/// print(user, io::stdout(), Some(Format::Json)) -/// .expect("Something went wrong!") -/// ``` -pub fn print(content: T, writer: W, format: Format) -> anyhow::Result<()> -where - T: serde::Serialize + Debug, - W: Write, -{ - let printer: Box> = match format { - Format::Json => Box::new(JsonPrinter { content, writer }), - Format::Yaml => Box::new(YamlPrinter { content, writer }), - _ => Box::new(TextPrinter { content, writer }), - }; - printer.print() -} - -/// Supported output formats -#[derive(clap::ValueEnum, Clone)] -pub enum Format { - Json, - Yaml, - Text, -} - -pub trait Printer { - fn print(self: Box) -> anyhow::Result<()>; -} - -pub struct JsonPrinter { - content: T, - writer: W, -} - -impl Printer for JsonPrinter { - fn print(mut self: Box) -> anyhow::Result<()> { - let json = serde_json::to_string(&self.content)?; - Ok(writeln!(self.writer, "{}", json)?) - } -} -pub struct TextPrinter { - content: T, - writer: W, -} - -impl Printer for TextPrinter { - fn print(mut self: Box) -> anyhow::Result<()> { - Ok(writeln!(self.writer, "{:?}", &self.content)?) - } -} - -pub struct YamlPrinter { - content: T, - writer: W, -} - -impl Printer for YamlPrinter { - fn print(self: Box) -> anyhow::Result<()> { - Ok(serde_yaml::to_writer(self.writer, &self.content)?) - } -} diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 714a954bb5..6216895e1b 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Thu Jun 20 12:58:32 UTC 2024 - Imobach Gonzalez Sosa + +- Add a new "config edit" command allows editing installation + settings using an external editor (gh#openSUSE/agama#1360). +- Remove the "--format" option (gh#openSUSE/agama#1360). + ------------------------------------------------------------------- Thu Jun 20 05:32:42 UTC 2024 - Imobach Gonzalez Sosa