max / makenotwork
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 { |