Skip to main content

databricks_tui/
shape.rs

1use std::str::FromStr;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
6pub enum Status {
7    Running,
8    Success,
9    Stopped,
10    Pending,
11    Failed,
12    Unknown(String),
13}
14
15impl FromStr for Status {
16    type Err = std::convert::Infallible;
17
18    fn from_str(s: &str) -> Result<Self, Self::Err> {
19        Ok(match s.to_uppercase().as_str() {
20            "RUNNING" => Status::Running,
21            "SUCCESS" | "COMPLETED" | "FINISHED" => Status::Success,
22            "IDLE" | "STOPPED" | "TERMINATED" | "DELETED" | "SKIPPED" | "CANCELED" => {
23                Status::Stopped
24            }
25            "PENDING"
26            | "STARTING"
27            | "RESTARTING"
28            | "DELETING"
29            | "TERMINATING"
30            | "QUEUED"
31            | "WAITING_FOR_RETRY"
32            | "BLOCKED"
33            | "CREATED"
34            | "INITIALIZING"
35            | "RESETTING"
36            | "SETTING_UP_TABLES"
37            | "WAITING_FOR_RESOURCES"
38            | "STOPPING" => Status::Pending,
39            "FAILED" | "ERROR" | "TIMEDOUT" | "TIMED_OUT" | "INTERNAL_ERROR" => Status::Failed,
40            other => Status::Unknown(other.to_string()),
41        })
42    }
43}
44
45impl Status {
46    /// Sort priority for active-first pane ordering: running work on top,
47    /// then starting, failures needing attention, finished, idle, the rest.
48    pub fn rank(&self) -> u8 {
49        match self {
50            Status::Running => 0,
51            Status::Pending => 1,
52            Status::Failed => 2,
53            Status::Success => 3,
54            Status::Stopped => 4,
55            Status::Unknown(_) => 5,
56        }
57    }
58
59    pub fn label(&self) -> &str {
60        match self {
61            Status::Running => "RUNNING",
62            Status::Success => "SUCCESS",
63            Status::Stopped => "IDLE",
64            Status::Pending => "PENDING",
65            Status::Failed => "FAILED",
66            Status::Unknown(s) => s.as_str(),
67        }
68    }
69}
70
71/// Human-friendly "how long ago" for a millisecond epoch timestamp.
72pub fn relative_time(epoch_ms: u64) -> String {
73    let now = std::time::SystemTime::now()
74        .duration_since(std::time::UNIX_EPOCH)
75        .map(|d| d.as_millis() as u64)
76        .unwrap_or(0);
77    let secs = now.saturating_sub(epoch_ms) / 1000;
78    match secs {
79        0..=59 => "just now".to_string(),
80        60..=3599 => format!("{}m ago", secs / 60),
81        3600..=86_399 => format!("{}h ago", secs / 3600),
82        _ => format!("{}d ago", secs / 86_400),
83    }
84}
85
86/// Compact duration for a millisecond span, e.g. "12m 30s".
87pub fn fmt_duration_ms(ms: u64) -> String {
88    let secs = ms / 1000;
89    if secs < 60 {
90        format!("{}s", secs)
91    } else if secs < 3600 {
92        format!("{}m {}s", secs / 60, secs % 60)
93    } else {
94        format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
95    }
96}
97
98#[derive(Debug, Clone)]
99pub enum Shape {
100    List(Vec<ListItem>),
101    Table(TableData),
102    Badge(BadgeData),
103    Text(String),
104}
105
106#[derive(Debug, Clone, Default)]
107pub struct ListItem {
108    pub name: String,
109    pub status: Status,
110    pub detail: Option<String>,
111    /// Resource id used to fetch the full detail view.
112    pub id: Option<String>,
113    /// Recent run results, oldest first — rendered as a ✓/✗ strip.
114    pub history: Vec<Status>,
115}
116
117impl Default for Status {
118    fn default() -> Self {
119        Status::Unknown(String::new())
120    }
121}
122
123/// Case-insensitive substring match against an item's name, detail text
124/// and status label — so `/running` greps for running things.
125pub fn item_matches(item: &ListItem, query: &str) -> bool {
126    if query.is_empty() {
127        return true;
128    }
129    let q = query.to_lowercase();
130    item.name.to_lowercase().contains(&q)
131        || item
132            .detail
133            .as_deref()
134            .is_some_and(|d| d.to_lowercase().contains(&q))
135        || item.status.label().to_lowercase().contains(&q)
136}
137
138/// Structured content for the item detail view.
139#[derive(Debug, Clone)]
140pub struct DetailData {
141    /// Key/value facts shown at the top.
142    pub summary: Vec<(String, String)>,
143    /// Recent events or runs, each with a status for the colored dot.
144    pub activity: Vec<(Status, String)>,
145    /// Full pretty-printed JSON, shown via the raw toggle.
146    pub raw: String,
147}
148
149#[derive(Debug, Clone)]
150pub struct TableData {
151    pub headers: Vec<String>,
152    pub rows: Vec<Vec<String>>,
153}
154
155impl TableData {
156    /// RFC-4180-ish CSV: fields with commas, quotes or newlines are
157    /// quoted, quotes doubled.
158    pub fn to_csv(&self) -> String {
159        fn field(s: &str) -> String {
160            if s.contains([',', '"', '\n', '\r']) {
161                format!("\"{}\"", s.replace('"', "\"\""))
162            } else {
163                s.to_string()
164            }
165        }
166        let mut out = String::new();
167        out.push_str(
168            &self
169                .headers
170                .iter()
171                .map(|h| field(h))
172                .collect::<Vec<_>>()
173                .join(","),
174        );
175        out.push('\n');
176        for row in &self.rows {
177            out.push_str(&row.iter().map(|c| field(c)).collect::<Vec<_>>().join(","));
178            out.push('\n');
179        }
180        out
181    }
182}
183
184#[derive(Debug, Clone)]
185pub struct BadgeData {
186    pub label: String,
187    pub value: String,
188}