//! TUI application state, network event model, and the async run loop. use crate::ft; use crate::layout::{Dir, Layout, Resize}; use crate::net::{self, Session}; use crate::registry; use crate::sbx; use crate::snapshot; use crate::theme::Theme; use crate::ui; use anyhow::Result; use base64::engine::general_purpose::STANDARD; use base64::Engine; use crossterm::event::{ DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind, }; use crossterm::execute; use crossterm::terminal::{ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, }; use futures_util::{SinkExt, StreamExt}; use ratatui::backend::CrosstermBackend; use ratatui::Terminal; use serde::{Deserialize, Serialize}; use serde_json::json; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio_tungstenite::tungstenite::Message as WsMsg; const SBX_NAME: &str = "hack-house"; #[derive(Clone)] pub struct ChatLine { pub ts: String, pub username: String, pub text: String, pub system: bool, } /// A power a user currently holds in the room. Badges stack: a user can be the /// `Host` *and* a `Sudoer` *and* a `Driver` at once. `Member` is the floor — /// returned only when none of the others apply. The order of the variants is /// the order badges are painted (host first). #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Role { Host, Sudoer, Driver, Member, } #[derive(Clone)] pub struct User { pub user_id: String, pub username: String, } /// An in-progress incoming transfer we accepted. Chunks stream straight to a /// disk-backed `Sink` (created lazily on the first chunk) so a multi-GB payload /// never sits in RAM. struct Transfer { meta: ft::Offer, sink: Option, accepted: bool, } /// An outgoing transfer awaiting / serving an accept. The payload is streamed /// from `src` on disk, not held in memory. struct ActiveSend { id: String, src: std::path::PathBuf, /// `src` is a temp tar we built for a directory — delete it after sending. temp: bool, sending: bool, } /// Decoded events arriving from the websocket reader task. pub enum Net { Init { lines: Vec, users: Vec, }, Message(ChatLine), Roster { users: Vec, capacity: usize, }, Joined(String), Left(String), SbxStatus { backend: String, ready: bool, rows: u16, cols: u16, }, SbxResize { rows: u16, cols: u16, }, SbxData(Vec), SbxInput { from: String, bytes: Vec, }, Perm { owner: String, drivers: Vec, sudoers: Vec, }, Ft(ft::Ft), /// An AI agent is generating a reply (`on`) or has finished (`!on`). AiTyping { name: String, on: bool, }, /// Incremental reply text from a streaming AI agent. `text` is the reply so /// far (cumulative); `done` clears the live preview (the final, persisted /// chat message arrives separately as a normal `Message`). AiStream { name: String, text: String, done: bool, }, /// A local system notice produced off-thread (e.g. async Ollama probe). Sys(String), Err(String), /// An outgoing `/send` finished hashing off-thread and is staged for /// streaming — the run loop records it in `active_send` so a peer's `/accept` /// can start the chunk stream. The offer frame is emitted by the same task. SendReady { id: String, src: std::path::PathBuf, temp: bool, name: String, size: u64, to: Option, }, /// Relayed to the room when a member opens a shared VirtualBox VM locally, so /// the *other* party knows the appliance is live and can open their own copy. /// `by` is the server-authenticated launcher; the receiver skips its own echo. VmOpened { by: String, vm: String, }, /// A peer asked for our shareable-VM catalog (`/sbx catalog @me`). `by` is the /// server-authenticated requester; `to` is whom the request named (we act only /// if it's us). We reply with a `_sbx:catalog` frame of our published VMs. SbxCatReq { by: String, to: String, }, /// A peer answered our `/sbx catalog @them` with their published-VM slice. SbxCatalog { by: String, to: String, items: Vec, }, /// A peer asked us to hand over a published VM (`/sbx pull @me