[WIP] New UI Logic
This commit is contained in:
parent
b8e18490f3
commit
6db4e806f8
26 changed files with 815 additions and 674 deletions
|
|
@ -1,10 +1,10 @@
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use crate::gui::elements::log_card::UiLogEntry;
|
||||
use json::{JsonValue, object};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub logs: VecDeque<String>,
|
||||
pub logs: VecDeque<UiLogEntry>,
|
||||
pub cpu: Vec<(f64, f64)>,
|
||||
pub ram: Vec<(f64, f64)>,
|
||||
pub ping: Vec<(f64, f64)>,
|
||||
|
|
@ -28,13 +28,17 @@ impl AppState {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn push_log(&mut self, msg: String) {
|
||||
pub fn push_log(&mut self, msg: UiLogEntry) {
|
||||
if self.logs.len() >= MAX_LOGS {
|
||||
self.logs.pop_front();
|
||||
}
|
||||
self.logs.push_back(msg);
|
||||
}
|
||||
|
||||
pub fn get_logs(&self) -> &VecDeque<UiLogEntry> {
|
||||
&self.logs
|
||||
}
|
||||
|
||||
pub fn push_cpu(&mut self, pt: (f64, f64)) {
|
||||
self.cpu.push(pt);
|
||||
if self.cpu.len() > MAX_POINTS {
|
||||
|
|
@ -112,28 +116,23 @@ impl AppState {
|
|||
let len = data.len();
|
||||
|
||||
if len >= width_usize {
|
||||
// Trim data to fit
|
||||
data[len - width_usize..].to_vec()
|
||||
} else {
|
||||
let mut result = Vec::with_capacity(width_usize);
|
||||
|
||||
// Define X spacing (so dummy points are properly spaced across the canvas)
|
||||
let dx = 1.0;
|
||||
let pad_len = width_usize - len;
|
||||
|
||||
// If we have real data, use its first x position to determine where to start padding
|
||||
let start_x = data
|
||||
.first()
|
||||
.map(|(x, _)| x - (dx * pad_len as f64))
|
||||
.unwrap_or(0.0);
|
||||
let _ = data.first().map(|(_, y)| *y).unwrap_or(0.0);
|
||||
|
||||
// Fill padding with increasing x positions so they're visible
|
||||
for i in 0..pad_len {
|
||||
result.push((start_x + i as f64 * dx, -1 as f64));
|
||||
}
|
||||
|
||||
// Then append the real data
|
||||
result.extend_from_slice(data);
|
||||
result
|
||||
}
|
||||
|
|
|
|||
35
src/gui/elements/elements.rs
Normal file
35
src/gui/elements/elements.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use std::any::Any;
|
||||
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::{Frame, layout::Rect};
|
||||
|
||||
use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen};
|
||||
|
||||
pub trait Element: Send + Sync + Any {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
|
||||
fn render(&self, f: &mut Frame, r: Rect);
|
||||
}
|
||||
|
||||
pub trait InfoElement: Send + Sync + Any {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn as_element(&self) -> &dyn Element;
|
||||
fn as_element_mut(&mut self) -> &mut dyn Element;
|
||||
|
||||
fn get_info_screen(&self) -> Box<dyn Screen>;
|
||||
}
|
||||
|
||||
pub trait InteractableElement: Send + Sync + Any {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn as_element(&self) -> &dyn Element;
|
||||
fn as_element_mut(&mut self) -> &mut dyn Element;
|
||||
|
||||
fn interact(&mut self, key: KeyEvent) -> InteractionResult;
|
||||
|
||||
fn can_focus(&self) -> bool;
|
||||
fn is_focused(&self) -> bool;
|
||||
fn focus(&mut self, f: bool);
|
||||
}
|
||||
113
src/gui/elements/log_card.rs
Normal file
113
src/gui/elements/log_card.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use crate::gui::elements::elements::InteractableElement;
|
||||
use crate::gui::interaction_result::InteractionResult;
|
||||
use crate::{APP_STATE, gui::elements::elements::Element};
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
};
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UiLogEntry {
|
||||
pub line: String,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
pub struct LogCard {
|
||||
focused: bool,
|
||||
scroll: u16,
|
||||
}
|
||||
|
||||
impl LogCard {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
focused: false,
|
||||
scroll: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Element for LogCard {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn render(&self, f: &mut Frame, area: Rect) {
|
||||
let state = APP_STATE.lock().unwrap();
|
||||
let logs = state.get_logs();
|
||||
|
||||
let lines: Vec<Line> = logs
|
||||
.iter()
|
||||
.map(|log| Line::from(Span::raw(log.line.clone())).style(log.color))
|
||||
.collect();
|
||||
|
||||
let block = Block::default()
|
||||
.title("Logs")
|
||||
.borders(Borders::ALL)
|
||||
.border_style(if self.focused {
|
||||
Style::default().fg(Color::Yellow)
|
||||
} else {
|
||||
Style::default()
|
||||
});
|
||||
|
||||
let paragraph = Paragraph::new(lines)
|
||||
.block(block)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((self.scroll, 0));
|
||||
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
}
|
||||
impl InteractableElement for LogCard {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_element(&self) -> &dyn Element {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_element_mut(&mut self) -> &mut dyn Element {
|
||||
self
|
||||
}
|
||||
|
||||
fn interact(&mut self, key: KeyEvent) -> InteractionResult {
|
||||
match key.code {
|
||||
KeyCode::Up => {
|
||||
if self.scroll > 0 {
|
||||
self.scroll -= 1;
|
||||
}
|
||||
InteractionResult::Handeled
|
||||
}
|
||||
KeyCode::Down => {
|
||||
self.scroll += 1;
|
||||
InteractionResult::Handeled
|
||||
}
|
||||
_ => InteractionResult::Unhandeled,
|
||||
}
|
||||
}
|
||||
|
||||
fn can_focus(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_focused(&self) -> bool {
|
||||
self.focused
|
||||
}
|
||||
|
||||
fn focus(&mut self, f: bool) {
|
||||
self.focused = f;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::ACTIVE_TASKS;
|
||||
use crate::{RELOAD, SHUTDOWN, gui::tui::UNIQUE, util::config_util::CONFIG};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, poll, read};
|
||||
|
||||
use json::JsonValue;
|
||||
|
||||
pub fn setup_input_handler() {
|
||||
tokio::spawn(async move {
|
||||
{
|
||||
let mut tasks = ACTIVE_TASKS.lock().unwrap();
|
||||
tasks.push("Input Handler".to_string());
|
||||
}
|
||||
|
||||
loop {
|
||||
{
|
||||
let should_shutdown = *SHUTDOWN.read().await;
|
||||
if should_shutdown {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let has_event = match poll(Duration::from_millis(100)) {
|
||||
Ok(true) => true,
|
||||
Ok(false) => false,
|
||||
Err(_) => false,
|
||||
};
|
||||
|
||||
if has_event {
|
||||
match read() {
|
||||
Ok(event) => {
|
||||
if let Event::Key(key_event) = event {
|
||||
handle_input(key_event).await;
|
||||
}
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut tasks = ACTIVE_TASKS.lock().unwrap();
|
||||
tasks.retain(|t| t != "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;
|
||||
}
|
||||
(KeyCode::Char('r'), KeyModifiers::CONTROL) => {
|
||||
{
|
||||
*RELOAD.write().await = true;
|
||||
}
|
||||
{
|
||||
*SHUTDOWN.write().await = true;
|
||||
}
|
||||
}
|
||||
(KeyCode::Backspace, KeyModifiers::NONE) => {
|
||||
let password: String = {
|
||||
let cfg = CONFIG.read().await;
|
||||
cfg.get("password").as_str().unwrap_or("").to_string()
|
||||
};
|
||||
let password: &str = &password;
|
||||
let password = match password.char_indices().next_back() {
|
||||
Some((i, _)) => &password[..i],
|
||||
_ => password,
|
||||
};
|
||||
|
||||
CONFIG
|
||||
.write()
|
||||
.await
|
||||
.change("password", JsonValue::String(password.to_string()));
|
||||
CONFIG.write().await.update();
|
||||
*UNIQUE.write().await = true;
|
||||
}
|
||||
(KeyCode::Char(c), KeyModifiers::NONE) => {
|
||||
let password: String = {
|
||||
let cfg = CONFIG.read().await;
|
||||
cfg.get("password").as_str().unwrap_or("").to_string()
|
||||
};
|
||||
let password = &format!("{}{}", password, c);
|
||||
|
||||
CONFIG
|
||||
.write()
|
||||
.await
|
||||
.change("password", JsonValue::String(password.to_string()));
|
||||
CONFIG.write().await.update();
|
||||
*UNIQUE.write().await = true;
|
||||
}
|
||||
(KeyCode::Char(c), KeyModifiers::SHIFT) => {
|
||||
let password: String = {
|
||||
let cfg = CONFIG.read().await;
|
||||
cfg.get("password").as_str().unwrap_or("").to_string()
|
||||
};
|
||||
let password = &format!("{}{}", password, c);
|
||||
|
||||
CONFIG
|
||||
.write()
|
||||
.await
|
||||
.change("password", JsonValue::String(password.to_string()));
|
||||
CONFIG.write().await.update();
|
||||
*UNIQUE.write().await = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
27
src/gui/interaction_result.rs
Normal file
27
src/gui/interaction_result.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use crate::gui::screens::screens::Screen;
|
||||
|
||||
pub enum InteractionResult {
|
||||
OpenScreen {
|
||||
screen: Box<dyn Screen>,
|
||||
},
|
||||
OpenFutureScreen {
|
||||
screen: Pin<Box<dyn Future<Output = Box<dyn Screen>> + Send>>,
|
||||
},
|
||||
Handeled,
|
||||
Unhandeled,
|
||||
}
|
||||
|
||||
impl Debug for InteractionResult {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
InteractionResult::OpenScreen { screen: _ } => write!(f, "OpenScreen"),
|
||||
InteractionResult::OpenFutureScreen { screen: _ } => write!(f, "OpenFutureScreen"),
|
||||
InteractionResult::Handeled => write!(f, "Handeled"),
|
||||
InteractionResult::Unhandeled => write!(f, "Unhandeled"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
use crate::ACTIVE_TASKS;
|
||||
use crate::APP_STATE;
|
||||
use crate::SHUTDOWN;
|
||||
use crate::gui::tui::UNIQUE;
|
||||
use crate::langu::language_manager::format;
|
||||
use crate::langu::language_manager::from_key;
|
||||
|
||||
use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes};
|
||||
|
||||
use std::{thread, time::Duration};
|
||||
use sysinfo::{RefreshKind, System};
|
||||
|
||||
pub fn log_cv(cv: &CommunicationValue) {
|
||||
if cv.is_type(CommunicationType::identification_response) {
|
||||
let args = [cv.get_data(DataTypes::accepted).unwrap().as_str().unwrap()];
|
||||
log_message(format(&"identification_response", &args));
|
||||
} else {
|
||||
log_message_trans(format!("{:?}", &cv.get_type()));
|
||||
}
|
||||
tokio::spawn(async move {
|
||||
*UNIQUE.write().await = true;
|
||||
});
|
||||
}
|
||||
pub fn log_message_trans(key: impl Into<String>) {
|
||||
APP_STATE.lock().unwrap().push_log(from_key(&key.into()));
|
||||
tokio::spawn(async move {
|
||||
*UNIQUE.write().await = true;
|
||||
});
|
||||
}
|
||||
pub fn log_message(msg: impl Into<String>) {
|
||||
APP_STATE.lock().unwrap().push_log(msg.into());
|
||||
tokio::spawn(async move {
|
||||
*UNIQUE.write().await = true;
|
||||
});
|
||||
}
|
||||
pub fn log_message_format(msg: impl Into<String>, args: &[&str]) {
|
||||
APP_STATE
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push_log(format(&msg.into(), args));
|
||||
tokio::spawn(async move {
|
||||
*UNIQUE.write().await = true;
|
||||
});
|
||||
}
|
||||
|
||||
pub fn setup() {
|
||||
tokio::spawn(async move {
|
||||
{
|
||||
ACTIVE_TASKS.lock().unwrap().push("metrics".to_string());
|
||||
}
|
||||
let mut sys = System::new_with_specifics(RefreshKind::new());
|
||||
let mut last_total_received = 0u64;
|
||||
let mut last_total_transmitted = 0u64;
|
||||
let mut counter = 0.0;
|
||||
loop {
|
||||
if *SHUTDOWN.read().await {
|
||||
break;
|
||||
}
|
||||
sys.refresh_all();
|
||||
|
||||
let mut tcpu = 0;
|
||||
for cpu in sys.cpus() {
|
||||
tcpu += cpu.cpu_usage() as i64;
|
||||
tcpu /= 2;
|
||||
}
|
||||
let ram = (sys.used_memory() as f64 / sys.total_memory() as f64) * 100.0;
|
||||
|
||||
let total_received = 0u64;
|
||||
let total_transmitted = 0u64;
|
||||
|
||||
let delta_received = if last_total_received == 0 {
|
||||
0
|
||||
} else {
|
||||
total_received.saturating_sub(last_total_received)
|
||||
};
|
||||
let delta_transmitted = if last_total_transmitted == 0 {
|
||||
0
|
||||
} else {
|
||||
total_transmitted.saturating_sub(last_total_transmitted)
|
||||
};
|
||||
last_total_received = total_received;
|
||||
last_total_transmitted = total_transmitted;
|
||||
|
||||
let net_down = delta_received as f64;
|
||||
let net_up = delta_transmitted as f64;
|
||||
|
||||
{
|
||||
let mut st = APP_STATE.lock().unwrap();
|
||||
st.push_cpu((counter, tcpu as f64));
|
||||
st.push_ram((counter, ram));
|
||||
st.push_net_down((counter, net_down));
|
||||
st.push_net_up((counter, net_up));
|
||||
|
||||
st.sys_info = format!("NetDown: {} NetUp: {}", delta_received, delta_transmitted);
|
||||
}
|
||||
|
||||
counter += 1.0;
|
||||
*UNIQUE.write().await = true;
|
||||
thread::sleep(Duration::from_millis(1000));
|
||||
}
|
||||
{
|
||||
ACTIVE_TASKS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.retain(|t| !t.eq(&"metrics".to_string()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
pub mod app_state;
|
||||
pub mod log_panel;
|
||||
pub mod settings_panel;
|
||||
pub mod widgets {
|
||||
pub mod betterblock;
|
||||
pub mod elements {
|
||||
pub mod elements;
|
||||
pub mod log_card;
|
||||
}
|
||||
pub mod input_handler;
|
||||
pub mod tui;
|
||||
pub mod screens {
|
||||
pub mod screens;
|
||||
}
|
||||
pub mod app_state;
|
||||
pub mod interaction_result;
|
||||
pub mod ui;
|
||||
|
|
|
|||
27
src/gui/screens/screens.rs
Normal file
27
src/gui/screens/screens.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::{Frame, layout::Rect};
|
||||
|
||||
use crate::gui::{interaction_result::InteractionResult, ui::UI};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NavDirection {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
|
||||
Next,
|
||||
Prev,
|
||||
}
|
||||
|
||||
pub trait Screen: Send + Sync + Any {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
|
||||
fn get_ui(&self) -> &Arc<UI>;
|
||||
|
||||
fn render(&self, f: &mut Frame, rect: Rect);
|
||||
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult;
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Direction, Layout, Margin, Rect},
|
||||
text::Span,
|
||||
widgets::{Block, Paragraph},
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
let padded = area.inner(Margin {
|
||||
horizontal: 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()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(constraints)
|
||||
.split(padded);
|
||||
|
||||
for i in 0..info.len() {
|
||||
frame.render_widget(Paragraph::new(info[i]), chunks[i]);
|
||||
}
|
||||
|
||||
let prefix = Span::raw(format!("select password : {}", password));
|
||||
frame.render_widget(Paragraph::new(prefix), chunks[info.len()]);
|
||||
}
|
||||
230
src/gui/tui.rs
230
src/gui/tui.rs
|
|
@ -1,230 +0,0 @@
|
|||
use std::{io::Stdout, sync::Arc, time::Duration};
|
||||
|
||||
use crate::{
|
||||
APP_STATE, SHUTDOWN,
|
||||
gui::{settings_panel, widgets::betterblock::draw_block_joins},
|
||||
util::config_util::CONFIG,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use ratatui::{
|
||||
Frame, Terminal,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
prelude::CrosstermBackend,
|
||||
style::Color,
|
||||
widgets::{
|
||||
Block, Borders, List, ListItem,
|
||||
canvas::{Canvas, Line},
|
||||
},
|
||||
};
|
||||
use tokio::{
|
||||
self,
|
||||
sync::{Mutex, RwLock},
|
||||
};
|
||||
|
||||
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(ratatui::init())));
|
||||
|
||||
pub fn start_tui() {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
if *SHUTDOWN.read().await {
|
||||
break;
|
||||
}
|
||||
|
||||
if *UNIQUE.read().await {
|
||||
render_tui().await;
|
||||
} else {
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
}
|
||||
ratatui::restore();
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn render_tui() {
|
||||
let password = CONFIG
|
||||
.read()
|
||||
.await
|
||||
.get("password")
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
let mut terminal = TERMINAL.lock().await;
|
||||
let _ = terminal.draw(|f| {
|
||||
let area = f.area();
|
||||
|
||||
let is_horizontal = area.width >= 120;
|
||||
let chunks = if is_horizontal {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage(40),
|
||||
Constraint::Percentage(60),
|
||||
Constraint::Min(40),
|
||||
])
|
||||
.split(area)
|
||||
} else {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
|
||||
.split(area)
|
||||
};
|
||||
|
||||
let state = APP_STATE.lock().unwrap().clone();
|
||||
|
||||
// LOGS PANEL
|
||||
{
|
||||
let borders = if is_horizontal {
|
||||
Borders::LEFT.union(Borders::TOP).union(Borders::BOTTOM)
|
||||
} else {
|
||||
Borders::LEFT.union(Borders::RIGHT).union(Borders::TOP)
|
||||
};
|
||||
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));
|
||||
f.render_widget(list, chunks[0]);
|
||||
}
|
||||
// SETTINGS PANEL
|
||||
{
|
||||
let borders = if is_horizontal {
|
||||
Borders::LEFT.union(Borders::TOP).union(Borders::BOTTOM)
|
||||
} else {
|
||||
Borders::ALL
|
||||
};
|
||||
settings_panel::draw(
|
||||
f,
|
||||
chunks[1],
|
||||
Block::default().title("Settings").borders(borders),
|
||||
password,
|
||||
state.clone(),
|
||||
);
|
||||
if is_horizontal {
|
||||
draw_block_joins(
|
||||
f,
|
||||
chunks[1],
|
||||
Borders::TOP.union(Borders::LEFT).union(Borders::BOTTOM),
|
||||
Borders::LEFT,
|
||||
);
|
||||
} else {
|
||||
draw_block_joins(f, chunks[1], Borders::ALL, Borders::TOP);
|
||||
}
|
||||
}
|
||||
|
||||
// PERFORMANCE GRAPHS
|
||||
if is_horizontal {
|
||||
let stack = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
])
|
||||
.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;
|
||||
}
|
||||
|
||||
pub fn render_graphs(
|
||||
f: &mut Frame<'_>,
|
||||
area: Rect,
|
||||
title: String,
|
||||
unit: String,
|
||||
graph: Vec<(f64, f64)>,
|
||||
borders: Borders,
|
||||
color: Color,
|
||||
) {
|
||||
let min_x = graph.first().map(|(x, _)| *x).unwrap_or(0.0);
|
||||
let max_x = graph.last().map(|(x, _)| *x).unwrap_or(100.0);
|
||||
let min_y = graph
|
||||
.iter()
|
||||
.map(|(_, y)| *y)
|
||||
.filter(|y| *y > 0.0)
|
||||
.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, 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])
|
||||
.y_bounds([0.0, 100.0])
|
||||
.paint(|ctx| {
|
||||
for (x, y) in &graph {
|
||||
ctx.draw(&Line {
|
||||
x1: *x,
|
||||
y1: 0.0,
|
||||
x2: *x,
|
||||
y2: *y,
|
||||
color,
|
||||
});
|
||||
}
|
||||
});
|
||||
f.render_widget(canvas, area);
|
||||
}
|
||||
197
src/gui/ui.rs
Normal file
197
src/gui/ui.rs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
use crossterm::{
|
||||
cursor::MoveTo,
|
||||
execute,
|
||||
terminal::{Clear, ClearType, EnterAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::Size,
|
||||
};
|
||||
use std::{
|
||||
io::{self, Error},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen};
|
||||
|
||||
struct ResizableBackend<'a> {
|
||||
backend: CrosstermBackend<&'a mut Vec<u8>>,
|
||||
size: Size,
|
||||
}
|
||||
|
||||
impl<'a> ResizableBackend<'a> {
|
||||
fn new(buf: &'a mut Vec<u8>, width: u16, height: u16) -> Self {
|
||||
Self {
|
||||
backend: CrosstermBackend::new(buf),
|
||||
size: Size::new(width, height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Backend for ResizableBackend<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn draw<'b, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'b ratatui::buffer::Cell)>,
|
||||
{
|
||||
self.backend.draw(content)
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.hide_cursor()
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.show_cursor()
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
self.backend.get_cursor()
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.backend.set_cursor(x, y)
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.backend.clear()
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, region: ratatui::backend::ClearType) -> io::Result<()> {
|
||||
self.backend.clear_region(region)
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Size, Error> {
|
||||
Ok(self.size)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.backend.flush()
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> Result<ratatui::prelude::Position, Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<ratatui::prelude::Position>>(
|
||||
&mut self,
|
||||
_position: P,
|
||||
) -> Result<(), Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<ratatui::prelude::backend::WindowSize, Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// UI state and rendering
|
||||
|
||||
pub struct UI {
|
||||
pub cols: Arc<Mutex<u16>>,
|
||||
pub rows: Arc<Mutex<u16>>,
|
||||
screen: Arc<Mutex<Option<Box<dyn Screen>>>>,
|
||||
|
||||
cached_render: Arc<Mutex<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl UI {
|
||||
pub fn new(cols: u16, rows: u16) -> Self {
|
||||
Self {
|
||||
cols: Arc::new(Mutex::new(cols)),
|
||||
rows: Arc::new(Mutex::new(rows)),
|
||||
screen: Arc::new(Mutex::new(None)),
|
||||
cached_render: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_screen(&self, screen: Box<dyn Screen>) {
|
||||
*self.screen.lock().unwrap() = Some(screen);
|
||||
}
|
||||
|
||||
pub fn resize(&self, cols: u32, rows: u32) {
|
||||
*self.cols.lock().unwrap() = cols as u16;
|
||||
*self.rows.lock().unwrap() = rows as u16;
|
||||
}
|
||||
|
||||
pub async fn handle_input(&self, input: &[u8]) {
|
||||
let key_event = if input.len() == 1 {
|
||||
let c = input[0] as char;
|
||||
if c.is_ascii() {
|
||||
Some(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let event = match input {
|
||||
b"\x1b[A" => Some(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)),
|
||||
b"\x1b[B" => Some(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)),
|
||||
b"\x1b[C" => Some(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)),
|
||||
b"\x1b[D" => Some(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)),
|
||||
b"\r" | b"\n" => Some(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)),
|
||||
b"\x7f" => Some(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)),
|
||||
_ => key_event,
|
||||
};
|
||||
|
||||
if let Some(event) = event {
|
||||
let result = {
|
||||
let mut guard = self.screen.lock().unwrap();
|
||||
if let Some(screen) = guard.as_mut() {
|
||||
screen.handle_input(event)
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
match result {
|
||||
InteractionResult::OpenScreen { screen } => {
|
||||
self.set_screen(screen);
|
||||
}
|
||||
InteractionResult::OpenFutureScreen { screen: fut } => {
|
||||
let ui = self.clone();
|
||||
let screen = fut.await;
|
||||
ui.set_screen(screen);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
|
||||
execute!(
|
||||
&mut buf,
|
||||
EnterAlternateScreen,
|
||||
Clear(ClearType::All),
|
||||
MoveTo(0, 0)
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
let backend = ResizableBackend::new(
|
||||
&mut buf,
|
||||
*self.cols.lock().unwrap(),
|
||||
*self.rows.lock().unwrap(),
|
||||
);
|
||||
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
if let Some(screen) = self.screen.lock().unwrap().as_ref() {
|
||||
let _ = terminal.draw(|f| screen.render(f, f.area()));
|
||||
}
|
||||
}
|
||||
|
||||
*self.cached_render.lock().unwrap() = buf.clone();
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
use ratatui::layout::Rect;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::widgets::Borders;
|
||||
|
||||
fn set_join_char(frame: &mut Frame, x: u16, y: u16, c: char) {
|
||||
frame
|
||||
.buffer_mut()
|
||||
.set_string(x, y, c.to_string(), Style::default());
|
||||
}
|
||||
|
||||
/// Draws corner join characters only if the block has borders on that side
|
||||
pub fn draw_block_joins(frame: &mut Frame, area: Rect, borders: Borders, joins: Borders) {
|
||||
let x0 = area.x;
|
||||
let y0 = area.y;
|
||||
let x1 = area.x + area.width - 1;
|
||||
let y1 = area.y + area.height - 1;
|
||||
|
||||
// Top-left corner
|
||||
if borders.contains(Borders::TOP) && borders.contains(Borders::LEFT) {
|
||||
let top_left = match (joins.contains(Borders::TOP), joins.contains(Borders::LEFT)) {
|
||||
(true, true) => '┼',
|
||||
(true, false) => '├',
|
||||
(false, true) => '┬',
|
||||
(false, false) => '┌',
|
||||
};
|
||||
set_join_char(frame, x0, y0, top_left);
|
||||
}
|
||||
|
||||
// Top-right corner
|
||||
if borders.contains(Borders::TOP) && borders.contains(Borders::RIGHT) {
|
||||
let top_right = match (joins.contains(Borders::TOP), joins.contains(Borders::RIGHT)) {
|
||||
(true, true) => '┼',
|
||||
(true, false) => '┤',
|
||||
(false, true) => '┬',
|
||||
(false, false) => '┐',
|
||||
};
|
||||
set_join_char(frame, x1, y0, top_right);
|
||||
}
|
||||
|
||||
// Bottom-left corner
|
||||
if borders.contains(Borders::BOTTOM) && borders.contains(Borders::LEFT) {
|
||||
let bottom_left = match (
|
||||
joins.contains(Borders::BOTTOM),
|
||||
joins.contains(Borders::LEFT),
|
||||
) {
|
||||
(true, true) => '┼',
|
||||
(true, false) => '├',
|
||||
(false, true) => '┴',
|
||||
(false, false) => '└',
|
||||
};
|
||||
set_join_char(frame, x0, y1, bottom_left);
|
||||
}
|
||||
|
||||
// Bottom-right corner
|
||||
if borders.contains(Borders::BOTTOM) && borders.contains(Borders::RIGHT) {
|
||||
let bottom_right = match (
|
||||
joins.contains(Borders::BOTTOM),
|
||||
joins.contains(Borders::RIGHT),
|
||||
) {
|
||||
(true, true) => '┼',
|
||||
(true, false) => '┤',
|
||||
(false, true) => '┴',
|
||||
(false, false) => '┘',
|
||||
};
|
||||
set_join_char(frame, x1, y1, bottom_right);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue