snarkos_display/
lib.rs

1// Copyright (c) 2019-2025 Provable Inc.
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
60fn header_style() -> Style {
61    Style::default().fg(Color::Cyan)
62}
63
64fn content_style() -> Style {
65    Style::default().fg(Color::White)
66}
67
68impl<N: Network> Display<N> {
69    /// Initializes a new display.
70    pub fn start(node: Node<N>, log_receiver: Receiver<Vec<u8>>) -> Result<()> {
71        // Initialize the display.
72        enable_raw_mode()?;
73        let mut stdout = io::stdout();
74        execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
75        let backend = CrosstermBackend::new(stdout);
76        let mut terminal = Terminal::new(backend)?;
77
78        // Initialize the display.
79        let mut display = Self {
80            node,
81            tick_rate: Duration::from_secs(1),
82            tabs: Tabs::new(PAGES.to_vec()),
83            logs: Logs::new(log_receiver),
84        };
85
86        // Render the display.
87        let res = display.render(&mut terminal);
88
89        // Terminate the display.
90        disable_raw_mode()?;
91        execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
92        terminal.show_cursor()?;
93
94        // Exit.
95        if let Err(err) = res {
96            println!("{err:?}")
97        }
98
99        Ok(())
100    }
101}
102
103impl<N: Network> Display<N> {
104    /// Renders the display.
105    fn render<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> io::Result<()> {
106        let mut last_tick = Instant::now();
107        loop {
108            terminal.draw(|f| self.draw(f))?;
109
110            // Set the timeout duration.
111            let timeout = self.tick_rate.checked_sub(last_tick.elapsed()).unwrap_or_else(|| Duration::from_secs(0));
112
113            if event::poll(timeout)? {
114                if let Event::Key(key) = event::read()? {
115                    match key.code {
116                        KeyCode::Esc => {
117                            // // TODO (howardwu): @ljedrz to implement a wrapping scope for Display within Node/Server.
118                            // #[allow(unused_must_use)]
119                            // {
120                            //     self.node.shut_down();
121                            // }
122                            return Ok(());
123                        }
124                        KeyCode::Left => self.tabs.previous(),
125                        KeyCode::Right => self.tabs.next(),
126                        _ => {}
127                    }
128                }
129            }
130
131            if last_tick.elapsed() >= self.tick_rate {
132                thread::sleep(Duration::from_millis(50));
133                last_tick = Instant::now();
134            }
135        }
136    }
137
138    /// Draws the display.
139    fn draw(&mut self, f: &mut Frame) {
140        /* Layout */
141
142        // Initialize the layout of the page.
143        let chunks = Layout::default()
144            .margin(1)
145            .direction(Direction::Vertical)
146            .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
147            .split(f.area());
148
149        /* Tabs */
150
151        // Initialize the tabs.
152        let block = Block::default().style(Style::default().bg(Color::Black).fg(Color::White));
153        f.render_widget(block, f.area());
154        let titles: Vec<_> = self
155            .tabs
156            .titles
157            .iter()
158            .map(|t| {
159                let (first, rest) = t.split_at(1);
160                Line::from(vec![
161                    Span::styled(first, Style::default().fg(Color::Yellow)),
162                    Span::styled(rest, Style::default().fg(Color::Green)),
163                ])
164            })
165            .collect();
166        let tabs = TabsTui::new(titles)
167            .block(
168                Block::default()
169                    .borders(Borders::ALL)
170                    .title("Welcome to Aleo.")
171                    .style(Style::default().add_modifier(Modifier::BOLD)),
172            )
173            .select(self.tabs.index)
174            .style(header_style())
175            .highlight_style(Style::default().add_modifier(Modifier::BOLD).bg(Color::White));
176        f.render_widget(tabs, chunks[0]);
177
178        /* Pages */
179
180        // Initialize the page.
181        match self.tabs.index {
182            0 => Overview.draw(f, chunks[1], &self.node),
183            1 => self.logs.draw(f, chunks[1]),
184            _ => unreachable!(),
185        };
186    }
187}