Skip to main content

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}