use console::Style;
use super::wizard::WizardResult;
pub(super) fn print_step_header(step: u8, title: &str) {
let bold = Style::new().bold();
eprintln!("{}", bold.apply_to(format!("Step {step}/8: {title}")));
eprintln!();
}
pub(super) fn print_step_subtitle(lines: &[&str]) {
let dim = Style::new().dim();
for line in lines {
eprintln!("{}", dim.apply_to(*line));
}
eprintln!();
}
pub(super) fn print_welcome_banner() {
let bold = Style::new().bold();
let dim = Style::new().dim();
eprintln!();
eprintln!("{}", bold.apply_to("Welcome to Tuitbot Setup"));
eprintln!(
"{}",
dim.apply_to("This wizard will create your configuration in 8 steps.")
);
eprintln!();
eprintln!("{}", dim.apply_to("You'll need:"));
eprintln!(
"{}",
dim.apply_to(" - X API credentials (https://developer.x.com)")
);
eprintln!("{}", dim.apply_to(" - Your product/business details"));
eprintln!(
"{}",
dim.apply_to(" - An LLM API key (OpenAI, Anthropic, or Ollama)")
);
eprintln!();
eprintln!(
"{}",
dim.apply_to("Tip: Defaults shown in [brackets] — just press Enter to accept them.")
);
eprintln!();
}
pub(super) fn print_summary(result: &WizardResult) {
let bold = Style::new().bold();
let dim = Style::new().dim();
eprintln!("{}", bold.apply_to("Configuration Summary"));
eprintln!("{}", dim.apply_to("─────────────────────"));
eprintln!(" X API Client ID: {}", result.client_id);
eprintln!(
" Client Secret: {}",
if result.client_secret.is_some() {
"(set)"
} else {
"(none)"
}
);
eprintln!();
eprintln!(" Product: {}", result.product_name);
eprintln!(" Description: {}", result.product_description);
eprintln!(
" URL: {}",
result.product_url.as_deref().unwrap_or("(none)")
);
eprintln!(" Audience: {}", result.target_audience);
eprintln!(
" Keywords: {}",
result.product_keywords.join(", ")
);
eprintln!(" Topics: {}", result.industry_topics.join(", "));
eprintln!();
eprintln!(
" Brand Voice: {}",
result.brand_voice.as_deref().unwrap_or("(default)")
);
eprintln!(
" Reply Style: {}",
result.reply_style.as_deref().unwrap_or("(default)")
);
eprintln!(
" Content Style: {}",
result.content_style.as_deref().unwrap_or("(default)")
);
eprintln!();
eprintln!(
" Opinions: {}",
if result.persona_opinions.is_empty() {
"(none)".to_string()
} else {
result.persona_opinions.join(", ")
}
);
eprintln!(
" Experiences: {}",
if result.persona_experiences.is_empty() {
"(none)".to_string()
} else {
result.persona_experiences.join(", ")
}
);
eprintln!(
" Content Pillars: {}",
if result.content_pillars.is_empty() {
"(none)".to_string()
} else {
result.content_pillars.join(", ")
}
);
eprintln!();
eprintln!(
" Target Accounts: {}",
if result.target_accounts.is_empty() {
"(none)".to_string()
} else {
result.target_accounts.join(", ")
}
);
eprintln!(
" Approval Mode: {}",
if result.approval_mode { "yes" } else { "no" }
);
eprintln!();
eprintln!(" Timezone: {}", result.timezone);
eprintln!(
" Active Hours: {}:00 – {}:00",
result.active_hours_start, result.active_hours_end
);
eprintln!(" Active Days: {}", result.active_days.join(", "));
eprintln!();
eprintln!(" LLM Provider: {}", result.llm_provider);
eprintln!(
" API Key: {}",
if result.llm_api_key.is_some() {
"(set)"
} else {
"(none)"
}
);
eprintln!(" Model: {}", result.llm_model);
if let Some(url) = &result.llm_base_url {
eprintln!(" Base URL: {}", url);
}
eprintln!();
}
pub(super) fn print_remaining_steps(steps: &[&str]) {
let bold = Style::new().bold();
eprintln!();
eprintln!("{}", bold.apply_to("Remaining steps:"));
for (i, step) in steps.iter().enumerate() {
eprintln!(" {}. {step}", i + 1);
}
}
pub(super) fn print_quickstart_banner() {
let bold = Style::new().bold();
let dim = Style::new().dim();
eprintln!();
eprintln!("{}", bold.apply_to("Tuitbot Quick Setup"));
eprintln!("{}", dim.apply_to("───────────────────"));
eprintln!("{}", dim.apply_to("Before we start, have these ready:"));
eprintln!(
"{}",
dim.apply_to(" • X API Client ID — from https://developer.x.com")
);
eprintln!(
"{}",
dim.apply_to(" • An LLM API key — OpenAI, Anthropic, or Ollama (free, local)")
);
eprintln!();
eprintln!(
"{}",
dim.apply_to("5 questions to get you running. Use --advanced for full configuration.")
);
eprintln!();
}
pub(super) fn print_x_api_guide() {
let dim = Style::new().dim();
eprintln!();
eprintln!(
"{}",
dim.apply_to("To get your Client ID (takes ~2 minutes):")
);
eprintln!(
"{}",
dim.apply_to(" 1. Go to https://developer.x.com/en/portal/dashboard")
);
eprintln!(
"{}",
dim.apply_to(" 2. Create a Project and App (or select an existing one)")
);
eprintln!(
"{}",
dim.apply_to(" 3. Under \"User authentication settings\", enable OAuth 2.0:")
);
eprintln!(
"{}",
dim.apply_to(" Type: \"Native App\" — Callback URL: http://127.0.0.1:8080/callback")
);
eprintln!(
"{}",
dim.apply_to(" 4. Copy the Client ID from the \"Keys and tokens\" tab")
);
eprintln!();
}
pub(super) fn print_llm_validation_ok(provider: &str, model: &str, latency_ms: u128) {
let green = Style::new().green();
eprintln!(
"{}",
green.apply_to(format!(
" ✓ Connected to {provider} ({model}, {latency_ms}ms)"
))
);
}
pub(super) fn print_llm_validation_fail(provider: &str, error: &str) {
let yellow = Style::new().yellow();
eprintln!(
"{}",
yellow.apply_to(format!(" ⚠ Could not connect to {provider}: {error}"))
);
}
#[cfg(test)]
mod tests {
use super::*;
fn test_wizard_result() -> WizardResult {
WizardResult {
client_id: "test-client".to_string(),
client_secret: Some("secret".to_string()),
product_name: "TestApp".to_string(),
product_description: "Test product".to_string(),
product_url: Some("https://test.com".to_string()),
target_audience: "developers".to_string(),
product_keywords: vec!["rust".to_string(), "cli".to_string()],
industry_topics: vec!["programming".to_string()],
brand_voice: Some("Friendly".to_string()),
reply_style: Some("Helpful".to_string()),
content_style: Some("Practical".to_string()),
persona_opinions: vec!["Rust is great".to_string()],
persona_experiences: vec!["Built CLI tools".to_string()],
content_pillars: vec!["Systems programming".to_string()],
target_accounts: vec!["user1".to_string()],
approval_mode: true,
timezone: "UTC".to_string(),
active_hours_start: 8,
active_hours_end: 22,
active_days: vec!["Mon".to_string(), "Tue".to_string()],
llm_provider: "openai".to_string(),
llm_api_key: Some("sk-test".to_string()),
llm_model: "gpt-4o-mini".to_string(),
llm_base_url: None,
}
}
fn minimal_wizard_result() -> WizardResult {
WizardResult {
client_id: "cid".to_string(),
client_secret: None,
product_name: "App".to_string(),
product_description: String::new(),
product_url: None,
target_audience: String::new(),
product_keywords: vec!["kw".to_string()],
industry_topics: vec![],
brand_voice: None,
reply_style: None,
content_style: None,
persona_opinions: vec![],
persona_experiences: vec![],
content_pillars: vec![],
target_accounts: vec![],
approval_mode: true,
timezone: "UTC".to_string(),
active_hours_start: 8,
active_hours_end: 22,
active_days: vec![
"Mon".to_string(),
"Tue".to_string(),
"Wed".to_string(),
"Thu".to_string(),
"Fri".to_string(),
"Sat".to_string(),
"Sun".to_string(),
],
llm_provider: "ollama".to_string(),
llm_api_key: None,
llm_model: "llama3.2".to_string(),
llm_base_url: Some("http://localhost:11434/v1".to_string()),
}
}
#[test]
fn print_step_header_does_not_panic() {
print_step_header(1, "Test Step");
print_step_header(8, "Last Step");
}
#[test]
fn print_step_subtitle_does_not_panic() {
print_step_subtitle(&["Line 1", "Line 2"]);
print_step_subtitle(&[]);
}
#[test]
fn print_welcome_banner_does_not_panic() {
print_welcome_banner();
}
#[test]
fn print_quickstart_banner_does_not_panic() {
print_quickstart_banner();
}
#[test]
fn print_x_api_guide_does_not_panic() {
print_x_api_guide();
}
#[test]
fn print_summary_does_not_panic() {
print_summary(&test_wizard_result());
}
#[test]
fn print_summary_minimal_does_not_panic() {
print_summary(&minimal_wizard_result());
}
#[test]
fn print_remaining_steps_does_not_panic() {
print_remaining_steps(&["tuitbot auth", "tuitbot test", "tuitbot run"]);
print_remaining_steps(&[]);
}
#[test]
fn print_llm_validation_ok_does_not_panic() {
print_llm_validation_ok("openai", "gpt-4o-mini", 150);
}
#[test]
fn print_llm_validation_fail_does_not_panic() {
print_llm_validation_fail("anthropic", "connection refused");
}
#[test]
fn summary_client_secret_display_set() {
let result = test_wizard_result();
let display = if result.client_secret.is_some() {
"(set)"
} else {
"(none)"
};
assert_eq!(display, "(set)");
}
#[test]
fn summary_client_secret_display_none() {
let result = minimal_wizard_result();
let display = if result.client_secret.is_some() {
"(set)"
} else {
"(none)"
};
assert_eq!(display, "(none)");
}
#[test]
fn summary_product_url_display() {
let result = test_wizard_result();
let display = result.product_url.as_deref().unwrap_or("(none)");
assert_eq!(display, "https://test.com");
}
#[test]
fn summary_product_url_none() {
let result = minimal_wizard_result();
let display = result.product_url.as_deref().unwrap_or("(none)");
assert_eq!(display, "(none)");
}
#[test]
fn summary_approval_mode_display() {
let display_true = if true { "yes" } else { "no" };
assert_eq!(display_true, "yes");
let display_false = if false { "yes" } else { "no" };
assert_eq!(display_false, "no");
}
#[test]
fn summary_empty_vec_display() {
let opinions: Vec<String> = vec![];
let display = if opinions.is_empty() {
"(none)".to_string()
} else {
opinions.join(", ")
};
assert_eq!(display, "(none)");
}
#[test]
fn summary_nonempty_vec_display() {
let opinions = vec!["opinion1".to_string(), "opinion2".to_string()];
let display = if opinions.is_empty() {
"(none)".to_string()
} else {
opinions.join(", ")
};
assert_eq!(display, "opinion1, opinion2");
}
#[test]
fn summary_active_hours_format() {
let result = test_wizard_result();
let hours = format!(
"{}:00 - {}:00",
result.active_hours_start, result.active_hours_end
);
assert_eq!(hours, "8:00 - 22:00");
}
#[test]
fn summary_api_key_display() {
let result = test_wizard_result();
let display = if result.llm_api_key.is_some() {
"(set)"
} else {
"(none)"
};
assert_eq!(display, "(set)");
}
#[test]
fn summary_base_url_display() {
let result = minimal_wizard_result();
if let Some(url) = &result.llm_base_url {
assert_eq!(url, "http://localhost:11434/v1");
} else {
panic!("expected base_url to be set for ollama");
}
}
}
pub(super) fn print_quickstart_summary(result: &WizardResult) {
let bold = Style::new().bold();
let dim = Style::new().dim();
eprintln!();
eprintln!("{}", bold.apply_to("Configuration Summary"));
eprintln!("{}", dim.apply_to("─────────────────────"));
eprintln!(" Product: {}", result.product_name);
eprintln!(" Keywords: {}", result.product_keywords.join(", "));
eprintln!(
" LLM: {} ({})",
result.llm_provider, result.llm_model
);
eprintln!(" X API: {} (Client ID set)", result.client_id);
eprintln!(" Approval: on (all posts queued for review)");
eprintln!();
eprintln!(
" {}",
dim.apply_to("Defaults applied: UTC schedule, no brand voice, no persona.")
);
eprintln!(
" {}",
dim.apply_to("Customize later: tuitbot init --advanced or tuitbot settings")
);
eprintln!();
}