Skip to main content

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 snarkos_utilities::Stoppable;
26
27use snarkvm::prelude::Network;
28
29use anyhow::{Result, anyhow};
30use crossterm::{
31    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
32    execute,
33    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
34};
35use ratatui::{
36    Frame,
37    Terminal,
38    backend::{Backend, CrosstermBackend},
39    layout::{Constraint, Direction, Layout},
40    style::{Color, Modifier, Style},
41    text::{Line, Span},
42    widgets::{Block, Borders, Tabs as TabsTui},
43};
44use std::{
45    io,
46    sync::Arc,
47    thread,
48    time::{Duration, Instant},
49};
50use tokio::sync::mpsc::Receiver;
51
52pub struct Display<N: Network> {
53    /// An instance of the node.
54    node: Node<N>,
55    /// The tick rate of the display.
56    /// This determines how frequent the UI is redrawn.
57    tick_rate: Duration,
58    /// The state of the tabs.
59    tabs: Tabs,
60    /// The logs tab.
61    logs: Logs,
62}
63
64fn header_style() -> Style {
65    Style::default().fg(Color::Cyan)
66}
67
68fn content_style() -> Style {
69    Style::default().fg(Color::White)
70}
71
72impl<N: Network> Display<N> {
73    /// Initializes a new display.
74    pub fn start(node: Node<N>, log_receiver: Receiver<Vec<u8>>, stoppable: Arc<dyn Stoppable>) -> Result<()> {
75        // Initialize the display.
76        enable_raw_mode()?;
77        let mut stdout = io::stdout();
78        execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
79        let backend = CrosstermBackend::new(stdout);
80        let mut terminal = Terminal::new(backend)?;
81
82        // Initialize the display.
83        let mut display = Self {
84            node,
85            tick_rate: Duration::from_secs(1),
86            tabs: Tabs::new(PAGES.to_vec()),
87            logs: Logs::new(log_receiver),
88        };
89
90        // Render the display.
91        let result = display.render(&mut terminal, stoppable);
92
93        // Shut down the display and return to normal terminal mode.
94        disable_raw_mode()?;
95        execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
96        terminal.show_cursor()?;
97
98        // Return error (if any).
99        result
100    }
101}
102
103impl<N: Network> Display<N> {
104    /// Renders the display.
105    fn render<B: Backend>(&mut self, terminal: &mut Terminal<B>, stoppable: Arc<dyn Stoppable>) -> Result<()> {
106        let mut last_tick = Instant::now();
107        loop {
108            if let Err(err) = terminal.draw(|f| self.draw(f)) {
109                return Err(anyhow!("{err}").context("Failed to draw terminal UI"));
110            }
111
112            // Determine how long to wait for an input event, before we redraw.
113            let timeout = self.tick_rate.saturating_sub(last_tick.elapsed());
114
115            if event::poll(timeout)? {
116                if let Event::Key(key) = event::read()? {
117                    match key.code {
118                        KeyCode::Esc => {
119                            stoppable.stop();
120                            return Ok(());
121                        }
122                        KeyCode::Left => self.tabs.previous(),
123                        KeyCode::Right => self.tabs.next(),
124                        _ => {}
125                    }
126                }
127            }
128
129            // Rate-limit how often we redraw the UI.
130            if last_tick.elapsed() >= self.tick_rate {
131                thread::sleep(Duration::from_millis(50));
132                last_tick = Instant::now();
133            }
134        }
135    }
136
137    /// Draws the display.
138    fn draw(&mut self, f: &mut Frame) {
139        /* Layout */
140
141        // Initialize the layout of the page.
142        let chunks = Layout::default()
143            .margin(1)
144            .direction(Direction::Vertical)
145            .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
146            .split(f.area());
147
148        /* Tabs */
149
150        // Initialize the tabs.
151        let block = Block::default().style(Style::default().bg(Color::Black).fg(Color::White));
152        f.render_widget(block, f.area());
153        let titles: Vec<_> = self
154            .tabs
155            .titles
156            .iter()
157            .map(|t| {
158                let (first, rest) = t.split_at(1);
159                Line::from(vec![
160                    Span::styled(first, Style::default().fg(Color::Yellow)),
161                    Span::styled(rest, Style::default().fg(Color::Green)),
162                ])
163            })
164            .collect();
165        let tabs = TabsTui::new(titles)
166            .block(
167                Block::default()
168                    .borders(Borders::ALL)
169                    .title("Welcome to Aleo.")
170                    .style(Style::default().add_modifier(Modifier::BOLD)),
171            )
172            .select(self.tabs.index)
173            .style(header_style())
174            .highlight_style(Style::default().add_modifier(Modifier::BOLD).bg(Color::White));
175        f.render_widget(tabs, chunks[0]);
176
177        /* Pages */
178
179        // Initialize the page.
180        match self.tabs.index {
181            0 => Overview.draw(f, chunks[1], &self.node),
182            1 => self.logs.draw(f, chunks[1]),
183            _ => unreachable!(),
184        };
185    }
186}