use std::{collections::VecDeque, sync::mpsc}; use egui::{ Align, CentralPanel, Context, FontFamily, Label, RichText, ScrollArea, ViewportBuilder, ViewportId, }; use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; use super::AppWindow; const SCROLLBACK: usize = 1000; pub struct TerminalWindow { sim_id: SimId, receiver: mpsc::Receiver, lines: VecDeque, } impl TerminalWindow { pub fn new(sim_id: SimId, client: &EmulatorClient) -> Self { let (sender, receiver) = mpsc::channel(); client.send_command(EmulatorCommand::WatchStdout(sim_id, sender)); let mut lines = VecDeque::new(); lines.push_back(String::new()); Self { sim_id, receiver, lines, } } } impl AppWindow for TerminalWindow { fn viewport_id(&self) -> ViewportId { ViewportId::from_hash_of(format!("terminal-{}", self.sim_id)) } fn sim_id(&self) -> SimId { self.sim_id } fn initial_viewport(&self) -> ViewportBuilder { ViewportBuilder::default() .with_title(format!("Terminal ({})", self.sim_id)) .with_inner_size((640.0, 480.0)) } fn show(&mut self, ctx: &Context) { if let Ok(text) = self.receiver.try_recv() { let mut rest = text.as_str(); while let Some(index) = rest.find('\n') { let (line, lines) = rest.split_at(index); let current = self.lines.back_mut().unwrap(); current.push_str(line); self.lines.push_back(String::new()); if self.lines.len() > SCROLLBACK { self.lines.pop_front(); } rest = &lines[1..]; } self.lines.back_mut().unwrap().push_str(rest); } CentralPanel::default().show(ctx, |ui| { ScrollArea::vertical() .stick_to_bottom(true) .auto_shrink([false, false]) .animated(false) .show(ui, |ui| { for line in &self.lines { let label = Label::new(RichText::new(line).family(FontFamily::Monospace)) .halign(Align::LEFT) .wrap(); ui.add(label); } }); }); } }