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

jzombie / term-wm / 20885462867

10 Jan 2026 10:38PM UTC coverage: 57.056% (+10.0%) from 47.071%
20885462867

Pull #20

github

web-flow
Merge d8a3a10c3 into bfecf0f75
Pull Request #20: Initial clipboard and offscreen buffer support

2046 of 3183 new or added lines in 26 files covered. (64.28%)

78 existing lines in 15 files now uncovered.

6788 of 11897 relevant lines covered (57.06%)

9.62 hits per line

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

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

6
use crate::components::{Component, ComponentContext};
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
}
21

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

NEW
37
        if inner.width == 0 || inner.height == 0 {
×
38
            return;
×
39
        }
×
40

NEW
41
        let total_count = self.items.len();
×
42
        // Assuming single line items
NEW
43
        if let Some(handle) = ctx.viewport_handle() {
×
NEW
44
            handle.set_content_size(inner.width as usize, total_count + 2);
×
NEW
45
            handle.ensure_vertical_visible(self.selected + 1, self.selected + 2);
×
NEW
46
        }
×
47

NEW
48
        let vp = ctx.viewport();
×
49
        // Similar logic to ListComponent
NEW
50
        let skip_n = vp.offset_y.saturating_sub(1);
×
51

NEW
52
        let items: Vec<ListItem> = self
×
53
            .items
×
54
            .iter()
×
NEW
55
            .enumerate()
×
NEW
56
            .skip(skip_n)
×
NEW
57
            .take(inner.height as usize)
×
NEW
58
            .map(|(i, item)| {
×
59
                let marker = if item.checked { "[x]" } else { "[ ]" };
×
NEW
60
                let mut li = ListItem::new(format!("{marker} {}", item.label));
×
NEW
61
                if i == self.selected {
×
NEW
62
                    li = li.style(Style::default().add_modifier(Modifier::REVERSED));
×
NEW
63
                }
×
NEW
64
                li
×
65
            })
×
66
            .collect::<Vec<_>>();
×
67

NEW
68
        let list = List::new(items);
×
NEW
69
        frame.render_widget(list, inner);
×
UNCOV
70
    }
×
71

72
    fn handle_event(&mut self, event: &Event, _ctx: &ComponentContext) -> bool {
3✔
73
        match event {
3✔
74
            Event::Key(key) => {
3✔
75
                let kb = crate::keybindings::KeyBindings::default();
3✔
76
                if kb.matches(crate::keybindings::Action::MenuUp, key)
3✔
77
                    || kb.matches(crate::keybindings::Action::MenuPrev, key)
3✔
78
                {
79
                    self.bump_selection(-1);
×
80
                    true
×
81
                } else if kb.matches(crate::keybindings::Action::MenuDown, key)
3✔
82
                    || kb.matches(crate::keybindings::Action::MenuNext, key)
2✔
83
                {
84
                    self.bump_selection(1);
1✔
85
                    true
1✔
86
                } else if kb.matches(crate::keybindings::Action::ScrollPageUp, key) {
2✔
87
                    self.bump_selection(-5);
×
88
                    true
×
89
                } else if kb.matches(crate::keybindings::Action::ScrollPageDown, key) {
2✔
90
                    self.bump_selection(5);
×
91
                    true
×
92
                } else if kb.matches(crate::keybindings::Action::ScrollHome, key) {
2✔
93
                    self.selected = 0;
1✔
94
                    true
1✔
95
                } else if kb.matches(crate::keybindings::Action::ScrollEnd, key) {
1✔
96
                    if !self.items.is_empty() {
1✔
97
                        self.selected = self.items.len() - 1;
1✔
98
                    }
1✔
99
                    true
1✔
100
                } else if kb.matches(crate::keybindings::Action::ToggleSelection, key) {
×
101
                    self.toggle_selected()
×
102
                } else {
103
                    false
×
104
                }
105
            }
106
            _ => false,
×
107
        }
108
    }
3✔
109
}
110

111
impl ToggleListComponent {
112
    pub fn new<T: Into<String>>(title: T) -> Self {
2✔
113
        Self {
2✔
114
            items: Vec::new(),
2✔
115
            selected: 0,
2✔
116
            title: title.into(),
2✔
117
        }
2✔
118
    }
2✔
119

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

127
    pub fn items(&self) -> &[ToggleItem] {
1✔
128
        &self.items
1✔
129
    }
1✔
130

131
    pub fn items_mut(&mut self) -> &mut [ToggleItem] {
×
132
        &mut self.items
×
133
    }
×
134

135
    pub fn selected(&self) -> usize {
7✔
136
        self.selected
7✔
137
    }
7✔
138

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

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

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

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

168
#[cfg(test)]
169
mod tests {
170
    use super::*;
171
    use crate::components::Component;
172
    use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
173

174
    fn make_items(n: usize) -> Vec<ToggleItem> {
2✔
175
        (0..n)
2✔
176
            .map(|i| ToggleItem {
2✔
177
                id: format!("id{}", i),
8✔
178
                label: format!("label{}", i),
8✔
179
                checked: i % 2 == 0,
8✔
180
            })
8✔
181
            .collect()
2✔
182
    }
2✔
183

184
    #[test]
185
    fn bump_selection_bounds_and_toggle() {
1✔
186
        let mut t = ToggleListComponent::new("test");
1✔
187
        t.set_items(make_items(3));
1✔
188
        assert_eq!(t.selected(), 0);
1✔
189
        t.move_selection(1);
1✔
190
        assert_eq!(t.selected(), 1);
1✔
191
        t.move_selection(10);
1✔
192
        assert_eq!(t.selected(), 2);
1✔
193
        t.move_selection(-100);
1✔
194
        assert_eq!(t.selected(), 0);
1✔
195

196
        // toggle the first item
197
        assert!(t.toggle_selected());
1✔
198
        assert!(!t.items()[0].checked);
1✔
199
    }
1✔
200

201
    #[test]
202
    fn handle_event_navigation() {
1✔
203
        let mut t = ToggleListComponent::new("s");
1✔
204
        t.set_items(make_items(5));
1✔
205
        let ctx = ComponentContext::new(true);
1✔
206
        t.handle_event(
1✔
207
            &Event::Key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)),
1✔
208
            &ctx,
1✔
209
        );
210
        assert_eq!(t.selected(), 1);
1✔
211
        t.handle_event(
1✔
212
            &Event::Key(KeyEvent::new(KeyCode::Home, KeyModifiers::NONE)),
1✔
213
            &ctx,
1✔
214
        );
215
        assert_eq!(t.selected(), 0);
1✔
216
        t.handle_event(
1✔
217
            &Event::Key(KeyEvent::new(KeyCode::End, KeyModifiers::NONE)),
1✔
218
            &ctx,
1✔
219
        );
220
        assert_eq!(t.selected(), 4);
1✔
221
    }
1✔
222
}
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