sylveos

Toy Operating System
Log | Files | Refs

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 }