diff --git a/src/cli.rs b/src/cli.rs index f5875db..93671a8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,7 @@ //! The CLI for `st`. use crate::{ - config::{StConfig, DEFAULT_CONFIG_PRETTY}, + config::{prompt_for_configuration, StConfig}, ctx::StContext, errors::{StError, StResult}, subcommands::Subcommands, @@ -35,32 +35,19 @@ impl Cli { let repo = crate::git::active_repository().ok_or(StError::NotAGitRepository)?; let config = Self::load_cfg_or_initialize()?; let context = Self::load_ctx_or_initialize(config, &repo)?; - self.subcommand.run(context).await } - /// Loads the [StConfig]. If the config does not exist, prompts the user to set up the - /// `st` for the first time. + /// Loads the [StConfig]. If the config does not exist or is the default config, prompts + /// the user to set up the `st` for the first time. /// /// ## Returns /// - `Result` - The global `st` config. pub(crate) fn load_cfg_or_initialize() -> StResult { // Load the global configuration for `st`, or initialize it if it doesn't exist. match StConfig::try_load()? { - Some(config) => Ok(config), - None => { - let setup_message = format!( - "No global configuration found for `{}`. Set up the environment.", - Blue.paint("st") - ); - - // Print the default config. - let ser_cfg = inquire::Editor::new(&setup_message) - .with_file_extension(".toml") - .with_predefined_text(DEFAULT_CONFIG_PRETTY) - .prompt()?; - Ok(toml::from_str(&ser_cfg)?) - } + Some(config) if config.validate().is_ok() => Ok(config), + _ => prompt_for_configuration(None), } } diff --git a/src/config.rs b/src/config.rs index 941e6cd..de4ea45 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ //! Contains the global configuration for `st`. -use crate::constants::ST_CFG_FILE_NAME; +use crate::{constants::ST_CFG_FILE_NAME, errors::StResult}; +use nu_ansi_term::Color; use serde::{Deserialize, Serialize}; use std::{fs, io, path::PathBuf}; use thiserror::Error; @@ -36,6 +37,14 @@ impl StConfig { Err(_) => Ok(None), } } + + /// Validates the configuration. + pub fn validate(&self) -> Result<(), StConfigError> { + if self.github_token.is_empty() { + return Err(StConfigError::MissingField("github_token".to_string())); + } + Ok(()) + } } impl Drop for StConfig { @@ -51,6 +60,35 @@ pub enum StConfigError { /// Failed to load the configuration file. #[error("Failed to load the configuration file: {}", .0)] FailedToLoad(io::Error), + /// Missing a reqired field. + #[error("Missing required field: {}", .0)] + MissingField(String), +} + +/// Prompts the user to set up the global configuration for `st`. +/// +/// ## Returns +/// - `Result` - The newly created global `st` config. +pub fn prompt_for_configuration(existing_config: Option<&str>) -> StResult { + let setup_text = format!( + "{} configuration found for `{}`. Set up the environment.", + existing_config.map(|_| "Existing").unwrap_or("No"), + Color::Blue.paint("st") + ); + + // Use the provided predefined text or fall back to the default. + let default_text = existing_config.unwrap_or(DEFAULT_CONFIG_PRETTY); + + // Print the default config. + let ser_cfg = inquire::Editor::new(&setup_text) + .with_file_extension(".toml") + .with_predefined_text(default_text) + .prompt()?; + + let config: StConfig = toml::from_str(&ser_cfg)?; + config.validate()?; + + Ok(config) } #[cfg(test)] diff --git a/src/errors.rs b/src/errors.rs index 6162446..716b134 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -83,6 +83,9 @@ pub enum StError { /// A [std::fmt::Write] error occurred. #[error("🖋️ write error: {}", .0)] WriteError(#[from] std::fmt::Error), + /// A [toml::ser::Error] occurred. + #[error("🍅 toml serialization error: {}", .0)] + TomlSerializationError(#[from] toml::ser::Error), /// A [toml::de::Error] occurred. #[error("🍅 toml decoding error: {}", .0)] TomlDecodingError(#[from] toml::de::Error), diff --git a/src/subcommands/local/config.rs b/src/subcommands/local/config.rs new file mode 100644 index 0000000..711990f --- /dev/null +++ b/src/subcommands/local/config.rs @@ -0,0 +1,17 @@ +//! `config` subcommand. + +use crate::{config::prompt_for_configuration, ctx::StContext, errors::StResult}; + +#[derive(Debug, Clone, Eq, PartialEq, clap::Args)] +pub struct ConfigCmd; + +impl ConfigCmd { + /// Run the `config` subcommand to force or allow configuration editing. + pub fn run(self, mut ctx: StContext<'_>) -> StResult<()> { + let ser = toml::to_string_pretty(&ctx.cfg)?; + let cfg = prompt_for_configuration(Some(&ser))?; + ctx.cfg = cfg; + + Ok(()) + } +} diff --git a/src/subcommands/local/mod.rs b/src/subcommands/local/mod.rs index 342f995..b83cedc 100644 --- a/src/subcommands/local/mod.rs +++ b/src/subcommands/local/mod.rs @@ -20,3 +20,6 @@ pub use track::TrackCmd; mod untrack; pub use untrack::UntrackCmd; + +mod config; +pub use config::ConfigCmd; diff --git a/src/subcommands/mod.rs b/src/subcommands/mod.rs index 4f5388a..2a6e8dd 100644 --- a/src/subcommands/mod.rs +++ b/src/subcommands/mod.rs @@ -4,7 +4,9 @@ use crate::{ctx::StContext, errors::StResult}; use clap::Subcommand; mod local; -use local::{CheckoutCmd, CreateCmd, DeleteCmd, LogCmd, RestackCmd, TrackCmd, UntrackCmd}; +use local::{ + CheckoutCmd, ConfigCmd, CreateCmd, DeleteCmd, LogCmd, RestackCmd, TrackCmd, UntrackCmd, +}; mod remote; use remote::{StatusCmd, SubmitCmd, SyncCmd}; @@ -41,6 +43,9 @@ pub enum Subcommands { /// Untrack the passed branch. #[clap(visible_alias = "ut")] Untrack(UntrackCmd), + /// Configure the st application. + #[clap(visible_alias = "cfg")] + Config(ConfigCmd), } impl Subcommands { @@ -59,6 +64,7 @@ impl Subcommands { Self::Log(args) => args.run(ctx), Self::Track(args) => args.run(ctx), Self::Untrack(args) => args.run(ctx), + Self::Config(args) => args.run(ctx), } } }