Skip to main content

rustant_core/voice/
toggle.rs

1//! Shared toggle state for voice commands and meeting recording.
2//!
3//! Provides a single `ToggleState` that all interfaces (REPL, TUI,
4//! Gateway, Dashboard) share to start/stop voice and meeting sessions.
5
6use super::meeting_session::{MeetingRecordingSession, MeetingResult, MeetingStatus};
7use super::session::VoiceCommandSession;
8use crate::config::{AgentConfig, MeetingConfig};
9use crate::error::VoiceError;
10use std::path::PathBuf;
11use std::sync::Arc;
12use tokio::sync::Mutex;
13use tracing::info;
14
15/// Shared state container for voice command and meeting recording toggles.
16///
17/// Wrapped in `Arc` and passed to REPL, TUI, Gateway, and Dashboard
18/// so all interfaces share a single source of truth.
19pub struct ToggleState {
20    voice_session: Mutex<Option<VoiceCommandSession>>,
21    meeting_session: Mutex<Option<MeetingRecordingSession>>,
22}
23
24impl ToggleState {
25    /// Create a new toggle state (no sessions active).
26    pub fn new() -> Arc<Self> {
27        Arc::new(Self {
28            voice_session: Mutex::new(None),
29            meeting_session: Mutex::new(None),
30        })
31    }
32
33    // ── Voice Commands ──────────────────────────────────────────────
34
35    /// Start the voice command session.
36    pub async fn voice_start(
37        &self,
38        config: AgentConfig,
39        workspace: PathBuf,
40        on_transcription: Arc<dyn Fn(String) + Send + Sync>,
41    ) -> Result<(), VoiceError> {
42        let mut guard = self.voice_session.lock().await;
43        if guard.as_ref().is_some_and(|s| s.is_active()) {
44            return Err(VoiceError::PipelineError {
45                message: "Voice command session is already active".into(),
46            });
47        }
48
49        let session = VoiceCommandSession::start(config, workspace, on_transcription).await?;
50        *guard = Some(session);
51        info!("Voice command session toggled ON");
52        Ok(())
53    }
54
55    /// Stop the voice command session.
56    pub async fn voice_stop(&self) -> Result<(), VoiceError> {
57        let mut guard = self.voice_session.lock().await;
58        match guard.take() {
59            Some(session) => {
60                session.stop().await?;
61                info!("Voice command session toggled OFF");
62                Ok(())
63            }
64            None => Err(VoiceError::PipelineError {
65                message: "No active voice command session".into(),
66            }),
67        }
68    }
69
70    /// Check if the voice command session is active.
71    pub async fn voice_active(&self) -> bool {
72        let guard = self.voice_session.lock().await;
73        guard.as_ref().is_some_and(|s| s.is_active())
74    }
75
76    // ── Meeting Recording ───────────────────────────────────────────
77
78    /// Start a meeting recording session.
79    pub async fn meeting_start(
80        &self,
81        config: MeetingConfig,
82        title: Option<String>,
83    ) -> Result<(), String> {
84        let mut guard = self.meeting_session.lock().await;
85        if guard.as_ref().is_some_and(|s| s.is_active()) {
86            return Err("Meeting recording is already active".into());
87        }
88
89        let session = MeetingRecordingSession::start(config, title).await?;
90        *guard = Some(session);
91        info!("Meeting recording toggled ON");
92        Ok(())
93    }
94
95    /// Stop the meeting recording and return results.
96    pub async fn meeting_stop(&self) -> Result<MeetingResult, String> {
97        let mut guard = self.meeting_session.lock().await;
98        match guard.take() {
99            Some(session) => {
100                let result = session.stop().await?;
101                info!("Meeting recording toggled OFF");
102                Ok(result)
103            }
104            None => Err("No active meeting recording".into()),
105        }
106    }
107
108    /// Check if a meeting recording is active.
109    pub async fn meeting_active(&self) -> bool {
110        let guard = self.meeting_session.lock().await;
111        guard.as_ref().is_some_and(|s| s.is_active())
112    }
113
114    /// Get the current meeting recording status.
115    pub async fn meeting_status(&self) -> Option<MeetingStatus> {
116        let guard = self.meeting_session.lock().await;
117        match guard.as_ref() {
118            Some(session) if session.is_active() => Some(session.status().await),
119            _ => None,
120        }
121    }
122
123    // ── Synchronous helpers (for TUI key handlers) ──────────────────
124
125    /// Non-blocking check if voice session is active.
126    /// Returns `None` if the mutex is contended.
127    pub fn voice_session_active_sync(&self) -> Option<bool> {
128        self.voice_session
129            .try_lock()
130            .ok()
131            .map(|guard| guard.as_ref().is_some_and(|s| s.is_active()))
132    }
133
134    /// Non-blocking check if meeting session is active.
135    /// Returns `None` if the mutex is contended.
136    pub fn meeting_session_active_sync(&self) -> Option<bool> {
137        self.meeting_session
138            .try_lock()
139            .ok()
140            .map(|guard| guard.as_ref().is_some_and(|s| s.is_active()))
141    }
142}
143
144impl Default for ToggleState {
145    fn default() -> Self {
146        Self {
147            voice_session: Mutex::new(None),
148            meeting_session: Mutex::new(None),
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[tokio::test]
158    async fn test_toggle_state_initial() {
159        let state = ToggleState::new();
160        assert!(!state.voice_active().await);
161        assert!(!state.meeting_active().await);
162        assert!(state.meeting_status().await.is_none());
163    }
164
165    #[tokio::test]
166    async fn test_voice_stop_without_start() {
167        let state = ToggleState::new();
168        let result = state.voice_stop().await;
169        assert!(result.is_err());
170    }
171
172    #[tokio::test]
173    async fn test_meeting_stop_without_start() {
174        let state = ToggleState::new();
175        let result = state.meeting_stop().await;
176        assert!(result.is_err());
177    }
178}