| @@ -122,15 +122,22 @@ pub fn pet_from_signals(signals: &[Signal], now_ms: u64) -> PetState { |
| 122 | 122 | let battery_pct = read(SignalName::BatteryPct); |
| 123 | 123 | let charging = read(SignalName::Charging); |
| 124 | 124 | |
| 125 | | let plugged_in = charging.map(|c| c.value >= 1.0).unwrap_or(false); |
| 125 | // Mood is driven only by readings that are still current. A cached value |
| 126 | // outlives what it described: the terminal collector stops publishing once |
| 127 | // a session falls outside its active window, so the last keystroke rate |
| 128 | // sits in the daemon's cache indefinitely and would otherwise leave the pet |
| 129 | // looking busy long after the typing stopped. |
| 130 | let fresh = |r: Option<Reading>| r.filter(|r| !r.stale).map(|r| r.value); |
| 131 | |
| 132 | let plugged_in = fresh(charging).map(|c| c >= 1.0).unwrap_or(false); |
| 126 | 133 | let mood = if !down.is_empty() { |
| 127 | 134 | Mood::Sick |
| 128 | | } else if thermal_state.map(|t| t.value >= HOT_THERMAL_STATE).unwrap_or(false) { |
| 135 | } else if fresh(thermal_state).map(|t| t >= HOT_THERMAL_STATE).unwrap_or(false) { |
| 129 | 136 | Mood::Overheating |
| 130 | | } else if battery_pct.map(|b| b.value < LOW_BATTERY_PCT).unwrap_or(false) && !plugged_in { |
| 137 | } else if fresh(battery_pct).map(|b| b < LOW_BATTERY_PCT).unwrap_or(false) && !plugged_in { |
| 131 | 138 | Mood::LowBattery |
| 132 | 139 | } else { |
| 133 | | match keys_per_min.map(|k| k.value) { |
| 140 | match fresh(keys_per_min) { |
| 134 | 141 | Some(k) if k >= BUSY_KEYS_PER_MIN => Mood::Busy, |
| 135 | 142 | Some(k) if k > 0.0 => Mood::Calm, |
| 136 | 143 | _ => Mood::Sleeping, |
| @@ -181,6 +188,21 @@ pub fn render(state: &PetState) -> String { |
| 181 | 188 | out |
| 182 | 189 | } |
| 183 | 190 | |
| 191 | /// Render the pet as a single line for a shell prompt: the face, and the |
| 192 | /// keystroke rate when the terminal collector has one. |
| 193 | /// |
| 194 | /// Deliberately short and fixed-ish in width — it shares a prompt with |
| 195 | /// everything else, and a segment that changes width every keystroke moves the |
| 196 | /// rest of the prompt around. |
| 197 | pub fn oneline(state: &PetState) -> String { |
| 198 | match state.keys_per_min { |
| 199 | Some(k) if !k.stale => format!("{} {:.0}k/m", state.mood.face(), k.value), |
| 200 | // A stale or absent rate is not worth a number: it would read as |
| 201 | // current and be minutes old. |
| 202 | _ => state.mood.face().to_string(), |
| 203 | } |
| 204 | } |
| 205 | |
| 184 | 206 | #[cfg(test)] |
| 185 | 207 | mod tests { |
| 186 | 208 | use super::*; |
| @@ -203,6 +225,28 @@ mod tests { |
| 203 | 225 | sig(Source::Terminal, SignalName::KeysPerMin, keys, NOW) |
| 204 | 226 | } |
| 205 | 227 | |
| 228 | #[test] |
| 229 | fn oneline_is_the_face_and_the_rate() { |
| 230 | let pet = pet_from_signals(&[typing(42.4)], NOW); |
| 231 | assert_eq!(oneline(&pet), "(^_^) 42k/m"); |
| 232 | } |
| 233 | |
| 234 | #[test] |
| 235 | fn oneline_drops_a_rate_it_cannot_vouch_for() { |
| 236 | let old = sig(Source::Terminal, SignalName::KeysPerMin, 99.0, NOW - STALE_AFTER_MS - 1); |
| 237 | assert_eq!(oneline(&pet_from_signals(&[old], NOW)), "(-.-)", "stale rate omitted"); |
| 238 | assert_eq!(oneline(&pet_from_signals(&[], NOW)), "(-.-)", "no rate at all"); |
| 239 | } |
| 240 | |
| 241 | #[test] |
| 242 | fn oneline_shows_a_sick_face() { |
| 243 | let signals = [ |
| 244 | typing(10.0), |
| 245 | sig(Source::Hardware, SignalName::CollectorUp, 0.0, NOW), |
| 246 | ]; |
| 247 | assert_eq!(oneline(&pet_from_signals(&signals, NOW)), "(x_x) 10k/m"); |
| 248 | } |
| 249 | |
| 206 | 250 | #[test] |
| 207 | 251 | fn typing_drives_energy() { |
| 208 | 252 | assert_eq!(pet_from_signals(&[typing(0.0)], NOW).mood, Mood::Sleeping); |
| @@ -278,6 +322,20 @@ mod tests { |
| 278 | 322 | assert_eq!(pet.down, vec![Source::Terminal, Source::Hardware]); |
| 279 | 323 | } |
| 280 | 324 | |
| 325 | /// The terminal collector stops publishing once a session falls outside |
| 326 | /// its active window, so the last rate sits in the cache. The pet must not |
| 327 | /// keep looking busy on the strength of it. |
| 328 | #[test] |
| 329 | fn a_stale_rate_does_not_keep_the_pet_busy() { |
| 330 | let old = sig( |
| 331 | Source::Terminal, |
| 332 | SignalName::KeysPerMin, |
| 333 | 300.0, |
| 334 | NOW - STALE_AFTER_MS - 1, |
| 335 | ); |
| 336 | assert_eq!(pet_from_signals(&[old], NOW).mood, Mood::Sleeping); |
| 337 | } |
| 338 | |
| 281 | 339 | /// Up but stalled is not the same as down: the collector never reported a |
| 282 | 340 | /// failure, the number behind it simply stopped moving. |
| 283 | 341 | #[test] |