use std::cmp::max;
use std::collections::hash_map::Entry::{Occupied, Vacant};
use std::collections::HashMap;
use std::fmt::Write;
use git2::Commit;
use git2::Repository;
use itertools::Itertools;
use textwrap::Options;
use yansi::Paint;
use crate::graph::{BranchInfo, CommitInfo, GitGraph, HeadInfo};
use crate::layout::BranchVis;
use crate::layout::TrackLayout;
use crate::print::format::CommitFormat;
use crate::print::label::list_labels;
use crate::print::label::Label;
use crate::print::label::LabelMap;
use crate::print::label::LabelType;
use crate::settings::{Characters, Settings};
use crate::track::TrackMap;
const SPACE: u8 = 0;
const DOT: u8 = 1;
const CIRCLE: u8 = 2;
const VER: u8 = 3;
const HOR: u8 = 4;
const CROSS: u8 = 5;
const R_U: u8 = 6;
const R_D: u8 = 7;
const L_D: u8 = 8;
const L_U: u8 = 9;
const VER_L: u8 = 10;
const VER_R: u8 = 11;
const HOR_U: u8 = 12;
const HOR_D: u8 = 13;
const ARR_L: u8 = 14;
const ARR_R: u8 = 15;
const WHITE: u8 = 7; const HEAD_COLOR: u8 = 14; const HASH_COLOR: u8 = 11;
pub type UnicodeGraphInfo = (Vec<String>, Vec<String>, Vec<usize>);
pub fn print_unicode(graph: &GitGraph, settings: &Settings) -> Result<UnicodeGraphInfo, String> {
let repo = &graph.repository;
let tracks = graph.tracks.lock().unwrap();
let layout = &graph.layout;
if tracks.all_branches.is_empty() {
return Ok((vec![], vec![], vec![]));
}
let num_cols = calculate_graph_dimensions(&graph.layout);
let inserts = get_inserts(&tracks, &layout, settings.compact);
let (indent1, indent2) = if let Some((_, ind1, ind2)) = settings.wrapping {
(" ".repeat(ind1.unwrap_or(0)), " ".repeat(ind2.unwrap_or(0)))
} else {
("".to_string(), "".to_string())
};
let wrap_options = get_wrapping_options(settings, num_cols, &indent1, &indent2)?;
let (mut text_lines, index_map) = build_commit_lines_and_map(
settings,
repo,
&tracks,
&layout,
&graph.head,
&inserts,
&wrap_options,
)?;
let total_rows = text_lines.len();
let mut grid = draw_graph_lines(
settings, &tracks, &layout, num_cols, &inserts, &index_map, total_rows,
);
if settings.reverse_commit_order {
text_lines.reverse();
grid.reverse();
}
let lines = print_graph(&settings.characters, &grid, text_lines, settings.colored);
Ok((lines.0, lines.1, index_map))
}
fn calculate_graph_dimensions(layout: &TrackLayout) -> usize {
let max_column = layout
.track_visual_vec()
.iter()
.map(|b_visual| b_visual.column.unwrap_or(0))
.max()
.unwrap_or(0);
2 * max_column + 1
}
fn get_wrapping_options<'a>(
settings: &Settings,
num_cols: usize,
indent1: &'a str, indent2: &'a str, ) -> Result<Option<Options<'a>>, String> {
if let Some((width, _, _)) = settings.wrapping {
create_wrapping_options(width, indent1, indent2, num_cols + 4)
} else {
Ok(None)
}
}
fn build_commit_lines_and_map<'a>(
settings: &Settings,
repository: &Repository,
tracks: &TrackMap,
layout: &TrackLayout,
the_head: &HeadInfo,
inserts: &HashMap<usize, Vec<Vec<Occ>>>,
wrap_options: &Option<Options<'a>>,
) -> Result<(Vec<Option<String>>, Vec<usize>), String> {
let labels = list_labels(settings, repository)?;
let head_idx = tracks.indices.get(&the_head.oid);
let mut index_map = vec![];
let mut text_lines = vec![];
let mut offset = 0;
for (idx, info) in tracks.commits.iter().enumerate() {
index_map.push(idx + offset);
let cnt_inserts = if let Some(inserts) = inserts.get(&idx) {
inserts
.iter()
.filter(|vec| {
vec.iter().all(|occ| match occ {
Occ::Commit(_, _) => false,
Occ::Range(_, _, _, _) => true,
})
})
.count()
} else {
0
};
let head = if head_idx == Some(&idx) {
Some(the_head)
} else {
None
};
let commit = &repository
.find_commit(info.oid)
.map_err(|err| err.message().to_string())?;
let lines = format(
&settings.format,
layout,
&labels,
commit,
info,
head,
settings.colored,
wrap_options,
)?;
let num_lines = if lines.is_empty() { 0 } else { lines.len() - 1 };
let max_inserts = max(cnt_inserts, num_lines);
let add_lines = max_inserts - num_lines;
text_lines.extend(lines.into_iter().map(Some));
text_lines.extend((0..add_lines).map(|_| None));
offset += max_inserts;
}
Ok((text_lines, index_map))
}
fn draw_graph_lines(
settings: &Settings,
tracks: &TrackMap,
layout: &TrackLayout,
num_cols: usize,
inserts: &HashMap<usize, Vec<Vec<Occ>>>,
index_map: &[usize],
total_rows: usize,
) -> Grid {
let mut grid = Grid::new(
num_cols,
total_rows,
GridCell {
character: SPACE,
color: WHITE,
pers: settings.branches.persistence.len() as u8 + 2,
},
);
for (idx, info) in tracks.commits.iter().enumerate() {
let Some(trace) = info.branch_trace else {
continue;
};
let branch = &tracks.all_branches[trace];
let branch_visual = layout
.track_visual(trace)
.expect("All commits in range has precomputed visuals");
let column = branch_visual.column.unwrap();
let idx_map = index_map[idx];
grid.set(
column * 2,
idx_map,
if info.is_merge { CIRCLE } else { DOT },
branch_visual.term_color,
branch.persistence,
);
draw_parent_lines(
tracks,
layout,
branch,
branch_visual,
&mut grid,
info,
inserts,
index_map,
idx,
);
}
grid
}
fn draw_parent_lines(
tracks: &TrackMap,
layout: &TrackLayout,
branch: &BranchInfo,
branch_visual: &BranchVis,
grid: &mut Grid,
info: &CommitInfo,
inserts: &HashMap<usize, Vec<Vec<Occ>>>,
index_map: &[usize],
idx: usize,
) {
let column = branch_visual.column.unwrap();
let idx_map = index_map[idx];
let branch_color = branch_visual.term_color;
for p in 0..2 {
let parent = info.parents[p];
let Some(par_oid) = parent else {
continue;
};
let Some(par_idx) = tracks.indices.get(&par_oid) else {
let idx_bottom = grid.height;
vline(
grid,
(idx_map, idx_bottom),
column,
branch_color,
branch.persistence,
);
continue;
};
let par_idx_map = index_map[*par_idx];
let par_info = &tracks.commits[*par_idx];
let par_track_idx = par_info.branch_trace.unwrap();
let par_branch = &tracks.all_branches[par_track_idx];
let par_branch_visual = layout
.track_visual(par_track_idx)
.expect("Parent must have visuals");
let par_column = par_branch_visual.column.unwrap();
let (color, pers) = if info.is_merge {
(par_branch_visual.term_color, par_branch.persistence)
} else {
(branch_color, branch.persistence)
};
if branch_visual.column == par_branch_visual.column {
if par_idx_map > idx_map + 1 {
vline(grid, (idx_map, par_idx_map), column, color, pers);
}
} else {
let split_index = get_deviate_index(tracks, layout, idx, *par_idx);
let split_idx_map = index_map[split_index];
let insert_idx = find_insert_idx(&inserts[&split_index], idx, *par_idx).unwrap();
let idx_split = split_idx_map + insert_idx;
let is_secondary_merge = info.is_merge && p > 0;
let row123 = (idx_map, idx_split, par_idx_map);
let col12 = (column, par_column);
zig_zag_line(grid, row123, col12, is_secondary_merge, color, pers);
}
}
}
fn create_wrapping_options<'a>(
width: Option<usize>,
indent1: &'a str,
indent2: &'a str,
graph_width: usize,
) -> Result<Option<Options<'a>>, String> {
let wrapping = if let Some(width) = width {
Some(
textwrap::Options::new(width)
.initial_indent(indent1)
.subsequent_indent(indent2),
)
} else if atty::is(atty::Stream::Stdout) {
let width = crossterm::terminal::size()
.map_err(|err| err.to_string())?
.0 as usize;
let text_width = width.saturating_sub(graph_width);
if text_width < 40 {
None
} else {
Some(
textwrap::Options::new(text_width)
.initial_indent(indent1)
.subsequent_indent(indent2),
)
}
} else {
None
};
Ok(wrapping)
}
fn find_insert_idx(inserts: &[Vec<Occ>], child_idx: usize, parent_idx: usize) -> Option<usize> {
for (insert_idx, sub_entry) in inserts.iter().enumerate() {
for occ in sub_entry {
if let Occ::Range(i1, i2, _, _) = occ {
if *i1 == child_idx && *i2 == parent_idx {
return Some(insert_idx);
}
}
}
}
None
}
fn zig_zag_line(
grid: &mut Grid,
row123: (usize, usize, usize),
col12: (usize, usize),
is_merge: bool,
color: u8,
pers: u8,
) {
let (row1, row2, row3) = row123;
let (col1, col2) = col12;
vline(grid, (row1, row2), col1, color, pers);
hline(grid, row2, (col2, col1), is_merge, color, pers);
vline(grid, (row2, row3), col2, color, pers);
}
fn vline(grid: &mut Grid, (from, to): (usize, usize), column: usize, color: u8, pers: u8) {
for i in (from + 1)..to {
let (curr, _, old_pers) = grid.get_tuple(column * 2, i);
let (new_col, new_pers) = if pers < old_pers {
(Some(color), Some(pers))
} else {
(None, None)
};
match curr {
DOT | CIRCLE => {}
HOR => {
grid.set_opt(column * 2, i, Some(CROSS), Some(color), Some(pers));
}
HOR_U | HOR_D => {
grid.set_opt(column * 2, i, Some(CROSS), Some(color), Some(pers));
}
CROSS | VER | VER_L | VER_R => grid.set_opt(column * 2, i, None, new_col, new_pers),
L_D | L_U => {
grid.set_opt(column * 2, i, Some(VER_L), new_col, new_pers);
}
R_D | R_U => {
grid.set_opt(column * 2, i, Some(VER_R), new_col, new_pers);
}
_ => {
grid.set_opt(column * 2, i, Some(VER), new_col, new_pers);
}
}
}
}
fn hline(
grid: &mut Grid,
index: usize,
(from, to): (usize, usize),
merge: bool,
color: u8,
pers: u8,
) {
if from == to {
return;
}
let from_2 = from * 2;
let to_2 = to * 2;
if from < to {
update_range_forward(grid, index, from_2, to_2, merge, color, pers);
update_left_cell_forward(grid, index, from_2, color, pers);
update_right_cell_forward(grid, index, to_2, color, pers);
} else {
update_range_backward(grid, index, from_2, to_2, merge, color, pers);
update_left_cell_backward(grid, index, to_2, color, pers);
update_right_cell_backward(grid, index, from_2, color, pers);
}
}
fn update_range_forward(
grid: &mut Grid,
index: usize,
from_2: usize,
to_2: usize,
merge: bool,
color: u8,
pers: u8,
) {
for column in (from_2 + 1)..to_2 {
if merge && column == to_2 - 1 {
grid.set(column, index, ARR_R, color, pers);
} else {
let (curr, _, old_pers) = grid.get_tuple(column, index);
let (new_col, new_pers) = if pers < old_pers {
(Some(color), Some(pers))
} else {
(None, None)
};
match curr {
DOT | CIRCLE => {}
VER => grid.set_opt(column, index, Some(CROSS), None, None),
HOR | CROSS | HOR_U | HOR_D => grid.set_opt(column, index, None, new_col, new_pers),
L_U | R_U => grid.set_opt(column, index, Some(HOR_U), new_col, new_pers),
L_D | R_D => grid.set_opt(column, index, Some(HOR_D), new_col, new_pers),
_ => {
grid.set_opt(column, index, Some(HOR), new_col, new_pers);
}
}
}
}
}
fn update_left_cell_forward(grid: &mut Grid, index: usize, from_2: usize, color: u8, pers: u8) {
let (left, _, old_pers) = grid.get_tuple(from_2, index);
let (new_col, new_pers) = if pers < old_pers {
(Some(color), Some(pers))
} else {
(None, None)
};
match left {
DOT | CIRCLE => {}
VER => grid.set_opt(from_2, index, Some(VER_R), new_col, new_pers),
VER_L => grid.set_opt(from_2, index, Some(CROSS), None, None),
VER_R => {}
HOR | L_U => grid.set_opt(from_2, index, Some(HOR_U), new_col, new_pers),
_ => {
grid.set_opt(from_2, index, Some(R_D), new_col, new_pers);
}
}
}
fn update_right_cell_forward(grid: &mut Grid, index: usize, to_2: usize, color: u8, pers: u8) {
let (right, _, old_pers) = grid.get_tuple(to_2, index);
let (new_col, new_pers) = if pers < old_pers {
(Some(color), Some(pers))
} else {
(None, None)
};
match right {
DOT | CIRCLE => {}
VER => grid.set_opt(to_2, index, Some(VER_L), None, None),
VER_L | HOR_U => grid.set_opt(to_2, index, None, new_col, new_pers),
HOR | R_U => grid.set_opt(to_2, index, Some(HOR_U), new_col, new_pers),
_ => {
grid.set_opt(to_2, index, Some(L_U), new_col, new_pers);
}
}
}
fn update_range_backward(
grid: &mut Grid,
index: usize,
from_2: usize,
to_2: usize,
merge: bool,
color: u8,
pers: u8,
) {
for column in (to_2 + 1)..from_2 {
if merge && column == to_2 + 1 {
grid.set(column, index, ARR_L, color, pers);
} else {
let (curr, _, old_pers) = grid.get_tuple(column, index);
let (new_col, new_pers) = if pers < old_pers {
(Some(color), Some(pers))
} else {
(None, None)
};
match curr {
DOT | CIRCLE => {}
VER => grid.set_opt(column, index, Some(CROSS), None, None),
HOR | CROSS | HOR_U | HOR_D => grid.set_opt(column, index, None, new_col, new_pers),
L_U | R_U => grid.set_opt(column, index, Some(HOR_U), new_col, new_pers),
L_D | R_D => grid.set_opt(column, index, Some(HOR_D), new_col, new_pers),
_ => {
grid.set_opt(column, index, Some(HOR), new_col, new_pers);
}
}
}
}
}
fn update_left_cell_backward(grid: &mut Grid, index: usize, to_2: usize, color: u8, pers: u8) {
let (left, _, old_pers) = grid.get_tuple(to_2, index);
let (new_col, new_pers) = if pers < old_pers {
(Some(color), Some(pers))
} else {
(None, None)
};
match left {
DOT | CIRCLE => {}
VER => grid.set_opt(to_2, index, Some(VER_R), None, None),
VER_R => grid.set_opt(to_2, index, None, new_col, new_pers),
HOR | L_U => grid.set_opt(to_2, index, Some(HOR_U), new_col, new_pers),
_ => {
grid.set_opt(to_2, index, Some(R_U), new_col, new_pers);
}
}
}
fn update_right_cell_backward(grid: &mut Grid, index: usize, from_2: usize, color: u8, pers: u8) {
let (right, _, old_pers) = grid.get_tuple(from_2, index);
let (new_col, new_pers) = if pers < old_pers {
(Some(color), Some(pers))
} else {
(None, None)
};
match right {
DOT | CIRCLE => {}
VER => grid.set_opt(from_2, index, Some(VER_L), new_col, new_pers),
VER_R => grid.set_opt(from_2, index, Some(CROSS), None, None),
VER_L => grid.set_opt(from_2, index, None, new_col, new_pers),
HOR | R_D => grid.set_opt(from_2, index, Some(HOR_D), new_col, new_pers),
_ => {
grid.set_opt(from_2, index, Some(L_D), new_col, new_pers);
}
}
}
fn get_inserts(
tracks: &TrackMap,
layout: &TrackLayout,
compact: bool,
) -> HashMap<usize, Vec<Vec<Occ>>> {
let mut inserts: HashMap<usize, Vec<Vec<Occ>>> = HashMap::new();
for (idx, info) in tracks.commits.iter().enumerate() {
let track_inx = info.branch_trace.unwrap();
let column = layout
.track_visual(track_inx)
.expect("Visuals must be present for track")
.column
.expect("Track must have a column");
inserts.insert(idx, vec![vec![Occ::Commit(idx, column)]]);
}
for (idx, info) in tracks.commits.iter().enumerate() {
if let Some(trace) = info.branch_trace {
let branch_visual = layout
.track_visual(trace)
.expect("All tracks in print range must have visuals");
let column = branch_visual.column.unwrap();
for p in 0..2 {
let parent = info.parents[p];
let Some(par_oid) = parent else {
continue;
};
if let Some(par_idx) = tracks.indices.get(&par_oid) {
let par_info = &tracks.commits[*par_idx];
let par_track_idx = par_info.branch_trace.unwrap();
let par_branch_visual = layout
.track_visual(par_track_idx)
.expect("Parent track must have visuals");
let par_column = par_branch_visual.column.unwrap();
let column_range = sorted(column, par_column);
if column != par_column {
let split_index = get_deviate_index(tracks, layout, idx, *par_idx);
match inserts.entry(split_index) {
Occupied(mut entry) => {
let mut insert_at = entry.get().len();
for (insert_idx, sub_entry) in entry.get().iter().enumerate() {
let mut occ = false;
for other_range in sub_entry {
if other_range.overlaps(&column_range) {
match other_range {
Occ::Commit(target_index, _) => {
if !compact
|| !info.is_merge
|| idx != *target_index
|| p == 0
{
occ = true;
break;
}
}
Occ::Range(o_idx, o_par_idx, _, _) => {
if idx != *o_idx && par_idx != o_par_idx {
occ = true;
break;
}
}
}
}
}
if !occ {
insert_at = insert_idx;
break;
}
}
let vec = entry.get_mut();
if insert_at == vec.len() {
vec.push(vec![Occ::Range(
idx,
*par_idx,
column_range.0,
column_range.1,
)]);
} else {
vec[insert_at].push(Occ::Range(
idx,
*par_idx,
column_range.0,
column_range.1,
));
}
}
Vacant(entry) => {
entry.insert(vec![vec![Occ::Range(
idx,
*par_idx,
column_range.0,
column_range.1,
)]]);
}
}
}
}
}
}
}
inserts
}
fn get_deviate_index(
tracks: &TrackMap,
layout: &TrackLayout,
index: usize,
par_index: usize,
) -> usize {
let info = &tracks.commits[index];
let par_info = &tracks.commits[par_index];
let par_track_idx = par_info.branch_trace.unwrap();
let par_branch_visual = layout
.track_visual(par_track_idx)
.expect("Parent must have visual");
let mut min_split_idx = index;
for sibling_oid in &par_info.children {
if let Some(&sibling_index) = tracks.indices.get(sibling_oid) {
if let Some(sibling) = tracks.commits.get(sibling_index) {
if let Some(sibling_trace) = sibling.branch_trace {
let sibling_branch_visual = layout
.track_visual(sibling_trace)
.expect("Sibling must have visual");
if sibling_oid != &info.oid
&& sibling_branch_visual.column == par_branch_visual.column
&& sibling_index > min_split_idx
{
min_split_idx = sibling_index;
}
}
}
}
}
if info.is_merge {
max(index, min_split_idx)
} else {
(par_index as i32 - 1) as usize
}
}
fn print_graph(
characters: &Characters,
grid: &Grid,
text_lines: Vec<Option<String>>,
color: bool,
) -> (Vec<String>, Vec<String>) {
let mut g_lines = vec![];
let mut t_lines = vec![];
for (row, line) in grid.data.chunks(grid.width).zip(text_lines.into_iter()) {
let mut g_out = String::new();
let mut t_out = String::new();
if color {
for cell in row {
let chars = cell.char(characters);
if cell.character == SPACE {
write!(g_out, "{}", chars)
} else {
write!(g_out, "{}", chars.to_string().fixed(cell.color))
}
.unwrap();
}
} else {
let str = row
.iter()
.map(|cell| cell.char(characters))
.collect::<String>();
write!(g_out, "{}", str).unwrap();
}
if let Some(line) = line {
write!(t_out, "{}", line).unwrap();
}
g_lines.push(g_out);
t_lines.push(t_out);
}
(g_lines, t_lines)
}
fn format(
format: &CommitFormat,
layout: &TrackLayout,
labels: &LabelMap,
commit: &Commit,
info: &CommitInfo,
head: Option<&HeadInfo>,
color: bool,
wrapping: &Option<Options>,
) -> Result<Vec<String>, String> {
let branch_str = format_branches(layout, info, labels, head, color);
let hash_color = if color { Some(HASH_COLOR) } else { None };
crate::print::format::format_commit_metadata(commit, branch_str, wrapping, hash_color, format)
}
pub fn format_branches(
layout: &TrackLayout,
info: &CommitInfo,
labels: &LabelMap,
head: Option<&HeadInfo>,
color: bool,
) -> String {
let curr_color = info
.branch_trace
.and_then(|branch_idx| layout.track_visual(branch_idx))
.map(|visual| visual.term_color);
let mut branch_str = String::new();
fn append_str_col(target: &mut String, s: &str, color: bool, s_col: u8) {
if color {
write!(target, "{}", s.fixed(s_col)).unwrap();
} else {
write!(target, "{}", s).unwrap();
}
}
let head_str = "HEAD ->";
if let Some(head) = head {
if !head.is_branch {
branch_str.push_str(" ");
append_str_col(&mut branch_str, head_str, color, HEAD_COLOR);
}
}
let commit_branches: Vec<Label> = labels
.get_labels(&info.oid)
.into_iter()
.flatten()
.filter(|label| {
label.kind == LabelType::LocalBranch || label.kind == LabelType::RemoteBranch
})
.map(|label| label.clone())
.collect();
if !commit_branches.is_empty() {
branch_str.push_str(" (");
let branches = commit_branches.iter().sorted_by_key(|label| {
if let Some(head) = head {
head.name != label.name
} else {
false
}
});
for (idx, label) in branches.enumerate() {
let branch_color = label.term_color;
if let Some(head) = head {
if idx == 0 && head.is_branch {
append_str_col(&mut branch_str, head_str, color, HEAD_COLOR);
branch_str.push_str(" ");
}
}
append_str_col(&mut branch_str, &label.name, color, branch_color);
if idx < commit_branches.len() - 1 {
branch_str.push_str(", ");
}
}
branch_str.push_str(")");
}
let commit_tags: Vec<_> = labels
.get_labels(&info.oid)
.into_iter()
.flatten()
.filter(|label| label.kind == LabelType::Tag)
.collect();
if !commit_tags.is_empty() {
branch_str.push_str(" [");
for (idx, tag_label) in commit_tags.iter().enumerate() {
let tag_color = curr_color.unwrap_or(tag_label.term_color);
append_str_col(&mut branch_str, &tag_label.name, color, tag_color);
if idx < commit_tags.len() - 1 {
branch_str.push_str(", ");
}
}
branch_str.push_str("]");
}
branch_str
}
enum Occ {
Commit(usize, usize),
Range(usize, usize, usize, usize), }
impl Occ {
fn overlaps(&self, (start, end): &(usize, usize)) -> bool {
match self {
Occ::Commit(_, col) => start <= col && end >= col,
Occ::Range(_, _, s, e) => s <= end && e >= start,
}
}
}
fn sorted(v1: usize, v2: usize) -> (usize, usize) {
if v2 > v1 {
(v1, v2)
} else {
(v2, v1)
}
}
#[derive(Clone, Copy)]
struct GridCell {
character: u8,
color: u8,
pers: u8,
}
impl GridCell {
pub fn char(&self, characters: &Characters) -> char {
characters.chars[self.character as usize]
}
}
struct Grid {
width: usize,
height: usize,
data: Vec<GridCell>,
}
impl Grid {
pub fn new(width: usize, height: usize, initial: GridCell) -> Self {
Grid {
width,
height,
data: vec![initial; width * height],
}
}
pub fn reverse(&mut self) {
self.data.reverse();
}
pub fn index(&self, x: usize, y: usize) -> usize {
y * self.width + x
}
pub fn get_tuple(&self, x: usize, y: usize) -> (u8, u8, u8) {
let v = self.data[self.index(x, y)];
(v.character, v.color, v.pers)
}
pub fn set(&mut self, x: usize, y: usize, character: u8, color: u8, pers: u8) {
let idx = self.index(x, y);
self.data[idx] = GridCell {
character,
color,
pers,
};
}
pub fn set_opt(
&mut self,
x: usize,
y: usize,
character: Option<u8>,
color: Option<u8>,
pers: Option<u8>,
) {
let idx = self.index(x, y);
let cell = &mut self.data[idx];
if let Some(character) = character {
cell.character = character;
}
if let Some(color) = color {
cell.color = color;
}
if let Some(pers) = pers {
cell.pers = pers;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const DEF_CH: u8 = SPACE;
const DEF_COL: u8 = 0;
const DEF_PERS: u8 = 10; const DEFAULT_CELL: GridCell = GridCell {
character: DEF_CH,
color: DEF_COL,
pers: DEF_PERS,
};
const ROW_INDEX: usize = 1;
const LINE_COLOR: u8 = 14;
const LINE_PERS: u8 = 5;
#[test]
fn hline_skip() {
let (width, height) = (10, 3);
let mut grid = Grid::new(width, height, DEFAULT_CELL);
let initial_char = grid.get_tuple(4 * 2, ROW_INDEX).0;
super::hline(&mut grid, ROW_INDEX, (4, 4), true, LINE_COLOR, LINE_PERS);
assert_eq!(
grid.get_tuple(4 * 2, ROW_INDEX).0,
initial_char,
"Same index call should not modify grid"
);
}
#[test]
fn hline_forward_no_merge_out_of_bounds() {
let (width, height) = (10, 3);
let mut grid = Grid::new(width, height, DEFAULT_CELL);
super::hline(&mut grid, ROW_INDEX, (2, 5), false, LINE_COLOR, LINE_PERS);
assert_eq!(
grid.get_tuple(0, ROW_INDEX).0,
SPACE,
"SPACE at start of row"
);
assert_eq!(grid.get_tuple(3, ROW_INDEX).0, SPACE, "SPACE before hline");
assert_eq!(grid.get_tuple(4, ROW_INDEX).0, R_D, "R_D at start of hline");
assert_eq!(
grid.get_tuple(4, ROW_INDEX).1,
LINE_COLOR,
"line_color at start of hline"
);
assert_eq!(
grid.get_tuple(4, ROW_INDEX).2,
LINE_PERS,
"line_pers at start of hline"
);
}
#[test]
fn hline_forward_no_merge_at_bounds() {
let safe_width = 7; let height = 3;
let mut grid = Grid::new(safe_width, height, DEFAULT_CELL);
let from_idx = 1;
let to_idx = 3;
assert_eq!(
grid.get_tuple(2, ROW_INDEX).0,
SPACE,
"SPACE at start of line, before written"
);
super::hline(
&mut grid,
ROW_INDEX,
(from_idx, to_idx),
false,
LINE_COLOR,
LINE_PERS,
);
let grid_cell = grid.get_tuple(1, ROW_INDEX);
assert_eq!(grid_cell.0, SPACE, "SPACE before hline");
assert_eq!(grid_cell.1, DEF_COL, "default colour before hline");
assert_eq!(grid_cell.2, DEF_PERS, "default persistence before hline");
let grid_cell = grid.get_tuple(2, ROW_INDEX);
assert_eq!(grid_cell.0, R_D, "R_D at start of hline");
assert_eq!(grid_cell.1, LINE_COLOR, "line_color at start of hline");
assert_eq!(grid_cell.2, LINE_PERS, "line_pers at start of hline");
let grid_cell = grid.get_tuple(3, ROW_INDEX);
assert_eq!(grid_cell.0, HOR, "HOR in range of hline");
assert_eq!(grid_cell.1, LINE_COLOR, "line_color in range of hline");
assert_eq!(grid_cell.2, LINE_PERS, "line_pers in range of hline");
let grid_cell = grid.get_tuple(4, ROW_INDEX);
assert_eq!(grid_cell.0, HOR, "HOR in range of hline");
assert_eq!(grid_cell.1, LINE_COLOR, "line_color in range of hline");
assert_eq!(grid_cell.2, LINE_PERS, "line_pers in range of hline");
let grid_cell = grid.get_tuple(5, ROW_INDEX);
assert_eq!(grid_cell.0, HOR, "HOR in range of hline");
assert_eq!(grid_cell.1, LINE_COLOR, "line_color in range of hline");
assert_eq!(grid_cell.2, LINE_PERS, "line_pers in range of hline");
let grid_cell = grid.get_tuple(6, ROW_INDEX);
assert_eq!(grid_cell.0, L_U, "L_U at end of hline");
assert_eq!(grid_cell.1, LINE_COLOR, "line_color at end of hline");
assert_eq!(grid_cell.2, LINE_PERS, "line_pers at end of hline");
let grid_cell = grid.get_tuple(7, ROW_INDEX);
assert_eq!(grid_cell.0, SPACE, "SPACE before hline");
assert_eq!(grid_cell.1, DEF_COL, "default colour before hline");
assert_eq!(grid_cell.2, DEF_PERS, "default persistence before hline");
}
#[test]
fn hline_backward() {
let (width, height) = (10, 3);
let mut grid = Grid::new(width, height, DEFAULT_CELL);
grid.set(4, ROW_INDEX, VER, 10, 10); grid.set(8, ROW_INDEX, HOR, 10, 10);
let from_idx = 4;
let to_idx = 2;
let merge = true;
super::hline(
&mut grid,
ROW_INDEX,
(from_idx, to_idx),
merge,
LINE_COLOR,
LINE_PERS,
);
assert_eq!(grid.get_tuple(3, ROW_INDEX).0, SPACE, "SPACE before hline");
assert_eq!(
grid.get_tuple(3, ROW_INDEX).1,
DEF_COL,
"default colour before hline"
);
assert_eq!(
grid.get_tuple(3, ROW_INDEX).2,
DEF_PERS,
"default persistence before hline"
);
assert_eq!(grid.get_tuple(4, ROW_INDEX).0, VER_R, "VER_R at hline 'to'");
assert_eq!(
grid.get_tuple(4, ROW_INDEX).1,
10,
"unchanged color at hline 'to'"
);
assert_eq!(
grid.get_tuple(4, ROW_INDEX).2,
10,
"unchanged pers at hline 'to'"
);
assert_eq!(
grid.get_tuple(5, ROW_INDEX).0,
ARR_L,
"ARR_L before hline 'to'"
);
assert_eq!(
grid.get_tuple(5, ROW_INDEX).1,
LINE_COLOR,
"line_color in hline"
);
assert_eq!(
grid.get_tuple(5, ROW_INDEX).2,
LINE_PERS,
"line_pers in hline"
);
assert_eq!(grid.get_tuple(6, ROW_INDEX).0, HOR, "HOR in hline");
assert_eq!(
grid.get_tuple(6, ROW_INDEX).1,
LINE_COLOR,
"line_color in hline"
);
assert_eq!(
grid.get_tuple(6, ROW_INDEX).2,
LINE_PERS,
"line_pers in hline"
);
assert_eq!(grid.get_tuple(7, ROW_INDEX).0, HOR, "HOR in hline");
assert_eq!(
grid.get_tuple(7, ROW_INDEX).1,
LINE_COLOR,
"line_color in hline"
);
assert_eq!(
grid.get_tuple(7, ROW_INDEX).2,
LINE_PERS,
"line_pers in hline"
);
assert_eq!(
grid.get_tuple(8, ROW_INDEX).0,
HOR_D,
"HOR_D at hline 'from'"
);
assert_eq!(
grid.get_tuple(8, ROW_INDEX).1,
LINE_COLOR,
"line_color at hline 'from'"
);
assert_eq!(
grid.get_tuple(8, ROW_INDEX).2,
LINE_PERS,
"line_pers at hline 'from'"
);
}
#[test]
fn hline_forward_merge() {
let merge = true;
let (width, height) = (7, 3);
let mut grid = Grid::new(width, height, DEFAULT_CELL);
grid.set(5, ROW_INDEX, R_U, 10, 10); grid.set(6, ROW_INDEX, VER, 11, 10);
let from_idx = 1;
let to_idx = 3;
super::hline(
&mut grid,
ROW_INDEX,
(from_idx, to_idx),
merge,
LINE_COLOR,
LINE_PERS,
);
assert_eq!(grid.get_tuple(2, ROW_INDEX).0, R_D);
assert_eq!(grid.get_tuple(2, ROW_INDEX).1, LINE_COLOR);
assert_eq!(grid.get_tuple(2, ROW_INDEX).2, LINE_PERS);
assert_eq!(grid.get_tuple(3, ROW_INDEX).0, HOR);
assert_eq!(grid.get_tuple(3, ROW_INDEX).1, LINE_COLOR);
assert_eq!(grid.get_tuple(3, ROW_INDEX).2, LINE_PERS);
assert_eq!(grid.get_tuple(4, ROW_INDEX).0, HOR);
assert_eq!(grid.get_tuple(4, ROW_INDEX).1, LINE_COLOR);
assert_eq!(grid.get_tuple(4, ROW_INDEX).2, LINE_PERS);
assert_eq!(grid.get_tuple(5, ROW_INDEX).0, ARR_R);
assert_eq!(grid.get_tuple(5, ROW_INDEX).1, LINE_COLOR);
assert_eq!(grid.get_tuple(5, ROW_INDEX).2, LINE_PERS);
assert_eq!(grid.get_tuple(6, ROW_INDEX).0, VER_L);
assert_eq!(grid.get_tuple(6, ROW_INDEX).1, 11);
assert_eq!(grid.get_tuple(6, ROW_INDEX).2, 10);
}
}