use GORBIE::prelude::CardCtx;
use GORBIE::themes::colorhash;
use triblespace::core::id::Id;
use super::storage::WidgetReader as PileSnapshot;
use crate::relations::{self as native_relations, Head};
use crate::status as native_status;
use crate::storage::FactArchive;
use crate::widgets::storage::{DatasetRevision, DatasetView};
fn color_muted(ui: &egui::Ui) -> egui::Color32 {
if ui.visuals().dark_mode {
egui::Color32::from_rgb(0x9a, 0x9a, 0x9a)
} else {
egui::Color32::from_rgb(0x6a, 0x6a, 0x6a)
}
}
fn color_frame(ui: &egui::Ui) -> egui::Color32 {
if ui.visuals().dark_mode {
egui::Color32::from_rgb(0x29, 0x32, 0x36)
} else {
egui::Color32::from_rgb(0xec, 0xec, 0xec)
}
}
fn window_color(id: Id) -> egui::Color32 {
colorhash::ral_categorical(id.as_ref())
}
#[derive(Clone, Debug)]
struct WindowStatus {
window: Id,
name: String,
text: String,
at_ns: i128,
}
struct StatusLive {
cached_revision: DatasetRevision,
relations_cached_revision: Option<DatasetRevision>,
windows: Vec<WindowStatus>,
}
impl StatusLive {
fn refresh(view: DatasetView<'_>, relations: Option<DatasetView<'_>>) -> Self {
let relations_cached_revision = relations.map(|relations| relations.revision);
let latest = native_status::latest_per_window(
native_status::load_status_rows(view.facts)
.expect("Viewer storage exposed an invalid Status collection"),
)
.expect("Viewer storage exposed ambiguous Status identities");
let mut keyed = latest
.into_values()
.map(|row| {
let at_ns = native_status::point_timestamp(row.at)
.expect("validated Status time is a point interval");
(
(at_ns, row.event),
WindowStatus {
window: row.window,
name: relations
.map(|relations| {
native_window_label(view.reader, relations.facts, row.window)
})
.unwrap_or_else(|| id_hex(row.window)),
text: native_status::read_text(view.reader, row.text)
.expect("validated Status text is resident"),
at_ns,
},
)
})
.collect::<Vec<_>>();
keyed.sort_by(|left, right| {
right
.0
.cmp(&left.0)
.then_with(|| left.1.name.cmp(&right.1.name))
});
let windows = keyed.into_iter().map(|(_, window)| window).collect();
StatusLive {
cached_revision: view.revision,
relations_cached_revision,
windows,
}
}
}
fn native_window_label(reader: &PileSnapshot, facts: &FactArchive, window: Id) -> String {
if !native_relations::person_anchors(facts).contains(&window) {
return id_hex(window);
}
let mut label = match native_relations::profile_head(facts, window)
.expect("Viewer storage exposed invalid Relations profile state")
{
Head::Unique(profile) => {
let snapshot = native_relations::profile_snapshot(facts, profile)
.expect("validated Relations profile is readable");
native_relations::read_text(reader, snapshot.label)
.expect("validated Relations label is resident")
}
Head::Forked(heads) => {
return format!("{} [profile fork: {} heads]", id_hex(window), heads.len());
}
Head::Missing => return format!("{} [missing profile]", id_hex(window)),
};
match native_relations::lifecycle_head(facts, window)
.expect("Viewer storage exposed invalid Relations lifecycle state")
{
Head::Forked(heads) => label.push_str(&format!(" [lifecycle fork: {} heads]", heads.len())),
Head::Missing => label.push_str(" [missing lifecycle]"),
Head::Unique(_) => {}
}
label
}
fn id_hex(id: Id) -> String {
format!("{id:x}")
}
fn now_tai_ns() -> Option<i128> {
crate::clock::tai_nanoseconds_now().ok()
}
fn age_label(now: Option<i128>, at: i128) -> String {
let Some(now) = now else {
return "unknown".to_owned();
};
let secs = ((now - at) / 1_000_000_000).max(0);
if secs < 60 {
format!("{secs}s")
} else if secs < 3_600 {
format!("{}m", secs / 60)
} else if secs < 86_400 {
format!("{}h", secs / 3_600)
} else if secs < 7 * 86_400 {
format!("{}d", secs / 86_400)
} else if secs < 30 * 86_400 {
format!("{}w", secs / (7 * 86_400))
} else {
format!("{}mo", secs / (30 * 86_400))
}
}
pub struct StatusViewer {
live: Option<StatusLive>,
}
impl Default for StatusViewer {
fn default() -> Self {
Self { live: None }
}
}
impl StatusViewer {
pub fn new() -> Self {
Self::default()
}
pub fn render(
&mut self,
ctx: &mut CardCtx<'_>,
view: DatasetView<'_>,
relations: Option<DatasetView<'_>>,
) {
let revision = view.revision;
let relations_revision = relations.map(|view| view.revision);
let need_refresh = match self.live.as_ref() {
None => true,
Some(l) => {
l.cached_revision != revision || l.relations_cached_revision != relations_revision
}
};
if need_refresh {
self.live = Some(StatusLive::refresh(view, relations));
}
ctx.section("Status", |ctx| {
let Some(live) = self.live.as_ref() else {
return;
};
let now = now_tai_ns();
ctx.grid(|g| {
g.full(|ctx| {
let ui = ctx.ui_mut();
let n = live.windows.len();
let newest = live
.windows
.first()
.map(|w| age_label(now, w.at_ns))
.unwrap_or_else(|| "-".to_string());
ui.label(
egui::RichText::new(format!(
"{n} WINDOW{} · NEWEST {}",
if n == 1 { "" } else { "S" },
newest.to_uppercase(),
))
.monospace()
.strong()
.small()
.color(color_muted(ui)),
);
});
if live.windows.is_empty() {
g.full(|ctx| {
let ui = ctx.ui_mut();
ui.add_space(16.0);
ui.vertical_centered(|ui| {
ui.label(
egui::RichText::new("\u{1F4CD}") .size(28.0)
.color(color_muted(ui)),
);
ui.add_space(4.0);
ui.label(
egui::RichText::new("No status set yet.")
.monospace()
.small()
.strong()
.color(color_muted(ui)),
);
ui.add_space(2.0);
ui.label(
egui::RichText::new(
"windows appear here when they `status set \"<text>\"`.",
)
.monospace()
.small()
.color(color_muted(ui)),
);
});
ui.add_space(16.0);
});
return;
}
for w in &live.windows {
g.full(|ctx| {
render_status_row(ctx.ui_mut(), w, now);
});
}
});
});
}
}
fn render_status_row(ui: &mut egui::Ui, w: &WindowStatus, now: Option<i128>) {
let accent = window_color(w.window);
let muted = color_muted(ui);
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 6.0;
let (dot, _) = ui.allocate_exact_size(egui::vec2(10.0, 10.0), egui::Sense::hover());
ui.painter()
.rect_filled(dot, egui::CornerRadius::ZERO, accent);
ui.label(
egui::RichText::new(&w.name)
.monospace()
.strong()
.size(13.0)
.color(ui.visuals().text_color()),
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(
egui::RichText::new(age_label(now, w.at_ns))
.monospace()
.small()
.color(muted),
);
});
});
let text = if w.text.trim().is_empty() {
"(empty status)".to_string()
} else {
w.text.clone()
};
ui.horizontal_top(|ui| {
ui.add_space(16.0); ui.vertical(|ui| {
ui.add(
egui::Label::new(
egui::RichText::new(text)
.size(13.0)
.color(ui.visuals().text_color()),
)
.wrap(),
);
});
});
ui.add_space(4.0);
let sep_y = ui.cursor().min.y;
let x = ui.min_rect().x_range();
ui.painter()
.hline(x, sep_y, egui::Stroke::new(1.0, color_frame(ui)));
}