use std::sync::Arc;
use std::thread;
use std::time::Duration;
use rich_rust::cells::cell_len;
use rich_rust::console::Console;
use rich_rust::interactive::Status;
use rich_rust::renderables::Renderable;
use rich_rust::renderables::panel::Panel;
use rich_rust::renderables::table::{Column, Table};
use rich_rust::style::Style;
use rich_rust::text::Text;
use crate::Config;
use crate::scenes::{Scene, SceneError};
const MAX_CONTENT_WIDTH: usize = 120;
pub struct HeroScene;
impl HeroScene {
#[must_use]
pub const fn new() -> Self {
Self
}
}
impl Scene for HeroScene {
fn name(&self) -> &'static str {
"hero"
}
fn summary(&self) -> &'static str {
"Introduce Nebula Deploy and the visual brand."
}
fn run(&self, console: &Arc<Console>, cfg: &Config) -> Result<(), SceneError> {
if let Ok(_status) = Status::new(console, "Validating environment…") {
let duration = if cfg.is_quick() {
Duration::from_millis(200)
} else {
Duration::from_millis(800)
};
thread::sleep(duration);
}
render_brand_title(console);
console.print("");
render_capabilities_panel(console);
console.print("");
render_palette_preview(console);
console.print("");
render_ctas(console);
Ok(())
}
}
fn center_padding(content_visible_width: usize, total_width: usize) -> String {
if content_visible_width >= total_width {
return String::new();
}
let padding = (total_width - content_visible_width) / 2;
" ".repeat(padding)
}
fn print_centered_renderable<R: Renderable>(console: &Console, renderable: &R) {
use rich_rust::segment::Segment;
let terminal_width = console.width();
if terminal_width > MAX_CONTENT_WIDTH {
let left_pad = (terminal_width - MAX_CONTENT_WIDTH) / 2;
let indent = " ".repeat(left_pad);
let options = console
.options()
.update_dimensions(MAX_CONTENT_WIDTH, console.height());
let segments = renderable.render(console, &options);
let mut output_segments: Vec<Segment<'static>> = Vec::new();
let mut at_line_start = true;
for seg in segments {
if seg.text.contains('\n') {
for (i, part) in seg.text.split('\n').enumerate() {
if i > 0 {
output_segments.push(Segment::new("\n".to_string(), None));
at_line_start = true;
}
if !part.is_empty() {
if at_line_start {
output_segments.push(Segment::new(indent.clone(), None));
at_line_start = false;
}
output_segments.push(Segment::new(part.to_string(), seg.style.clone()));
}
}
} else {
if at_line_start && !seg.text.is_empty() {
output_segments.push(Segment::new(indent.clone(), None));
at_line_start = false;
}
output_segments.push(seg.into_owned());
}
}
console.print_segments(&output_segments);
} else {
console.print_renderable(renderable);
}
}
fn render_brand_title(console: &Console) {
let width = console.width();
if width < 50 {
let title_text = "✦ NEBULA DEPLOY ✦";
let title_visible_width = cell_len(title_text);
let box_width = title_visible_width + 4; let inner_width = box_width - 2;
let top = format!("┌{}┐", "─".repeat(inner_width));
let mid_content = format!(" {} ", title_text);
let bot = format!("└{}┘", "─".repeat(inner_width));
let pad = center_padding(box_width, width);
console.print(&format!("{pad}[brand.title]{top}[/]"));
console.print(&format!(
"{pad}[brand.title]│[/][bold #a78bfa]{mid_content}[/][brand.title]│[/]"
));
console.print(&format!("{pad}[brand.title]{bot}[/]"));
console.print("");
let subtitle = "Beautiful terminal output";
let pad_sub = center_padding(cell_len(subtitle), width);
console.print(&format!("{pad_sub}[brand.subtitle]{subtitle}[/]"));
let powered = "powered by rich_rust";
let pad_pow = center_padding(cell_len(powered), width);
console.print(&format!("{pad_pow}[brand.muted]{powered}[/]"));
} else {
let title_text = "✦ N E B U L A D E P L O Y ✦";
let title_visible_width = cell_len(title_text);
let inner_padding = 4; let box_width = title_visible_width + (inner_padding * 2) + 2; let inner_width = box_width - 2;
let top = format!("╭{}╮", "─".repeat(inner_width));
let empty_line = format!("│{}│", " ".repeat(inner_width));
let bot = format!("╰{}╯", "─".repeat(inner_width));
let title_inner_pad = " ".repeat(inner_padding);
let title_line_content = format!("{title_inner_pad}{title_text}{title_inner_pad}");
let pad = center_padding(box_width, width);
console.print(&format!("{pad}[brand.title]{top}[/]"));
console.print(&format!("{pad}[brand.title]{empty_line}[/]"));
console.print(&format!(
"{pad}[brand.title]│[/][bold #a78bfa]{title_line_content}[/][brand.title]│[/]"
));
console.print(&format!("{pad}[brand.title]{empty_line}[/]"));
console.print(&format!("{pad}[brand.title]{bot}[/]"));
console.print("");
let subtitle = "Beautiful terminal output for Rust";
let pad_sub = center_padding(cell_len(subtitle), width);
console.print(&format!("{pad_sub}[brand.subtitle]{subtitle}[/]"));
let powered = "powered by rich_rust";
let pad_pow = center_padding(cell_len(powered), width);
console.print(&format!("{pad_pow}[brand.muted]{powered}[/]"));
}
}
fn render_capabilities_panel(console: &Console) {
let width = console.width();
let height = console.height();
let is_terminal = console.is_terminal();
let is_interactive = console.is_interactive();
let color_system = console.color_system();
let emoji_enabled = true;
let color_name = match color_system {
Some(cs) => format!("{cs:?}"),
None => "None (no color)".to_string(),
};
let lines = [
format!(
"[dim]Terminal size:[/] [brand.accent]{width}[/] × [brand.accent]{height}[/] cells"
),
format!("[dim]Color system:[/] [brand.accent]{color_name}[/]"),
format!(
"[dim]Is terminal:[/] {}",
if is_terminal {
"[status.ok]yes[/]"
} else {
"[status.warn]no (piped)[/]"
}
),
format!(
"[dim]Interactive:[/] {}",
if is_interactive {
"[status.ok]yes[/]"
} else {
"[status.warn]no[/]"
}
),
format!(
"[dim]Emoji:[/] {}",
if emoji_enabled {
"[status.ok]enabled[/] ✨"
} else {
"[status.warn]disabled[/]"
}
),
];
let content: Vec<Vec<rich_rust::segment::Segment>> = lines
.iter()
.map(|line| {
let text = rich_rust::markup::render_or_plain(line);
text.render("")
.into_iter()
.map(rich_rust::segment::Segment::into_owned)
.collect()
})
.collect();
let panel = Panel::new(content)
.title(Text::new("Environment Detection"))
.border_style(Style::parse("dim #38bdf8").unwrap_or_default())
.expand(false);
print_centered_renderable(console, &panel);
}
fn render_palette_preview(console: &Console) {
let mut table = Table::new().title("Color Palette");
table.add_column(Column::new("Category").style(Style::parse("dim").unwrap_or_default()));
table.add_column(Column::new("Preview"));
table.add_row_markup([
"Brand",
"[#a78bfa]████[/] [#c4b5fd]████[/] [#38bdf8]████[/]",
]);
table.add_row_markup([
"Status",
"[green]████[/] [yellow]████[/] [red]████[/] [cyan]████[/]",
]);
table.add_row_markup([
"Badges",
"[bold white on green] OK [/] [bold black on yellow] WARN [/] [bold white on red] ERR [/]",
]);
table.add_row_markup(["Muted", "[dim #94a3b8]████[/] [dim #64748b]████[/]"]);
print_centered_renderable(console, &table);
}
fn render_ctas(console: &Console) {
console.print("[section.title]Get Started[/]");
console.print("");
console.print(" [dim]📖[/] Documentation: [link=https://docs.rs/rich_rust][brand.accent]docs.rs/rich_rust[/][/link]");
console.print(" [dim]📦[/] Repository: [link=https://github.com/Dicklesworthstone/rich_rust][brand.accent]github.com/Dicklesworthstone/rich_rust[/][/link]");
console.print(" [dim]🦀[/] Crates.io: [link=https://crates.io/crates/rich_rust][brand.accent]crates.io/crates/rich_rust[/][/link]");
console.print("");
console.print("[hint]Press any key to continue, or run with --scene <name> to jump to a specific demo.[/]");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hero_scene_has_correct_name() {
let scene = HeroScene::new();
assert_eq!(scene.name(), "hero");
}
#[test]
fn hero_scene_has_summary() {
let scene = HeroScene::new();
assert!(!scene.summary().is_empty());
}
#[test]
fn hero_scene_runs_without_error() {
let scene = HeroScene::new();
let console = Console::builder()
.force_terminal(false)
.markup(true)
.build()
.shared();
let cfg = Config::with_defaults();
let result = scene.run(&console, &cfg);
assert!(result.is_ok());
}
#[test]
fn hero_scene_produces_output() {
let scene = HeroScene::new();
let console = Console::builder()
.force_terminal(false)
.markup(true)
.width(80)
.build()
.shared();
let cfg = Config::with_defaults();
console.begin_capture();
let _ = scene.run(&console, &cfg);
let segments = console.end_capture();
let output: String = segments.iter().map(|s| s.text.as_ref()).collect();
assert!(
output.contains("N E B U L A") || output.contains("D E P L O Y"),
"output should contain brand title"
);
assert!(
output.contains("Terminal size") || output.contains("Color system"),
"output should contain capability info"
);
}
}