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

jzombie / term-wm / 20796173160

07 Jan 2026 08:53PM UTC coverage: 38.847% (+2.9%) from 35.923%
20796173160

Pull #5

github

web-flow
Merge 6220a88ab into bbd85351a
Pull Request #5: Add splash screen, debug logging, and unified scroll support

603 of 1185 new or added lines in 16 files covered. (50.89%)

8 existing lines in 6 files now uncovered.

3018 of 7769 relevant lines covered (38.85%)

2.96 hits per line

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

12.94
/src/runner.rs
1
use std::io;
2
use std::time::Duration;
3

4
use crossterm::event::{Event, KeyCode, KeyEventKind};
5
use ratatui::prelude::{Constraint, Direction};
6
use ratatui::style::Style;
7

8
use crate::components::ConfirmAction;
9
use crate::drivers::{InputDriver, OutputDriver};
10
use crate::event_loop::{ControlFlow, EventLoop};
11
use crate::layout::{LayoutNode, TilingLayout};
12
use crate::ui::UiFrame;
13
use crate::window::{AppWindowDraw, LayoutContract, WindowManager, WmMenuAction};
14

15
pub trait HasWindowManager<W: Copy + Eq + Ord, R: Copy + Eq + Ord> {
16
    fn windows(&mut self) -> &mut WindowManager<W, R>;
17
    fn wm_new_window(&mut self) -> std::io::Result<()> {
×
18
        Ok(())
×
19
    }
×
20
    fn wm_close_window(&mut self, _id: R) -> std::io::Result<()> {
×
21
        Ok(())
×
22
    }
×
23
}
24

25
pub trait WindowApp<W: Copy + Eq + Ord, R: Copy + Eq + Ord>: HasWindowManager<W, R> {
26
    fn enumerate_windows(&mut self) -> Vec<R>;
27
    fn render_window(&mut self, frame: &mut UiFrame<'_>, window: AppWindowDraw<R>);
28

29
    fn empty_window_message(&self) -> &str {
×
30
        "No windows"
×
31
    }
×
32

33
    fn layout_for_windows(&mut self, windows: &[R]) -> Option<TilingLayout<R>> {
×
34
        auto_layout_for_windows(windows)
×
35
    }
×
36
}
37

38
#[allow(clippy::too_many_arguments)]
39
pub fn run_app<O, D, A, W, R, FDraw, FDispatch, FQuit, FMap, FFocus>(
×
40
    output: &mut O,
×
41
    driver: &mut D,
×
42
    app: &mut A,
×
43
    focus_regions: &[R],
×
44
    map_region: FMap,
×
45
    _map_focus: FFocus,
×
46
    poll_interval: Duration,
×
47
    mut draw: FDraw,
×
48
    mut dispatch: FDispatch,
×
49
    mut should_quit: FQuit,
×
50
) -> io::Result<()>
×
51
where
×
52
    O: OutputDriver,
×
53
    D: InputDriver,
×
54
    A: HasWindowManager<W, R>,
×
55
    W: Copy + Eq + Ord,
×
56
    R: Copy + Eq + Ord + PartialEq<W> + std::fmt::Debug,
×
57
    FDraw: for<'frame> FnMut(UiFrame<'frame>, &mut A),
×
58
    FDispatch: FnMut(&Event, &mut A) -> bool,
×
59
    FQuit: FnMut(Option<&Event>, &mut A) -> bool,
×
60
    FMap: Fn(R) -> W + Copy,
×
61
    FFocus: Fn(W) -> Option<R>,
