• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jzombie / term-wm / 20885255367

10 Jan 2026 10:20PM UTC coverage: 57.056% (+10.0%) from 47.071%
20885255367

Pull #20

github

web-flow
Merge 123a43984 into bfecf0f75
Pull Request #20: Initial clipboard and offscreen buffer support

2045 of 3183 new or added lines in 26 files covered. (64.25%)

76 existing lines in 15 files now uncovered.

6788 of 11897 relevant lines covered (57.06%)

9.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.65
/src/components/sys/help_overlay.rs
1
use std::str;
2

3
use crossterm::event::Event;
4
use ratatui::layout::Rect;
5
use ratatui::widgets::{Block, Borders, Clear};
6

7
use crate::components::{
8
    Component, ComponentContext, DialogOverlayComponent, MarkdownViewerComponent,
9
    ScrollViewComponent,
10
};
11
use crate::keybindings::{Action, KeyBindings};
12
use crate::ui::UiFrame;
13

14
const HELP_CONTENT_BYTES: &[u8] =
15
    include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/help.md"));
16

17
#[derive(Debug)]
18
pub struct HelpOverlayComponent {
19
    dialog: DialogOverlayComponent,
20
    visible: bool,
21
    viewer: ScrollViewComponent<MarkdownViewerComponent>,
22
    area: Rect,
23
}
24

25
impl Component for HelpOverlayComponent {
NEW
26
    fn resize(&mut self, area: Rect, _ctx: &ComponentContext) {
×
27
        self.area = area;
×
28
    }
×
29

NEW
30
    fn render(&mut self, frame: &mut UiFrame<'_>, area: Rect, _ctx: &ComponentContext) {
×
31
        self.area = area;
×
32
        if !self.visible || area.width == 0 || area.height == 0 {
×
33
            return;
×
34
        }
×
35
        // If the dialog requests a dim backdrop, apply it across the full frame
36
        // before clearing and drawing the help dialog contents.
37
        self.dialog.render_backdrop(frame, area);
×
38
        let rect = self.dialog.rect_for(area);
×
39
        frame.render_widget(Clear, rect);
×
40
        let title = format!("{} — About / Help", env!("CARGO_PKG_NAME"));
×
41
        let block = Block::default().title(title).borders(Borders::ALL);
×
42
        let inner = Rect {
×
43
            x: rect.x.saturating_add(1),
×
44
            y: rect.y.saturating_add(1),
×
45
            width: rect.width.saturating_sub(2),
×
46
            height: rect.height.saturating_sub(2),
×
47
        };
×
48
        frame.render_widget(block, rect);
×
49
        // Overlays are not part of the standard focus ring, so they often
50
        // receive `focused=false`. Force the viewer to stay logically focused
51
        // so selection drags are preserved while the help dialog is visible.
NEW
52
        let viewer_ctx = self.viewer_context();
×
NEW
53
        self.viewer.render(frame, inner, &viewer_ctx);
×
UNCOV
54
    }
×
55

NEW
56
    fn handle_event(&mut self, event: &Event, ctx: &ComponentContext) -> bool {
×
NEW
57
        self.handle_help_event_in_area(event, self.area, ctx)
×
UNCOV
58
    }
×
59
}
60

61
impl HelpOverlayComponent {
62
    pub fn handle_help_event_in_area(
1✔
63
        &mut self,
1✔
64
        event: &Event,
1✔
65
        area: Rect,
1✔
66
        _ctx: &ComponentContext,
1✔
67
    ) -> bool {
1✔
68
        if !self.visible {
1✔
69
            return false;
×
70
        }
1✔
71
        match event {
1✔
72
            Event::Key(key) => {
×
73
                let kb = KeyBindings::default();
×
74
                if kb.matches(crate::keybindings::Action::CloseHelp, key) {
×
75
                    self.close();
×
76
                    true
×
77
                } else {
NEW
78
                    let viewer_ctx = self.viewer_context();
×
NEW
79
                    self.viewer.handle_event(event, &viewer_ctx)
×
80
                }
81
            }
82
            Event::Mouse(_) => {
83
                // If configured, allow clicking outside the dialog to auto-close it.
84
                if self.dialog.handle_click_outside(event, area) {
1✔
85
                    self.close();
1✔
86
                    return true;
1✔
87
                }
×
NEW
88
                let viewer_ctx = self.viewer_context();
×
NEW
89
                self.viewer.handle_event(event, &viewer_ctx)
×
90
            }
91
            _ => false,
×
92
        }
93
    }
1✔
94
}
95

