Ambient system companions over one privacy-preserving signal daemon (aggregate-only, no keystroke content): a git-driven terminal garden and IOKit hardware collectors. ambient daemon macos privacy terminal

crates/pet-life/src/main.rs

86 lines · 2901 bytes

 1//! # pet-life
 2//!
 3//! Prints the pet's current state as JSON and exits. The menu-bar app runs it
 4//! on a timer; everything interesting — ageing, death, the cemetery — happens
 5//! here, where it can be tested.
 6//!
 7//! Usage:
 8//! ```text
 9//!   pet-life [--db <path>] [--state <path>]
10//!   pet-life --help | --version
11//! ```
12
13use std::path::PathBuf;
14
15use signald::history::History;
16
17const USAGE: &str = "\
18Usage: pet-life [--db <path>] [--state <path>]
19
20  --db <path>       signald's history database (default: beside its socket)
21  --state <path>    the pet's own state (default $XDG_DATA_HOME, else
22                    ~/.local/share/ambient-companions/pet.state)
23  -h, --help        print this and exit
24  -V, --version     print the version and exit
25
26Prints the pet's state as JSON. Exits 0 even with no daemon: a pet whose
27history cannot be read is not a dead pet, only an unobserved one.
28";
29
30fn main() {
31    let mut db: Option<PathBuf> = None;
32    let mut state_path: Option<PathBuf> = None;
33    let mut args = std::env::args().skip(1);
34    while let Some(arg) = args.next() {
35        match arg.as_str() {
36            "--db" => db = args.next().map(PathBuf::from),
37            "--state" => state_path = args.next().map(PathBuf::from),
38            "--help" | "-h" => {
39                print!("{USAGE}");
40                return;
41            }
42            "--version" | "-V" => {
43                println!("pet-life {}", env!("CARGO_PKG_VERSION"));
44                return;
45            }
46            _ => {
47                eprintln!("pet-life: unknown option {arg}\n");
48                eprint!("{USAGE}");
49                std::process::exit(2);
50            }
51        }
52    }
53
54    let state_path = state_path.unwrap_or_else(pet_life::default_state_path);
55    let db = db.unwrap_or_else(default_db_path);
56
57    // No daemon, or no history yet, is not fatal. The persisted activity still
58    // stands, so the pet ages on the record it already has rather than dying
59    // because nothing could be read this once.
60    let observed = History::open_read_only(&db)
61        .ok()
62        .and_then(|h| h.last_activity_ms().ok().flatten())
63        .unwrap_or(0);
64
65    let (state, snapshot) = pet_life::advance(pet_life::load(&state_path), observed, now_millis());
66    if let Err(e) = pet_life::save(&state_path, &state) {
67        eprintln!("pet-life: cannot write {}: {e}", state_path.display());
68    }
69    println!("{}", pet_life::to_json(&snapshot));
70}
71
72/// The history database sits beside the daemon's socket.
73fn default_db_path() -> PathBuf {
74    let socket = signal_client::default_socket_path();
75    socket
76        .parent()
77        .map(|d| d.join("signald.sqlite"))
78        .unwrap_or_else(|| PathBuf::from("signald.sqlite"))
79}
80
81fn now_millis() -> u64 {
82    std::time::SystemTime::now()
83        .duration_since(std::time::UNIX_EPOCH)
84        .map(|d| d.as_millis() as u64)
85        .unwrap_or(0)
86}