×
62
{
63
    let capture_timeout = Duration::from_millis(500);
×
64
    let mut event_loop = EventLoop::new(driver, poll_interval);
×
65
    event_loop
×
66
        .driver()
×
67
        .set_mouse_capture(app.windows().mouse_capture_enabled())?;
×
68

69
    // The WindowManager now provides `take_closed_app_windows()` to drain app ids
70
    // whose windows were closed; we'll poll that each loop and call `app.wm_close_window`.
71
    // No additional setup required here.
72

73
    event_loop.run(|driver, event| {
×
74
        let handler = || -> io::Result<ControlFlow> {
×
75
            // Drain any pending closed app ids recorded by the WindowManager and invoke app cleanup.
76
            for id in app.windows().take_closed_app_windows() {
×
77
                app.wm_close_window(id)?;
×
78
            }
79
            let mut flush_mouse_capture = |app: &mut A, flow: ControlFlow| {
×
80
                if let Some(enabled) = app.windows().take_mouse_capture_change() {
×
81
                    let _ = driver.set_mouse_capture(enabled);
×
82
                }
×
83
                Ok(flow)
×
84
            };
×
85
            if let Some(evt) = event {
×
86
                if app.windows().exit_confirm_visible() {
×
87
                    if let Some(action) = app.windows().handle_exit_confirm_event(&evt) {
×
88
                        match action {
×
89
                            ConfirmAction::Confirm => return Ok(ControlFlow::Quit),
×
90
                            ConfirmAction::Cancel => app.windows().close_exit_confirm(),
×
91
                        }
92
                    }
×
93
                    return flush_mouse_capture(app, ControlFlow::Continue);
×
94
                }
×
95

NEW
96
                if app.windows().help_overlay_visible() {
×
NEW
97
                    let _ = app.windows().handle_help_event(&evt);
×
NEW
98
                    return flush_mouse_capture(app, ControlFlow::Continue);
×
NEW
99
                }
×
100
                let wm_mode = app.windows().layout_contract() == LayoutContract::WindowManaged;
×
101
                if wm_mode
×
102
                    && let Event::Key(key) = evt
×
103
                    && key.code == KeyCode::Esc
×
104
                    && key.kind == KeyEventKind::Press
×
105
                {
106
                    if app.windows().wm_overlay_visible() {
×
107
                        let passthrough = app.windows().esc_passthrough_active();
×
108
                        app.windows().close_wm_overlay();
×
109
                        if passthrough {
×
110
                            let _ = dispatch(&Event::Key(key), app);
×
111
                        }
×
112
                    } else {
×
113
                        app.windows().open_wm_overlay();
×
114
                    }
×
115
                    return flush_mouse_capture(app, ControlFlow::Continue);
×
116
                }
×
117
                if wm_mode && app.windows().wm_overlay_visible() {
×
118
                    if let Some(action) = app.windows().handle_wm_menu_event(&evt) {
×
119
                        match action {
×
120
                            WmMenuAction::CloseMenu => {
×
121
                                app.windows().close_wm_overlay();
×
122
                            }
×
123
                            WmMenuAction::ToggleMouseCapture => {
×
124
                                app.windows().toggle_mouse_capture();
×
125
                            }
×
126
                            WmMenuAction::MinimizeWindow => {
×
127
                                let id = app.windows().wm_focus();
×
128
                                app.windows().minimize_window(id);
×
129
                                app.windows().close_wm_overlay();
×
130
                            }
×
131
                            WmMenuAction::MaximizeWindow => {
×
132
                                let id = app.windows().wm_focus();
×
133
                                app.windows().toggle_maximize(id);
×
134
                                app.windows().close_wm_overlay();
×
135
                            }
×
136
                            WmMenuAction::CloseWindow => {
×
137
                                let id = app.windows().wm_focus();
×
138
                                app.windows().close_window(id);
×
139
                                app.windows().close_wm_overlay();
×
140
                            }
×
141
                            WmMenuAction::NewWindow => {
142
                                app.wm_new_window()?;
×
143
                                app.windows().close_wm_overlay();
×
144
                            }
145
                            WmMenuAction::ToggleDebugWindow => {
×
146
                                app.windows().toggle_debug_window();
×
147
                                app.windows().close_wm_overlay();
×
148
                            }
×
149
                            WmMenuAction::BringFloatingFront => {
×
150
                                app.windows().bring_all_floating_to_front();
×
151
                                app.windows().close_wm_overlay();
×
152
                            }
×
153
                            WmMenuAction::ExitUi => {
154
                                app.windows().close_wm_overlay();
×
155
                                app.windows().open_exit_confirm();
×
156
                                return flush_mouse_capture(app, ControlFlow::Continue);
×
157
                            }
158
                        }
159
                        return flush_mouse_capture(app, ControlFlow::Continue);
×
160
                    }
×
161
                    if app.windows().wm_menu_consumes_event(&evt) {
×
162
                        return flush_mouse_capture(app, ControlFlow::Continue);
×
163
                    }
×
164
                    if let Event::Key(key) = evt
×
165
                        && key.code == KeyCode::Char('n')
×
166
                        && key.modifiers.is_empty()
×
167
                    {
168
                        app.wm_new_window()?;
×
169
                        app.windows().close_wm_overlay();
×
170
                        return flush_mouse_capture(app, ControlFlow::Continue);
×
171
                    }
×
172
                }
×
173
                if should_quit(Some(&evt), app) {
×
174
                    app.windows().open_exit_confirm();
×
175
                    return flush_mouse_capture(app, ControlFlow::Continue);
×
176
                }
×
177
                if matches!(evt, Event::Mouse(_)) && !app.windows().mouse_capture_enabled() {
×
178
                    return flush_mouse_capture(app, ControlFlow::Continue);
×
179
                }
×
180
                match &evt {
×
181
                    Event::Key(key) if key.code == KeyCode::BackTab => {
×
182
                        if app.windows().capture_active() {
×
183
                            if wm_mode {
×
184
                                app.windows().arm_capture(capture_timeout);
×
185
                            }
×
186
                            let _ = app.windows().handle_focus_event(
×
187
                                &evt,
×
188
                                focus_regions,
×
189
                                &map_region,
×
190
                                &_map_focus,
×
191
                            );
×
192
                            return flush_mouse_capture(app, ControlFlow::Continue);
×
193
                        }
×
194
                        if dispatch(&evt, app) {
×
195
                            return flush_mouse_capture(app, ControlFlow::Continue);
×
196
                        }
×
197
                        let _ = app.windows().handle_focus_event(
×
198
                            &evt,
×
199
                            focus_regions,
×
200
                            &map_region,
×
201
                            &_map_focus,
×
202
                        );
×
203
                        return flush_mouse_capture(app, ControlFlow::Continue);
×
204
                    }
205
                    Event::Key(key) if key.code == KeyCode::Tab => {
×
206
                        if app.windows().capture_active() {
×
207
                            if wm_mode {
×
208
                                app.windows().arm_capture(capture_timeout);
×
209
                            }
×
210
                            let _ = app.windows().handle_focus_event(
×
211
                                &evt,
×
212
                                focus_regions,
×
213
                                &map_region,
×
214
                                &_map_focus,
×
215
                            );
×
216
                            return flush_mouse_capture(app, ControlFlow::Continue);
×
217
                        }
×
218
                        if dispatch(&evt, app) {
×
219
                            return flush_mouse_capture(app, ControlFlow::Continue);
×
220
                        }
×
221
                        let _ = app.windows().handle_focus_event(
×
222
                            &evt,
×
223
                            focus_regions,
×
224
                            &map_region,
×
225
                            &_map_focus,
×
226
                        );
×
227
                        return flush_mouse_capture(app, ControlFlow::Continue);
×
228
                    }
229
                    Event::Key(_) if app.windows().capture_active() => {
×
230
                        app.windows().clear_capture();
×
231
                        let _ = dispatch(&evt, app);
×
232
                    }
×
233
                    _ => {
×
234
                        let _ = app.windows().handle_focus_event(
×
235
                            &evt,
×
236
                            focus_regions,
×
237
                            &map_region,
×
238
                            &_map_focus,
×
239
                        );
×
240
                        let _ = dispatch(&evt, app);
×
241
                    }
×
242
                }
243
            } else {
244
                if should_quit(None, app) {
×
245
                    return flush_mouse_capture(app, ControlFlow::Quit);
×
246
                }
×
247
                app.windows().begin_frame();
×
248
                output.draw(|frame| draw(frame, app))?;
×
249
            }
250
            flush_mouse_capture(app, ControlFlow::Continue)
×
251
        };
×
252

253
        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(handler)) {
×
254
            Ok(result) => result,
×
255
            Err(_) => {
256
                // TODO: This needs to be improved; currently requires resizing the terminal window to
257
                // "stabilize" the messages, to produce them in a debug log window. Also, directly setting
258
                // the mouse capture here bypasses the state, and the UI is not reflected. It might be better
259
                // to just turn off mouse capturing and crash the app naturally if this cannot be improved.
260

261
                // A panic occurred; stop mouse capture to avoid terminal spam
262
                let _ = driver.set_mouse_capture(false);
×
263
                // Attempt to immediately redraw the UI so the debug log (populated by the panic hook)
264
                // is visible to the user without waiting for another input event like a resize.
265
                let mut redraw = || -> io::Result<()> {
×
266
                    app.windows().begin_frame();
×
267
                    output.draw(|frame| draw(frame, app))
×
268
                };
×
269
                let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
×
270
                    let _ = redraw();
×
271
                }));
×
272
                // Let the panic hook have recorded details into the debug log; continue event loop.
273
                Ok(ControlFlow::Continue)
×
274
            }
