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

jzombie / term-wm / 20770181342

07 Jan 2026 04:02AM UTC coverage: 36.037% (+2.5%) from 33.582%
20770181342

Pull #3

github

web-flow
Merge 4d97c195c into ab0b82880
Pull Request #3: General UI improvements

454 of 2495 new or added lines in 26 files covered. (18.2%)

17 existing lines in 7 files now uncovered.

2515 of 6979 relevant lines covered (36.04%)

2.06 hits per line

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

13.1
/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>;
NEW
17
    fn wm_new_window(&mut self) -> std::io::Result<()> {
×
NEW
18
        Ok(())
×
NEW
19
    }
×
NEW
20
    fn wm_close_window(&mut self, _id: R) -> std::io::Result<()> {
×
NEW
21
        Ok(())
×
NEW
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)]
NEW
39
pub fn run_app<O, D, A, W, R, FDraw, FDispatch, FQuit, FMap, FFocus>(
×
NEW
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,
×
NEW
50
) -> io::Result<()>
×
51
where
×
NEW
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,
×
NEW
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
{
UNCOV
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| {
×
NEW
74
        let handler = || -> io::Result<ControlFlow> {
×
75
            // Drain any pending closed app ids recorded by the WindowManager and invoke app cleanup.
NEW
76
            for id in app.windows().take_closed_app_windows() {
×
NEW
77
                app.wm_close_window(id)?;
×
78
            }
NEW
79
            let mut flush_mouse_capture = |app: &mut A, flow: ControlFlow| {
×
NEW
80
                if let Some(enabled) = app.windows().take_mouse_capture_change() {
×
NEW
81
                    let _ = driver.set_mouse_capture(enabled);
×
82
                }
×
NEW
83
                Ok(flow)
×
NEW
84
            };
×
NEW
85
            if let Some(evt) = event {
×
NEW
86
                if app.windows().exit_confirm_visible() {
×
NEW
87
                    if let Some(action) = app.windows().handle_exit_confirm_event(&evt) {
×
NEW
88
                        match action {
×
NEW
89
                            ConfirmAction::Confirm => return Ok(ControlFlow::Quit),
×
NEW
90
                            ConfirmAction::Cancel => app.windows().close_exit_confirm(),
×
91
                        }
UNCOV
92
                    }
×
93
                    return flush_mouse_capture(app, ControlFlow::Continue);
×
94
                }
×
NEW
95
                let wm_mode = app.windows().layout_contract() == LayoutContract::WindowManaged;
×
NEW
96
                if wm_mode
×
NEW
97
                    && let Event::Key(key) = evt
×
NEW
98
                    && key.code == KeyCode::Esc
×
NEW
99
                    && key.kind == KeyEventKind::Press
×
100
                {
NEW
101
                    if app.windows().wm_overlay_visible() {
×
NEW
102
                        let passthrough = app.windows().esc_passthrough_active();
×
NEW
103
                        app.windows().close_wm_overlay();
×
NEW
104
                        if passthrough {
×
NEW
105
                            let _ = dispatch(&Event::Key(key), app);
×
NEW
106
                        }
×
NEW
107
                    } else {
×
NEW
108
                        app.windows().open_wm_overlay();
×
NEW
109
                    }
×
110
                    return flush_mouse_capture(app, ControlFlow::Continue);
×
111
                }
×
NEW
112
                if wm_mode && app.windows().wm_overlay_visible() {
×
NEW
113
                    if let Some(action) = app.windows().handle_wm_menu_event(&evt) {
×
NEW
114
                        match action {
×
NEW
115
                            WmMenuAction::CloseMenu => {
×
NEW
116
                                app.windows().close_wm_overlay();
×
NEW
117
                            }
×
NEW
118
                            WmMenuAction::ToggleMouseCapture => {
×
NEW
119
                                app.windows().toggle_mouse_capture();
×
NEW
120
                            }
×
NEW
121
                            WmMenuAction::MinimizeWindow => {
×
NEW
122
                                let id = app.windows().wm_focus();
×
NEW
123
                                app.windows().minimize_window(id);
×
NEW
124
                                app.windows().close_wm_overlay();
×
NEW
125
                            }
×
NEW
126
                            WmMenuAction::MaximizeWindow => {
×
NEW
127
                                let id = app.windows().wm_focus();
×
NEW
128
                                app.windows().toggle_maximize(id);
×
NEW
129
                                app.windows().close_wm_overlay();
×
NEW
130
                            }
×
NEW
131
                            WmMenuAction::CloseWindow => {
×
NEW
132
                                let id = app.windows().wm_focus();
×
NEW
133
                                app.windows().close_window(id);
×
NEW
134
                                app.windows().close_wm_overlay();
×
NEW
135
                            }
×
136
                            WmMenuAction::NewWindow => {
NEW
137
                                app.wm_new_window()?;
×
NEW
138
                                app.windows().close_wm_overlay();
×
139
                            }
NEW
140
                            WmMenuAction::ToggleDebugWindow => {
×
NEW
141
                                app.windows().toggle_debug_window();
×
NEW
142
                                app.windows().close_wm_overlay();
×
NEW
143
                            }
×
NEW
144
                            WmMenuAction::BringFloatingFront => {
×
NEW
145
                                app.windows().bring_all_floating_to_front();
×
NEW
146
                                app.windows().close_wm_overlay();
×
NEW
147
                            }
×
148
                            WmMenuAction::ExitUi => {
NEW
149
                                app.windows().close_wm_overlay();
×
NEW
150
                                app.windows().open_exit_confirm();
×
NEW
151
                                return flush_mouse_capture(app, ControlFlow::Continue);
×
152
                            }
153
                        }
154
                        return flush_mouse_capture(app, ControlFlow::Continue);
×
155
                    }
×
NEW
156
                    if app.windows().wm_menu_consumes_event(&evt) {
×
NEW
157
                        return flush_mouse_capture(app, ControlFlow::Continue);
×
NEW
158
                    }
×
NEW
159
                    if let Event::Key(key) = evt
×
NEW
160
                        && key.code == KeyCode::Char('n')
×
NEW
161
                        && key.modifiers.is_empty()
×
162
                    {
NEW
163
                        app.wm_new_window()?;
×
NEW
164
                        app.windows().close_wm_overlay();
×
165
                        return flush_mouse_capture(app, ControlFlow::Continue);
×
166
                    }
×
NEW
167
                }
×
NEW
168
                if should_quit(Some(&evt), app) {
×
NEW
169
                    app.windows().open_exit_confirm();
×
NEW
170
                    return flush_mouse_capture(app, ControlFlow::Continue);
×
NEW
171
                }
×
NEW
172
                if matches!(evt, Event::Mouse(_)) && !app.windows().mouse_capture_enabled() {
×
UNCOV
173
                    return flush_mouse_capture(app, ControlFlow::Continue);
×
UNCOV
174
                }
×
NEW
175
                match &evt {
×
NEW
176
                    Event::Key(key) if key.code == KeyCode::BackTab => {
×
NEW
177
                        if app.windows().capture_active() {
×
NEW
178
                            if wm_mode {
×
NEW
179
                                app.windows().arm_capture(capture_timeout);
×
NEW
180
                            }
×
NEW
181
                            let _ = app.windows().handle_focus_event(
×
NEW
182
                                &evt,
×
NEW
183
                                focus_regions,
×
NEW
184
                                &map_region,
×
NEW
185
                                &_map_focus,
×
NEW
186
                            );
×
NEW
187
                            return flush_mouse_capture(app, ControlFlow::Continue);
×
NEW
188
                        }
×
NEW
189
                        if dispatch(&evt, app) {
×
NEW
190
                            return flush_mouse_capture(app, ControlFlow::Continue);
×
191
                        }
×
NEW
192
                        let _ = app.windows().handle_focus_event(
×
NEW
193
                            &evt,
×
NEW
194
                            focus_regions,
×
NEW
195
                            &map_region,
×
NEW
196
                            &_map_focus,
×
NEW
197
                        );
×
198
                        return flush_mouse_capture(app, ControlFlow::Continue);
×
199
                    }
NEW
200
                    Event::Key(key) if key.code == KeyCode::Tab => {
×
NEW
201
                        if app.windows().capture_active() {
×
NEW
202
                            if wm_mode {
×
NEW
203
                                app.windows().arm_capture(capture_timeout);
×
NEW
204
                            }
×
NEW
205
                            let _ = app.windows().handle_focus_event(
×
NEW
206
                                &evt,
×
NEW
207
                                focus_regions,
×
NEW
208
                                &map_region,
×
NEW
209
                                &_map_focus,
×
NEW
210
                            );
×
NEW
211
                            return flush_mouse_capture(app, ControlFlow::Continue);
×
NEW
212
                        }
×
NEW
213
                        if dispatch(&evt, app) {
×
NEW
214
                            return flush_mouse_capture(app, ControlFlow::Continue);
×
NEW
215
                        }
×
NEW
216
                        let _ = app.windows().handle_focus_event(
×
NEW
217
                            &evt,
×
NEW
218
                            focus_regions,
×
NEW
219
                            &map_region,
×
NEW
220
                            &_map_focus,
×
NEW
221
                        );
×
222
                        return flush_mouse_capture(app, ControlFlow::Continue);
×
223
                    }
NEW
224
                    Event::Key(_) if app.windows().capture_active() => {
×
NEW
225
                        app.windows().clear_capture();
×
NEW
226
                        let _ = dispatch(&evt, app);
×
NEW
227
                    }
×
NEW
228
                    _ => {
×
NEW
229
                        let _ = app.windows().handle_focus_event(
×
NEW
230
                            &evt,
×
NEW
231
                            focus_regions,
×
NEW
232
                            &map_region,
×
NEW
233
                            &_map_focus,
×
NEW
234
                        );
×
NEW
235
                        let _ = dispatch(&evt, app);
×
NEW
236
                    }
×
237
                }
238
            } else {
NEW
239
                if should_quit(None, app) {
×
NEW
240
                    return flush_mouse_capture(app, ControlFlow::Quit);
×
UNCOV
241
                }
×
NEW
242
                app.windows().begin_frame();
×
NEW
243
                output.draw(|frame| draw(frame, app))?;
×
244
            }
NEW
245
            flush_mouse_capture(app, ControlFlow::Continue)
×
NEW
246
        };
×
247

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

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

273
    Ok(())
×
274
}
×
275

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

318
struct WindowDrawState<R> {
319
    known: Vec<R>,
320
}
321

322
impl<R> Default for WindowDrawState<R> {
323
    fn default() -> Self {
1✔
324
        Self { known: Vec::new() }
1✔
325
    }
1✔
326
}
327

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

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

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

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

406
#[cfg(test)]
407
mod tests {
408
    use super::*;
409
    #[test]
410
    fn auto_layout_empty_and_multiple() {
1✔
411
        let empty: Vec<u8> = vec![];
1✔
412
        assert!(auto_layout_for_windows(&empty).is_none());
1✔
413

414
        let one = vec![1u8];
1✔
415
        let layout = auto_layout_for_windows(&one).unwrap();
1✔
416
        // single node should be a leaf
417
        assert!(matches!(layout.root(), crate::layout::LayoutNode::Leaf(_)));
1✔
418

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

428
    #[test]
429
    fn window_draw_state_update_changes() {
1✔
430
        let mut s: WindowDrawState<u8> = WindowDrawState::default();
1✔
431
        assert!(!s.update(&[]));
1✔
432
        assert!(s.update(&[1, 2]));
1✔
433
        assert!(!s.update(&[1, 2]));
1✔
434
    }
1✔
435
}
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