1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// SPDX-License-Identifier: MIT OR Apache-2.0
//! 2026-09-26: A recipe's "updated" date: its `metadata.updated`, else the file's last GitHub commit.
//!
//! The GitHub lookup is lazy: only the recipe whose details are on screen,
//! only when it has no date of its own, and only once per id, failures
//! included. It runs on a worker thread (`recipe::fetch::updated_in_background`);
//! the render thread only `try_recv`s.
//!
//! Owner: server tui.
//! Invariants:
//! - At most one lookup is in flight (`pending_date`), and an id enters
//! `dated` before its lookup starts and never leaves it.
//! - A recipe's own `metadata.updated` wins over a fetched date, and a
//! starting-point card shows no date.
use super::lib_state::{LibState, View};
use crate::recipe::fetch;
impl LibState {
/// 2026-09-26: The `updated` row's value for one recipe, used by both detail panes.
///
/// In order: empty for a starting point; `metadata.updated`; a date in
/// `fetched_dates`; a skeleton while this id's lookup is in flight; else
/// empty, which the panes render as no row.
///
/// Fetched dates live in their own map because `LibState::poll` replaces
/// `index` wholesale on a refresh.
pub fn date_text(&self, recipe: &crate::recipe::Recipe) -> String {
// 2026-09-26: A starting point carries the donor's id, so a lookup would date the donor's file.
if recipe.starting_point.is_some() {
return String::new();
}
if !recipe.updated.is_empty() {
return recipe.updated.clone();
}
if let Some(d) = self.fetched_dates.get(&recipe.id) {
return d.clone();
}
if self.dating.as_deref() == Some(recipe.id.as_str()) {
// 2026-09-26: Ten cells, the width of a `YYYY-MM-DD` date.
return "░░░░░░░░░░".to_string();
}
String::new()
}
/// 2026-09-26: The recipe whose details are on screen: the row's `primary()` in List, the selected card
/// in Cards and Config; never a starting point.
pub fn visible_recipe_id(&self) -> Option<String> {
match self.view {
View::List => self
.current()
.and_then(|e| e.primary())
.map(|r| r.id.clone()),
View::Cards | View::Config => self
.selected_card()
.filter(|r| r.starting_point.is_none())
.map(|r| r.id.clone()),
}
}
/// 2026-09-26: Start a GitHub date lookup for `id`, if no lookup is in flight, `id` was never looked up,
/// and the index holds `id` with an empty `metadata.updated`.
///
/// The event loop calls it every tick while the Library is polled.
pub fn want_date_for(&mut self, id: &str) {
if self.pending_date.is_some() || self.dated.contains(id) {
return;
}
let needs = self
.index
.recipes
.iter()
.any(|r| r.id == id && r.updated.is_empty());
if !needs {
return;
}
tracing::debug!("dating recipe {id} from GitHub commit history");
self.dated.insert(id.to_string());
self.dating = Some(id.to_string());
self.pending_date = Some(fetch::updated_in_background(id));
}
/// 2026-09-26: Collect a finished date lookup into `fetched_dates`; true when a non-empty date arrived.
pub fn poll_date(&mut self) -> bool {
let Some(rx) = &self.pending_date else {
return false;
};
match rx.try_recv() {
Ok((id, date)) => {
self.pending_date = None;
self.dating = None;
// 2026-09-26: Keyed by the id the worker echoed back, not the current selection.
if let Some(d) = date.filter(|d| !d.is_empty()) {
self.fetched_dates.insert(id, d);
return true;
}
false
}
Err(std::sync::mpsc::TryRecvError::Empty) => false,
// 2026-09-26: The worker died without sending; the id stays in `dated`, so it is not retried.
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
self.pending_date = None;
self.dating = None;
false
}
}
}
}
#[cfg(test)]
#[path = "lib_dates_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "lib_dates_more_tests.rs"]
mod more_tests;