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}