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}