216 lines
5.8 KiB
Rust
216 lines
5.8 KiB
Rust
use super::colors::{bold_color, color, dim, Colors};
|
|
use super::content::{Experience, NavItem, SiteContent, Skill, EXPERIENCES, NAV_ITEMS, SKILLS};
|
|
use super::layout::{box_bottom, box_empty, box_row, box_top, center};
|
|
|
|
const WIDTH: usize = 67;
|
|
|
|
/// ASCII art logo for "VISUAL ALCHEMIST"
|
|
const LOGO: &str = r#"██╗ ██╗██╗███████╗██╗ ██╗ █████╗ ██╗
|
|
██║ ██║██║██╔════╝██║ ██║██╔══██╗██║
|
|
██║ ██║██║███████╗██║ ██║███████║██║
|
|
╚██╗ ██╔╝██║╚════██║██║ ██║██╔══██║██║
|
|
╚████╔╝ ██║███████║╚██████╔╝██║ ██║███████╗
|
|
╚═══╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝"#;
|
|
|
|
/// Render the complete terminal UI
|
|
pub fn render() -> String {
|
|
let mut output = String::new();
|
|
|
|
output.push_str(&render_header());
|
|
output.push('\n');
|
|
output.push_str(&render_experience());
|
|
output.push('\n');
|
|
output.push_str(&render_skills());
|
|
output.push('\n');
|
|
output.push_str(&render_navigation());
|
|
output.push('\n');
|
|
output.push_str(&render_footer());
|
|
output.push('\n');
|
|
|
|
output
|
|
}
|
|
|
|
fn render_header() -> String {
|
|
let mut lines = Vec::new();
|
|
|
|
lines.push(box_top(WIDTH, None));
|
|
lines.push(box_empty(WIDTH));
|
|
|
|
// Logo
|
|
for line in LOGO.lines() {
|
|
let centered = center(&color(line, Colors::RED), WIDTH - 4);
|
|
lines.push(box_row(¢ered, WIDTH));
|
|
}
|
|
|
|
// Subtitle
|
|
lines.push(box_row(¢er(&bold_color("ALCHEMIST", Colors::RED), WIDTH - 4), WIDTH));
|
|
lines.push(box_empty(WIDTH));
|
|
|
|
// Name and title
|
|
lines.push(box_row(
|
|
¢er(
|
|
&format!(
|
|
"{} — {}",
|
|
bold_color(SiteContent::NAME, Colors::WHITE),
|
|
color(SiteContent::TITLE, Colors::RED)
|
|
),
|
|
WIDTH - 4,
|
|
),
|
|
WIDTH,
|
|
));
|
|
|
|
// Location and year
|
|
lines.push(box_row(
|
|
¢er(
|
|
&format!(
|
|
"{}{}{}",
|
|
dim(SiteContent::LOCATION),
|
|
dim(" "),
|
|
dim(SiteContent::YEAR)
|
|
),
|
|
WIDTH - 4,
|
|
),
|
|
WIDTH,
|
|
));
|
|
|
|
lines.push(box_empty(WIDTH));
|
|
|
|
// Tagline - wrap it manually
|
|
let tagline = SiteContent::TAGLINE;
|
|
let max_line = WIDTH - 8;
|
|
|
|
for wrapped_line in wrap_text(tagline, max_line) {
|
|
lines.push(box_row(¢er(&color(&wrapped_line, Colors::LIGHT_GRAY), WIDTH - 4), WIDTH));
|
|
}
|
|
|
|
lines.push(box_empty(WIDTH));
|
|
lines.push(box_bottom(WIDTH));
|
|
|
|
lines.join("\n")
|
|
}
|
|
|
|
fn render_experience() -> String {
|
|
let mut lines = Vec::new();
|
|
|
|
lines.push(box_top(WIDTH, Some("Experience")));
|
|
lines.push(box_empty(WIDTH));
|
|
|
|
for (i, exp) in EXPERIENCES.iter().enumerate() {
|
|
lines.push(box_row(&format_experience(exp), WIDTH));
|
|
lines.push(box_row(&dim(exp.period), WIDTH));
|
|
lines.push(box_row(&color(exp.description, Colors::LIGHT_GRAY), WIDTH));
|
|
|
|
if i < EXPERIENCES.len() - 1 {
|
|
lines.push(box_empty(WIDTH));
|
|
}
|
|
}
|
|
|
|
lines.push(box_empty(WIDTH));
|
|
lines.push(box_bottom(WIDTH));
|
|
|
|
lines.join("\n")
|
|
}
|
|
|
|
fn format_experience(exp: &Experience) -> String {
|
|
let status_color = if exp.status == "ACTIVE" {
|
|
Colors::GREEN
|
|
} else {
|
|
Colors::YELLOW
|
|
};
|
|
|
|
format!(
|
|
"{} {} {} — {}",
|
|
dim(&format!("[{}]", exp.code)),
|
|
color(exp.status, status_color),
|
|
bold_color(exp.title, Colors::WHITE),
|
|
color(exp.role, Colors::RED)
|
|
)
|
|
}
|
|
|
|
fn render_skills() -> String {
|
|
let mut lines = Vec::new();
|
|
|
|
lines.push(box_top(WIDTH, Some("Skills")));
|
|
lines.push(box_empty(WIDTH));
|
|
|
|
for skill in SKILLS.iter() {
|
|
lines.push(box_row(&format_skill(skill), WIDTH));
|
|
}
|
|
|
|
lines.push(box_empty(WIDTH));
|
|
lines.push(box_bottom(WIDTH));
|
|
|
|
lines.join("\n")
|
|
}
|
|
|
|
fn format_skill(skill: &Skill) -> String {
|
|
format!(
|
|
"{} {}{}{}",
|
|
color(skill.num, Colors::RED),
|
|
bold_color(&format!("{:<18}", skill.name), Colors::WHITE),
|
|
" ",
|
|
dim(skill.tools)
|
|
)
|
|
}
|
|
|
|
fn render_navigation() -> String {
|
|
let mut lines = Vec::new();
|
|
|
|
lines.push(box_top(WIDTH, Some("Navigation")));
|
|
lines.push(box_empty(WIDTH));
|
|
|
|
for nav in NAV_ITEMS.iter() {
|
|
lines.push(box_row(&format_nav(nav), WIDTH));
|
|
}
|
|
|
|
lines.push(box_empty(WIDTH));
|
|
lines.push(box_bottom(WIDTH));
|
|
|
|
lines.join("\n")
|
|
}
|
|
|
|
fn format_nav(nav: &NavItem) -> String {
|
|
let cmd = color(nav.command, Colors::WHITE);
|
|
let desc = dim(nav.description);
|
|
let padding = 35_usize.saturating_sub(nav.command.len());
|
|
format!(
|
|
"{} {}{}{}",
|
|
color("$", Colors::RED),
|
|
cmd,
|
|
" ".repeat(padding),
|
|
desc
|
|
)
|
|
}
|
|
|
|
fn render_footer() -> String {
|
|
format!(
|
|
" {} • {}",
|
|
color(SiteContent::WEBSITE, Colors::RED),
|
|
color(SiteContent::EMAIL, Colors::LIGHT_GRAY)
|
|
)
|
|
}
|
|
|
|
/// Simple text wrapper
|
|
fn wrap_text(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);
|
|
}
|
|
|
|
lines
|
|
}
|