radicle_cli/terminal/
issue.rs

1use std::io;
2
3use radicle_term::table::TableOptions;
4use radicle_term::{Table, VStack};
5
6use radicle::cob;
7use radicle::cob::issue;
8use radicle::cob::issue::CloseReason;
9use radicle::Profile;
10
11use crate::terminal as term;
12use crate::terminal::format::Author;
13use crate::terminal::Element;
14
15pub const OPEN_MSG: &str = r#"
16<!--
17Please enter an issue title and description.
18
19The first line is the issue title. The issue description
20follows, and must be separated by a blank line, just
21like a commit message. Markdown is supported in the title
22and description.
23-->
24"#;
25
26/// Display format.
27#[derive(Default, Debug, PartialEq, Eq)]
28pub enum Format {
29    #[default]
30    Full,
31    Header,
32}
33
34pub fn get_title_description(
35    title: Option<cob::Title>,
36    description: Option<String>,
37) -> io::Result<Option<(cob::Title, String)>> {
38    term::patch::Message::edit_title_description(title, description, OPEN_MSG)
39}
40
41pub fn show(
42    issue: &issue::Issue,
43    id: &cob::ObjectId,
44    format: Format,
45    verbose: bool,
46    profile: &Profile,
47) -> anyhow::Result<()> {
48    let labels: Vec<String> = issue.labels().cloned().map(|t| t.into()).collect();
49    let assignees: Vec<String> = issue
50        .assignees()
51        .map(|a| term::format::did(a).to_string())
52        .collect();
53    let author = issue.author();
54    let did = author.id();
55    let author = Author::new(did, profile, verbose);
56
57    let mut attrs = Table::<2, term::Line>::new(TableOptions {
58        spacing: 2,
59        ..TableOptions::default()
60    });
61
62    attrs.push([
63        term::format::tertiary("Title".to_owned()).into(),
64        term::format::bold(issue.title().to_owned()).into(),
65    ]);
66
67    attrs.push([
68        term::format::tertiary("Issue".to_owned()).into(),
69        term::format::bold(id.to_string()).into(),
70    ]);
71
72    attrs.push([
73        term::format::tertiary("Author".to_owned()).into(),
74        author.line(),
75    ]);
76
77    if !labels.is_empty() {
78        attrs.push([
79            term::format::tertiary("Labels".to_owned()).into(),
80            term::format::secondary(labels.join(", ")).into(),
81        ]);
82    }
83
84    if !assignees.is_empty() {
85        attrs.push([
86            term::format::tertiary("Assignees".to_owned()).into(),
87            term::format::dim(assignees.join(", ")).into(),
88        ]);
89    }
90
91    attrs.push([
92        term::format::tertiary("Status".to_owned()).into(),
93        match issue.state() {
94            issue::State::Open => term::format::positive("open".to_owned()).into(),
95            issue::State::Closed {
96                reason: CloseReason::Solved,
97            } => term::Line::spaced([
98                term::format::negative("closed").into(),
99                term::format::negative("(solved)").italic().dim().into(),
100            ]),
101            issue::State::Closed {
102                reason: CloseReason::Other,
103            } => term::Line::spaced([term::format::negative("closed").into()]),
104        },
105    ]);
106
107    let description = issue.description();
108    let mut widget = VStack::default()
109        .border(Some(term::colors::FAINT))
110        .child(attrs)
111        .children(if !description.is_empty() {
112            vec![
113                term::Label::blank().boxed(),
114                term::textarea(description.trim()).wrap(60).boxed(),
115            ]
116        } else {
117            vec![]
118        });
119
120    if format == Format::Full {
121        for (id, comment) in issue.replies() {
122            let hstack = term::comment::header(id, comment, profile);
123
124            widget = widget.divider();
125            widget.push(hstack);
126            widget.push(term::textarea(comment.body()).wrap(60));
127        }
128    }
129    widget.print();
130
131    Ok(())
132}