275
        }
276
    })?;
×
277

278
    Ok(())
×
279
}
×
280

281
#[allow(clippy::too_many_arguments)]
282
pub fn run_window_app<O, D, A, W, R, FDispatch, FQuit, FMap, FFocus>(
×
283
    output: &mut O,
×
284
    driver: &mut D,
×
285
    app: &mut A,
×
286
    focus_regions: &[R],
×
287
    map_region: FMap,
×
288
    _map_focus: FFocus,
×
289
    poll_interval: Duration,
×
290
    dispatch: FDispatch,
×
291
    should_quit: FQuit,
×
292
) -> io::Result<()>
×
293
where
×
294
    O: OutputDriver,
×
295
    D: InputDriver,
×
296
    A: WindowApp<W, R>,
×
297
    W: Copy + Eq + Ord,
×
298
    R: Copy + Eq + Ord + PartialEq<W> + std::fmt::Debug,
×
299
    FDispatch: FnMut(&Event, &mut A) -> bool,
×
300
    FQuit: FnMut(Option<&Event>, &mut A) -> bool,
×
301
    FMap: Fn(R) -> W + Copy,
×
302
    FFocus: Fn(W) -> Option<R>,
×
303
{
304
    let draw_map = map_region;
×
305
    let mut draw_state = WindowDrawState::default();
×
306
    run_app(
×
307
        output,
×
308
        driver,
×
309
        app,
×
310
        focus_regions,
×
311
        map_region,
×
312
        _map_focus,
×
313
        poll_interval,
×
314
        move |frame, app| {
×
315
            let mut frame = frame;
×
316
            draw_window_app(&mut frame, app, &mut draw_state, draw_map);
×
317
        },
×
318
        dispatch,
×
319
        should_quit,
×
320
    )
321
}
×
322

