152 lines
4.9 KiB
Rust
152 lines
4.9 KiB
Rust
//! Launcher application state management
|
|
|
|
use shepherd_api::{ServiceStateSnapshot, EntryView, Event, EventPayload};
|
|
use shepherd_util::SessionId;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::sync::watch;
|
|
|
|
/// Current state of the launcher UI
|
|
#[derive(Debug, Clone)]
|
|
pub enum LauncherState {
|
|
/// Not connected to shepherdd
|
|
Disconnected,
|
|
/// Connected, waiting for initial state
|
|
Connecting,
|
|
/// Connected, no session running - show grid
|
|
Idle { entries: Vec<EntryView> },
|
|
/// Launch requested, waiting for response
|
|
Launching { entry_id: String },
|
|
/// Session is running
|
|
SessionActive {
|
|
session_id: SessionId,
|
|
entry_label: String,
|
|
time_remaining: Option<Duration>,
|
|
},
|
|
/// Error state
|
|
Error { message: String },
|
|
}
|
|
|
|
impl Default for LauncherState {
|
|
fn default() -> Self {
|
|
Self::Disconnected
|
|
}
|
|
}
|
|
|
|
/// Shared state container
|
|
#[derive(Clone)]
|
|
pub struct SharedState {
|
|
sender: watch::Sender<LauncherState>,
|
|
receiver: watch::Receiver<LauncherState>,
|
|
}
|
|
|
|
impl SharedState {
|
|
pub fn new() -> Self {
|
|
let (sender, receiver) = watch::channel(LauncherState::default());
|
|
Self { sender, receiver }
|
|
}
|
|
|
|
pub fn set(&self, state: LauncherState) {
|
|
let _ = self.sender.send(state);
|
|
}
|
|
|
|
pub fn get(&self) -> LauncherState {
|
|
self.receiver.borrow().clone()
|
|
}
|
|
|
|
pub fn subscribe(&self) -> watch::Receiver<LauncherState> {
|
|
self.receiver.clone()
|
|
}
|
|
|
|
/// Update state from shepherdd event
|
|
pub fn handle_event(&self, event: Event) {
|
|
tracing::info!(event = ?event.payload, "Received event from shepherdd");
|
|
match event.payload {
|
|
EventPayload::StateChanged(snapshot) => {
|
|
tracing::info!(has_session = snapshot.current_session.is_some(), "Applying state snapshot");
|
|
self.apply_snapshot(snapshot);
|
|
}
|
|
EventPayload::SessionStarted {
|
|
session_id,
|
|
entry_id: _,
|
|
label,
|
|
deadline,
|
|
} => {
|
|
tracing::info!(session_id = %session_id, label = %label, "Session started event");
|
|
let now = shepherd_util::now();
|
|
// For unlimited sessions (deadline=None), time_remaining is None
|
|
let time_remaining = deadline.and_then(|d| {
|
|
if d > now {
|
|
(d - now).to_std().ok()
|
|
} else {
|
|
Some(Duration::ZERO)
|
|
}
|
|
});
|
|
self.set(LauncherState::SessionActive {
|
|
session_id,
|
|
entry_label: label,
|
|
time_remaining,
|
|
});
|
|
}
|
|
EventPayload::SessionEnded { session_id, entry_id, reason, .. } => {
|
|
tracing::info!(session_id = %session_id, entry_id = %entry_id, reason = ?reason, "Session ended event - setting Connecting");
|
|
// Will be followed by StateChanged, but set to connecting
|
|
// to ensure grid reloads
|
|
self.set(LauncherState::Connecting);
|
|
}
|
|
EventPayload::SessionExpiring { .. } => {
|
|
// Time's up indicator handled by HUD
|
|
}
|
|
EventPayload::WarningIssued { .. } => {
|
|
// Warnings handled by HUD
|
|
}
|
|
EventPayload::PolicyReloaded { .. } => {
|
|
// Request fresh state
|
|
self.set(LauncherState::Connecting);
|
|
}
|
|
EventPayload::EntryAvailabilityChanged { .. } => {
|
|
// Request fresh state
|
|
self.set(LauncherState::Connecting);
|
|
}
|
|
EventPayload::Shutdown => {
|
|
// Service is shutting down
|
|
self.set(LauncherState::Disconnected);
|
|
}
|
|
EventPayload::AuditEntry { .. } => {
|
|
// Audit events are for admin clients, ignore
|
|
}
|
|
EventPayload::VolumeChanged { .. } => {
|
|
// Volume events are handled by HUD
|
|
}
|
|
}
|
|
}
|
|
|
|
fn apply_snapshot(&self, snapshot: ServiceStateSnapshot) {
|
|
if let Some(session) = snapshot.current_session {
|
|
let now = shepherd_util::now();
|
|
// For unlimited sessions (deadline=None), time_remaining is None
|
|
let time_remaining = session.deadline.and_then(|d| {
|
|
if d > now {
|
|
(d - now).to_std().ok()
|
|
} else {
|
|
Some(Duration::ZERO)
|
|
}
|
|
});
|
|
self.set(LauncherState::SessionActive {
|
|
session_id: session.session_id,
|
|
entry_label: session.label,
|
|
time_remaining,
|
|
});
|
|
} else {
|
|
self.set(LauncherState::Idle {
|
|
entries: snapshot.entries,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for SharedState {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|