Skip to content
This repository was archived by the owner on May 11, 2023. It is now read-only.

Commit 77d8f0c

Browse files
committed
issue: Implement tui component for comments
Signed-off-by: Erik Kundt <[email protected]>
1 parent b305479 commit 77d8f0c

File tree

3 files changed

+198
-26
lines changed

3 files changed

+198
-26
lines changed

issue/src/tui/app.rs

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ use radicle_terminal_tui as tui;
1515
use tui::components::{ApplicationTitle, Shortcut, ShortcutBar, TabContainer};
1616
use tui::{App, Tui};
1717

18-
use super::components::{GlobalListener, IssueList};
18+
use super::components::{CommentList, GlobalListener, IssueList};
1919

2020
/// Messages handled by this tui-application.
2121
#[derive(Debug, Eq, PartialEq)]
2222
pub enum Message {
2323
TabChanged(usize),
24+
EnterDetail(IssueId),
25+
LeaveDetail,
2426
Quit,
2527
}
2628

@@ -29,14 +31,21 @@ pub enum Message {
2931
pub enum Id {
3032
Global,
3133
Title,
32-
Content,
34+
Browser,
35+
Detail,
3336
Shortcuts,
3437
}
3538

36-
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
37-
pub enum Group {
38-
Open,
39-
Closed,
39+
#[derive(Debug, Eq, PartialEq, Clone)]
40+
pub enum Mode {
41+
Browser,
42+
Detail,
43+
}
44+
45+
impl Default for Mode {
46+
fn default() -> Self {
47+
Mode::Browser
48+
}
4049
}
4150

4251
#[derive(Default)]
@@ -45,13 +54,19 @@ pub struct IssueGroups {
4554
closed: Vec<(IssueId, Issue)>,
4655
}
4756

57+
impl From<&IssueGroups> for Vec<(IssueId, Issue)> {
58+
fn from(groups: &IssueGroups) -> Self {
59+
[groups.open.clone(), groups.closed.clone()].concat()
60+
}
61+
}
62+
4863
/// App-window used by this application.
4964
#[derive(Default)]
5065
pub struct IssueTui {
5166
/// Issues currently displayed by this tui.
5267
issues: IssueGroups,
53-
/// Current issue.
54-
active: Option<(Group, IssueId)>,
68+
/// Represents the active view
69+
mode: Mode,
5570
/// True if application should quit.
5671
quit: bool,
5772
}
@@ -69,7 +84,7 @@ impl IssueTui {
6984

7085
Self {
7186
issues: Self::group_issues(&issues),
72-
active: None,
87+
mode: Mode::Browser,
7388
quit: false,
7489
}
7590
}
@@ -139,15 +154,15 @@ impl Tui<Id, Message> for IssueTui {
139154
fn init(&mut self, app: &mut App<Id, Message>) -> Result<()> {
140155
app.mount(Id::Title, ApplicationTitle::new("my-project"), vec![])?;
141156
app.mount(
142-
Id::Content,
157+
Id::Browser,
143158
TabContainer::default()
144159
.child(
145160
format!("{} Open", self.issues.open.len()),
146-
IssueList::new(self.issues.open.clone(), Group::Open),
161+
IssueList::new(self.issues.open.clone()),
147162
)
148163
.child(
149164
format!("{} Closed", self.issues.closed.len()),
150-
IssueList::new(self.issues.closed.clone(), Group::Closed),
165+
IssueList::new(self.issues.closed.clone()),
151166
),
152167
vec![
153168
Sub::new(
@@ -174,6 +189,8 @@ impl Tui<Id, Message> for IssueTui {
174189
],
175190
)?;
176191

192+
app.mount(Id::Detail, CommentList::<()>::new(vec![]), vec![])?;
193+
177194
app.mount(
178195
Id::Shortcuts,
179196
ShortcutBar::default().child(Shortcut::new("q", "quit")),
@@ -193,23 +210,50 @@ impl Tui<Id, Message> for IssueTui {
193210
)?;
194211

195212
// We need to give focus to a component then
196-
app.activate(Id::Content)?;
213+
app.activate(Id::Browser)?;
197214

198215
Ok(())
199216
}
200217

201218
fn view(&mut self, app: &mut App<Id, Message>, frame: &mut Frame) {
202219
let layout = Self::layout(app, frame);
203220

204-
app.view(Id::Title, frame, layout[0]);
205-
app.view(Id::Content, frame, layout[1]);
221+
match self.mode {
222+
Mode::Browser => {
223+
app.view(Id::Title, frame, layout[0]);
224+
app.view(Id::Browser, frame, layout[1]);
225+
}
226+
Mode::Detail => {
227+
app.view(Id::Detail, frame, layout[1]);
228+
}
229+
}
206230
app.view(Id::Shortcuts, frame, layout[2]);
207231
}
208232

209233
fn update(&mut self, app: &mut App<Id, Message>) {
210234
for message in app.poll() {
211235
match message {
212236
Message::Quit => self.quit = true,
237+
Message::EnterDetail(issue_id) => {
238+
let issues = Vec::<(IssueId, Issue)>::from(&self.issues);
239+
if let Some((_, issue)) = issues.iter().find(|(id, _)| *id == issue_id) {
240+
let comments = issue
241+
.comments()
242+
.iter()
243+
.map(|comment| comment.clone())
244+
.collect::<Vec<_>>();
245+
246+
self.mode = Mode::Detail;
247+
248+
app.remount(Id::Detail, CommentList::new(comments), vec![])
249+
.ok();
250+
app.activate(Id::Detail).ok();
251+
}
252+
}
253+
Message::LeaveDetail => {
254+
self.mode = Mode::Browser;
255+
app.blur().ok();
256+
}
213257
_ => {}
214258
}
215259
}

issue/src/tui/components.rs

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
use std::str::FromStr;
12
use std::time::{Duration, SystemTime, UNIX_EPOCH};
23

34
use timeago;
45

6+
use librad::collaborative_objects::ObjectId;
7+
58
use tui_realm_stdlib::Phantom;
69

710
use tuirealm::command::{Cmd, CmdResult, Direction};
@@ -14,11 +17,11 @@ use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState as TuiListStat
1417
use tuirealm::{Component, Frame, MockComponent, NoUserEvent, State, StateValue};
1518

1619
use radicle_common::cobs::issue::*;
20+
use radicle_common::cobs::Comment;
1721
use radicle_terminal_tui as tui;
1822
use tui::components::{ApplicationTitle, ShortcutBar, TabContainer};
1923
use tui::state::ListState;
2024

21-
use super::app::Group;
2225
use super::app::Message;
2326

2427
/// Since `terminal-tui` does not know the type of messages that are being
@@ -64,6 +67,20 @@ impl Component<Message, NoUserEvent> for TabContainer {
6467
_ => None,
6568
}
6669
}
70+
Event::Keyboard(KeyEvent {
71+
code: Key::Enter, ..
72+
}) => match self.perform(Cmd::Submit) {
73+
CmdResult::Batch(batch) => batch.iter().fold(None, |_, result| match result {
74+
CmdResult::Submit(State::One(StateValue::String(id))) => {
75+
match ObjectId::from_str(&id) {
76+
Ok(id) => Some(Message::EnterDetail(id)),
77+
Err(_) => None,
78+
}
79+
}
80+
_ => None,
81+
}),
82+
_ => None,
83+
},
6784
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
6885
self.perform(Cmd::Move(Direction::Up));
6986
None
@@ -80,16 +97,14 @@ impl Component<Message, NoUserEvent> for TabContainer {
8097
}
8198

8299
pub struct IssueList {
83-
props: Props,
84-
group: Group,
100+
attributes: Props,
85101
issues: ListState<(IssueId, Issue)>,
86102
}
87103

88104
impl IssueList {
89-
pub fn new(issues: Vec<(IssueId, Issue)>, group: Group) -> Self {
105+
pub fn new(issues: Vec<(IssueId, Issue)>) -> Self {
90106
Self {
91-
props: Props::default(),
92-
group: group,
107+
attributes: Props::default(),
93108
issues: ListState::new(issues),
94109
}
95110
}
@@ -150,11 +165,11 @@ impl MockComponent for IssueList {
150165
}
151166

152167
fn query(&self, attr: Attribute) -> Option<AttrValue> {
153-
self.props.get(attr)
168+
self.attributes.get(attr)
154169
}
155170

156171
fn attr(&mut self, attr: Attribute, value: AttrValue) {
157-
self.props.set(attr, value);
172+
self.attributes.set(attr, value);
158173
}
159174

160175
fn state(&self) -> State {
@@ -165,18 +180,113 @@ impl MockComponent for IssueList {
165180
match cmd {
166181
Cmd::Move(Direction::Up) => {
167182
self.issues.select_previous();
183+
let selected = self.issues.items().selected_index();
184+
CmdResult::Changed(State::One(StateValue::Usize(selected)))
168185
}
169186
Cmd::Move(Direction::Down) => {
170187
self.issues.select_next();
188+
let selected = self.issues.items().selected_index();
189+
CmdResult::Changed(State::One(StateValue::Usize(selected)))
171190
}
172-
_ => {}
191+
Cmd::Submit => {
192+
let (id, _) = self.issues.items().selected().unwrap();
193+
CmdResult::Submit(State::One(StateValue::String(id.to_string())))
194+
}
195+
_ => CmdResult::None,
173196
}
174-
CmdResult::None
175197
}
176198
}
177199

178200
impl Component<Message, NoUserEvent> for IssueList {
179-
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Message> {
201+
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
180202
None
181203
}
182204
}
205+
206+
pub struct CommentList<R> {
207+
attributes: Props,
208+
comments: ListState<Comment<R>>,
209+
}
210+
211+
impl<R> CommentList<R> {
212+
pub fn new(comments: Vec<Comment<R>>) -> Self {
213+
Self {
214+
attributes: Props::default(),
215+
comments: ListState::new(comments),
216+
}
217+
}
218+
219+
fn items(&self, comment: &Comment<R>) -> ListItem {
220+
let lines = vec![Spans::from(Span::styled(
221+
comment.body.clone(),
222+
Style::default().fg(Color::Rgb(117, 113, 249)),
223+
))];
224+
ListItem::new(lines)
225+
}
226+
}
227+
228+
impl<R> MockComponent for CommentList<R> {
229+
fn view(&mut self, render: &mut Frame, area: Rect) {
230+
let items = self
231+
.comments
232+
.items()
233+
.all()
234+
.iter()
235+
.map(|comment| self.items(comment))
236+
.collect::<Vec<_>>();
237+
238+
let list = TuiList::new(items)
239+
.style(Style::default().fg(Color::White))
240+
.highlight_style(Style::default().fg(Color::Rgb(238, 111, 248)))
241+
.highlight_symbol("│ ")
242+
.repeat_highlight_symbol(true);
243+
244+
let mut state: TuiListState = TuiListState::default();
245+
state.select(Some(self.comments.items().selected_index()));
246+
render.render_stateful_widget(list, area, &mut state);
247+
}
248+
249+
fn query(&self, attr: Attribute) -> Option<AttrValue> {
250+
self.attributes.get(attr)
251+
}
252+
253+
fn attr(&mut self, attr: Attribute, value: AttrValue) {
254+
self.attributes.set(attr, value);
255+
}
256+
257+
fn state(&self) -> State {
258+
State::One(StateValue::Usize(self.comments.items().selected_index()))
259+
}
260+
261+
fn perform(&mut self, cmd: Cmd) -> CmdResult {
262+
match cmd {
263+
Cmd::Move(Direction::Up) => {
264+
self.comments.select_previous();
265+
}
266+
Cmd::Move(Direction::Down) => {
267+
self.comments.select_next();
268+
}
269+
_ => {}
270+
}
271+
CmdResult::None
272+
}
273+
}
274+
275+
impl<R> Component<Message, NoUserEvent> for CommentList<R> {
276+
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
277+
match event {
278+
Event::Keyboard(KeyEvent {
279+
code: Key::Down, ..
280+
}) => {
281+
self.perform(Cmd::Move(Direction::Down));
282+
None
283+
}
284+
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
285+
self.perform(Cmd::Move(Direction::Up));
286+
None
287+
}
288+
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Message::LeaveDetail),
289+
_ => None,
290+
}
291+
}
292+
}

terminal-tui/src/lib.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,24 @@ where
6060
Ok(())
6161
}
6262

63+
pub fn remount<C>(
64+
&mut self,
65+
id: Id,
66+
component: C,
67+
subs: Vec<Sub<Id, NoUserEvent>>,
68+
) -> Result<(), Error>
69+
where
70+
C: Component<Message, NoUserEvent> + 'static,
71+
{
72+
self.backend.remount(id, Box::new(component), subs)?;
73+
Ok(())
74+
}
75+
76+
pub fn blur(&mut self) -> Result<(), Error> {
77+
self.backend.blur()?;
78+
Ok(())
79+
}
80+
6381
pub fn activate(&mut self, id: Id) -> Result<(), Error> {
6482
self.backend.active(&id)?;
6583
Ok(())

0 commit comments

Comments
 (0)