[WIP] New UI Logic

This commit is contained in:
Alex Emmet 2026-02-18 10:30:41 +01:00
commit 6db4e806f8
26 changed files with 815 additions and 674 deletions

13
logs/log_1771405721.txt Normal file
View file

@ -0,0 +1,13 @@
1771405721036 IOTA ID: 0
1771405721036 User IDS:
1771405721046 Community IDS:
1771405721047 TLS certificate 'certs/cert.pem' not found.
1771405721047 HTTP Server running on 0.0.0.0:1984
1771405721048 Communities active on ws://10.209.226.34:1984/community/...
1771405721048 Downloading ZIP file...
1771405721507 Failed to download file: Status 404 Not Found
1771405721507 Error downloading file: Failed to download file: Status 404 Not Found
1771405721507 IOTA_REGISTER_NEW
1771405721512 OMIKRON_CONNECTING
1771405721987 OMIKRON_CONNECTION_SUCCESS
1771405731879 Web Server shutdown complete.

View file

@ -160,7 +160,7 @@ impl CommunityConnection {
let user_pub_key: PublicKey = match PublicKey::from_bytes(&user_public_key_bytes) { let user_pub_key: PublicKey = match PublicKey::from_bytes(&user_public_key_bytes) {
Some(key) => key, Some(key) => key,
None => { __ => {
self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id)
.await; .await;
return; return;
@ -178,7 +178,7 @@ impl CommunityConnection {
let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) {
Some(secret) => secret, Some(secret) => secret,
None => { _ => {
self.send_error_response(&cv.get_id(), CommunicationType::error) self.send_error_response(&cv.get_id(), CommunicationType::error)
.await; .await;
return; return;
@ -223,7 +223,7 @@ impl CommunityConnection {
async fn handle_challenge_response(self: Arc<Self>, cv: CommunicationValue) { async fn handle_challenge_response(self: Arc<Self>, cv: CommunicationValue) {
let client_challenge_response_b64 = match cv.get_data(DataTypes::challenge) { let client_challenge_response_b64 = match cv.get_data(DataTypes::challenge) {
Some(data) => data.to_string(), Some(data) => data.to_string(),
None => { _ => {
self.send_error_response(&cv.get_id(), CommunicationType::error) self.send_error_response(&cv.get_id(), CommunicationType::error)
.await; .await;
return; return;
@ -273,7 +273,7 @@ impl CommunityConnection {
let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) {
Some(secret) => secret, Some(secret) => secret,
None => { _ => {
self.send_error_response(&cv.get_id(), CommunicationType::error) self.send_error_response(&cv.get_id(), CommunicationType::error)
.await; .await;
return; return;

View file

@ -1,5 +1,5 @@
use crate::communities::community::{self, Community}; use crate::communities::community::{self, Community};
use crate::gui::log_panel; use crate::log;
use crate::util::file_util; use crate::util::file_util;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::collections::HashMap; use std::collections::HashMap;
@ -35,7 +35,7 @@ pub async fn load_communities() {
if let Some(community) = community::load(&name).await { if let Some(community) = community::load(&name).await {
add_community(community).await; add_community(community).await;
} else { } else {
log_panel::log_message(format!("failed to load the {} community", &name)); log!("failed to load the {} community", &name);
} }
} }
} }

View file

@ -4,7 +4,7 @@ use crate::{
interactables::interactable::Interactable, interactables::interactable::Interactable,
}, },
data::communication::{CommunicationType, CommunicationValue, DataTypes}, data::communication::{CommunicationType, CommunicationValue, DataTypes},
gui::log_panel::log_message, log,
util::file_util::{get_children, load_file, save_file}, util::file_util::{get_children, load_file, save_file},
}; };
use async_trait::async_trait; use async_trait::async_trait;
@ -37,7 +37,7 @@ impl TextChat {
); );
if let Err(e) = fs::create_dir_all(user_dir) { if let Err(e) = fs::create_dir_all(user_dir) {
log_message(format!("Failed to create chat directory: {}", e)); log!("Failed to create chat directory: {}", e);
return; return;
} }
@ -56,16 +56,15 @@ impl TextChat {
break; break;
} }
} else { } else {
log_message(format!("Failed to parse existing JSON file: {}", file_name)); log!("Failed to parse existing JSON file: {}", file_name);
} }
} else { } else {
// New file, use empty array
break; break;
} }
chunk_index += 1; chunk_index += 1;
if chunk_index > 1000 { if chunk_index > 1000 {
log_message(format!("Too many message chunks. Aborting add.")); log!("Too many message chunks. Aborting add.");
return; return;
} }
} }
@ -77,12 +76,12 @@ impl TextChat {
}; };
if let Err(e) = message_chunk.push(json_obj) { if let Err(e) = message_chunk.push(json_obj) {
log_message(format!("Failed to push new message into JSON array: {}", e)); log!("Failed to push new message into JSON array: {}", e);
return; return;
} }
let file_name = format!("msgs_{}.json", chunk_index); let file_name = format!("msgs_{}.json", chunk_index);
log_message(format!("Saving message to {}/{}", user_dir, file_name)); log!("Saving message to {}/{}", user_dir, file_name);
save_file(&user_dir, &file_name, &message_chunk.dump()); save_file(&user_dir, &file_name, &message_chunk.dump());
} }
pub fn get_messages(&self, loaded_messages: i64, amount: i64) -> JsonValue { pub fn get_messages(&self, loaded_messages: i64, amount: i64) -> JsonValue {

View file

@ -1,10 +1,10 @@
use std::collections::VecDeque; use crate::gui::elements::log_card::UiLogEntry;
use json::{JsonValue, object}; use json::{JsonValue, object};
use std::collections::VecDeque;
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
pub logs: VecDeque<String>, pub logs: VecDeque<UiLogEntry>,
pub cpu: Vec<(f64, f64)>, pub cpu: Vec<(f64, f64)>,
pub ram: Vec<(f64, f64)>, pub ram: Vec<(f64, f64)>,
pub ping: 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 { if self.logs.len() >= MAX_LOGS {
self.logs.pop_front(); self.logs.pop_front();
} }
self.logs.push_back(msg); self.logs.push_back(msg);
} }
pub fn get_logs(&self) -> &VecDeque<UiLogEntry> {
&self.logs
}
pub fn push_cpu(&mut self, pt: (f64, f64)) { pub fn push_cpu(&mut self, pt: (f64, f64)) {
self.cpu.push(pt); self.cpu.push(pt);
if self.cpu.len() > MAX_POINTS { if self.cpu.len() > MAX_POINTS {
@ -112,28 +116,23 @@ impl AppState {
let len = data.len(); let len = data.len();
if len >= width_usize { if len >= width_usize {
// Trim data to fit
data[len - width_usize..].to_vec() data[len - width_usize..].to_vec()
} else { } else {
let mut result = Vec::with_capacity(width_usize); 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 dx = 1.0;
let pad_len = width_usize - len; 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 let start_x = data
.first() .first()
.map(|(x, _)| x - (dx * pad_len as f64)) .map(|(x, _)| x - (dx * pad_len as f64))
.unwrap_or(0.0); .unwrap_or(0.0);
let _ = data.first().map(|(_, y)| *y).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 { for i in 0..pad_len {
result.push((start_x + i as f64 * dx, -1 as f64)); result.push((start_x + i as f64 * dx, -1 as f64));
} }
// Then append the real data
result.extend_from_slice(data); result.extend_from_slice(data);
result result
} }

View 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);
}

View 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;
}
}

View file

@ -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;
}
_ => {}
}
}

