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

jzombie / term-wm / 20832337328

08 Jan 2026 09:27PM UTC coverage: 44.799% (+0.9%) from 43.932%
20832337328

push

github

web-flow
Remove last-modified tracking (#11)

* Remove last-modified tracking

* Migrate help overlay into sys subdirectory

71 of 75 new or added lines in 1 file covered. (94.67%)

4160 of 9286 relevant lines covered (44.8%)

10.1 hits per line

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

71.7
/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::{Component, DialogOverlayComponent, MarkdownViewerComponent};
8
use crate::keybindings::{Action, KeyBindings};
9
use crate::ui::UiFrame;
10

11
const HELP_CONTENT_BYTES: &[u8] =
12
    include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/help.md"));
13

14
#[derive(Debug)]
15
pub struct HelpOverlayComponent {
16
    dialog: DialogOverlayComponent,
17
    visible: bool,
18
    viewer: MarkdownViewerComponent,
19
    area: Rect,
20
}
21

22
impl Component for HelpOverlayComponent {
23
    fn resize(&mut self, area: Rect) {
×
24
        self.area = area;
×
25
    }
×
26

27
    fn render(&mut self, frame: &mut UiFrame<'_>, area: Rect, _focused: bool) {
×
28
        self.area = area;
×
29
        if !self.visible || area.width == 0 || area.height == 0 {
×
30
            return;
×
31
        }
×
32
        // If the dialog requests a dim backdrop, apply it across the full frame
33
        // before clearing and drawing the help dialog contents.
34
        self.dialog.render_backdrop(frame, area);
×
35
        let rect = self.dialog.rect_for(area);
×
36
        frame.render_widget(Clear, rect);
×
37
        let title = format!("{} — About / Help", env!("CARGO_PKG_NAME"));
×
38
        let block = Block::default().title(title).borders(Borders::ALL);
×
39
        let inner = Rect {
×
40
            x: rect.x.saturating_add(1),
×
41
            y: rect.y.saturating_add(1),
×
42
            width: rect.width.saturating_sub(2),
×
43
            height: rect.height.saturating_sub(2),
×
44
        };
×
45
        frame.render_widget(block, rect);
×
46
        self.viewer.render_content(frame, inner);
×
47
    }
×
48

49
    fn handle_event(&mut self, event: &Event) -> bool {
×
50
        self.handle_help_event_in_area(event, self.area)
×
51
    }
×
52
}
53

54
impl HelpOverlayComponent {
55
    pub fn handle_help_event_in_area(&mut self, event: &Event, area: Rect) -> bool {
1✔
56
        if !self.visible {
1✔
57
            return false;
×
58
        }
1✔
59
        match event {
1✔
60
            Event::Key(key) => {
×
61
                let kb = KeyBindings::default();
×
62
                if kb.matches(crate::keybindings::Action::CloseHelp, key) {
×
63
                    self.close();
×
64
                    true
×
65
                } else {
66
                    self.viewer.handle_key_event(key)
×
67
                }
68
            }
69
            Event::Mouse(_) => {
70
                // If configured, allow clicking outside the dialog to auto-close it.
71
                if self.dialog.handle_click_outside(event, area) {
1✔
72
                    self.close();
1✔
73
                    return true;
1✔
74
                }
×
75
                let rect = self.dialog.rect_for(area);
×
76
                let inner = Rect {
×
77
                    x: rect.x.saturating_add(1),
×
78
                    y: rect.y.saturating_add(1),
×
79
                    width: rect.width.saturating_sub(2),
×
80
                    height: rect.height.saturating_sub(2),
×
81
                };
×
82
                self.viewer.handle_pointer_event_in_area(event, inner)
×
83
            }
84
            _ => false,
×
85
        }
86
    }
1✔
87
}
88

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

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

145
            s = s
5✔
146
                .replace("%FOCUS_NEXT%", &focus_next)
5✔
147
                .replace("%FOCUS_PREV%", &focus_prev)
5✔
148
                .replace("%NEW_WINDOW%", &new_win)
5✔
149
                .replace("%MENU_NAV%", &menu_nav)
5✔
150
                .replace("%MENU_ALT%", &menu_alt)
5✔
151
                .replace("%MENU_SELECT%", &select)
5✔
152
                .replace("%SUPER%", &super_key)
5✔
153
                .replace("%HELP_MENU%", &help_label);
5✔
154
            overlay.viewer.set_markdown(&s);
5✔
155
        }
×
156
        overlay.viewer.set_link_handler_fn(|url| {
5✔
157
            let _ = webbrowser::open(url);
×
158
            true
×
159
        });
×
160
        overlay
5✔
161
    }
5✔
162

163
    pub fn show(&mut self) {
3✔
164
        self.visible = true;
3✔
165
        self.viewer.set_keyboard_enabled(true);
3✔
166
        self.dialog.set_visible(true);
3✔
167
    }
3✔
168

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

176
    pub fn visible(&self) -> bool {
5✔
177
        self.visible
5✔
178
    }
5✔
179

180
    pub fn handle_help_event(&mut self, event: &Event) -> bool {
1✔
181
        match event {
1✔
182
            Event::Key(key) => {
1✔
183
                let kb = KeyBindings::default();
1✔
184
                if kb.matches(crate::keybindings::Action::CloseHelp, key) {
1✔
185
                    self.close();
1✔
186
                    true
1✔
187
                } else {
188
                    self.viewer.handle_key_event(key)
×
189
                }
190
            }
191
            Event::Mouse(_) => self.viewer.handle_pointer_event(event),
×
192
            _ => false,
×
193
        }
194
    }
