[Fix] UI patches

This commit is contained in:
Alex Emmet 2026-02-01 13:17:28 +01:00
commit 9d89fd310a
6 changed files with 867 additions and 228 deletions

734
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,6 @@ once_cell = "1.21.3"
pkcs8 = { version = "*", features = ["alloc"] } pkcs8 = { version = "*", features = ["alloc"] }
rand = "0.8" rand = "0.8"
rand_core = { version = "0.6", features = ["getrandom", "std"] } rand_core = { version = "0.6", features = ["getrandom", "std"] }
ratatui = { version = "*", features = ["all-widgets"]}
reactive-rs = "*" reactive-rs = "*"
aws-lc-rs = "*" aws-lc-rs = "*"
reqwest = "0.12.23" reqwest = "0.12.23"
@ -51,10 +50,11 @@ async-trait = "0.1.89"
sha1 = "0.10.6" sha1 = "0.10.6"
async-tungstenite = "0.32.0" async-tungstenite = "0.32.0"
zip = "6.0.0" zip = "6.0.0"
ratatui_input = "0.1.3"
tui-textarea = "0.7.0" tui-textarea = "0.7.0"
tui-input = "0.14.0" tui-input = "0.14.0"
pnet = "0.35.0" pnet = "0.35.0"
dashmap = "6.1.0" dashmap = "6.1.0"
strum = "0.27.2" strum = "0.27.2"
strum_macros = "0.27.2" strum_macros = "0.27.2"
ratatui = "0.30.0"
ratatui_input = "0.1.3"

View file

@ -172,11 +172,7 @@ impl ConsentUiState {
} }
fn run_consent_ui() -> UserChoice { fn run_consent_ui() -> UserChoice {
enable_raw_mode().unwrap(); let mut terminal = ratatui::init();
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen).unwrap();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ConsentUiState { let mut state = ConsentUiState {
eula: false, eula: false,
@ -188,23 +184,34 @@ fn run_consent_ui() -> UserChoice {
let result = loop { let result = loop {
terminal terminal
.draw(|f| { .draw(|f| {
let size = f.area();
let mut needed_height = 5; let mut needed_height = 5;
let margin = if size.width > 80 && size.height > 18 { let size = f.area();
2
} else if size.width > 78 && size.height > 16 { if size.height < 6
1 || size.width < 27 {
} else { f.render_widget(Line::from(format!("too small {}/63 by {}/12", size.width, size.height)), size);
0 return;
}; }
let max_width = 132;
let max_height = 20;
let content_width = if max_width < size.width { max_width } else { size.width} ;
let content_height = if max_height < size.height { max_height } else { size.height} ;
let horizontal_margin = (size.width.saturating_sub(content_width)) / 2;
let vertical_margin = (size.height.saturating_sub(content_height)) / 2;
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Min(7), Constraint::Length(3)]) .constraints([Constraint::Min(7), Constraint::Length(3)])
.margin(margin) .split(Rect {
.split(size); x: horizontal_margin,
y: vertical_margin,
width: content_width,
height: content_height,
});
let eula_text = if size.width < 76 { let eula_text = if size.width < 76 {
"EULA ¹ (https://docs.tensamin.net/legal/eula/)" "EULA ¹ (https://docs.tensamin.net/legal/eula/)"
} else { } else {
@ -222,25 +229,25 @@ fn run_consent_ui() -> UserChoice {
}; };
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) = let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) =
if size.width > 131 { if size.width > 132 {
( (
vec![7, 3, 4], vec![7, 3, 4],
vec![ vec![
"", "",
"By selecting Continue, you confirm that you have read and agree to the End User License Agreement and applicable Terms of Service", "By selecting Continue, you confirm that you have read and agree to the End User License Agreement and applicable Terms of Service.",
"", "",
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.", "Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
] ]
) )
} else if size.width > 67 { } else if size.width > 68 {
( (
vec![8, 3, 4], vec![8, 3, 4],
vec![ vec![
"", "",
"By selecting Continue, you confirm that you have read and agree", "By selecting Continue, you confirm that you have read and agree",
"to the End User License Agreement and applicable Terms of Service", "to the End User License Agreement and applicable Terms of Service.",
"", "",
"Tensamin services require acceptance of the ToS and Privacy Policy", "Tensamin services require acceptance of the ToS and Privacy Policy.",
] ]
) )
} else { } else {
@ -250,10 +257,10 @@ fn run_consent_ui() -> UserChoice {
"", "",
"By selecting Continue, you confirm that you have", "By selecting Continue, you confirm that you have",
"read and agree to the End User License Agreement", "read and agree to the End User License Agreement",
"and applicable Terms of Service", "and applicable Terms of Service.",
"", "",
"Tensamin services require acceptance of the", "Tensamin services require acceptance of the",
"Terms of Service and Privacy Policy" "Terms of Service and Privacy Policy."
] ]
) )
}; };
@ -291,7 +298,7 @@ fn run_consent_ui() -> UserChoice {
let height_style = if size.height < needed_height as u16 { let height_style = if size.height < needed_height as u16 {
Style::default().fg(Color::Red) Style::default().fg(Color::Red)
} else if size.height >= 11 { } else if size.height < 17 {
Style::default().fg(Color::Yellow) Style::default().fg(Color::Yellow)
} else { } else {
Style::default().fg(Color::Green) Style::default().fg(Color::Green)
@ -368,10 +375,7 @@ fn run_consent_ui() -> UserChoice {
} }
} }
}; };
ratatui::restore();
disable_raw_mode().unwrap();
execute!(terminal.backend_mut(), LeaveAlternateScreen).unwrap();
terminal.show_cursor().unwrap();
result result
} }