View 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"),
}
}
}

View file

@ -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()));
}
});
}

View file

@ -1,8 +1,10 @@
pub mod app_state; pub mod elements {
pub mod log_panel; pub mod elements;
pub mod settings_panel; pub mod log_card;
pub mod widgets {
pub mod betterblock;
} }
pub mod input_handler; pub mod screens {
pub mod tui; pub mod screens;
}
pub mod app_state;
pub mod interaction_result;
pub mod ui;

View 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;
}

View file

@ -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()]);
}

View file

@ -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
View 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
}
}

View file

@ -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);
}
}

View file

@ -20,12 +20,8 @@ mod util;
use crate::communities::community_manager; use crate::communities::community_manager;
use crate::communities::interactables::registry; use crate::communities::interactables::registry;
use crate::gui::app_state::AppState; use crate::gui::app_state::AppState;
use crate::gui::input_handler; use crate::gui::ui;
use crate::gui::log_panel;
use crate::gui::log_panel::{log_message, log_message_trans};
use crate::gui::tui;
use crate::langu::language_creator; use crate::langu::language_creator;
use crate::langu::language_manager::format;
use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection}; use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection};
use crate::server::server::start; use crate::server::server::start;
use crate::terms::consent_state; use crate::terms::consent_state;
@ -33,6 +29,7 @@ use crate::users::user_manager;
use crate::util::config_util::CONFIG; use crate::util::config_util::CONFIG;
use crate::util::file_util::download_and_extract_zip; use crate::util::file_util::download_and_extract_zip;
use crate::util::file_util::has_dir; use crate::util::file_util::has_dir;
use crate::util::logger;
pub static APP_STATE: LazyLock<Arc<Mutex<AppState>>> = pub static APP_STATE: LazyLock<Arc<Mutex<AppState>>> =
LazyLock::new(|| Arc::new(Mutex::new(AppState::new()))); LazyLock::new(|| Arc::new(Mutex::new(AppState::new())));
@ -48,6 +45,9 @@ async fn main() {
*RELOAD.write().await = false; *RELOAD.write().await = false;
*SHUTDOWN.write().await = false; *SHUTDOWN.write().await = false;
let tui = ui::UI::new(100, 100);
tokio::spawn(async move { tui.render() });
// EULA // EULA
let (tos, pp) = consent_state::ConsentManager::check().await; let (tos, pp) = consent_state::ConsentManager::check().await;
if !tos { if !tos {
@ -71,16 +71,14 @@ async fn main() {
} }
// UI // UI
log_panel::setup(); logger::startup();
tui::start_tui();
input_handler::setup_input_handler();
// BASIC CONFIGURATION // BASIC CONFIGURATION
&CONFIG.write().await.load(); &CONFIG.write().await.load();
// USER MANAGEMENT // USER MANAGEMENT
if let Err(_) = user_manager::load_users().await { if let Err(_) = user_manager::load_users().await {
log_message_trans("user_load_failed"); log_t!("user_load_failed");
} }
let mut sb = "".to_string(); let mut sb = "".to_string();
@ -94,11 +92,11 @@ async fn main() {
sb.remove(0); sb.remove(0);
sb = sb + ","; sb = sb + ",";
} }
log_message(format!( log!(
"IOTA ID: {}", "IOTA ID: {}",
CONFIG.read().await.get_iota_id().to_string() CONFIG.read().await.get_iota_id().to_string()
)); );
log_message(format!("User IDS: {}", sb)); log!("User IDS: {}", sb);
// COMMUNITY MANAGEMENT // COMMUNITY MANAGEMENT
registry::load_interactables().await; registry::load_interactables().await;
@ -113,7 +111,7 @@ async fn main() {
sb1.remove(0); sb1.remove(0);
sb1 = sb1 + ","; sb1 = sb1 + ",";
} }
log_message(format!("Community IDS: {}", sb1)); log!("Community IDS: {}", sb1);
let port = CONFIG.read().await.get_port(); let port = CONFIG.read().await.get_port();
let mut ip = "0.0.0.0".to_string(); let mut ip = "0.0.0.0".to_string();
for iface in pnet::datalink::interfaces() { for iface in pnet::datalink::interfaces() {
@ -127,12 +125,12 @@ async fn main() {
} }
} }
if start(port).await { if start(port).await {
log_message(format("community_active", &[&ip, &port.to_string()])); log_t!("community_active", ip, port.to_string());
} else { } else {
if port < 1024 { if port < 1024 {
log_message(format("community_start_error_admin", &[&port.to_string()])); log_t!("community_start_error_admin", port.to_string());
} else { } else {
log_message(format("community_start_error", &[&port.to_string()])); log_t!("community_start_error", port.to_string());
} }
} }
if !has_dir("web") { if !has_dir("web") {
@ -150,7 +148,7 @@ async fn main() {
omikron.connect().await; omikron.connect().await;
let mut omikron_connection = OMIKRON_CONNECTION.write().await; let mut omikron_connection = OMIKRON_CONNECTION.write().await;
*omikron_connection = Some(omikron.clone()); *omikron_connection = Some(omikron.clone());
log_message_trans("setup_completed"); log_t!("setup_completed");
loop { loop {
if *SHUTDOWN.read().await { if *SHUTDOWN.read().await {
break; break;

View file

@ -1,15 +1,14 @@
use crate::gui::log_panel::{log_cv, log_message_format};
use crate::users::contact::Contact; use crate::users::contact::Contact;
use crate::users::user_community_util::UserCommunityUtil; use crate::users::user_community_util::UserCommunityUtil;
use crate::util::chat_files::{MessageState, change_message_state}; use crate::util::chat_files::{MessageState, change_message_state};
use crate::util::chats_util::{get_user, mod_user}; use crate::util::chats_util::{get_user, mod_user};
use crate::util::crypto_util::{DataFormat, SecurePayload}; use crate::util::crypto_util::{DataFormat, SecurePayload};
use crate::util::file_util::{get_children, load_file, save_file}; use crate::util::file_util::{get_children, load_file, save_file};
use crate::util::logger::PrintType;
use crate::util::{chat_files, chats_util}; use crate::util::{chat_files, chats_util};
use crate::{ACTIVE_TASKS, SHUTDOWN}; use crate::{ACTIVE_TASKS, SHUTDOWN, log, log_cv, log_t};
use crate::{ use crate::{
data::communication::{CommunicationType, CommunicationValue, DataTypes}, data::communication::{CommunicationType, CommunicationValue, DataTypes},
gui::log_panel::{log_message, log_message_trans},
util::{config_util::CONFIG, crypto_helper}, util::{config_util::CONFIG, crypto_helper},
}; };
use dashmap::DashMap; use dashmap::DashMap;
@ -26,6 +25,7 @@ use tokio::time::{Duration, Instant, sleep};
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
use tungstenite::Utf8Bytes; use tungstenite::Utf8Bytes;
use uuid::Uuid; use uuid::Uuid;
use warp::filters::log::log;
pub static OMIKRON_CONNECTION: LazyLock<Arc<RwLock<Option<Arc<OmikronConnection>>>>> = pub static OMIKRON_CONNECTION: LazyLock<Arc<RwLock<Option<Arc<OmikronConnection>>>>> =
LazyLock::new(|| Arc::new(RwLock::new(None))); LazyLock::new(|| Arc::new(RwLock::new(None)));
@ -66,7 +66,7 @@ impl OmikronConnection {
drop(conf); drop(conf);
if iota_id == 0 || public_key.is_none() || private_key.is_none() { if iota_id == 0 || public_key.is_none() || private_key.is_none() {
log_message_trans("iota_register_new"); log_t!("iota_register_new");
let key_pair = crypto_helper::generate_keypair(); let key_pair = crypto_helper::generate_keypair();
let public_key_base64 = crypto_helper::public_key_to_base64(&key_pair.public); let public_key_base64 = crypto_helper::public_key_to_base64(&key_pair.public);
let private_key_base64 = crypto_helper::secret_key_to_base64(&key_pair.secret); let private_key_base64 = crypto_helper::secret_key_to_base64(&key_pair.secret);
@ -97,10 +97,10 @@ impl OmikronConnection {
conf_write.change("iota_id", iota_json.clone()); conf_write.change("iota_id", iota_json.clone());
conf_write.update(); conf_write.update();
drop(conf_write); drop(conf_write);
log_message(format!("Registered with Iota-ID: {}", iota_id)); log!("Registered with Iota-ID: {}", iota_id);
} }
Err(timeout) => { Err(timeout) => {
log_message(timeout); log!("{}", timeout);
} }
} }
} }
@ -121,7 +121,7 @@ impl OmikronConnection {
if self.is_connected().await { if self.is_connected().await {
return true; return true;
} }
log_message_trans("omikron_connecting"); log_t!("omikron_connecting");
let conf = CONFIG.read().await; let conf = CONFIG.read().await;
let addr = conf let addr = conf
@ -130,11 +130,11 @@ impl OmikronConnection {
.unwrap_or("wss://app.tensamin.net/ws/iota/"); .unwrap_or("wss://app.tensamin.net/ws/iota/");
let stream_res = connect_async(addr).await; let stream_res = connect_async(addr).await;
if let Err(e) = stream_res { if let Err(e) = stream_res {
log_message(format!("con error {}", e.to_string())); log!("con error {}", e.to_string());
return false; return false;
} }
let (stream, _) = stream_res.unwrap(); let (stream, _) = stream_res.unwrap();
log_message_trans("omikron_connection_success"); log_t!("omikron_connection_success");
let (write_half, read_half) = stream.split(); let (write_half, read_half) = stream.split();
@ -196,7 +196,7 @@ impl OmikronConnection {
); );
} }
*is_connected_out.lock().await = false; *is_connected_out.lock().await = false;
log_message("Connection closed."); log!("Connection closed.");
}); });
{ {
ACTIVE_TASKS ACTIVE_TASKS
@ -219,7 +219,7 @@ impl OmikronConnection {
tokio::spawn(async move { tokio::spawn(async move {
match msg { match msg {
Ok(Message::Close(Some(frame))) => { Ok(Message::Close(Some(frame))) => {
log_message(format!("[Omikron] Closed: {:?}", frame)); log!("[Omikron] Closed: {:?}", frame);
*is_connected.lock().await = false; *is_connected.lock().await = false;
return; return;
} }
@ -271,7 +271,7 @@ impl OmikronConnection {
self.send_message(&response).await; self.send_message(&response).await;
} else { } else {
log_message("Failed to decrypt challenge"); log!("Failed to decrypt challenge");
} }
return; return;
@ -286,9 +286,8 @@ impl OmikronConnection {
let mut conf = CONFIG.write().await; let mut conf = CONFIG.write().await;
conf.change("iota_id", JsonValue::Number(iota_id.into())); conf.change("iota_id", JsonValue::Number(iota_id.into()));
conf.update(); conf.update();
log_message(format!("Iota registered with ID: {}", iota_id)); log!("Iota registered with ID: {}", iota_id);
// Now, proceed to login
let login_message = let login_message =
CommunicationValue::new(CommunicationType::identification) CommunicationValue::new(CommunicationType::identification)
.add_data( .add_data(
@ -301,13 +300,13 @@ impl OmikronConnection {
self_clone.send_message(&login_message).await; self_clone.send_message(&login_message).await;
}); });
} else { } else {
log_message("Iota registration failed."); log("Iota registration failed.");
} }
return; return;
} }
if cv.is_type(CommunicationType::identification_response) { if cv.is_type(CommunicationType::identification_response) {
if let Some(accepted) = cv.get_data(DataTypes::accepted) { if let Some(accepted) = cv.get_data(DataTypes::accepted) {
log_message(format!("Omikron connected: {}", accepted.to_string())); log!("Omikron connected: {}", accepted.to_string());
} }
return; return;
} }
@ -315,7 +314,7 @@ impl OmikronConnection {
// ************************************************ // // ************************************************ //
// Direct messages // // Direct messages //
// ************************************************ // // ************************************************ //
log_cv(&cv); log_cv!(PrintType::General, &cv);
if let Some((_, y)) = waiting.remove(&cv.get_id()) { if let Some((_, y)) = waiting.remove(&cv.get_id()) {
y(cv); y(cv);
return; return;
@ -699,7 +698,7 @@ impl OmikronConnection {
} }
} }
Err(e) => { Err(e) => {
log_message(format!("[Omikron] Error: {}", e)); log!("Omikron] Error: {}", e);
*is_connected.lock().await = false; *is_connected.lock().await = false;
return; return;
} }
@ -721,17 +720,17 @@ impl OmikronConnection {
Ok(_) => match writer.flush().await { Ok(_) => match writer.flush().await {
Ok(_) => return, Ok(_) => return,
Err(e) => { Err(e) => {
log_message_format("send_message_failed", &[&e.to_string()]); log_t!("send_message_failed", e.to_string());
*connected.lock().await = false; *connected.lock().await = false;
} }
}, },
Err(e) => { Err(e) => {
log_message_format("send_message_failed", &[&e.to_string()]); log_t!("send_message_failed", e.to_string());
*connected.lock().await = false; *connected.lock().await = false;
} }
} }
} else { } else {
log_message_format("send_message_failed", &["Immutable Writer"]); log_t!("send_message_failed", "Immutable Writer".to_string());
*connected.lock().await = false; *connected.lock().await = false;
} }
} }
@ -750,7 +749,7 @@ impl OmikronConnection {
let inner_tx = task_tx.clone(); let inner_tx = task_tx.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = inner_tx.send(response_cv).await { if let Err(e) = inner_tx.send(response_cv).await {
log_message(format!("Failed to send response back to awaiter: {}", &e)); log_t!("Failed to send response back to awaiter: {}", e.to_string());
} }
}); });
}), }),

