Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::constants::{exit_codes, verbosity};
use crate::prompt::PromptTheme;
use clap::{error::ErrorKind, CommandFactory, Parser, ValueEnum};
use log::LevelFilter;
use std::fmt::Display;
Expand Down Expand Up @@ -34,6 +35,36 @@ impl Display for SkipConfirm {
}
}

/// Available terminal prompt themes.
#[derive(Debug, Clone, ValueEnum, Copy, PartialEq, Eq, Default)]
#[value(rename_all = "lowercase")]
pub enum PromptThemeArg {
/// dialoguer-like default colorful theme.
Classic,
/// Baker-branded prompt theme.
#[default]
Fancy,
}

impl Display for PromptThemeArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
PromptThemeArg::Classic => "classic",
PromptThemeArg::Fancy => "fancy",
};
write!(f, "{s}")
}
}

impl From<PromptThemeArg> for PromptTheme {
fn from(value: PromptThemeArg) -> Self {
match value {
PromptThemeArg::Classic => PromptTheme::Classic,
PromptThemeArg::Fancy => PromptTheme::Fancy,
}
}
}

/// CLI arguments for Baker.
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
Expand Down Expand Up @@ -74,6 +105,10 @@ pub struct Args {
/// Preview actions without touching the filesystem.
#[arg(long = "dry-run")]
pub dry_run: bool,

/// Prompt theme (`classic` or `fancy`).
#[arg(long = "theme", value_enum, default_value_t = PromptThemeArg::Fancy)]
pub theme: PromptThemeArg,
}

/// Parse command line arguments with custom handling for missing required inputs.
Expand Down Expand Up @@ -133,6 +168,12 @@ mod tests {
assert_eq!(SkipConfirm::Hooks.to_string(), "hooks");
}

#[test]
fn display_prompt_theme_variants() {
assert_eq!(PromptThemeArg::Classic.to_string(), "classic");
assert_eq!(PromptThemeArg::Fancy.to_string(), "fancy");
}

#[test]
fn parses_full_feature_flags() {
use clap::Parser;
Expand All @@ -148,6 +189,8 @@ mod tests {
"all,overwrite",
"--non-interactive",
"--dry-run",
"--theme",
"classic",
]);
assert_eq!(args.template, "template_dir");
assert_eq!(args.output_dir, PathBuf::from("output_dir"));
Expand All @@ -158,6 +201,7 @@ mod tests {
assert!(args.skip_confirms.contains(&SkipConfirm::Overwrite));
assert!(args.non_interactive);
assert!(args.dry_run);
assert_eq!(args.theme, PromptThemeArg::Classic);
}

#[test]
Expand All @@ -174,4 +218,11 @@ mod tests {
assert_eq!(args.output_dir, PathBuf::from("output_dir"));
assert_eq!(args.answers_file, Some(PathBuf::from("/path/to/answers.json")));
}

#[test]
fn parses_minimal_args_with_default_theme() {
use clap::Parser;
let args = Args::parse_from(["baker", "template_dir", "output_dir"]);
assert_eq!(args.theme, PromptThemeArg::Fancy);
}
}
2 changes: 1 addition & 1 deletion src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ pub mod hooks;
pub mod processor;
pub mod runner;

pub use args::{get_args, get_log_level_from_verbose, Args, SkipConfirm};
pub use args::{get_args, get_log_level_from_verbose, Args, PromptThemeArg, SkipConfirm};
pub use runner::run;
5 changes: 4 additions & 1 deletion src/cli/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
error::{Error, Result},
ignore::parse_bakerignore_file,
loader::get_template,
prompt::confirm,
prompt::{confirm, set_prompt_theme},
renderer::TemplateRenderer,
template::{get_template_engine, processor::TemplateProcessor},
};
Expand Down Expand Up @@ -480,6 +480,7 @@ fn log_dry_run_action<A: AsRef<Path>>(action: &str, target: A) {
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::PromptThemeArg;
use serde_json::json;
use tempfile::TempDir;

Expand All @@ -494,6 +495,7 @@ mod tests {
skip_confirms: Vec::new(),
non_interactive: false,
dry_run: false,
theme: PromptThemeArg::Fancy,
}
}

