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

103 lines · 3620 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 --oneline [--socket <path>]
 11//!   terminal-pet --help | --version
 12//! ```
 13//! Socket defaults to `$XDG_RUNTIME_DIR/signald.sock`, falling back to
 14//! `~/.local/state/signald/sock`.
 15
 16use std::collections::BTreeMap;
 17use std::path::{Path, PathBuf};
 18
 19use signal_schema::Signal;
 20use terminal_pet::{oneline, pet_from_signals, render};
 21
 22const USAGE: &str = "\
 23Usage: terminal-pet [--socket <path>]
 24
 25  --socket <path>   signald's socket (default $XDG_RUNTIME_DIR/signald.sock,
 26                    else ~/.local/state/signald/sock)
 27  --oneline         print one line and exit, for a shell prompt. Prints
 28                    nothing and exits 1 if signald is not running.
 29  -h, --help        print this and exit
 30  -V, --version     print the version and exit
 31";
 32
 33fn main() {
 34    let socket = parse_socket();
 35    if std::env::args().any(|a| a == "--oneline") {
 36        // A prompt segment must never print a diagnostic or hang a shell: no
 37        // daemon means no output and a non-zero exit, so the segment hides.
 38        match one_shot(&socket) {
 39            Ok(line) => println!("{line}"),
 40            Err(_) => std::process::exit(1),
 41        }
 42        return;
 43    }
 44    if let Err(e) = run(&socket) {
 45        eprintln!("terminal-pet: could not read signald at {}: {e}", socket.display());
 46        eprintln!("terminal-pet: is signald running? (signald --socket {})", socket.display());
 47        std::process::exit(1);
 48    }
 49}
 50
 51/// Connect and render live. Frames this build cannot decode are skipped by the
 52/// iterator, so a metric appended after this renderer was built does not end
 53/// the stream.
 54fn run(socket: &Path) -> std::io::Result<()> {
 55    let frames = signal_client::Frames::connect(socket)?;
 56    // Keyed by (name, source) so a metric and the health of the collector that
 57    // produced it stay distinct, matching the daemon's own cache.
 58    let mut latest: BTreeMap<(u8, u8), Signal> = BTreeMap::new();
 59    for sig in frames {
 60        let sig = sig?;
 61        latest.insert((sig.name.to_u8(), sig.source.to_u8()), sig);
 62        let snapshot: Vec<Signal> = latest.values().cloned().collect();
 63        let pet = pet_from_signals(&snapshot, now_millis());
 64        print!("\x1b[2J\x1b[H{}", render(&pet));
 65    }
 66    Ok(())
 67}
 68
 69/// Read the daemon's cached snapshot, render one line, and return.
 70fn one_shot(socket: &Path) -> std::io::Result<String> {
 71    let snapshot = signal_client::Frames::connect(socket)?.snapshot()?;
 72    Ok(oneline(&pet_from_signals(&snapshot, now_millis())))
 73}
 74
 75fn now_millis() -> u64 {
 76    std::time::SystemTime::now()
 77        .duration_since(std::time::UNIX_EPOCH)
 78        .map(|d| d.as_millis() as u64)
 79        .unwrap_or(0)
 80}
 81
 82fn parse_socket() -> PathBuf {
 83    let mut args = std::env::args().skip(1);
 84    while let Some(arg) = args.next() {
 85        match arg.as_str() {
 86            "--help" | "-h" => {
 87                print!("{USAGE}");
 88                std::process::exit(0);
 89            }
 90            "--version" | "-V" => {
 91                println!("terminal-pet {}", env!("CARGO_PKG_VERSION"));
 92                std::process::exit(0);
 93            }
 94            "--socket" => {
 95                if let Some(path) = args.next() {
 96                    return PathBuf::from(path);
 97                }
 98            }
 99            _ => {}
100        }
101    }
102    signal_client::default_socket_path()
103}