sylveos.rs (7802B)
1 use std::{ 2 sync::{ 3 Arc, 4 atomic::{AtomicBool, Ordering}, 5 }, 6 time::Duration, 7 }; 8 9 use anyhow::Result; 10 use crossterm::event::{Event, EventStream, KeyCode}; 11 use ratatui::{ 12 DefaultTerminal, Frame, 13 buffer::Buffer, 14 layout::{Constraint, Layout, Rect}, 15 style::{Color, Modifier, Style, Stylize}, 16 symbols::scrollbar, 17 text::Line, 18 widgets::{ 19 Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Tabs, 20 Widget, 21 }, 22 }; 23 use tokio::io::{self, AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}; 24 use tokio_serial::SerialStream; 25 use tokio_stream::StreamExt; 26 27 pub async fn start(port: SerialStream) -> Result<()> { 28 let terminal = ratatui::init(); 29 let app_result = App::new(port).run(terminal).await; 30 ratatui::restore(); 31 app_result 32 } 33 34 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 35 enum TabIndex { 36 Console = 0, 37 Journal = 1, 38 } 39 40 impl TabIndex { 41 fn next(self) -> Self { 42 match self { 43 TabIndex::Console => TabIndex::Journal, 44 TabIndex::Journal => TabIndex::Console, 45 } 46 } 47 48 fn previous(self) -> Self { 49 match self { 50 TabIndex::Console => TabIndex::Journal, 51 TabIndex::Journal => TabIndex::Console, 52 } 53 } 54 } 55 56 #[derive(Debug)] 57 struct App { 58 should_quit: bool, 59 current_tab: TabIndex, 60 disconnected: Arc<AtomicBool>, 61 console_widget: ConsoleWidget, 62 journal_widget: JournalWidget, 63 } 64 65 impl App { 66 pub fn new(port: SerialStream) -> Self { 67 let (read_port, write_port) = io::split(port); 68 69 let console_state = Arc::new(std::sync::RwLock::new(ConsoleState::default())); 70 let journal_state = Arc::new(std::sync::RwLock::new(LogState::default())); 71 let disconnected = Arc::new(AtomicBool::new(false)); 72 73 let app = Self { 74 should_quit: false, 75 current_tab: TabIndex::Console, 76 console_widget: ConsoleWidget { 77 state: console_state, 78 write_port: Arc::new(tokio::sync::RwLock::new(write_port)), 79 disconnected: disconnected.clone(), 80 }, 81 journal_widget: JournalWidget { 82 state: journal_state, 83 disconnected: disconnected.clone(), 84 }, 85 disconnected: disconnected.clone(), 86 }; 87 88 // Start the message router 89 let console_state = app.console_widget.state.clone(); 90 let journal_state = app.journal_widget.state.clone(); 91 92 tokio::spawn(Self::route_messages( 93 read_port, 94 console_state, 95 journal_state, 96 disconnected.clone(), 97 )); 98 99 app 100 } 101 102 async fn route_messages( 103 mut read_port: ReadHalf<SerialStream>, 104 console_state: Arc<std::sync::RwLock<ConsoleState>>, 105 journal_state: Arc<std::sync::RwLock<LogState>>, 106 disconnected: Arc<AtomicBool>, 107 ) { 108 loop { 109 match parse_message(&mut read_port).await { 110 Ok(message) => match message { 111 Message::Console(data) => { 112 let mut state = console_state.write().unwrap(); 113 state.buffer.extend(data); 114 } 115 Message::Journal(data) => { 116 let mut state = journal_state.write().unwrap(); 117 state.buffer.extend(data); 118 } 119 Message::Disconnect => { 120 disconnected.store(true, Ordering::SeqCst); 121 return; 122 } 123 }, 124 Err(_) => { 125 disconnected.store(true, Ordering::SeqCst); 126 return; 127 } 128 } 129 } 130 } 131 132 pub async fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { 133 let period = Duration::from_secs_f32(1.0 / 60.0); 134 let mut interval = tokio::time::interval(period); 135 let mut events = EventStream::new(); 136 137 while !self.should_quit { 138 tokio::select! { 139 _ = interval.tick() => { terminal.draw(|frame| self.render(frame))?; }, 140 Some(Ok(event)) = events.next() => self.handle_event(&event).await?, 141 } 142 } 143 Ok(()) 144 } 145 146 fn render(&self, frame: &mut Frame) { 147 let layout = Layout::vertical([ 148 Constraint::Length(1), 149 Constraint::Length(1), 150 Constraint::Fill(1), 151 ]); 152 let [title_area, tabs_area, body_area] = frame.area().layout(&layout); 153 154 let title = Line::from("SylveOS").centered().bold(); 155 frame.render_widget(title, title_area); 156 157 let tabs = Tabs::new(vec!["Console", "Journal"]) 158 .select(self.current_tab as usize) 159 .style(Style::default().fg(Color::White)) 160 .highlight_style( 161 Style::default() 162 .fg(Color::Yellow) 163 .add_modifier(Modifier::BOLD), 164 ) 165 .divider(" | "); 166 frame.render_widget(tabs, tabs_area); 167 168 // Render active tab content 169 match self.current_tab { 170 TabIndex::Console => frame.render_widget(&self.console_widget, body_area), 171 TabIndex::Journal => frame.render_widget(&self.journal_widget, body_area), 172 } 173 } 174 175 async fn handle_event(&mut self, event: &Event) -> Result<()> { 176 let disconnected = self.disconnected.load(Ordering::Relaxed); 177 if let Some(key) = event.as_key_press_event() { 178 match key.code { 179 KeyCode::Esc => self.should_quit = true, 180 KeyCode::Tab => self.current_tab = self.current_tab.next(), 181 KeyCode::BackTab => self.current_tab = self.current_tab.previous(), 182 KeyCode::Down => match self.current_tab { 183 TabIndex::Console => self.console_widget.scroll_down(), 184 TabIndex::Journal => self.journal_widget.scroll_down(), 185 }, 186 KeyCode::Up => match self.current_tab { 187 TabIndex::Console => self.console_widget.scroll_up(), 188 TabIndex::Journal => self.journal_widget.scroll_up(), 189 }, 190 KeyCode::PageDown => match self.current_tab { 191 TabIndex::Console => self.console_widget.scroll_to_bottom(), 192 TabIndex::Journal => self.journal_widget.scroll_to_bottom(), 193 }, 194 KeyCode::Left if self.current_tab == TabIndex::Console && !disconnected => { 195 self.console_widget.move_cursor_left() 196 } 197 KeyCode::Right if self.current_tab == TabIndex::Console && !disconnected => { 198 self.console_widget.move_cursor_right() 199 } 200 KeyCode::Enter if self.current_tab == TabIndex::Console && !disconnected => { 201 self.console_widget.submit().await? 202 } 203 KeyCode::Char(to_insert) 204 if self.current_tab == TabIndex::Console && !disconnected => 205 { 206 self.console_widget.enter_char(to_insert) 207 } 208 KeyCode::Backspace if self.current_tab == TabIndex::Console && !disconnected => { 209 self.console_widget.delete_char() 210 } 211 _ => {} 212 } 213 } 214 215 Ok(()) 216 } 217 } 218 219 #[derive(Debug)] 220 enum Message { 221 Console(Vec<u8>), 222 Journal(Vec<u8>), 223 Disconnect, 224 } 225 226 async fn parse_message(port: &mut ReadHalf<SerialStream>) -> Result<Message> { 227 let byte = port.read_u8().await?; 228 if byte == 0 { 229 return Ok(Message::Disconnect); 230 } 231 232 Ok(Message::Console(vec![byte])) 233 }