View file

@ -2,7 +2,6 @@ use std::time::Duration;
use crate::ACTIVE_TASKS; use crate::ACTIVE_TASKS;
use crate::{RELOAD, SHUTDOWN, gui::tui::UNIQUE, util::config_util::CONFIG}; use crate::{RELOAD, SHUTDOWN, gui::tui::UNIQUE, util::config_util::CONFIG};
// Switched to poll/read which are available by default in crossterm
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, poll, read}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, poll, read};
use json::JsonValue; use json::JsonValue;
@ -48,6 +47,9 @@ pub fn setup_input_handler() {
pub async fn handle_input(key: KeyEvent) { pub async fn handle_input(key: KeyEvent) {
match (key.code, key.modifiers) { match (key.code, key.modifiers) {
(KeyCode::Char('q'), KeyModifiers::CONTROL) => {
*SHUTDOWN.write().await = true;
}
(KeyCode::Char('c'), KeyModifiers::CONTROL) => { (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
*SHUTDOWN.write().await = true; *SHUTDOWN.write().await = true;
} }
@ -67,7 +69,7 @@ pub async fn handle_input(key: KeyEvent) {
let password: &str = &password; let password: &str = &password;
let password = match password.char_indices().next_back() { let password = match password.char_indices().next_back() {
Some((i, _)) => &password[..i], Some((i, _)) => &password[..i],
None => password, _ => password,
}; };
CONFIG CONFIG

View file

@ -6,6 +6,7 @@ use ratatui::{
}; };
use crate::gui::app_state::AppState; use crate::gui::app_state::AppState;
pub fn draw(frame: &mut Frame, area: Rect, block: Block, password: String, _state: AppState) { pub fn draw(frame: &mut Frame, area: Rect, block: Block, password: String, _state: AppState) {
frame.render_widget(block, area); frame.render_widget(block, area);
@ -14,16 +15,29 @@ pub fn draw(frame: &mut Frame, area: Rect, block: Block, password: String, _stat
vertical: 1, vertical: 1,
}); });
let info = vec![
"This is the technical end of your Iota",
"Press Ctrl+Q to quit",
"Press Ctrl+R to reload",
"Select a password for the WebUI",
];
let mut constraints: Vec<Constraint> = Vec::new();
for _ in 0..info.len() {
constraints.push(Constraint::Length(1));
}
constraints.push(Constraint::Length(3));
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints(constraints)
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
])
.split(padded); .split(padded);
let prefix = Span::raw(format!("select password : {}", password)); for i in 0..info.len() {
frame.render_widget(Paragraph::new(info[i]), chunks[i]);
}
frame.render_widget(Paragraph::new(prefix), chunks[0]); let prefix = Span::raw(format!("select password : {}", password));
frame.render_widget(Paragraph::new(prefix), chunks[info.len()]);
} }

View file

