Skip to main content

ai_usagebar/
error.rs

1//! Shared error type. Vendors and renderers convert their failures into
2//! `AppError` so the widget shell can decide whether to retry, fall back to
3//! cache, show ⚠, or show "Loading…".
4
5use std::io;
6use std::path::PathBuf;
7
8pub type Result<T> = std::result::Result<T, AppError>;
9
10#[derive(Debug, thiserror::Error)]
11pub enum AppError {
12    /// Local I/O failed (cache write, credentials read, theme file, etc.).
13    #[error("io error at {path}: {source}")]
14    Io {
15        path: PathBuf,
16        #[source]
17        source: io::Error,
18    },
19
20    /// Generic I/O without a meaningful path (e.g. stdout writes).
21    #[error(transparent)]
22    IoBare(#[from] io::Error),
23
24    /// A vendor's credentials file is missing, unreadable, or malformed.
25    /// Distinct from `Io` because the widget treats it as "user must re-auth"
26    /// rather than a transient failure.
27    #[error("credentials error: {0}")]
28    Credentials(String),
29
30    /// HTTP request failed at the transport layer (DNS, TLS, timeout, connect).
31    /// Maps to claudebar's "HTTP 000" — show `Loading…`, don't write
32    /// `.last_error`, retry next tick.
33    #[error("network transport error: {0}")]
34    Transport(String),
35
36    /// HTTP request reached the server but returned a non-2xx status.
37    /// Carries the code + best-effort body so the widget can populate
38    /// `.last_error` for the tooltip.
39    #[error("HTTP {status}: {body}")]
40    Http { status: u16, body: String },
41
42    /// API returned 2xx but the body did not match our expected schema.
43    /// Treated like an HTTP error for tooltip purposes, but logged separately
44    /// because it signals undocumented-endpoint drift.
45    #[error("schema mismatch: {0}")]
46    Schema(String),
47
48    /// JSON serialization/deserialization failure (config files, response bodies).
49    #[error("json error: {0}")]
50    Json(#[from] serde_json::Error),
51
52    /// TOML config parse failure.
53    #[error("toml error: {0}")]
54    Toml(#[from] toml::de::Error),
55
56    /// Catch-all for unexpected conditions (cache lock contention, etc.).
57    #[error("{0}")]
58    Other(String),
59}
60
61impl AppError {
62    /// Convenience for non-pathful I/O.
63    pub fn io_at(path: impl Into<PathBuf>, source: io::Error) -> Self {
64        AppError::Io {
65            path: path.into(),
66            source,
67        }
68    }
69
70    /// True for transient network errors that the widget should hide behind a
71    /// "Loading…" rather than a "⚠".
72    pub fn is_transient(&self) -> bool {
73        matches!(self, AppError::Transport(_))
74    }
75}
76
77/// Map a reqwest error into the right variant. Connection-class failures
78/// become `Transport` (transient); the rest become generic `Http`/`Other`.
79impl From<reqwest::Error> for AppError {
80    fn from(err: reqwest::Error) -> Self {
81        if err.is_timeout() || err.is_connect() || err.is_request() {
82            return AppError::Transport(err.to_string());
83        }
84        if let Some(status) = err.status() {
85            return AppError::Http {
86                status: status.as_u16(),
87                body: err.to_string(),
88            };
89        }
90        AppError::Other(err.to_string())
91    }
92}