View file

@ -133,7 +133,7 @@ async fn users_add(
let username = match payload.get("username").and_then(|v| v.as_str()) { let username = match payload.get("username").and_then(|v| v.as_str()) {
Some(u) => u, Some(u) => u,
None => return error(), _ => return error(),
}; };
if let (Some(user), Some(_)) = crate::users::user_manager::create_user(username).await { if let (Some(user), Some(_)) = crate::users::user_manager::create_user(username).await {

View file

@ -1,13 +1,11 @@
use crate::gui::log_panel::log_message; use crate::log;
use crate::server::api::api_config; use crate::server::api::api_config;
use crate::server::socket::WsSession; use crate::server::socket::WsSession;
use crate::server::web_path_parser; use crate::server::web_path_parser;
use crate::util::file_util::load_file_buf; use crate::util::file_util::load_file_buf;
use crate::{ACTIVE_TASKS, SHUTDOWN}; use crate::{ACTIVE_TASKS, SHUTDOWN};
use actix_web::{App, Error, HttpRequest, HttpServer, Responder, dev::ServerHandle, web}; use actix_web::{App, Error, HttpRequest, HttpServer, Responder, dev::ServerHandle, web};
use actix_web_actors::ws; use actix_web_actors::ws;
use rustls::ServerConfig; use rustls::ServerConfig;
use rustls::pki_types::{CertificateDer, PrivateKeyDer}; use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use std::{ use std::{
@ -20,7 +18,7 @@ use std::{
async fn ws_handler(req: HttpRequest, stream: web::Payload) -> Result<impl Responder, Error> { async fn ws_handler(req: HttpRequest, stream: web::Payload) -> Result<impl Responder, Error> {
let path = req.path().to_string(); let path = req.path().to_string();
log_message(format!("WS connection from {:?}", req.peer_addr())); log!("WS connection from {:?}", req.peer_addr());
let session = WsSession::new(path); let session = WsSession::new(path);
ws::start(session, &req, stream) ws::start(session, &req, stream)
} }
@ -33,7 +31,7 @@ pub async fn start(port: u16) -> bool {
let _ = tokio::spawn(async move { let _ = tokio::spawn(async move {
let server = match load_tls_config() { let server = match load_tls_config() {
Ok(Some(tls_config)) => { Ok(Some(tls_config)) => {
log_message(format!("HTTPS (HTTP/2) Server running on 0.0.0.0:{}", port)); log!("HTTPS (HTTP/2) Server running on 0.0.0.0:{}", port);
let _config = (*tls_config).clone(); let _config = (*tls_config).clone();
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
@ -46,8 +44,8 @@ pub async fn start(port: u16) -> bool {
.unwrap() .unwrap()
.run() .run()
} }
Ok(None) => { Ok(_) => {
log_message(format!("HTTP Server running on 0.0.0.0:{}", port)); log!("HTTP Server running on 0.0.0.0:{}", port);
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.app_data(web::Data::new(false)) .app_data(web::Data::new(false))
@ -60,7 +58,7 @@ pub async fn start(port: u16) -> bool {
.run() .run()
} }
Err(e) => { Err(e) => {
log_message(format!("TLS config error: {}", e)); log!("TLS config error: {}", e);
return; return;
} }
}; };
@ -71,7 +69,7 @@ pub async fn start(port: u16) -> bool {
ACTIVE_TASKS.lock().unwrap().push("WebServer".into()); ACTIVE_TASKS.lock().unwrap().push("WebServer".into());
server.await.unwrap(); server.await.unwrap();
ACTIVE_TASKS.lock().unwrap().retain(|t| t != "WebServer"); ACTIVE_TASKS.lock().unwrap().retain(|t| t != "WebServer");
log_message("Web Server shutdown complete."); log!("Web Server shutdown complete.");
}); });
if let Ok(server_handle) = rx.await { if let Ok(server_handle) = rx.await {
@ -87,7 +85,7 @@ pub async fn start(port: u16) -> bool {
async fn wait_for_shutdown(server_handle: ServerHandle) { async fn wait_for_shutdown(server_handle: ServerHandle) {
loop { loop {
if *SHUTDOWN.read().await { if *SHUTDOWN.read().await {
log_message("Shutdown signal received."); log!("Shutdown signal received.");
server_handle.stop(true).await; server_handle.stop(true).await;
break; break;
} }
@ -102,7 +100,7 @@ fn load_tls_config() -> Result<Option<Arc<ServerConfig>>, Box<dyn StdError>> {
let cert_file_buf = match cert_file_res { let cert_file_buf = match cert_file_res {
Ok(b) => b, Ok(b) => b,
Err(e) if e.kind() == ErrorKind::NotFound => { Err(e) if e.kind() == ErrorKind::NotFound => {
log_message("TLS certificate 'certs/cert.pem' not found."); log!("TLS certificate 'certs/cert.pem' not found.");
return Ok(None); return Ok(None);
} }
Err(e) => return Err(e.into()), // Other IO error Err(e) => return Err(e.into()), // Other IO error
@ -111,7 +109,7 @@ fn load_tls_config() -> Result<Option<Arc<ServerConfig>>, Box<dyn StdError>> {
let key_file_buf = match key_file_res { let key_file_buf = match key_file_res {
Ok(b) => b, Ok(b) => b,
Err(e) if e.kind() == ErrorKind::NotFound => { Err(e) if e.kind() == ErrorKind::NotFound => {
log_message("TLS key 'certs/cert.key' not found."); log!("TLS key 'certs/cert.key' not found.");
return Ok(None); return Ok(None);
} }
Err(e) => return Err(e.into()), // Other IO error Err(e) => return Err(e.into()), // Other IO error

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
data::communication::{CommunicationType, CommunicationValue}, data::communication::{CommunicationType, CommunicationValue},
gui::log_panel::log_message, log,
}; };
use actix::{Actor, ActorContext, AsyncContext, StreamHandler}; use actix::{Actor, ActorContext, AsyncContext, StreamHandler};
use actix_web_actors::ws; use actix_web_actors::ws;
@ -57,21 +57,21 @@ impl Actor for WsSession {
fn started(&mut self, ctx: &mut Self::Context) { fn started(&mut self, ctx: &mut Self::Context) {
self.start_heartbeat(ctx); self.start_heartbeat(ctx);
log_message(format!("WebSocket session started for path: {}", self.path)); log!("WebSocket session started for path: {}", self.path);
if self.path.starts_with("/ws/users/") { if self.path.starts_with("/ws/users/") {
log_message(format!("UserConnection handling is not yet implemented.",)); log!("UserConnection handling is not yet implemented.");
} else if self.path.starts_with("/ws/community/") { } else if self.path.starts_with("/ws/community/") {
let community_id = self.path.split('/').nth(3).unwrap_or_default(); let community_id = self.path.split('/').nth(3).unwrap_or_default();
log_message(format!( log!(
"CommunityConnection handling for {} is not yet implemented.", "CommunityConnection handling for {} is not yet implemented.",
community_id community_id
)); );
} }
} }
fn stopped(&mut self, _ctx: &mut Self::Context) { fn stopped(&mut self, _ctx: &mut Self::Context) {
log_message(format!("WebSocket session stopped for path: {}", self.path)); log!("WebSocket session stopped for path: {}", self.path);
} }
} }

View file

@ -1,5 +1,5 @@
use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes}; use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes};
use crate::gui::log_panel::log_message; use crate::log;
use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection}; use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection};
use crate::users::user_profile::UserProfile; use crate::users::user_profile::UserProfile;
use crate::util::crypto_helper::{self, public_key_to_base64}; use crate::util::crypto_helper::{self, public_key_to_base64};
@ -113,7 +113,7 @@ pub async fn create_user(username: &str) -> (Option<UserProfile>, Option<String>
} }
*SHUTDOWN.write().await = true; *SHUTDOWN.write().await = true;
*RELOAD.write().await = true; *RELOAD.write().await = true;
log_message("Created User"); log!("Created User");
save_file( save_file(
"", "",
&format!("{}.tu", username), &format!("{}.tu", username),

View file

@ -3,7 +3,7 @@ use json::{self, JsonValue, array, object};
use std::fs::{self}; use std::fs::{self};
use std::path::Path; use std::path::Path;
use crate::gui::log_panel::log_message; use crate::log;
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub enum MessageState { pub enum MessageState {
@ -58,7 +58,7 @@ pub fn add_message(
); );
if let Err(e) = fs::create_dir_all(&user_dir) { if let Err(e) = fs::create_dir_all(&user_dir) {
log_message(format!("Failed to create chat directory: {}", e)); log!("Failed to create chat directory: {}", e);
return; return;
} }
@ -77,7 +77,7 @@ pub fn add_message(
break; break;
} }
} else { } else {
log_message(format!("Failed to parse existing JSON file: {}", file_name)); log!("Failed to parse existing JSON file: {}", file_name);
} }
} else { } else {
break; break;
@ -85,7 +85,7 @@ pub fn add_message(
chunk_index += 1; chunk_index += 1;
if chunk_index > 1000 { if chunk_index > 1000 {
log_message(format!("Too many message chunks. Aborting add.")); log!("Too many message chunks. Aborting add.");
return; return;
} }
} }
@ -98,7 +98,7 @@ pub fn add_message(
}; };
if let Err(e) = message_chunk.push(json_obj) { if let Err(e) = message_chunk.push(json_obj) {
log_message(format!("Failed to push new message into JSON array: {}", e)); log!("Failed to push new message into JSON array: {}", e);
return; return;
} }

View file

@ -9,7 +9,7 @@ use uuid::Uuid;
use walkdir::WalkDir; use walkdir::WalkDir;
use zip::ZipArchive; use zip::ZipArchive;
use crate::gui::log_panel::log_message; use crate::log;
#[allow(dead_code)] #[allow(dead_code)]
pub fn delete_directory(path: &str) -> bool { pub fn delete_directory(path: &str) -> bool {
@ -23,11 +23,11 @@ fn delete_dir_recursive(directory: &Path) -> bool {
return false; return false;
} }
if let Err(e) = fs::remove_dir_all(directory) { if let Err(e) = fs::remove_dir_all(directory) {
log_message(format!( log!(
"[IMPORTANT] Couldn't delete directory {}: {}", "[IMPORTANT] Couldn't delete directory {}: {}",
directory.display(), directory.display(),
e, e,
)); );
return false; return false;
} }
true true
@ -97,7 +97,7 @@ pub fn load_file(path: &str, name: &str) -> String {
if !dir.exists() { if !dir.exists() {
if let Err(e) = fs::create_dir_all(&dir) { if let Err(e) = fs::create_dir_all(&dir) {
log_message(format!("[IMPORTANT] Couldn't create directories: {}", e)); log!("[IMPORTANT] Couldn't create directories: {}", e);
return String::new(); return String::new();
} }
return String::new(); return String::new();
@ -105,7 +105,7 @@ pub fn load_file(path: &str, name: &str) -> String {
if !file_path.exists() { if !file_path.exists() {
if let Err(e) = File::create(&file_path) { if let Err(e) = File::create(&file_path) {
log_message(format!("[IMPORTANT] Couldn't create file: {}", e)); log!("[IMPORTANT] Couldn't create file: {}", e);
} }
return String::new(); return String::new();
} }
@ -130,17 +130,17 @@ pub fn save_file(path: &str, name: &str, value: &str) {
if !dir.exists() { if !dir.exists() {
if let Err(e) = fs::create_dir_all(&dir) { if let Err(e) = fs::create_dir_all(&dir) {
log_message(format!("[IMPORTANT] Couldn't create directories: {}", e)); log!("[IMPORTANT] Couldn't create directories: {}", e);
return; return;
} }
} }
if let Err(e) = fs::write(&file_path, value) { if let Err(e) = fs::write(&file_path, value) {
log_message(format!( log!(
"[IMPORTANT] Couldn't write file {}: {}", "[IMPORTANT] Couldn't write file {}: {}",
file_path.display(), file_path.display(),
e e
)); );
} }
} }
@ -231,7 +231,7 @@ pub async fn download_zip(url: &str, zip_path: &Path) -> Result<(), Box<dyn std:
if !response.status().is_success() { if !response.status().is_success() {
let err_msg = format!("Failed to download file: Status {}", response.status()); let err_msg = format!("Failed to download file: Status {}", response.status());
log_message(err_msg.clone()); log!("{}", err_msg.clone());
return Err(err_msg.into()); return Err(err_msg.into());
} }
@ -314,7 +314,7 @@ fn extract_zip_contents_to_folder(
} }
} }
log_message("Extracting directly (no single root folder detected)."); log!("Extracting directly (no single root folder detected).");
let _ = fs::remove_dir_all(target_dir); let _ = fs::remove_dir_all(target_dir);
fs::rename(&staging_dir, target_dir)?; fs::rename(&staging_dir, target_dir)?;
@ -323,14 +323,14 @@ fn extract_zip_contents_to_folder(
#[allow(dead_code)] #[allow(dead_code)]
pub async fn download_and_extract_zip(url: &str, as_name: &str) { pub async fn download_and_extract_zip(url: &str, as_name: &str) {
log_message("Downloading ZIP file..."); log!("Downloading ZIP file...");
let base_dir = PathBuf::from(get_directory()); let base_dir = PathBuf::from(get_directory());
let zip_filename = format!("{}.zip", Uuid::new_v4()); let zip_filename = format!("{}.zip", Uuid::new_v4());
let zip_path = base_dir.join(&zip_filename); let zip_path = base_dir.join(&zip_filename);
let target_dir = base_dir.join(as_name); let target_dir = base_dir.join(as_name);
if let Err(e) = download_zip(url, &zip_path).await { if let Err(e) = download_zip(url, &zip_path).await {
log_message(format!("Error downloading file: {}", e)); log!("Error downloading file: {}", e);
return; return;
} }
@ -341,18 +341,14 @@ pub async fn download_and_extract_zip(url: &str, as_name: &str) {
let successful = match extract_result { let successful = match extract_result {
Ok(()) => true, Ok(()) => true,
Err(e) => { Err(e) => {
log_message(format!("Error during ZIP extraction: {}", e)); log!("Error during ZIP extraction: {}", e);
false false
} }
}; };
if let Err(e) = tokio::fs::remove_file(&zip_path).await { if let Err(e) = tokio::fs::remove_file(&zip_path).await {
log_message(format!( log!("Error cleaning up ZIP file {}: {}", zip_path.display(), e);
"Error cleaning up ZIP file {}: {}",
zip_path.display(),
e
));
} else if successful { } else if successful {
log_message("Downloaded and extracted ZIP file successfully."); log!("Downloaded and extracted ZIP file successfully.");
} }
} }

298
src/util/logger.rs Normal file
View file

@ -0,0 +1,298 @@
use std::{
fs::{self, OpenOptions},
io::Write,
path::Path,
sync::{OnceLock, mpsc},
thread,
time::{SystemTime, UNIX_EPOCH},
};
use ratatui::style::Color;
use crate::{APP_STATE, gui::elements::log_card::UiLogEntry, langu::language_manager};
static LOGGER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new();
#[derive(Clone, Copy)]
#[allow(unused)]
pub enum PrintType {
Call,
Client,
Iota,
Omikron,
Omega,
General,
}
struct LogMessage {
timestamp_ms: u128,
prefix: &'static str,
kind: PrintType,
is_error: bool,
translation_key: Option<String>,
format_args: Vec<String>,
message: Option<String>,
}
pub fn startup() {
let (tx, rx) = mpsc::channel::<LogMessage>();
LOGGER.set(tx).expect("Logger already initialized");
thread::spawn(move || {
let log_dir = Path::new("logs");
fs::create_dir_all(log_dir).expect("Failed to create log directory");
let start_ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let path = log_dir.join(format!("log_{}.txt", start_ts));
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.expect("Failed to open log file");
for msg in rx {
let resolved_message = if let Some(key) = msg.translation_key {
let args: Vec<&str> = msg.format_args.iter().map(|s| s.as_str()).collect();
language_manager::format(&key, &args)
} else {
msg.message.unwrap_or_default()
};
let ts = fixed_box(&msg.timestamp_ms.to_string(), 13);
let line = format!("{} {}", msg.prefix, resolved_message);
let _ = writeln!(file, "{} {}", ts, line);
let color = colorize(msg.kind, msg.is_error);
let ui_entry = UiLogEntry {
line: line.clone(),
color,
};
{
let mut state = APP_STATE.lock().unwrap();
state.push_log(ui_entry);
}
}
});
}
fn colorize(kind: PrintType, is_error: bool) -> Color {
if is_error {
return Color::Red;
}
match kind {
PrintType::Call => Color::Magenta,
PrintType::Client => Color::Green,
PrintType::Iota => Color::Yellow,
PrintType::Omikron => Color::Blue,
PrintType::Omega => Color::Cyan,
PrintType::General => Color::White,
}
}
fn fixed_box(content: &str, width: usize) -> String {
let s: String = content.chars().take(width).collect();
let len = s.chars().count();
if len < width {
format!("[{}{}]", " ".repeat(width - len), s)
} else {
s
}
}
pub fn log_internal_translated(
kind: PrintType,
prefix: &'static str,
is_error: bool,
key: &str,
args: Vec<String>,
) {
if let Some(tx) = LOGGER.get() {
let _ = tx.send(LogMessage {
timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
prefix,
kind,
is_error,
translation_key: Some(key.to_string()),
format_args: args,
message: None,
});
}
}
pub fn log_internal(kind: PrintType, prefix: &'static str, is_error: bool, message: String) {
if let Some(tx) = LOGGER.get() {
let _ = tx.send(LogMessage {
timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
prefix,
kind,
is_error,
translation_key: None,
format_args: Vec::new(),
message: Some(message),
});
}
}
use crate::data::communication::CommunicationValue;
use json::JsonValue;
pub fn log_cv_internal(cv: &CommunicationValue, print_type: Option<PrintType>) {
let formatted = format_cv(cv);
log_internal(
print_type.unwrap_or(PrintType::General),
"",
false,
formatted,
);
}
pub fn format_cv(cv: &CommunicationValue) -> String {
let mut parts = Vec::new();
let sender = cv.get_sender();
let receiver = cv.get_receiver();
if sender > 0 && receiver > 0 {
parts.push(format!("{} > {}", sender, receiver));
} else if sender > 0 {
parts.push(format!("{}", sender));
} else if receiver > 0 {
parts.push(format!("> {}", receiver));
}
let comm_type = cv.get_type().to_string();
parts.push(format!("{}", comm_type));
let mut data_parts = Vec::new();
if let JsonValue::Object(data) = &cv.clone().to_json()["data"] {
for (key, value) in data.iter() {
let val_string = match value {
JsonValue::String(s) => s.clone(),
_ => value.dump(),
};
data_parts.push(format!("{} {}", key, val_string));
}
}
if !data_parts.is_empty() {
parts.push(format!("{}", data_parts.join(", ")));
}
parts.join(": ")
}
#[macro_export]
macro_rules! log_cv {
($kind:expr, $cv:expr) => {
$crate::util::logger::log_cv_internal(&$cv, Some($kind))
};
($cv:expr) => {
$crate::util::logger::log_cv_internal(&$cv, None)
};
}
#[macro_export]
macro_rules! log_t {
($key:expr) => {
$crate::util::logger::log_internal_translated(
$crate::util::logger::PrintType::General,
"",
false,
$key,
vec![]
)
};
($key:expr, $($arg:expr),+) => {
$crate::util::logger::log_internal_translated(
$crate::util::logger::PrintType::General,
"",
false,
$key,
vec![$($arg),+]
)
};
}
#[macro_export]
macro_rules! log_t_err {
($key:expr) => {
$crate::util::logger::log_internal_translated(
$crate::util::logger::PrintType::General,
">>",
true,
$key,
vec![]
)
};
($key:expr, $($arg:expr),+) => {
$crate::util::logger::log_internal_translated(
$crate::util::logger::PrintType::General,
">>",
true,
$key,
vec![$($arg.to_string()),+]
)
};
}
/// Log a general informational message.
#[macro_export]
macro_rules! log {
($($arg:tt)*) => {
$crate::util::logger::log_internal($crate::util::logger::PrintType::General, "", false, format!($($arg)*))
};
}
/// Log an inbound message (`>`).
#[macro_export]
macro_rules! log_in {
($($arg:tt)*) => {
$crate::util::logger::log_internal(
$crate::util::logger::PrintType::General,
">",
false,
format!($($arg)*)
)
};
}
/// Log an outbound message (`<`).
#[macro_export]
macro_rules! log_out {
($($arg:tt)*) => {
$crate::util::logger::log_internal(
$crate::util::logger::PrintType::General,
"<",
false,
format!($($arg)*)
)
};
}
/// Log an error message (`>>`).
#[macro_export]
macro_rules! log_err {
($($arg:tt)*) => {
$crate::util::logger::log_internal(
$crate::util::logger::PrintType::General,
">>",
true,
format!($($arg)*)
)
};
}

View file

@ -4,3 +4,4 @@ pub mod config_util;
pub mod crypto_helper; pub mod crypto_helper;
pub mod crypto_util; pub mod crypto_util;
pub mod file_util; pub mod file_util;
pub mod logger;