Skip to main content

max / makenotwork

viewer: adopt universal clippy/lint baseline; warnings to zero Add the shared pedantic + unreachable_pub baseline. This is a binary, so cargo fix demoted 122 crate-internal `pub` items to `pub(crate)` (unreachable_pub). Remaining clippy fixes: unnested or-patterns in the key match, inlined format args, map_or (with key.as_str() so both arms are &str), Duration::from_mins(2), a path-form closure. Five owned/consumed params carry a documented #[allow(needless_pass_by_value)]. clippy --all-targets clean, cargo fmt clean, 81 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-23 13:18 UTC
Commit: 6cc283a62026325b8c4ea483d403f37ff1393c88
Parent: 7cc64f2
8 files changed, +108 insertions, -68 deletions
@@ -24,3 +24,35 @@ anyhow = "1.0.102"
24 24
25 25 [dev-dependencies]
26 26 tempfile = "3.20"
27 +
28 + [lints.rust]
29 + unused = "warn"
30 + unreachable_pub = "warn"
31 +
32 + [lints.clippy]
33 + pedantic = { level = "warn", priority = -1 }
34 + # Allow-list tuned from a measured breakdown across server/multithreaded/pter
35 + # (2026-07-22). These are the high-churn / low-signal pedantic lints; everything
36 + # else in `pedantic` stays a warning. Keep this block identical across repos.
37 + module_name_repetitions = "allow"
38 + # Doc lints — no docs-completeness push underway.
39 + missing_errors_doc = "allow"
40 + missing_panics_doc = "allow"
41 + doc_markdown = "allow"
42 + # Numeric casts — endemic and mostly intentional in size/byte math.
43 + cast_possible_truncation = "allow"
44 + cast_sign_loss = "allow"
45 + cast_precision_loss = "allow"
46 + cast_possible_wrap = "allow"
47 + cast_lossless = "allow"
48 + # Subjective structure/style nags — high churn, low signal.
49 + must_use_candidate = "allow"
50 + too_many_lines = "allow"
51 + struct_excessive_bools = "allow"
52 + similar_names = "allow"
53 + items_after_statements = "allow"
54 + single_match_else = "allow"
55 + # Frequent false-positives in TUI/router-heavy code — added from the buckets breakdown.
56 + match_same_arms = "allow"
57 + unnecessary_wraps = "allow"
58 + type_complexity = "allow"
@@ -22,7 +22,7 @@ const DEFAULT_POLL_SECS: u64 = 5;
22 22 const DEFAULT_STALE_SECS: u64 = 60;
23 23
24 24 #[derive(Debug, Clone, Deserialize)]
25 - pub struct Config {
25 + pub(crate) struct Config {
26 26 /// Fallback for any source that does not set its own.
27 27 #[serde(default = "default_stale")]
28 28 pub stale_after_secs: u64,
@@ -31,7 +31,7 @@ pub struct Config {
31 31 }
32 32
33 33 #[derive(Debug, Clone, Deserialize)]
34 - pub struct Source {
34 + pub(crate) struct Source {
35 35 /// Tab label, and the `source` name the payload should carry.
36 36 pub name: String,
37 37 /// Base URL. `/status.json` is appended.
@@ -70,11 +70,11 @@ fn default_stale() -> u64 {
70 70 }
71 71
72 72 impl Source {
73 - pub fn status_url(&self) -> String {
73 + pub(crate) fn status_url(&self) -> String {
74 74 format!("{}/status.json", self.url.trim_end_matches('/'))
75 75 }
76 76
77 - pub fn poll_interval(&self) -> Duration {
77 + pub(crate) fn poll_interval(&self) -> Duration {
78 78 Duration::from_secs(self.poll_secs.max(1))
79 79 }
80 80
@@ -83,7 +83,7 @@ impl Source {
83 83 /// An action carries a path (`/rollback/b`); a producer that instead gives a
84 84 /// full URL is honored as-is, so a daemon can point an action at somewhere
85 85 /// other than itself without the viewer second-guessing it.
86 - pub fn action_url(&self, path: &str) -> String {
86 + pub(crate) fn action_url(&self, path: &str) -> String {
87 87 if path.starts_with("http://") || path.starts_with("https://") {
88 88 return path.to_string();
89 89 }
@@ -95,7 +95,7 @@ impl Source {
95 95 }
96 96
97 97 /// Resolve the bearer token from the environment, if one is named.
98 - pub fn token(&self) -> Option<String> {
98 + pub(crate) fn token(&self) -> Option<String> {
99 99 self.token_env
100 100 .as_deref()
101 101 .and_then(|name| std::env::var(name).ok())
@@ -104,7 +104,7 @@ impl Source {
104 104 }
105 105
106 106 impl Config {
107 - pub fn load(path: &Path) -> Result<Self> {
107 + pub(crate) fn load(path: &Path) -> Result<Self> {
108 108 let raw = std::fs::read_to_string(path)
109 109 .with_context(|| format!("reading viewer config at {}", path.display()))?;
110 110 let cfg: Config = toml::from_str(&raw)
@@ -135,7 +135,7 @@ impl Config {
135 135 }
136 136
137 137 /// Staleness limit for one source: its own, else the global default.
138 - pub fn stale_after(&self, source: &Source) -> chrono::TimeDelta {
138 + pub(crate) fn stale_after(&self, source: &Source) -> chrono::TimeDelta {
139 139 let secs = source.stale_after_secs.unwrap_or(self.stale_after_secs);
140 140 chrono::TimeDelta::seconds(secs as i64)
141 141 }
@@ -20,7 +20,7 @@ use crate::config::Source;
20 20 /// The result of one fired action, tagged with the key that fired it so the UI
21 21 /// can name it in the footer without tracking the request itself.
22 22 #[derive(Debug, Clone)]
23 - pub struct Outcome {
23 + pub(crate) struct Outcome {
24 24 pub key: String,
25 25 /// `Ok` carries the 2xx status code; `Err` a short reason.
26 26 pub result: Result<u16, String>,
@@ -34,7 +34,7 @@ pub struct Outcome {
34 34 /// daemon that never answers does not leak a task forever. A synchronous
35 35 /// operation that genuinely outlasts this is a daemon-shape problem, not a
36 36 /// viewer one; the next poll still shows the true resulting state.
37 - const ACTION_TIMEOUT: Duration = Duration::from_secs(120);
37 + const ACTION_TIMEOUT: Duration = Duration::from_mins(2);
38 38
39 39 /// Body text kept from a failed response, past which it is noise in a one-line
40 40 /// footer.
@@ -45,7 +45,7 @@ const BODY_KEEP: usize = 120;
45 45 /// Spawns a detached task and returns immediately; the caller must be inside a
46 46 /// Tokio runtime (the UI loop is). A send failure means the UI is gone, which
47 47 /// is not this task's problem to solve.
48 - pub fn fire(source: &Source, action: Action, key: String, tx: mpsc::Sender<Outcome>) {
48 + pub(crate) fn fire(source: &Source, action: Action, key: String, tx: mpsc::Sender<Outcome>) {
49 49 let url = source.action_url(&action.url);
50 50 let token = source.token();
51 51 tokio::spawn(async move {
@@ -110,6 +110,8 @@ fn clip(body: &str) -> String {
110 110
111 111 /// The same short-reason treatment `poll` gives transport errors: the footer has
112 112 /// one line, so keep the part that says what broke.
113 + // e is consumed into the short string.
114 + #[allow(clippy::needless_pass_by_value)]
113 115 fn short_error(e: reqwest::Error) -> String {
114 116 if e.is_timeout() {
115 117 return "timed out".into();
@@ -90,6 +90,8 @@ fn config_path() -> Result<PathBuf> {
90 90 Ok(PathBuf::from(home).join(".config/ops-viewer/viewer.toml"))
91 91 }
92 92
93 + // The TUI run loop owns model/sources/channels for the app's lifetime.
94 + #[allow(clippy::needless_pass_by_value)]
93 95 fn run(
94 96 terminal: &mut ratatui::DefaultTerminal,
95 97 mut model: Model,
@@ -135,6 +137,8 @@ fn run(
135 137 /// The action is looked up again here, not trusted from when the prompt opened:
136 138 /// a poll in between can retract it, and firing a `promote` the daemon no longer
137 139 /// offers is exactly the surprise the whole confirm path exists to prevent.
140 + // request is consumed into the fired action.
141 + #[allow(clippy::needless_pass_by_value)]
138 142 fn dispatch(
139 143 model: &mut Model,
140 144 sources: &[Source],
@@ -259,8 +263,8 @@ fn handle_prompt_key(model: &mut Model, key: KeyEvent) -> KeyResult {
259 263 Some(Prompt::Confirm { .. }) => match key.code {
260 264 // 'y' is the only key that fires; 'n'/Esc back out; the rest do
261 265 // nothing, so a fat-fingered key neither fires nor loses the prompt.
262 - KeyCode::Char('y') | KeyCode::Char('Y') => model.confirm_yes(),
263 - KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => model.cancel_prompt(),
266 + KeyCode::Char('y' | 'Y') => model.confirm_yes(),
267 + KeyCode::Esc | KeyCode::Char('n' | 'N') => model.cancel_prompt(),
264 268 _ => PromptStep::Idle,
265 269 },
266 270 Some(Prompt::Type { .. }) => match key.code {
@@ -9,7 +9,7 @@ use chrono::{DateTime, TimeDelta, Utc};
9 9 use ops_status::{Action, Node, Payload, Status};
10 10
11 11 /// One source, as last heard from.
12 - pub struct SourceState {
12 + pub(crate) struct SourceState {
13 13 pub name: String,
14 14 /// Whether this source's declared actions may be fired. Off by default; set
15 15 /// from the config's per-source `allow_actions`. Kept on the state rather
@@ -31,7 +31,7 @@ pub struct SourceState {
31 31 }
32 32
33 33 impl SourceState {
34 - pub fn new(name: impl Into<String>, stale_after: TimeDelta) -> Self {
34 + pub(crate) fn new(name: impl Into<String>, stale_after: TimeDelta) -> Self {
35 35 SourceState {
36 36 name: name.into(),
37 37 allow_actions: false,
@@ -45,7 +45,7 @@ impl SourceState {
45 45
46 46 /// Let this source's declared actions be fired. Builder-style so tests and
47 47 /// `main` set it without a wider constructor.
48 - pub fn with_actions(mut self, allow: bool) -> Self {
48 + pub(crate) fn with_actions(mut self, allow: bool) -> Self {
49 49 self.allow_actions = allow;
50 50 self
51 51 }
@@ -53,16 +53,16 @@ impl SourceState {
53 53 /// A declared action by key, from the current payload. `None` if the source
54 54 /// has not answered or no longer offers it — the latter matters because a
55 55 /// poll between opening a prompt and confirming can retract an action.
56 - pub fn action(&self, key: &str) -> Option<&Action> {
56 + pub(crate) fn action(&self, key: &str) -> Option<&Action> {
57 57 self.payload.as_ref().and_then(|p| p.actions.get(key))
58 58 }
59 59
60 60 /// How old the current payload is, if there is one.
61 - pub fn age(&self, now: DateTime<Utc>) -> Option<TimeDelta> {
61 + pub(crate) fn age(&self, now: DateTime<Utc>) -> Option<TimeDelta> {
62 62 self.payload.as_ref().map(|p| p.age(now))
63 63 }
64 64
65 - pub fn is_stale(&self, now: DateTime<Utc>) -> bool {
65 + pub(crate) fn is_stale(&self, now: DateTime<Utc>) -> bool {
66 66 self.age(now).is_some_and(|age| age > self.stale_after)
67 67 }
68 68
@@ -78,7 +78,7 @@ impl SourceState {
78 78 /// The middle case is the one that is normally missed. A backup check that
79 79 /// answers "ok" about a snapshot taken forty days ago is not ok, and every
80 80 /// check that existed said it was.
81 - pub fn status(&self, now: DateTime<Utc>) -> Status {
81 + pub(crate) fn status(&self, now: DateTime<Utc>) -> Status {
82 82 let Some(payload) = &self.payload else {
83 83 return Status::Unknown;
84 84 };
@@ -89,7 +89,7 @@ impl SourceState {
89 89 }
90 90
91 91 /// A short phrase for why this source reads the way it does.
92 - pub fn summary(&self, now: DateTime<Utc>) -> String {
92 + pub(crate) fn summary(&self, now: DateTime<Utc>) -> String {
93 93 if let Some(error) = &self.error {
94 94 let last = match self.last_ok {
95 95 Some(at) => crate::value::relative(at, now),
@@ -126,7 +126,7 @@ impl SourceState {
126 126 /// flat list becomes a tree. Depth stops at one: the contract allows deeper
127 127 /// nesting but nothing emits it, and an unbounded recursion over
128 128 /// producer-supplied ids is a denial-of-service waiting to happen.
129 - pub fn rows(&self) -> Vec<Row<'_>> {
129 + pub(crate) fn rows(&self) -> Vec<Row<'_>> {
130 130 let Some(payload) = &self.payload else {
131 131 return Vec::new();
132 132 };
@@ -149,13 +149,13 @@ impl SourceState {
149 149 }
150 150
151 151 /// The node the cursor is on.
152 - pub fn selected_node(&self) -> Option<&Node> {
152 + pub(crate) fn selected_node(&self) -> Option<&Node> {
153 153 let rows = self.rows();
154 154 rows.get(self.selected.min(rows.len().saturating_sub(1)))
155 155 .map(|r| r.node)
156 156 }
157 157
158 - pub fn move_selection(&mut self, delta: isize) {
158 + pub(crate) fn move_selection(&mut self, delta: isize) {
159 159 let len = self.rows().len();
160 160 if len == 0 {
161 161 self.selected = 0;
@@ -166,7 +166,7 @@ impl SourceState {
166 166 }
167 167
168 168 /// Record a successful poll.
169 - pub fn observe(&mut self, payload: Payload, at: DateTime<Utc>) {
169 + pub(crate) fn observe(&mut self, payload: Payload, at: DateTime<Utc>) {
170 170 self.payload = Some(payload);
171 171 self.error = None;
172 172 self.last_ok = Some(at);
@@ -179,20 +179,20 @@ impl SourceState {
179 179 }
180 180
181 181 /// Record a failed poll, keeping the last known payload.
182 - pub fn observe_error(&mut self, error: impl Into<String>) {
182 + pub(crate) fn observe_error(&mut self, error: impl Into<String>) {
183 183 self.error = Some(error.into());
184 184 }
185 185 }
186 186
187 187 /// One line in a source tab.
188 - pub struct Row<'a> {
188 + pub(crate) struct Row<'a> {
189 189 pub node: &'a Node,
190 190 pub depth: usize,
191 191 }
192 192
193 193 /// Which tab is showing.
194 194 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
195 - pub enum Tab {
195 + pub(crate) enum Tab {
196 196 /// Every source at once, worst first. The tab that earns the product.
197 197 Rollup,
198 198 Source(usize),
@@ -205,7 +205,7 @@ pub enum Tab {
205 205 /// guard's weight is set by the action itself — a `danger` action is confirmed
206 206 /// by typing its key, a `confirm` one by a `y`, a plain one not at all.
207 207 #[derive(Debug, Clone, PartialEq, Eq)]
208 - pub enum Prompt {
208 + pub(crate) enum Prompt {
209 209 /// Choosing which of the selected node's actions to run.
210 210 Pick {
211 211 source: usize,
@@ -227,14 +227,14 @@ pub enum Prompt {
227 227 /// there; resolving the source's URL and token and making the HTTP call is the
228 228 /// loop's job, which keeps every network effect out of the model.
229 229 #[derive(Debug, Clone, PartialEq, Eq)]
230 - pub struct FireRequest {
230 + pub(crate) struct FireRequest {
231 231 pub source: usize,
232 232 pub key: String,
233 233 }
234 234
235 235 /// What a keypress asked the model to do once the prompt resolved.
236 236 #[derive(Debug, PartialEq, Eq)]
237 - pub enum PromptStep {
237 + pub(crate) enum PromptStep {
238 238 /// Still in a prompt (or none was open); nothing to dispatch.
239 239 Idle,
240 240 /// The prompt closed with a confirmed request to fire.
@@ -243,7 +243,7 @@ pub enum PromptStep {
243 243 Cancelled,
244 244 }
245 245
246 - pub struct Model {
246 + pub(crate) struct Model {
247 247 pub sources: Vec<SourceState>,
248 248 pub tab: Tab,
249 249 /// Cursor within the rollup tab.
@@ -255,7 +255,7 @@ pub struct Model {
255 255 }
256 256
257 257 impl Model {
258 - pub fn new(sources: Vec<SourceState>) -> Self {
258 + pub(crate) fn new(sources: Vec<SourceState>) -> Self {
259 259 Model {
260 260 sources,
261 261 tab: Tab::Rollup,
@@ -273,7 +273,7 @@ impl Model {
273 273 /// so rather than silently ignoring the key, because a viewer that looks
274 274 /// like it should be able to act and does not is worse than one that says it
275 275 /// cannot.
276 - pub fn open_actions(&mut self) {
276 + pub(crate) fn open_actions(&mut self) {
277 277 let Tab::Source(i) = self.tab else { return };
278 278 let Some(source) = self.sources.get(i) else {
279 279 return;
@@ -299,7 +299,7 @@ impl Model {
299 299 }
300 300
301 301 /// Move the cursor inside an open picker. No-op for the other prompts.
302 - pub fn prompt_move(&mut self, delta: isize) {
302 + pub(crate) fn prompt_move(&mut self, delta: isize) {
303 303 if let Some(Prompt::Pick { keys, selected, .. }) = &mut self.prompt {
304 304 if keys.is_empty() {
305 305 return;
@@ -310,7 +310,7 @@ impl Model {
310 310 }
311 311
312 312 /// A digit inside a picker jumps straight to that action (1-based).
313 - pub fn prompt_digit(&mut self, n: usize) {
313 + pub(crate) fn prompt_digit(&mut self, n: usize) {
314 314 if let Some(Prompt::Pick { keys, selected, .. }) = &mut self.prompt
315 315 && (1..=keys.len()).contains(&n)
316 316 {
@@ -319,14 +319,14 @@ impl Model {
319 319 }
320 320
321 321 /// A printable character while typing a `danger` action's key.
322 - pub fn prompt_push(&mut self, c: char) {
322 + pub(crate) fn prompt_push(&mut self, c: char) {
323 323 if let Some(Prompt::Type { typed, .. }) = &mut self.prompt {
324 324 typed.push(c);
325 325 }
326 326 }
327 327
328 328 /// Backspace while typing.
329 - pub fn prompt_backspace(&mut self) {
329 + pub(crate) fn prompt_backspace(&mut self) {
330 330 if let Some(Prompt::Type { typed, .. }) = &mut self.prompt {
331 331 typed.pop();
332 332 }
@@ -339,7 +339,7 @@ impl Model {
339 339 /// - On a `Type` guard, fires only when the typed text matches the key.
340 340 /// - `Confirm` does not respond to Enter; it wants an explicit `y`
341 341 /// ([`confirm_yes`]), so a stray Enter cannot promote through it.
342 - pub fn prompt_enter(&mut self) -> PromptStep {
342 + pub(crate) fn prompt_enter(&mut self) -> PromptStep {
343 343 match self.prompt.take() {
344 344 Some(Prompt::Pick {
345 345 source,
@@ -388,7 +388,7 @@ impl Model {
388 388 }
389 389
390 390 /// `y` on a `Confirm` guard fires; anywhere else it is nothing.
391 - pub fn confirm_yes(&mut self) -> PromptStep {
391 + pub(crate) fn confirm_yes(&mut self) -> PromptStep {
392 392 if let Some(Prompt::Confirm { source, key }) = self.prompt.take() {
393 393 self.fire(source, key)
394 394 } else {
@@ -397,7 +397,7 @@ impl Model {
397 397 }
398 398
399 399 /// Close any open prompt without firing.
400 - pub fn cancel_prompt(&mut self) -> PromptStep {
400 + pub(crate) fn cancel_prompt(&mut self) -> PromptStep {
401 401 if self.prompt.take().is_some() {
402 402 PromptStep::Cancelled
403 403 } else {
@@ -418,7 +418,7 @@ impl Model {
418 418 /// Worst-first is the whole argument for the rollup existing. Sorted any
419 419 /// other way it is a list you still have to read all of, which is the
420 420 /// situation it replaces.
421 - pub fn rollup_order(&self, now: DateTime<Utc>) -> Vec<usize> {
421 + pub(crate) fn rollup_order(&self, now: DateTime<Utc>) -> Vec<usize> {
422 422 let mut order: Vec<usize> = (0..self.sources.len()).collect();
423 423 order.sort_by(|&a, &b| {
424 424 let (sa, sb) = (self.sources[a].status(now), self.sources[b].status(now));
@@ -429,7 +429,7 @@ impl Model {
429 429 }
430 430
431 431 /// The worst status across every source: the one thing to look at first.
432 - pub fn worst(&self, now: DateTime<Utc>) -> Status {
432 + pub(crate) fn worst(&self, now: DateTime<Utc>) -> Status {
433 433 self.sources
434 434 .iter()
435 435 .map(|s| s.status(now))
@@ -437,20 +437,20 @@ impl Model {
437 437 .unwrap_or(Status::Unknown)
438 438 }
439 439
440 - pub fn tab_titles(&self) -> Vec<String> {
440 + pub(crate) fn tab_titles(&self) -> Vec<String> {
441 441 let mut titles = vec!["rollup".to_string()];
442 442 titles.extend(self.sources.iter().map(|s| s.name.clone()));
443 443 titles
444 444 }
445 445
446 - pub fn tab_index(&self) -> usize {
446 + pub(crate) fn tab_index(&self) -> usize {
447 447 match self.tab {
448 448 Tab::Rollup => 0,
449 449 Tab::Source(i) => i + 1,
450 450 }
451 451 }
452 452
453 - pub fn select_tab(&mut self, index: usize) {
453 + pub(crate) fn select_tab(&mut self, index: usize) {
454 454 self.tab = match index {
455 455 0 => Tab::Rollup,
456 456 n if n <= self.sources.len() => Tab::Source(n - 1),
@@ -458,19 +458,19 @@ impl Model {
458 458 };
459 459 }
460 460
461 - pub fn next_tab(&mut self) {
461 + pub(crate) fn next_tab(&mut self) {
462 462 let next = (self.tab_index() + 1) % (self.sources.len() + 1);
463 463 self.select_tab(next);
464 464 }
465 465
466 - pub fn prev_tab(&mut self) {
466 + pub(crate) fn prev_tab(&mut self) {
467 467 let count = self.sources.len() + 1;
468 468 let next = (self.tab_index() + count - 1) % count;
469 469 self.select_tab(next);
470 470 }
471 471
472 472 /// Move the cursor in whichever tab is showing.
473 - pub fn move_selection(&mut self, delta: isize, now: DateTime<Utc>) {
473 + pub(crate) fn move_selection(&mut self, delta: isize, now: DateTime<Utc>) {
474 474 match self.tab {
475 475 Tab::Rollup => {
476 476 let len = self.rollup_order(now).len();
@@ -489,7 +489,7 @@ impl Model {
489 489 }
490 490
491 491 /// Enter on a rollup row opens that source's tab.
492 - pub fn open_selected(&mut self, now: DateTime<Utc>) {
492 + pub(crate) fn open_selected(&mut self, now: DateTime<Utc>) {
493 493 if self.tab == Tab::Rollup {
494 494 let order = self.rollup_order(now);
495 495 if let Some(&index) = order.get(self.rollup_selected) {
@@ -537,7 +537,7 @@ mod tests {
537 537 /// allowed, sitting on its own tab ready to prompt.
538 538 fn actionable(node_actions: &[&str], declared: Vec<(&str, Action)>) -> Model {
539 539 let mut n = node("tier:b", Status::Ok, vec![]);
540 - n.actions = node_actions.iter().map(|s| s.to_string()).collect();
540 + n.actions = node_actions.iter().map(ToString::to_string).collect();
541 541 let mut p = payload(now(), vec![n]);
542 542 p.actions = declared
543 543 .into_iter()
@@ -14,7 +14,7 @@ use tokio::sync::mpsc;
14 14 use crate::config::Source;
15 15
16 16 /// A poll result, tagged with which source it came from.
17 - pub struct Update {
17 + pub(crate) struct Update {
18 18 pub index: usize,
19 19 pub at: chrono::DateTime<Utc>,
20 20 pub result: Result<Payload, String>,
@@ -28,7 +28,7 @@ pub struct Update {
28 28 const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
29 29
30 30 /// Spawn one polling task per source. Returns the receiving end.
31 - pub fn spawn_all(sources: &[Source]) -> mpsc::Receiver<Update> {
31 + pub(crate) fn spawn_all(sources: &[Source]) -> mpsc::Receiver<Update> {
32 32 // Capacity comfortably exceeds one in-flight update per source, so a
33 33 // momentarily busy UI loop cannot make a poller block or drop a result.
34 34 let (tx, rx) = mpsc::channel(sources.len().max(1) * 4);
@@ -105,6 +105,8 @@ async fn fetch(
105 105
106 106 /// reqwest errors stringify into a paragraph with a full URL chain. The rollup
107 107 /// has one column for this, so keep the part that says what went wrong.
108 + // e is consumed into the short string.
109 + #[allow(clippy::needless_pass_by_value)]
108 110 fn short_error(e: reqwest::Error) -> String {
109 111 if e.is_timeout() {
110 112 return "timed out".into();
@@ -17,7 +17,7 @@ use ratatui::widgets::{Block, Cell, Clear, List, ListItem, Paragraph, Row, Table
17 17 use crate::model::{Model, Prompt, SourceState, Tab};
18 18 use crate::value;
19 19
20 - pub fn render(model: &Model, now: DateTime<Utc>, frame: &mut Frame) {
20 + pub(crate) fn render(model: &Model, now: DateTime<Utc>, frame: &mut Frame) {
21 21 let [header, body, footer] = Layout::vertical([
22 22 Constraint::Length(1),
23 23 Constraint::Min(1),
@@ -286,7 +286,7 @@ fn render_prompt(model: &Model, frame: &mut Frame, area: Rect) {
286 286 let mut lines = Vec::new();
287 287 for (i, key) in keys.iter().enumerate() {
288 288 let action = source.and_then(|s| s.action(key));
289 - let label = action.map(|a| a.label.as_str()).unwrap_or(key);
289 + let label = action.map_or(key.as_str(), |a| a.label.as_str());
290 290 let danger = action.is_some_and(|a| a.danger);
291 291 let marker = if i == *selected { "> " } else { " " };
292 292 let mut style = Style::default();
@@ -15,7 +15,7 @@ use ops_status::{Status, Value};
15 15 use ratatui::style::{Color, Modifier, Style};
16 16
17 17 /// The one place a status becomes a color.
18 - pub fn status_style(status: Status) -> Style {
18 + pub(crate) fn status_style(status: Status) -> Style {
19 19 match status {
20 20 Status::Ok => Style::default().fg(Color::Green),
21 21 Status::Degraded => Style::default().fg(Color::Yellow),
@@ -28,7 +28,7 @@ pub fn status_style(status: Status) -> Style {
28 28 }
29 29
30 30 /// A short, fixed-width-ish marker so columns line up.
31 - pub fn status_mark(status: Status) -> &'static str {
31 + pub(crate) fn status_mark(status: Status) -> &'static str {
32 32 match status {
33 33 Status::Ok => "ok",
34 34 Status::Degraded => "degr",
@@ -39,7 +39,7 @@ pub fn status_mark(status: Status) -> &'static str {
39 39 }
40 40
41 41 /// Render one value. `width` is the budget for things that elide.
42 - pub fn render(value: &Value, now: DateTime<Utc>, width: usize) -> String {
42 + pub(crate) fn render(value: &Value, now: DateTime<Utc>, width: usize) -> String {
43 43 match value {
44 44 Value::Text { value } => value.clone(),
45 45 Value::Ident { value, abbrev_to } => abbreviate(value, abbrev_to.unwrap_or(12)),
@@ -55,7 +55,7 @@ pub fn render(value: &Value, now: DateTime<Utc>, width: usize) -> String {
55 55 }
56 56
57 57 /// The style a value carries on its own, if any.
58 - pub fn style(value: &Value) -> Style {
58 + pub(crate) fn style(value: &Value) -> Style {
59 59 match value {
60 60 Value::State { value } => status_style(*value),
61 61 Value::Ident { .. } | Value::Path { .. } => Style::default().fg(Color::Cyan),
@@ -71,7 +71,7 @@ pub fn style(value: &Value) -> Style {
71 71 /// Char-wise rather than byte-wise: a digest is ASCII, but nothing in the
72 72 /// contract promises that, and slicing a multi-byte value at a byte index
73 73 /// panics.
74 - pub fn abbreviate(value: &str, to: usize) -> String {
74 + pub(crate) fn abbreviate(value: &str, to: usize) -> String {
75 75 if value.chars().count() <= to {
76 76 return value.to_string();
77 77 }
@@ -81,7 +81,7 @@ pub fn abbreviate(value: &str, to: usize) -> String {
81 81 /// Elide the middle of a path, keeping both ends: the leading directories say
82 82 /// where you are and the basename says what it is. Truncating either end alone
83 83 /// throws away the half you needed.
84 - pub fn middle_elide(value: &str, max: usize) -> String {
84 + pub(crate) fn middle_elide(value: &str, max: usize) -> String {
85 85 let chars: Vec<char> = value.chars().collect();
86 86 if chars.len() <= max {
87 87 return value.to_string();
@@ -99,7 +99,7 @@ pub fn middle_elide(value: &str, max: usize) -> String {
99 99 }
100 100
101 101 /// "3m ago", "in 2m", "just now".
102 - pub fn relative(at: DateTime<Utc>, now: DateTime<Utc>) -> String {
102 + pub(crate) fn relative(at: DateTime<Utc>, now: DateTime<Utc>) -> String {
103 103 let delta = now - at;
104 104 let secs = delta.num_seconds();
105 105 if secs.abs() < 5 {
@@ -115,7 +115,7 @@ pub fn relative(at: DateTime<Utc>, now: DateTime<Utc>) -> String {
115 115
116 116 /// Humanize a span. Two units at most: "2h 14m" is useful, "2h 14m 7s" is
117 117 /// noise at every scale where hours matter.
118 - pub fn duration(seconds: i64) -> String {
118 + pub(crate) fn duration(seconds: i64) -> String {
119 119 let s = seconds.abs();
120 120 let (d, h, m, sec) = (s / 86_400, (s % 86_400) / 3600, (s % 3600) / 60, s % 60);
121 121 match (d, h, m) {
@@ -130,7 +130,7 @@ pub fn duration(seconds: i64) -> String {
130 130
131 131 /// A magnitude with its unit. The number is humanized; the unit is the
132 132 /// producer's, verbatim.
133 - pub fn quantity(value: f64, unit: Option<&str>) -> String {
133 + pub(crate) fn quantity(value: f64, unit: Option<&str>) -> String {
134 134 let n = humanize_number(value);
135 135 match unit {
136 136 Some(u) => format!("{n} {u}"),
@@ -150,15 +150,15 @@ fn humanize_number(value: f64) -> String {
150 150 (value, "")
151 151 };
152 152 let rendered = if scaled.fract().abs() < 0.05 {
153 - format!("{:.0}", scaled)
153 + format!("{scaled:.0}")
154 154 } else {
155 - format!("{:.1}", scaled)
155 + format!("{scaled:.1}")
156 156 };
157 157 format!("{rendered}{suffix}")
158 158 }
159 159
160 160 /// A progress bar plus its numbers, e.g. `[####------] 31/48 hour`.
161 - pub fn progress(value: f64, max: f64, unit: Option<&str>, width: usize) -> String {
161 + pub(crate) fn progress(value: f64, max: f64, unit: Option<&str>, width: usize) -> String {
162 162 let numbers = match unit {
163 163 Some(u) => format!("{}/{} {u}", humanize_number(value), humanize_number(max)),
164 164 None => format!("{}/{}", humanize_number(value), humanize_number(max)),
@@ -179,7 +179,7 @@ pub fn progress(value: f64, max: f64, unit: Option<&str>, width: usize) -> Strin
179 179
180 180 /// How a progress value should be colored: complete is done, not merely far
181 181 /// along.
182 - pub fn progress_style(value: f64, max: f64) -> Style {
182 + pub(crate) fn progress_style(value: f64, max: f64) -> Style {
183 183 if max > 0.0 && value >= max {
184 184 Style::default().fg(Color::Green)
185 185 } else {