323
struct WindowDrawState<R> {
324
    known: Vec<R>,
325
}
326

327
impl<R> Default for WindowDrawState<R> {
328
    fn default() -> Self {
1✔
329
        Self { known: Vec::new() }
1✔
330
    }
1✔
331
}
332

333
impl<R: Copy + Eq> WindowDrawState<R> {
334
    fn update(&mut self, windows: &[R]) -> bool {
3✔
335
        if self.known == windows {
3✔
336
            false
2✔
337
        } else {
338
            self.known = windows.to_vec();
1✔
339
            true
1✔
340
        }
341
    }
3✔
342
}
343

344
fn draw_window_app<A, W, R, FMap>(
×
345
    frame: &mut UiFrame<'_>,
×
346
    app: &mut A,
×
347
    state: &mut WindowDrawState<R>,
×
348
    map_region: FMap,
×
349
) where
×
350
    A: WindowApp<W, R>,
×
351
    W: Copy + Eq + Ord,
×
352
    R: Copy + Eq + Ord + PartialEq<W> + std::fmt::Debug,
×
353
    FMap: Fn(R) -> W,
×
354
{
355
    let area = frame.area();
×
356
    let windows = app.enumerate_windows();
×
357
    let windows_changed = state.update(&windows);
×
358
    if windows.is_empty() {
×
359
        let message = app.empty_window_message();
×
360
        if !message.is_empty() {
×
361
            frame
×
362
                .buffer_mut()
×
363
                .set_string(area.x, area.y, message, Style::default());
×
364
        }
×
365
        app.windows().render_overlays(frame);
×
366
        return;
×
367
    }
×
368

369
    if windows_changed && let Some(layout) = app.layout_for_windows(&windows) {
×
370
        app.windows().set_managed_layout(layout);
×
371
    }
×
372
    let focus_order: Vec<W> = windows.iter().copied().map(map_region).collect();
×
373
    if !focus_order.is_empty() {
×
374
        app.windows().set_focus_order(focus_order);
×
375
    }
×
376
    app.windows().register_managed_layout(area);
×
377
    let plan = app.windows().window_draw_plan(frame);
×
378
    for window in plan {
×
379
        app.render_window(frame, window);
×
380
    }
×
381
    app.windows().render_overlays(frame);
×
382
}
×
383

