use glyphs::{style, gradient, Color};
use scoria::{Spinner, SpinnerStyle, Progress, ProgressStyle};
use std::io::{stdout, Write};
use glyphs::Color as GlyphsColor;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
pub fn brand_color(color: molten_brand::Color) -> Color {
let rgb = color.to_rgb();
Color::Rgb { r: rgb.r, g: rgb.g, b: rgb.b }
}
pub mod colors {
use glyphs::Color;
pub const MOLTEN: Color = Color::Rgb { r: 249, g: 115, b: 22 };
pub const FORGE: Color = Color::Rgb { r: 10, g: 10, b: 10 };
pub const STEEL: Color = Color::Rgb { r: 113, g: 113, b: 122 };
pub const IRON: Color = Color::Rgb { r: 59, g: 130, b: 246 };
pub const EMBER: Color = Color::Rgb { r: 239, g: 68, b: 68 };
pub const GOBLIN: Color = Color::Rgb { r: 124, g: 58, b: 237 };
pub const HEARTH: Color = Color::Rgb { r: 59, g: 130, b: 246 };
pub const ALLOY: Color = Color::Rgb { r: 249, g: 115, b: 22 };
pub const SUCCESS: Color = Color::Rgb { r: 16, g: 185, b: 129 };
pub const GREEN: Color = SUCCESS;
pub const WARNING: Color = Color::Rgb { r: 245, g: 158, b: 11 };
pub const YELLOW: Color = WARNING;
pub const ERROR: Color = Color::Rgb { r: 239, g: 68, b: 68 };
pub const RED: Color = ERROR;
pub const INFO: Color = Color::Rgb { r: 59, g: 130, b: 246 };
pub const CYAN: Color = Color::Rgb { r: 6, g: 182, b: 212 };
pub const PINK: Color = Color::Rgb { r: 255, g: 135, b: 175 };
pub const PURPLE: Color = Color::Rgb { r: 167, g: 139, b: 250 };
pub const VIOLET: Color = Color::Rgb { r: 147, g: 112, b: 219 };
pub const BLUE: Color = Color::Rgb { r: 135, g: 175, b: 255 };
pub const WHITE: Color = Color::Rgb { r: 250, g: 250, b: 250 };
pub const MUTED: Color = Color::Rgb { r: 161, g: 161, b: 170 };
pub const DIM: Color = Color::Rgb { r: 113, g: 113, b: 122 };
pub const BORDER: Color = Color::Rgb { r: 63, g: 63, b: 70 };
pub const SURFACE: Color = Color::Rgb { r: 24, g: 24, b: 27 };
}
pub fn banner() {
println!();
let lines = [
" ██╗ ██╗███████╗██████╗ █████╗ ██╗ ██████╗ ",
" ██║ ██║██╔════╝██╔══██╗██╔══██╗██║ ██╔══██╗",
" ███████║█████╗ ██████╔╝███████║██║ ██║ ██║",
" ██╔══██║██╔══╝ ██╔══██╗██╔══██║██║ ██║ ██║",
" ██║ ██║███████╗██║ ██║██║ ██║███████╗██████╔╝",
" ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═════╝ ",
];
for (i, line) in lines.iter().enumerate() {
let t = i as f32 / (lines.len() - 1) as f32;
let r = (255.0 - t * 43.0) as u8; let g = (135.0 - t * 43.0) as u8; let b = (175.0 + t * 60.0) as u8; println!("{}", style(*line).fg(Color::Rgb { r, g, b }).bold());
}
println!();
println!(
" {}",
gradient("📢 Viral tweets for developers who ship", colors::PINK, colors::PURPLE)
);
println!();
}
pub fn success(msg: &str) {
println!(
" {} {}",
style("✓").fg(colors::GREEN).bold(),
style(msg).fg(colors::WHITE)
);
}
pub fn error(msg: &str) {
eprintln!(
" {} {}",
style("✗").fg(colors::RED).bold(),
style(msg).fg(colors::WHITE)
);
}
pub fn warning(msg: &str) {
println!(
" {} {}",
style("⚠").fg(colors::YELLOW).bold(),
style(msg).fg(colors::MUTED)
);
}
pub fn info(msg: &str) {
println!(
" {} {}",
style("ℹ").fg(colors::CYAN).bold(),
style(msg).fg(colors::MUTED)
);
}
pub fn step(num: usize, msg: &str) {
let bullet = match num {
1 => "❶",
2 => "❷",
3 => "❸",
4 => "❹",
5 => "❺",
_ => "●",
};
println!(
" {} {}",
style(bullet).fg(colors::PURPLE).bold(),
style(msg).fg(colors::WHITE)
);
}
pub fn header(msg: &str) {
println!();
println!(" {}", gradient(msg, colors::PINK, colors::PURPLE));
println!(" {}", style("─".repeat(msg.len().min(50))).fg(colors::BORDER));
}
pub fn subheader(msg: &str) {
println!(
" {} {}",
style("▸").fg(colors::PINK).bold(),
style(msg).fg(colors::VIOLET)
);
}
pub fn tweet_preview(content: &str, chars: usize) {
let width = 54;
let top = format!("╭{}╮", "─".repeat(width));
let bottom = format!("╰{}╯", "─".repeat(width));
let divider = format!("├{}┤", "─".repeat(width));
println!();
println!(" {}", style(&top).fg(colors::BORDER));
println!(" {} {} {}",
style("│").fg(colors::BORDER),
style(format!("{:^width$}", "🐦 Tweet Preview")).fg(colors::PURPLE),
style("│").fg(colors::BORDER)
);
println!(" {}", style(÷r).fg(colors::BORDER));
for line in word_wrap(content, width - 2) {
println!(" {} {} {}",
style("│").fg(colors::BORDER),
style(format!("{:<width$}", line, width = width - 2)).fg(colors::WHITE),
style("│").fg(colors::BORDER)
);
}
println!(" {}", style(÷r).fg(colors::BORDER));
let char_display = format!("{} characters", chars);
let status = if chars <= 280 {
format!("✓ {}", char_display)
} else {
format!("✗ {} (over limit!)", char_display)
};
let status_color = if chars <= 280 { colors::GREEN } else { colors::RED };
println!(" {} {} {}",
style("│").fg(colors::BORDER),
style(format!("{:^width$}", status, width = width - 2)).fg(status_color),
style("│").fg(colors::BORDER)
);
println!(" {}", style(&bottom).fg(colors::BORDER));
println!();
}
fn word_wrap(text: &str, max_width: usize) -> Vec<String> {
let mut lines = Vec::new();
let mut current_line = String::new();
for word in text.split_whitespace() {
if current_line.is_empty() {
current_line = word.to_string();
} else if current_line.len() + 1 + word.len() <= max_width {
current_line.push(' ');
current_line.push_str(word);
} else {
lines.push(current_line);
current_line = word.to_string();
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
if lines.is_empty() {
lines.push(String::new());
}
lines
}
pub fn kv(key: &str, value: &str) {
println!(
" {} {}",
style(format!("{}:", key)).fg(colors::MUTED),
style(value).fg(colors::WHITE)
);
}
pub fn list_item(bullet: &str, text: &str) {
println!(
" {} {}",
style(bullet).fg(colors::PINK),
style(text).fg(colors::WHITE)
);
}
pub fn scheduled_tweets_table(tweets: &[(String, String, String, String)]) {
if tweets.is_empty() {
println!();
println!(" {} {}",
style("📭").fg(colors::MUTED),
style("No scheduled tweets").fg(colors::MUTED).dim()
);
println!();
return;
}
header("📅 Scheduled Tweets");
println!();
let col_id = 8;
let col_time = 18;
let col_status = 10;
let col_content = 35;
let total_width = col_id + col_time + col_status + col_content + 10;
println!(
" {} {} {} {} {} {} {} {}",
style("│").fg(colors::BORDER),
style(format!("{:<col_id$}", "ID")).fg(colors::PURPLE).bold(),
style("│").fg(colors::BORDER),
style(format!("{:<col_time$}", "Time")).fg(colors::PURPLE).bold(),
style("│").fg(colors::BORDER),
style(format!("{:<col_status$}", "Status")).fg(colors::PURPLE).bold(),
style("│").fg(colors::BORDER),
style(format!("{:<col_content$}", "Content")).fg(colors::PURPLE).bold(),
);
println!(" {}", style("─".repeat(total_width)).fg(colors::BORDER));
for (id, time, status, content) in tweets {
let short_id: String = id.chars().take(col_id).collect();
let short_content: String = content.chars().take(col_content - 2).collect();
let (status_icon, status_color) = match status.as_str() {
"Pending" => ("⏳", colors::CYAN),
"Posted" => ("✓", colors::GREEN),
"Failed" => ("✗", colors::RED),
_ => ("○", colors::DIM),
};
println!(
" {} {} {} {} {} {} {} {}",
style("│").fg(colors::BORDER),
style(format!("{:<col_id$}", short_id)).fg(colors::MUTED),
style("│").fg(colors::BORDER),
style(format!("{:<col_time$}", time)).fg(colors::WHITE),
style("│").fg(colors::BORDER),
style(format!("{} {:<w$}", status_icon, status, w = col_status - 2)).fg(status_color),
style("│").fg(colors::BORDER),
style(format!("{:<col_content$}", short_content)).fg(colors::WHITE),
);
}
println!();
}
pub fn queue_stats(pending: usize, posted: usize, failed: usize, cancelled: usize) {
header("📊 Queue Statistics");
println!();
let total = pending + posted + failed + cancelled;
let bar_width = 20;
fn stat_bar(label: &str, icon: &str, count: usize, total: usize, color: Color, bar_width: usize) {
let pct = if total > 0 { (count as f32 / total as f32 * bar_width as f32) as usize } else { 0 };
let filled = "█".repeat(pct);
let empty = "░".repeat(bar_width - pct);
println!(
" {} {} {} {}{}",
style(icon).fg(color),
style(format!("{:<12}", label)).fg(colors::MUTED),
style(format!("{:>3}", count)).fg(color).bold(),
style(&filled).fg(color),
style(&empty).fg(colors::BORDER)
);
}
stat_bar("Pending", "◐", pending, total, colors::CYAN, bar_width);
stat_bar("Posted", "●", posted, total, colors::GREEN, bar_width);
stat_bar("Failed", "○", failed, total, colors::RED, bar_width);
stat_bar("Cancelled", "◌", cancelled, total, colors::MUTED, bar_width);
println!();
println!(
" {} {}",
style("Total:").fg(colors::MUTED),
style(total.to_string()).fg(colors::WHITE).bold()
);
println!();
}
pub fn confirm_prompt(msg: &str) -> bool {
print!(
" {} {} ",
style("?").fg(colors::PURPLE).bold(),
style(msg).fg(colors::WHITE)
);
print!("{}", style("[y/N] ").fg(colors::MUTED));
stdout().flush().ok();
let mut input = String::new();
std::io::stdin().read_line(&mut input).ok();
input.trim().eq_ignore_ascii_case("y")
}
pub fn with_spinner<F, T>(title: &str, f: F) -> T
where
F: FnOnce() -> T,
{
let running = Arc::new(AtomicBool::new(true));
let running_clone = running.clone();
let title_owned = title.to_string();
let spinner_thread = thread::spawn(move || {
let mut spinner = Spinner::new(SpinnerStyle::Dots)
.title(&title_owned)
.color(GlyphsColor::Rgb { r: 212, g: 92, b: 235 }); let mut stdout = stdout();
while running_clone.load(Ordering::Relaxed) {
spinner.tick();
print!("\r {}", spinner.view());
stdout.flush().ok();
thread::sleep(Duration::from_millis(80));
}
print!("\r{}\r", " ".repeat(title_owned.len() + 10));
stdout.flush().ok();
});
let result = f();
running.store(false, Ordering::Relaxed);
spinner_thread.join().ok();
result
}
pub async fn with_spinner_async<F, T>(title: &str, f: F) -> T
where
F: std::future::Future<Output = T>,
{
let running = Arc::new(AtomicBool::new(true));
let running_clone = running.clone();
let title_owned = title.to_string();
let spinner_thread = thread::spawn(move || {
let mut spinner = Spinner::new(SpinnerStyle::Dots)
.title(&title_owned)
.color(GlyphsColor::Rgb { r: 212, g: 92, b: 235 }); let mut stdout = stdout();
while running_clone.load(Ordering::Relaxed) {
spinner.tick();
print!("\r {}", spinner.view());
stdout.flush().ok();
thread::sleep(Duration::from_millis(80));
}
print!("\r{}\r", " ".repeat(title_owned.len() + 10));
stdout.flush().ok();
});
let result = f.await;
running.store(false, Ordering::Relaxed);
spinner_thread.join().ok();
result
}
pub fn progress_bar(current: usize, total: usize, label: &str) {
let pct = if total > 0 { current as f64 / total as f64 } else { 0.0 };
let progress = Progress::new()
.progress(pct)
.style(ProgressStyle::BLOCK.filled_color(GlyphsColor::Rgb { r: 212, g: 92, b: 235 }))
.width(40);
println!(
" {} {} {}",
style(label).fg(colors::MUTED),
progress.view(),
style(format!("{}/{}", current, total)).fg(colors::DIM)
);
}
pub fn generated_tweet(content: &str, chars: usize, event_type: Option<&str>) {
if let Some(evt) = event_type {
println!();
println!(
" {} {}",
style("Event:").fg(colors::MUTED),
style(evt).fg(colors::PURPLE).bold()
);
}
tweet_preview(content, chars);
}
pub fn template_help() {
header("🎨 Available Templates");
println!();
let templates = [
("📦", "crate-release", "<name> <version> <tagline>", "Announce a new crate version"),
("🌟", "open-source", "<name> <description> <url>", "Share an open source project"),
("✨", "feature", "<name> <feature> <benefit>", "Highlight a new feature"),
("🎉", "milestone", "<name> <metric> <value>", "Celebrate a milestone"),
];
for (icon, name, args, desc) in templates {
println!(
" {} {} {}",
style(icon).fg(colors::WHITE),
style(name).fg(colors::PINK).bold(),
style(args).fg(colors::MUTED)
);
println!(
" {}",
style(desc).fg(colors::MUTED).dim()
);
println!();
}
println!(
" {} {}",
style("💡").fg(colors::YELLOW),
style("Tip: Pipe to pbcopy (macOS) or xclip (Linux) to copy").fg(colors::MUTED)
);
}
pub fn config_created(path: &str) {
println!();
println!(
" {} {}",
style("🎉").fg(colors::GREEN),
gradient("Configuration created!", colors::GREEN, colors::CYAN)
);
println!();
let width = path.len() + 4;
println!(" {}", style(format!("╭{}╮", "─".repeat(width))).fg(colors::BORDER));
println!(" {} {} {}",
style("│").fg(colors::BORDER),
style(format!(" {} ", path)).fg(colors::WHITE),
style("│").fg(colors::BORDER)
);
println!(" {}", style(format!("╰{}╯", "─".repeat(width))).fg(colors::BORDER));
println!();
subheader("Next Steps");
println!();
step(1, "Add your Twitter API credentials");
step(2, "Add your LLM API key (Anthropic/OpenAI)");
step(3, "Configure projects to monitor");
println!();
println!(
" {} {} {}",
style("Run").fg(colors::MUTED),
style("herald i").fg(colors::PINK).bold(),
style("to get started interactively!").fg(colors::MUTED)
);
println!();
}
pub fn tweet_posted(url: &str) {
println!();
println!(
" {} {}",
style("🎉").fg(colors::GREEN),
gradient("Tweet posted!", colors::GREEN, colors::CYAN)
);
println!();
println!(
" {} {}",
style("🔗").fg(colors::CYAN),
style(url).fg(colors::CYAN).underline()
);
println!();
}
pub fn thread_posted(urls: &[String]) {
println!();
println!(
" {} {}",
style("🎉").fg(colors::GREEN),
gradient(&format!("Thread posted! ({} tweets)", urls.len()), colors::GREEN, colors::CYAN)
);
println!();
for (i, url) in urls.iter().enumerate() {
println!(
" {} {}",
style(format!("{}.", i + 1)).fg(colors::MUTED),
style(url).fg(colors::CYAN).underline()
);
}
println!();
}
pub fn divider() {
println!(" {}", style("─".repeat(50)).fg(colors::BORDER));
}
pub fn welcome() {
banner();
println!(
" {}",
style("Welcome! What would you like to do?").fg(colors::MUTED)
);
println!();
}
pub fn render_markdown(content: &str) {
let rendered = aglow::render(content);
for line in rendered.lines() {
println!(" {}", line);
}
}
pub fn demo() {
banner();
header("🔥 Molten TUI Stack Demo");
println!();
subheader("glyphs - ANSI Styling");
println!(" {}", style("Bold text").bold());
println!(" {}", style("Italic text").italic());
println!(" {}", style("Underlined").underline());
println!(" {}", gradient("Gradient text from pink to purple!", colors::PINK, colors::PURPLE));
println!();
subheader("scoria - TUI Components");
let spinner = Spinner::new(SpinnerStyle::Dots).title("Loading...");
println!(" Spinner: {}", spinner.view());
let progress = Progress::new()
.progress(0.75)
.style(ProgressStyle::BLOCK.filled_color(GlyphsColor::Rgb { r: 212, g: 92, b: 235 }))
.width(30);
println!(" Progress: {} 75%", progress.view());
println!();
subheader("aglow - Markdown Rendering");
let md = "**Bold**, *italic*, and `code`";
println!(" {}", aglow::render(md).trim());
println!();
subheader("molten_brand - Brand Colors");
println!(
" {} {} {}",
style("■ Molten").fg(colors::MOLTEN),
style("■ Goblin").fg(colors::GOBLIN),
style("■ Iron").fg(colors::IRON)
);
println!();
success("All Molten libraries working!");
println!();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_colors_defined() {
let _ = colors::ORANGE;
let _ = colors::CYAN;
let _ = colors::GREEN;
}
#[test]
fn test_spinner_frames() {
assert!(!SPINNER_FRAMES.is_empty());
}
#[test]
fn test_word_wrap() {
let lines = word_wrap("hello world this is a test", 10);
assert!(!lines.is_empty());
}
}