Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
563 changes: 325 additions & 238 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 2 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ chrono = "0.4.11"
thiserror = "1"
clap = { version = "2.33", features = ["yaml"] }
ctrlc = { version = "3.1", features = ["termination"] }
cursive_table_view = "0.15.0"
ratatui = "0.29"
crossterm = "0.28"
humansize = "1.1.0"
serde = "1"
serde_derive = "1"
Expand All @@ -44,10 +45,6 @@ grin_servers = { path = "./servers", version = "5.5.1-alpha.0" }
grin_util = { path = "./util", version = "5.5.1-alpha.0" }
grin_store = { path = "./store", version = "5.5.1-alpha.0" }

[dependencies.cursive]
version = "0.21"
default-features = false
features = ["crossterm-backend"]

[build-dependencies]
built = { version = "0.8.0", features = ["git2"]}
Expand Down
152 changes: 152 additions & 0 deletions src/bin/tui/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright 2026 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Central application state for the ratatui-based TUI

use crate::servers::ServerStats;
use grin_util::logger::LogEntry;
use ratatui::layout::Rect;
use ratatui::widgets::TableState;
use std::collections::VecDeque;

/// Number of log lines retained in the ring buffer
pub const LOG_BUFFER_SIZE: usize = 200;

/// Top level tabs, in the order they appear in the side menu
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Tab {
Status,
Peers,
Mining,
Logs,
Version,
}

impl Tab {
pub const ALL: [Tab; 5] = [
Tab::Status,
Tab::Peers,
Tab::Mining,
Tab::Logs,
Tab::Version,
];

pub fn title(&self) -> &'static str {
match self {
Tab::Status => "Basic Status",
Tab::Peers => "Peers and Sync",
Tab::Mining => "Mining",
Tab::Logs => "Logs",
Tab::Version => "Version Info",
}
}

pub fn index(&self) -> usize {
Tab::ALL.iter().position(|t| t == self).unwrap_or(0)
}
}

/// Which sub-screen of the Mining tab is showing
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum MiningSubview {
Workers,
Difficulty,
}

/// Which pane currently receives key input
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Focus {
Menu,
Content,
}

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum DialogKind {
Info,
Error,
}

pub struct Dialog {
pub text: String,
pub kind: DialogKind,
}

/// All mutable state the UI renders from. Replaces the tree of named
/// cursive views with a single struct that every `draw` function reads.
pub struct App {
pub tab: Tab,
pub mining_subview: MiningSubview,
pub focus: Focus,
pub stats: Option<ServerStats>,
pub logs: VecDeque<LogEntry>,
pub peers_table: TableState,
pub mining_workers_table: TableState,
pub mining_diff_table: TableState,
pub dialog: Option<Dialog>,
pub should_quit: bool,
/// Screen area of the menu list, stored at draw time for mouse hit-testing
pub menu_area: Rect,
}

impl App {
pub fn new() -> App {
App {
tab: Tab::Status,
mining_subview: MiningSubview::Workers,
focus: Focus::Menu,
stats: None,
logs: VecDeque::with_capacity(LOG_BUFFER_SIZE),
peers_table: TableState::default(),
mining_workers_table: TableState::default(),
mining_diff_table: TableState::default(),
dialog: None,
should_quit: false,
menu_area: Rect::default(),
}
}

/// Number of rows in the table currently on screen, if any
pub fn current_table_len(&self) -> usize {
let stats = match &self.stats {
Some(s) => s,
None => return 0,
};
match self.tab {
Tab::Peers => stats.peer_stats.len(),
Tab::Mining => match self.mining_subview {
MiningSubview::Workers => stats.stratum_stats.worker_stats.len(),
MiningSubview::Difficulty => stats.diff_stats.last_blocks.len(),
},
_ => 0,
}
}

pub fn push_log(&mut self, entry: LogEntry) {
self.logs.push_front(entry);
if self.logs.len() > LOG_BUFFER_SIZE {
self.logs.pop_back();
}
}

pub fn select_menu_next(&mut self) {
let next = (self.tab.index() + 1) % Tab::ALL.len();
self.tab = Tab::ALL[next];
}

pub fn select_menu_prev(&mut self) {
let len = Tab::ALL.len();
let prev = (self.tab.index() + len - 1) % len;
self.tab = Tab::ALL[prev];
}
}
66 changes: 20 additions & 46 deletions src/bin/tui/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,55 +12,29 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//! Identifiers for various TUI elements, because they may be referenced
//! from a few different places

// Basic Status view
pub const VIEW_BASIC_STATUS: &str = "basic_status_view";

// Peer/Sync View
pub const VIEW_PEER_SYNC: &str = "peer_sync_view";
pub const TABLE_PEER_STATUS: &str = "peer_status_table";

// Mining View
pub const VIEW_MINING: &str = "mining_view";
pub const SUBMENU_MINING_BUTTON: &str = "mining_submenu_button";
pub const TABLE_MINING_STATUS: &str = "mining_status_table";
pub const TABLE_MINING_DIFF_STATUS: &str = "mining_diff_status_table";

// Logs View
pub const VIEW_LOGS: &str = "logs_view";

// Mining View
pub const VIEW_VERSION: &str = "version_view";

// Menu and root elements
pub const MAIN_MENU: &str = "main_menu";
pub const ROOT_STACK: &str = "root_stack";

// Logo (not final, to be used somewhere eventually
pub const _WELCOME_LOGO: &str = " GGGGG GGGGGGG
GGGGGGG GGGGGGGGG
GGGGGGGGG GGGG GGGGGGGGGG
GGGGGGGGGGG GGGGGGGG GGGGGGGGGGG
GGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGG
GGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGG
GGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGG
GGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGG
pub const _WELCOME_LOGO: &str = " GGGGG GGGGGGG
GGGGGGG GGGGGGGGG
GGGGGGGGG GGGG GGGGGGGGGG
GGGGGGGGGGG GGGGGGGG GGGGGGGGGGG
GGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGG
GGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGG
GGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGG
GGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGG
GGGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG
GGGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG
GGGGGG
GGGGGGG
GGGGGGGG
GGGGGG
GGGGGGG
GGGGGGGG
GGGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGGG
GGGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGGG
GGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGGG
GGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGG
GGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGG
GGGGGGGGGGG GGGGGGGG GGGGGGGGGGGG
GGGGGGGGGG GGGGGGGG GGGGGGGGGGG
GGGGGGGG GGGGGGGG GGGGGGGGG
GGGGGGG GGGGGGGG GGGGGGG
GGGG GGGGGGGG GGGG
GG GGGGGGGG GG
GGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGG
GGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGG
GGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGG
GGGGGGGGGGG GGGGGGGG GGGGGGGGGGGG
GGGGGGGGGG GGGGGGGG GGGGGGGGGGG
GGGGGGGG GGGGGGGG GGGGGGGGG
GGGGGGG GGGGGGGG GGGGGGG
GGGG GGGGGGGG GGGG
GG GGGGGGGG GG
GGGGGGGG ";
Loading