[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

View file

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

View file

@ -2,7 +2,6 @@ use std::time::Duration;
use crate::ACTIVE_TASKS;
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 json::JsonValue;
@ -48,6 +47,9 @@ pub fn setup_input_handler() {
pub async fn handle_input(key: KeyEvent) {
match (key.code, key.modifiers) {
(KeyCode::Char('q'), KeyModifiers::CONTROL) => {
*SHUTDOWN.write().await = true;
}
(KeyCode::Char('c'), KeyModifiers::CONTROL) => {
*SHUTDOWN.write().await = true;
}
@ -67,7 +69,7 @@ pub async fn handle_input(key: KeyEvent) {
let password: &str = &password;
let password = match password.char_indices().next_back() {
Some((i, _)) => &password[..i],
None => password,
_ => password,
};
CONFIG

View file

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