1use std::path::Path;
6
7pub struct Status<'a> {
9 pub root: &'a Path,
10 pub snapshot: &'a Path,
11 pub running: bool,
12 pub ram_only: bool,
14 pub state: Option<String>,
16 pub files: Option<usize>,
17 pub trigrams: Option<usize>,
18 pub memory_bytes: Option<u64>,
19}
20
21impl Status<'_> {
22 pub fn render(&self) -> String {
23 let row = |label: &str, value: &str| format!(" {label:<9} {value}\n");
24 let mut s = String::from("rgx index status\n\n");
25 s.push_str(&row("root", &self.root.display().to_string()));
26
27 if self.running {
30 if let Some(state) = &self.state {
31 s.push_str(&row("state", state));
32 }
33 } else {
34 s.push_str(&row("daemon", "not running (run a search to start it)"));
35 }
36 if let Some(f) = self.files {
37 s.push_str(&row("files", &human_count(f as u64)));
38 }
39 if let Some(t) = self.trigrams {
40 s.push_str(&row("trigrams", &human_count(t as u64)));
41 }
42 if let Some(m) = self.memory_bytes {
43 s.push_str(&row("index", &human_bytes(m)));
44 }
45
46 if self.ram_only {
48 s.push_str(&row("snapshot", "ram-only (rebuilt on start)"));
49 return s;
50 }
51
52 match std::fs::metadata(self.snapshot) {
54 Ok(m) => {
55 let age = m
56 .modified()
57 .ok()
58 .and_then(|t| t.elapsed().ok())
59 .map(|d| format!("last synced {} ago", human_duration(d.as_secs())))
60 .unwrap_or_else(|| "on disk".into());
61 s.push_str(&row(
62 "snapshot",
63 &format!("{} ({age})", human_bytes(m.len())),
64 ));
65 }
66 Err(_) => s.push_str(&row("snapshot", "not built yet")),
67 }
68 s.push_str(&format!(" {}\n", self.snapshot.display()));
69 s
70 }
71}
72
73pub fn human_count(n: u64) -> String {
77 if n < 1_000 {
78 n.to_string()
79 } else if n as f64 / 1_000.0 < 999.95 {
80 format!("{:.1}k", n as f64 / 1_000.0)
81 } else {
82 format!("{:.1}m", n as f64 / 1_000_000.0)
83 }
84}
85
86pub fn human_bytes(n: u64) -> String {
87 const U: [&str; 4] = ["B", "KB", "MB", "GB"];
88 let mut v = n as f64;
89 let mut i = 0;
90 while v >= 1023.95 && i < U.len() - 1 {
93 v /= 1024.0;
94 i += 1;
95 }
96 if i == 0 {
97 format!("{n} B")
98 } else {
99 format!("{v:.1} {}", U[i])
100 }
101}
102
103pub fn human_duration(secs: u64) -> String {
104 match secs {
105 0..=59 => format!("{secs}s"),
106 60..=3599 => format!("{}m{}s", secs / 60, secs % 60),
107 3600..=86399 => format!("{}h{}m", secs / 3600, (secs % 3600) / 60),
108 _ => format!("{}d{}h", secs / 86400, (secs % 86400) / 3600),
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn counts_use_k_and_m_suffixes() {
118 assert_eq!(human_count(758), "758");
119 assert_eq!(human_count(93_596), "93.6k");
120 assert_eq!(human_count(549_600), "549.6k");
121 assert_eq!(human_count(1_500_000), "1.5m");
122 }
123
124 #[test]
125 fn no_daemon_status_shows_snapshot_location() {
126 let block = Status {
127 root: Path::new("/repo"),
128 snapshot: Path::new("/cache/rgx/abc/index.bin"),
129 running: false,
130 ram_only: false,
131 state: None,
132 files: None,
133 trigrams: None,
134 memory_bytes: None,
135 }
136 .render();
137 assert!(block.contains("daemon not running"));
138 assert!(block.contains("/cache/rgx/abc/index.bin"));
139 assert!(block.contains("not built yet")); }
141
142 #[test]
143 fn ram_only_status_reports_no_snapshot() {
144 let snapshot = Path::new("/cache/rgx/abc/index.bin");
145 let status = |ram_only| {
146 Status {
147 root: Path::new("/repo"),
148 snapshot,
149 running: true,
150 ram_only,
151 state: Some("ready".into()),
152 files: Some(120),
153 trigrams: Some(5000),
154 memory_bytes: Some(1024),
155 }
156 .render()
157 };
158
159 let ram = status(true);
160 assert!(ram.contains("ram-only (rebuilt on start)"));
161 assert!(!ram.contains(&snapshot.display().to_string()));
164 assert!(status(false).contains(&snapshot.display().to_string()));
165 }
166}