radicle-cli 0.20.0

Radicle CLI
Documentation
use std::io;

use radicle_term::table::TableOptions;
use radicle_term::{Table, VStack};

use radicle::cob;
use radicle::cob::issue;
use radicle::cob::issue::CloseReason;
use radicle::Profile;

use crate::terminal as term;
use crate::terminal::format::Author;
use crate::terminal::Element;

pub const OPEN_MSG: &str = r#"
<!--
Please enter an issue title and description.

The first line is the issue title. The issue description
follows, and must be separated by a blank line, just
like a commit message. Markdown is supported in the title
and description.
-->
"#;

/// Display format.
#[derive(Default, Debug, PartialEq, Eq)]
pub enum Format {
    #[default]
    Full,
    Header,
}

pub fn get_title_description(
    title: Option<cob::Title>,
    description: Option<String>,
) -> io::Result<Option<(cob::Title, String)>> {
    term::patch::Message::edit_title_description(title, description, OPEN_MSG)
}

pub fn show(
    issue: &issue::Issue,
    id: &cob::ObjectId,
    format: Format,
    verbose: bool,
    profile: &Profile,
) -> anyhow::Result<()> {
    let labels: Vec<String> = issue.labels().cloned().map(|t| t.into()).collect();
    let assignees: Vec<String> = issue
        .assignees()
        .map(|a| term::format::did(a).to_string())
        .collect();
    let author = issue.author();
    let did = author.id();
    let author = Author::new(did, profile, verbose);

    let mut attrs = Table::<2, term::Line>::new(TableOptions {
        spacing: 2,
        ..TableOptions::default()
    });

    attrs.push([
        term::format::tertiary("Title".to_owned()).into(),
        term::format::bold(issue.title().to_owned()).into(),
    ]);

    attrs.push([
        term::format::tertiary("Issue".to_owned()).into(),
        term::format::bold(id.to_string()).into(),
    ]);

    attrs.push([
        term::format::tertiary("Author".to_owned()).into(),
        author.line(),
    ]);

    if !labels.is_empty() {
        attrs.push([
            term::format::tertiary("Labels".to_owned()).into(),
            term::format::secondary(labels.join(", ")).into(),
        ]);
    }

    if !assignees.is_empty() {
        attrs.push([
            term::format::tertiary("Assignees".to_owned()).into(),
            term::format::dim(assignees.join(", ")).into(),
        ]);
    }

    attrs.push([
        term::format::tertiary("Status".to_owned()).into(),
        match issue.state() {
            issue::State::Open => term::format::positive("open".to_owned()).into(),
            issue::State::Closed {
                reason: CloseReason::Solved,
            } => term::Line::spaced([
                term::format::negative("closed").into(),
                term::format::negative("(solved)").italic().dim().into(),
            ]),
            issue::State::Closed {
                reason: CloseReason::Other,
            } => term::Line::spaced([term::format::negative("closed").into()]),
        },
    ]);

    let description = issue.description();
    let mut widget = VStack::default()
        .border(Some(term::colors::FAINT))
        .child(attrs)
        .children(if !description.is_empty() {
            vec![
                term::Label::blank().boxed(),
                term::textarea(description.trim()).wrap(60).boxed(),
            ]
        } else {
            vec![]
        });

    if format == Format::Full {
        for (id, comment) in issue.replies() {
            let hstack = term::comment::header(id, comment, profile);

            widget = widget.divider();
            widget.push(hstack);
            widget.push(radicle_term::Label::blank());
            widget.push(term::textarea(comment.body()).wrap(60));
        }
    }
    widget.print();

    Ok(())
}