384
fn auto_layout_for_windows<R: Copy + Eq + Ord>(windows: &[R]) -> Option<TilingLayout<R>> {
3✔
385
    let node = match windows.len() {
3✔
386
        0 => return None,
1✔
387
        1 => LayoutNode::leaf(windows[0]),
1✔
388
        2 => LayoutNode::split(
×
389
            Direction::Horizontal,
×
390
            vec![Constraint::Percentage(50), Constraint::Percentage(50)],
×
391
            vec![LayoutNode::leaf(windows[0]), LayoutNode::leaf(windows[1])],
×
392
        ),
393
        len => {
1✔
394
            let mut constraints = Vec::with_capacity(len);
1✔
395
            let base = (100 / len as u16).max(1);
1✔
396
            for idx in 0..len {
4✔
397
                if idx == len - 1 {
4✔
398
                    let used = base.saturating_mul((len - 1) as u16);
1✔
399
                    constraints.push(Constraint::Percentage(100u16.saturating_sub(used)));
1✔
400
                } else {
3✔
401
                    constraints.push(Constraint::Percentage(base));
3✔
402
                }
3✔
403
            }
404
            let children = windows.iter().map(|&id| LayoutNode::leaf(id)).collect();
4✔
405
            LayoutNode::split(Direction::Vertical, constraints, children)
1✔
406
        }
407
    };
408
    Some(TilingLayout::new(node))
2✔
409
}
3✔
410

411
#[cfg(test)]
412
mod tests {
413
    use super::*;
414
    #[test]
415
    fn auto_layout_empty_and_multiple() {
1✔
416
        let empty: Vec<u8> = vec![];
1✔
417
        assert!(auto_layout_for_windows(&empty).is_none());
1✔
418

419
        let one = vec![1u8];
1✔
420
        let layout = auto_layout_for_windows(&one).unwrap();
1✔
421
        // single node should be a leaf
422
        assert!(matches!(layout.root(), crate::layout::LayoutNode::Leaf(_)));
1✔
423

424
        let many = vec![1u8, 2, 3, 4];
1✔
425
        let layout2 = auto_layout_for_windows(&many).unwrap();
1✔
426
        // for many windows the top-level node should be a split
427
        assert!(matches!(
1✔
428
            layout2.root(),
1✔
429
            crate::layout::LayoutNode::Split { .. }
430
        ));
431
    }
1✔
432

433
    #[test]
434
    fn window_draw_state_update_changes() {
1✔
435
        let mut s: WindowDrawState<u8> = WindowDrawState::default();
1✔
436
        assert!(!s.update(&[]));
1✔
437
        assert!(s.update(&[1, 2]));
1✔
438
        assert!(!s.update(&[1, 2]));
1✔
439
    }
1✔
440
}
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