[Feat] Split Daemon & TUI

This commit is contained in:
Alex Emmet 2026-07-20 23:40:33 +02:00
commit 36a70e82a0
35 changed files with 970 additions and 239 deletions

View file

@ -1,10 +1,17 @@
use crossterm::event::{KeyCode, KeyEvent};
use iota_logger::{log, log_command, log_cv};
#[cfg(feature = "legacy-commands")]
use iota_logger::{log, log_cv};
#[cfg(feature = "legacy-commands")]
use iota_state::{ACTIVE_TASKS, RELOAD, SHUTDOWN};
#[cfg(feature = "legacy-commands")]
use iota_storage::users::{user_manager, user_profile::UserProfile};
#[cfg(feature = "legacy-commands")]
use iota_storage::util::config_util::modify_config;
#[cfg(feature = "legacy-commands")]
use iota_util::file_util;
#[cfg(feature = "legacy-commands")]
use mtp::codec::{CommunicationType, CommunicationValue};
#[cfg(feature = "legacy-commands")]
use omikron_connector::omikron_connection::OMIKRON_CONNECTION;
use ratatui::{
Frame,
@ -13,7 +20,6 @@ use ratatui::{
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
use uuid::Uuid;
use std::{
any::Any,
@ -25,11 +31,12 @@ use tokio::time::Instant;
use crate::{
elements::elements::{Element, InteractableElement, JoinableElement},
interaction_result::InteractionResult,
ui::FPS,
ipc_client::IpcClient,
util::borders::draw_block_joins,
};
pub struct ConsoleCard {
ipc: Arc<IpcClient>,
focused: bool,
pub title: String,
pub content: String,
@ -44,8 +51,9 @@ pub struct ConsoleCard {
}
impl ConsoleCard {
pub fn new(title: &str, content: &str) -> Self {
pub fn new(title: &str, content: &str, ipc: Arc<IpcClient>) -> Self {
ConsoleCard {
ipc,
focused: false,
title: title.to_string(),
content: content.to_string(),
@ -290,22 +298,17 @@ impl InteractableElement for ConsoleCard {
match key.code {
KeyCode::Enter => {
if self.content.is_empty() {
log!("");
return InteractionResult::Handled;
}
let command = self.content.clone();
let id = Uuid::new_v4();
let id = id.to_string();
let id = id.split_at(8).0;
let task_id = format!("command_{}_{}", command, id);
ACTIVE_TASKS.insert(task_id.clone());
log_command!("{}", command);
let ipc = self.ipc.clone();
let seq = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
tokio::spawn(async move {
run_command(&command).await;
ACTIVE_TASKS.remove(&task_id);
let _ = ipc.send_command(seq, command).await;
});
self.content.clear();
@ -379,6 +382,7 @@ impl InteractableElement for ConsoleCard {
}
}
#[cfg(feature = "legacy-commands")]
pub async fn run_command(command: &str) {
let parts = command.split(" ").collect::<Vec<&str>>();
@ -503,6 +507,7 @@ pub async fn run_command(command: &str) {
}
}
#[cfg(feature = "legacy-commands")]
pub async fn ping(time: u64) {
let conn = OMIKRON_CONNECTION.clone();

View file

@ -1,7 +1,7 @@
use std::{any::Any, sync::Arc};
use crossterm::event::KeyEvent;
use iota_state::APP_STATE;
use iota_state::ClientState;
use ratatui::{
Frame,
layout::Rect,
@ -34,11 +34,29 @@ impl GRAPHS {
}
}
pub fn get_graph(&self) -> Vec<(f64, f64)> {
pub fn get_graph(&self, state: &ClientState) -> Vec<(f64, f64)> {
match self {
GRAPHS::Ram => APP_STATE.lock().unwrap().with_width(28).ram.clone(),
GRAPHS::Cpu => APP_STATE.lock().unwrap().with_width(28).cpu.clone(),
GRAPHS::Ping => APP_STATE.lock().unwrap().with_width(28).ping.clone(),
GRAPHS::Ram => state
.app
.lock()
.unwrap_or_else(|error| error.into_inner())
.with_width(28)
.ram
.clone(),
GRAPHS::Cpu => state
.app
.lock()
.unwrap_or_else(|error| error.into_inner())
.with_width(28)
.cpu
.clone(),
GRAPHS::Ping => state
.app
.lock()
.unwrap_or_else(|error| error.into_inner())
.with_width(28)
.ping
.clone(),
}
}
@ -54,6 +72,7 @@ impl GRAPHS {
#[allow(unused)]
pub struct GraphCard {
ui: Arc<UI>,
state: ClientState,
graph_type: GRAPHS,
focused: bool,
@ -66,9 +85,10 @@ pub struct GraphCard {
}
impl GraphCard {
pub fn new(ui: Arc<UI>, graph_type: GRAPHS, title: String) -> Self {
pub fn new(ui: Arc<UI>, state: ClientState, graph_type: GRAPHS, title: String) -> Self {
Self {
ui,
state,
graph_type,
focused: false,
title,
@ -93,7 +113,7 @@ impl Element for GraphCard {
fn render(&self, f: &mut Frame, r: Rect) {
if self.open {
let graph = self.graph_type.get_graph();
let graph = self.graph_type.get_graph(&self.state);
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);

View file

@ -1,9 +1,8 @@
use crate::app_state::APP_STATE;
use crate::elements::elements::{Element, InteractableElement, JoinableElement};
use crate::interaction_result::InteractionResult;
use crate::util::borders::draw_block_joins;
use crossterm::event::{KeyCode, KeyEvent};
use iota_logger::PrintType;
use iota_state::{ClientState, UiLogEntry};
use ratatui::{
Frame,
layout::Rect,
@ -12,60 +11,9 @@ use ratatui::{
widgets::{Block, Borders, Paragraph},
};
use std::any::Any;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Clone, Debug)]
pub struct UiLogEntry {
pub timestamp_ms: u128,
pub sender: PrintType,
pub message: String,
pub is_error: bool,
}
impl UiLogEntry {
pub fn format_timestamp(&self) -> String {
let secs = (self.timestamp_ms / 1000) as i64;
let hours = (secs / 3600) % 24;
let minutes = (secs / 60) % 60;
let seconds = secs % 60;
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
}
}
impl From<LogEntry> for UiLogEntry {
fn from(entry: LogEntry) -> Self {
Self {
timestamp_ms: entry.timestamp_ms,
sender: entry.sender,
message: entry.message,
is_error: entry.is_error,
}
}
}
#[derive(Clone, Debug)]
pub struct LogEntry {
pub timestamp_ms: u128,
pub sender: PrintType,
pub message: String,
pub is_error: bool,
}
impl LogEntry {
pub fn new(sender: PrintType, message: String, is_error: bool) -> Self {
Self {
timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
sender,
message,
is_error,
}
}
}
pub struct LogCard {
state: ClientState,
focused: bool,
selected: bool,
scroll_offset: usize,
@ -76,8 +24,9 @@ pub struct LogCard {
}
impl LogCard {
pub fn new() -> Self {
pub fn new(state: ClientState) -> Self {
Self {
state,
focused: false,
selected: false,
scroll_offset: 0,
@ -89,21 +38,17 @@ impl LogCard {
}
fn get_logs(&self) -> Vec<UiLogEntry> {
let state = APP_STATE.lock().unwrap();
let state = self
.state
.app
.lock()
.unwrap_or_else(|error| error.into_inner());
state
.get_logs()
.iter()
.map(|e| UiLogEntry {
timestamp_ms: e.timestamp_ms,
sender: match e.sender.as_str() {
"Call" => PrintType::Call,
"Client" => PrintType::Client,
"Iota" => PrintType::Iota,
"Omikron" => PrintType::Omikron,
"Omega" => PrintType::Omega,
"Command" => PrintType::Command,
_ => PrintType::General,
},
sender: e.sender.clone(),
message: e.message.clone(),
is_error: e.is_error,
})
@ -196,12 +141,24 @@ impl LogCard {
line.push_str(&timestamp);
}
result.push((line, entry.sender.prefix_color(), entry.is_error));
result.push((line, Self::sender_color(&entry.sender), entry.is_error));
}
result
}
fn sender_color(sender: &str) -> Color {
match sender {
"Call" => Color::Magenta,
"Client" => Color::Green,
"Iota" => Color::Yellow,
"Omikron" => Color::Blue,
"Omega" => Color::Cyan,
"Command" => Color::LightGreen,
_ => Color::LightCyan,
}
}
fn build_all_lines(
&self,
entries: Vec<UiLogEntry>,

View file

@ -1,14 +1,12 @@
use crate::ui::UI;
use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyModifiers, poll, read};
use iota_state::{RELOAD, SHUTDOWN, UNIQUE};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration;
pub fn setup_input_handler(ui: Arc<UI>) {
tokio::spawn(async move {
loop {
if *SHUTDOWN.read().await {
if ui.is_shutdown() {
break;
}
@ -27,7 +25,6 @@ pub fn setup_input_handler(ui: Arc<UI>) {
match event_result {
Ok(Some(key_event)) => {
handle_input(key_event, ui.clone()).await;
UNIQUE.store(true, Ordering::Relaxed);
}
Ok(_) => {}
Err(e) => {
@ -43,11 +40,11 @@ pub async fn handle_input(key: KeyEvent, ui: Arc<UI>) {
match (key.code, key.modifiers) {
(crossterm::event::KeyCode::Char('q'), KeyModifiers::CONTROL)
| (crossterm::event::KeyCode::Char('c'), KeyModifiers::CONTROL) => {
*SHUTDOWN.write().await = true;
ui.request_shutdown();
}
(crossterm::event::KeyCode::Char('r'), KeyModifiers::CONTROL) => {
*RELOAD.write().await = true;
*SHUTDOWN.write().await = true;
let _ = ui.send_restart().await;
ui.request_shutdown();
}
_ => {
ui.handle_input(key).await;

View file

@ -0,0 +1,83 @@
use iota_ipc::{ClientMessage, DaemonMessage, read_msg, write_msg};
use iota_state::{ClientState, UiLogEntry};
use std::io::Result;
use std::path::Path;
use std::sync::Arc;
use tokio::net::UnixStream;
use tokio::net::unix::OwnedWriteHalf;
use tokio::sync::Mutex;
/* The TUI owns this cache. IPC updates replace daemon snapshots and append
* logs, so rendering never reaches into daemon-owned storage or connections. */
pub struct IpcClient {
state: ClientState,
writer: Mutex<OwnedWriteHalf>,
}
impl IpcClient {
pub async fn connect(path: impl AsRef<Path>) -> Result<Arc<Self>> {
let stream = UnixStream::connect(path).await?;
let (mut reader, writer) = stream.into_split();
let client = Arc::new(Self {
state: ClientState::new(),
writer: Mutex::new(writer),
});
let reader_client = client.clone();
tokio::spawn(async move {
while let Ok(message) = read_msg::<_, DaemonMessage>(&mut reader).await {
reader_client.apply(message).await;
}
});
client.send(ClientMessage::Subscribe).await?;
Ok(client)
}
pub fn state(&self) -> ClientState {
self.state.clone()
}
pub async fn send_command(&self, seq: u64, line: String) -> Result<()> {
self.send(ClientMessage::Command { seq, line }).await
}
async fn send(&self, message: ClientMessage) -> Result<()> {
let mut writer = self.writer.lock().await;
write_msg(&mut *writer, &message).await
}
async fn apply(&self, message: DaemonMessage) {
let mut state = self
.state
.app
.lock()
.unwrap_or_else(|error| error.into_inner());
match message {
DaemonMessage::LogEntry(entry) => state.push_log(UiLogEntry {
timestamp_ms: entry.timestamp_ms,
sender: entry.sender,
message: entry.message,
is_error: entry.is_error,
}),
DaemonMessage::StateUpdate(snapshot) => {
state.cpu = snapshot.cpu;
state.ram = snapshot.ram;
state.ping = snapshot.ping;
state.net_up = snapshot.net_up;
state.net_down = snapshot.net_down;
state.sys_info = snapshot.sys_info;
}
DaemonMessage::CommandResult {
success, message, ..
} => state.push_log(UiLogEntry {
timestamp_ms: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis(),
sender: "Command".into(),
message,
is_error: !success,
}),
DaemonMessage::Pong { .. } => {}
}
}
}

View file

@ -18,5 +18,6 @@ pub mod util {
}
pub mod app_state;
pub mod input_handler;
pub mod ipc_client;
pub mod interaction_result;
pub mod ui;

View file

@ -36,22 +36,23 @@ impl MainScreen {
vec![Some(1), Some(4)],
];
let mut log_card = LogCard::new();
let state = ui.client_state();
let mut log_card = LogCard::new(state.clone());
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", "", ui.ipc());
console_card.set_joins(Borders::TOP);
elements.push(Box::new(log_card));
elements.push(Box::new(console_card));
let mut ram_graph = GraphCard::new(ui.clone(), GRAPHS::Ram, "RAM".into());
let mut ram_graph = GraphCard::new(ui.clone(), state.clone(), GRAPHS::Ram, "RAM".into());
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".into());
let mut cpu_graph = GraphCard::new(ui.clone(), state.clone(), GRAPHS::Cpu, "CPU".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());
let mut ping_graph = GraphCard::new(ui.clone(), state, GRAPHS::Ping, "Ping".into());
ping_graph.set_joins(Borders::TOP);
elements.push(Box::new(ping_graph));

View file

@ -1,15 +1,17 @@
use crate::{
input_handler::setup_input_handler, interaction_result::InteractionResult,
screens::screens::Screen,
ipc_client::IpcClient, screens::screens::Screen,
};
use crossterm::event::KeyEvent;
use iota_state::{ACTIVE_TASKS, SHUTDOWN, UNIQUE};
use once_cell::sync::Lazy;
use ratatui::{Terminal, backend::CrosstermBackend, init};
use std::{
collections::VecDeque,
io::Stdout,
sync::{Arc, Mutex, atomic::Ordering},
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use tokio::{sync::RwLock, time::Instant};
@ -19,14 +21,15 @@ use tokio::{sync::RwLock, time::Instant};
pub static FPS: Lazy<RwLock<(f64, f64)>> = Lazy::new(|| RwLock::new((0.0, 0.0)));
pub struct UI {
ipc: Arc<IpcClient>,
shutdown: AtomicBool,
pub terminal: Arc<Mutex<Terminal<CrosstermBackend<Stdout>>>>,
screen_stack: Arc<RwLock<Vec<Box<dyn Screen>>>>,
}
pub fn start_tui() -> Arc<UI> {
let ui = Arc::new(UI::new());
pub fn start_tui(ipc: Arc<IpcClient>) -> Arc<UI> {
let ui = Arc::new(UI::new(ipc));
let uic = ui.clone();
ACTIVE_TASKS.insert("UI Renderer".to_string());
tokio::spawn(async move {
let mut last_render = Instant::now();
@ -39,11 +42,11 @@ pub fn start_tui() -> Arc<UI> {
let mut skipped = 0;
loop {
if *SHUTDOWN.read().await {
if uic.is_shutdown() {
break;
}
if skipped > 5 || UNIQUE.load(Ordering::Relaxed) {
if skipped > 5 {
uic.render().await;
skip_samples.push_back(skipped);
@ -88,27 +91,47 @@ pub fn start_tui() -> Arc<UI> {
*FPS.write().await = (avg_fps, avg_skips_percentage);
last_render = Instant::now();
UNIQUE.store(false, Ordering::Relaxed);
} else {
skipped += 1;
}
tokio::time::sleep(Duration::from_millis(16)).await;
}
ACTIVE_TASKS.remove("UI Renderer");
ratatui::restore();
});
setup_input_handler(ui.clone());
ui
}
impl UI {
pub fn new() -> Self {
pub fn new(ipc: Arc<IpcClient>) -> Self {
let terminal = init();
Self {
ipc,
shutdown: AtomicBool::new(false),
terminal: Arc::new(Mutex::new(terminal)),
screen_stack: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn ipc(&self) -> Arc<IpcClient> {
self.ipc.clone()
}
pub fn client_state(&self) -> iota_state::ClientState {
self.ipc.state()
}
pub fn is_shutdown(&self) -> bool {
self.shutdown.load(Ordering::Relaxed)
}
pub fn request_shutdown(&self) {
self.shutdown.store(true, Ordering::Relaxed);
}
pub async fn send_restart(&self) -> std::io::Result<()> {
self.ipc.send_command(0, "restart".into()).await
}
pub async fn set_screen(&self, screen: Box<dyn Screen>) {
self.screen_stack.write().await.push(screen);
}
@ -140,7 +163,7 @@ impl UI {
stack.pop();
if stack.is_empty() {
*SHUTDOWN.write().await = true;
self.request_shutdown();
}
}
InteractionResult::Handled => {}