par_term_scripting/plugin_manager/overlay.rs
1//! Overlay focus, event delivery, and the `SetOverlay` update-rate clamp for
2//! [`super::PluginHost`].
3//!
4//! Split out of `mod.rs` to keep it under the 800-line production limit.
5//! [`clear_focus_if`](PluginHost::clear_focus_if) and
6//! [`overlay_update_permitted`](PluginHost::overlay_update_permitted) are
7//! `pub(super)` because the supervisor's command drain and teardown call them.
8
9use std::collections::HashMap;
10use std::time::Instant;
11
12use crate::protocol::{ScriptEvent, ScriptEventData};
13
14use super::PluginHost;
15
16impl PluginHost {
17 /// Live plugin overlays (plugin id → overlay), for the render layer.
18 pub fn overlays(&self) -> &HashMap<String, crate::protocol::PluginOverlay> {
19 &self.overlays
20 }
21
22 /// The focused overlay's plugin id, if any — the render layer's
23 /// click-to-focus target check and the key router's focus check.
24 pub fn focused_overlay(&self) -> Option<&str> {
25 self.focused_overlay.as_deref()
26 }
27
28 /// Focus one plugin's overlay (the render layer's click path). Only an
29 /// overlay that is live AND interactive can take focus — everything
30 /// else clears focus, so the call doubles as the unfocus path.
31 pub fn focus_overlay(&mut self, plugin_id: &str) {
32 self.focused_overlay = self
33 .overlays
34 .get(plugin_id)
35 .filter(|o| o.interactive)
36 .map(|_| plugin_id.to_string());
37 }
38
39 /// Drop overlay focus entirely (Escape path; also called when the
40 /// focused overlay stops being focusable).
41 pub fn unfocus_overlay(&mut self) {
42 self.focused_overlay = None;
43 }
44
45 /// Clear focus when it names this plugin's overlay (the upsert and
46 /// teardown paths) — other plugins' focus is untouched.
47 pub(super) fn clear_focus_if(&mut self, plugin_id: &str) {
48 if self.focused_overlay.as_deref() == Some(plugin_id) {
49 self.focused_overlay = None;
50 }
51 }
52
53 /// Deliver one semantic overlay-widget event to a plugin's running
54 /// overlay process (the renderer's interaction sink). Returns false
55 /// without warning when there is nothing to deliver to — the renderer
56 /// only calls this for a focused overlay it just drew, so a miss means
57 /// the process raced an exit, which the supervisor reports.
58 pub fn send_overlay_event(
59 &mut self,
60 plugin_id: &str,
61 widget_id: &str,
62 event: crate::protocol::OverlayWidgetEvent,
63 ) -> bool {
64 let Some(&sid) = self.overlay_running.get(plugin_id) else {
65 return false;
66 };
67 let Some(overlay) = self.overlays.get(plugin_id) else {
68 return false;
69 };
70 let script_event = ScriptEvent {
71 kind: crate::protocol::OVERLAY_EVENT_KIND.to_string(),
72 data: ScriptEventData::OverlayEvent {
73 overlay: overlay.id.clone(),
74 widget: widget_id.to_string(),
75 event,
76 },
77 };
78 match self.manager.send_event(sid, &script_event) {
79 Ok(()) => {
80 self.overlay_events_dispatched += 1;
81 true
82 }
83 Err(error) => {
84 if self.warned_event_delivery.should_warn(plugin_id) {
85 log::warn!(
86 "failed to deliver overlay event to plugin '{}': {}",
87 plugin_id,
88 error
89 );
90 }
91 false
92 }
93 }
94 }
95
96 /// Successful overlay-event stdin writes this session — the
97 /// `plugin_overlay_event` ui-test operand's source. Monotonic.
98 pub fn overlay_events_dispatched_count(&self) -> u64 {
99 self.overlay_events_dispatched
100 }
101
102 /// The SetOverlay rate clamp's budget: accepted upserts per sliding
103 /// second (design names ~30/sec with a warning).
104 const OVERLAY_UPDATE_WINDOW_SECS: u64 = 1;
105 const OVERLAY_UPDATE_MAX_PER_WINDOW: usize = 30;
106
107 /// Whether one more `SetOverlay` from this plugin lands inside the
108 /// update-rate budget; records the attempt either way. Over-budget
109 /// upserts are dropped after the first, which warns once per episode
110 /// (the episode resets after a full quiet second, so a plugin that
111 /// bursts, stops, and bursts again warns once per burst).
112 pub(super) fn overlay_update_permitted(&mut self, id: &str, now: Instant) -> bool {
113 let times = self.overlay_update_times.entry(id.to_string()).or_default();
114 times.retain(|t| now.duration_since(*t).as_secs() < Self::OVERLAY_UPDATE_WINDOW_SECS);
115 if times.len() >= Self::OVERLAY_UPDATE_MAX_PER_WINDOW {
116 if self
117 .warned_event_delivery
118 .should_warn(&format!("{id}/overlay-rate"))
119 {
120 log::warn!(
121 "plugin '{id}' is pushing SetOverlay faster than {} per second; \
122 dropping the excess (update-rate clamp)",
123 Self::OVERLAY_UPDATE_MAX_PER_WINDOW
124 );
125 }
126 return false;
127 }
128 times.push(now);
129 true
130 }
131}