Skip to main content

quincy_gui/gui/
types.rs

1use iced::widget::text_editor;
2use iced::window;
3use quincy::config::ClientConfig;
4use std::fmt;
5use std::path::PathBuf;
6use std::sync::Arc;
7use std::time::Instant;
8use tokio::sync::Mutex;
9
10use super::error::GuiError;
11use crate::ipc::{ConnectionMetrics, IpcConnection};
12
13/// Connection state machine for a VPN configuration.
14///
15/// This enum represents all possible states a configuration can be in,
16/// making state transitions explicit and eliminating scattered state tracking.
17#[derive(Debug, Clone, Default)]
18pub enum ConfigState {
19    /// No connection activity - idle state
20    #[default]
21    Idle,
22    /// Connection is being established
23    Connecting {
24        /// When the connection attempt started
25        started_at: Instant,
26        /// The instance being connected (holds IPC connection for cancellation).
27        /// None while spawning daemon, Some once IPC is established.
28        instance: Option<QuincyInstance>,
29    },
30    /// Successfully connected to the VPN
31    Connected {
32        /// The active VPN instance
33        instance: QuincyInstance,
34        /// Connection metrics (bytes sent/received, duration, etc.)
35        metrics: Option<ConnectionMetrics>,
36    },
37    /// Disconnection is in progress
38    Disconnecting,
39    /// An error occurred
40    Error {
41        /// Error to display
42        error: GuiError,
43    },
44}
45
46impl ConfigState {
47    /// Returns true if the configuration is in a connected state.
48    pub fn is_connected(&self) -> bool {
49        matches!(self, Self::Connected { .. })
50    }
51
52    /// Returns true if the configuration is connecting or disconnecting.
53    pub fn is_transitioning(&self) -> bool {
54        matches!(self, Self::Connecting { .. } | Self::Disconnecting)
55    }
56
57    /// Returns true if the configuration has an active instance (connected or transitioning).
58    pub fn has_active_instance(&self) -> bool {
59        matches!(
60            self,
61            Self::Connecting { .. } | Self::Connected { .. } | Self::Disconnecting
62        )
63    }
64
65    /// Returns the instance if connected, None otherwise.
66    pub fn instance(&self) -> Option<&QuincyInstance> {
67        match self {
68            Self::Connected { instance, .. } => Some(instance),
69            _ => None,
70        }
71    }
72
73    /// Returns a mutable reference to the instance if connected.
74    pub fn instance_mut(&mut self) -> Option<&mut QuincyInstance> {
75        match self {
76            Self::Connected { instance, .. } => Some(instance),
77            _ => None,
78        }
79    }
80
81    /// Returns the metrics if connected and available.
82    pub fn metrics(&self) -> Option<&ConnectionMetrics> {
83        match self {
84            Self::Connected { metrics, .. } => metrics.as_ref(),
85            _ => None,
86        }
87    }
88
89    /// Returns the error if in error state.
90    pub fn error(&self) -> Option<&GuiError> {
91        match self {
92            Self::Error { error } => Some(error),
93            _ => None,
94        }
95    }
96}
97
98/// Represents a running Quincy VPN client instance.
99///
100/// Each instance manages the IPC connection to the daemon process.
101/// Connection status and metrics are tracked separately in `ConfigState`.
102#[derive(Clone)]
103pub struct QuincyInstance {
104    /// Unique identifier for this instance
105    pub name: String,
106    /// IPC connection for communication with the daemon
107    pub(crate) ipc_client: Option<Arc<Mutex<IpcConnection>>>,
108}
109
110impl fmt::Debug for QuincyInstance {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        f.debug_struct("QuincyInstance")
113            .field("name", &self.name)
114            .field("has_ipc", &self.ipc_client.is_some())
115            .finish()
116    }
117}
118
119impl QuincyInstance {
120    /// Creates a new instance with the given name and IPC connection.
121    pub fn new(name: String, ipc_client: Option<Arc<Mutex<IpcConnection>>>) -> Self {
122        Self { name, ipc_client }
123    }
124
125    /// Returns a reference to the IPC client if available.
126    pub fn ipc_client(&self) -> Option<&Arc<Mutex<IpcConnection>>> {
127        self.ipc_client.as_ref()
128    }
129
130    /// Takes ownership of the IPC client, leaving None in its place.
131    pub fn take_ipc_client(&mut self) -> Option<Arc<Mutex<IpcConnection>>> {
132        self.ipc_client.take()
133    }
134}
135
136/// Configuration file information for a Quincy VPN client.
137#[derive(Clone)]
138pub struct QuincyConfig {
139    /// Display name of the configuration
140    pub name: String,
141    /// File system path to the configuration file
142    pub path: PathBuf,
143}
144
145/// A configuration entry combining file info, runtime state, and parsed data.
146pub struct ConfigEntry {
147    /// Configuration file metadata
148    pub config: QuincyConfig,
149    /// Connection state machine
150    pub state: ConfigState,
151    /// Parsed configuration for display (None if parsing failed)
152    pub parsed: Option<ClientConfig>,
153    /// Parse error message if configuration failed to parse
154    pub parse_error: Option<String>,
155}
156
157/// State for the inline editor modal.
158#[derive(Debug)]
159pub struct EditorState {
160    /// Name of the configuration being edited
161    pub config_name: String,
162    /// Text editor content with syntax highlighting
163    pub content: text_editor::Content,
164}
165
166/// State for confirmation dialogs
167#[derive(Debug, Clone)]
168pub struct ConfirmationState {
169    pub title: String,
170    pub message: String,
171    pub confirm_action: ConfirmAction,
172}
173
174#[derive(Debug, Clone)]
175pub enum ConfirmAction {
176    DeleteConfig(String), // config name to delete
177    DiscardEditorChanges,
178}
179
180/// Domain-specific message groups to improve clarity.
181#[derive(Debug, Clone)]
182pub enum ConfigMsg {
183    Selected(String),
184    NameChanged(String),
185    NameSaved,
186    Delete,
187    New,
188}
189
190#[derive(Debug, Clone)]
191pub enum ConfirmMsg {
192    Show(ConfirmationState),
193    Confirm,
194    Cancel,
195}
196
197#[derive(Debug, Clone)]
198pub enum EditorMsg {
199    /// Text editor action (keystroke, selection, etc.)
200    Action(text_editor::Action),
201    /// Open the editor modal
202    Open,
203    /// Close the editor modal without saving
204    Close,
205    /// Save changes and close the editor modal
206    Save,
207}
208
209/// Messages related to VPN instance lifecycle and status.
210#[derive(Debug, Clone)]
211pub enum InstanceMsg {
212    /// User requested to connect the selected configuration
213    Connect,
214    /// Connection was successfully established (legacy, prefer ConnectedInstance)
215    Connected(String),
216    /// A new instance was created and connected with initial metrics
217    ConnectedInstance(QuincyInstance, Option<ConnectionMetrics>),
218    /// User requested to disconnect
219    Disconnect,
220    /// User requested to cancel an in-progress connection
221    CancelConnect,
222    /// Disconnection completed
223    Disconnected,
224    /// Status/metrics update received from daemon
225    StatusUpdated(String, Option<ConnectionMetrics>),
226    /// Connection was lost with an error
227    DisconnectedWithError(String, GuiError),
228    /// Connection attempt failed
229    ConnectFailed(String, GuiError),
230}
231
232#[derive(Debug, Clone)]
233pub enum SystemMsg {
234    WindowClosed(window::Id),
235    UpdateMetrics,
236    Noop,
237}
238
239#[derive(Debug, Clone)]
240pub enum Message {
241    Config(ConfigMsg),
242    Editor(EditorMsg),
243    Instance(InstanceMsg),
244    System(SystemMsg),
245    Confirm(ConfirmMsg),
246}