use std::borrow::Cow;
use crate::style::Style;
use crate::config::Config;
use crate::gitu_diff::Status;
use crate::item_data::{ItemData, Ref, SectionHeader};
use crate::items::{Item, RenderedRow};
use crate::rebase_todo::TodoEntry;
use crate::ui::layout::opts;
use crate::ui::{UiTree, layout_span};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
pub(crate) fn layout_item<'a>(
layout: &mut UiTree<'a>,
item: &'a Item,
config: &Config,
base: Style,
) {
let style = &config.style;
if let Some(rendered) = &item.rendered {
layout_rendered(layout, rendered, base);
return;
}
match &item.data {
ItemData::Raw(content) => {
layout_span(layout, (content.as_str().into(), base));
}
ItemData::AllUnstaged(count) => {
layout_span(
layout,
(
"Unstaged changes".into(),
base.patch(Style::from(&style.section_header)),
),
);
layout_span(layout, (format!(" ({count})").into(), base));
}
ItemData::AllStaged(count) => {
layout_span(
layout,
(
"Staged changes".into(),
base.patch(Style::from(&style.section_header)),
),
);
layout_span(layout, (format!(" ({count})").into(), base));
}
ItemData::AllUntracked(_) => {
layout_span(
layout,
(
"Untracked files".into(),
base.patch(Style::from(&style.section_header)),
),
);
}
ItemData::Reference { kind, prefix } => {
layout_span(layout, ((*prefix).into(), base));
layout_reference(layout, kind, config, base);
}
ItemData::Commit {
short_id,
associated_references,
summary,
author,
age,
..
} => {
layout_span(
layout,
(
short_id.as_str().into(),
base.patch(Style::from(&style.hash)),
),
);
layout.row(opts().fill_x(), |layout| {
for reference in associated_references {
layout_span(layout, (" ".into(), base));
layout_reference(layout, reference, config, base);
}
layout_span(layout, (" ".into(), base));
layout_span(layout, (summary.as_str().into(), base));
});
layout_span(layout, (" ".into(), base));
layout_span(
layout,
(
truncate(author, config.general.log_author_width),
base.patch(Style::from(&style.author)),
),
);
layout_span(layout, (" ".into(), base));
layout_span(
layout,
(age.as_str().into(), base.patch(Style::from(&style.age))),
);
layout_span(layout, (" ".into(), base));
}
ItemData::Untracked(path) => {
layout_span(
layout,
(
path.to_string_lossy(),
base.patch(Style::from(&style.file_header)),
),
);
}
ItemData::Delta { diff, file_i, .. } => {
let file_diff = &diff.file_diffs[*file_i];
let content = format!(
"{:8} {}",
format!("{:?}", file_diff.header.status).to_lowercase(),
match file_diff.header.status {
Status::Renamed | Status::Copied => format!(
"{} -> {}",
file_diff.header.old_file.fmt(&diff.text),
file_diff.header.new_file.fmt(&diff.text)
),
Status::Deleted => file_diff.header.old_file.fmt(&diff.text).to_string(),
Status::Added => file_diff.header.new_file.fmt(&diff.text).to_string(),
Status::Modified => file_diff.header.new_file.fmt(&diff.text).to_string(),
Status::Unmerged => file_diff.header.new_file.fmt(&diff.text).to_string(),
}
);
layout_span(
layout,
(content.into(), base.patch(Style::from(&style.file_header))),
);
}
ItemData::Hunk {
diff,
file_i,
hunk_i,
} => {
let hunk = &diff.file_diffs[*file_i].hunks[*hunk_i];
let content = &diff.text[hunk.header.range.clone()];
layout_span(
layout,
(content.into(), base.patch(Style::from(&style.hunk_header))),
);
}
ItemData::HunkLine { .. } => {}
ItemData::Stash { message, id, .. } => {
layout_span(
layout,
(
format!("stash@{id}").into(),
base.patch(Style::from(&style.hash)),
),
);
layout_span(layout, (format!(" {message}").into(), base));
}
ItemData::RebaseTodo { todo, index } => {
let content = match todo.borrow().entries.get(*index) {
Some(TodoEntry::Commit { action, oid }) => format!("{} {oid}", action.keyword()),
Some(TodoEntry::Other(instruction)) => instruction.clone(),
None => String::new(),
};
layout_span(layout, (content.into(), base));
}
ItemData::Header(header) => {
let content: Cow<str> = match header {
SectionHeader::Remote(remote) => format!("Remote {remote}").into(),
SectionHeader::Tags => "Tags".into(),
SectionHeader::Branches => "Branches".into(),
SectionHeader::NoBranch => "No branch".into(),
SectionHeader::OnBranch(branch) => {
if config.general.verbose_branch_status {
format!("Branch: {branch}").into()
} else {
branch.as_str().into()
}
}
SectionHeader::Rebase(head, onto) => format!("Rebasing {head} onto {onto}").into(),
SectionHeader::Merge(head) => format!("Merging {head}").into(),
SectionHeader::Revert(head) => format!("Reverting {head}").into(),
SectionHeader::CherryPick(head) => format!("Cherry-picking {head}").into(),
SectionHeader::Stashes => "Stashes".into(),
SectionHeader::RecentCommits => "Recent commits".into(),
SectionHeader::StashRef(stash_ref) => stash_ref.as_str().into(),
SectionHeader::StagedChanges(count) => format!("Staged changes ({count})").into(),
SectionHeader::UnstagedChanges(count) => {
format!("Unstaged changes ({count})").into()
}
SectionHeader::UntrackedFiles(count) => format!("Untracked files ({count})").into(),
SectionHeader::Blame(file, commit) => format!("Blame {file} @ {commit}").into(),
};
layout_span(
layout,
(content, base.patch(Style::from(&style.section_header))),
);
}
ItemData::BranchStatus(upstream, ahead, behind) => {
let content = if *ahead == 0 && *behind == 0 {
format!("Your branch is up to date with '{upstream}'.")
} else if *ahead > 0 && *behind == 0 {
format!("Your branch is ahead of '{upstream}' by {ahead} commit(s).")
} else if *ahead == 0 && *behind > 0 {
format!("Your branch is behind '{upstream}' by {behind} commit(s).")
} else {
format!(
"Your branch and '{upstream}' have diverged, and have {ahead} and {behind} different commits each, respectively."
)
};
layout_span(layout, (content.into(), base));
}
ItemData::Error(err) => {
layout_span(layout, (err.as_str().into(), base));
}
ItemData::BlameHeader {
short_hash,
summary,
..
} => {
layout_span(
layout,
(
format!("{short_hash:<8}").into(),
base.patch(Style::from(&style.hash)),
),
);
layout_span(layout, (" ".into(), base));
layout_span(layout, (summary.as_str().into(), base));
}
ItemData::BlameCodeLine {
line_num, content, ..
} => {
let base = base.patch(Style::from(&style.blame.code_line));
layout_span(
layout,
(
format!("{line_num:>4} ").into(),
base.patch(Style::from(&style.blame.line_num)),
),
);
layout_span(layout, (content.replace('\t', " ").into(), base));
}
}
}
pub(crate) fn layout_rendered<'a>(layout: &mut UiTree<'a>, row: &'a RenderedRow, base: Style) {
for (text, span_style) in row.iter() {
layout_span(layout, (text.as_str().into(), base.patch(*span_style)));
}
}
fn truncate(text: &str, width: usize) -> Cow<'_, str> {
if text.width() <= width {
return Cow::Borrowed(text);
}
if width == 0 {
return Cow::Borrowed("");
}
let mut truncated = String::new();
let mut taken = 0;
for grapheme in text.graphemes(true) {
if taken + grapheme.width() > width - 1 {
break;
}
truncated.push_str(grapheme);
taken += grapheme.width();
}
truncated.push('…');
Cow::Owned(truncated)
}
fn layout_reference<'a>(layout: &mut UiTree<'a>, reference: &'a Ref, config: &Config, base: Style) {
let (name, style) = match reference {
Ref::Tag(tag) => (tag, &config.style.tag),
Ref::Head(branch) => (branch, &config.style.branch),
Ref::Remote(remote) => (remote, &config.style.remote),
};
layout_span(
layout,
(name.as_str().into(), base.patch(Style::from(style))),
);
}