ui.rs
1 use ratatui::{ 2 Frame, 3 layout::{Alignment, Constraint, Layout, Rect}, 4 style::{Color, Modifier, Style}, 5 symbols, 6 text::{self, Span}, 7 widgets::{ 8 Block, BorderType, Cell, Paragraph, Row, Table, Tabs, Wrap, 9 canvas::{self, Canvas, Circle, Map, MapResolution, Rectangle}, 10 }, 11 }; 12 use tachyonfx::Duration; 13 // use tui_big_text::{BigText, PixelSize}; 14 15 use crate::app::App; 16 17 pub fn draw(elapsed: Duration, frame: &mut Frame, app: &mut App) { 18 let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(frame.area()); 19 let tabs = app 20 .tabs 21 .titles 22 .iter() 23 .map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::LightGreen)))) 24 .collect::<Tabs>() 25 .block( 26 Block::bordered() 27 .title(Span::styled( 28 app.title, 29 Style::default() 30 .fg(Color::LightMagenta) 31 .add_modifier(Modifier::BOLD), 32 )) 33 .title_alignment(Alignment::Center) 34 .border_style(Color::Magenta) 35 .border_type(BorderType::Double), 36 ) 37 .highlight_style(Style::default().fg(Color::LightYellow)) 38 .select(app.tabs.index); 39 frame.render_widget(tabs, chunks[0]); 40 match app.tabs.index { 41 0 => draw_first_tab(frame, app, chunks[1]), 42 1 => draw_second_tab(frame, app, chunks[1]), 43 2 => draw_third_tab(frame, app, chunks[1]), 44 _ => {} 45 }; 46 47 let area = frame.area(); 48 app.effects 49 .process_effects(elapsed, frame.buffer_mut(), area); 50 } 51 52 fn draw_first_tab(frame: &mut Frame, _app: &mut App, area: Rect) { 53 draw_text(frame, area); 54 } 55 56 fn draw_text(frame: &mut Frame, area: Rect) { 57 use ratatui::text::Line; 58 59 let text = vec![ 60 Line::from(""), 61 Line::from(vec![Span::styled( 62 " โ Open-Source @ Microvisor Systems & LikeC4 core team ๐ง", 63 Style::default() 64 .fg(Color::LightCyan) 65 .add_modifier(Modifier::BOLD), 66 )]), 67 Line::from(""), 68 Line::from(Span::styled( 69 "๐ Site under construction ๐", 70 Style::default().fg(Color::LightYellow), 71 )), 72 Line::from(""), 73 Line::from(Span::styled( 74 "๐จ ========== ~/artwork ========== ๐จ", 75 Style::default() 76 .fg(Color::LightMagenta) 77 .add_modifier(Modifier::BOLD), 78 )), 79 Line::from(""), 80 Line::from("๐น DoomBSD: ๐"), 81 Line::from("๐งฎ Microvisor: ๐"), 82 Line::from("๐ฆ Mira AMM: mira.ly"), 83 Line::from("โจ cuHacking 2025 Platform: docs.cuhacking.ca"), 84 Line::from(""), 85 Line::from(Span::styled( 86 "๐ฑ ========== ~/socials ========== ๐ฑ", 87 Style::default() 88 .fg(Color::LightMagenta) 89 .add_modifier(Modifier::BOLD), 90 )), 91 Line::from(""), 92 Line::from("๐ฅ LinkedIn: linkedin.com/in/mfarabi"), 93 Line::from("๐ GitHub: github.com/MFarabi619/MFarabi619"), 94 Line::from(""), 95 Line::from(Span::styled( 96 "๐ค =========== ~/todo ============ ๐ค", 97 Style::default() 98 .fg(Color::LightMagenta) 99 .add_modifier(Modifier::BOLD), 100 )), 101 Line::from(""), 102 Line::from(" [DONE] Create something, anything"), 103 Line::from(" [DONE] Deploy to Netlify"), 104 Line::from(" [DONE] Make responsive"), 105 Line::from(" [ ] Self-host from home data center with Kubernetes and/or Kubenix"), 106 Line::from(" [ ] Add hyperlink support"), 107 Line::from(" [ ] Add markdown support"), 108 Line::from(" [ ] Set up CI"), 109 Line::from(" [ ] Create regression test suite"), 110 Line::from(" [ ] Teach others how to do it too"), 111 Line::from(""), 112 Line::from(Span::styled( 113 "๐ github.com/MFarabi619/MFarabi619/apps/web", 114 Style::default() 115 .fg(Color::LightCyan) 116 .add_modifier(Modifier::UNDERLINED), 117 )), 118 Line::from(""), 119 Line::from(vec![ 120 Span::raw("Made with utmost โค๏ธโ๐ฅ by ๐ using "), 121 Span::styled("๐ฆ Rust", Style::default().fg(Color::Rgb(250, 100, 0))), 122 Span::raw(", "), 123 Span::styled("โ Nix", Style::default().fg(Color::Rgb(80, 130, 255))), 124 Span::raw(", "), 125 Span::styled("๐น FreeBSD", Style::default().fg(Color::LightRed)), 126 Span::raw(", and "), 127 Span::styled("๐ GNU/Linux ๐ง", Style::default().fg(Color::LightGreen)), 128 Span::raw("."), 129 ]), 130 Line::from(""), 131 Line::from(vec![ 132 Span::raw("Site theme heavily inspired by "), 133 Span::styled("Xe Iaso's ", Style::default().fg(Color::LightMagenta)), 134 Span::raw("blog: "), 135 Span::styled("xeiaso.net", Style::default().fg(Color::LightCyan)), 136 ]), 137 ]; 138 139 let block = Block::bordered() 140 .border_style(Color::Magenta) 141 .border_type(BorderType::Double) 142 .title(Span::styled( 143 "๐ README ๐", 144 Style::default() 145 .fg(Color::LightMagenta) 146 .add_modifier(Modifier::BOLD), 147 )); 148 149 let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true }); 150 frame.render_widget(paragraph, area); 151 } 152 153 fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) { 154 let chunks = 155 Layout::horizontal([Constraint::Percentage(100), Constraint::Percentage(00)]).split(area); 156 let up_style = Style::default().fg(Color::LightGreen); 157 let idle_style = Style::default().fg(Color::LightYellow); 158 let failure_style = Style::default() 159 .fg(Color::LightRed) 160 .add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT); 161 let rows = app.servers.iter().map(|s| { 162 let style = if s.status == "Up" { 163 up_style 164 } else if s.status == "Idle" { 165 idle_style 166 } else { 167 failure_style 168 }; 169 Row::new(vec![ 170 s.user, s.hostname, s.chassis, s.os, s.kernel, s.display, s.desktop, s.cpu, s.gpu, 171 s.memory, s.disk, s.uptime, s.terminal, s.location, s.status, 172 ]) 173 .style(style) 174 }); 175 let table = Table::new( 176 rows, 177 [ 178 Constraint::Length(10), 179 Constraint::Length(18), 180 Constraint::Length(18), 181 Constraint::Length(18), 182 Constraint::Length(18), 183 Constraint::Length(18), 184 Constraint::Length(18), 185 Constraint::Length(18), 186 Constraint::Length(15), 187 Constraint::Length(15), 188 Constraint::Length(10), 189 Constraint::Length(10), 190 Constraint::Length(10), 191 Constraint::Length(10), 192 Constraint::Length(10), 193 Constraint::Length(10), 194 Constraint::Length(10), 195 ], 196 ) 197 .header( 198 Row::new(vec![ 199 "๐ฟ User", 200 "๐ท Hostname", 201 "๐ Chassis", 202 "๐ง OS", 203 "๐ฅ Kernel", 204 "๐ Display", 205 "๐ DE/WM", 206 "๐ง CPU", 207 "๐งซ GPU", 208 "๐ฝ Memory", 209 "๐พ Disk", 210 "๐งซ Uptime", 211 "๐น Terminal", 212 "๐ Location", 213 "๐ Status", 214 ]) 215 .style(Style::default().fg(Color::White)) 216 .top_margin(1) 217 .bottom_margin(1), 218 ) 219 .block( 220 Block::bordered() 221 .title(Span::styled( 222 "๐ Servers ๐ ", 223 Style::default() 224 .fg(Color::LightMagenta) 225 .add_modifier(Modifier::BOLD), 226 )) 227 .border_style(Color::Magenta) 228 .border_type(BorderType::Double), 229 ); 230 frame.render_widget(table, chunks[0]); 231 232 let _map = Canvas::default() 233 .block(Block::bordered().title("World")) 234 .paint(|ctx| { 235 ctx.draw(&Map { 236 color: Color::White, 237 resolution: MapResolution::High, 238 }); 239 ctx.layer(); 240 ctx.draw(&Rectangle { 241 x: 0.0, 242 y: 30.0, 243 width: 10.0, 244 height: 10.0, 245 color: Color::Yellow, 246 }); 247 ctx.draw(&Circle { 248 x: app.servers[2].coords.1, 249 y: app.servers[2].coords.0, 250 radius: 10.0, 251 color: Color::LightGreen, 252 }); 253 for (i, s1) in app.servers.iter().enumerate() { 254 for s2 in &app.servers[i + 1..] { 255 ctx.draw(&canvas::Line { 256 x1: s1.coords.1, 257 y1: s1.coords.0, 258 y2: s2.coords.0, 259 x2: s2.coords.1, 260 color: Color::Yellow, 261 }); 262 } 263 } 264 for server in &app.servers { 265 let color = if server.status == "Up" { 266 Color::LightGreen 267 } else { 268 Color::LightYellow 269 }; 270 ctx.print( 271 server.coords.1, 272 server.coords.0, 273 Span::styled("X", Style::default().fg(color)), 274 ); 275 } 276 }) 277 .marker(if app.enhanced_graphics { 278 symbols::Marker::Braille 279 } else { 280 symbols::Marker::Dot 281 }) 282 .x_bounds([-180.0, 180.0]) 283 .y_bounds([-90.0, 90.0]); 284 // frame.render_widget(map, chunks[1]); 285 } 286 287 fn draw_third_tab(frame: &mut Frame, _app: &mut App, area: Rect) { 288 // let chunks = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area); 289 let chunks = Layout::horizontal([Constraint::Ratio(1, 1)]).split(area); 290 let colors = [ 291 Color::Reset, 292 Color::Black, 293 Color::Red, 294 Color::Green, 295 Color::Yellow, 296 Color::Blue, 297 Color::LightMagenta, 298 Color::Cyan, 299 Color::Gray, 300 Color::DarkGray, 301 Color::LightRed, 302 Color::LightGreen, 303 Color::LightYellow, 304 Color::LightBlue, 305 Color::LightMagenta, 306 Color::LightCyan, 307 Color::White, 308 ]; 309 let items: Vec<Row> = colors 310 .iter() 311 .map(|c| { 312 let cells = vec![ 313 Cell::from(Span::raw(format!("{c:?}: "))), 314 Cell::from(Span::styled("Foreground", Style::default().fg(*c))), 315 Cell::from(Span::styled("Background", Style::default().bg(*c))), 316 ]; 317 Row::new(cells) 318 }) 319 .collect(); 320 321 let table = Table::new( 322 items, 323 [ 324 Constraint::Ratio(1, 3), 325 Constraint::Ratio(1, 3), 326 Constraint::Ratio(1, 3), 327 Constraint::Ratio(1, 3), 328 Constraint::Ratio(1, 3), 329 ], 330 ) 331 .block( 332 Block::bordered() 333 .title(Span::styled( 334 "๐คน workspace ๐คน", 335 Style::default() 336 .fg(Color::LightMagenta) 337 .add_modifier(Modifier::BOLD), 338 )) 339 .border_style(Color::Magenta) 340 .border_type(BorderType::Double), 341 ); 342 343 frame.render_widget(table, chunks[0]); 344 }