src/tui/widgets.rs
Ref: Size: 31.1 KiB
use git2::{Oid, Repository};
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
use crate::event::{Action, ReviewVerdict};
use crate::state::{IssueState, IssueStatus, PatchState, PatchStatus};
use super::state::{App, InputMode, ListMode, Pane, StatusFilter, ViewMode};
pub(crate) fn action_type_label(action: &Action) -> &str {
match action {
Action::IssueOpen { .. } => "Issue Open",
Action::IssueComment { .. } => "Issue Comment",
Action::IssueClose { .. } => "Issue Close",
Action::IssueReopen => "Issue Reopen",
Action::PatchCreate { .. } => "Patch Create",
Action::PatchRevision { .. } => "Patch Revision",
Action::PatchReview { .. } => "Patch Review",
Action::PatchComment { .. } => "Patch Comment",
Action::PatchInlineComment { .. } => "Inline Comment",
Action::PatchClose { .. } => "Patch Close",
Action::PatchMerge => "Patch Merge",
Action::Merge => "Merge",
Action::IssueEdit { .. } => "Issue Edit",
Action::IssueLabel { .. } => "Issue Label",
Action::IssueUnlabel { .. } => "Issue Unlabel",
Action::IssueAssign { .. } => "Issue Assign",
Action::IssueUnassign { .. } => "Issue Unassign",
Action::IssueCommitLink { .. } => "Issue Commit Link",
}
}
pub(crate) fn format_event_detail(oid: &Oid, event: &crate::event::Event) -> String {
let short_oid = &oid.to_string()[..7];
let action_label = action_type_label(&event.action);
let mut detail = format!(
"Commit: {}\nAuthor: {} <{}>\nDate: {}\nType: {}\n",
short_oid, event.author.name, event.author.email, event.timestamp, action_label,
);
// Action-specific payload
match &event.action {
Action::IssueOpen { title, body, .. } => {
detail.push_str(&format!("\nTitle: {}\n", title));
if !body.is_empty() {
detail.push_str(&format!("\n{}\n", body));
}
}
Action::IssueComment { body } | Action::PatchComment { body } => {
detail.push_str(&format!("\n{}\n", body));
}
Action::IssueClose { reason } | Action::PatchClose { reason } => {
if let Some(r) = reason {
detail.push_str(&format!("\nReason: {}\n", r));
}
}
Action::PatchCreate {
title,
body,
base_ref,
branch,
..
} => {
detail.push_str(&format!("\nTitle: {}\n", title));
detail.push_str(&format!("Base: {}\n", base_ref));
detail.push_str(&format!("Branch: {}\n", branch));
if !body.is_empty() {
detail.push_str(&format!("\n{}\n", body));
}
}
Action::PatchRevision { commit, tree, body } => {
detail.push_str(&format!("\nCommit: {}\n", commit));
detail.push_str(&format!("Tree: {}\n", tree));
if let Some(b) = body {
if !b.is_empty() {
detail.push_str(&format!("\n{}\n", b));
}
}
}
Action::PatchReview { verdict, body, .. } => {
detail.push_str(&format!("\nVerdict: {}\n", verdict));
if !body.is_empty() {
detail.push_str(&format!("\n{}\n", body));
}
}
Action::PatchInlineComment { file, line, body, .. } => {
detail.push_str(&format!("\nFile: {}:{}\n", file, line));
if !body.is_empty() {
detail.push_str(&format!("\n{}\n", body));
}
}
Action::IssueEdit { title, body } => {
if let Some(t) = title {
detail.push_str(&format!("\nNew Title: {}\n", t));
}
if let Some(b) = body {
if !b.is_empty() {
detail.push_str(&format!("\nNew Body: {}\n", b));
}
}
}
Action::IssueLabel { label } => {
detail.push_str(&format!("\nLabel: {}\n", label));
}
Action::IssueUnlabel { label } => {
detail.push_str(&format!("\nRemoved Label: {}\n", label));
}
Action::IssueAssign { assignee } => {
detail.push_str(&format!("\nAssignee: {}\n", assignee));
}
Action::IssueUnassign { assignee } => {
detail.push_str(&format!("\nRemoved Assignee: {}\n", assignee));
}
Action::IssueCommitLink { commit } => {
detail.push_str(&format!("\nCommit: {}\n", commit));
}
Action::IssueReopen | Action::PatchMerge | Action::Merge => {}
}
detail
}
pub(crate) fn ui(frame: &mut Frame, app: &mut App, repo: Option<&Repository>) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(1),
Constraint::Length(1),
])
.split(frame.area());
let main_area = chunks[0];
let footer_area = chunks[1];
let panes = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(35), Constraint::Percentage(65)])
.split(main_area);
render_list(frame, app, panes[0]);
render_detail(frame, app, panes[1], repo);
render_footer(frame, app, footer_area);
}
fn render_list(frame: &mut Frame, app: &mut App, area: Rect) {
let border_style = if app.pane == Pane::ItemList {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::DarkGray)
};
match app.list_mode {
ListMode::Issues => {
let visible = app.visible_issues();
let items: Vec<ListItem> = visible
.iter()
.map(|i| {
let status = i.status.as_str();
let style = match i.status {
IssueStatus::Open => Style::default().fg(Color::Green),
IssueStatus::Closed => Style::default().fg(Color::Red),
};
ListItem::new(format!("{:.8} {:6} {}", i.id, status, i.title)).style(style)
})
.collect();
let title = format!("Issues ({})", app.status_filter.label());
let list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.title(title)
.border_style(border_style),
)
.highlight_style(
Style::default()
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("> ");
frame.render_stateful_widget(list, area, &mut app.list_state);
}
ListMode::Patches => {
let visible = app.visible_patches();
let items: Vec<ListItem> = visible
.iter()
.map(|p| {
let status = p.status.as_str();
let style = match p.status {
PatchStatus::Open => Style::default().fg(Color::Green),
PatchStatus::Closed => Style::default().fg(Color::Red),
PatchStatus::Merged => Style::default().fg(Color::Cyan),
};
ListItem::new(format!("{:.8} {:6} {}", p.id, status, p.title)).style(style)
})
.collect();
let title = format!("Patches ({})", app.status_filter.label());
let list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.title(title)
.border_style(border_style),
)
.highlight_style(
Style::default()
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("> ");
frame.render_stateful_widget(list, area, &mut app.patch_list_state);
}
}
}
fn render_detail(frame: &mut Frame, app: &mut App, area: Rect, repo: Option<&Repository>) {
let border_style = if app.pane == Pane::Detail {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::DarkGray)
};
// Handle patch detail mode
if app.mode == ViewMode::PatchDetail {
let content = build_patch_detail_text(app);
let block = Block::default()
.borders(Borders::ALL)
.title("Patch Detail")
.border_style(border_style);
let para = Paragraph::new(content)
.block(block)
.wrap(Wrap { trim: false })
.scroll((app.patch_scroll, 0));
frame.render_widget(para, area);
return;
}
// Handle commit browser modes
if app.mode == ViewMode::CommitList {
let items: Vec<ListItem> = app
.event_history
.iter()
.map(|(_oid, evt)| {
let label = action_type_label(&evt.action);
ListItem::new(format!(
"{} | {} | {}",
label, evt.author.name, evt.timestamp
))
})
.collect();
let list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.title("Event History")
.border_style(border_style),
)
.highlight_style(
Style::default()
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("> ");
frame.render_stateful_widget(list, area, &mut app.event_list_state);
return;
}
if app.mode == ViewMode::CommitDetail {
let content = if let Some(idx) = app.event_list_state.selected() {
if let Some((oid, evt)) = app.event_history.get(idx) {
format_event_detail(oid, evt)
} else {
"No event selected.".to_string()
}
} else {
"No event selected.".to_string()
};
let block = Block::default()
.borders(Borders::ALL)
.title("Event Detail")
.border_style(border_style);
let para = Paragraph::new(content)
.block(block)
.wrap(Wrap { trim: false })
.scroll((app.scroll, 0));
frame.render_widget(para, area);
return;
}
match app.list_mode {
ListMode::Issues => {
let visible = app.visible_issues();
let selected_idx = app.list_state.selected().unwrap_or(0);
let content: Text = match visible.get(selected_idx) {
Some(issue) => build_issue_detail(issue, &app.patches, repo),
None => Text::raw("No matches for current filter."),
};
let block = Block::default()
.borders(Borders::ALL)
.title("Issue Details")
.border_style(border_style);
let para = Paragraph::new(content)
.block(block)
.wrap(Wrap { trim: false })
.scroll((app.scroll, 0));
frame.render_widget(para, area);
}
ListMode::Patches => {
let visible = app.visible_patches();
let selected_idx = app.patch_list_state.selected().unwrap_or(0);
let content: Text = match visible.get(selected_idx) {
Some(patch) => build_patch_summary(patch),
None => Text::raw("No patches for current filter."),
};
let block = Block::default()
.borders(Borders::ALL)
.title("Patch Details")
.border_style(border_style);
let para = Paragraph::new(content)
.block(block)
.wrap(Wrap { trim: false })
.scroll((app.scroll, 0));
frame.render_widget(para, area);
}
}
}
fn build_issue_detail(
issue: &IssueState,
patches: &[PatchState],
repo: Option<&Repository>,
) -> Text<'static> {
let status = issue.status.as_str();
let mut lines: Vec<Line> = vec![
Line::from(vec![
Span::styled("Issue ", Style::default().add_modifier(Modifier::BOLD)),
Span::styled(
format!("{:.8}", issue.id),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw(format!(" [{}]", status)),
]),
Line::from(vec![
Span::styled("Title: ", Style::default().fg(Color::DarkGray)),
Span::raw(issue.title.clone()),
]),
Line::from(vec![
Span::styled("Author: ", Style::default().fg(Color::DarkGray)),
Span::raw(format!("{} <{}>", issue.author.name, issue.author.email)),
]),
Line::from(vec![
Span::styled("Created: ", Style::default().fg(Color::DarkGray)),
Span::raw(issue.created_at.clone()),
]),
];
if !issue.labels.is_empty() {
lines.push(Line::from(vec![
Span::styled("Labels: ", Style::default().fg(Color::DarkGray)),
Span::raw(issue.labels.join(", ")),
]));
}
if !issue.assignees.is_empty() {
lines.push(Line::from(vec![
Span::styled("Assign: ", Style::default().fg(Color::DarkGray)),
Span::raw(issue.assignees.join(", ")),
]));
}
if let Some(ref reason) = issue.close_reason {
lines.push(Line::from(vec![
Span::styled("Closed: ", Style::default().fg(Color::Red)),
Span::raw(reason.clone()),
]));
}
if let Some(ref oid) = issue.closed_by {
lines.push(Line::from(vec![
Span::styled("Commit: ", Style::default().fg(Color::DarkGray)),
Span::styled(
format!("{:.8}", oid),
Style::default().fg(Color::Cyan),
),
]));
}
// Show patches that reference this issue via --fixes
let fixing_patches: Vec<&PatchState> = patches
.iter()
.filter(|p| p.fixes.as_deref() == Some(&issue.id))
.collect();
if !fixing_patches.is_empty() {
lines.push(Line::raw(""));
lines.push(Line::styled(
"--- Linked Patches ---",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
for p in &fixing_patches {
let status = match p.status {
PatchStatus::Open => ("open", Color::Green),
PatchStatus::Closed => ("closed", Color::Red),
PatchStatus::Merged => ("merged", Color::Cyan),
};
lines.push(Line::from(vec![
Span::styled(
format!("{:.8}", p.id),
Style::default().fg(Color::Yellow),
),
Span::raw(" "),
Span::styled(status.0, Style::default().fg(status.1)),
Span::raw(format!(" {}", p.title)),
]));
}
}
if !issue.body.is_empty() {
lines.push(Line::raw(""));
for l in issue.body.lines() {
lines.push(Line::raw(l.to_string()));
}
}
if !issue.comments.is_empty() {
lines.push(Line::raw(""));
lines.push(Line::styled(
"--- Comments ---",
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::BOLD),
));
for c in &issue.comments {
lines.push(Line::raw(""));
lines.push(Line::from(vec![
Span::styled(
c.author.name.clone(),
Style::default().add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" ({})", c.timestamp),
Style::default().fg(Color::DarkGray),
),
]));
for l in c.body.lines() {
lines.push(Line::raw(format!(" {}", l)));
}
}
}
if !issue.linked_commits.is_empty() {
lines.push(Line::raw(""));
lines.push(Line::styled(
"--- Linked Commits ---",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
for lc in &issue.linked_commits {
let short_sha: String = lc.commit.chars().take(7).collect();
let (subject, commit_author) = repo
.and_then(|r| {
Oid::from_str(&lc.commit)
.ok()
.and_then(|oid| r.find_commit(oid).ok())
.map(|commit| {
let subject = commit
.summary()
.map(|s| crate::truncate_summary(s, 60))
.unwrap_or_default();
let author = commit.author().name().unwrap_or("unknown").to_string();
(subject, author)
})
})
.unwrap_or_else(|| (String::new(), String::new()));
let line_text = if commit_author.is_empty() {
format!(
"· linked {} (commit {} not in local repo) (linked by {}, {})",
short_sha, short_sha, lc.event_author.name, lc.event_timestamp
)
} else {
format!(
"· linked {} \"{}\" by {} (linked by {}, {})",
short_sha, subject, commit_author, lc.event_author.name, lc.event_timestamp
)
};
lines.push(Line::raw(line_text));
}
}
Text::from(lines)
}
fn build_patch_summary(patch: &PatchState) -> Text<'static> {
let status_str = patch.status.as_str();
let status_color = match patch.status {
PatchStatus::Open => Color::Green,
PatchStatus::Closed => Color::Red,
PatchStatus::Merged => Color::Cyan,
};
let mut lines: Vec<Line> = vec![
Line::from(vec![
Span::styled("Patch ", Style::default().add_modifier(Modifier::BOLD)),
Span::styled(
format!("{:.8}", patch.id),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(status_str, Style::default().fg(status_color)),
]),
Line::from(vec![
Span::styled("Title: ", Style::default().fg(Color::DarkGray)),
Span::raw(patch.title.clone()),
]),
Line::from(vec![
Span::styled("Author: ", Style::default().fg(Color::DarkGray)),
Span::raw(format!("{} <{}>", patch.author.name, patch.author.email)),
]),
Line::from(vec![
Span::styled("Branch: ", Style::default().fg(Color::DarkGray)),
Span::raw(patch.branch.clone()),
]),
Line::from(vec![
Span::styled("Base: ", Style::default().fg(Color::DarkGray)),
Span::raw(patch.base_ref.clone()),
]),
Line::from(vec![
Span::styled("Revisions:", Style::default().fg(Color::DarkGray)),
Span::raw(format!(" {}", patch.revisions.len())),
]),
];
if let Some(ref fixes) = patch.fixes {
lines.push(Line::from(vec![
Span::styled("Fixes: ", Style::default().fg(Color::DarkGray)),
Span::raw(format!("{:.8}", fixes)),
]));
}
if !patch.body.is_empty() {
lines.push(Line::raw(""));
for l in patch.body.lines() {
lines.push(Line::raw(l.to_string()));
}
}
lines.push(Line::raw(""));
lines.push(Line::styled(
"Press Enter to view full detail with diff",
Style::default().fg(Color::DarkGray),
));
Text::from(lines)
}
fn build_patch_detail_text(app: &App) -> Text<'static> {
let patch = match &app.current_patch {
Some(p) => p,
None => return Text::raw("No patch loaded."),
};
let status_str = patch.status.as_str();
let status_color = match patch.status {
PatchStatus::Open => Color::Green,
PatchStatus::Closed => Color::Red,
PatchStatus::Merged => Color::Cyan,
};
let mut lines: Vec<Line> = vec![
Line::from(vec![
Span::styled("Patch ", Style::default().add_modifier(Modifier::BOLD)),
Span::styled(
format!("{:.8}", patch.id),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(status_str, Style::default().fg(status_color)),
]),
Line::from(vec![
Span::styled("Title: ", Style::default().fg(Color::DarkGray)),
Span::raw(patch.title.clone()),
]),
Line::from(vec![
Span::styled("Author: ", Style::default().fg(Color::DarkGray)),
Span::raw(format!("{} <{}>", patch.author.name, patch.author.email)),
]),
Line::from(vec![
Span::styled("Branch: ", Style::default().fg(Color::DarkGray)),
Span::raw(patch.branch.clone()),
]),
Line::from(vec![
Span::styled("Base: ", Style::default().fg(Color::DarkGray)),
Span::raw(patch.base_ref.clone()),
]),
Line::from(vec![
Span::styled("Revisions:", Style::default().fg(Color::DarkGray)),
Span::raw(format!(" {}", patch.revisions.len())),
]),
];
if let Some(ref fixes) = patch.fixes {
lines.push(Line::from(vec![
Span::styled("Fixes: ", Style::default().fg(Color::DarkGray)),
Span::raw(format!("{:.8}", fixes)),
]));
}
// Staleness warning (stored in status_msg or computed text)
if let Some(ref warning) = app.status_msg {
if warning.contains("behind") {
lines.push(Line::raw(""));
lines.push(Line::styled(
warning.clone(),
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
));
}
}
// Revision list
if !patch.revisions.is_empty() {
lines.push(Line::raw(""));
lines.push(Line::styled(
"--- Revisions ---",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
for (i, rev) in patch.revisions.iter().enumerate() {
let short = if rev.commit.len() >= 8 {
&rev.commit[..8]
} else {
&rev.commit
};
let marker = if i == app.patch_revision_idx {
"> "
} else {
" "
};
let label = if i == 0 { " (initial)" } else { "" };
lines.push(Line::from(vec![
Span::raw(format!("{}r{} {} {}{}", marker, rev.number, short, rev.timestamp, label)),
]));
}
}
// Reviews
if !patch.reviews.is_empty() {
lines.push(Line::raw(""));
lines.push(Line::styled(
"--- Reviews ---",
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::BOLD),
));
for review in &patch.reviews {
let verdict_str = review.verdict.as_str();
let verdict_color = match review.verdict {
ReviewVerdict::Approve => Color::Green,
ReviewVerdict::RequestChanges => Color::Yellow,
ReviewVerdict::Comment => Color::White,
ReviewVerdict::Reject => Color::Red,
};
let rev_label = review
.revision
.map(|r| format!(" (r{})", r))
.unwrap_or_default();
lines.push(Line::from(vec![
Span::styled(
review.author.name.clone(),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!(" {} ", review.timestamp)),
Span::styled(verdict_str, Style::default().fg(verdict_color)),
Span::raw(rev_label),
]));
if !review.body.is_empty() {
for l in review.body.lines() {
lines.push(Line::raw(format!(" {}", l)));
}
}
}
}
// Inline comments
if !patch.inline_comments.is_empty() {
lines.push(Line::raw(""));
lines.push(Line::styled(
"--- Inline Comments ---",
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::BOLD),
));
for ic in &patch.inline_comments {
let rev_label = ic
.revision
.map(|r| format!(" (r{})", r))
.unwrap_or_default();
lines.push(Line::from(vec![
Span::styled(
ic.author.name.clone(),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!(" {}:{}", ic.file, ic.line)),
Span::raw(rev_label),
]));
for l in ic.body.lines() {
lines.push(Line::raw(format!(" {}", l)));
}
}
}
// Thread comments
if !patch.comments.is_empty() {
lines.push(Line::raw(""));
lines.push(Line::styled(
"--- Comments ---",
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::BOLD),
));
for c in &patch.comments {
lines.push(Line::raw(""));
lines.push(Line::from(vec![
Span::styled(
c.author.name.clone(),
Style::default().add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" ({})", c.timestamp),
Style::default().fg(Color::DarkGray),
),
]));
for l in c.body.lines() {
lines.push(Line::raw(format!(" {}", l)));
}
}
}
// Diff header
lines.push(Line::raw(""));
let diff_mode = if app.patch_interdiff_mode {
let rev_idx = app.patch_revision_idx;
if rev_idx > 0 {
let from_rev = patch.revisions.get(rev_idx - 1).map(|r| r.number).unwrap_or(0);
let to_rev = patch.revisions.get(rev_idx).map(|r| r.number).unwrap_or(0);
format!("--- Interdiff r{} -> r{} ---", from_rev, to_rev)
} else {
"--- Diff vs base (no previous revision) ---".to_string()
}
} else {
let rev_num = patch.revisions.get(app.patch_revision_idx).map(|r| r.number).unwrap_or(1);
format!("--- Diff r{} vs base ---", rev_num)
};
lines.push(Line::styled(
diff_mode,
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
));
// Diff content
if app.patch_diff.is_empty() {
lines.push(Line::raw("(no diff available)"));
} else {
for l in app.patch_diff.lines() {
let style = if l.starts_with('+') {
Style::default().fg(Color::Green)
} else if l.starts_with('-') {
Style::default().fg(Color::Red)
} else if l.starts_with("@@") {
Style::default().fg(Color::Cyan)
} else if l.starts_with("diff ") {
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
} else {
Style::default()
};
lines.push(Line::styled(l.to_string(), style));
}
}
Text::from(lines)
}
fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
match app.input_mode {
InputMode::Search => {
let max_query_len = (area.width as usize).saturating_sub(12);
let display_query = if app.search_query.len() > max_query_len {
&app.search_query[app.search_query.len() - max_query_len..]
} else {
&app.search_query
};
let text = format!(" Search: {}_", display_query);
let style = Style::default().bg(Color::Blue).fg(Color::White);
let para = Paragraph::new(text).style(style);
frame.render_widget(para, area);
return;
}
InputMode::CreateTitle => {
let max_len = (area.width as usize).saturating_sub(22);
let display = if app.input_buf.len() > max_len {
&app.input_buf[app.input_buf.len() - max_len..]
} else {
&app.input_buf
};
let text = format!(" New issue - Title: {}_", display);
let style = Style::default().bg(Color::Green).fg(Color::Black);
let para = Paragraph::new(text).style(style);
frame.render_widget(para, area);
return;
}
InputMode::CreateBody => {
let max_len = (area.width as usize).saturating_sub(21);
let display = if app.input_buf.len() > max_len {
&app.input_buf[app.input_buf.len() - max_len..]
} else {
&app.input_buf
};
let text = format!(" New issue - Body: {}_ (Esc: skip)", display);
let style = Style::default().bg(Color::Green).fg(Color::Black);
let para = Paragraph::new(text).style(style);
frame.render_widget(para, area);
return;
}
InputMode::Normal => {}
}
// Show status message if present
if let Some(ref msg) = app.status_msg {
let para =
Paragraph::new(format!(" {}", msg)).style(Style::default().bg(Color::Yellow).fg(Color::Black));
frame.render_widget(para, area);
return;
}
let text = match app.mode {
ViewMode::CommitList => " j/k:navigate Enter:detail Esc:back q:quit".to_string(),
ViewMode::CommitDetail => " j/k:scroll Esc:back q:quit".to_string(),
ViewMode::PatchDetail => " j/k:scroll Esc:back [/]:revision d:interdiff q:quit".to_string(),
ViewMode::Details => {
let list_hint = match app.list_mode {
ListMode::Issues => "P:patches",
ListMode::Patches => "P:issues",
};
let filter_hint = match app.status_filter {
StatusFilter::Open => "a:show all",
StatusFilter::All => "a:closed",
StatusFilter::Closed => "a:open only",
};
let mode_hint = match app.list_mode {
ListMode::Issues => " c:events p:patch",
ListMode::Patches => " Enter:view patch",
};
format!(
" j/k:navigate Tab:pane {} {}{} /:search r:refresh q:quit",
list_hint, filter_hint, mode_hint
)
}
};
let para = Paragraph::new(text).style(Style::default().bg(Color::DarkGray).fg(Color::White));
frame.render_widget(para, area);
}