hatchery_session_events/
engine.rs1use crate::event::*;
2use crate::providers::{parser_for, Item, Parsed, ProviderParser};
3use std::collections::{hash_map::DefaultHasher, HashMap};
4use std::hash::{Hash, Hasher};
5
6const STABLE_MS: u64 = 350;
8const QUIET_MS: u64 = 1200;
10
11struct Slot {
12 id: u64,
13 emitted_hash: Option<u64>,
14 rev: u32,
15 cand_hash: u64,
16 cand_since: u64,
17}
18
19pub struct EventStream {
21 provider: String,
22 parser: Box<dyn ProviderParser>,
23 slots: HashMap<String, Slot>,
24 next_id: u64,
25 last_items: Vec<Item>,
26 last_parsed: Parsed,
27 screen_hash: u64,
28 last_change: u64,
29 state: Option<(SessionState, String)>,
30 state_id: u64,
31 state_rev: u32,
32 emu: Option<vt100::Parser>,
33 size: (u16, u16),
34}
35
36fn h<T: Hash + ?Sized>(t: &T) -> u64 {
37 let mut s = DefaultHasher::new();
38 t.hash(&mut s);
39 s.finish()
40}
41
42impl EventStream {
43 pub fn new(provider: &str) -> Self {
44 Self::with_parser(provider, parser_for(provider))
45 }
46
47 pub fn with_parser(provider: &str, parser: Box<dyn ProviderParser>) -> Self {
48 Self {
49 provider: provider.to_owned(), parser, slots: HashMap::new(), next_id: 1, last_items: Vec::new(), last_parsed: Parsed::default(),
50 screen_hash: 0, last_change: 0, state: None, state_id: 0, state_rev: 0, emu: None, size: (30, 100),
51 }
52 }
53
54 pub fn set_size(&mut self, rows: u16, cols: u16) {
55 self.size = (rows, cols);
56 if let Some(e) = self.emu.as_mut() { e.screen_mut().set_size(rows, cols); }
57 }
58
59 pub fn feed_bytes(&mut self, bytes: &[u8], now_ms: u64) -> Vec<SessionEvent> {
61 let (r, c) = self.size;
62 let emu = self.emu.get_or_insert_with(|| vt100::Parser::new(r, c, 0));
63 emu.process(bytes);
64 let text = emu.screen().contents();
65 self.observe_screen(&text, now_ms)
66 }
67
68 pub fn observe_screen(&mut self, text: &str, now_ms: u64) -> Vec<SessionEvent> {
70 let rows: Vec<String> = text.lines().map(|l| l.trim_end().to_owned()).collect();
71 let hash = h(&rows);
72 if hash != self.screen_hash {
73 self.screen_hash = hash;
74 self.last_change = now_ms;
75 }
76 let parsed = self.parser.parse(&rows);
77 self.last_items = parsed.items.clone();
78 self.last_parsed = parsed;
79 self.settle(now_ms)
80 }
81
82 pub fn tick(&mut self, now_ms: u64) -> Vec<SessionEvent> {
84 self.settle(now_ms)
85 }
86
87 fn settle(&mut self, now_ms: u64) -> Vec<SessionEvent> {
88 let mut out = Vec::new();
89 let items = std::mem::take(&mut self.last_items);
90 for it in &items {
91 let hash = h(&it.kind);
92 let next_id = &mut self.next_id;
93 let slot = self.slots.entry(it.key.clone()).or_insert_with(|| {
94 let id = *next_id;
95 *next_id += 1;
96 Slot { id, emitted_hash: None, rev: 0, cand_hash: hash, cand_since: now_ms }
97 });
98 if slot.cand_hash != hash {
99 slot.cand_hash = hash;
100 slot.cand_since = now_ms;
101 }
102 if now_ms.saturating_sub(slot.cand_since) >= STABLE_MS && slot.emitted_hash != Some(hash) {
103 slot.emitted_hash = Some(hash);
104 slot.rev += 1;
105 out.push(SessionEvent { id: slot.id, rev: slot.rev, provider: self.provider.clone(), source: format!("pty:{}", self.provider), confidence: Confidence::Heuristic, ts_ms: now_ms, event: it.kind.clone() });
106 }
107 }
108 self.last_items = items;
109 let p = &self.last_parsed;
111 let quiet = now_ms.saturating_sub(self.last_change) >= QUIET_MS;
112 let (state, detail) = if p.needs_input {
113 (SessionState::NeedsInput, p.prompt_hint.clone())
114 } else if let Some(d) = &p.working {
115 (SessionState::Working, d.clone())
116 } else if self.state.is_none() && quiet && self.last_items.is_empty() {
117 (SessionState::Starting, String::new())
118 } else if !quiet {
119 (SessionState::Working, "output changing".to_owned())
120 } else {
121 (SessionState::Idle, String::new())
122 };
123 if self.state.as_ref().map(|s| (s.0, &s.1)) != Some((state, &detail)) {
124 if self.state.is_none() { self.state_id = self.next_id; self.next_id += 1; }
125 self.state_rev += 1;
126 self.state = Some((state, detail.clone()));
127 out.push(SessionEvent { id: self.state_id, rev: self.state_rev, provider: self.provider.clone(), source: format!("pty:{}", self.provider), confidence: Confidence::Heuristic, ts_ms: now_ms, event: EventKind::State { state, detail } });
128 }
129 out
130 }
131
132 pub fn state(&self) -> Option<SessionState> { self.state.as_ref().map(|s| s.0) }
133}