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

108 lines · 3966 bytes

  1//! # terminal-garden
  2//!
  3//! The first face of the bus: a renderer, not a collector.
  4//! Git is the cleanest signal in the suite — discrete, unambiguous, no privacy
  5//! questions — so the garden proves the bus before anything reads the shell.
  6//!
  7//! Like every renderer, the garden is thin: connect to the socket → read the
  8//! git aggregates → fold them into per-repo plots → render. It never touches a
  9//! sensor. The mapping (commits → growth, staleness → wilt) lives in the
 10//! `terminal-garden` library so it can be unit-tested; this binary is just the
 11//! subscriber loop.
 12//!
 13//! Usage:
 14//! ```text
 15//!   terminal-garden [--socket <path>]
 16//!   terminal-garden --help | --version
 17//! ```
 18//! Socket defaults to `$XDG_RUNTIME_DIR/signald.sock`, falling back to
 19//! `~/.local/state/signald/sock`.
 20
 21use std::collections::BTreeMap;
 22use std::path::{Path, PathBuf};
 23
 24use signal_schema::{Signal, SignalName};
 25use terminal_garden::{plots_from_signals, render};
 26
 27/// The `name`s the garden cares about: the git aggregates.
 28const SUBSCRIBE: &[SignalName] = &[
 29    SignalName::CommitsWindow,
 30    SignalName::CommitsToday,
 31    SignalName::BranchCount,
 32    SignalName::DaysSinceLastCommit,
 33];
 34
 35fn main() {
 36    let socket = parse_socket();
 37    if let Err(e) = run(&socket) {
 38        eprintln!("terminal-garden: could not read signald at {}: {e}", socket.display());
 39        eprintln!("terminal-garden: is signald running? (signald --socket {})", socket.display());
 40        std::process::exit(1);
 41    }
 42}
 43
 44/// Connect and render live: the daemon replays the last-value cache on connect,
 45/// then streams updates. We keep the latest value of each metric (keyed by name
 46/// and repo tag) and re-render the garden on every frame. `Ok(())` is a clean EOF
 47/// (the daemon closed the stream). Frames this build cannot decode are skipped,
 48/// so an older renderer keeps working against a newer daemon.
 49fn run(socket: &Path) -> std::io::Result<()> {
 50    let frames = signal_client::Frames::connect(socket)?;
 51
 52    // Keyed by (metric name, source, repo tag) so per-repo signals coexist,
 53    // two collectors reporting the same name stay distinct, and updates replace
 54    // prior values rather than accumulating.
 55    let mut latest: BTreeMap<(u8, u8, Option<String>), Signal> = BTreeMap::new();
 56    // Frames this build cannot decode are skipped by the iterator, so a metric
 57    // appended after this renderer was built does not end the stream.
 58    for sig in frames {
 59        let sig = sig?;
 60        if !SUBSCRIBE.contains(&sig.name) {
 61            continue;
 62        }
 63        let key = (
 64            sig.name.to_u8(),
 65            sig.source.to_u8(),
 66            sig.tag.as_ref().map(|t| t.as_str().to_string()),
 67        );
 68        latest.insert(key, sig);
 69
 70        let signals: Vec<Signal> = latest.values().cloned().collect();
 71        let plots = plots_from_signals(&signals);
 72        // Clear + home so the garden redraws in place as signals change.
 73        print!("\x1b[2J\x1b[H{}", render(&plots));
 74    }
 75    Ok(())
 76}
 77
 78const USAGE: &str = "\
 79Usage: terminal-garden [--socket <path>]
 80
 81  --socket <path>   signald's socket (default $XDG_RUNTIME_DIR/signald.sock,
 82                    else ~/.local/state/signald/sock)
 83  -h, --help        print this and exit
 84  -V, --version     print the version and exit
 85";
 86
 87fn parse_socket() -> PathBuf {
 88    let mut args = std::env::args().skip(1);
 89    while let Some(arg) = args.next() {
 90        match arg.as_str() {
 91            "--help" | "-h" => {
 92                print!("{USAGE}");
 93                std::process::exit(0);
 94            }
 95            "--version" | "-V" => {
 96                println!("terminal-garden {}", env!("CARGO_PKG_VERSION"));
 97                std::process::exit(0);
 98            }
 99            _ => {}
100        }
101        if arg == "--socket" {
102            if let Some(path) = args.next() {
103                return PathBuf::from(path);
104            }
105        }
106    }
107    signal_client::default_socket_path()
108}