//! # terminal-garden //! //! The first face of the bus: a renderer, not a collector. //! Git is the cleanest signal in the suite — discrete, unambiguous, no privacy //! questions — so the garden proves the bus before anything reads the shell. //! //! Like every renderer, the garden is thin: connect to the socket → read the //! git aggregates → fold them into per-repo plots → render. It never touches a //! sensor. The mapping (commits → growth, staleness → wilt) lives in the //! `terminal-garden` library so it can be unit-tested; this binary is just the //! subscriber loop. //! //! Usage: //! ```text //! terminal-garden [--socket ] //! ``` //! Socket defaults to `$XDG_RUNTIME_DIR/signald.sock`, falling back to //! `~/.local/state/signald/sock`. use std::collections::BTreeMap; use std::io::BufReader; use std::os::unix::net::UnixStream; use std::path::PathBuf; use signal_schema::{wire, Signal, SignalName}; use terminal_garden::{plots_from_signals, render}; /// The `name`s the garden cares about: the git aggregates. const SUBSCRIBE: &[SignalName] = &[ SignalName::CommitsWindow, SignalName::CommitsToday, SignalName::BranchCount, SignalName::DaysSinceLastCommit, ]; fn main() { let socket = parse_socket(); if let Err(e) = run(&socket) { eprintln!("terminal-garden: could not read signald at {}: {e}", socket.display()); eprintln!("terminal-garden: is signald running? (signald --socket {})", socket.display()); std::process::exit(1); } } /// Connect and render live: the daemon replays the last-value cache on connect, /// then streams updates. We keep the latest value of each metric (keyed by name /// and repo tag) and re-render the garden on every frame. `Ok(())` is a clean EOF /// (the daemon closed the stream). Frames this build cannot decode are skipped, /// so an older renderer keeps working against a newer daemon. fn run(socket: &PathBuf) -> std::io::Result<()> { let stream = UnixStream::connect(socket)?; let mut reader = BufReader::new(stream); // Keyed by (metric name, repo tag) so per-repo signals coexist and updates // replace prior values rather than accumulating. let mut latest: BTreeMap<(u8, Option), Signal> = BTreeMap::new(); loop { let sig = match wire::read_frame(&mut reader)? { wire::Frame::Signal(sig) => sig, // A record this build does not understand: a newer daemon, or a // metric appended after this renderer was built. Keep rendering. wire::Frame::Skipped => continue, wire::Frame::Eof => break, }; if !SUBSCRIBE.contains(&sig.name) { continue; } let key = (sig.name.to_u8(), sig.tag.as_ref().map(|t| t.as_str().to_string())); latest.insert(key, sig); let signals: Vec = latest.values().cloned().collect(); let plots = plots_from_signals(&signals); // Clear + home so the garden redraws in place as signals change. print!("\x1b[2J\x1b[H{}", render(&plots)); } Ok(()) } fn parse_socket() -> PathBuf { let mut args = std::env::args().skip(1); while let Some(arg) = args.next() { if arg == "--socket" { if let Some(path) = args.next() { return PathBuf::from(path); } } } default_socket_path() } /// `$XDG_RUNTIME_DIR/signald.sock`, else `$HOME/.local/state/signald/sock`. /// /// Split from the environment so it can be tested without mutating it. The /// same resolution is duplicated in the other binary and in /// `shell-hooks/signald-hooks.zsh`; the README's "Paths" table is the one /// place they are written down, and the tests below pin them to it. fn socket_path_from(xdg_runtime_dir: Option<&str>, home: Option<&str>) -> PathBuf { if let Some(dir) = xdg_runtime_dir { return PathBuf::from(dir).join("signald.sock"); } PathBuf::from(home.unwrap_or(".")).join(".local/state/signald/sock") } fn default_socket_path() -> PathBuf { socket_path_from( std::env::var("XDG_RUNTIME_DIR").ok().as_deref(), std::env::var("HOME").ok().as_deref(), ) } #[cfg(test)] mod path_tests { use super::*; /// Pinned to the README "Paths" table. If this changes, the table and /// `shell-hooks/signald-hooks.zsh` change with it. #[test] fn socket_default_follows_xdg_then_home() { assert_eq!( socket_path_from(Some("/run/user/501"), Some("/Users/x")), PathBuf::from("/run/user/501/signald.sock") ); assert_eq!( socket_path_from(None, Some("/Users/x")), PathBuf::from("/Users/x/.local/state/signald/sock") ); } }