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

jzombie / term-wm / 20796477388

07 Jan 2026 09:04PM UTC coverage: 38.931% (+3.0%) from 35.923%
20796477388

Pull #5

github

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

629 of 1236 new or added lines in 16 files covered. (50.89%)

8 existing lines in 6 files now uncovered.

3044 of 7819 relevant lines covered (38.93%)

2.99 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::{Component, scroll_view::ScrollViewComponent};
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: ScrollViewComponent,
21
}
22

23
impl Component for ToggleListComponent {
NEW
24
    fn render(&mut self, frame: &mut UiFrame<'_>, area: Rect, focused: bool) {
×
NEW
25
        let block = if focused {
×
NEW
26
            Block::default()
×
NEW
27
                .borders(Borders::ALL)
×
NEW
28
                .title(format!("{} (focus)", self.title))
×
NEW
29
                .border_style(Style::default().fg(crate::theme::success_fg()))
×
30
        } else {
NEW
31
            Block::default()
×
NEW
32
                .borders(Borders::ALL)
×
NEW
33
                .title(self.title.as_str())
×
34
        };
NEW
35
        let inner = block.inner(area);
×
NEW
36
        frame.render_widget(block, area);
×
NEW
37
        if inner.height == 0 || inner.width == 0 {
×
NEW
38
            return;
×
NEW
39
        }
×
40

NEW
41
        let total = self.items.len();
×
NEW
42
        let view = inner.height as usize;
×
NEW
43
        self.scroll_view.update(inner, total, view);
×
NEW
44
        self.keep_selected_in_view(view);
×
45

NEW
46
        let offset = self.scroll_view.offset();
×
NEW
47
        let items = self
×
NEW
48
            .items
×
NEW
49
            .iter()
×
NEW
50
            .skip(offset)
×
NEW
51
            .take(view)
×
NEW
52
            .map(|item| {
×
NEW
53
                let marker = if item.checked { "[x]" } else { "[ ]" };
×
NEW
54
                ListItem::new(format!("{marker} {}", item.label))
×
NEW
55
            })
×
NEW
56
            .collect::<Vec<_>>();
×
57

NEW
58
        let mut state = ListState::default();
×
NEW
59
        if total > 0 && self.selected >= offset {
×
NEW
60
            state.select(Some(self.selected - offset));
×
NEW
61
        }
×
62

NEW
63
        let list =
×
NEW
64
            List::new(items).highlight_style(Style::default().add_modifier(Modifier::REVERSED));
×
NEW
65
        frame.render_stateful_widget(list, inner, &mut state);
×
NEW
66
        self.scroll_view.render(frame);
×
NEW
67
    }
×
68

69
    fn handle_event(&mut self, event: &Event) -> bool {
3✔
70
        match event {
3✔
71
            Event::Key(key) => match key.code {
3✔
72
                KeyCode::Up | KeyCode::Char('k') => {
NEW
73
                    self.bump_selection(-1);
×
NEW
74
                    true
×
75
                }
76
                KeyCode::Down | KeyCode::Char('j') => {
77
                    self.bump_selection(1);
1✔
78
                    true
1✔
79
                }
80
                KeyCode::PageUp => {
NEW
81
                    self.bump_selection(-5);
×
NEW
82
                    true
×
83
                }
84
                KeyCode::PageDown => {
NEW
85
                    self.bump_selection(5);
×
NEW
86
                    true
×
87
                }
88
                KeyCode::Home => {
89
                    self.selected = 0;
1✔
90
                    true
1✔
91
                }
92
                KeyCode::End => {
93
                    if !self.items.is_empty() {
1✔
94
                        self.selected = self.items.len() - 1;
1✔
95
                    }
1✔
96
                    true
1✔
97
                }
NEW
98
                KeyCode::Char(' ') => self.toggle_selected(),
×
NEW
99
                _ => false,
×
100
            },
NEW
101
            Event::Mouse(_) => self.handle_scrollbar_event(event),
×
NEW
102
            _ => false,
×
103
        }
104
    }
3✔
105
}
106

107
impl ToggleListComponent {
108
    pub fn new<T: Into<String>>(title: T) -> Self {
2✔
109
        Self {
2✔
110
            items: Vec::new(),
2✔
111
            selected: 0,
2✔
112
            title: title.into(),
2✔
113
            scroll_view: ScrollViewComponent::new(),
2✔
114
        }
2✔
115
    }
2✔
116

117
    pub fn set_items(&mut self, items: Vec<ToggleItem>) {
2✔
118
        self.items = items;
2✔
119
        if self.selected >= self.items.len() {
2✔
120
            self.selected = self.items.len().saturating_sub(1);
×
121
        }
2✔
122
    }
2✔
123

124
    pub fn items(&self) -> &[ToggleItem] {
1✔
125
        &self.items
1✔
126
    }
1✔
127

128
    pub fn items_mut(&mut self) -> &mut [ToggleItem] {
×
129
        &mut self.items
×
130
    }
×
131

132
    pub fn selected(&self) -> usize {
7✔
133
        self.selected
7✔
134
    }
7✔
135

136
    pub fn set_selected(&mut self, selected: usize) {
×
137
        self.selected = selected.min(self.items.len().saturating_sub(1));
×
138
    }
×
139

140
    pub fn scroll_offset(&self) -> usize {
×
141
        self.scroll_view.offset()
×
142
    }
×
143

144
    pub fn move_selection(&mut self, delta: isize) {
3✔
145
        self.bump_selection(delta);
3✔
146
    }
3✔
147

148
    fn bump_selection(&mut self, delta: isize) {
4✔
149
        if self.items.is_empty() {
4✔
150
            self.selected = 0;
×
151
            return;
×
152
        }
4✔
153
        if delta.is_negative() {
4✔
154
            self.selected = self.selected.saturating_sub(delta.unsigned_abs());
1✔
155
        } else {
3✔
156
            self.selected = (self.selected + delta as usize).min(self.items.len() - 1);
3✔
157
        }
3✔
158
    }
4✔
159

160
    pub fn toggle_selected(&mut self) -> bool {
1✔
161
        if let Some(item) = self.items.get_mut(self.selected) {
1✔
162
            item.checked = !item.checked;
1✔
163
            return true;
1✔
164
        }
×
165
        false
×
166
    }
1✔
167

168
    fn keep_selected_in_view(&mut self, view: usize) {
×
169
        if view == 0 {
×
170
            self.scroll_view.set_offset(0);
×
171
            return;
×
172
        }
×
173
        if self.items.is_empty() {
×
174
            self.scroll_view.set_offset(0);
×
175
            return;
×
176
        }
×
177
        let mut offset = self.scroll_view.offset();
×
178
        if self.selected < offset {
×
179
            offset = self.selected;
×
180
        } else if self.selected >= offset + view {
×
181
            offset = self.selected + 1 - view;
×
182
        }
×
183
        self.scroll_view.set_offset(offset);
×
184
    }
×
185

186
    fn handle_scrollbar_event(&mut self, event: &Event) -> bool {
×
187
        let response = self.scroll_view.handle_event(event);
×
NEW
188
        if let Some(offset) = response.v_offset {
×
189
            self.scroll_view.set_offset(offset);
×
190
        }
×
191
        if response.handled {
×
192
            self.scroll_view
×
193
                .set_total_view(self.items.len(), self.scroll_view.view());
×
194
            let view = self.scroll_view.view();
×
195
            if view > 0 {
×
196
                if self.selected < self.scroll_view.offset() {
×
197
                    self.selected = self.scroll_view.offset();
×
198
                } else if self.selected >= self.scroll_view.offset() + view {
×
199
                    self.selected = self.scroll_view.offset() + view - 1;
×
200
                }
×
201
            }
×
202
        }
×
203
        response.handled
×
204
    }
×
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