Skip to main content

githist/
lib.rs

1use crate::git::branching::BranchInfo;
2use ratatui::backend::CrosstermBackend;
3use ratatui::widgets::ListState;
4use ratatui::Terminal;
5use std::io::Stdout;
6
7pub mod git;
8pub mod ui;
9
10pub struct StatefulList {
11    pub state: ListState,
12    pub items: Vec<BranchInfo>,
13    pub filtered: Option<Box<Vec<BranchInfo>>>,
14}
15
16pub struct App {
17    pub items: StatefulList,
18    pub filter: String,
19    pub filter_mode: bool,
20    pub pending: String,
21    pub delete_confirmation: Option<String>,
22}
23
24impl StatefulList {
25    fn with_items(items: Vec<BranchInfo>) -> StatefulList {
26        let filtered = Some(Box::new(items.clone()));
27        StatefulList {
28            state: ListState::default(),
29            items,
30            filtered,
31        }
32    }
33
34    pub fn unselect(&mut self) {
35        self.state.select(None);
36    }
37
38    pub fn next(&mut self) {
39        let len = self
40            .filtered
41            .as_ref()
42            .map_or(0, |f| f.len());
43        if len == 0 {
44            return;
45        }
46        let i = match self.state.selected() {
47            Some(i) => {
48                if i >= len - 1 {
49                    0
50                } else {
51                    i + 1
52                }
53            }
54            None => 0,
55        };
56        self.state.select(Some(i));
57    }
58
59    pub fn previous(&mut self) {
60        let len = self
61            .filtered
62            .as_ref()
63            .map_or(0, |f| f.len());
64        if len == 0 {
65            return;
66        }
67        let i = match self.state.selected() {
68            Some(i) => {
69                if i == 0 {
70                    len - 1
71                } else {
72                    i - 1
73                }
74            }
75            None => 0,
76        };
77        self.state.select(Some(i));
78    }
79
80    pub fn page_down(&mut self, page_size: usize) {
81        let len = self.filtered.as_ref().map_or(0, |f| f.len());
82        if len == 0 {
83            return;
84        }
85        let i = self.state.selected().unwrap_or(0);
86        let new_i = (i + page_size).min(len - 1);
87        self.state.select(Some(new_i));
88    }
89
90    pub fn page_up(&mut self, page_size: usize) {
91        let len = self.filtered.as_ref().map_or(0, |f| f.len());
92        if len == 0 {
93            return;
94        }
95        let i = self.state.selected().unwrap_or(0);
96        let new_i = i.saturating_sub(page_size);
97        self.state.select(Some(new_i));
98    }
99
100    pub fn go_to_first(&mut self) {
101        let len = self.filtered.as_ref().map_or(0, |f| f.len());
102        if len > 0 {
103            self.state.select(Some(0));
104        }
105    }
106
107    pub fn go_to_last(&mut self) {
108        let len = self.filtered.as_ref().map_or(0, |f| f.len());
109        if len > 0 {
110            self.state.select(Some(len - 1));
111        }
112    }
113}
114
115pub struct NoSelectionError;
116
117impl App {
118    #[must_use]
119    pub fn new(branches: Vec<BranchInfo>) -> App {
120        App {
121            items: StatefulList::with_items(branches),
122            filter: String::new(),
123            filter_mode: false,
124            pending: String::new(),
125            delete_confirmation: None,
126        }
127    }
128    pub fn select_first_item_if_none(&mut self) {
129        if self.items.state.selected().is_none() {
130            self.items.state.select(Some(0));
131        }
132    }
133
134    /// # Errors
135    ///
136    /// Will return `NoSelectionError` if a branch was not selected.
137    pub fn get_selected_branch_info(&self) -> Result<BranchInfo, NoSelectionError> {
138        let index = self.items.state.selected().ok_or(NoSelectionError)?;
139        let filtered = self.items.filtered.as_ref().ok_or(NoSelectionError)?;
140        filtered.get(index).cloned().ok_or(NoSelectionError)
141    }
142
143    /// # Errors
144    ///
145    /// Will return `NoSelectionError` if a branch was not selected.
146    pub fn get_selected_branch_name(&self) -> Result<String, NoSelectionError> {
147        self.get_selected_branch_info()
148            .map(|info| info.branch_name)
149    }
150
151    pub fn filtered_len(&self) -> usize {
152        self.items.filtered.as_ref().map_or(0, |f| f.len())
153    }
154
155    pub fn total_len(&self) -> usize {
156        self.items.items.len()
157    }
158
159    pub fn update_with_status(
160        &mut self,
161        terminal: &mut Terminal<CrosstermBackend<Stdout>>,
162        pending_status: String,
163    ) {
164        self.filter.clear();
165        self.update_with_status_preserve_filter(terminal, pending_status);
166    }
167
168    pub fn update_with_status_preserve_filter(
169        &mut self,
170        terminal: &mut Terminal<CrosstermBackend<Stdout>>,
171        pending_status: String,
172    ) {
173        self.pending = pending_status;
174        terminal.draw(|f| self.ui(f)).expect("error updating!");
175    }
176
177    pub fn clear_pending_status(&mut self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) {
178        self.pending.clear();
179        terminal.draw(|f| self.ui(f)).expect("error updating!");
180    }
181
182    fn update_filtered(&mut self) {
183        let filtered: Vec<BranchInfo> = self
184            .items
185            .items
186            .clone()
187            .into_iter()
188            .filter(|x| {
189                if self.filter.is_empty() {
190                    true
191                } else {
192                    x.branch_name.to_lowercase().contains(&self.filter.to_lowercase())
193                }
194            })
195            .collect();
196        self.items.filtered = if filtered.is_empty() {
197            self.items.state.select(None);
198            None
199        } else {
200            self.items.state.select(Some(0));
201            Some(Box::new(filtered))
202        };
203    }
204
205    pub fn set_branches(&mut self, branches: Vec<BranchInfo>) {
206        self.items.items = branches;
207        self.update_filtered();
208    }
209}