use crate::runtime::{RuntimeController, RuntimeEvent}; use crate::state::{OverlayState, VoiceLevelTracker}; use crate::theme::transparent; use gpui::{prelude::*, *}; use std::time::Duration; pub struct OriginSpeakApp { _runtime: RuntimeController, overlay: OverlayState, audio_level: f32, voice_level: VoiceLevelTracker, status_overlay_window: Option>, status_overlay_opening: bool, _runtime_bridge: Task<()>, } impl OriginSpeakApp { pub fn new(runtime: RuntimeController, cx: &mut Context) -> Self { let bridge_events = runtime.events(); let runtime_bridge = cx.spawn(async move |this, cx| { loop { cx.background_executor() .timer(Duration::from_millis(20)) .await; let pending = match bridge_events.lock() { Ok(receiver) => receiver.try_iter().collect::>(), Err(_) => return, }; let quit_requested = crate::platform_shell::take_quit_request(); if pending.is_empty() && !quit_requested { continue; } if this .update(cx, |this, cx| { if quit_requested { cx.quit(); return; } for event in pending { let auto_idle = match event { RuntimeEvent::Success => { this.apply_runtime_state(OverlayState::Success, cx); Some((OverlayState::Success, Duration::from_millis(900))) } RuntimeEvent::Error => { this.apply_runtime_state(OverlayState::Error, cx); Some((OverlayState::Error, Duration::from_millis(1500))) } RuntimeEvent::Idle => { this.apply_runtime_state(OverlayState::Idle, cx); None } RuntimeEvent::Listening => { this.apply_runtime_state(OverlayState::Listening, cx); None } RuntimeEvent::Processing => { this.apply_runtime_state(OverlayState::Processing, cx); None } RuntimeEvent::AudioLevel(level) => { if this.overlay == OverlayState::Listening { this.audio_level = this.voice_level.update(level); cx.notify(); } None } }; if let Some((expected, delay)) = auto_idle { cx.spawn(async move |this, cx| { cx.background_executor().timer(delay).await; let _ = this.update(cx, |this, cx| { if this.overlay == expected { this.apply_runtime_state(OverlayState::Idle, cx); } }); }) .detach(); } } }) .is_err() { return; } } }); Self { _runtime: runtime, overlay: OverlayState::Idle, audio_level: 0.0, voice_level: VoiceLevelTracker::default(), status_overlay_window: None, status_overlay_opening: false, _runtime_bridge: runtime_bridge, } } pub(crate) fn overlay_snapshot(&self) -> (OverlayState, f32) { (self.overlay, self.audio_level) } fn apply_runtime_state(&mut self, state: OverlayState, cx: &mut Context) { self.overlay = state; if state != OverlayState::Listening { self.audio_level = 0.0; } self.reconcile_overlay(cx); cx.notify(); } fn reconcile_overlay(&mut self, cx: &mut Context) { let should_be_open = self.overlay != OverlayState::Idle; if should_be_open && self.status_overlay_window.is_none() && !self.status_overlay_opening { self.status_overlay_opening = true; let app = cx.entity(); cx.defer(move |cx| { let opened = crate::overlay::open_status_overlay(app.clone(), cx); let mut close_after_open = None; app.update(cx, |this, _| { this.status_overlay_opening = false; match opened { Ok(window) if this.overlay != OverlayState::Idle => { this.status_overlay_window = Some(window); } Ok(window) => close_after_open = Some(window), Err(_error) => { #[cfg(debug_assertions)] eprintln!("[Origin Speak] could not open status overlay: {_error}"); } } }); if let Some(window) = close_after_open { let _ = window.update(cx, |_, window, _| window.remove_window()); } }); } else if !should_be_open && let Some(window) = self.status_overlay_window.take() { cx.defer(move |cx| { let _ = window.update(cx, |_, window, _| window.remove_window()); }); } } } impl Render for OriginSpeakApp { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { // The resident window is deliberately hidden. GPUI remains alive only // to own the platform event loop, global shortcut manager, and compact // overlay windows. div().size_full().bg(transparent()) } }