virtuoso_cli/tui/app/
state.rs1use crate::models::{DaemonStats, SessionInfo, TunnelState};
2use crate::spectre::jobs::Job;
3use crate::tui::app::overlay::Overlay;
4use std::collections::HashMap;
5use std::time::Instant;
6
7#[derive(Clone, Copy, PartialEq, Eq)]
8pub enum Tab {
9 Sessions,
10 Jobs,
11 Config,
12}
13
14impl Tab {
15 pub fn label(self) -> &'static str {
16 match self {
17 Tab::Sessions => "Sessions",
18 Tab::Jobs => "Jobs",
19 Tab::Config => "Config",
20 }
21 }
22
23 pub fn next(self) -> Self {
24 match self {
25 Tab::Sessions => Tab::Jobs,
26 Tab::Jobs => Tab::Config,
27 Tab::Config => Tab::Sessions,
28 }
29 }
30
31 pub fn prev(self) -> Self {
32 match self {
33 Tab::Sessions => Tab::Config,
34 Tab::Jobs => Tab::Sessions,
35 Tab::Config => Tab::Jobs,
36 }
37 }
38
39 pub fn all() -> [Tab; 3] {
40 [Tab::Sessions, Tab::Jobs, Tab::Config]
41 }
42}
43
44#[derive(Clone, Copy, PartialEq, Eq)]
45pub enum StatusKind {
46 Info,
47 Ok,
48 Warn,
49 Err,
50}
51
52pub struct StatusToast {
53 pub message: String,
54 pub kind: StatusKind,
55 pub at: Instant,
56}
57
58pub struct ConfigField {
60 pub key: String,
61 pub value: String,
62 pub hint: &'static str,
63}
64
65pub struct App {
66 pub tab: Tab,
67 pub overlay: Overlay,
68
69 pub sessions: Vec<SessionInfo>,
70 pub jobs: Vec<Job>,
71 pub tunnel_state: Option<TunnelState>,
72 pub config_fields: Vec<ConfigField>,
73 pub daemon_stats: HashMap<u16, DaemonStats>,
74
75 pub selected_session: usize,
76 pub selected_job: usize,
77 pub selected_config: usize,
78
79 pub spinner_frame: usize,
80 pub status: Option<StatusToast>,
81 pub should_quit: bool,
82}
83
84impl App {
85 pub fn new() -> Self {
86 let mut app = Self {
87 tab: Tab::Sessions,
88 overlay: Overlay::None,
89 sessions: Vec::new(),
90 jobs: Vec::new(),
91 tunnel_state: None,
92 config_fields: Vec::new(),
93 daemon_stats: HashMap::new(),
94 selected_session: 0,
95 selected_job: 0,
96 selected_config: 0,
97 spinner_frame: 0,
98 status: None,
99 should_quit: false,
100 };
101 crate::tui::app::data::initial_load(&mut app);
102 app
103 }
104
105 pub fn set_status(&mut self, message: impl Into<String>, kind: StatusKind) {
106 self.status = Some(StatusToast {
107 message: message.into(),
108 kind,
109 at: Instant::now(),
110 });
111 }
112
113 pub fn refresh_daemon_stats(&mut self) {
114 for s in &self.sessions {
115 if let Some(stats) = DaemonStats::load(s.port) {
116 self.daemon_stats.insert(s.port, stats);
117 }
118 }
119 }
120
121 pub fn clear_expired_status(&mut self) {
122 if let Some(s) = &self.status {
123 if s.at.elapsed().as_secs() >= 3 {
124 self.status = None;
125 }
126 }
127 }
128
129 pub fn selected_session_info(&self) -> Option<&SessionInfo> {
130 self.sessions.get(self.selected_session)
131 }
132
133 pub fn selected_config_field(&self) -> Option<&ConfigField> {
134 self.config_fields.get(self.selected_config)
135 }
136
137 pub fn move_selection(&mut self, delta: i64) {
139 let (cursor, len) = match self.tab {
140 Tab::Sessions => (&mut self.selected_session, self.sessions.len()),
141 Tab::Jobs => (&mut self.selected_job, self.jobs.len()),
142 Tab::Config => (&mut self.selected_config, self.config_fields.len()),
143 };
144 if len == 0 {
145 *cursor = 0;
146 return;
147 }
148 let new = (*cursor as i64 + delta).rem_euclid(len as i64);
149 *cursor = new as usize;
150 }
151}
152
153impl Default for App {
154 fn default() -> Self {
155 Self::new()
156 }
157}