Skip to main content

virtuoso_cli/tui/app/
state.rs

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