Skip to main content

virtuoso_cli/tui/app/
state.rs

1use 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
58/// A single .env config field.
59pub 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    /// Wrap-safe cursor movement for the active tab's list.
138    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}