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/terminal-pet/src/main.rs

85 lines · 2804 bytes

 1//! # terminal-pet
 2//!
 3//! A pet whose mood follows the shell and the machine. Like every renderer it
 4//! is thin: connect to the socket, read frames, redraw. It never touches a
 5//! sensor and never reads the shell.
 6//!
 7//! Usage:
 8//! ```text
 9//!   terminal-pet [--socket <path>]
10//!   terminal-pet --help | --version
11//! ```
12//! Socket defaults to `$XDG_RUNTIME_DIR/signald.sock`, falling back to
13//! `~/.local/state/signald/sock`.
14
15use std::collections::BTreeMap;
16use std::path::{Path, PathBuf};
17
18use signal_schema::Signal;
19use terminal_pet::{pet_from_signals, render};
20
21const USAGE: &str = "\
22Usage: terminal-pet [--socket <path>]
23
24  --socket <path>   signald's socket (default $XDG_RUNTIME_DIR/signald.sock,
25                    else ~/.local/state/signald/sock)
26  -h, --help        print this and exit
27  -V, --version     print the version and exit
28";
29
30fn main() {
31    let socket = parse_socket();
32    if let Err(e) = run(&socket) {
33        eprintln!("terminal-pet: could not read signald at {}: {e}", socket.display());
34        eprintln!("terminal-pet: is signald running? (signald --socket {})", socket.display());
35        std::process::exit(1);
36    }
37}
38
39/// Connect and render live. Frames this build cannot decode are skipped by the
40/// iterator, so a metric appended after this renderer was built does not end
41/// the stream.
42fn run(socket: &Path) -> std::io::Result<()> {
43    let frames = signal_client::Frames::connect(socket)?;
44    // Keyed by (name, source) so a metric and the health of the collector that
45    // produced it stay distinct, matching the daemon's own cache.
46    let mut latest: BTreeMap<(u8, u8), Signal> = BTreeMap::new();
47    for sig in frames {
48        let sig = sig?;
49        latest.insert((sig.name.to_u8(), sig.source.to_u8()), sig);
50        let snapshot: Vec<Signal> = latest.values().cloned().collect();
51        let pet = pet_from_signals(&snapshot, now_millis());
52        print!("\x1b[2J\x1b[H{}", render(&pet));
53    }
54    Ok(())
55}
56
57fn now_millis() -> u64 {
58    std::time::SystemTime::now()
59        .duration_since(std::time::UNIX_EPOCH)
60        .map(|d| d.as_millis() as u64)
61        .unwrap_or(0)
62}
63
64fn parse_socket() -> PathBuf {
65    let mut args = std::env::args().skip(1);
66    while let Some(arg) = args.next() {
67        match arg.as_str() {
68            "--help" | "-h" => {
69                print!("{USAGE}");
70                std::process::exit(0);
71            }
72            "--version" | "-V" => {
73                println!("terminal-pet {}", env!("CARGO_PKG_VERSION"));
74                std::process::exit(0);
75            }
76            "--socket" => {
77                if let Some(path) = args.next() {
78                    return PathBuf::from(path);
79                }
80            }
81            _ => {}
82        }
83    }
84    signal_client::default_socket_path()
85}