@ -1,7 +1,6 @@
use std::{ use std::{
io::{Stdout, stdout}, io::{Stdout, Write, stdout},
sync::Arc, sync::Arc,
thread,
time::Duration, time::Duration,
}; };
@ -11,12 +10,14 @@ use crate::{
util::config_util::CONFIG, util::config_util::CONFIG,
}; };
use crossterm::{ use crossterm::{
cursor::Show,
execute, execute,
terminal::{EnterAlternateScreen, enable_raw_mode}, terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode},
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use ratatui::{ use ratatui::{
Frame, Terminal, Frame, Terminal,
crossterm::terminal::disable_raw_mode,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
prelude::CrosstermBackend, prelude::CrosstermBackend,
style::Color, style::Color,
@ -30,46 +31,27 @@ use tokio::{
sync::{Mutex, RwLock}, sync::{Mutex, RwLock},
}; };
// ****** UTIL ******
fn init_terminal() {
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen).unwrap();
enable_raw_mode().unwrap();
}
// ****** MAIN ******
pub static UNIQUE: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(true)); pub static UNIQUE: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(true));
pub static TERMINAL: Lazy<Arc<Mutex<Terminal<CrosstermBackend<Stdout>>>>> = Lazy::new(|| { pub static TERMINAL: Lazy<Arc<Mutex<Terminal<CrosstermBackend<Stdout>>>>> =
Arc::new(Mutex::new( Lazy::new(|| Arc::new(Mutex::new(ratatui::init())));
Terminal::new(CrosstermBackend::new(stdout())).unwrap(),
))
});
pub fn start_tui() { pub fn start_tui() {
tokio::spawn(async move { tokio::spawn(async move {
init_terminal();
{
ACTIVE_TASKS.lock().unwrap().push("UI".to_string());
}
loop { loop {
if *SHUTDOWN.read().await { if *SHUTDOWN.read().await {
break; break;
} }
if *UNIQUE.read().await { if *UNIQUE.read().await {
render_tui().await; render_tui().await;
} else { } else {
thread::sleep(Duration::from_millis(50)); tokio::time::sleep(Duration::from_millis(50)).await;
} }
} }
{ ratatui::restore();
ACTIVE_TASKS
.lock()
.unwrap()
.retain(|t| !t.eq(&"UI".to_string()));
}
}); });
} }
pub async fn render_tui() { pub async fn render_tui() {
let password = CONFIG let password = CONFIG
.read() .read()
@ -78,29 +60,22 @@ pub async fn render_tui() {
.as_str() .as_str()
.unwrap_or("") .unwrap_or("")
.to_string(); .to_string();
TERMINAL
.lock() let mut terminal = TERMINAL.lock().await;
.await let _ = terminal.draw(|f| {
.draw(|f| {
let area = f.area(); let area = f.area();
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(40),
Constraint::Percentage(60),
Constraint::Max(40),
])
.constraints([ .constraints([
Constraint::Percentage(40), Constraint::Percentage(40),
Constraint::Percentage(60), Constraint::Percentage(60),
Constraint::Min(40), Constraint::Min(40),
]) ])
.split(area); .split(area);
let state;
{ let state = APP_STATE.lock().unwrap().clone();
state = APP_STATE.lock().unwrap().clone();
} // LOGS PANEL
// LOGS
{ {
let items: Vec<ListItem> = state let items: Vec<ListItem> = state
.logs .logs
@ -115,7 +90,7 @@ pub async fn render_tui() {
); );
f.render_widget(list, chunks[0]); f.render_widget(list, chunks[0]);
} }
// SETTINGS // SETTINGS PANEL
{ {
settings_panel::draw( settings_panel::draw(
f, f,
@ -133,24 +108,22 @@ pub async fn render_tui() {
Borders::LEFT, Borders::LEFT,
); );
} }
// GRAPHS // PERFORMANCE GRAPHS
{ {
let stack = Layout::default() let stack = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints([
[
Constraint::Ratio(1, 3), Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3), Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3), Constraint::Ratio(1, 3),
] ])
.as_ref(),
)
.split(chunks[2]); .split(chunks[2]);
render_graphs( render_graphs(
f, f,
stack[0], stack[0],
"CPU".to_string(), "CPU".into(),
"%".to_string(), "%".into(),
state.with_width(38).cpu, state.with_width(38).cpu,
Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT), Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT),
Color::Cyan, Color::Cyan,
@ -161,11 +134,12 @@ pub async fn render_tui() {
Borders::TOP.union(Borders::LEFT), Borders::TOP.union(Borders::LEFT),
Borders::LEFT, Borders::LEFT,
); );
render_graphs( render_graphs(
f, f,
stack[1], stack[1],
"RAM".to_string(), "RAM".into(),
"%".to_string(), "%".into(),
state.with_width(38).ram, state.with_width(38).ram,
Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT), Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT),
Color::Green, Color::Green,
@ -176,11 +150,12 @@ pub async fn render_tui() {
Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT), Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT),
Borders::TOP, Borders::TOP,
); );
render_graphs( render_graphs(
f, f,
stack[2], stack[2],
"PING".to_string(), "PING".into(),
"ms".to_string(), "ms".into(),
state.with_width(38).ping, state.with_width(38).ping,
Borders::ALL, Borders::ALL,
Color::Magenta, Color::Magenta,
@ -193,8 +168,8 @@ pub async fn render_tui() {
Borders::LEFT, Borders::LEFT,
); );
} }
}) });
.unwrap();
*UNIQUE.write().await = false; *UNIQUE.write().await = false;
} }
@ -216,16 +191,18 @@ pub fn render_graphs(
.min_by(|a, b| a.total_cmp(b)) .min_by(|a, b| a.total_cmp(b))
.unwrap_or(0.0); .unwrap_or(0.0);
let max_y = graph.iter().map(|(_, y)| *y).fold(f64::MIN, f64::max); let max_y = graph.iter().map(|(_, y)| *y).fold(f64::MIN, f64::max);
let block = Block::default() let block = Block::default()
.title(format!( .title(format!(
"─{}:─{}{}──{}/{}─MIN/MAX", "─{}:─{}{}──{}/{}─MIN/MAX",
title, title,
graph.last().unwrap_or(&(0.0 as f64, 0.0 as f64)).1 as i64, graph.last().unwrap_or(&(0.0, 0.0)).1 as i64,
unit, unit,
min_y as i64, min_y as i64,
max_y as i64 max_y as i64
)) ))
.borders(borders); .borders(borders);
let canvas = Canvas::default() let canvas = Canvas::default()
.block(block) .block(block)
.x_bounds([min_x, max_x]) .x_bounds([min_x, max_x])