heatsync_core/lib.rs
1//! heatsync-core — the brain. protocol types, heat ramp, emote resolution, ws
2//! parsing, mock feed, and the editing model. no terminal, no async, no i/o
3//! (clocks and keystrokes are passed in). every client is a thin face over this.
4//!
5//! The editing half (`key`, `edit`, `vi`, `slash`, `clip`) lived in the tui
6//! until a second face needed it. None of it ever touched the terminal except
7//! for crossterm's key type, which `key` now replaces — a face maps its own
8//! keystrokes into `key::KeyEvent` and gets the same editor.
9
10pub mod clip;
11pub mod edit;
12pub mod emote;
13pub mod heat;
14pub mod key;
15pub mod mock;
16pub mod proto;
17pub mod sanitize;
18pub mod slash;
19pub mod vi;
20
21use std::collections::VecDeque;
22
23/// where a channel's chat comes from.
24#[derive(Clone, Copy, PartialEq, Eq, Debug)]
25pub enum Platform {
26 Twitch,
27 Kick,
28}
29
30impl Platform {
31 pub fn tag(self) -> &'static str {
32 match self {
33 Platform::Twitch => "tw",
34 Platform::Kick => "kk",
35 }
36 }
37}
38
39/// one chat line. `heat` is the channel's heat snapshotted at arrival, so a
40/// message sent during a spike stays warm in scrollback. `color` is the user's
41/// chat color (hex) if the platform sent one.
42#[derive(Clone, Debug)]
43pub struct Message {
44 pub user: String,
45 pub text: String,
46 pub color: Option<String>,
47 pub heat: f64,
48}
49
50/// live state for a single channel. bounded ring buffer — a raid can flood
51/// forever and we never grow past `cap` (mele is 8GB; resource-conscious).
52/// heat is a decaying counter driven by message arrivals.
53#[derive(Clone, Debug)]
54pub struct Channel {
55 pub name: String,
56 pub platform: Platform,
57 pub heat: f64,
58 /// wall-clock ms of the last heat update — decay is computed against it.
59 pub last_ms: u64,
60 pub messages: VecDeque<Message>,
61 cap: usize,
62}
63
64impl Channel {
65 pub fn new(name: &str, platform: Platform, cap: usize) -> Channel {
66 Channel {
67 name: name.to_string(),
68 platform,
69 heat: 0.0,
70 last_ms: 0,
71 messages: VecDeque::with_capacity(cap),
72 cap,
73 }
74 }
75
76 /// advance heat decay to `now_ms` without adding anything (idle cooling).
77 pub fn cool(&mut self, now_ms: u64) {
78 if self.last_ms == 0 {
79 self.last_ms = now_ms;
80 return;
81 }
82 let dt = now_ms.saturating_sub(self.last_ms) as f64;
83 self.heat = heat::decay(self.heat, dt);
84 self.last_ms = now_ms;
85 }
86
87 /// record a message: decay to now, add one increment, snapshot heat onto
88 /// the line, then store it (evicting the oldest past the cap).
89 pub fn record(&mut self, mut msg: Message, now_ms: u64) {
90 self.cool(now_ms);
91 self.heat += heat::INCREMENT;
92 msg.heat = self.heat;
93 if self.messages.len() == self.cap {
94 self.messages.pop_front();
95 }
96 self.messages.push_back(msg);
97 }
98}