[Add] UI optimizations, Graphs, More console commands
This commit is contained in:
parent
faf60b144a
commit
997c567c80
13 changed files with 424 additions and 115 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
use json::JsonValue;
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
|
|
@ -7,14 +6,15 @@ use ratatui::{
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Borders, Paragraph},
|
widgets::{Block, Borders, Paragraph},
|
||||||
};
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ACTIVE_TASKS,
|
ACTIVE_TASKS, RELOAD, SHUTDOWN,
|
||||||
data::communication::{CommunicationType, CommunicationValue, DataTypes},
|
data::communication::{CommunicationType, CommunicationValue},
|
||||||
gui::{
|
gui::{
|
||||||
elements::elements::{Element, InteractableElement, JoinableElement},
|
elements::elements::{Element, InteractableElement, JoinableElement},
|
||||||
interaction_result::InteractionResult,
|
interaction_result::InteractionResult,
|
||||||
ui::{FPS, UI},
|
ui::FPS,
|
||||||
util::borders::draw_block_joins,
|
util::borders::draw_block_joins,
|
||||||
},
|
},
|
||||||
log, log_cv,
|
log, log_cv,
|
||||||
|
|
@ -22,11 +22,7 @@ use crate::{
|
||||||
users::{user_manager, user_profile::UserProfile},
|
users::{user_manager, user_profile::UserProfile},
|
||||||
util::file_util,
|
util::file_util,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{any::Any, time::Duration};
|
||||||
any::Any,
|
|
||||||
sync::Arc,
|
|
||||||
time::{Duration, SystemTime},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct ConsoleCard {
|
pub struct ConsoleCard {
|
||||||
focused: bool,
|
focused: bool,
|
||||||
|
|
@ -129,8 +125,12 @@ impl InteractableElement for ConsoleCard {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let command = self.content.clone();
|
let command = self.content.clone();
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
let task_id = format!("command_{}_{}", command, id);
|
||||||
|
ACTIVE_TASKS.insert(task_id.clone());
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
run_command(&command).await;
|
run_command(&command).await;
|
||||||
|
ACTIVE_TASKS.remove(&task_id);
|
||||||
});
|
});
|
||||||
self.content = "".to_string();
|
self.content = "".to_string();
|
||||||
InteractionResult::Handled
|
InteractionResult::Handled
|
||||||
|
|
@ -171,66 +171,43 @@ pub async fn run_command(command: &str) {
|
||||||
["tasks"] => {
|
["tasks"] => {
|
||||||
let active_tasks: Vec<String> =
|
let active_tasks: Vec<String> =
|
||||||
ACTIVE_TASKS.clone().iter().map(|v| v.to_string()).collect();
|
ACTIVE_TASKS.clone().iter().map(|v| v.to_string()).collect();
|
||||||
log!("Active tasks: {:?}", active_tasks);
|
let info = if *SHUTDOWN.read().await && *RELOAD.read().await {
|
||||||
|
"Rebooting, "
|
||||||
|
} else if *SHUTDOWN.read().await {
|
||||||
|
"Shutting , "
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
log!("{}Active tasks: {:?}", info, active_tasks);
|
||||||
}
|
}
|
||||||
["fps"] => {
|
["fps"] => {
|
||||||
log!("FPS: {}", *FPS.read().await);
|
let (fps, skips) = *FPS.read().await;
|
||||||
|
log!("{:.1} FPS with {:.1}% of attempts skipped", fps, skips);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
["help"] => {
|
||||||
|
log!("Available commands: tasks, fps, ping, user");
|
||||||
|
}
|
||||||
|
|
||||||
|
["help", "tasks"] => {
|
||||||
|
log!("Tasks command usage: tasks");
|
||||||
|
}
|
||||||
|
["help", "fps"] => {
|
||||||
|
log!("FPS command usage: fps");
|
||||||
|
}
|
||||||
|
["help", "ping"] => {
|
||||||
|
log!("Ping command usage: ping [time]");
|
||||||
|
}
|
||||||
|
["help", "user"] => {
|
||||||
|
log!("User command usage: user add <username> | user remove <username> | user list");
|
||||||
|
}
|
||||||
|
|
||||||
["ping"] => {
|
["ping"] => {
|
||||||
let time = 20;
|
ping(20).await;
|
||||||
|
|
||||||
let now = SystemTime::now();
|
|
||||||
|
|
||||||
let conn = {
|
|
||||||
let guard = OMIKRON_CONNECTION.read().await;
|
|
||||||
guard.as_ref().cloned()
|
|
||||||
};
|
|
||||||
|
|
||||||
let conn = match conn {
|
|
||||||
Some(c) => c,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
let response_cv = conn
|
|
||||||
.await_response(
|
|
||||||
&CommunicationValue::new(CommunicationType::ping),
|
|
||||||
Some(Duration::from_secs(time)),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let elapsed = now.elapsed().unwrap_or(Duration::ZERO);
|
|
||||||
|
|
||||||
match response_cv {
|
|
||||||
Ok(response) => log_cv!(response.add_data(
|
|
||||||
DataTypes::get_time,
|
|
||||||
JsonValue::from(elapsed.as_millis() as i64)
|
|
||||||
)),
|
|
||||||
Err(err) => log!("Ping error: {:?}", err),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
["ping", time] => {
|
["ping", time] => {
|
||||||
let time = time.parse::<u64>().unwrap_or(20);
|
let time = time.parse::<u64>().unwrap_or(20);
|
||||||
|
ping(time).await;
|
||||||
let conn = {
|
|
||||||
let guard = OMIKRON_CONNECTION.read().await;
|
|
||||||
guard.as_ref().cloned()
|
|
||||||
};
|
|
||||||
|
|
||||||
let conn = match conn {
|
|
||||||
Some(c) => c,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
let response_cv = conn
|
|
||||||
.await_response(
|
|
||||||
&CommunicationValue::new(CommunicationType::ping),
|
|
||||||
Some(Duration::from_secs(time)),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
match response_cv {
|
|
||||||
Ok(response) => log_cv!(response),
|
|
||||||
Err(err) => log!("Ping error: {:?}", err),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
["user", "add", username] => {
|
["user", "add", username] => {
|
||||||
if let (Some(user), Some(_)) = user_manager::create_user(username).await {
|
if let (Some(user), Some(_)) = user_manager::create_user(username).await {
|
||||||
|
|
@ -273,3 +250,28 @@ pub async fn run_command(command: &str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn ping(time: u64) {
|
||||||
|
let time = time;
|
||||||
|
|
||||||
|
let conn = {
|
||||||
|
let guard = OMIKRON_CONNECTION.read().await;
|
||||||
|
guard.as_ref().cloned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let conn = match conn {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response_cv = conn
|
||||||
|
.await_response(
|
||||||
|
&CommunicationValue::new(CommunicationType::ping),
|
||||||
|
Some(Duration::from_secs(time)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match response_cv {
|
||||||
|
Ok(response) => log_cv!(response),
|
||||||
|
Err(err) => log!("Ping error: {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
208
src/gui/elements/graph_card.rs
Normal file
208
src/gui/elements/graph_card.rs
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
use std::{any::Any, sync::Arc};
|
||||||
|
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
|
layout::Rect,
|
||||||
|
style::{Color, Style},
|
||||||
|
widgets::{
|
||||||
|
Block, Borders,
|
||||||
|
canvas::{Canvas, Line},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
APP_STATE,
|
||||||
|
gui::{
|
||||||
|
elements::elements::{Element, InteractableElement, JoinableElement},
|
||||||
|
interaction_result::InteractionResult,
|
||||||
|
ui::UI,
|
||||||
|
util::borders::draw_block_joins,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum GRAPHS {
|
||||||
|
Ram,
|
||||||
|
Cpu,
|
||||||
|
Ping,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GRAPHS {
|
||||||
|
pub fn get_graph(&self) -> Vec<(f64, f64)> {
|
||||||
|
match self {
|
||||||
|
GRAPHS::Ram => APP_STATE.lock().unwrap().ram.clone(),
|
||||||
|
GRAPHS::Cpu => APP_STATE.lock().unwrap().cpu.clone(),
|
||||||
|
GRAPHS::Ping => APP_STATE.lock().unwrap().ping.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_unit(&self) -> String {
|
||||||
|
match self {
|
||||||
|
GRAPHS::Ram => "MB".to_string(),
|
||||||
|
GRAPHS::Cpu => "%".to_string(),
|
||||||
|
GRAPHS::Ping => "ms".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GraphCard {
|
||||||
|
ui: Arc<UI>,
|
||||||
|
graph_type: GRAPHS,
|
||||||
|
|
||||||
|
focused: bool,
|
||||||
|
pub title: String,
|
||||||
|
color: Color,
|
||||||
|
|
||||||
|
borders: Borders,
|
||||||
|
joins: Borders,
|
||||||
|
|
||||||
|
open: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraphCard {
|
||||||
|
pub fn new(ui: Arc<UI>, graph_type: GRAPHS, title: String) -> Self {
|
||||||
|
Self {
|
||||||
|
ui,
|
||||||
|
graph_type,
|
||||||
|
focused: false,
|
||||||
|
title,
|
||||||
|
color: Color::White,
|
||||||
|
borders: Borders::ALL,
|
||||||
|
joins: Borders::NONE,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_open(&mut self, open: bool) {
|
||||||
|
self.open = open;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Element for GraphCard {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self, f: &mut Frame, r: Rect) {
|
||||||
|
if self.open {
|
||||||
|
let graph = self.graph_type.get_graph();
|
||||||
|
let unit = self.graph_type.get_unit();
|
||||||
|
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",
|
||||||
|
self.title,
|
||||||
|
graph.last().unwrap_or(&(0.0, 0.0)).1 as i64,
|
||||||
|
unit,
|
||||||
|
min_y as i64,
|
||||||
|
max_y as i64,
|
||||||
|
))
|
||||||
|
.borders(self.borders)
|
||||||
|
.border_style(if self.focused {
|
||||||
|
Style::default().fg(Color::Yellow)
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
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: self.color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
f.render_widget(canvas, r);
|
||||||
|
} else {
|
||||||
|
let block = Block::default()
|
||||||
|
.title("")
|
||||||
|
.borders(self.borders)
|
||||||
|
.border_style(if self.focused {
|
||||||
|
Style::default().fg(Color::Yellow)
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
});
|
||||||
|
f.render_widget(block, r);
|
||||||
|
}
|
||||||
|
draw_block_joins(f, r, self.borders, self.joins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JoinableElement for GraphCard {
|
||||||
|
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 set_borders(&mut self, borders: Borders) {
|
||||||
|
self.borders = borders;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_joins(&mut self, joins: Borders) {
|
||||||
|
self.joins = joins;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractableElement for GraphCard {
|
||||||
|
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 {
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_focus(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_focused(&self) -> bool {
|
||||||
|
self.focused
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus(&mut self, f: bool) {
|
||||||
|
self.focused = f;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/gui/elements/log_card.rs
Normal file → Executable file
25
src/gui/elements/log_card.rs
Normal file → Executable file
|
|
@ -30,7 +30,7 @@ impl LogCard {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
focused: false,
|
focused: false,
|
||||||
scroll: 0,
|
scroll: 1,
|
||||||
borders: Borders::ALL,
|
borders: Borders::ALL,
|
||||||
joins: Borders::NONE,
|
joins: Borders::NONE,
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +51,9 @@ impl Element for LogCard {
|
||||||
|
|
||||||
let lines: Vec<Line> = logs
|
let lines: Vec<Line> = logs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|log| Line::from(Span::raw(log.line.clone())).style(log.color))
|
.map(|log| {
|
||||||
|
Line::from(Span::raw(log.line.clone())).style(Style::default().fg(log.color))
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
|
|
@ -67,10 +69,17 @@ impl Element for LogCard {
|
||||||
Style::default()
|
Style::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let inner_height = area.height.saturating_sub(2) as usize;
|
||||||
|
|
||||||
|
let total_lines = lines.len();
|
||||||
|
let base_scroll = total_lines.saturating_sub(inner_height) as u16;
|
||||||
|
|
||||||
|
let scroll = base_scroll.saturating_sub(self.scroll);
|
||||||
|
|
||||||
let paragraph = Paragraph::new(lines)
|
let paragraph = Paragraph::new(lines)
|
||||||
.block(block)
|
.block(block)
|
||||||
.wrap(Wrap { trim: false })
|
.wrap(Wrap { trim: false })
|
||||||
.scroll((self.scroll, 0));
|
.scroll((scroll, 0));
|
||||||
|
|
||||||
f.render_widget(paragraph, area);
|
f.render_widget(paragraph, area);
|
||||||
draw_block_joins(f, area, self.borders, self.joins);
|
draw_block_joins(f, area, self.borders, self.joins);
|
||||||
|
|
@ -121,13 +130,17 @@ impl InteractableElement for LogCard {
|
||||||
fn interact(&mut self, key: KeyEvent) -> InteractionResult {
|
fn interact(&mut self, key: KeyEvent) -> InteractionResult {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('J') | KeyCode::Char('j') => {
|
KeyCode::Char('J') | KeyCode::Char('j') => {
|
||||||
if self.scroll > 0 {
|
let state = APP_STATE.lock().unwrap();
|
||||||
self.scroll -= 1;
|
let logs = state.get_logs();
|
||||||
|
if self.scroll < logs.len() as u16 {
|
||||||
|
self.scroll += 1;
|
||||||
}
|
}
|
||||||
InteractionResult::Handled
|
InteractionResult::Handled
|
||||||
}
|
}
|
||||||
KeyCode::Char('K') | KeyCode::Char('k') => {
|
KeyCode::Char('K') | KeyCode::Char('k') => {
|
||||||
self.scroll += 1;
|
if self.scroll > 1 {
|
||||||
|
self.scroll -= 1;
|
||||||
|
}
|
||||||
InteractionResult::Handled
|
InteractionResult::Handled
|
||||||
}
|
}
|
||||||
_ => InteractionResult::Unhandled,
|
_ => InteractionResult::Unhandled,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use crate::gui::ui::{UI, UNIQUE};
|
||||||
use crate::{RELOAD, SHUTDOWN};
|
use crate::{RELOAD, SHUTDOWN};
|
||||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, poll, read};
|
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, poll, read};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub fn setup_input_handler(ui: Arc<UI>) {
|
pub fn setup_input_handler(ui: Arc<UI>) {
|
||||||
|
|
@ -30,7 +31,7 @@ pub fn setup_input_handler(ui: Arc<UI>) {
|
||||||
if key_event.kind == KeyEventKind::Press {
|
if key_event.kind == KeyEventKind::Press {
|
||||||
let uic = ui.clone();
|
let uic = ui.clone();
|
||||||
handle_input(key_event, uic).await;
|
handle_input(key_event, uic).await;
|
||||||
*UNIQUE.write().await = true;
|
UNIQUE.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
pub mod elements {
|
pub mod elements {
|
||||||
pub mod console_card;
|
pub mod console_card;
|
||||||
pub mod elements;
|
pub mod elements;
|
||||||
|
pub mod graph_card;
|
||||||
pub mod log_card;
|
pub mod log_card;
|
||||||
}
|
}
|
||||||
pub mod screens {
|
pub mod screens {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use crate::gui::{
|
||||||
elements::{
|
elements::{
|
||||||
console_card::ConsoleCard,
|
console_card::ConsoleCard,
|
||||||
elements::{InteractableElement, JoinableElement},
|
elements::{InteractableElement, JoinableElement},
|
||||||
|
graph_card::{GRAPHS, GraphCard},
|
||||||
log_card::LogCard,
|
log_card::LogCard,
|
||||||
},
|
},
|
||||||
interaction_result::InteractionResult,
|
interaction_result::InteractionResult,
|
||||||
|
|
@ -19,34 +20,49 @@ use ratatui::{
|
||||||
use std::{any::Any, sync::Arc};
|
use std::{any::Any, sync::Arc};
|
||||||
|
|
||||||
pub struct MainScreen {
|
pub struct MainScreen {
|
||||||
ui: Arc<UI>,
|
|
||||||
elements: Vec<Box<dyn InteractableElement>>,
|
elements: Vec<Box<dyn InteractableElement>>,
|
||||||
nav_grid: Vec<Vec<Option<usize>>>,
|
nav_grid: Vec<Vec<Option<usize>>>,
|
||||||
selected_coords: (usize, usize),
|
selected_coords: (usize, usize),
|
||||||
|
graphs_open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MainScreen {
|
impl MainScreen {
|
||||||
pub async fn new(ui: Arc<UI>) -> Self {
|
pub async fn new(ui: Arc<UI>) -> Self {
|
||||||
let mut elements: Vec<Box<dyn InteractableElement>> = Vec::new();
|
let mut elements: Vec<Box<dyn InteractableElement>> = Vec::new();
|
||||||
let mut nav_grid: Vec<Vec<Option<usize>>> = Vec::new();
|
|
||||||
|
let nav_grid = vec![
|
||||||
|
vec![Some(0), Some(2)],
|
||||||
|
vec![Some(1), Some(3)],
|
||||||
|
vec![None, Some(4)],
|
||||||
|
];
|
||||||
|
|
||||||
let mut log_card = LogCard::new();
|
let mut log_card = LogCard::new();
|
||||||
log_card.set_borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT));
|
log_card.set_borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT));
|
||||||
let mut console_card = ConsoleCard::new("Console", "");
|
let mut console_card = ConsoleCard::new("Console", "");
|
||||||
console_card.set_joins(Borders::TOP);
|
console_card.set_joins(Borders::TOP);
|
||||||
|
|
||||||
elements.push(Box::new(log_card));
|
elements.push(Box::new(log_card));
|
||||||
elements.push(Box::new(console_card));
|
elements.push(Box::new(console_card));
|
||||||
|
|
||||||
nav_grid.push(vec![Some(0)]);
|
let mut ram_graph = GraphCard::new(ui.clone(), GRAPHS::Ram, "RAM Usage".into());
|
||||||
nav_grid.push(vec![Some(1)]);
|
ram_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT));
|
||||||
|
elements.push(Box::new(ram_graph));
|
||||||
|
let mut cpu_graph = GraphCard::new(ui.clone(), GRAPHS::Cpu, "CPU Usage".into());
|
||||||
|
cpu_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT));
|
||||||
|
cpu_graph.set_joins(Borders::TOP);
|
||||||
|
elements.push(Box::new(cpu_graph));
|
||||||
|
let mut ping_graph = GraphCard::new(ui.clone(), GRAPHS::Ping, "Ping".into());
|
||||||
|
ping_graph.set_joins(Borders::TOP);
|
||||||
|
elements.push(Box::new(ping_graph));
|
||||||
|
|
||||||
|
let graphs_open = true;
|
||||||
|
|
||||||
let mut screen = MainScreen {
|
let mut screen = MainScreen {
|
||||||
ui,
|
|
||||||
elements,
|
elements,
|
||||||
nav_grid,
|
nav_grid,
|
||||||
selected_coords: (1, 0),
|
selected_coords: (1, 0),
|
||||||
|
graphs_open,
|
||||||
};
|
};
|
||||||
|
|
||||||
screen.focus_current();
|
screen.focus_current();
|
||||||
screen
|
screen
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +117,6 @@ impl MainScreen {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp X to row length
|
|
||||||
if let Some(row) = self.nav_grid.get(y) {
|
if let Some(row) = self.nav_grid.get(y) {
|
||||||
if x >= row.len() {
|
if x >= row.len() {
|
||||||
x = row.len() - 1;
|
x = row.len() - 1;
|
||||||
|
|
@ -129,10 +144,6 @@ impl Screen for MainScreen {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_ui(&self) -> &Arc<UI> {
|
|
||||||
&self.ui
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&self, f: &mut Frame, rect: Rect) {
|
fn render(&self, f: &mut Frame, rect: Rect) {
|
||||||
let main_block = Block::default().title("Main").borders(Borders::ALL);
|
let main_block = Block::default().title("Main").borders(Borders::ALL);
|
||||||
f.render_widget(main_block, rect);
|
f.render_widget(main_block, rect);
|
||||||
|
|
@ -142,14 +153,42 @@ impl Screen for MainScreen {
|
||||||
horizontal: 1,
|
horizontal: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
let chunks = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(inner);
|
let graphs_width = if self.graphs_open { 30 } else { 2 };
|
||||||
|
let main_width = inner.width.saturating_sub(graphs_width);
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(ratatui::layout::Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(main_width),
|
||||||
|
Constraint::Length(graphs_width),
|
||||||
|
])
|
||||||
|
.split(inner);
|
||||||
|
let left_chunks =
|
||||||
|
Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(chunks[0]);
|
||||||
|
|
||||||
if let Some(Some(index)) = self.nav_grid.get(0).and_then(|r| r.get(0)) {
|
if let Some(Some(index)) = self.nav_grid.get(0).and_then(|r| r.get(0)) {
|
||||||
self.elements[*index].as_element().render(f, chunks[0]);
|
self.elements[*index].as_element().render(f, left_chunks[0]);
|
||||||
|
}
|
||||||
|
if let Some(Some(index)) = self.nav_grid.get(1).and_then(|r| r.get(0)) {
|
||||||
|
self.elements[*index].as_element().render(f, left_chunks[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(Some(index)) = self.nav_grid.get(1).and_then(|r| r.get(0)) {
|
let graph_elements: Vec<_> = self
|
||||||
self.elements[*index].as_element().render(f, chunks[1]);
|
.elements
|
||||||
|
.iter()
|
||||||
|
.filter(|el| el.as_any().is::<GraphCard>())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let graph_chunks = Layout::vertical(
|
||||||
|
graph_elements
|
||||||
|
.iter()
|
||||||
|
.map(|_| Constraint::Length(chunks[1].height / graph_elements.len() as u16))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.split(chunks[1]);
|
||||||
|
|
||||||
|
for (el, area) in graph_elements.iter().zip(graph_chunks.iter()) {
|
||||||
|
el.as_element().render(f, *area);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,6 +198,15 @@ impl Screen for MainScreen {
|
||||||
KeyCode::Down => self.navigate(NavDirection::Down),
|
KeyCode::Down => self.navigate(NavDirection::Down),
|
||||||
KeyCode::Left => self.navigate(NavDirection::Left),
|
KeyCode::Left => self.navigate(NavDirection::Left),
|
||||||
KeyCode::Right => self.navigate(NavDirection::Right),
|
KeyCode::Right => self.navigate(NavDirection::Right),
|
||||||
|
KeyCode::Enter | KeyCode::Char(' ') if self.selected_coords.1 == 1 => {
|
||||||
|
self.graphs_open = !self.graphs_open;
|
||||||
|
for element in self.elements.iter_mut() {
|
||||||
|
if let Some(graph) = element.as_any_mut().downcast_mut::<GraphCard>() {
|
||||||
|
graph.set_open(self.graphs_open);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return InteractionResult::Handled;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let (y, x) = self.selected_coords;
|
let (y, x) = self.selected_coords;
|
||||||
if let Some(Some(index)) = self.nav_grid.get(y).and_then(|r| r.get(x)) {
|
if let Some(Some(index)) = self.nav_grid.get(y).and_then(|r| r.get(x)) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use std::{any::Any, sync::Arc};
|
use std::any::Any;
|
||||||
|
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use ratatui::{Frame, layout::Rect};
|
use ratatui::{Frame, layout::Rect};
|
||||||
|
|
||||||
use crate::gui::{interaction_result::InteractionResult, ui::UI};
|
use crate::gui::interaction_result::InteractionResult;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum NavDirection {
|
pub enum NavDirection {
|
||||||
|
|
@ -20,8 +20,6 @@ pub trait Screen: Send + Sync + Any {
|
||||||
fn as_any(&self) -> &dyn Any;
|
fn as_any(&self) -> &dyn Any;
|
||||||
fn as_any_mut(&mut self) -> &mut 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 render(&self, f: &mut Frame, rect: Rect);
|
||||||
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult;
|
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,6 @@ impl Screen for TermsCheckerScreen {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_ui(&self) -> &Arc<UI> {
|
|
||||||
&self.ui
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&self, f: &mut Frame, size: Rect) {
|
fn render(&self, f: &mut Frame, size: Rect) {
|
||||||
let mut needed_height = 5;
|
let mut needed_height = 5;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,10 +121,6 @@ impl Screen for TermsUpdaterScreen {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_ui(&self) -> &Arc<UI> {
|
|
||||||
&self.ui
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&self, f: &mut Frame, size: Rect) {
|
fn render(&self, f: &mut Frame, size: Rect) {
|
||||||
let mut needed_height = 5;
|
let mut needed_height = 5;
|
||||||
|
|
||||||
|
|
|
||||||
0
src/gui/tui.rs
Normal file
0
src/gui/tui.rs
Normal file
|
|
@ -9,16 +9,21 @@ use crossterm::event::KeyEvent;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use ratatui::{Terminal, backend::CrosstermBackend, init};
|
use ratatui::{Terminal, backend::CrosstermBackend, init};
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
io::Stdout,
|
io::Stdout,
|
||||||
sync::{Arc, Mutex},
|
sync::{
|
||||||
|
Arc, Mutex,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tokio::{sync::RwLock, time::Instant};
|
use tokio::{sync::RwLock, time::Instant};
|
||||||
|
|
||||||
/// UI state and rendering
|
/// UI state and rendering
|
||||||
pub static UNIQUE: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(true));
|
pub static UNIQUE: AtomicBool = AtomicBool::new(true);
|
||||||
|
|
||||||
|
pub static FPS: Lazy<RwLock<(f64, f64)>> = Lazy::new(|| RwLock::new((0.0, 0.0)));
|
||||||
|
|
||||||
pub static FPS: Lazy<RwLock<f64>> = Lazy::new(|| RwLock::new(0.0));
|
|
||||||
pub struct UI {
|
pub struct UI {
|
||||||
pub terminal: Arc<Mutex<Terminal<CrosstermBackend<Stdout>>>>,
|
pub terminal: Arc<Mutex<Terminal<CrosstermBackend<Stdout>>>>,
|
||||||
screen: Arc<RwLock<Option<Box<dyn Screen>>>>,
|
screen: Arc<RwLock<Option<Box<dyn Screen>>>>,
|
||||||
|
|
@ -27,26 +32,71 @@ pub struct UI {
|
||||||
pub fn start_tui() -> Arc<UI> {
|
pub fn start_tui() -> Arc<UI> {
|
||||||
let ui = Arc::new(UI::new());
|
let ui = Arc::new(UI::new());
|
||||||
let uic = ui.clone();
|
let uic = ui.clone();
|
||||||
|
ACTIVE_TASKS.insert("UI Renderer".to_string());
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
ACTIVE_TASKS.insert("UI Renderer".to_string());
|
|
||||||
let mut last_render = Instant::now();
|
let mut last_render = Instant::now();
|
||||||
let mut last: Vec<f64> = Vec::new();
|
|
||||||
|
let mut fps_samples: VecDeque<f64> = VecDeque::with_capacity(20);
|
||||||
|
let mut skip_samples: VecDeque<u16> = VecDeque::with_capacity(20);
|
||||||
|
|
||||||
|
let mut fps_sum = 0.0;
|
||||||
|
let mut skip_sum: u32 = 0;
|
||||||
|
|
||||||
|
let mut skipped = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if *SHUTDOWN.read().await {
|
if *SHUTDOWN.read().await {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if *UNIQUE.read().await {
|
if skipped > 5 || UNIQUE.load(Ordering::Relaxed) {
|
||||||
uic.render().await;
|
uic.render().await;
|
||||||
|
|
||||||
|
skip_samples.push_back(skipped);
|
||||||
|
skip_sum += skipped as u32;
|
||||||
|
|
||||||
|
if skip_samples.len() > 20 {
|
||||||
|
if let Some(old) = skip_samples.pop_front() {
|
||||||
|
skip_sum -= old as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skipped = 0;
|
||||||
|
|
||||||
let elapsed = last_render.elapsed().as_secs_f64();
|
let elapsed = last_render.elapsed().as_secs_f64();
|
||||||
if elapsed > 0.0 {
|
if elapsed > 0.0 {
|
||||||
last.push(1.0 / elapsed);
|
let fps = 1.0 / elapsed;
|
||||||
|
|
||||||
|
fps_samples.push_back(fps);
|
||||||
|
fps_sum += fps;
|
||||||
|
|
||||||
|
if fps_samples.len() > 20 {
|
||||||
|
if let Some(old) = fps_samples.pop_front() {
|
||||||
|
fps_sum -= old;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if last.len() > 10 {
|
|
||||||
last.remove(0);
|
let avg_fps = if !fps_samples.is_empty() {
|
||||||
}
|
fps_sum / fps_samples.len() as f64
|
||||||
*FPS.write().await = last.iter().sum::<f64>() / last.len() as f64;
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let avg_skips_percentage = if !skip_samples.is_empty() {
|
||||||
|
let avg_skipped = skip_sum as f64 / skip_samples.len() as f64;
|
||||||
|
let total_iterations = avg_skipped + 1.0;
|
||||||
|
(avg_skipped / total_iterations) * 100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
*FPS.write().await = (avg_fps, avg_skips_percentage);
|
||||||
|
|
||||||
last_render = Instant::now();
|
last_render = Instant::now();
|
||||||
|
UNIQUE.store(false, Ordering::Relaxed);
|
||||||
|
} else {
|
||||||
|
skipped += 1;
|
||||||
}
|
}
|
||||||
tokio::time::sleep(Duration::from_millis(16)).await;
|
tokio::time::sleep(Duration::from_millis(16)).await;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ pub static SHUTDOWN: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(false));
|
||||||
pub static RELOAD: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(true));
|
pub static RELOAD: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(true));
|
||||||
pub static ACTIVE_TASKS: Lazy<DashSet<String>> = Lazy::new(|| DashSet::new());
|
pub static ACTIVE_TASKS: Lazy<DashSet<String>> = Lazy::new(|| DashSet::new());
|
||||||
|
|
||||||
#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
|
#[tokio::main(flavor = "multi_thread", worker_threads = 16)]
|
||||||
#[allow(unused_must_use, dead_code)]
|
#[allow(unused_must_use, dead_code)]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
while *RELOAD.read().await {
|
while *RELOAD.read().await {
|
||||||
|
|
@ -57,7 +57,7 @@ async fn main() {
|
||||||
if ACTIVE_TASKS.is_empty() {
|
if ACTIVE_TASKS.is_empty() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
sleep(Duration::from_secs(1)).await;
|
sleep(Duration::from_millis(100)).await;
|
||||||
}
|
}
|
||||||
println!("You need to accept our End User Licence Agreement before launching!");
|
println!("You need to accept our End User Licence Agreement before launching!");
|
||||||
println!("You can find this at 'agreements'!");
|
println!("You can find this at 'agreements'!");
|
||||||
|
|
@ -176,7 +176,7 @@ async fn main() {
|
||||||
if !omikron.is_connected().await {
|
if !omikron.is_connected().await {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
sleep(Duration::from_secs(1)).await;
|
sleep(Duration::from_millis(100)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if *RELOAD.read().await {
|
if *RELOAD.read().await {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::{
|
||||||
fs::{self, OpenOptions},
|
fs::{self, OpenOptions},
|
||||||
io::Write,
|
io::Write,
|
||||||
path::Path,
|
path::Path,
|
||||||
sync::{OnceLock, mpsc},
|
sync::{OnceLock, atomic::Ordering, mpsc},
|
||||||
thread,
|
thread,
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
@ -122,9 +122,7 @@ pub fn log_internal_translated(
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
) {
|
) {
|
||||||
if let Some(tx) = LOGGER.get() {
|
if let Some(tx) = LOGGER.get() {
|
||||||
tokio::spawn(async move {
|
UNIQUE.store(true, Ordering::Relaxed);
|
||||||
*UNIQUE.write().await = true;
|
|
||||||
});
|
|
||||||
let _ = tx.send(LogMessage {
|
let _ = tx.send(LogMessage {
|
||||||
timestamp_ms: SystemTime::now()
|
timestamp_ms: SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|
@ -142,9 +140,7 @@ pub fn log_internal_translated(
|
||||||
|
|
||||||
pub fn log_internal(kind: PrintType, prefix: String, is_error: bool, message: String) {
|
pub fn log_internal(kind: PrintType, prefix: String, is_error: bool, message: String) {
|
||||||
if let Some(tx) = LOGGER.get() {
|
if let Some(tx) = LOGGER.get() {
|
||||||
tokio::spawn(async move {
|
UNIQUE.store(true, Ordering::Relaxed);
|
||||||
*UNIQUE.write().await = true;
|
|
||||||
});
|
|
||||||
let _ = tx.send(LogMessage {
|
let _ = tx.send(LogMessage {
|
||||||
timestamp_ms: SystemTime::now()
|
timestamp_ms: SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue