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

jzombie / term-wm / 20788957288

07 Jan 2026 04:43PM UTC coverage: 35.923% (+2.3%) from 33.582%
20788957288

push

github

web-flow
General UI improvements (#3)

* Rename to `DefaultDecorator`

* Add theme.rs

* Rename to term_color.rs

* Use static display order

* Add pill-like labels

* Migrate more state to state.rs

* Add `HeaderAction` struct

* Use same window title in title bar and window list

* Simplify window creation

* Prototype centralized output driver

* Prototype crash reporting

* Fix issue where dual image example would crash when moving windows

* Remove inner decorative frame in dual image example

* Preliminary support for window titles

* Extract window manager

* Fix issue where debug window would not auto-snap when other window snaps

* Fix issue where vertical resizing of tiles would become unresponsive

* Simplify window focusing

* Window header double-click toggles maximize/restore state

* Simplify io drivers

496 of 2597 new or added lines in 28 files covered. (19.1%)

19 existing lines in 8 files now uncovered.

2515 of 7001 relevant lines covered (35.92%)

2.06 hits per line

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

46.74
/src/components/toggle_list.rs
1
use crossterm::event::{Event, KeyCode};
2
use ratatui::layout::Rect;
3
use ratatui::style::{Modifier, Style};
4
use ratatui::widgets::{Block, Borders, List, ListItem, ListState};
5

6
use crate::components::scroll_view::ScrollView;
7
use crate::ui::UiFrame;
8

9
#[derive(Clone)]
10
pub struct ToggleItem {
11
    pub id: String,
12
    pub label: String,
13
    pub checked: bool,
14
}
15

16
pub struct ToggleListComponent {
17
    items: Vec<ToggleItem>,
18
    selected: usize,
19
    title: String,
20
    scroll_view: ScrollView,
21
}
22

23
impl ToggleListComponent {
24
    pub fn new<T: Into<String>>(title: T) -> Self {
2✔
25
        Self {
2✔
26
            items: Vec::new(),
2✔
27
            selected: 0,
2✔
28
            title: title.into(),
2✔
29
            scroll_view: ScrollView::new(),
2✔
30
        }
2✔
31
    }
2✔
32

33
    pub fn set_items(&mut self, items: Vec<ToggleItem>) {
2✔
34
        self.items = items;
2✔
35
        if self.selected >= self.items.len() {
2✔
36
            self.selected = self.items.len().saturating_sub(1);
×
37
        }
2✔
38
    }
2✔
39

40
    pub fn items(&self) -> &[ToggleItem] {
1✔
41
        &self.items
1✔
42
    }
1✔
43

44
    pub fn items_mut(&mut self) -> &mut [ToggleItem] {
×
45
        &mut self.items
×
46
    }
×
47

48
    pub fn selected(&self) -> usize {
7✔
49
        self.selected
7✔
50
    }
7✔
51

52
    pub fn set_selected(&mut self, selected: usize) {
×
53
        self.selected = selected.min(self.items.len().saturating_sub(1));
×
54
    }
×
55

56
    pub fn scroll_offset(&self) -> usize {
×
57
        self.scroll_view.offset()
×
58
    }
×
59

60
    pub fn move_selection(&mut self, delta: isize) {
3✔
61
        self.bump_selection(delta);
3✔
62
    }
3✔
63

64
    fn bump_selection(&mut self, delta: isize) {
4✔
65
        if self.items.is_empty() {
4✔
66
            self.selected = 0;
×
67
            return;
×
68
        }
4✔
69
        if delta.is_negative() {
4✔
70
            self.selected = self.selected.saturating_sub(delta.unsigned_abs());
1✔
71
        } else {
3✔
72
            self.selected = (self.selected + delta as usize).min(self.items.len() - 1);
3✔
73
        }
3✔
74
    }
4✔
75

76
    pub fn toggle_selected(&mut self) -> bool {
1✔
77
        if let Some(item) = self.items.get_mut(self.selected) {
1✔
78
            item.checked = !item.checked;
1✔
79
            return true;
1✔
80
        }
×
81
        false
×
82
    }
1✔
83

84
    fn keep_selected_in_view(&mut self, view: usize) {
×
85
        if view == 0 {
×
86
            self.scroll_view.set_offset(0);
×
87
            return;
×
88
        }
×
89
        if self.items.is_empty() {
×
90
            self.scroll_view.set_offset(0);
×
91
            return;
×
92
        }
×
93
        let mut offset = self.scroll_view.offset();
×
94
        if self.selected < offset {
×
95
            offset = self.selected;
×
96
        } else if self.selected >= offset + view {
×
97
            offset = self.selected + 1 - view;
×
98
        }
×
99
        self.scroll_view.set_offset(offset);
×
100
    }
×
101

102
    fn handle_scrollbar_event(&mut self, event: &Event) -> bool {
×
103
        let response = self.scroll_view.handle_event(event);
×
104
        if let Some(offset) = response.offset {
×
105
            self.scroll_view.set_offset(offset);
×
106
        }
×
107
        if response.handled {
×
108
            self.scroll_view
×
109
                .set_total_view(self.items.len(), self.scroll_view.view());
×
110
            let view = self.scroll_view.view();
×
111
            if view > 0 {
×
112
                if self.selected < self.scroll_view.offset() {
×
113
                    self.selected = self.scroll_view.offset();
×
114
                } else if self.selected >= self.scroll_view.offset() + view {
×
115
                    self.selected = self.scroll_view.offset() + view - 1;
×
116
                }
×
117
            }
×
118
        }
×
119
        response.handled
×
120
    }
×
121
}
122

123
impl super::Component for ToggleListComponent {
NEW
124
    fn render(&mut self, frame: &mut UiFrame<'_>, area: Rect, focused: bool) {
×
125
        let block = if focused {
×
126
            Block::default()
×
127
                .borders(Borders::ALL)
×
128
                .title(format!("{} (focus)", self.title))
×
NEW
129
                .border_style(Style::default().fg(crate::theme::success_fg()))
×
130
        } else {
131
            Block::default()
×
132
                .borders(Borders::ALL)
×
133
                .title(self.title.as_str())
×
134
        };
135
        let inner = block.inner(area);
×
136
        frame.render_widget(block, area);
×
137
        if inner.height == 0 || inner.width == 0 {
×
138
            return;
×
139
        }
×
140

141
        let total = self.items.len();
×
142
        let view = inner.height as usize;
×
143
        self.scroll_view.update(inner, total, view);
×
144
        self.keep_selected_in_view(view);
×
145

146
        let offset = self.scroll_view.offset();
×
147
        let items = self
×
148
            .items
×
149
            .iter()
×
150
            .skip(offset)
×
151
            .take(view)
×
152
            .map(|item| {
×
153
                let marker = if item.checked { "[x]" } else { "[ ]" };
×
154
                ListItem::new(format!("{marker} {}", item.label))
×
155
            })
×
156
            .collect::<Vec<_>>();
×
157

158
        let mut state = ListState::default();
×
159
        if total > 0 && self.selected >= offset {
×
160
            state.select(Some(self.selected - offset));
×
161
        }
×
162

163
        let list =
×
164
            List::new(items).highlight_style(Style::default().add_modifier(Modifier::REVERSED));
×
165
        frame.render_stateful_widget(list, inner, &mut state);
×
166
        self.scroll_view.render(frame);
×
167
    }
×
168

169
    fn handle_event(&mut self, event: &Event) -> bool {
3✔
170
        match event {
3✔
171
            Event::Key(key) => match key.code {
3✔
172
                KeyCode::Up | KeyCode::Char('k') => {
173
                    self.bump_selection(-1);
×
174
                    true
×
175
                }
176
                KeyCode::Down | KeyCode::Char('j') => {
177
                    self.bump_selection(1);
1✔
178
                    true
1✔
179
                }
180
                KeyCode::PageUp => {
181
                    self.bump_selection(-5);
×
182
                    true
×
183
                }
184
                KeyCode::PageDown => {
185
                    self.bump_selection(5);
×
186
                    true
×
187
                }
188
                KeyCode::Home => {
189
                    self.selected = 0;
1✔
190
                    true
1✔
191
                }
192
                KeyCode::End => {
193
                    if !self.items.is_empty() {
1✔
194
                        self.selected = self.items.len() - 1;
1✔
195
                    }
1✔
196
                    true
1✔
197
                }
198
                KeyCode::Char(' ') => self.toggle_selected(),
×
199
                _ => false,
×
200
            },
201
            Event::Mouse(_) => self.handle_scrollbar_event(event),
×
202
            _ => false,
×
203
        }
204
    }
3✔
205
}
206

207
#[cfg(test)]
208
mod tests {
209
    use super::*;
210
    use crate::components::Component;
211
    use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
212

213
    fn make_items(n: usize) -> Vec<ToggleItem> {
2✔
214
        (0..n)
2✔
215
            .map(|i| ToggleItem {
2✔
216
                id: format!("id{}", i),
8✔
217
                label: format!("label{}", i),
8✔
218
                checked: i % 2 == 0,
8✔
219
            })
8✔
220
            .collect()
2✔
221
    }
2✔
222

223
    #[test]
224
    fn bump_selection_bounds_and_toggle() {
1✔
225
        let mut t = ToggleListComponent::new("test");
1✔
226
        t.set_items(make_items(3));
1✔
227
        assert_eq!(t.selected(), 0);
1✔
228
        t.move_selection(1);
1✔
229
        assert_eq!(t.selected(), 1);
1✔
230
        t.move_selection(10);
1✔
231
        assert_eq!(t.selected(), 2);
1✔
232
        t.move_selection(-100);
1✔
233
        assert_eq!(t.selected(), 0);
1✔
234

235
        // toggle the first item
236
        assert!(t.toggle_selected());
1✔
237
        assert!(!t.items()[0].checked);
1✔
238
    }
1✔
239

240
    #[test]
241
    fn handle_event_navigation() {
1✔
242
        let mut t = ToggleListComponent::new("s");
1✔
243
        t.set_items(make_items(5));
1✔
244
        t.handle_event(&Event::Key(KeyEvent::new(
1✔
245
            KeyCode::Down,
1✔
246
            KeyModifiers::NONE,
1✔
247
        )));
1✔
248
        assert_eq!(t.selected(), 1);
1✔
249
        t.handle_event(&Event::Key(KeyEvent::new(
1✔
250
            KeyCode::Home,
1✔
251
            KeyModifiers::NONE,
1✔
252
        )));
1✔
253
        assert_eq!(t.selected(), 0);
1✔
254
        t.handle_event(&Event::Key(KeyEvent::new(KeyCode::End, KeyModifiers::NONE)));
1✔
255
        assert_eq!(t.selected(), 4);
1✔
256
    }
1✔
257
}
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