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

Commit

Permalink
issue: Implement tui component for comments
Browse files Browse the repository at this point in the history
Signed-off-by: Erik Kundt <[email protected]>
  • Loading branch information
erak committed Nov 7, 2022
1 parent b305479 commit 77d8f0c
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 26 deletions.
74 changes: 59 additions & 15 deletions issue/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ use radicle_terminal_tui as tui;
use tui::components::{ApplicationTitle, Shortcut, ShortcutBar, TabContainer};
use tui::{App, Tui};

use super::components::{GlobalListener, IssueList};
use super::components::{CommentList, GlobalListener, IssueList};

/// Messages handled by this tui-application.
#[derive(Debug, Eq, PartialEq)]
pub enum Message {
TabChanged(usize),
EnterDetail(IssueId),
LeaveDetail,
Quit,
}

Expand All @@ -29,14 +31,21 @@ pub enum Message {
pub enum Id {
Global,
Title,
Content,
Browser,
Detail,
Shortcuts,
}

#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
pub enum Group {
Open,
Closed,
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum Mode {
Browser,
Detail,
}

impl Default for Mode {
fn default() -> Self {
Mode::Browser
}
}

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

impl From<&IssueGroups> for Vec<(IssueId, Issue)> {
fn from(groups: &IssueGroups) -> Self {
[groups.open.clone(), groups.closed.clone()].concat()
}
}

/// App-window used by this application.
#[derive(Default)]
pub struct IssueTui {
/// Issues currently displayed by this tui.
issues: IssueGroups,
/// Current issue.
active: Option<(Group, IssueId)>,
/// Represents the active view
mode: Mode,
/// True if application should quit.
quit: bool,
}
Expand All @@ -69,7 +84,7 @@ impl IssueTui {

Self {
issues: Self::group_issues(&issues),
active: None,
mode: Mode::Browser,
quit: false,
}
}
Expand Down Expand Up @@ -139,15 +154,15 @@ impl Tui<Id, Message> for IssueTui {
fn init(&mut self, app: &mut App<Id, Message>) -> Result<()> {
app.mount(Id::Title, ApplicationTitle::new("my-project"), vec![])?;
app.mount(
Id::Content,
Id::Browser,
TabContainer::default()
.child(
format!("{} Open", self.issues.open.len()),
IssueList::new(self.issues.open.clone(), Group::Open),
IssueList::new(self.issues.open.clone()),
)
.child(
format!("{} Closed", self.issues.closed.len()),
IssueList::new(self.issues.closed.clone(), Group::Closed),
IssueList::new(self.issues.closed.clone()),
),
vec![
Sub::new(
Expand All @@ -174,6 +189,8 @@ impl Tui<Id, Message> for IssueTui {
],
)?;

app.mount(Id::Detail, CommentList::<()>::new(vec![]), vec![])?;

app.mount(
Id::Shortcuts,
ShortcutBar::default().child(Shortcut::new("q", "quit")),
Expand All @@ -193,23 +210,50 @@ impl Tui<Id, Message> for IssueTui {
)?;

// We need to give focus to a component then
app.activate(Id::Content)?;
app.activate(Id::Browser)?;

Ok(())
}

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

app.view(Id::Title, frame, layout[0]);
app.view(Id::Content, frame, layout[1]);
match self.mode {
Mode::Browser => {
app.view(Id::Title, frame, layout[0]);
app.view(Id::Browser, frame, layout[1]);
}
Mode::Detail => {
app.view(Id::Detail, frame, layout[1]);
}
}
app.view(Id::Shortcuts, frame, layout[2]);
}

fn update(&mut self, app: &mut App<Id, Message>) {
for message in app.poll() {
match message {
Message::Quit => self.quit = true,
Message::EnterDetail(issue_id) => {
let issues = Vec::<(IssueId, Issue)>::from(&self.issues);
if let Some((_, issue)) = issues.iter().find(|(id, _)| *id == issue_id) {
let comments = issue
.comments()
.iter()
.map(|comment| comment.clone())
.collect::<Vec<_>>();

self.mode = Mode::Detail;

app.remount(Id::Detail, CommentList::new(comments), vec![])
.ok();
app.activate(Id::Detail).ok();
}
}
Message::LeaveDetail => {
self.mode = Mode::Browser;
app.blur().ok();
}
_ => {}
}
}
Expand Down
132 changes: 121 additions & 11 deletions issue/src/tui/components.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use timeago;

use librad::collaborative_objects::ObjectId;

use tui_realm_stdlib::Phantom;

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

use radicle_common::cobs::issue::*;
use radicle_common::cobs::Comment;
use radicle_terminal_tui as tui;
use tui::components::{ApplicationTitle, ShortcutBar, TabContainer};
use tui::state::ListState;

use super::app::Group;
use super::app::Message;

/// Since `terminal-tui` does not know the type of messages that are being
Expand Down Expand Up @@ -64,6 +67,20 @@ impl Component<Message, NoUserEvent> for TabContainer {
_ => None,
}
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => match self.perform(Cmd::Submit) {
CmdResult::Batch(batch) => batch.iter().fold(None, |_, result| match result {
CmdResult::Submit(State::One(StateValue::String(id))) => {
match ObjectId::from_str(&id) {
Ok(id) => Some(Message::EnterDetail(id)),
Err(_) => None,
}
}
_ => None,
}),
_ => None,
},
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
None
Expand All @@ -80,16 +97,14 @@ impl Component<Message, NoUserEvent> for TabContainer {
}

pub struct IssueList {
props: Props,
group: Group,
attributes: Props,
issues: ListState<(IssueId, Issue)>,
}

impl IssueList {
pub fn new(issues: Vec<(IssueId, Issue)>, group: Group) -> Self {
pub fn new(issues: Vec<(IssueId, Issue)>) -> Self {
Self {
props: Props::default(),
group: group,
attributes: Props::default(),
issues: ListState::new(issues),
}
}
Expand Down Expand Up @@ -150,11 +165,11 @@ impl MockComponent for IssueList {
}

fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.props.get(attr)
self.attributes.get(attr)
}

fn attr(&mut self, attr: Attribute, value: AttrValue) {
self.props.set(attr, value);
self.attributes.set(attr, value);
}

fn state(&self) -> State {
Expand All @@ -165,18 +180,113 @@ impl MockComponent for IssueList {
match cmd {
Cmd::Move(Direction::Up) => {
self.issues.select_previous();
let selected = self.issues.items().selected_index();
CmdResult::Changed(State::One(StateValue::Usize(selected)))
}
Cmd::Move(Direction::Down) => {
self.issues.select_next();
let selected = self.issues.items().selected_index();
CmdResult::Changed(State::One(StateValue::Usize(selected)))
}
_ => {}
Cmd::Submit => {
let (id, _) = self.issues.items().selected().unwrap();
CmdResult::Submit(State::One(StateValue::String(id.to_string())))
}
_ => CmdResult::None,
}
CmdResult::None
}
}

impl Component<Message, NoUserEvent> for IssueList {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Message> {
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
None
}
}

pub struct CommentList<R> {
attributes: Props,
comments: ListState<Comment<R>>,
}

impl<R> CommentList<R> {
pub fn new(comments: Vec<Comment<R>>) -> Self {
Self {
attributes: Props::default(),
comments: ListState::new(comments),
}
}

fn items(&self, comment: &Comment<R>) -> ListItem {
let lines = vec![Spans::from(Span::styled(
comment.body.clone(),
Style::default().fg(Color::Rgb(117, 113, 249)),
))];
ListItem::new(lines)
}
}

impl<R> MockComponent for CommentList<R> {
fn view(&mut self, render: &mut Frame, area: Rect) {
let items = self
.comments
.items()
.all()
.iter()
.map(|comment| self.items(comment))
.collect::<Vec<_>>();

let list = TuiList::new(items)
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().fg(Color::Rgb(238, 111, 248)))
.highlight_symbol("│ ")
.repeat_highlight_symbol(true);

let mut state: TuiListState = TuiListState::default();
state.select(Some(self.comments.items().selected_index()));
render.render_stateful_widget(list, area, &mut state);
}

fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.attributes.get(attr)
}

fn attr(&mut self, attr: Attribute, value: AttrValue) {
self.attributes.set(attr, value);
}

fn state(&self) -> State {
State::One(StateValue::Usize(self.comments.items().selected_index()))
}

fn perform(&mut self, cmd: Cmd) -> CmdResult {
match cmd {
Cmd::Move(Direction::Up) => {
self.comments.select_previous();
}
Cmd::Move(Direction::Down) => {
self.comments.select_next();
}
_ => {}
}
CmdResult::None
}
}

impl<R> Component<Message, NoUserEvent> for CommentList<R> {
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
match event {
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
None
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
None
}
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Message::LeaveDetail),
_ => None,
}
}
}
18 changes: 18 additions & 0 deletions terminal-tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ where
Ok(())
}

pub fn remount<C>(
&mut self,
id: Id,
component: C,
subs: Vec<Sub<Id, NoUserEvent>>,
) -> Result<(), Error>
where
C: Component<Message, NoUserEvent> + 'static,
{
self.backend.remount(id, Box::new(component), subs)?;
Ok(())
}

pub fn blur(&mut self) -> Result<(), Error> {
self.backend.blur()?;
Ok(())
}

pub fn activate(&mut self, id: Id) -> Result<(), Error> {
self.backend.active(&id)?;
Ok(())
Expand Down

0 comments on commit 77d8f0c

Please sign in to comment.