Add milestone progress to the TUI (#69)
This commit is contained in:
@@ -103,12 +103,31 @@ pub struct Item {
|
|||||||
pub key: String,
|
pub key: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub graph_lane: Option<usize>,
|
pub graph_lane: Option<usize>,
|
||||||
pub activity_kind: Option<ActivityKind>,
|
pub decoration: ItemDecoration,
|
||||||
pub meta: String,
|
pub meta: String,
|
||||||
pub detail: String,
|
pub detail: String,
|
||||||
pub target: Target,
|
pub target: Target,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum WorkItemState {
|
||||||
|
Open,
|
||||||
|
Closed,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum ItemDecoration {
|
||||||
|
None,
|
||||||
|
Activity(ActivityKind),
|
||||||
|
State(WorkItemState),
|
||||||
|
Milestone {
|
||||||
|
state: WorkItemState,
|
||||||
|
closed: i64,
|
||||||
|
total: i64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum ActivityKind {
|
pub enum ActivityKind {
|
||||||
Repository,
|
Repository,
|
||||||
@@ -1167,7 +1186,7 @@ impl App {
|
|||||||
name.clone()
|
name.clone()
|
||||||
},
|
},
|
||||||
graph_lane: None,
|
graph_lane: None,
|
||||||
activity_kind: None,
|
decoration: ItemDecoration::None,
|
||||||
meta: format!("{} · {}", server.provider, server.url),
|
meta: format!("{} · {}", server.provider, server.url),
|
||||||
detail: format!(
|
detail: format!(
|
||||||
"Profile: {name}\nProvider: {}\nURL: {}\nToken: ••••••••",
|
"Profile: {name}\nProvider: {}\nURL: {}\nToken: ••••••••",
|
||||||
@@ -1245,7 +1264,7 @@ impl App {
|
|||||||
),
|
),
|
||||||
title,
|
title,
|
||||||
graph_lane: None,
|
graph_lane: None,
|
||||||
activity_kind: Some(activity_kind),
|
decoration: ItemDecoration::Activity(activity_kind),
|
||||||
meta: activity.created.unwrap_or_default(),
|
meta: activity.created.unwrap_or_default(),
|
||||||
detail,
|
detail,
|
||||||
target,
|
target,
|
||||||
@@ -1343,7 +1362,7 @@ impl App {
|
|||||||
key: format!("{}/{}", id.owner, id.repository),
|
key: format!("{}/{}", id.owner, id.repository),
|
||||||
title: format!("{} {name}", if favorite { "★" } else { " " }),
|
title: format!("{} {name}", if favorite { "★" } else { " " }),
|
||||||
graph_lane: None,
|
graph_lane: None,
|
||||||
activity_kind: None,
|
decoration: ItemDecoration::None,
|
||||||
meta: format!(
|
meta: format!(
|
||||||
"{} · {} open · {}",
|
"{} · {} open · {}",
|
||||||
repository.language.as_deref().unwrap_or(""),
|
repository.language.as_deref().unwrap_or(""),
|
||||||
@@ -1444,7 +1463,7 @@ impl App {
|
|||||||
.unwrap_or("unknown")
|
.unwrap_or("unknown")
|
||||||
),
|
),
|
||||||
graph_lane: None,
|
graph_lane: None,
|
||||||
activity_kind: None,
|
decoration: ItemDecoration::None,
|
||||||
meta: comment.updated_at.unwrap_or_default(),
|
meta: comment.updated_at.unwrap_or_default(),
|
||||||
detail: body.clone(),
|
detail: body.clone(),
|
||||||
target: id.map_or(Target::None, |id| {
|
target: id.map_or(Target::None, |id| {
|
||||||
@@ -1557,7 +1576,7 @@ impl App {
|
|||||||
key: path.clone(),
|
key: path.clone(),
|
||||||
title: format!("{} {name}", if directory { "▸" } else { " " }),
|
title: format!("{} {name}", if directory { "▸" } else { " " }),
|
||||||
graph_lane: None,
|
graph_lane: None,
|
||||||
activity_kind: None,
|
decoration: ItemDecoration::None,
|
||||||
meta: if directory {
|
meta: if directory {
|
||||||
"directory".into()
|
"directory".into()
|
||||||
} else {
|
} else {
|
||||||
@@ -1662,7 +1681,7 @@ impl App {
|
|||||||
key: path.clone(),
|
key: path.clone(),
|
||||||
title: path.clone(),
|
title: path.clone(),
|
||||||
graph_lane: None,
|
graph_lane: None,
|
||||||
activity_kind: None,
|
decoration: ItemDecoration::None,
|
||||||
meta: file.status.unwrap_or_else(|| "modified".into()),
|
meta: file.status.unwrap_or_else(|| "modified".into()),
|
||||||
detail: format!(
|
detail: format!(
|
||||||
"+{} -{}",
|
"+{} -{}",
|
||||||
@@ -1688,7 +1707,7 @@ impl App {
|
|||||||
.unwrap_or("unknown")
|
.unwrap_or("unknown")
|
||||||
),
|
),
|
||||||
graph_lane: None,
|
graph_lane: None,
|
||||||
activity_kind: None,
|
decoration: ItemDecoration::None,
|
||||||
meta: comment.updated_at.unwrap_or_default(),
|
meta: comment.updated_at.unwrap_or_default(),
|
||||||
detail: comment.body.unwrap_or_default(),
|
detail: comment.body.unwrap_or_default(),
|
||||||
target: Target::None,
|
target: Target::None,
|
||||||
@@ -1699,7 +1718,8 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn load_milestones(&self, repository: RepositoryId) -> Result<Screen, String> {
|
async fn load_milestones(&self, repository: RepositoryId) -> Result<Screen, String> {
|
||||||
let milestones = self.client.milestones(&repository).await.map_err(err)?;
|
let mut milestones = self.client.milestones(&repository).await.map_err(err)?;
|
||||||
|
sort_milestones(&mut milestones);
|
||||||
let mut screen = Screen::new(
|
let mut screen = Screen::new(
|
||||||
ScreenKind::Milestones(repository.clone()),
|
ScreenKind::Milestones(repository.clone()),
|
||||||
format!(
|
format!(
|
||||||
@@ -1827,7 +1847,7 @@ fn issue_item(repository: &RepositoryId, issue: models::Issue) -> Option<Item> {
|
|||||||
key: number.to_string(),
|
key: number.to_string(),
|
||||||
title: format!("#{number} {}", issue.title.as_deref().unwrap_or("Untitled")),
|
title: format!("#{number} {}", issue.title.as_deref().unwrap_or("Untitled")),
|
||||||
graph_lane: None,
|
graph_lane: None,
|
||||||
activity_kind: None,
|
decoration: ItemDecoration::State(work_item_state(&state)),
|
||||||
meta: format!(
|
meta: format!(
|
||||||
"{} · {} · {} comments",
|
"{} · {} · {} comments",
|
||||||
state,
|
state,
|
||||||
@@ -1851,7 +1871,7 @@ fn pull_item(issue: models::Issue) -> Option<Item> {
|
|||||||
issue.title.as_deref().unwrap_or("Untitled")
|
issue.title.as_deref().unwrap_or("Untitled")
|
||||||
),
|
),
|
||||||
graph_lane: None,
|
graph_lane: None,
|
||||||
activity_kind: None,
|
decoration: ItemDecoration::None,
|
||||||
meta: format!(
|
meta: format!(
|
||||||
"{} · {} comments",
|
"{} · {} comments",
|
||||||
issue.state.as_deref().unwrap_or("unknown"),
|
issue.state.as_deref().unwrap_or("unknown"),
|
||||||
@@ -1871,19 +1891,26 @@ fn pull_item(issue: models::Issue) -> Option<Item> {
|
|||||||
fn milestone_item(repository: &RepositoryId, milestone: models::Milestone) -> Option<Item> {
|
fn milestone_item(repository: &RepositoryId, milestone: models::Milestone) -> Option<Item> {
|
||||||
let id = milestone.id?;
|
let id = milestone.id?;
|
||||||
let state = milestone.state.unwrap_or_else(|| "unknown".into());
|
let state = milestone.state.unwrap_or_else(|| "unknown".into());
|
||||||
|
let open = milestone.open_issues.unwrap_or_default();
|
||||||
|
let closed = milestone.closed_issues.unwrap_or_default();
|
||||||
|
let total = open.saturating_add(closed);
|
||||||
|
let due = milestone
|
||||||
|
.due_on
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|date| date.get(..10))
|
||||||
|
.map_or_else(|| "no due date".into(), |date| format!("due {date}"));
|
||||||
Some(Item {
|
Some(Item {
|
||||||
key: id.to_string(),
|
key: id.to_string(),
|
||||||
title: milestone
|
title: milestone
|
||||||
.title
|
.title
|
||||||
.unwrap_or_else(|| "Untitled milestone".into()),
|
.unwrap_or_else(|| "Untitled milestone".into()),
|
||||||
graph_lane: None,
|
graph_lane: None,
|
||||||
activity_kind: None,
|
decoration: ItemDecoration::Milestone {
|
||||||
meta: format!(
|
state: work_item_state(&state),
|
||||||
"{} · {} open · {} closed",
|
closed,
|
||||||
state,
|
total,
|
||||||
milestone.open_issues.unwrap_or_default(),
|
},
|
||||||
milestone.closed_issues.unwrap_or_default()
|
meta: format!("{state} · {closed} of {total} closed · {due}"),
|
||||||
),
|
|
||||||
detail: milestone
|
detail: milestone
|
||||||
.description
|
.description
|
||||||
.unwrap_or_else(|| "No description".into()),
|
.unwrap_or_else(|| "No description".into()),
|
||||||
@@ -1891,6 +1918,19 @@ fn milestone_item(repository: &RepositoryId, milestone: models::Milestone) -> Op
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sort_milestones(milestones: &mut [models::Milestone]) {
|
||||||
|
milestones.sort_by_key(|milestone| {
|
||||||
|
(
|
||||||
|
milestone.state.as_deref() == Some("closed"),
|
||||||
|
milestone
|
||||||
|
.title
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_lowercase(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn commit_item(repository: &RepositoryId, history: HistoryCommit) -> Option<Item> {
|
fn commit_item(repository: &RepositoryId, history: HistoryCommit) -> Option<Item> {
|
||||||
let HistoryCommit {
|
let HistoryCommit {
|
||||||
commit,
|
commit,
|
||||||
@@ -1907,7 +1947,7 @@ fn commit_item(repository: &RepositoryId, history: HistoryCommit) -> Option<Item
|
|||||||
key: sha.clone(),
|
key: sha.clone(),
|
||||||
title: message.lines().next().unwrap_or("Commit").into(),
|
title: message.lines().next().unwrap_or("Commit").into(),
|
||||||
graph_lane: node_lane,
|
graph_lane: node_lane,
|
||||||
activity_kind: None,
|
decoration: ItemDecoration::None,
|
||||||
meta: format!(
|
meta: format!(
|
||||||
"{} · {}{}",
|
"{} · {}{}",
|
||||||
short_sha(&sha),
|
short_sha(&sha),
|
||||||
@@ -1934,7 +1974,7 @@ fn diff_files(diff: &str) -> Vec<Item> {
|
|||||||
key: path.into(),
|
key: path.into(),
|
||||||
title: path.into(),
|
title: path.into(),
|
||||||
graph_lane: None,
|
graph_lane: None,
|
||||||
activity_kind: None,
|
decoration: ItemDecoration::None,
|
||||||
meta: "changed file".into(),
|
meta: "changed file".into(),
|
||||||
detail: diff_for_file(diff, path),
|
detail: diff_for_file(diff, path),
|
||||||
target: Target::Text(path.into(), diff_for_file(diff, path)),
|
target: Target::Text(path.into(), diff_for_file(diff, path)),
|
||||||
@@ -1969,6 +2009,14 @@ fn state_from_detail(detail: &str) -> &str {
|
|||||||
.unwrap_or("open")
|
.unwrap_or("open")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn work_item_state(state: &str) -> WorkItemState {
|
||||||
|
match state {
|
||||||
|
"open" => WorkItemState::Open,
|
||||||
|
"closed" => WorkItemState::Closed,
|
||||||
|
_ => WorkItemState::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn opposite_state(state: &str) -> &'static str {
|
fn opposite_state(state: &str) -> &'static str {
|
||||||
if state == "closed" { "open" } else { "closed" }
|
if state == "closed" { "open" } else { "closed" }
|
||||||
}
|
}
|
||||||
@@ -2262,4 +2310,47 @@ mod tests {
|
|||||||
"Proper release workflow · hugo/Gotcha"
|
"Proper release workflow · hugo/Gotcha"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn milestones_follow_app_order_and_expose_progress() {
|
||||||
|
let milestone = |title: &str, state: &str, open, closed| models::Milestone {
|
||||||
|
id: Some(if state == "open" { 1 } else { 2 }),
|
||||||
|
title: Some(title.into()),
|
||||||
|
state: Some(state.into()),
|
||||||
|
open_issues: Some(open),
|
||||||
|
closed_issues: Some(closed),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut milestones = vec![
|
||||||
|
milestone("Archived", "closed", 0, 5),
|
||||||
|
milestone("Version 2", "open", 3, 2),
|
||||||
|
milestone("Version 1", "open", 1, 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
sort_milestones(&mut milestones);
|
||||||
|
assert_eq!(
|
||||||
|
milestones
|
||||||
|
.iter()
|
||||||
|
.filter_map(|milestone| milestone.title.as_deref())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
["Version 1", "Version 2", "Archived"]
|
||||||
|
);
|
||||||
|
let item = milestone_item(
|
||||||
|
&RepositoryId {
|
||||||
|
owner: "hugo".into(),
|
||||||
|
repository: "Gotcha".into(),
|
||||||
|
},
|
||||||
|
milestones.remove(1),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
item.decoration,
|
||||||
|
ItemDecoration::Milestone {
|
||||||
|
state: WorkItemState::Open,
|
||||||
|
closed: 2,
|
||||||
|
total: 5,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(item.meta, "open · 2 of 5 closed · no due date");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use syntect::{
|
|||||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{ActivityKind, App, ScreenKind, Tab},
|
app::{ActivityKind, App, ItemDecoration, ScreenKind, Tab, WorkItemState},
|
||||||
editor::Editor,
|
editor::Editor,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,13 +124,14 @@ fn draw_list(frame: &mut Frame<'_>, app: &mut App, area: Rect) {
|
|||||||
item_title(
|
item_title(
|
||||||
&item.title,
|
&item.title,
|
||||||
item.graph_lane,
|
item.graph_lane,
|
||||||
item.activity_kind,
|
item.decoration,
|
||||||
|
area.width.saturating_sub(4) as usize,
|
||||||
|
),
|
||||||
|
item_meta(
|
||||||
|
&item.meta,
|
||||||
|
item.decoration,
|
||||||
area.width.saturating_sub(4) as usize,
|
area.width.saturating_sub(4) as usize,
|
||||||
),
|
),
|
||||||
Line::from(Span::styled(
|
|
||||||
item.meta.clone(),
|
|
||||||
Style::default().fg(Color::DarkGray),
|
|
||||||
)),
|
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -155,7 +156,7 @@ fn draw_list(frame: &mut Frame<'_>, app: &mut App, area: Rect) {
|
|||||||
fn item_title(
|
fn item_title(
|
||||||
title: &str,
|
title: &str,
|
||||||
graph_lane: Option<usize>,
|
graph_lane: Option<usize>,
|
||||||
activity_kind: Option<ActivityKind>,
|
decoration: ItemDecoration,
|
||||||
max_width: usize,
|
max_width: usize,
|
||||||
) -> Line<'static> {
|
) -> Line<'static> {
|
||||||
let mut spans = graph_lane.map_or_else(Vec::new, |node_lane| {
|
let mut spans = graph_lane.map_or_else(Vec::new, |node_lane| {
|
||||||
@@ -168,8 +169,14 @@ fn item_title(
|
|||||||
));
|
));
|
||||||
spans
|
spans
|
||||||
});
|
});
|
||||||
if let Some(kind) = activity_kind {
|
let icon = match decoration {
|
||||||
let (icon, color) = activity_icon(kind);
|
ItemDecoration::Activity(kind) => Some(activity_icon(kind)),
|
||||||
|
ItemDecoration::State(state) | ItemDecoration::Milestone { state, .. } => {
|
||||||
|
Some(state_icon(state))
|
||||||
|
}
|
||||||
|
ItemDecoration::None => None,
|
||||||
|
};
|
||||||
|
if let Some((icon, color)) = icon {
|
||||||
spans.push(Span::styled(icon, Style::default().fg(color)));
|
spans.push(Span::styled(icon, Style::default().fg(color)));
|
||||||
}
|
}
|
||||||
let prefix_width = spans.iter().map(Span::width).sum::<usize>();
|
let prefix_width = spans.iter().map(Span::width).sum::<usize>();
|
||||||
@@ -180,6 +187,43 @@ fn item_title(
|
|||||||
Line::from(spans)
|
Line::from(spans)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn item_meta(meta: &str, decoration: ItemDecoration, max_width: usize) -> Line<'static> {
|
||||||
|
let ItemDecoration::Milestone { closed, total, .. } = decoration else {
|
||||||
|
return Line::from(Span::styled(
|
||||||
|
truncate(meta, max_width),
|
||||||
|
Style::default().fg(Color::DarkGray),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let bar_width = 10.min(max_width.saturating_sub(1));
|
||||||
|
let closed = closed.max(0) as usize;
|
||||||
|
let total = total.max(0) as usize;
|
||||||
|
let filled = closed
|
||||||
|
.saturating_mul(bar_width)
|
||||||
|
.saturating_add(total / 2)
|
||||||
|
.checked_div(total)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.min(bar_width);
|
||||||
|
let mut spans = vec![
|
||||||
|
Span::styled("█".repeat(filled), Style::default().fg(Color::LightGreen)),
|
||||||
|
Span::styled(
|
||||||
|
"░".repeat(bar_width - filled),
|
||||||
|
Style::default().fg(if total == 0 {
|
||||||
|
Color::DarkGray
|
||||||
|
} else {
|
||||||
|
Color::LightYellow
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
if bar_width < max_width {
|
||||||
|
spans.push(Span::raw(" "));
|
||||||
|
spans.push(Span::styled(
|
||||||
|
truncate(meta, max_width - bar_width - 1),
|
||||||
|
Style::default().fg(Color::DarkGray),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Line::from(spans)
|
||||||
|
}
|
||||||
|
|
||||||
fn truncate(value: &str, width: usize) -> String {
|
fn truncate(value: &str, width: usize) -> String {
|
||||||
if UnicodeWidthStr::width(value) <= width {
|
if UnicodeWidthStr::width(value) <= width {
|
||||||
return value.to_string();
|
return value.to_string();
|
||||||
@@ -213,6 +257,14 @@ fn activity_icon(kind: ActivityKind) -> (&'static str, Color) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn state_icon(state: WorkItemState) -> (&'static str, Color) {
|
||||||
|
match state {
|
||||||
|
WorkItemState::Open => ("● ", Color::LightGreen),
|
||||||
|
WorkItemState::Closed => ("✓ ", Color::LightMagenta),
|
||||||
|
WorkItemState::Unknown => ("? ", Color::DarkGray),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn lane_color(lane: usize) -> Color {
|
fn lane_color(lane: usize) -> Color {
|
||||||
const COLORS: [Color; 6] = [
|
const COLORS: [Color; 6] = [
|
||||||
Color::LightRed,
|
Color::LightRed,
|
||||||
@@ -493,7 +545,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn commit_graph_lanes_use_stable_distinct_colors() {
|
fn commit_graph_lanes_use_stable_distinct_colors() {
|
||||||
let title = item_title("Merge feature", Some(2), None, 30);
|
let title = item_title("Merge feature", Some(2), ItemDecoration::None, 30);
|
||||||
assert_eq!(title.to_string(), "│ │ ● Merge feature");
|
assert_eq!(title.to_string(), "│ │ ● Merge feature");
|
||||||
assert_eq!(title.spans[0].style.fg, Some(lane_color(0)));
|
assert_eq!(title.spans[0].style.fg, Some(lane_color(0)));
|
||||||
assert_eq!(title.spans[1].style.fg, Some(lane_color(1)));
|
assert_eq!(title.spans[1].style.fg, Some(lane_color(1)));
|
||||||
@@ -524,7 +576,7 @@ mod tests {
|
|||||||
let issue = item_title(
|
let issue = item_title(
|
||||||
"Notification settings opening issue · hugo/Gotcha",
|
"Notification settings opening issue · hugo/Gotcha",
|
||||||
None,
|
None,
|
||||||
Some(ActivityKind::Issue),
|
ItemDecoration::Activity(ActivityKind::Issue),
|
||||||
24,
|
24,
|
||||||
);
|
);
|
||||||
assert!(issue.to_string().ends_with('…'));
|
assert!(issue.to_string().ends_with('…'));
|
||||||
@@ -547,6 +599,41 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn work_item_states_and_milestone_progress_have_native_terminal_treatments() {
|
||||||
|
let open = item_title(
|
||||||
|
"Open issue",
|
||||||
|
None,
|
||||||
|
ItemDecoration::State(WorkItemState::Open),
|
||||||
|
20,
|
||||||
|
);
|
||||||
|
let closed = item_title(
|
||||||
|
"Closed issue",
|
||||||
|
None,
|
||||||
|
ItemDecoration::State(WorkItemState::Closed),
|
||||||
|
20,
|
||||||
|
);
|
||||||
|
assert_eq!(open.spans[0].content, "● ");
|
||||||
|
assert_eq!(open.spans[0].style.fg, Some(Color::LightGreen));
|
||||||
|
assert_eq!(closed.spans[0].content, "✓ ");
|
||||||
|
assert_eq!(closed.spans[0].style.fg, Some(Color::LightMagenta));
|
||||||
|
|
||||||
|
let progress = item_meta(
|
||||||
|
"open · 2 of 5 closed · no due date",
|
||||||
|
ItemDecoration::Milestone {
|
||||||
|
state: WorkItemState::Open,
|
||||||
|
closed: 2,
|
||||||
|
total: 5,
|
||||||
|
},
|
||||||
|
40,
|
||||||
|
);
|
||||||
|
assert_eq!(progress.spans[0].content, "████");
|
||||||
|
assert_eq!(progress.spans[1].content, "░░░░░░");
|
||||||
|
assert_eq!(progress.spans[0].style.fg, Some(Color::LightGreen));
|
||||||
|
assert_eq!(progress.spans[1].style.fg, Some(Color::LightYellow));
|
||||||
|
assert!(progress.width() <= 40);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn previews_render_markdown_source_and_diffs_with_styles() {
|
fn previews_render_markdown_source_and_diffs_with_styles() {
|
||||||
let repository = gotcha_gitea::RepositoryId {
|
let repository = gotcha_gitea::RepositoryId {
|
||||||
|
|||||||
Reference in New Issue
Block a user