use std::sync::Arc; use std::time::Instant; use log::info; use winit::application::ApplicationHandler; use winit::event::WindowEvent; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::window::{Window, WindowId}; use spectral_core::{Config, Terminal}; use spectral_font::{Font, FontFamily, FontStyle, GlyphKey}; use spectral_render::Renderer; struct SpectralApp { window: Option>, renderer: Option, terminal: Terminal, config: Config, font_family: Option, last_render: Instant, } impl SpectralApp { fn new(config: Config) -> Self { let terminal = Terminal::new(config.clone()); Self { window: None, renderer: None, terminal, config, font_family: None, last_render: Instant::now(), } } fn init_window(&mut self, event_loop: &ActiveEventLoop) { let window_attrs = Window::default_attributes() .with_title("Spectral Terminal") .with_inner_size(winit::dpi::LogicalSize::new(800, 600)); let window = Arc::new( event_loop.create_window(window_attrs).expect("Failed to create window"), ); let rt = tokio::runtime::Runtime::new().unwrap(); let renderer = rt.block_on(Renderer::new( window.clone(), self.terminal.grid.cols as u32, self.terminal.grid.rows as u32, 2048, )); self.window = Some(window); self.renderer = Some(renderer); self.load_fonts(); } fn load_fonts(&mut self) { let font_names = [ &*self.config.font_family, "Iosevka Extended", "Iosevka", "JetBrains Mono", "Fira Code", "monospace", ]; for &name in &font_names { if let Some(font_data) = try_load_font(name) { let mut family = FontFamily::new(name); if let Some(font) = Font::from_bytes(0, font_data, 0, FontStyle::Regular) { family.add_font(font); self.font_family = Some(family); info!("Loaded font: {}", name); break; } } } if let Some(renderer) = &mut self.renderer { if let Some(family) = &self.font_family { if let Some(font) = family.get_font(FontStyle::Regular) { let px = self.config.font_size; for ch in (32u8..=126).map(|b| b as char) { let glyph_id = font.lookup_glyph(ch); let (metrics, bitmap) = font.rasterize_glyph(glyph_id, px); let w = metrics.width as u16; let h = metrics.height as u16; if w > 0 && h > 0 { let key = GlyphKey { font_id: font.id, glyph_id, size_bucket: (px * 4.0) as u16, style_flags: 0, }; let rgba = spectral_font::msdf::r8_to_rgba(&bitmap); renderer.glyph_atlas.insert(key, w, h, &rgba); } } renderer.needs_atlas_upload = true; } } } } fn handle_input(&mut self, text: &str) { let bytes = text.as_bytes(); self.terminal.feed(bytes); self.request_redraw(); } fn request_redraw(&mut self) { if let Some(window) = &self.window { window.request_redraw(); } } fn render(&mut self) { if let Some(renderer) = &mut self.renderer { renderer.update_atlas(); renderer.update_instances(&self.terminal); renderer.render(); } self.last_render = Instant::now(); } } fn try_load_font(name: &str) -> Option>> { let paths = [ format!("/usr/share/fonts/truetype/{}/{}.ttf", name.to_lowercase().replace(' ', ""), name), format!("/usr/share/fonts/TTF/{}.ttf", name.replace(' ', "")), format!("/usr/share/fonts/truetype/{}.ttf", name.replace(' ', "")), format!("/usr/share/fonts/{}.ttf", name), format!("/usr/share/fonts/truetype/{}.ttf", name.to_lowercase()), format!("/usr/share/fonts/truetype/iosevka/{}-Regular.ttf", name.replace(' ', "")), format!("/usr/share/fonts/truetype/iosevka/{}-Regular.ttf", name.replace(" Extended", "")), ]; for path in &paths { if let Ok(data) = std::fs::read(path) { return Some(Arc::new(data)); } } if let Ok(output) = std::process::Command::new("fc-match") .args(["-f", "%{file}", name]).output() { let path = String::from_utf8_lossy(&output.stdout); let path = path.trim(); if !path.is_empty() && path != name { if let Ok(data) = std::fs::read(path) { return Some(Arc::new(data)); } } } None } impl ApplicationHandler for SpectralApp { fn resumed(&mut self, event_loop: &ActiveEventLoop) { if self.window.is_none() { self.init_window(event_loop); } } fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) { match event { WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::Resized(size) => { if let Some(renderer) = &mut self.renderer { renderer.resize(size.width, size.height); } } WindowEvent::KeyboardInput { event, .. } => { if event.state == winit::event::ElementState::Pressed { if let Some(text) = event.text { self.handle_input(&text); } } } WindowEvent::RedrawRequested => { self.render(); } _ => {} } } fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {} } fn main() { env_logger::init(); let event_loop = EventLoop::new().unwrap(); event_loop.set_control_flow(ControlFlow::Poll); let config = Config::default(); let mut app = SpectralApp::new(config); event_loop.run_app(&mut app).unwrap(); }