96
impl HelpOverlayComponent {
97
    pub fn new() -> Self {
5✔
98
        let mut overlay = Self {
5✔
99
            dialog: DialogOverlayComponent::new(),
5✔
100
            visible: false,
5✔
101
            viewer: ScrollViewComponent::new(MarkdownViewerComponent::new()),
5✔
102
            area: Rect::default(),
5✔
103
        };
5✔
104
        overlay.dialog.set_size(70, 20);
5✔
105
        overlay.dialog.set_dim_backdrop(true);
5✔
106
        // allow clicking outside the help dialog to auto-close it
107
        overlay.dialog.set_auto_close_on_outside_click(true);
5✔
108
        overlay.dialog.set_bg(crate::theme::dialog_bg());
5✔
109
        // substitute package/version placeholders and set markdown
110
        if let Ok(raw) = str::from_utf8(HELP_CONTENT_BYTES) {
5✔
111
            // Build a compile-time platform string (OS/ARCH) to indicate the
112
            // target the binary was built for.
113
            // Use std::env::consts (which reflect the compilation target) to
114
            // build a concise platform identifier.
115
            let platform = format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH);
5✔
116
            let mut s = raw
5✔
117
                .replace("%PACKAGE%", env!("CARGO_PKG_NAME"))
5✔
118
                .replace("%VERSION%", env!("CARGO_PKG_VERSION"))
5✔
119
                .replace("%PLATFORM%", &platform)
5✔
120
                .replace("%REPOSITORY%", env!("CARGO_PKG_REPOSITORY"));
5✔
121

122
            // Replace placeholder tokens that allow the help file to
123
            // contain the descriptive text while only key combo strings are
124
            // produced here. This keeps the markdown authoritative and
125
            // avoids hardcoding user-visible sentences in code.
126
            let kb = KeyBindings::default();
5✔
127
            let focus_next = kb.combos_for(Action::FocusNext).join(" / ");
5✔
128
            let focus_prev = kb.combos_for(Action::FocusPrev).join(" / ");
5✔
129
            let new_win = kb.combos_for(Action::NewWindow).join(" / ");
5✔
130
            let menu_nav = {
5✔
131
                let a = kb.combos_for(Action::MenuNext).join(" / ");
5✔
132
                let b = kb.combos_for(Action::MenuPrev).join(" / ");
5✔
133
                format!("{} / {}", a, b)
5✔
134
            };
135
            let menu_alt = {
5✔
136
                let a = kb.combos_for(Action::MenuUp).join(" / ");
5✔
137
                let b = kb.combos_for(Action::MenuDown).join(" / ");
5✔
138
                format!("{} / {}", a, b)
5✔
139
            };
140
            let select = kb.combos_for(Action::MenuSelect).join(" / ");
5✔
141
            let super_key = kb.combos_for(Action::WmToggleOverlay).join(" / ");
5✔
142
            let help_combo = kb.combos_for(Action::OpenHelp).join(" / ");
5✔
143
            // If no combo is configured for `OpenHelp` we prefer the
144
            // literal 'Help menu' label in the markdown so no empty
145
            // placeholder appears in the rendered help.
146
            let help_label = if help_combo.is_empty() {
5✔
147
                "Help menu".to_string()
5✔
148
            } else {
149
                help_combo
×
150
            };
151

152
            s = s
5✔
153
                .replace("%FOCUS_NEXT%", &focus_next)
5✔
154
                .replace("%FOCUS_PREV%", &focus_prev)
5✔
155
                .replace("%NEW_WINDOW%", &new_win)
5✔
156
                .replace("%MENU_NAV%", &menu_nav)
5✔
157
                .replace("%MENU_ALT%", &menu_alt)
5✔
158
                .replace("%MENU_SELECT%", &select)
5✔
159
                .replace("%SUPER%", &super_key)
5✔
160
                .replace("%HELP_MENU%", &help_label);
5✔
161
            overlay.viewer.content.set_markdown(&s);
5✔
162
        }
×
163
        overlay.viewer.content.set_link_handler_fn(|url| {
5✔
164
            let _ = webbrowser::open(url);
×
165
            true
×
166
        });
×
167
        overlay
5✔
168
    }
