use anyhow::Result;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, poll, read};
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Clear, Paragraph, Wrap},
};
use std::time::{Duration, Instant};
use tokio::time::sleep;
pub struct BootScreen {
start_time: Instant,
animation_phase: AnimationPhase,
show_loading_dots: u8,
animation_frame: u32,
#[allow(dead_code)]
terminal_size: (u16, u16),
}
#[derive(Debug, Clone, PartialEq)]
enum AnimationPhase {
FadeIn,
ShowLogo,
Loading,
Complete,
}
impl BootScreen {
pub fn new(terminal_size: (u16, u16)) -> Self {
Self {
start_time: Instant::now(),
animation_phase: AnimationPhase::FadeIn,
show_loading_dots: 0,
animation_frame: 0,
terminal_size,
}
}
pub async fn run<F>(&mut self, draw_fn: F) -> Result<bool>
where
F: Fn(&mut Self, &mut Frame) + Send + 'static,
{
use crossterm::{
execute,
terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
},
tty::IsTty,
};
use ratatui::{Terminal, backend::CrosstermBackend};
use std::io;
if !std::io::stdout().is_tty() {
return Ok(true);
}
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
let boot_duration = Duration::from_millis(2500); let frame_duration = Duration::from_millis(33);
loop {
let elapsed = self.start_time.elapsed();
if poll(Duration::from_millis(0))? {
if let Event::Key(KeyEvent {
code, modifiers, ..
}) = read()?
{
match code {
KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => {
ratatui::restore();
return Ok(false); }
KeyCode::Char('q') | KeyCode::Esc | KeyCode::Enter | KeyCode::Char(' ') => {
break; }
_ => {}
}
}
}
self.update_animation_phase(elapsed);
self.animation_frame = self.animation_frame.wrapping_add(1);
terminal.draw(|frame| {
draw_fn(self, frame);
})?;
if elapsed >= boot_duration || self.animation_phase == AnimationPhase::Complete {
break;
}
sleep(frame_duration).await;
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(true) }
fn update_animation_phase(&mut self, elapsed: Duration) {
self.animation_phase = match elapsed.as_millis() {
0..=500 => AnimationPhase::FadeIn, 501..=1800 => AnimationPhase::ShowLogo, 1801..=2300 => AnimationPhase::Loading, _ => AnimationPhase::Complete,
};
if elapsed.as_millis() % 200 == 0 {
self.show_loading_dots = (self.show_loading_dots + 1) % 4;
}
}
fn ease_in_out_cubic(t: f32) -> f32 {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powf(3.0) / 2.0
}
}
fn ease_out_cubic(t: f32) -> f32 {
1.0 - (1.0 - t).powf(3.0)
}
fn apply_animation_style(
&self,
lines: Vec<Line<'static>>,
progress: f32,
) -> Vec<Line<'static>> {
self.apply_minimal_fade(lines, progress)
}
fn apply_minimal_fade(&self, lines: Vec<Line<'static>>, progress: f32) -> Vec<Line<'static>> {
let time = self.animation_frame as f32 * 0.05;
let glow = ((time).sin() + 1.0) / 2.0 * 0.3 + 0.7;
let total_lines = lines.len();
let center = total_lines / 2;
lines
.into_iter()
.enumerate()
.map(|(i, line)| {
let distance_from_center =
((i as i32 - center as i32).abs() as f32) / (total_lines as f32);
let line_progress = ((progress - distance_from_center * 0.3) * 1.5).clamp(0.0, 1.0);
let faded_spans = line
.spans
.into_iter()
.map(|span| {
let opacity = (line_progress * glow * 255.0) as u8;
let new_color = if span.style.fg == Some(Color::Indexed(15))
|| span.style.fg == Some(Color::White)
{
if opacity > 200 {
Color::Indexed(15) } else if opacity > 100 {
Color::Indexed(7) } else {
Color::Indexed(8) }
} else if span.style.fg == Some(Color::Indexed(10)) {
if opacity > 128 {
Color::Indexed(10) } else {
Color::Indexed(2) }
} else {
if opacity > 200 {
Color::Indexed(15) } else if opacity > 100 {
Color::Indexed(7) } else {
Color::Indexed(8) }
};
let mut style = Style::default().fg(new_color);
if line_progress > 0.9 && glow > 0.95 {
style = style.add_modifier(Modifier::BOLD);
}
Span::styled(span.content, style)
})
.collect::<Vec<_>>();
Line::from(faded_spans)
})
.collect()
}
pub fn draw(&self, frame: &mut Frame) {
let area = frame.area();
frame.render_widget(Clear, area);
let scale_factor = self.calculate_scale_factor(area.width, area.height);
match self.animation_phase {
AnimationPhase::FadeIn => self.draw_fade_in(frame, area, scale_factor),
AnimationPhase::ShowLogo => self.draw_logo(frame, area, scale_factor),
AnimationPhase::Loading => self.draw_loading(frame, area, scale_factor),
AnimationPhase::Complete => self.draw_complete(frame, area, scale_factor),
}
}
fn calculate_scale_factor(&self, width: u16, height: u16) -> f32 {
let original_width = 80.0;
let original_height = 16.0;
let width_scale = if width < 40 {
(width as f32 * 0.95) / 20.0 } else {
(width as f32 * 0.8) / original_width };
let height_scale = if height < 10 {
(height as f32 * 0.9) / 4.0 } else {
(height as f32 * 0.6) / original_height };
let min_scale = if width < 30 || height < 8 { 0.1 } else { 0.3 };
(width_scale.min(height_scale)).clamp(min_scale, 1.5)
}
fn draw_fade_in(&self, frame: &mut Frame, area: Rect, _scale_factor: f32) {
let elapsed_ms = self.start_time.elapsed().as_millis() as f32;
let fade_progress = Self::ease_in_out_cubic((elapsed_ms / 500.0).min(1.0));
let pulse_phase = (elapsed_ms / 150.0).sin(); let dots = if pulse_phase > 0.5 {
"●"
} else if pulse_phase > 0.0 {
"◐"
} else if pulse_phase > -0.5 {
"◑"
} else {
"◒"
};
let center_y = area.height / 2;
let center_area = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(center_y),
Constraint::Length(3),
Constraint::Min(0),
])
.split(area);
let fade_text = Paragraph::new(Line::from(vec![Span::styled(
format!(" {dots} Initializing Awesome Omarchy TUI {dots} "),
Style::default()
.fg(Color::Rgb(
(255.0 * fade_progress) as u8,
(255.0 * fade_progress) as u8,
(255.0 * fade_progress) as u8,
))
.add_modifier(Modifier::BOLD),
)]))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
frame.render_widget(fade_text, center_area[1]);
}
fn draw_logo(&self, frame: &mut Frame, area: Rect, scale_factor: f32) {
let elapsed_ms = self.start_time.elapsed().as_millis() as f32;
let raw_progress = ((elapsed_ms - 500.0) / 1300.0).clamp(0.0, 1.0);
let show_progress = Self::ease_out_cubic(raw_progress);
let vertical_constraints = if area.height < 15 {
let available_height = area.height.saturating_sub(4); let margin = available_height / 2;
[
Constraint::Length(margin), Constraint::Length(4), Constraint::Length(margin), ]
} else {
[
Constraint::Percentage(20),
Constraint::Min(16), Constraint::Percentage(20),
]
};
let horizontal_constraints = if area.width < 80 {
let ascii_width = 19; let available_width = area.width.saturating_sub(ascii_width);
let margin = available_width / 2;
[
Constraint::Length(margin), Constraint::Length(ascii_width), Constraint::Length(margin), ]
} else {
[
Constraint::Percentage(10),
Constraint::Min(80), Constraint::Percentage(10),
]
};
let vertical_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(vertical_constraints)
.split(area);
let horizontal_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(horizontal_constraints)
.split(vertical_chunks[1]);
let ascii_area = horizontal_chunks[1];
let base_ascii_lines = if area.width < 80 || area.height < 20 {
vec![
Line::from(vec![
Span::styled(
"AWESOME ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
),
Span::styled(
"OMARCHY",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
),
]),
Line::from(vec![Span::styled(
"╔═══════╗ ╔══════╗",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![
Span::styled("║ ", Style::default().fg(Color::Cyan)),
Span::styled("◆ ◆ ◆", Style::default().fg(Color::White)),
Span::styled(" ║ ║ ", Style::default().fg(Color::Cyan)),
Span::styled(
"TUI",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
),
Span::styled(" ║", Style::default().fg(Color::Cyan)),
]),
Line::from(vec![Span::styled(
"╚═══════╝ ╚══════╝",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)]),
]
} else if scale_factor >= 1.0 {
self.get_full_ascii_art()
} else if scale_factor >= 0.7 {
self.get_medium_ascii_art()
} else {
self.get_compact_ascii_art()
};
let animated_lines = self.apply_animation_style(base_ascii_lines, show_progress);
let ascii_paragraph = Paragraph::new(animated_lines)
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
frame.render_widget(ascii_paragraph, ascii_area);
}
fn draw_loading(&self, frame: &mut Frame, area: Rect, scale_factor: f32) {
self.draw_logo_complete(frame, area, scale_factor);
let vertical_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(60),
Constraint::Length(3),
Constraint::Percentage(37),
])
.split(area);
let loading_dots = match self.show_loading_dots {
0 => "● ○ ○",
1 => "○ ● ○",
2 => "○ ○ ●",
_ => "○ ● ○",
};
let loading_text = Paragraph::new(vec![
Line::from(vec![Span::styled(
"Loading awesome resources",
Style::default()
.fg(Color::Indexed(12))
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
loading_dots,
Style::default().fg(Color::Indexed(10)),
)]),
])
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
frame.render_widget(loading_text, vertical_chunks[1]);
let skip_text = Paragraph::new(Line::from(vec![Span::styled(
"Press any key to skip",
Style::default()
.fg(Color::Indexed(8))
.add_modifier(Modifier::ITALIC),
)]))
.alignment(Alignment::Center);
let bottom_area = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(1)])
.split(vertical_chunks[2]);
frame.render_widget(skip_text, bottom_area[1]);
}
fn draw_complete(&self, frame: &mut Frame, area: Rect, scale_factor: f32) {
self.draw_logo_complete(frame, area, scale_factor);
let vertical_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(65),
Constraint::Length(1),
Constraint::Percentage(34),
])
.split(area);
let ready_text = Paragraph::new(Line::from(vec![Span::styled(
"✨ Ready! ✨",
Style::default()
.fg(Color::Indexed(15))
.add_modifier(Modifier::BOLD),
)]))
.alignment(Alignment::Center);
frame.render_widget(ready_text, vertical_chunks[1]);
}
fn draw_logo_complete(&self, frame: &mut Frame, area: Rect, scale_factor: f32) {
let vertical_constraints = if area.height < 15 {
let available_height = area.height.saturating_sub(4); let margin = available_height / 2;
[
Constraint::Length(margin), Constraint::Length(4), Constraint::Length(margin), ]
} else {
[
Constraint::Percentage(15),
Constraint::Min(16),
Constraint::Percentage(25),
]
};
let horizontal_constraints = if area.width < 80 {
let ascii_width = 19; let available_width = area.width.saturating_sub(ascii_width);
let margin = available_width / 2;
[
Constraint::Length(margin), Constraint::Length(ascii_width), Constraint::Length(margin), ]
} else {
[
Constraint::Percentage(10),
Constraint::Min(80),
Constraint::Percentage(10),
]
};
let vertical_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(vertical_constraints)
.split(area);
let horizontal_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(horizontal_constraints)
.split(vertical_chunks[1]);
let ascii_area = horizontal_chunks[1];
let ascii_lines = if area.width < 80 || area.height < 20 {
vec![
Line::from(vec![
Span::styled(
"AWESOME ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
),
Span::styled(
"OMARCHY",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
),
]),
Line::from(vec![Span::styled(
"╔═══════╗ ╔══════╗",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![
Span::styled("║ ", Style::default().fg(Color::Cyan)),
Span::styled("◆ ◆ ◆", Style::default().fg(Color::White)),
Span::styled(" ║ ║ ", Style::default().fg(Color::Cyan)),
Span::styled(
"TUI",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
),
Span::styled(" ║", Style::default().fg(Color::Cyan)),
]),
Line::from(vec![Span::styled(
"╚═══════╝ ╚══════╝",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)]),
]
} else if scale_factor >= 1.0 {
self.get_full_ascii_art()
} else if scale_factor >= 0.7 {
self.get_medium_ascii_art()
} else {
self.get_compact_ascii_art()
};
let ascii_paragraph = Paragraph::new(ascii_lines)
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
frame.render_widget(ascii_paragraph, ascii_area);
}
fn get_full_ascii_art(&self) -> Vec<Line<'static>> {
vec![
Line::from(vec![Span::styled(
" ▄████████ ▄█ █▄ ████████ ▄████████ ▄██████▄ ▄▄▄▄███▄▄▄▄ ████████ ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ▄██▀▀▀███▀▀▀██▄ ███ ███ ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ███ ███ ███ ███ ███ █▀ ███ █▀ ███ ███ ███ ███ ███ ███ █▀ ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ███ ███ ███ ███ ▄███▄▄▄ ███ ███ ███ ███ ███ ███ ▄███▄▄▄ ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ▀███████████ ███ ███ ▀▀███▀▀▀ ▀▀███████████ ███ ███ ███ ███ ███ ▀▀███▀▀▀ ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ███ ███ ███ ███ ███ █▄ ███ ███ ███ ███ ███ ███ ███ █▄ ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ███ ███ ███ ▄█▄ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███ ███ ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ███ █▀ ▀███▀███▀ ██████████ ▄████████▀ ▀██████▀ ▀█ ███ █▀ ██████████ ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ▄▄▄",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ▄█████▄ ▄███████████▄ ▄███████ ▄███████ ▄███████ ▄█ █▄ ▄█ █▄",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
"███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
"███ ███ ███ ███ ███ ███ ███ ███ ███ ███ █▀ ███ ███ ███ ███",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
"███ ███ ███ ███ ███ ▄███▄▄▄███ ▄███▄▄▄██▀ ███ ▄███▄▄▄███▄ ███▄▄▄███",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
"███ ███ ███ ███ ███ ▀███▀▀▀███ ▀███▀▀▀▀ ███ ▀▀███▀▀▀███ ▀▀▀▀▀▀███",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
"███ ███ ███ ███ ███ ███ ███ ██████████ ███ █▄ ███ ███ ▄██ ███",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
"███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ▀█████▀ ▀█ ███ █▀ ███ █▀ ███ ███ ███████▀ ███ █▀ ▀█████▀",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
]
}
fn get_medium_ascii_art(&self) -> Vec<Line<'static>> {
vec![
Line::from(vec![Span::styled(
" ████████ ██ ██ ███████ ███████ ████████ ██ ██ ███████",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ████████ ██ ██ ██ █████ ███████ ████████ ██ ██ █████ ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ██ ██ ██ █ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
" ██ ██ ██████ ██ ███████ ███████ ██ ██ ██ ██ ███████",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled("", Style::default())]),
Line::from(vec![Span::styled(
" ████████ ██ ██ ████████ ████████ ████████ ██ ██ ██ ██",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
"██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
"████████ ██ ██ ████████ ████████ ██ ███████ ████ ",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
"██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![Span::styled(
"██ ██ ██ ██ ██ ██ ██ ██ ████████ ██ ██ ██ ",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
)]),
]
}
fn get_compact_ascii_art(&self) -> Vec<Line<'static>> {
vec![
Line::from(vec![
Span::styled(
"AWESOME ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
),
Span::styled(
"OMARCHY",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
),
]),
Line::from(vec![Span::styled(
"╔═══════╗ ╔══════╗",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![
Span::styled("║ ", Style::default().fg(Color::Cyan)),
Span::styled("◆ ◆ ◆", Style::default().fg(Color::White)),
Span::styled(" ║ ║ ", Style::default().fg(Color::Cyan)),
Span::styled(
"TUI",
Style::default()
.fg(Color::Indexed(10))
.add_modifier(Modifier::BOLD),
),
Span::styled(" ║", Style::default().fg(Color::Cyan)),
]),
Line::from(vec![Span::styled(
"╚═══════╝ ╚══════╝",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)]),
]
}
}