use std::sync::{ atomic::{AtomicUsize, Ordering}, Arc, }; use color_eyre::{eyre::eyre, Result}; use figlet_rs::FIGfont; use ratatui::{prelude::*, widgets::*}; use tokio::sync::mpsc::UnboundedSender; use super::Component; use crate::{action::Action, config::Config}; #[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| post.title.clone().unwrap_or("".to_string())) .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(()) } }