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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
//! Palace activity classification, spinner, and colour helpers.
//!
//! Why: operators want to see at a glance which memory palaces are doing
//! something (indexing, dreaming, recently active) vs. idle. Centralising the
//! derivation, the spinner-frame lookup, and the colour mapping keeps the left
//! pane and the detail panel agreeing on what each palace is doing while
//! staying pure and unit-testable.
//! What: [`palace_activity`] bins a [`CollectionRow`]'s write recency into a
//! [`PalaceActivity`]; [`spinner_frame`] and [`activity_color`] map that state
//! to a glyph and colour; [`current_spinner_tick`] derives a wall-clock tick so
//! the renderer can animate without threading state.
//! Test: `palace_activity_from_recent_write`, `spinner_frame_for_each_state`,
//! `activity_colour_is_distinct_per_state`.
use Color;
use crate;
/// Threshold below which a palace is considered actively indexing.
///
/// Why: a write timestamp newer than this almost certainly reflects an
/// in-flight ingestion path — the operator should see the spinner.
/// What: 10 seconds.
/// Test: `palace_activity_from_recent_write`.
const INDEXING_WINDOW_SECS: i64 = 10;
/// Threshold below which a palace is considered "recently active".
///
/// Why: writes within the last minute are still relevant to the operator
/// even if the ingestion path has finished; the cyan indicator highlights
/// the row without animating it.
/// What: 60 seconds.
/// Test: `palace_activity_from_recent_write`.
const ACTIVE_WINDOW_SECS: i64 = 60;
/// Frames of the indexing spinner (the canonical braille rotation).
///
/// Why: a rotating spinner communicates "this is changing right now" without
/// reading a label. The braille frames are the same set ratatui's `Throbber`
/// uses, kept inline here so the spinner stays self-contained.
/// What: ten frames cycled at ~10 Hz by the render loop.
/// Test: `spinner_frame_cycles_through_indexing_frames`.
pub const INDEXING_SPINNER: & = &;
/// Frames of the dreaming/compacting spinner.
///
/// Why: a heavier glyph set distinguishes the compaction state from
/// indexing at a glance.
/// What: eight frames cycled at ~10 Hz by the render loop.
/// Test: `spinner_frame_cycles_through_dreaming_frames`.
pub const DREAMING_SPINNER: & = &;
/// Derive a [`PalaceActivity`] from a collection row.
///
/// Why: the indicator/colour mapping needs one source of truth so the left
/// pane and the (future) detail panel agree on what each palace is doing.
/// What: returns `Error` for an unhealthy row, otherwise parses
/// `last_write_at` and bins the resulting age against the [`INDEXING_WINDOW_SECS`]
/// / [`ACTIVE_WINDOW_SECS`] thresholds. A missing or unparseable timestamp
/// yields `Idle`.
/// Test: `palace_activity_from_recent_write`.
/// Pick a spinner / indicator character for a palace activity state.
///
/// Why: the left pane prefixes each row with a single glyph; centralising
/// the lookup keeps the spinner-frame arithmetic in one place and lets the
/// renderer stay terse.
/// What: returns `None` for `Idle` (no prefix), `Some('✗')` for `Error`, a
/// static `Some('⠿')` for `Active`, and a rotating frame for `Indexing` /
/// `Dreaming` selected by `tick % frames.len()`.
/// Test: `spinner_frame_for_each_state`,
/// `spinner_frame_cycles_through_indexing_frames`.
/// Pick the colour for a palace activity state.
///
/// Why: alongside the glyph, each row carries an activity-driven colour so
/// the operator can scan the pane at a glance.
/// What: maps `Idle` to default (`Reset`), `Indexing` to yellow, `Dreaming`
/// to magenta, `Active` to cyan, and `Error` to red.
/// Test: `activity_colour_is_distinct_per_state`.
/// Compute the current spinner-frame tick from wall-clock time.
///
/// Why: the render path is otherwise pure — passing a tick from the event
/// loop would require threading state through every call site. Deriving the
/// tick from wall time keeps the renderer self-contained while still
/// animating predictably.
/// What: returns the number of 100 ms slots elapsed since the unix epoch,
/// modulo a large constant so it stays a small `usize`. The render path
/// re-evaluates this every frame.
/// Test: covered indirectly by `spinner_frame_cycles_through_indexing_frames`
/// — the modular arithmetic is enough to ensure rotation.
pub