use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use color_eyre::eyre::eyre; use color_eyre::Result; use figlet_rs::FIGfont; use ratatui::prelude::*; use ratatui::widgets::*; use tokio::sync::mpsc::UnboundedSender; use super::Component; use crate::action::Action; #[cfg(feature = "blog")] use crate::components::Post; use crate::config::Config; #[allow(dead_code)] pub(super) fn truncate(s: &str, max: usize) -> String { s.char_indices() .find(|(idx, ch)| idx + ch.len_utf8() > max) .map_or(s.to_string(), |(idx, _)| s[..idx].to_string() + "...") } #[derive(Default)] pub struct Content { command_tx: Option>, config: Config, selected_tab: Arc, } impl Content { pub fn new(selected_tab: Arc) -> Self { Self { selected_tab, ..Default::default() } } /// Generate the content for the "About" tab fn about_content(&self, area: Rect) -> Result>> { let greetings_header = FIGfont::from_content(include_str!("../../assets/drpepper.flf")) .map_err(|err| eyre!(err))? .convert("hiya!") .ok_or(eyre!("Failed to create figlet header for about page"))? .to_string(); let lines: Vec = greetings_header.trim_end_matches('\n').split('\n').map(String::from).collect(); let mut content = lines .iter() .enumerate() .map(|(pos, line)| { if pos == lines.len() - 3 { return Line::from(vec![ Span::from(" "), Span::from(line.clone()), Span::from(" I'm Erica ("), Span::styled( "she/they", Style::default().add_modifier(Modifier::ITALIC), ), Span::from("), and I make scalable systems or something. IDFK."), ]); } Line::raw(format!(" {}", line)) .style(Style::default().add_modifier(Modifier::BOLD)) }) .collect::>>(); content.extend(vec![ Line::from(""), Line::from(vec![ Span::from(" "), Span::from("I specialize in systems programming, primarily in "), Span::styled( "Rust 🦀", Style::default() .fg(Color::LightRed) .add_modifier(Modifier::BOLD | Modifier::ITALIC), ), Span::from(" and "), Span::styled( "Luau 🦭", Style::default() .fg(Color::LightBlue) .add_modifier(Modifier::BOLD | Modifier::ITALIC), ), Span::from("."), ]), Line::from(""), Line::from( " I am an avid believer of open-source software, and contribute to a few \ projects such as:", ), ]); let projects = vec![ ( Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD), "lune-org/lune: A standalone Luau runtime", ), ( Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD), "DiscordLuau/discord-luau: A Luau library for creating Discord bots, powered \ by Lune", ), ( Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), "pesde-pkg/pesde: A package manager for the Luau programming language, \ supporting multiple runtimes including Roblox and Lune", ), ]; for (style, project) in projects { let parts: Vec<&str> = project.splitn(2, ':').collect(); let (left, right) = if parts.len() == 2 { (parts[0], parts[1]) } else { (project, "") }; let formatted_left = Span::styled(left, style); let bullet = " • "; let indent = " "; let first_line = if project.len() > area.width as usize - bullet.len() { let split_point = project .char_indices() .take_while(|(i, _)| *i < area.width as usize - bullet.len()) .last() .map(|(i, _)| i) .unwrap_or(project.len()); let (first, rest) = project.split_at(split_point); content.push(Line::from(vec![ Span::from(bullet), formatted_left, Span::from(":"), Span::styled( first.trim_start_matches(format!("{left}:").as_str()).to_string(), Style::default().fg(Color::White), ), ])); rest.to_string() } else { content.push(Line::from(vec![ Span::from(bullet), formatted_left, Span::from(":"), Span::styled(right.to_string(), Style::default().fg(Color::White)), ])); String::new() }; let mut remaining_text = first_line; while !remaining_text.is_empty() { if remaining_text.len() > area.width as usize - indent.len() { let split_point = remaining_text .char_indices() .take_while(|(i, _)| *i < area.width as usize - indent.len()) .last() .map(|(i, _)| i) .unwrap_or(remaining_text.len()); let (first, rest) = remaining_text.split_at(split_point); content.push(Line::from(vec![ Span::from(indent), Span::styled(first.to_string(), Style::default().fg(Color::White)), ])); remaining_text = rest.to_string(); } else { content.push(Line::from(vec![ Span::from(indent), Span::styled( remaining_text.clone(), Style::default().fg(Color::White), ), ])); remaining_text.clear(); } } } content.extend(vec![ Line::from(""), Line::from( " I am also a fan of the 8 bit aesthetic and think seals are super adorable \ :3", ), ]); Ok(content) } /// Generate the content for the "Projects" tab fn projects_content(&self) -> Vec> { vec![Line::from("WIP")] } /// Generate the content for the "Blog" tab #[cfg(feature = "blog")] pub async fn blog_content(&self) -> Result> { Ok(crate::atproto::blog::get_all_posts() .await? .iter() .map(|post| Arc::new(post.clone())) .collect()) } } impl Component for Content { fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { self.command_tx = Some(tx); Ok(()) } fn register_config_handler(&mut self, config: Config) -> Result<()> { self.config = config; Ok(()) } fn update(&mut self, action: Action) -> Result> { match action { Action::Tick => {} Action::Render => {} _ => {} } Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let selected_tab = self.selected_tab.load(Ordering::Relaxed); let content = match selected_tab { 0 => self.about_content(area)?, 1 => self.projects_content(), /* Blog tab handled in `App::render` */ _ => vec![], }; // Create the border lines let mut border_top = Line::default(); border_top.spans.push(Span::styled("╭", Style::default().fg(Color::DarkGray))); let devcomp_width = 13; border_top.spans.push(Span::styled( "─".repeat(devcomp_width), Style::default().fg(Color::DarkGray), )); let tabs = ["about", "projects", "blog"]; let mut current_pos = 1 + devcomp_width; for (i, &tab) in tabs.iter().enumerate() { let (char, style) = if i == self.selected_tab.load(Ordering::Relaxed) { ("━", Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD)) } else { ("─", Style::default().fg(Color::DarkGray)) }; let default_style = Style::default().fg(Color::DarkGray); border_top.spans.push(Span::styled("┴", default_style)); border_top.spans.push(Span::styled("─", default_style)); border_top.spans.push(Span::styled(char.repeat(tab.len()), style)); border_top.spans.push(Span::styled("─", default_style)); border_top.spans.push(Span::styled("┴", default_style)); current_pos += tab.len() + 4; } border_top.spans.push(Span::styled( "─".repeat(area.width as usize - current_pos - 1), Style::default().fg(Color::DarkGray), )); border_top.spans.push(Span::styled("╮", Style::default().fg(Color::DarkGray))); let border_bottom = Line::from(Span::styled( "╰".to_owned() + &"─".repeat(area.width as usize - 2) + "╯", Style::default().fg(Color::DarkGray), )); let border_left = Span::styled("│", Style::default().fg(Color::DarkGray)); let border_right = Span::styled("│", Style::default().fg(Color::DarkGray)); // Render the content let content_widget = Paragraph::new(content) .block(Block::default().borders(Borders::NONE)) .wrap(Wrap { trim: false }); frame.render_widget( content_widget, Rect { x: area.x + 1, y: area.y + 1, width: area.width - 2, height: area.height - 2, }, ); // Render the borders frame.render_widget( Paragraph::new(border_top), Rect { x: area.x, y: area.y, width: area.width, height: 1 }, ); frame.render_widget( Paragraph::new(border_bottom), Rect { x: area.x, y: area.y + area.height - 1, width: area.width, height: 1 }, ); for i in 1..area.height - 1 { frame.render_widget( Paragraph::new(Line::from(border_left.clone())), Rect { x: area.x, y: area.y + i, width: 1, height: 1 }, ); frame.render_widget( Paragraph::new(Line::from(border_right.clone())), Rect { x: area.x + area.width - 1, y: area.y + i, width: 1, height: 1 }, ); } Ok(()) } }