Expand Down Expand Up @@ -607,6 +609,7 @@ fn completion_message(dry_run: bool, output_root: &Path) -> String {

/// Main entry point for CLI execution
pub fn run(args: Args) -> Result<()> {
set_prompt_theme(args.theme.into());
let runner = Runner::new(args);
runner.run()
}
57 changes: 47 additions & 10 deletions src/prompt/dialoguer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,51 @@ use super::interface::{
ConfirmationConfig, MultipleChoiceConfig, SecretConfig, SingleChoiceConfig,
StructuredDataConfig, TextPromptConfig,
};
use super::theme::PromptTheme;
use crate::{error::Result, prompt::parser::DataParser};
use dialoguer::{Confirm, Editor, Input, MultiSelect, Password, Select};
use dialoguer::console::{style, Style};
use dialoguer::{
theme::ColorfulTheme, Confirm, Editor, Input, MultiSelect, Password, Select,
};
use serde_json::Value;

/// Default terminal-backed prompt provider implemented with `dialoguer`.
pub struct DialoguerPrompter;
pub struct DialoguerPrompter {
theme: ColorfulTheme,
}

impl DialoguerPrompter {
pub fn new() -> Self {
Self
Self::with_theme(PromptTheme::default())
}

pub fn with_theme(theme: PromptTheme) -> Self {
let theme = match theme {
PromptTheme::Classic => ColorfulTheme::default(),
PromptTheme::Fancy => Self::baker_theme(),
};
Self { theme }
}

fn baker_theme() -> ColorfulTheme {
ColorfulTheme {
defaults_style: Style::new().for_stderr().cyan(),
prompt_style: Style::new().for_stderr().bold().cyan(),
prompt_prefix: style("baker".to_string()).for_stderr().cyan(),
prompt_suffix: style(">".to_string()).for_stderr().black().bright(),
success_prefix: style("ok".to_string()).for_stderr().green(),
success_suffix: style(":".to_string()).for_stderr().black().bright(),
error_prefix: style("err".to_string()).for_stderr().red(),
values_style: Style::new().for_stderr().green(),
active_item_style: Style::new().for_stderr().bold().green(),
active_item_prefix: style(">".to_string()).for_stderr().green(),
inactive_item_prefix: style(" ".to_string()).for_stderr(),
checked_item_prefix: style("[x]".to_string()).for_stderr().green(),
unchecked_item_prefix: style("[ ]".to_string()).for_stderr().yellow(),
picked_item_prefix: style(">".to_string()).for_stderr().green(),
unpicked_item_prefix: style(" ".to_string()).for_stderr(),
..ColorfulTheme::default()
}
}
}

Expand All @@ -41,7 +76,9 @@ impl super::interface::TextPrompter for DialoguerPrompter {

impl super::interface::SingleChoicePrompter for DialoguerPrompter {
fn prompt_single_choice(&self, config: &SingleChoiceConfig) -> Result<usize> {
let mut select = Select::new().with_prompt(&config.prompt).items(&config.choices);
let mut select = Select::with_theme(&self.theme)
.with_prompt(&config.prompt)
.items(&config.choices);

if let Some(default_index) = config.default_index {
select = select.default(default_index);
Expand All @@ -56,7 +93,7 @@ impl super::interface::MultipleChoicePrompter for DialoguerPrompter {
&self,
config: &MultipleChoiceConfig,
) -> Result<Vec<usize>> {
let indices = MultiSelect::new()
let indices = MultiSelect::with_theme(&self.theme)
.with_prompt(&config.prompt)
.items(&config.choices)
.defaults(&config.defaults)
Expand All @@ -68,7 +105,7 @@ impl super::interface::MultipleChoicePrompter for DialoguerPrompter {

impl super::interface::ConfirmationPrompter for DialoguerPrompter {
fn prompt_confirmation(&self, config: &ConfirmationConfig) -> Result<bool> {
let result = Confirm::new()
let result = Confirm::with_theme(&self.theme)
.with_prompt(&config.prompt)
.default(config.default)
.interact()?;
Expand All @@ -84,7 +121,7 @@ impl super::interface::StructuredDataPrompter for DialoguerPrompter {

let options = vec!["Enter in terminal", "Open editor"];

let selection = Select::new()
let selection = Select::with_theme(&self.theme)
.with_prompt(&config.prompt)
.items(&options)
.default(0)
Expand All @@ -109,7 +146,7 @@ impl DialoguerPrompter {
prompt: &str,
secret_config: &SecretConfig,
) -> Result<String> {
let mut password = Password::new().with_prompt(prompt);
let mut password = Password::with_theme(&self.theme).with_prompt(prompt);

if secret_config.confirm {
let error_message = if secret_config.mismatch_error.is_empty() {
Expand All @@ -127,7 +164,7 @@ impl DialoguerPrompter {

/// Handle regular text input
fn prompt_regular_text(&self, prompt: &str, default: &str) -> Result<String> {
Ok(Input::new()
Ok(Input::with_theme(&self.theme)
.with_prompt(prompt)
.default(default.to_string())
.interact_text()?)
Expand All @@ -139,7 +176,7 @@ impl DialoguerPrompter {
default_content: &str,
is_yaml: bool,
) -> Result<Value> {
let content: String = Input::new()
let content: String = Input::with_theme(&self.theme)
.with_prompt("Enter content")
.default(default_content.to_string())
.interact_text()?;
Expand Down
7 changes: 6 additions & 1 deletion src/prompt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@
//! - [`handler`]: orchestration that chooses which prompt to display.
//! - [`context`]: immutable data passed to prompt providers.
//! - [`provider`]: convenience helpers exposed to the rest of the crate.
//! - [`theme`]: prompt appearance presets.

pub mod context;
pub mod dialoguer;
pub mod handler;
pub mod interface;
pub mod parser;
pub mod provider;
pub mod theme;

pub use context::PromptContext;
pub use interface::*;
pub use provider::{ask_question, confirm, get_prompt_provider, Prompter};
pub use provider::{
ask_question, confirm, get_prompt_provider, set_prompt_theme, Prompter,
};
pub use theme::PromptTheme;
33 changes: 31 additions & 2 deletions src/prompt/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,49 @@ use crate::{
error::Result,
};
use serde_json::Value;
use std::sync::atomic::{AtomicU8, Ordering};

use super::{
context::PromptContext, dialoguer::DialoguerPrompter, handler::PromptHandler,
interface::PromptProvider,
interface::PromptProvider, theme::PromptTheme,
};

const PROMPT_THEME_CLASSIC: u8 = 0;
const PROMPT_THEME_FANCY: u8 = 1;

static ACTIVE_PROMPT_THEME: AtomicU8 = AtomicU8::new(PROMPT_THEME_FANCY);

/// Trait implemented by prompt backends that can render a question via a [`PromptContext`].
pub trait Prompter<'a> {
fn prompt(&self, prompt_context: &PromptContext<'a>) -> Result<Value>;
}

/// Convenience function to construct the default terminal prompt provider.
pub fn get_prompt_provider() -> impl PromptProvider {
DialoguerPrompter::new()
DialoguerPrompter::with_theme(get_prompt_theme())
}

/// Sets the active prompt theme for all interactive dialogs in the current process.
pub fn set_prompt_theme(theme: PromptTheme) {
ACTIVE_PROMPT_THEME.store(theme_to_u8(theme), Ordering::Relaxed);
}

fn get_prompt_theme() -> PromptTheme {
theme_from_u8(ACTIVE_PROMPT_THEME.load(Ordering::Relaxed))
}

const fn theme_to_u8(theme: PromptTheme) -> u8 {
match theme {
PromptTheme::Classic => PROMPT_THEME_CLASSIC,
PromptTheme::Fancy => PROMPT_THEME_FANCY,
}
}

const fn theme_from_u8(raw: u8) -> PromptTheme {
match raw {
PROMPT_THEME_CLASSIC => PromptTheme::Classic,
_ => PromptTheme::Fancy,
}
}

/// High-level helper that collects an answer for a single configuration question.
Expand Down
9 changes: 9 additions & 0 deletions src/prompt/theme.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// Visual themes for interactive terminal prompts.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PromptTheme {
/// Uses dialoguer's default colorful look.
Classic,
/// Uses Baker-branded colors and prefixes.
#[default]
Fancy,
}
5 changes: 4 additions & 1 deletion tests/gitea_integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! Run these tests with: `cargo test --test gitea_integration_tests -- --ignored`

use baker::cli::SkipConfirm::All;
use baker::cli::{run, Args};
use baker::cli::{run, Args, PromptThemeArg};
use git2::{Cred, PushOptions, RemoteCallbacks, Repository, Signature};
use reqwest::blocking::Client;
use serde_json::json;
Expand Down Expand Up @@ -676,6 +676,7 @@ fn test_jsonschema_file_template_from_gitea() {
skip_confirms: vec![All],
non_interactive: true,
dry_run: false,
theme: PromptThemeArg::Fancy,
};

run(args).expect("Baker run failed");
Expand Down Expand Up @@ -876,6 +877,7 @@ fn test_template_with_submodule_schema_file() {
skip_confirms: vec![All],
non_interactive: true,
dry_run: false,
theme: PromptThemeArg::Fancy,
};

run(args).expect("Baker run failed - submodule schema_file should be accessible");
Expand Down Expand Up @@ -1083,6 +1085,7 @@ Entities count: {{ entities | length }}
skip_confirms: vec![All],
non_interactive: true,
dry_run: false,
theme: PromptThemeArg::Fancy,
};

// Run baker - this should succeed because submodules are now initialized
Expand Down
Loading
Loading