Lint: electric boogaloo
This commit is contained in:
parent
3fd49b2efd
commit
5e5e6f6806
22 changed files with 106 additions and 291 deletions
|
|
@ -52,12 +52,16 @@ Run the test suite:
|
|||
|
||||
```sh
|
||||
cargo test
|
||||
# as run in CI:
|
||||
cargo test --all-targets
|
||||
```
|
||||
|
||||
Run lint checks:
|
||||
|
||||
```sh
|
||||
cargo clippy
|
||||
# as run in CI:
|
||||
cargo clippy --all-targets -- -D warnings
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -135,12 +135,6 @@ pub enum Command {
|
|||
/// Set volume to a specific percentage
|
||||
SetVolume { percent: u8 },
|
||||
|
||||
/// Increase volume by a step
|
||||
VolumeUp { step: u8 },
|
||||
|
||||
/// Decrease volume by a step
|
||||
VolumeDown { step: u8 },
|
||||
|
||||
/// Toggle mute state
|
||||
ToggleMute,
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
//! Validated policy structures
|
||||
|
||||
use crate::schema::{RawConfig, RawDays, RawEntry, RawEntryKind, RawVolumeConfig, RawServiceConfig, RawWarningThreshold};
|
||||
use crate::schema::{RawConfig, RawEntry, RawEntryKind, RawVolumeConfig, RawServiceConfig, RawWarningThreshold};
|
||||
use crate::validation::{parse_days, parse_time};
|
||||
use shepherd_api::{EntryKind, WarningSeverity, WarningThreshold};
|
||||
use shepherd_util::{DaysOfWeek, EntryId, TimeWindow, WallClock};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
|
|||
|
|
@ -113,8 +113,8 @@ fn validate_entry(entry: &RawEntry, config: &RawConfig) -> Vec<ValidationError>
|
|||
.or(config.service.default_max_run_seconds);
|
||||
|
||||
// Only validate warnings if max_run is Some and not 0 (unlimited)
|
||||
if let (Some(warnings), Some(max_run)) = (&entry.warnings, max_run) {
|
||||
if max_run > 0 {
|
||||
if let (Some(warnings), Some(max_run)) = (&entry.warnings, max_run)
|
||||
&& max_run > 0 {
|
||||
for warning in warnings {
|
||||
if warning.seconds_before >= max_run {
|
||||
errors.push(ValidationError::WarningExceedsMaxRun {
|
||||
|
|
@ -124,7 +124,6 @@ fn validate_entry(entry: &RawEntry, config: &RawConfig) -> Vec<ValidationError>
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Note: warnings are ignored for unlimited entries (max_run = 0)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use chrono::{DateTime, Local};
|
||||
use shepherd_api::{
|
||||
ServiceStateSnapshot, EntryKindTag, EntryView, ReasonCode, SessionEndReason,
|
||||
ServiceStateSnapshot, EntryView, ReasonCode, SessionEndReason,
|
||||
WarningSeverity, API_VERSION,
|
||||
};
|
||||
use shepherd_config::{Entry, Policy};
|
||||
|
|
@ -11,7 +11,7 @@ use shepherd_store::{AuditEvent, AuditEventType, Store};
|
|||
use shepherd_util::{EntryId, MonotonicInstant, SessionId};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{ActiveSession, CoreEvent, SessionPlan, StopResult};
|
||||
|
||||
|
|
@ -128,22 +128,20 @@ impl CoreEngine {
|
|||
}
|
||||
|
||||
// Check cooldown
|
||||
if let Ok(Some(until)) = self.store.get_cooldown_until(&entry.id) {
|
||||
if until > now {
|
||||
if let Ok(Some(until)) = self.store.get_cooldown_until(&entry.id)
|
||||
&& until > now {
|
||||
enabled = false;
|
||||
reasons.push(ReasonCode::CooldownActive { available_at: until });
|
||||
}
|
||||
}
|
||||
|
||||
// Check daily quota
|
||||
if let Some(quota) = entry.limits.daily_quota {
|
||||
let today = now.date_naive();
|
||||
if let Ok(used) = self.store.get_usage(&entry.id, today) {
|
||||
if used >= quota {
|
||||
if let Ok(used) = self.store.get_usage(&entry.id, today)
|
||||
&& used >= quota {
|
||||
enabled = false;
|
||||
reasons.push(ReasonCode::QuotaExhausted { used, quota });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate max run if enabled (None when disabled, Some(None) flattened for unlimited)
|
||||
|
|
@ -393,12 +391,11 @@ impl CoreEngine {
|
|||
let _ = self.store.add_usage(&session.plan.entry_id, today, duration);
|
||||
|
||||
// Set cooldown if configured
|
||||
if let Some(entry) = self.policy.get_entry(&session.plan.entry_id) {
|
||||
if let Some(cooldown) = entry.limits.cooldown {
|
||||
if let Some(entry) = self.policy.get_entry(&session.plan.entry_id)
|
||||
&& let Some(cooldown) = entry.limits.cooldown {
|
||||
let until = now + chrono::Duration::from_std(cooldown).unwrap();
|
||||
let _ = self.store.set_cooldown_until(&session.plan.entry_id, until);
|
||||
}
|
||||
}
|
||||
|
||||
// Log to audit
|
||||
let _ = self.store.append_audit(AuditEvent::new(AuditEventType::SessionEnded {
|
||||
|
|
@ -443,12 +440,11 @@ impl CoreEngine {
|
|||
let _ = self.store.add_usage(&session.plan.entry_id, today, duration);
|
||||
|
||||
// Set cooldown if configured
|
||||
if let Some(entry) = self.policy.get_entry(&session.plan.entry_id) {
|
||||
if let Some(cooldown) = entry.limits.cooldown {
|
||||
if let Some(entry) = self.policy.get_entry(&session.plan.entry_id)
|
||||
&& let Some(cooldown) = entry.limits.cooldown {
|
||||
let until = now + chrono::Duration::from_std(cooldown).unwrap();
|
||||
let _ = self.store.set_cooldown_until(&session.plan.entry_id, until);
|
||||
}
|
||||
}
|
||||
|
||||
// Log to audit
|
||||
let _ = self.store.append_audit(AuditEvent::new(AuditEventType::SessionEnded {
|
||||
|
|
@ -510,8 +506,8 @@ impl CoreEngine {
|
|||
pub fn extend_current(
|
||||
&mut self,
|
||||
by: Duration,
|
||||
now_mono: MonotonicInstant,
|
||||
now: DateTime<Local>,
|
||||
_now_mono: MonotonicInstant,
|
||||
_now: DateTime<Local>,
|
||||
) -> Option<DateTime<Local>> {
|
||||
let session = self.current_session.as_mut()?;
|
||||
|
||||
|
|
|
|||
|
|
@ -37,9 +37,7 @@ pub struct VolumeStatus {
|
|||
impl VolumeStatus {
|
||||
/// Get an icon name for the current volume status
|
||||
pub fn icon_name(&self) -> &'static str {
|
||||
if self.muted {
|
||||
"audio-volume-muted-symbolic"
|
||||
} else if self.percent == 0 {
|
||||
if self.muted || self.percent == 0 {
|
||||
"audio-volume-muted-symbolic"
|
||||
} else if self.percent < 33 {
|
||||
"audio-volume-low-symbolic"
|
||||
|
|
|
|||
|
|
@ -126,11 +126,10 @@ impl HostAdapter for LinuxHost {
|
|||
// followed by any additional args.
|
||||
let mut argv = vec!["snap".to_string(), "run".to_string(), snap_name.clone()];
|
||||
// If a custom command is specified (different from snap_name), add it
|
||||
if let Some(cmd) = command {
|
||||
if cmd != snap_name {
|
||||
if let Some(cmd) = command
|
||||
&& cmd != snap_name {
|
||||
argv.push(cmd.clone());
|
||||
}
|
||||
}
|
||||
argv.extend(args.clone());
|
||||
(argv, env.clone(), None, Some(snap_name.clone()))
|
||||
}
|
||||
|
|
@ -336,8 +335,8 @@ mod tests {
|
|||
|
||||
// Process should have exited
|
||||
match handle.payload() {
|
||||
HostHandlePayload::Linux { pid, .. } => {
|
||||
let procs = host.processes.lock().unwrap();
|
||||
HostHandlePayload::Linux { pid: _, .. } => {
|
||||
let _procs = host.processes.lock().unwrap();
|
||||
// Process may or may not still be tracked depending on monitor timing
|
||||
}
|
||||
_ => panic!("Expected Linux handle"),
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ impl ManagedProcess {
|
|||
unsafe {
|
||||
cmd.pre_exec(|| {
|
||||
nix::unistd::setsid().map_err(|e| {
|
||||
std::io::Error::new(std::io::ErrorKind::Other, e.to_string())
|
||||
std::io::Error::other(e.to_string())
|
||||
})?;
|
||||
Ok(())
|
||||
});
|
||||
|
|
@ -304,14 +304,12 @@ impl ManagedProcess {
|
|||
if let Some(paren_end) = stat.rfind(')') {
|
||||
let after_comm = &stat[paren_end + 2..];
|
||||
let fields: Vec<&str> = after_comm.split_whitespace().collect();
|
||||
if fields.len() >= 2 {
|
||||
if let Ok(ppid) = fields[1].parse::<i32>() {
|
||||
if ppid == parent_pid {
|
||||
descendants.push(pid);
|
||||
to_check.push(pid);
|
||||
}
|
||||
if fields.len() >= 2
|
||||
&& let Ok(ppid) = fields[1].parse::<i32>()
|
||||
&& ppid == parent_pid {
|
||||
descendants.push(pid);
|
||||
to_check.push(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,11 +147,10 @@ impl LinuxVolumeController {
|
|||
debug!("pactl get-sink-volume output: {}", stdout.trim());
|
||||
|
||||
// Output: "Volume: front-left: 65536 / 100% / -0.00 dB, front-right: ..."
|
||||
if let Some(percent_str) = stdout.split('/').nth(1) {
|
||||
if let Ok(percent) = percent_str.trim().trim_end_matches('%').parse::<u8>() {
|
||||
if let Some(percent_str) = stdout.split('/').nth(1)
|
||||
&& let Ok(percent) = percent_str.trim().trim_end_matches('%').parse::<u8>() {
|
||||
status.percent = percent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check mute status
|
||||
|
|
@ -184,13 +183,11 @@ impl LinuxVolumeController {
|
|||
for line in stdout.lines() {
|
||||
if line.contains("Playback") && line.contains('%') {
|
||||
// Extract percentage: [100%]
|
||||
if let Some(start) = line.find('[') {
|
||||
if let Some(end) = line[start..].find('%') {
|
||||
if let Ok(percent) = line[start + 1..start + end].parse::<u8>() {
|
||||
status.percent = percent;
|
||||
}
|
||||
if let Some(start) = line.find('[')
|
||||
&& let Some(end) = line[start..].find('%')
|
||||
&& let Ok(percent) = line[start + 1..start + end].parse::<u8>() {
|
||||
status.percent = percent;
|
||||
}
|
||||
}
|
||||
// Check mute status: [on] or [off]
|
||||
status.muted = line.contains("[off]");
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -56,9 +56,6 @@ impl HudApp {
|
|||
}
|
||||
});
|
||||
|
||||
// Start periodic updates for battery/volume
|
||||
start_metrics_updates(state.clone());
|
||||
|
||||
// Subscribe to state changes
|
||||
let window_clone = window.clone();
|
||||
let state_clone = state.clone();
|
||||
|
|
@ -727,9 +724,3 @@ fn run_event_loop(socket_path: PathBuf, state: SharedState) -> anyhow::Result<()
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn start_metrics_updates(_state: SharedState) {
|
||||
// Battery and volume are now updated in the main UI loop
|
||||
// This function could be used for more expensive operations
|
||||
// that don't need to run as frequently
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,19 +34,17 @@ impl BatteryStatus {
|
|||
let name_str = name.to_string_lossy();
|
||||
|
||||
// Check for battery
|
||||
if name_str.starts_with("BAT") {
|
||||
if let Some((percent, charging)) = read_battery_info(&path) {
|
||||
if name_str.starts_with("BAT")
|
||||
&& let Some((percent, charging)) = read_battery_info(&path) {
|
||||
status.percent = Some(percent);
|
||||
status.charging = charging;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for AC adapter
|
||||
if name_str.starts_with("AC") || name_str.contains("ADP") {
|
||||
if let Some(online) = read_ac_status(&path) {
|
||||
if (name_str.starts_with("AC") || name_str.contains("ADP"))
|
||||
&& let Some(online) = read_ac_status(&path) {
|
||||
status.ac_connected = online;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +68,7 @@ impl BatteryStatus {
|
|||
}
|
||||
|
||||
/// Check if battery is critically low
|
||||
#[allow(dead_code)]
|
||||
pub fn is_critical(&self) -> bool {
|
||||
matches!(self.percent, Some(p) if p < 10 && !self.charging)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ mod volume;
|
|||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use gtk4::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
//!
|
||||
//! The HUD subscribes to events from shepherdd and tracks session state.
|
||||
|
||||
use chrono::Local;
|
||||
use shepherd_api::{Event, EventPayload, SessionEndReason, VolumeInfo, VolumeRestrictions, WarningSeverity};
|
||||
use shepherd_api::{Event, EventPayload, VolumeInfo, VolumeRestrictions, WarningSeverity};
|
||||
use shepherd_util::{EntryId, SessionId};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::watch;
|
||||
|
|
@ -21,6 +20,7 @@ pub enum SessionState {
|
|||
entry_name: String,
|
||||
started_at: std::time::Instant,
|
||||
time_limit_secs: Option<u64>,
|
||||
#[allow(dead_code)]
|
||||
time_remaining_secs: Option<u64>,
|
||||
},
|
||||
|
||||
|
|
@ -64,19 +64,6 @@ impl SessionState {
|
|||
}
|
||||
}
|
||||
|
||||
/// System metrics for display
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SystemMetrics {
|
||||
/// Battery percentage (0-100)
|
||||
pub battery_percent: Option<u8>,
|
||||
/// Whether battery is charging
|
||||
pub battery_charging: bool,
|
||||
/// Volume percentage (0-100)
|
||||
pub volume_percent: Option<u8>,
|
||||
/// Whether volume is muted
|
||||
pub volume_muted: bool,
|
||||
}
|
||||
|
||||
/// Shared state for the HUD
|
||||
#[derive(Clone)]
|
||||
pub struct SharedState {
|
||||
|
|
@ -84,10 +71,6 @@ pub struct SharedState {
|
|||
session_tx: Arc<watch::Sender<SessionState>>,
|
||||
/// Session state receiver
|
||||
session_rx: watch::Receiver<SessionState>,
|
||||
/// System metrics sender
|
||||
metrics_tx: Arc<watch::Sender<SystemMetrics>>,
|
||||
/// System metrics receiver
|
||||
metrics_rx: watch::Receiver<SystemMetrics>,
|
||||
/// Volume info sender (updated via events, not polling)
|
||||
volume_tx: Arc<watch::Sender<Option<VolumeInfo>>>,
|
||||
/// Volume info receiver
|
||||
|
|
@ -97,14 +80,11 @@ pub struct SharedState {
|
|||
impl SharedState {
|
||||
pub fn new() -> Self {
|
||||
let (session_tx, session_rx) = watch::channel(SessionState::NoSession);
|
||||
let (metrics_tx, metrics_rx) = watch::channel(SystemMetrics::default());
|
||||
let (volume_tx, volume_rx) = watch::channel(None);
|
||||
|
||||
Self {
|
||||
session_tx: Arc::new(session_tx),
|
||||
session_rx,
|
||||
metrics_tx: Arc::new(metrics_tx),
|
||||
metrics_rx,
|
||||
volume_tx: Arc::new(volume_tx),
|
||||
volume_rx,
|
||||
}
|
||||
|
|
@ -116,25 +96,16 @@ impl SharedState {
|
|||
}
|
||||
|
||||
/// Subscribe to session state changes
|
||||
#[allow(dead_code)]
|
||||
pub fn subscribe_session(&self) -> watch::Receiver<SessionState> {
|
||||
self.session_rx.clone()
|
||||
}
|
||||
|
||||
/// Subscribe to metrics changes
|
||||
pub fn subscribe_metrics(&self) -> watch::Receiver<SystemMetrics> {
|
||||
self.metrics_rx.clone()
|
||||
}
|
||||
|
||||
/// Update session state
|
||||
pub fn set_session_state(&self, state: SessionState) {
|
||||
let _ = self.session_tx.send(state);
|
||||
}
|
||||
|
||||
/// Update system metrics
|
||||
pub fn set_metrics(&self, metrics: SystemMetrics) {
|
||||
let _ = self.metrics_tx.send(metrics);
|
||||
}
|
||||
|
||||
/// Get current volume info (cached from events)
|
||||
pub fn volume_info(&self) -> Option<VolumeInfo> {
|
||||
self.volume_rx.borrow().clone()
|
||||
|
|
@ -165,6 +136,7 @@ impl SharedState {
|
|||
}
|
||||
|
||||
/// Update time remaining for current session
|
||||
#[allow(dead_code)]
|
||||
pub fn update_time_remaining(&self, remaining_secs: u64) {
|
||||
self.session_tx.send_modify(|state| {
|
||||
if let SessionState::Active {
|
||||
|
|
@ -246,8 +218,7 @@ impl SharedState {
|
|||
entry_name,
|
||||
..
|
||||
} = state
|
||||
{
|
||||
if sid == session_id {
|
||||
&& sid == session_id {
|
||||
*state = SessionState::Warning {
|
||||
session_id: session_id.clone(),
|
||||
entry_id: entry_id.clone(),
|
||||
|
|
@ -258,7 +229,6 @@ impl SharedState {
|
|||
severity: *severity,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -275,11 +245,11 @@ impl SharedState {
|
|||
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| {
|
||||
let time_remaining = session.deadline.map(|d| {
|
||||
if d > now {
|
||||
Some((d - now).num_seconds().max(0) as u64)
|
||||
(d - now).num_seconds().max(0) as u64
|
||||
} else {
|
||||
Some(0)
|
||||
0
|
||||
}
|
||||
});
|
||||
self.set_session_state(SessionState::Active {
|
||||
|
|
|
|||
|
|
@ -75,52 +75,6 @@ pub fn toggle_mute() -> anyhow::Result<()> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Increase volume by a step via shepherdd
|
||||
pub fn volume_up(step: u8) -> anyhow::Result<()> {
|
||||
let socket_path = get_socket_path();
|
||||
|
||||
let rt = Runtime::new()?;
|
||||
|
||||
rt.block_on(async {
|
||||
let mut client = IpcClient::connect(&socket_path).await?;
|
||||
let response = client.send(Command::VolumeUp { step }).await?;
|
||||
|
||||
match response.result {
|
||||
shepherd_api::ResponseResult::Ok(ResponsePayload::VolumeSet) => Ok(()),
|
||||
shepherd_api::ResponseResult::Ok(ResponsePayload::VolumeDenied { reason }) => {
|
||||
Err(anyhow::anyhow!("Volume denied: {}", reason))
|
||||
}
|
||||
shepherd_api::ResponseResult::Err(e) => {
|
||||
Err(anyhow::anyhow!("Error: {}", e.message))
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Unexpected response")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Decrease volume by a step via shepherdd
|
||||
pub fn volume_down(step: u8) -> anyhow::Result<()> {
|
||||
let socket_path = get_socket_path();
|
||||
|
||||
let rt = Runtime::new()?;
|
||||
|
||||
rt.block_on(async {
|
||||
let mut client = IpcClient::connect(&socket_path).await?;
|
||||
let response = client.send(Command::VolumeDown { step }).await?;
|
||||
|
||||
match response.result {
|
||||
shepherd_api::ResponseResult::Ok(ResponsePayload::VolumeSet) => Ok(()),
|
||||
shepherd_api::ResponseResult::Ok(ResponsePayload::VolumeDenied { reason }) => {
|
||||
Err(anyhow::anyhow!("Volume denied: {}", reason))
|
||||
}
|
||||
shepherd_api::ResponseResult::Err(e) => {
|
||||
Err(anyhow::anyhow!("Error: {}", e.message))
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Unexpected response")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Set volume to a specific percentage via shepherdd
|
||||
pub fn set_volume(percent: u8) -> anyhow::Result<()> {
|
||||
let socket_path = get_socket_path();
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ impl IpcServer {
|
|||
let client_id_clone = client_id.clone();
|
||||
|
||||
// Spawn reader task
|
||||
let reader_handle = tokio::spawn(async move {
|
||||
let _reader_handle = tokio::spawn(async move {
|
||||
let mut reader = BufReader::new(read_half);
|
||||
let mut line = String::new();
|
||||
|
||||
|
|
@ -235,8 +235,8 @@ impl IpcServer {
|
|||
clients.get(&client_id_writer).map(|h| h.subscribed).unwrap_or(false)
|
||||
};
|
||||
|
||||
if is_subscribed {
|
||||
if let Ok(json) = serde_json::to_string(&event) {
|
||||
if is_subscribed
|
||||
&& let Ok(json) = serde_json::to_string(&event) {
|
||||
let mut msg = json;
|
||||
msg.push('\n');
|
||||
if let Err(e) = writer.write_all(msg.as_bytes()).await {
|
||||
|
|
@ -244,7 +244,6 @@ impl IpcServer {
|
|||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,13 @@
|
|||
|
||||
use gtk4::glib;
|
||||
use gtk4::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::client::{ClientCommand, CommandClient, ServiceClient};
|
||||
use crate::client::{CommandClient, ServiceClient};
|
||||
use crate::grid::LauncherGrid;
|
||||
use crate::state::{LauncherState, SharedState};
|
||||
|
||||
|
|
@ -166,7 +164,7 @@ impl LauncherApp {
|
|||
let runtime = Arc::new(Runtime::new().expect("Failed to create tokio runtime"));
|
||||
|
||||
// Create command channel
|
||||
let (command_tx, command_rx) = mpsc::unbounded_channel();
|
||||
let (_command_tx, command_rx) = mpsc::unbounded_channel();
|
||||
|
||||
// Create command client for sending commands
|
||||
let command_client = Arc::new(CommandClient::new(&socket_path));
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
//! IPC client wrapper for the launcher UI
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use shepherd_api::{Command, Event, ReasonCode, Response, ResponsePayload, ResponseResult};
|
||||
use shepherd_api::{Command, ReasonCode, Response, ResponsePayload, ResponseResult};
|
||||
use shepherd_ipc::IpcClient;
|
||||
use shepherd_util::EntryId;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::state::{LauncherState, SharedState};
|
||||
|
||||
/// Messages from UI to client task
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum ClientCommand {
|
||||
/// Request to launch an entry
|
||||
Launch(EntryId),
|
||||
|
|
@ -98,7 +99,7 @@ impl ServiceClient {
|
|||
info!("Shutdown requested");
|
||||
return Ok(());
|
||||
}
|
||||
ClientCommand::Launch(entry_id) => {
|
||||
ClientCommand::Launch(_entry_id) => {
|
||||
// We can't send commands after subscribing since client is consumed
|
||||
// Need to reconnect for commands
|
||||
warn!("Launch command received but cannot send after subscribe");
|
||||
|
|
@ -222,6 +223,7 @@ impl CommandClient {
|
|||
}).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn stop_current(&self) -> Result<Response> {
|
||||
let mut client = IpcClient::connect(&self.socket_path).await?;
|
||||
client.send(Command::StopCurrent {
|
||||
|
|
@ -234,6 +236,7 @@ impl CommandClient {
|
|||
client.send(Command::GetState).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn list_entries(&self) -> Result<Response> {
|
||||
let mut client = IpcClient::connect(&self.socket_path).await?;
|
||||
client.send(Command::ListEntries { at_time: None }).await.map_err(Into::into)
|
||||
|
|
|
|||
|
|
@ -13,10 +13,12 @@ use crate::tile::LauncherTile;
|
|||
mod imp {
|
||||
use super::*;
|
||||
|
||||
type LaunchCallback = Rc<RefCell<Option<Box<dyn Fn(EntryId) + 'static>>>>;
|
||||
|
||||
pub struct LauncherGrid {
|
||||
pub flow_box: gtk4::FlowBox,
|
||||
pub tiles: RefCell<Vec<LauncherTile>>,
|
||||
pub on_launch: Rc<RefCell<Option<Box<dyn Fn(EntryId) + 'static>>>>,
|
||||
pub on_launch: LaunchCallback,
|
||||
}
|
||||
|
||||
impl Default for LauncherGrid {
|
||||
|
|
@ -114,11 +116,10 @@ impl LauncherGrid {
|
|||
// Connect click handler
|
||||
let on_launch = imp.on_launch.clone();
|
||||
tile.connect_clicked(move |tile| {
|
||||
if let Some(entry_id) = tile.entry_id() {
|
||||
if let Some(callback) = on_launch.borrow().as_ref() {
|
||||
if let Some(entry_id) = tile.entry_id()
|
||||
&& let Some(callback) = on_launch.borrow().as_ref() {
|
||||
callback(entry_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
imp.flow_box.insert(&tile, -1);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ mod tile;
|
|||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use gtk4::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,37 +2,36 @@
|
|||
|
||||
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)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum LauncherState {
|
||||
/// Not connected to shepherdd
|
||||
#[default]
|
||||
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 },
|
||||
Launching {
|
||||
#[allow(dead_code)]
|
||||
entry_id: String
|
||||
},
|
||||
/// Session is running
|
||||
SessionActive {
|
||||
#[allow(dead_code)]
|
||||
session_id: SessionId,
|
||||
entry_label: String,
|
||||
#[allow(dead_code)]
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! SQLite-based store implementation
|
||||
|
||||
use chrono::{DateTime, Local, NaiveDate, TimeZone};
|
||||
use chrono::{DateTime, Local, NaiveDate};
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use shepherd_util::EntryId;
|
||||
use std::path::Path;
|
||||
|
|
@ -8,7 +8,7 @@ use std::sync::Mutex;
|
|||
use std::time::Duration;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::{AuditEvent, SessionSnapshot, StateSnapshot, Store, StoreError, StoreResult};
|
||||
use crate::{AuditEvent, StateSnapshot, Store, StoreResult};
|
||||
|
||||
/// SQLite-based store
|
||||
pub struct SqliteStore {
|
||||
|
|
|
|||
|
|
@ -12,22 +12,21 @@
|
|||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use shepherd_api::{
|
||||
Command, ServiceStateSnapshot, ErrorCode, ErrorInfo, Event, EventPayload, HealthStatus,
|
||||
Command, ErrorCode, ErrorInfo, Event, EventPayload, HealthStatus,
|
||||
Response, ResponsePayload, SessionEndReason, StopMode, VolumeInfo, VolumeRestrictions,
|
||||
API_VERSION,
|
||||
};
|
||||
use shepherd_config::{load_config, Policy, VolumePolicy};
|
||||
use shepherd_config::{load_config, VolumePolicy};
|
||||
use shepherd_core::{CoreEngine, CoreEvent, LaunchDecision, StopDecision};
|
||||
use shepherd_host_api::{HostAdapter, HostEvent, StopMode as HostStopMode, VolumeController};
|
||||
use shepherd_host_linux::{LinuxHost, LinuxVolumeController};
|
||||
use shepherd_ipc::{IpcServer, ServerMessage};
|
||||
use shepherd_store::{AuditEvent, AuditEventType, SqliteStore, Store};
|
||||
use shepherd_util::{ClientId, EntryId, MonotonicInstant, RateLimiter};
|
||||
use shepherd_util::{ClientId, MonotonicInstant, RateLimiter};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, error, info, warn, Level};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
/// shepherdd - Policy enforcement service for child-focused computing
|
||||
|
|
@ -243,8 +242,8 @@ impl Service {
|
|||
.and_then(|s| s.host_handle.clone())
|
||||
};
|
||||
|
||||
if let Some(handle) = handle {
|
||||
if let Err(e) = host
|
||||
if let Some(handle) = handle
|
||||
&& let Err(e) = host
|
||||
.stop(
|
||||
&handle,
|
||||
HostStopMode::Graceful {
|
||||
|
|
@ -256,7 +255,6 @@ impl Service {
|
|||
warn!(error = %e, "Failed to stop session gracefully, forcing");
|
||||
let _ = host.stop(&handle, HostStopMode::Force).await;
|
||||
}
|
||||
}
|
||||
|
||||
ipc.broadcast_event(Event::new(EventPayload::SessionExpiring {
|
||||
session_id: session_id.clone(),
|
||||
|
|
@ -336,13 +334,12 @@ impl Service {
|
|||
|
||||
info!(has_event = core_event.is_some(), "notify_session_exited result");
|
||||
|
||||
if let Some(event) = core_event {
|
||||
if let CoreEvent::SessionEnded {
|
||||
session_id,
|
||||
entry_id,
|
||||
reason,
|
||||
duration,
|
||||
} = event
|
||||
if let Some(CoreEvent::SessionEnded {
|
||||
session_id,
|
||||
entry_id,
|
||||
reason,
|
||||
duration,
|
||||
}) = core_event
|
||||
{
|
||||
info!(
|
||||
session_id = %session_id,
|
||||
|
|
@ -366,7 +363,6 @@ impl Service {
|
|||
info!("Broadcasting StateChanged");
|
||||
ipc.broadcast_event(Event::new(EventPayload::StateChanged(state)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HostEvent::WindowReady { handle } => {
|
||||
|
|
@ -443,6 +439,7 @@ impl Service {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn handle_command(
|
||||
engine: &Arc<Mutex<CoreEngine>>,
|
||||
host: &Arc<LinuxHost>,
|
||||
|
|
@ -530,13 +527,12 @@ impl Service {
|
|||
Err(e) => {
|
||||
// Notify session ended with error and broadcast to subscribers
|
||||
let mut eng = engine.lock().await;
|
||||
if let Some(event) = eng.notify_session_exited(Some(-1), now_mono, now) {
|
||||
if let CoreEvent::SessionEnded {
|
||||
session_id,
|
||||
entry_id,
|
||||
reason,
|
||||
duration,
|
||||
} = event
|
||||
if let Some(CoreEvent::SessionEnded {
|
||||
session_id,
|
||||
entry_id,
|
||||
reason,
|
||||
duration,
|
||||
}) = eng.notify_session_exited(Some(-1), now_mono, now)
|
||||
{
|
||||
ipc.broadcast_event(Event::new(EventPayload::SessionEnded {
|
||||
session_id,
|
||||
|
|
@ -549,7 +545,6 @@ impl Service {
|
|||
let state = eng.get_state();
|
||||
ipc.broadcast_event(Event::new(EventPayload::StateChanged(state)));
|
||||
}
|
||||
}
|
||||
|
||||
Response::error(
|
||||
request_id,
|
||||
|
|
@ -629,14 +624,13 @@ impl Service {
|
|||
|
||||
Command::ReloadConfig => {
|
||||
// Check permission
|
||||
if let Some(info) = ipc.get_client_info(client_id).await {
|
||||
if !info.role.can_reload_config() {
|
||||
if let Some(info) = ipc.get_client_info(client_id).await
|
||||
&& !info.role.can_reload_config() {
|
||||
return Response::error(
|
||||
request_id,
|
||||
ErrorInfo::new(ErrorCode::PermissionDenied, "Admin role required"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Reload from original config path
|
||||
Response::error(
|
||||
|
|
@ -672,14 +666,13 @@ impl Service {
|
|||
|
||||
Command::ExtendCurrent { by } => {
|
||||
// Check permission
|
||||
if let Some(info) = ipc.get_client_info(client_id).await {
|
||||
if !info.role.can_extend() {
|
||||
if let Some(info) = ipc.get_client_info(client_id).await
|
||||
&& !info.role.can_extend() {
|
||||
return Response::error(
|
||||
request_id,
|
||||
ErrorInfo::new(ErrorCode::PermissionDenied, "Admin role required"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut eng = engine.lock().await;
|
||||
match eng.extend_current(by, now_mono, now) {
|
||||
|
|
@ -694,7 +687,7 @@ impl Service {
|
|||
}
|
||||
|
||||
Command::GetVolume => {
|
||||
let restrictions = Self::get_current_volume_restrictions(&engine).await;
|
||||
let restrictions = Self::get_current_volume_restrictions(engine).await;
|
||||
|
||||
match volume.get_status().await {
|
||||
Ok(status) => {
|
||||
|
|
@ -722,7 +715,7 @@ impl Service {
|
|||
}
|
||||
|
||||
Command::SetVolume { percent } => {
|
||||
let restrictions = Self::get_current_volume_restrictions(&engine).await;
|
||||
let restrictions = Self::get_current_volume_restrictions(engine).await;
|
||||
|
||||
if !restrictions.allow_change {
|
||||
return Response::success(
|
||||
|
|
@ -755,80 +748,8 @@ impl Service {
|
|||
}
|
||||
}
|
||||
|
||||
Command::VolumeUp { step } => {
|
||||
let restrictions = Self::get_current_volume_restrictions(&engine).await;
|
||||
|
||||
if !restrictions.allow_change {
|
||||
return Response::success(
|
||||
request_id,
|
||||
ResponsePayload::VolumeDenied {
|
||||
reason: "Volume changes are not allowed".into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Get current volume and check if we'd exceed max
|
||||
let current = volume.get_status().await.map(|s| s.percent).unwrap_or(0);
|
||||
let target = current.saturating_add(step);
|
||||
let clamped = restrictions.clamp_volume(target);
|
||||
|
||||
match volume.set_volume(clamped).await {
|
||||
Ok(()) => {
|
||||
if let Ok(status) = volume.get_status().await {
|
||||
ipc.broadcast_event(Event::new(EventPayload::VolumeChanged {
|
||||
percent: status.percent,
|
||||
muted: status.muted,
|
||||
}));
|
||||
}
|
||||
Response::success(request_id, ResponsePayload::VolumeSet)
|
||||
}
|
||||
Err(e) => Response::success(
|
||||
request_id,
|
||||
ResponsePayload::VolumeDenied {
|
||||
reason: e.to_string(),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Command::VolumeDown { step } => {
|
||||
let restrictions = Self::get_current_volume_restrictions(&engine).await;
|
||||
|
||||
if !restrictions.allow_change {
|
||||
return Response::success(
|
||||
request_id,
|
||||
ResponsePayload::VolumeDenied {
|
||||
reason: "Volume changes are not allowed".into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Get current volume and check if we'd go below min
|
||||
let current = volume.get_status().await.map(|s| s.percent).unwrap_or(0);
|
||||
let target = current.saturating_sub(step);
|
||||
let clamped = restrictions.clamp_volume(target);
|
||||
|
||||
match volume.set_volume(clamped).await {
|
||||
Ok(()) => {
|
||||
if let Ok(status) = volume.get_status().await {
|
||||
ipc.broadcast_event(Event::new(EventPayload::VolumeChanged {
|
||||
percent: status.percent,
|
||||
muted: status.muted,
|
||||
}));
|
||||
}
|
||||
Response::success(request_id, ResponsePayload::VolumeSet)
|
||||
}
|
||||
Err(e) => Response::success(
|
||||
request_id,
|
||||
ResponsePayload::VolumeDenied {
|
||||
reason: e.to_string(),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Command::ToggleMute => {
|
||||
let restrictions = Self::get_current_volume_restrictions(&engine).await;
|
||||
let restrictions = Self::get_current_volume_restrictions(engine).await;
|
||||
|
||||
if !restrictions.allow_mute {
|
||||
return Response::success(
|
||||
|
|
@ -859,7 +780,7 @@ impl Service {
|
|||
}
|
||||
|
||||
Command::SetMute { muted } => {
|
||||
let restrictions = Self::get_current_volume_restrictions(&engine).await;
|
||||
let restrictions = Self::get_current_volume_restrictions(engine).await;
|
||||
|
||||
if !restrictions.allow_mute {
|
||||
return Response::success(
|
||||
|
|
@ -900,13 +821,11 @@ impl Service {
|
|||
let eng = engine.lock().await;
|
||||
|
||||
// Check if there's an active session with volume restrictions
|
||||
if let Some(session) = eng.current_session() {
|
||||
if let Some(entry) = eng.policy().get_entry(&session.plan.entry_id) {
|
||||
if let Some(ref vol_policy) = entry.volume {
|
||||
return Self::convert_volume_policy(vol_policy);
|
||||
}
|
||||
if let Some(session) = eng.current_session()
|
||||
&& let Some(entry) = eng.policy().get_entry(&session.plan.entry_id)
|
||||
&& let Some(ref vol_policy) = entry.volume {
|
||||
return Self::convert_volume_policy(vol_policy);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to global policy
|
||||
Self::convert_volume_policy(&eng.policy().volume)
|
||||
|
|
|
|||
Loading…
Reference in a new issue