mod apogee_core_v1 { hyprwire::include_protocol!("apogee_core_v1"); pub use client::*; pub use spec::*; } use apogee_core_v1::{ apogee_condition_v1, apogee_environment_v1, apogee_manager_v1, apogee_reload_v1, apogee_service_logs_v1, apogee_service_v1, apogee_task_v1, }; use clap::{Parser, Subcommand}; use comfy_table::presets; use hyprwire::client; use owo_colors::OwoColorize; use std::collections::BTreeMap; use std::{path, process}; #[derive(Clone, Copy, PartialEq, Eq)] enum SnapshotState { Pending, Complete, Destroyed, } #[derive(Clone, Copy, PartialEq, Eq)] enum ListTarget { All, Conditions, } #[derive(Default, PartialEq, Eq)] enum OperationState { #[default] Pending, Complete, } struct Service { object: apogee_service_v1::ApogeeServiceV1, requested_name: String, name: Option, pid: Option, started_at: Option, state: Option, description: Option, command: Option, args: Vec, conditions: Vec, restart_count: Option, user: Option, group: Option, snapshot: SnapshotState, } struct Task { object: apogee_task_v1::ApogeeTaskV1, requested_name: String, name: Option, pid: Option, started_at: Option, state: Option, description: Option, command: Option, args: Vec, conditions: Vec, snapshot: SnapshotState, } struct LogEntry { message: String, } struct ServiceLogs { object: apogee_service_logs_v1::ApogeeServiceLogsV1, entries: Vec, snapshot: SnapshotState, } struct Condition { object: apogee_condition_v1::ApogeeConditionV1, requested_name: String, name: Option, fulfilled: Option, can_modify: Option, snapshot: SnapshotState, } struct Environment { object: apogee_environment_v1::ApogeeEnvironmentV1, variables: BTreeMap, can_modify: Option, snapshot: SnapshotState, } #[derive(Default)] struct App { reloaded: bool, failed: bool, manager_done: bool, operation: OperationState, list_target: Option, services: Vec, tasks: Vec, conditions: Vec, environment: Option, logs: Option, } impl App { fn list_complete(&self) -> bool { if !self.manager_done { return false; } match self.list_target { Some(ListTarget::All) => { self.services .iter() .all(|service| service.snapshot != SnapshotState::Pending) && self .tasks .iter() .all(|task| task.snapshot != SnapshotState::Pending) } Some(ListTarget::Conditions) => self .conditions .iter() .all(|condition| condition.snapshot != SnapshotState::Pending), None => true, } } fn condition_snapshot_complete(&self) -> bool { self.conditions .first() .is_some_and(|condition| condition.snapshot != SnapshotState::Pending) } fn condition_snapshots_complete(&self) -> bool { self.conditions .iter() .all(|condition| condition.snapshot != SnapshotState::Pending) } fn service_snapshot_complete(&self) -> bool { self.services .first() .is_some_and(|service| service.snapshot != SnapshotState::Pending) } fn task_snapshot_complete(&self) -> bool { self.tasks .first() .is_some_and(|task| task.snapshot != SnapshotState::Pending) } fn logs_snapshot_complete(&self) -> bool { self.logs .as_ref() .is_some_and(|logs| logs.snapshot != SnapshotState::Pending) } fn environment_snapshot_complete(&self) -> bool { self.environment .as_ref() .is_some_and(|environment| environment.snapshot == SnapshotState::Complete) } } impl hyprwire::Dispatch for App { fn event( &mut self, object: &apogee_manager_v1::ApogeeManagerV1, event: ::Event<'_>, ) { match event { apogee_manager_v1::Event::ServiceAdded { name } if self.list_target == Some(ListTarget::All) => { if self .services .iter() .any(|service| service.requested_name == name) { return; } if let Some(service) = object.send_get_service::(&name) { self.services.push(Service { object: service, requested_name: name, name: None, pid: None, started_at: None, state: None, description: None, command: None, args: Vec::new(), conditions: Vec::new(), restart_count: None, user: None, group: None, snapshot: SnapshotState::Pending, }); } else { self.failed = true; } } apogee_manager_v1::Event::ServiceRemoved { name } if self.list_target == Some(ListTarget::All) => { for service in &mut self.services { if service.requested_name == name || service.name.as_deref() == Some(&name) { service.snapshot = SnapshotState::Destroyed; } } } apogee_manager_v1::Event::TaskAdded { name } if self.list_target == Some(ListTarget::All) => { if self.tasks.iter().any(|task| task.requested_name == name) { return; } if let Some(task) = object.send_get_task::(&name) { self.tasks.push(Task { object: task, requested_name: name, name: None, pid: None, started_at: None, state: None, description: None, command: None, args: Vec::new(), conditions: Vec::new(), snapshot: SnapshotState::Pending, }); } else { self.failed = true; } } apogee_manager_v1::Event::TaskRemoved { name } if self.list_target == Some(ListTarget::All) => { for task in &mut self.tasks { if task.requested_name == name || task.name.as_deref() == Some(&name) { task.snapshot = SnapshotState::Destroyed; } } } apogee_manager_v1::Event::ConditionAdded { name } if self.list_target == Some(ListTarget::Conditions) => { if self .conditions .iter() .any(|condition| condition.requested_name == name) { return; } if let Some(condition) = object.send_get_condition::(&name) { self.conditions.push(Condition { object: condition, requested_name: name, name: None, fulfilled: None, can_modify: None, snapshot: SnapshotState::Pending, }); } else { self.failed = true; } } apogee_manager_v1::Event::ConditionRemoved { name } if self.list_target == Some(ListTarget::Conditions) => { for condition in &mut self.conditions { if condition.requested_name == name || condition.name.as_deref() == Some(&name) { condition.snapshot = SnapshotState::Destroyed; } } } apogee_manager_v1::Event::Done => self.manager_done = true, _ => {} } } } impl hyprwire::Dispatch for App { fn event( &mut self, object: &apogee_condition_v1::ApogeeConditionV1, event: ::Event<'_>, ) { let Some(index) = self .conditions .iter() .position(|condition| &condition.object == object) else { return; }; match event { apogee_condition_v1::Event::Name { name } => { self.conditions[index].name = Some(name); } apogee_condition_v1::Event::Fulfilled { fulfilled } => { self.conditions[index].fulfilled = Some(fulfilled != 0); } apogee_condition_v1::Event::CanModify { can_modify } => { self.conditions[index].can_modify = Some(can_modify != 0); } apogee_condition_v1::Event::Done => { let condition = &mut self.conditions[index]; condition.snapshot = SnapshotState::Complete; if condition.name.is_none() || condition.fulfilled.is_none() || condition.can_modify.is_none() { eprintln!( "error: incomplete snapshot for condition {}", condition.requested_name ); self.failed = true; } } apogee_condition_v1::Event::Destroyed => { self.conditions[index].snapshot = SnapshotState::Destroyed; if self.list_target.is_none() { let label = "error:".if_supports_color(owo_colors::Stream::Stderr, |text| text.red()); eprintln!( "{label} condition {} was not found", self.conditions[index].requested_name ); self.failed = true; } } apogee_condition_v1::Event::Ok => { self.operation = OperationState::Complete; } apogee_condition_v1::Event::Error { error, message } => { let label = "error:".if_supports_color(owo_colors::Stream::Stderr, |text| text.red()); eprintln!( "{label} condition operation failed for {}: {message} ({error:?})", self.conditions[index].requested_name ); self.failed = true; } } } } impl hyprwire::Dispatch for App { fn event( &mut self, object: &apogee_environment_v1::ApogeeEnvironmentV1, event: ::Event<'_>, ) { let Some(environment) = self .environment .as_mut() .filter(|environment| &environment.object == object) else { return; }; match event { apogee_environment_v1::Event::Getenv { name, value } => { environment.variables.insert(name, value); } apogee_environment_v1::Event::CanModify { can_modify } => { environment.can_modify = Some(can_modify != 0); } apogee_environment_v1::Event::Done => { environment.snapshot = SnapshotState::Complete; if environment.can_modify.is_none() { eprintln!("error: incomplete global environment snapshot"); self.failed = true; } } } } } impl hyprwire::Dispatch for App { fn event( &mut self, object: &apogee_service_v1::ApogeeServiceV1, event: ::Event<'_>, ) { let Some(index) = self .services .iter() .position(|service| &service.object == object) else { return; }; match event { apogee_service_v1::Event::Name { name } => self.services[index].name = Some(name), apogee_service_v1::Event::Pid { pid } => self.services[index].pid = Some(pid), apogee_service_v1::Event::ProcessStopped => { self.services[index].pid = None; self.services[index].started_at = None; } apogee_service_v1::Event::ProcessStarted { started_at } => { self.services[index].started_at = Some(started_at); } apogee_service_v1::Event::Command { command } => { self.services[index].command = Some(command); } apogee_service_v1::Event::Args { args } => self.services[index].args = args, apogee_service_v1::Event::Conditions { conditions } => { self.services[index].conditions = conditions; } apogee_service_v1::Event::RestartCount { count } => { self.services[index].restart_count = Some(count); } apogee_service_v1::Event::User { user } => self.services[index].user = Some(user), apogee_service_v1::Event::Group { group } => self.services[index].group = Some(group), apogee_service_v1::Event::Description { description } => { self.services[index].description = Some(description); } apogee_service_v1::Event::State { state } => { self.services[index].state = Some(state); } apogee_service_v1::Event::Done => { let service = &mut self.services[index]; service.snapshot = SnapshotState::Complete; if service.name.is_none() || service.state.is_none() || service.description.is_none() { eprintln!( "error: incomplete snapshot for service {}", service.requested_name ); self.failed = true; } } apogee_service_v1::Event::Destroyed => { self.services[index].snapshot = SnapshotState::Destroyed; if self.list_target.is_none() { let label = "error:".if_supports_color(owo_colors::Stream::Stderr, |text| text.red()); eprintln!( "{label} service {} was not found", self.services[index].requested_name ); self.failed = true; } } apogee_service_v1::Event::Ok => { self.operation = OperationState::Complete; } apogee_service_v1::Event::Error { error, payload } => { let label = "error:".if_supports_color(owo_colors::Stream::Stderr, |text| text.red()); eprintln!( "{label} service operation failed for {}: {payload} ({error:?})", self.services[index].requested_name ); self.failed = true; } _ => {} } } } impl hyprwire::Dispatch for App { fn event( &mut self, object: &apogee_task_v1::ApogeeTaskV1, event: ::Event<'_>, ) { let Some(index) = self.tasks.iter().position(|task| &task.object == object) else { return; }; match event { apogee_task_v1::Event::Name { name } => self.tasks[index].name = Some(name), apogee_task_v1::Event::Pid { pid } => self.tasks[index].pid = Some(pid), apogee_task_v1::Event::ProcessStarted { started_at } => { self.tasks[index].started_at = Some(started_at) } apogee_task_v1::Event::ProcessStopped => { self.tasks[index].pid = None; self.tasks[index].started_at = None; } apogee_task_v1::Event::Description { description } => { self.tasks[index].description = Some(description) } apogee_task_v1::Event::Command { command } => self.tasks[index].command = Some(command), apogee_task_v1::Event::Args { args } => self.tasks[index].args = args, apogee_task_v1::Event::Conditions { conditions } => { self.tasks[index].conditions = conditions } apogee_task_v1::Event::State { state } => self.tasks[index].state = Some(state), apogee_task_v1::Event::Done => { let task = &mut self.tasks[index]; task.snapshot = SnapshotState::Complete; if task.name.is_none() || task.state.is_none() || task.description.is_none() { eprintln!( "error: incomplete snapshot for task {}", task.requested_name ); self.failed = true; } } apogee_task_v1::Event::Destroyed => { self.tasks[index].snapshot = SnapshotState::Destroyed; if self.list_target.is_none() { eprintln!( "error: task {} was not found", self.tasks[index].requested_name ); self.failed = true; } } _ => {} } } } impl hyprwire::Dispatch for App { fn event( &mut self, object: &apogee_service_logs_v1::ApogeeServiceLogsV1, event: ::Event<'_>, ) { let Some(logs) = self.logs.as_mut().filter(|logs| &logs.object == object) else { return; }; match event { apogee_service_logs_v1::Event::Entry { message, .. } => { logs.entries.push(LogEntry { message }) } apogee_service_logs_v1::Event::Done => logs.snapshot = SnapshotState::Complete, apogee_service_logs_v1::Event::Destroyed => logs.snapshot = SnapshotState::Destroyed, } } } impl hyprwire::Dispatch for App { fn event( &mut self, _object: &apogee_reload_v1::ApogeeReloadV1, event: ::Event<'_>, ) { match event { apogee_reload_v1::Event::Finished => { println!("reloaded services"); self.reloaded = true; } apogee_reload_v1::Event::Failed { error, message } => { let label = "error:".if_supports_color(owo_colors::Stream::Stderr, |text| text.red()); eprintln!("{label} failed to reload services: {message} ({error:?})"); self.failed = true; } } } } #[derive(Parser)] struct Args { /// Connect to the per-user apogee instance. #[arg(long, global = true)] user: bool, #[command(subcommand)] command: Command, } #[derive(Subcommand)] enum Command { /// List configured services and tasks. List, /// Show the status of a service or task. Status { /// Service or task name. name: String, /// Treat the name as a task. #[arg(long)] task: bool, }, /// Start a service or task. Start { /// Service or task name. name: String, }, /// Stop a service or task. Stop { /// Service or task name. name: String, }, /// Restart a service or task. Restart { /// Service or task name. name: String, }, /// Read the daemon's global environment. Getenv { /// Variable name. Omit to list every variable. name: Option, }, /// Set a variable in the daemon's global environment. Setenv { /// Environment variable name. name: String, /// Environment variable value. value: String, }, /// Inspect and control conditions. Cond { #[command(subcommand)] command: ConditionCommand, }, /// Reload service definitions. Reload, } #[derive(Subcommand)] enum ConditionCommand { /// List known conditions. List, /// Mark a condition fulfilled. Start { /// Condition name. name: String, }, /// Mark a condition unfulfilled. Stop { /// Condition name. name: String, }, } #[derive(Clone, Copy)] enum ServiceAction { Start, Stop, Restart, } #[derive(Clone, Copy)] enum TaskAction { Start, Stop, Restart, } impl TaskAction { fn send(self, object: &apogee_task_v1::ApogeeTaskV1) { match self { Self::Start => object.send_start(), Self::Stop => object.send_stop(), Self::Restart => object.send_restart(), } } } impl ServiceAction { fn send(self, object: &apogee_service_v1::ApogeeServiceV1) { match self { Self::Start => object.send_start(), Self::Stop => object.send_stop(), Self::Restart => object.send_restart(), } } } const fn state_name(state: apogee_core_v1::ServiceState) -> &'static str { match state { apogee_core_v1::ServiceState::Waiting => "waiting", apogee_core_v1::ServiceState::Running => "running", apogee_core_v1::ServiceState::Restarting => "restarting", apogee_core_v1::ServiceState::Stopped => "stopped", apogee_core_v1::ServiceState::Failed => "failed", } } fn colored_state(state: Option) -> String { let Some(state) = state else { return String::from("unknown"); }; let name = state_name(state); match state { apogee_core_v1::ServiceState::Running => name .if_supports_color(owo_colors::Stream::Stdout, |text| text.green()) .to_string(), apogee_core_v1::ServiceState::Waiting => name .if_supports_color(owo_colors::Stream::Stdout, |text| text.yellow()) .to_string(), apogee_core_v1::ServiceState::Restarting => name .if_supports_color(owo_colors::Stream::Stdout, |text| text.cyan()) .to_string(), apogee_core_v1::ServiceState::Stopped => name .if_supports_color(owo_colors::Stream::Stdout, |text| text.blue()) .to_string(), apogee_core_v1::ServiceState::Failed => name .if_supports_color(owo_colors::Stream::Stdout, |text| text.red()) .to_string(), } } const fn task_state_name(state: apogee_core_v1::TaskState) -> &'static str { match state { apogee_core_v1::TaskState::Waiting => "waiting", apogee_core_v1::TaskState::Running => "running", apogee_core_v1::TaskState::Successful => "successful", apogee_core_v1::TaskState::Failed => "failed", apogee_core_v1::TaskState::Stopped => "stopped", } } fn print_units(services: &[Service], tasks: &[Task]) { struct Row<'a> { pid: String, name: &'a str, state: &'static str, description: &'a str, } let mut rows = Vec::with_capacity(services.len() + tasks.len()); rows.extend( services .iter() .filter(|service| service.snapshot == SnapshotState::Complete) .map(|service| Row { pid: service .pid .map_or_else(|| String::from("-"), |pid| pid.to_string()), name: service.name.as_deref().unwrap_or("?"), state: service.state.map_or("unknown", state_name), description: service.description.as_deref().unwrap_or_default(), }), ); rows.extend( tasks .iter() .filter(|task| task.snapshot == SnapshotState::Complete) .map(|task| Row { pid: task .pid .map_or_else(|| String::from("-"), |pid| pid.to_string()), name: task.name.as_deref().unwrap_or("?"), state: task.state.map_or("unknown", task_state_name), description: task.description.as_deref().unwrap_or_default(), }), ); rows.sort_by_key(|row| row.name); let mut table = comfy_table::Table::new(); table.load_preset(presets::NOTHING); table .set_style(comfy_table::TableComponent::LeftHeaderIntersection, '─') .set_style(comfy_table::TableComponent::HeaderLines, '─') .set_style(comfy_table::TableComponent::MiddleHeaderIntersections, '─') .set_style(comfy_table::TableComponent::RightHeaderIntersection, '─') .set_style(comfy_table::TableComponent::VerticalLines, '│'); table.set_header(["PID", "NAME", "STATE", "DESCRIPTION"]); for row in rows { table.add_row([row.pid.as_str(), row.name, row.state, row.description]); } println!("{table}"); } fn print_task_status(task: &Task, conditions: &[Condition]) { let pid = task .pid .map_or_else(|| "none".to_owned(), |pid| pid.to_string()); let command = std::iter::once(task.command.as_deref().unwrap_or("?")) .chain(task.args.iter().map(String::as_str)) .collect::>() .join(" "); let mut rows = vec![ ( "Status", task.state.map_or("unknown", task_state_name).to_owned(), ), ("Name", task.name.as_deref().unwrap_or("?").to_owned()), ( "Description", task.description.as_deref().unwrap_or_default().to_owned(), ), ("Command", command), ("PID", pid), ]; if let Some(started_at) = task.started_at { rows.push(("Uptime", format_uptime(started_at))); } if !task.conditions.is_empty() { let condition_status = task .conditions .iter() .map(|name| { let marker = conditions .iter() .find(|condition| condition.requested_name == *name) .and_then(|condition| condition.fulfilled) .map_or("?", |fulfilled| if fulfilled { "✓" } else { "✗" }); format!("{marker} {name}") }) .collect::>() .join(", "); rows.insert(3, ("Condition(s)", condition_status)); } let label_width = rows.iter().map(|(label, _)| label.len()).max().unwrap_or(0); for (label, value) in rows { println!("{label:>label_width$} │ {value}"); } } fn print_service_status(service: &Service, conditions: &[Condition], logs: &[LogEntry]) { let pid = service .pid .map_or_else(|| "none".to_owned(), |pid| pid.to_string()); let command = std::iter::once(service.command.as_deref().unwrap_or("?")) .chain(service.args.iter().map(String::as_str)) .collect::>() .join(" "); let restarts = service .restart_count .map_or_else(|| "?".to_owned(), |count| count.to_string()); let mut rows = vec![ ("Status", colored_state(service.state)), ("Name", service.name.as_deref().unwrap_or("?").to_owned()), ( "Description", service .description .as_deref() .unwrap_or_default() .to_owned(), ), ("Command", command), ("PID", pid), ("User", service.user.as_deref().unwrap_or("?").to_owned()), ("Group", service.group.as_deref().unwrap_or("?").to_owned()), ("Restarts", restarts), ]; if let Some(started_at) = service.started_at { rows.insert(6, ("Uptime", format_uptime(started_at))); } if !service.conditions.is_empty() { let condition_status = service .conditions .iter() .map(|name| { let marker = conditions .iter() .find(|condition| condition.requested_name == *name) .and_then(|condition| condition.fulfilled) .map_or("?", |fulfilled| if fulfilled { "✓" } else { "✗" }); format!("{marker} {name}") }) .collect::>() .join(", "); rows.insert(3, ("Condition(s)", condition_status)); } let label_width = rows.iter().map(|(label, _)| label.len()).max().unwrap_or(0); for (label, value) in rows { println!("{label:>label_width$} │ {value}"); } if !logs.is_empty() { println!(); for log in logs { for line in log.message.lines().filter(|line| !line.is_empty()) { println!("{line}"); } } } } fn format_uptime(started_at: u32) -> String { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map_or(0, |duration| duration.as_secs()); let elapsed = now.saturating_sub(u64::from(started_at)); let hours = elapsed / 3600; let minutes = elapsed / 60 % 60; let seconds = elapsed % 60; format!("{hours} hour {minutes} min {seconds} sec") } fn print_conditions(conditions: &[Condition]) { let mut conditions = conditions .iter() .filter(|condition| condition.snapshot == SnapshotState::Complete) .collect::>(); conditions.sort_by_key(|condition| condition.name.as_deref()); let mut table = comfy_table::Table::new(); table.load_preset(presets::NOTHING); table .set_style(comfy_table::TableComponent::LeftHeaderIntersection, '─') .set_style(comfy_table::TableComponent::HeaderLines, '─') .set_style(comfy_table::TableComponent::MiddleHeaderIntersections, '─') .set_style(comfy_table::TableComponent::RightHeaderIntersection, '─') .set_style(comfy_table::TableComponent::VerticalLines, '│'); table.set_header(["NAME", "STATE", "MODIFIABLE"]); for condition in conditions { table.add_row([ condition.name.as_deref().unwrap_or("?"), condition.fulfilled.map_or("unknown", |fulfilled| { if fulfilled { "fulfilled" } else { "unfulfilled" } }), condition.can_modify.map_or( "unknown", |can_modify| if can_modify { "yes" } else { "no" }, ), ]); } println!("{table}"); } fn print_environment(environment: &Environment, name: Option<&str>, failed: &mut bool) { if let Some(name) = name { if let Some(value) = environment.variables.get(name) { println!("{value}"); } else { let label = "error:".if_supports_color(owo_colors::Stream::Stderr, |text| text.red()); eprintln!("{label} environment variable {name} was not found"); *failed = true; } return; } let mut table = comfy_table::Table::new(); table.load_preset(presets::NOTHING); table .set_style(comfy_table::TableComponent::LeftHeaderIntersection, '─') .set_style(comfy_table::TableComponent::HeaderLines, '─') .set_style(comfy_table::TableComponent::MiddleHeaderIntersections, '─') .set_style(comfy_table::TableComponent::RightHeaderIntersection, '─') .set_style(comfy_table::TableComponent::VerticalLines, '│'); table.set_header(["NAME", "VALUE"]); for (name, value) in &environment.variables { table.add_row(<[&String; 2]>::from((name, value))); } println!("{table}"); } fn get_environment( manager: &apogee_manager_v1::ApogeeManagerV1, event_queue: &hyprwire::EventQueue, app: &mut App, ) -> hyprwire::Result<()> { let object = manager .send_get_environment::() .ok_or(hyprwire::Error::ConnectionClosed)?; app.environment = Some(Environment { object, variables: BTreeMap::new(), can_modify: None, snapshot: SnapshotState::Pending, }); event_queue.roundtrip(app)?; while !app.environment_snapshot_complete() && !app.failed { event_queue.dispatch_events(app, true)?; } Ok(()) } fn control_condition( manager: &apogee_manager_v1::ApogeeManagerV1, event_queue: &hyprwire::EventQueue, app: &mut App, name: String, fulfilled: bool, ) -> hyprwire::Result<()> { let object = manager .send_get_condition::(&name) .ok_or(hyprwire::Error::ConnectionClosed)?; app.conditions.push(Condition { object: object.clone(), requested_name: name.clone(), name: None, fulfilled: None, can_modify: None, snapshot: SnapshotState::Pending, }); event_queue.roundtrip(app)?; while !app.condition_snapshot_complete() && !app.failed { event_queue.dispatch_events(app, true)?; } if app.failed { return Ok(()); } if fulfilled { object.send_start(); } else { object.send_stop(); } event_queue.roundtrip(app)?; while app.operation != OperationState::Complete && !app.failed { event_queue.dispatch_events(app, true)?; } Ok(()) } fn control_unit( manager: &apogee_manager_v1::ApogeeManagerV1, event_queue: &hyprwire::EventQueue, app: &mut App, name: String, service_action: ServiceAction, task_action: TaskAction, ) -> hyprwire::Result<()> { let service = manager .send_get_service::(&name) .ok_or(hyprwire::Error::ConnectionClosed)?; let task = manager .send_get_task::(&name) .ok_or(hyprwire::Error::ConnectionClosed)?; app.services.push(Service { object: service, requested_name: name.clone(), name: None, pid: None, started_at: None, state: None, description: None, command: None, args: Vec::new(), conditions: Vec::new(), restart_count: None, user: None, group: None, snapshot: SnapshotState::Pending, }); app.tasks.push(Task { object: task, requested_name: name.clone(), name: None, pid: None, started_at: None, state: None, description: None, command: None, args: Vec::new(), conditions: Vec::new(), snapshot: SnapshotState::Pending, }); let original_target = app.list_target; app.list_target = Some(ListTarget::Conditions); event_queue.roundtrip(app)?; while !app.service_snapshot_complete() || !app.task_snapshot_complete() { event_queue.dispatch_events(app, true)?; } app.list_target = original_target; if app.failed { return Ok(()); } let service_exists = app .services .last() .is_some_and(|unit| unit.snapshot == SnapshotState::Complete); let task_exists = app .tasks .last() .is_some_and(|unit| unit.snapshot == SnapshotState::Complete); match (service_exists, task_exists) { (true, false) => service_action.send(&app.services.last().unwrap().object), (false, true) => task_action.send(&app.tasks.last().unwrap().object), (true, true) => { eprintln!("error: both a service and task are named {name}"); app.failed = true; return Ok(()); } (false, false) => { eprintln!("error: service or task {name} was not found"); app.failed = true; return Ok(()); } } event_queue.roundtrip(app)?; while app.operation != OperationState::Complete && !app.failed { event_queue.dispatch_events(app, true)?; } Ok(()) } fn main() -> hyprwire::Result<()> { let args = Args::parse(); let list_target = match &args.command { Command::List => Some(ListTarget::All), Command::Cond { command: ConditionCommand::List, } => Some(ListTarget::Conditions), Command::Cond { command: ConditionCommand::Start { .. } | ConditionCommand::Stop { .. }, } | Command::Status { .. } | Command::Start { .. } | Command::Stop { .. } | Command::Restart { .. } | Command::Getenv { .. } | Command::Setenv { .. } | Command::Reload => None, }; let runtime_dir = if args.user { dirs::runtime_dir().unwrap_or_else(|| path::PathBuf::from("run")) } else { path::PathBuf::from("/run") }; let socket_path = runtime_dir.join("apogee/apogee.sock"); let mut socket = client::Client::connect(&socket_path)?; let event_queue = socket.new_event_queue(); let mut app = App { list_target, ..App::default() }; socket.add_implementation::(); event_queue.wait_for_handshake(&mut app)?; let manager = socket.bind::(&event_queue, &mut app, 1)?; match args.command { Command::List => { event_queue.roundtrip(&mut app)?; while !app.list_complete() && !app.failed { event_queue.dispatch_events(&mut app, true)?; } if !app.failed { print_units(&app.services, &app.tasks); } } Command::Status { name, task: true } => { let object = manager .send_get_task::(&name) .ok_or(hyprwire::Error::ConnectionClosed)?; app.tasks.push(Task { object, requested_name: name, name: None, pid: None, started_at: None, state: None, description: None, command: None, args: Vec::new(), conditions: Vec::new(), snapshot: SnapshotState::Pending, }); event_queue.roundtrip(&mut app)?; while !app .tasks .last() .is_some_and(|task| task.snapshot != SnapshotState::Pending) && !app.failed { event_queue.dispatch_events(&mut app, true)?; } if !app.failed { let condition_names = app .tasks .last() .map(|task| task.conditions.clone()) .unwrap_or_default(); for name in condition_names { let object = manager .send_get_condition::(&name) .ok_or(hyprwire::Error::ConnectionClosed)?; app.conditions.push(Condition { object, requested_name: name, name: None, fulfilled: None, can_modify: None, snapshot: SnapshotState::Pending, }); } if !app.conditions.is_empty() { event_queue.roundtrip(&mut app)?; while !app.condition_snapshots_complete() && !app.failed { event_queue.dispatch_events(&mut app, true)?; } } if !app.failed { if let Some(task) = app.tasks.last() { print_task_status(task, &app.conditions); } } } } Command::Status { name, task: false } => { let object = manager .send_get_service::(&name) .ok_or(hyprwire::Error::ConnectionClosed)?; app.services.push(Service { object, requested_name: name, name: None, pid: None, started_at: None, state: None, description: None, command: None, args: Vec::new(), conditions: Vec::new(), restart_count: None, user: None, group: None, snapshot: SnapshotState::Pending, }); event_queue.roundtrip(&mut app)?; while !app.service_snapshot_complete() && !app.failed { event_queue.dispatch_events(&mut app, true)?; } if !app.failed { let condition_names = app .services .last() .map(|service| service.conditions.clone()) .unwrap_or_default(); for name in condition_names { let object = manager .send_get_condition::(&name) .ok_or(hyprwire::Error::ConnectionClosed)?; app.conditions.push(Condition { object, requested_name: name, name: None, fulfilled: None, can_modify: None, snapshot: SnapshotState::Pending, }); } if !app.conditions.is_empty() { event_queue.roundtrip(&mut app)?; while !app.condition_snapshots_complete() && !app.failed { event_queue.dispatch_events(&mut app, true)?; } } if !app.failed { if let Some(service) = app.services.last() { let logs = service .object .send_get_logs::() .ok_or(hyprwire::Error::ConnectionClosed)?; app.logs = Some(ServiceLogs { object: logs, entries: Vec::new(), snapshot: SnapshotState::Pending, }); } event_queue.roundtrip(&mut app)?; while !app.logs_snapshot_complete() && !app.failed { event_queue.dispatch_events(&mut app, true)?; } if !app.failed { if let (Some(service), Some(logs)) = (app.services.last(), app.logs.as_ref()) { print_service_status(service, &app.conditions, &logs.entries); } app.logs = None; } } } } Command::Cond { command: ConditionCommand::List, } => { event_queue.roundtrip(&mut app)?; while !app.list_complete() && !app.failed { event_queue.dispatch_events(&mut app, true)?; } if !app.failed { print_conditions(&app.conditions); } } Command::Cond { command: ConditionCommand::Start { name }, } => control_condition(&manager, &event_queue, &mut app, name, true)?, Command::Cond { command: ConditionCommand::Stop { name }, } => control_condition(&manager, &event_queue, &mut app, name, false)?, Command::Start { name } => control_unit( &manager, &event_queue, &mut app, name, ServiceAction::Start, TaskAction::Start, )?, Command::Stop { name } => control_unit( &manager, &event_queue, &mut app, name, ServiceAction::Stop, TaskAction::Stop, )?, Command::Restart { name } => control_unit( &manager, &event_queue, &mut app, name, ServiceAction::Restart, TaskAction::Restart, )?, Command::Getenv { name } => { get_environment(&manager, &event_queue, &mut app)?; if !app.failed { if let Some(environment) = app.environment.as_ref() { print_environment(environment, name.as_deref(), &mut app.failed); } else { eprintln!("error: global environment snapshot is missing"); app.failed = true; } } } Command::Setenv { name, value } => { get_environment(&manager, &event_queue, &mut app)?; if !app.failed { if let Some(environment) = app.environment.as_ref() { if environment.can_modify == Some(true) { environment.object.send_setenv(&name, &value); event_queue.roundtrip(&mut app)?; } else { let label = "error:" .if_supports_color(owo_colors::Stream::Stderr, |text| text.red()); eprintln!("{label} the global environment cannot be modified"); app.failed = true; } } else { eprintln!("error: global environment snapshot is missing"); app.failed = true; } } } Command::Reload => { let _reload = manager .send_reload::() .ok_or(hyprwire::Error::ConnectionClosed)?; event_queue.roundtrip(&mut app)?; while !app.reloaded && !app.failed { event_queue.dispatch_events(&mut app, true)?; } } } process::exit(i32::from(app.failed)); }