5✔
169

170
    pub fn show(&mut self) {
3✔
171
        self.visible = true;
3✔
172
        self.viewer.set_keyboard_enabled(true);
3✔
173
        self.dialog.set_visible(true);
3✔
174
    }
3✔
175

176
    pub fn close(&mut self) {
3✔
177
        self.visible = false;
3✔
178
        self.viewer.set_keyboard_enabled(false);
3✔
179
        self.dialog.set_visible(false);
3✔
180
        self.viewer.content.reset();
3✔
181
    }
3✔
182

183
    pub fn visible(&self) -> bool {
5✔
184
        self.visible
5✔
185
    }
5✔
186

187
    pub fn handle_help_event(&mut self, event: &Event, _ctx: &ComponentContext) -> bool {
1✔
188
        match event {
1✔
189
            Event::Key(key) => {
1✔
190
                let kb = KeyBindings::default();
1✔
191
                if kb.matches(crate::keybindings::Action::CloseHelp, key) {
1✔
192
                    self.close();
1✔
193
                    true
1✔
194
                } else {
NEW
195
                    let viewer_ctx = self.viewer_context();
×
NEW
196
                    self.viewer.handle_event(event, &viewer_ctx)
×
197
                }
198
            }
199
            Event::Mouse(_) => {
NEW
200
                let viewer_ctx = self.viewer_context();
×
NEW
201
                self.viewer.handle_event(event, &viewer_ctx)
×
202
            }
UNCOV
203
            _ => false,
×
204
        }
205
    }
1✔
206

207
    /// Manually set keyboard handling for the underlying viewer.
208
    pub fn set_keyboard_enabled(&mut self, enabled: bool) {
×
209
        self.viewer.set_keyboard_enabled(enabled);
×
210
    }
×
211

NEW
212
    pub fn set_selection_enabled(&mut self, enabled: bool) {
×
NEW
213
        self.viewer.content.set_selection_enabled(enabled);
×
NEW
214
    }
×
215
}
216

217
impl HelpOverlayComponent {
218
    fn viewer_context(&self) -> ComponentContext {
1✔
219
        ComponentContext::new(self.visible).with_overlay(true)
1✔
220
    }
1✔
221
}
222

223
impl Default for HelpOverlayComponent {
224
    fn default() -> Self {
×
225
        Self::new()
×
226
    }
×
227
}
228

