Skip to content

Commit 62d0347

Browse files
committed
Add popup for tags
This closes gitui-org#483.
1 parent a31f185 commit 62d0347

File tree

8 files changed

+360
-2
lines changed

8 files changed

+360
-2
lines changed

src/app.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::{
99
InspectCommitComponent, MsgComponent, PullComponent,
1010
PushComponent, PushTagsComponent, RenameBranchComponent,
1111
ResetComponent, RevisionFilesComponent, StashMsgComponent,
12-
TagCommitComponent,
12+
TagCommitComponent, TagListComponent,
1313
},
1414
input::{Input, InputEvent, InputState},
1515
keys::{KeyConfig, SharedKeyConfig},
@@ -54,6 +54,7 @@ pub struct App {
5454
create_branch_popup: CreateBranchComponent,
5555
rename_branch_popup: RenameBranchComponent,
5656
select_branch_popup: BranchListComponent,
57+
tags_popup: TagListComponent,
5758
cmdbar: RefCell<CommandBar>,
5859
tab: usize,
5960
revlog: Revlog,
@@ -162,6 +163,10 @@ impl App {
162163
theme.clone(),
163164
key_config.clone(),
164165
),
166+
tags_popup: TagListComponent::new(
167+
theme.clone(),
168+
key_config.clone(),
169+
),
165170
do_quit: false,
166171
cmdbar: RefCell::new(CommandBar::new(
167172
theme.clone(),
@@ -395,6 +400,7 @@ impl App {
395400
rename_branch_popup,
396401
select_branch_popup,
397402
revision_files_popup,
403+
tags_popup,
398404
help,
399405
revlog,
400406
status_tab,
@@ -548,6 +554,9 @@ impl App {
548554
InternalEvent::SelectBranch => {
549555
self.select_branch_popup.open()?;
550556
}
557+
InternalEvent::Tags => {
558+
self.tags_popup.open()?;
559+
}
551560
InternalEvent::TabSwitch => self.set_tab(0)?,
552561
InternalEvent::InspectCommit(id, tags) => {
553562
self.inspect_commit_popup.open(id, tags)?;
@@ -694,6 +703,7 @@ impl App {
694703
|| self.push_tags_popup.is_visible()
695704
|| self.pull_popup.is_visible()
696705
|| self.select_branch_popup.is_visible()
706+
|| self.tags_popup.is_visible()
697707
|| self.rename_branch_popup.is_visible()
698708
|| self.revision_files_popup.is_visible()
699709
}
@@ -721,6 +731,7 @@ impl App {
721731
self.external_editor_popup.draw(f, size)?;
722732
self.tag_commit_popup.draw(f, size)?;
723733
self.select_branch_popup.draw(f, size)?;
734+
self.tags_popup.draw(f, size)?;
724735
self.create_branch_popup.draw(f, size)?;
725736
self.rename_branch_popup.draw(f, size)?;
726737
self.revision_files_popup.draw(f, size)?;

src/components/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ mod reset;
2121
mod revision_files;
2222
mod stashmsg;
2323
mod tag_commit;
24+
mod taglist;
2425
mod textinput;
2526
mod utils;
2627

@@ -46,6 +47,7 @@ pub use reset::ResetComponent;
4647
pub use revision_files::RevisionFilesComponent;
4748
pub use stashmsg::StashMsgComponent;
4849
pub use tag_commit::TagCommitComponent;
50+
pub use taglist::TagListComponent;
4951
pub use textinput::{InputType, TextInputComponent};
5052
pub use utils::filetree::FileTreeItemKind;
5153

src/components/taglist.rs

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
use super::{
2+
utils, visibility_blocking, CommandBlocking, CommandInfo,
3+
Component, DrawableComponent, EventState,
4+
};
5+
use crate::{
6+
components::ScrollType,
7+
keys::SharedKeyConfig,
8+
strings,
9+
ui::{self, Size},
10+
};
11+
use anyhow::Result;
12+
use asyncgit::{
13+
sync::{get_commit_info, get_tags, CommitId},
14+
CWD,
15+
};
16+
use crossterm::event::Event;
17+
use std::convert::TryInto;
18+
use tui::{
19+
backend::Backend,
20+
layout::{Constraint, Margin, Rect},
21+
text::Span,
22+
widgets::{
23+
Block, BorderType, Borders, Cell, Clear, Row, Table,
24+
TableState,
25+
},
26+
Frame,
27+
};
28+
use ui::style::SharedTheme;
29+
30+
///
31+
struct Tag {
32+
name: String,
33+
author: String,
34+
time: i64,
35+
message: String,
36+
_commit_id: CommitId,
37+
}
38+
39+
///
40+
pub struct TagListComponent {
41+
theme: SharedTheme,
42+
tags: Option<Vec<Tag>>,
43+
visible: bool,
44+
table_state: std::cell::Cell<TableState>,
45+
current_height: std::cell::Cell<usize>,
46+
key_config: SharedKeyConfig,
47+
}
48+
49+
impl DrawableComponent for TagListComponent {
50+
fn draw<B: Backend>(
51+
&self,
52+
f: &mut Frame<B>,
53+
rect: Rect,
54+
) -> Result<()> {
55+
if self.visible {
56+
const PERCENT_SIZE: Size = Size::new(80, 50);
57+
const MIN_SIZE: Size = Size::new(60, 20);
58+
59+
let area = ui::centered_rect(
60+
PERCENT_SIZE.width,
61+
PERCENT_SIZE.height,
62+
f.size(),
63+
);
64+
let area =
65+
ui::rect_inside(MIN_SIZE, f.size().into(), area);
66+
let area = area.intersection(rect);
67+
68+
let tag_name_width =
69+
self.tags.as_ref().map_or(0, |tags| {
70+
tags.iter()
71+
.fold(0, |acc, tag| acc.max(tag.name.len()))
72+
});
73+
74+
let constraints = [
75+
// tag name
76+
Constraint::Length(tag_name_width.try_into()?),
77+
// commit date
78+
Constraint::Length(10),
79+
// author width
80+
Constraint::Length(19),
81+
// commit id
82+
Constraint::Min(0),
83+
];
84+
85+
let rows = self.get_rows();
86+
let number_of_rows = rows.len();
87+
88+
let table = Table::new(rows)
89+
.widths(&constraints)
90+
.column_spacing(1)
91+
.highlight_style(self.theme.text(true, true))
92+
.block(
93+
Block::default()
94+
.borders(Borders::ALL)
95+
.title(Span::styled(
96+
strings::title_tags(),
97+
self.theme.title(true),
98+
))
99+
.border_style(self.theme.block(true))
100+
.border_type(BorderType::Thick),
101+
);
102+
103+
let mut table_state = self.table_state.take();
104+
105+
f.render_widget(Clear, area);
106+
f.render_stateful_widget(table, area, &mut table_state);
107+
108+
let area = area.inner(&Margin {
109+
vertical: 1,
110+
horizontal: 0,
111+
});
112+
113+
ui::draw_scrollbar(
114+
f,
115+
area,
116+
&self.theme,
117+
number_of_rows,
118+
table_state.selected().unwrap_or(0),
119+
);
120+
121+
self.table_state.set(table_state);
122+
self.current_height.set(area.height.into());
123+
}
124+
125+
Ok(())
126+
}
127+
}
128+
129+
impl Component for TagListComponent {
130+
fn commands(
131+
&self,
132+
out: &mut Vec<CommandInfo>,
133+
force_all: bool,
134+
) -> CommandBlocking {
135+
if self.visible || force_all {
136+
out.clear();
137+
138+
out.push(CommandInfo::new(
139+
strings::commands::scroll(&self.key_config),
140+
true,
141+
true,
142+
));
143+
144+
out.push(CommandInfo::new(
145+
strings::commands::close_popup(&self.key_config),
146+
true,
147+
true,
148+
));
149+
}
150+
visibility_blocking(self)
151+
}
152+
153+
fn event(&mut self, event: Event) -> Result<EventState> {
154+
if self.visible {
155+
if let Event::Key(key) = event {
156+
if key == self.key_config.exit_popup {
157+
self.hide()
158+
} else if key == self.key_config.move_up {
159+
self.move_selection(ScrollType::Up);
160+
} else if key == self.key_config.move_down {
161+
self.move_selection(ScrollType::Down);
162+
} else if key == self.key_config.shift_up
163+
|| key == self.key_config.home
164+
{
165+
self.move_selection(ScrollType::Home);
166+
} else if key == self.key_config.shift_down
167+
|| key == self.key_config.end
168+
{
169+
self.move_selection(ScrollType::End);
170+
} else if key == self.key_config.page_down {
171+
self.move_selection(ScrollType::PageDown);
172+
} else if key == self.key_config.page_up {
173+
self.move_selection(ScrollType::PageUp);
174+
}
175+
}
176+
177+
Ok(EventState::Consumed)
178+
} else {
179+
Ok(EventState::NotConsumed)
180+
}
181+
}
182+
183+
fn is_visible(&self) -> bool {
184+
self.visible
185+
}
186+
187+
fn hide(&mut self) {
188+
self.visible = false
189+
}
190+
191+
fn show(&mut self) -> Result<()> {
192+
self.visible = true;
193+
194+
Ok(())
195+
}
196+
}
197+
198+
impl TagListComponent {
199+
pub fn new(
200+
theme: SharedTheme,
201+
key_config: SharedKeyConfig,
202+
) -> Self {
203+
Self {
204+
theme,
205+
tags: None,
206+
visible: false,
207+
table_state: std::cell::Cell::new(TableState::default()),
208+
current_height: std::cell::Cell::new(0),
209+
key_config,
210+
}
211+
}
212+
213+
///
214+
pub fn open(&mut self) -> Result<()> {
215+
self.table_state.get_mut().select(Some(0));
216+
self.show()?;
217+
218+
self.update_tags()?;
219+
220+
Ok(())
221+
}
222+
223+
/// fetch list of tags
224+
pub fn update_tags(&mut self) -> Result<()> {
225+
let tags_grouped_by_commit_id = get_tags(CWD)?;
226+
227+
let mut tags: Vec<Tag> = tags_grouped_by_commit_id
228+
.iter()
229+
.flat_map(|(commit_id, tags)| {
230+
tags.iter()
231+
.map(|tag| {
232+
let commit_info =
233+
get_commit_info(CWD, commit_id)
234+
.expect("could not get commit info");
235+
236+
Tag {
237+
name: tag.clone(),
238+
author: commit_info.author,
239+
time: commit_info.time,
240+
message: commit_info.message,
241+
_commit_id: *commit_id,
242+
}
243+
})
244+
.collect::<Vec<Tag>>()
245+
})
246+
.collect();
247+
248+
tags.sort_by(|a, b| a.name.cmp(&b.name));
249+
250+
self.tags = Some(tags);
251+
252+
Ok(())
253+
}
254+
255+
///
256+
fn move_selection(&mut self, scroll_type: ScrollType) -> bool {
257+
let mut table_state = self.table_state.take();
258+
259+
let old_selection = table_state.selected().unwrap_or(0);
260+
let max_selection =
261+
self.tags.as_ref().map_or(0, |tags| tags.len() - 1);
262+
263+
let new_selection = match scroll_type {
264+
ScrollType::Up => old_selection.saturating_sub(1),
265+
ScrollType::Down => {
266+
old_selection.saturating_add(1).min(max_selection)
267+
}
268+
ScrollType::Home => 0,
269+
ScrollType::End => max_selection,
270+
ScrollType::PageUp => old_selection.saturating_sub(
271+
self.current_height.get().saturating_sub(2),
272+
),
273+
ScrollType::PageDown => old_selection
274+
.saturating_add(
275+
self.current_height.get().saturating_sub(2),
276+
)
277+
.min(max_selection),
278+
};
279+
280+
let needs_update = new_selection != old_selection;
281+
282+
table_state.select(Some(new_selection));
283+
self.table_state.set(table_state);
284+
285+
needs_update
286+
}
287+
288+
///
289+
fn get_rows(&self) -> Vec<Row> {
290+
if let Some(ref tags) = self.tags {
291+
tags.iter().map(|tag| self.get_row(tag)).collect()
292+
} else {
293+
vec![]
294+
}
295+
}
296+
297+
///
298+
fn get_row(&self, tag: &Tag) -> Row {
299+
let cells: Vec<Cell> = vec![
300+
Cell::from(tag.name.clone())
301+
.style(self.theme.text(true, false)),
302+
Cell::from(utils::time_to_string(tag.time, true))
303+
.style(self.theme.commit_time(false)),
304+
Cell::from(tag.author.clone())
305+
.style(self.theme.commit_author(false)),
306+
Cell::from(tag.message.clone())
307+
.style(self.theme.text(true, false)),
308+
];
309+
310+
Row::new(cells)
311+
}
312+
}

0 commit comments

Comments
 (0)