Skip to main content

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}