snarkos_display/
lib.rs

1// Copyright 2024-2025 Aleo Network Foundation
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16#![forbid(unsafe_code)]
17
18mod pages;
19use pages::*;
20
21mod tabs;
22use tabs::Tabs;
23
24use snarkos_node::Node;
25use snarkvm::prelude::Network;
26
27use anyhow::Result;
28use crossterm::{
29    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
30    execute,
31    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
32};
33use ratatui::{
34    Frame,
35    Terminal,
36    backend::{Backend, CrosstermBackend},
37    layout::{Constraint, Direction, Layout},
38    style::{Color, Modifier, Style},
39    text::{Line, Span},
40    widgets::{Block, Borders, Tabs as TabsTui},
41};
42use std::{
43    io,
44    thread,
45    time::{Duration, Instant},
46};
47use tokio::sync::mpsc::Receiver;
48
49pub struct Display<N: Network> {
50    /// An instance of the node.
51    node: Node<N>,
52    /// The tick rate of the display.
53    tick_rate: Duration,
54    /// The state of the tabs.
55    tabs: Tabs,
56    /// The logs tab.
57    logs: Logs,
58}
59
60impl<N: Network> Display<N> {
61    /// Initializes a new display.
62    pub fn start(node: Node<N>, log_receiver: Receiver<Vec<u8>>) -> Result<()> {
63        // Initialize the display.
64        enable_raw_mode()?;
65        let mut stdout = io::stdout();
66        execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
67        let backend = CrosstermBackend::new(stdout);
68        let mut terminal = Terminal::new(backend)?;
69
70        // Initialize the display.
71        let mut display = Self {
72            node,
73            tick_rate: Duration::from_secs(1),
74            tabs: Tabs::new(PAGES.to_vec()),
75            logs: Logs::new(log_receiver),
76        };
77
78        // Render the display.
79        let res = display.render(&mut terminal);
80
81        // Terminate the display.
82        disable_raw_mode()?;
83        execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
84        terminal.show_cursor()?;
85
86        // Exit.
87        if let Err(err) = res {
88            println!("{err:?}")
89        }
90
91        Ok(())
92    }
93}
94
95impl<N: Network> Display<N> {
96    /// Renders the display.
97    fn render<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> io::Result<()> {
98        let mut last_tick = Instant::now();
99        loop {
100            terminal.draw(|f| self.draw(f))?;
101
102            // Set the timeout duration.
103            let timeout = self.tick_rate.checked_sub(last_tick.elapsed()).unwrap_or_else(|| Duration::from_secs(0));
104
105            if event::poll(timeout)? {
106                if let Event::Key(key) = event::read()? {
107                    match key.code {
108                        KeyCode::Esc => {
109                            // // TODO (howardwu): @ljedrz to implement a wrapping scope for Display within Node/Server.
110                            // #[allow(unused_must_use)]
111                            // {
112                            //     self.node.shut_down();
113                            // }
114                            return Ok(());
115                        }
116                        KeyCode::Left => self.tabs.previous(),
117                        KeyCode::Right => self.tabs.next(),
118                        _ => {}
119                    }
120                }
121            }
122
123            if last_tick.elapsed() >= self.tick_rate {
124                thread::sleep(Duration::from_millis(50));
125                last_tick = Instant::now();
126            }
127        }
128    }
129
130    /// Draws the display.
131    fn draw(&mut self, f: &mut Frame) {
132        /* Layout */
133
134        // Initialize the layout of the page.
135        let chunks = Layout::default()
136            .margin(1)
137            .direction(Direction::Vertical)
138            .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
139            .split(f.size());
140
141        /* Tabs */
142
143        // Initialize the tabs.
144        let block = Block::default().style(Style::default().bg(Color::Black).fg(Color::White));
145        f.render_widget(block, f.size());
146        let titles = self
147            .tabs
148            .titles
149            .iter()
150            .map(|t| {
151                let (first, rest) = t.split_at(1);
152                Line::from(vec![
153                    Span::styled(first, Style::default().fg(Color::Yellow)),
154                    Span::styled(rest, Style::default().fg(Color::Green)),
155                ])
156            })
157            .collect();
158        let tabs = TabsTui::new(titles)
159            .block(
160                Block::default()
161                    .borders(Borders::ALL)
162                    .title("Welcome to Aleo.")
163                    .style(Style::default().add_modifier(Modifier::BOLD)),
164            )
165            .select(self.tabs.index)
166            .style(Style::default().fg(Color::Cyan))
167            .highlight_style(Style::default().add_modifier(Modifier::BOLD).bg(Color::White));
168        f.render_widget(tabs, chunks[0]);
169
170        /* Pages */
171
172        // Initialize the page.
173        match self.tabs.index {
174            0 => Overview.draw(f, chunks[1], &self.node),
175            1 => self.logs.draw(f, chunks[1]),
176            _ => unreachable!(),
177        };
178    }
179}