1✔
195

196
    /// Manually set keyboard handling for the underlying viewer.
197
    pub fn set_keyboard_enabled(&mut self, enabled: bool) {
×
198
        self.viewer.set_keyboard_enabled(enabled);
×
199
    }
×
200
}
201

202
impl Default for HelpOverlayComponent {
203
    fn default() -> Self {
×
204
        Self::new()
×
205
    }
×
206
}
207

208
#[cfg(test)]
209
mod tests {
210
    use super::*;
211

212
    use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
213
    use ratatui::layout::Rect;
214

215
    #[test]
216
    fn help_constructs() {
1✔
217
        let h = HelpOverlayComponent::new();
1✔
218
        // should create without panic
219
        let _ = h;
1✔
220
    }
1✔
221

222
    #[test]
223
    fn placeholders_are_replaced_in_markdown() {
1✔
224
        let mut overlay = HelpOverlayComponent::new();
1✔
225
        use ratatui::buffer::Buffer;
226

227
        // Render the viewer into a buffer and inspect visible text to
228
        // avoid accessing private internals of `MarkdownViewerComponent`.
229
        let area = Rect {
1✔
230
            x: 0,
1✔
231
            y: 0,
1✔
232
            width: 80,
1✔
233
            height: 24,
1✔
234
        };
1✔
235
        let mut buffer = Buffer::empty(area);
1✔
236
        {
1✔
237
            let mut frame = crate::ui::UiFrame::from_parts(area, &mut buffer);
1✔
238
            overlay.viewer.render_content(&mut frame, area);
1✔
239
        }
1✔
240

241
        let mut joined = String::new();
1✔
242
        for y in 0..area.height {
24✔
243
            let mut row = String::new();
24✔
244
            for x in 0..area.width {
1,920✔
245
                if let Some(cell) = buffer.cell((x, y)) {
1,920✔
246
                    row.push_str(cell.symbol());
1,920✔
247
                }
1,920✔
248
            }
249
            joined.push_str(&row);
24✔
250
            joined.push('\n');
24✔
251
        }
252
        let joined = joined.to_lowercase();
1✔
253

254
        // The embedded help should include the package name and version
255
        let pkg = env!("CARGO_PKG_NAME").to_lowercase();
1✔
256
        assert!(
1✔
257
            joined.contains(&pkg),
1✔
NEW
258
            "markdown should include package name"
×
259
        );
260
        let ver = env!("CARGO_PKG_VERSION").to_lowercase();
1✔
261
        assert!(
1✔
262
            joined.contains(&ver),
1✔
NEW
263
            "markdown should include package version"
×
264
        );
265
    }
1✔
266

267
    #[test]
268
    fn show_and_close_toggle_visibility() {
1✔
269
        let mut overlay = HelpOverlayComponent::new();
1✔
270
        assert!(!overlay.visible(), "initially hidden");
1✔
271

272
        overlay.show();
1✔
273
        assert!(overlay.visible(), "visible after show");
1✔
274
        assert!(overlay.dialog.visible(), "dialog visible after show");
1✔
275

276
        overlay.close();
1✔
277
        assert!(!overlay.visible(), "hidden after close");
1✔
278
        assert!(!overlay.dialog.visible(), "dialog hidden after close");
1✔
279
    }
1✔
280

281
    #[test]
282
    fn handle_help_event_closes_on_close_key() {
1✔
283
        let mut overlay = HelpOverlayComponent::new();
1✔
284
        overlay.show();
1✔
285
        // Default CloseHelp binding includes Esc
286
        let ev = Event::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
1✔
287
        let handled = overlay.handle_help_event(&ev);
1✔
288
        assert!(handled, "close key should be handled");
1✔
289
        assert!(!overlay.visible(), "overlay should be closed by key");
1✔
290
    }
1✔
291

292
    #[test]
293
    fn clicking_outside_auto_closes_when_enabled() {
1✔
294
        let mut overlay = HelpOverlayComponent::new();
1✔
295
        overlay.dialog.set_auto_close_on_outside_click(true);
1✔
296
        overlay.show();
1✔
297

298
        let area = Rect {
1✔
299
            x: 0,
1✔
300
            y: 0,
1✔
301
            width: 80,
1✔
302
            height: 24,
1✔
303
        };
1✔
304

305
        // Click at (0,0) which will be outside the centered dialog rect
306
        let ev = Event::Mouse(MouseEvent {
1✔
307
            kind: MouseEventKind::Down(crossterm::event::MouseButton::Left),
1✔
308
            column: 0,
1✔
309
            row: 0,
1✔
310
            modifiers: crossterm::event::KeyModifiers::NONE,
1✔
311
        });
1✔
312

313
        let handled = overlay.handle_help_event_in_area(&ev, area);
1✔
314
        assert!(
1✔
315
            handled,
1✔
NEW
316
            "outside click should be handled when auto-close enabled"
×
317
        );
318
        assert!(
1✔
319
            !overlay.visible(),
1✔
NEW
320
            "overlay should be closed by outside click"
×
321
        );
322
    }
1✔
323
}
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