single-user service manager
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394mod 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<String>, pid: Option<u32>, started_at: Option<u32>, state: Option<apogee_core_v1::ServiceState>, description: Option<String>, command: Option<String>, args: Vec<String>, conditions: Vec<String>, restart_count: Option<u32>, user: Option<String>, group: Option<String>, snapshot: SnapshotState,}
struct Task { object: apogee_task_v1::ApogeeTaskV1, requested_name: String, name: Option<String>, pid: Option<u32>, started_at: Option<u32>, state: Option<apogee_core_v1::TaskState>, description: Option<String>, command: Option<String>, args: Vec<String>, conditions: Vec<String>, snapshot: SnapshotState,}
struct LogEntry { message: String,}
struct ServiceLogs { object: apogee_service_logs_v1::ApogeeServiceLogsV1, entries: Vec<LogEntry>, snapshot: SnapshotState,}
struct Condition { object: apogee_condition_v1::ApogeeConditionV1, requested_name: String, name: Option<String>, fulfilled: Option<bool>, can_modify: Option<bool>, snapshot: SnapshotState,}
struct Environment { object: apogee_environment_v1::ApogeeEnvironmentV1, variables: BTreeMap<String, String>, can_modify: Option<bool>, snapshot: SnapshotState,}
#[derive(Default)]struct App { reloaded: bool, failed: bool, manager_done: bool, operation: OperationState, list_target: Option<ListTarget>, services: Vec<Service>, tasks: Vec<Task>, conditions: Vec<Condition>, environment: Option<Environment>, logs: Option<ServiceLogs>,}
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<apogee_manager_v1::ApogeeManagerV1> for App { fn event( &mut self, object: &apogee_manager_v1::ApogeeManagerV1, event: <apogee_manager_v1::ApogeeManagerV1 as hyprwire::Object>::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::<Self>(&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::<Self>(&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::<Self>(&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<apogee_condition_v1::ApogeeConditionV1> for App { fn event( &mut self, object: &apogee_condition_v1::ApogeeConditionV1, event: <apogee_condition_v1::ApogeeConditionV1 as hyprwire::Object>::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<apogee_environment_v1::ApogeeEnvironmentV1> for App { fn event( &mut self, object: &apogee_environment_v1::ApogeeEnvironmentV1, event: <apogee_environment_v1::ApogeeEnvironmentV1 as hyprwire::Object>::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<apogee_service_v1::ApogeeServiceV1> for App { fn event( &mut self, object: &apogee_service_v1::ApogeeServiceV1, event: <apogee_service_v1::ApogeeServiceV1 as hyprwire::Object>::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<apogee_task_v1::ApogeeTaskV1> for App { fn event( &mut self, object: &apogee_task_v1::ApogeeTaskV1, event: <apogee_task_v1::ApogeeTaskV1 as hyprwire::Object>::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<apogee_service_logs_v1::ApogeeServiceLogsV1> for App { fn event( &mut self, object: &apogee_service_logs_v1::ApogeeServiceLogsV1, event: <apogee_service_logs_v1::ApogeeServiceLogsV1 as hyprwire::Object>::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<apogee_reload_v1::ApogeeReloadV1> for App { fn event( &mut self, _object: &apogee_reload_v1::ApogeeReloadV1, event: <apogee_reload_v1::ApogeeReloadV1 as hyprwire::Object>::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<String>, }, /// 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<apogee_core_v1::ServiceState>) -> 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::<Vec<_>>() .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::<Vec<_>>() .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::<Vec<_>>() .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::<Vec<_>>() .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::<Vec<_>>(); 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::<App>() .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::<App>(&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::<App>(&name) .ok_or(hyprwire::Error::ConnectionClosed)?; let task = manager .send_get_task::<App>(&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::<apogee_core_v1::ApogeeCoreV1>(); event_queue.wait_for_handshake(&mut app)?;
let manager = socket.bind::<apogee_manager_v1::ApogeeManagerV1, _>(&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::<App>(&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::<App>(&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::<App>(&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::<App>(&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::<App>() .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::<App>() .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));}