229
#[cfg(test)]
230
mod tests {
231
    use super::*;
232

233
    use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
234
    use ratatui::layout::Rect;
235

236
    #[test]
237
    fn help_constructs() {
1✔
238
        let h = HelpOverlayComponent::new();
1✔
239
        // should create without panic
240
        let _ = h;
1✔
241
    }
1✔
242

243
    #[test]
244
    fn placeholders_are_replaced_in_markdown() {
1✔
245
        let mut overlay = HelpOverlayComponent::new();
1✔
246
        use ratatui::buffer::Buffer;
247

248
        // Render the viewer into a buffer and inspect visible text to
249
        // avoid accessing private internals of `MarkdownViewerComponent`.
250
        let area = Rect {
1✔
251
            x: 0,
1✔
252
            y: 0,
1✔
253
            width: 80,
1✔
254
            height: 24,
1✔
255
        };
1✔
256
        let mut buffer = Buffer::empty(area);
1✔
257
        {
1✔
258
            let mut frame = crate::ui::UiFrame::from_parts(area, &mut buffer);
1✔
259
            overlay
1✔
260
                .viewer
1✔
261
                .render(&mut frame, area, &overlay.viewer_context());
1✔
262
        }
1✔
263

264
        let mut joined = String::new();
1✔
265
        for y in 0..area.height {
24✔
266
            let mut row = String::new();
24✔
267
            for x in 0..area.width {
1,920✔
268
                if let Some(cell) = buffer.cell((x, y)) {
1,920✔
269
                    row.push_str(cell.symbol());
1,920✔
270
                }
1,920✔
271
            }
272
            joined.push_str(&row);
24✔
273
            joined.push('\n');
24✔
274
        }
275
        let joined = joined.to_lowercase();
1✔
276

277
        // The embedded help should include the package name and version
278
        let pkg = env!("CARGO_PKG_NAME").to_lowercase();
1✔
279
        assert!(
1✔
280
            joined.contains(&pkg),
1✔
281
            "markdown should include package name"
×
282
        );
283
        let ver = env!("CARGO_PKG_VERSION").to_lowercase();
1✔
284
        assert!(
1✔
285
            joined.contains(&ver),
1✔
286
            "markdown should include package version"
×
287
        );
288
    }
1✔
289

290
    #[test]
291
    fn show_and_close_toggle_visibility() {
1✔
292
        let mut overlay = HelpOverlayComponent::new();
1✔
293
        assert!(!overlay.visible(), "initially hidden");
1✔
294

295
        overlay.show();
1✔
296
        assert!(overlay.visible(), "visible after show");
1✔
297
        assert!(overlay.dialog.visible(), "dialog visible after show");
1✔
298

299
        overlay.close();
1✔
300
        assert!(!overlay.visible(), "hidden after close");
1✔
301
        assert!(!overlay.dialog.visible(), "dialog hidden after close");
1✔
302
    }
1✔
303

304
    #[test]
305
    fn handle_help_event_closes_on_close_key() {
1✔
306
        let mut overlay = HelpOverlayComponent::new();
1✔
307
        overlay.show();
1✔
308
        // Default CloseHelp binding includes Esc
309
        let ev = Event::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
1✔
310
        let handled = overlay.handle_help_event(&ev, &ComponentContext::new(true));
1✔
311
        assert!(handled, "close key should be handled");
1✔
312
        assert!(!overlay.visible(), "overlay should be closed by key");
1✔
313
    }
1✔
314

315
    #[test]
316
    fn clicking_outside_auto_closes_when_enabled() {
1✔
317
        let mut overlay = HelpOverlayComponent::new();
1✔
318
        overlay.dialog.set_auto_close_on_outside_click(true);
1✔
319
        overlay.show();
1✔
320

321
        let area = Rect {
1✔
322
            x: 0,
1✔
323
            y: 0,
1✔
324
            width: 80,
1✔
325
            height: 24,
1✔
326
        };
1✔
327

328
        // Click at (0,0) which will be outside the centered dialog rect
329
        let ev = Event::Mouse(MouseEvent {
1✔
330
            kind: MouseEventKind::Down(crossterm::event::MouseButton::Left),
1✔
331
            column: 0,
1✔
332
            row: 0,
1✔
333
            modifiers: crossterm::event::KeyModifiers::NONE,
1✔
334
        });
1✔
335

336
        let handled = overlay.handle_help_event_in_area(&ev, area, &ComponentContext::new(true));
1✔
337
        assert!(
1✔
338
            handled,
1✔
339
            "outside click should be handled when auto-close enabled"
×
340
        );
341
        assert!(
1✔
342
            !overlay.visible(),
1✔
343
            "overlay should be closed by outside click"
×
344
        );
345
    }
1✔
346
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc