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 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 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}