// SPDX-License-Identifier: AGPL-3.0-only // Copyright (C) 2026 Andy Dixon //! State shared by the main window and every feature module: the service, cached profiles and //! preferences, open tabs, toasts, and a request queue features use to ask the app to act. use crate::views::View; use nexus_core::models::{AppPreferences, Profile, Protocol, SessionInfo, SessionState}; use nexus_core::service::Service; use std::collections::HashSet; use std::sync::Arc; /// Things a feature asks the main window to do (handled once per frame, in order). #[allow(clippy::large_enum_variant)] // a per-frame queue; boxing buys nothing pub enum Request { /// Merge profiles with the sync folder (manual, or after a change). SyncProfiles, /// Open the profile editor for a new profile (optionally pre-set protocol / folder). NewProfile { protocol: Option, folder: String, }, EditProfile(String), Connect(String), ConnectQuick(Profile), /// Reload `Shared::profiles` from the database. ReloadProfiles, /// Re-read and apply preferences (theme, terminal theme, fonts…). ReloadPreferences, ExportEncrypted(Vec), ImportEncrypted, Sftp(String), /// Type text into the active session (snippets). SendToActive(String), /// Bytes to a specific session. Write(String, Vec), } #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum SettingsTab { #[default] General, Terminal, Security, KnownHosts, Diagnostics, Help, About, } const MAX_TOASTS: usize = 8; pub struct Toast { pub text: String, pub kind: ToastKind, pub until: f64, } #[derive(Clone, Copy, PartialEq, Eq)] pub enum ToastKind { Info, Success, Warning, Error, } /// A session on screen (a tab's own session or a split pane) and how it's drawn. pub struct Pane { pub info: SessionInfo, pub view: View, pub recording: bool, /// Local shell exited: Enter restarts it. pub local_exited: bool, /// Wall-clock time the session last connected. pub connected_since: Option>, /// Bumped to make trigger highlights flash. pub highlight_until: f64, /// Serial DTR / RTS as last set (starts from the profile's on-connect settings). pub lines: (bool, bool), } /// A tab. Its session is `id`; `split` lays out extra panes (which have no tab of their own). pub struct Tab { pub id: String, /// Output arrived while the tab wasn't active. pub activity: bool, pub split: Option, } pub struct Shared { pub svc: Arc, pub profiles: Vec, pub prefs: AppPreferences, pub tabs: Vec, /// Every on-screen session by id (tabs and split panes). pub panes: std::collections::HashMap, /// Session id of the active tab. pub active: Option, /// Session id of the focused pane inside the active tab (the tab itself or a split child). pub focused: Option, pub toasts: Vec, pub requests: Vec, pub broadcast: bool, /// None = every connected session. pub broadcast_targets: Option>, pub now: f64, pub ctx: egui::Context, } impl Shared { pub fn new(svc: Arc, ctx: egui::Context) -> Shared { let prefs = svc.preferences(); let profiles = svc.profiles(); Shared { svc, profiles, prefs, tabs: vec![], panes: Default::default(), active: None, focused: None, toasts: vec![], requests: vec![], broadcast: false, broadcast_targets: None, now: 0.0, ctx, } } pub fn request(&mut self, r: Request) { self.requests.push(r); self.ctx.request_repaint(); } fn push(&mut self, text: impl Into, kind: ToastKind) { let (text, secs) = (text.into(), if kind == ToastKind::Error { 6.0 } else { 4.0 }); // Remote output can raise toasts (triggers, clipboard writes) at line rate: repeats only // extend the visible one, and at most a handful are kept so a flood can't stall frames. if let Some(t) = self.toasts.iter_mut().find(|t| t.text == text && t.kind == kind) { t.until = self.now + secs; } else { self.toasts.push(Toast { text, kind, until: self.now + secs }); if self.toasts.len() > MAX_TOASTS { self.toasts.remove(0); } } self.ctx.request_repaint(); } pub fn info(&mut self, text: impl Into) { self.push(text, ToastKind::Info); } pub fn success(&mut self, text: impl Into) { self.push(text, ToastKind::Success); } pub fn warn(&mut self, text: impl Into) { self.push(text, ToastKind::Warning); } pub fn error(&mut self, text: impl Into) { self.push(text, ToastKind::Error); } pub fn profile(&self, id: &str) -> Option<&Profile> { self.profiles.iter().find(|p| p.id == id) } pub fn reload_profiles(&mut self) { self.profiles = self.svc.profiles(); } pub fn tab(&self, id: &str) -> Option<&Tab> { self.tabs.iter().find(|t| t.id == id) } pub fn pane(&self, id: &str) -> Option<&Pane> { self.panes.get(id) } pub fn pane_mut(&mut self, id: &str) -> Option<&mut Pane> { self.panes.get_mut(id) } /// The session keyboard actions apply to: the focused pane, else the active tab. pub fn focused_session(&self) -> Option { self.focused.clone().or_else(|| self.active.clone()) } /// Every session id on screen (tabs and split panes). pub fn all_sessions(&self) -> Vec { self.panes.keys().cloned().collect() } pub fn session_state(&self, id: &str) -> Option { self.svc.sessions.info(id).map(|i| i.state) } /// Runs `f` on a worker thread and toasts its error, if any (for slow service calls). pub fn spawn(&self, f: impl FnOnce() -> nexus_core::Result + Send + 'static, done: impl FnOnce(T) + Send + 'static) { let ctx = self.ctx.clone(); std::thread::spawn(move || { match f() { Ok(v) => done(v), Err(e) => crate::app::post_error(e.to_string()), } ctx.request_repaint(); }); } } /// Formats a command-key shortcut for menus: "⌘K" on macOS, "Ctrl+K" elsewhere. pub fn keys(s: &str) -> String { if cfg!(target_os = "macos") { s.replace("Mod+Shift+", "⇧⌘").replace("Mod+", "⌘") } else { s.replace("Mod+", "Ctrl+") } }