git_graph/print/
unicode.rs

1//! Create graphs in Unicode format with ANSI X3.64 / ISO 6429 colour codes
2
3use crate::graph::{CommitInfo, GitGraph, HeadInfo};
4use crate::print::format::CommitFormat;
5use crate::settings::{Characters, Settings};
6use itertools::Itertools;
7use std::cmp::max;
8use std::collections::hash_map::Entry::{Occupied, Vacant};
9use std::collections::HashMap;
10use std::fmt::Write;
11use textwrap::Options;
12use yansi::Paint;
13
14const SPACE: u8 = 0;
15const DOT: u8 = 1;
16const CIRCLE: u8 = 2;
17const VER: u8 = 3;
18const HOR: u8 = 4;
19const CROSS: u8 = 5;
20const R_U: u8 = 6;
21const R_D: u8 = 7;
22const L_D: u8 = 8;
23const L_U: u8 = 9;
24const VER_L: u8 = 10;
25const VER_R: u8 = 11;
26const HOR_U: u8 = 12;
27const HOR_D: u8 = 13;
28
29const ARR_L: u8 = 14;
30const ARR_R: u8 = 15;
31
32const WHITE: u8 = 7;
33const HEAD_COLOR: u8 = 14;
34const HASH_COLOR: u8 = 11;
35
36/**
37UnicodeGraphInfo is a type alias for a tuple containing three elements:
38graph-lines, text-lines, start-row
39
401.  graph_lines: `Vec<String>` - This represents the lines of the generated text-based graph
41    visualization. Each `String` in this vector corresponds to a single row of
42    the graph output, containing characters that form the visual representation
43    of the commit history (like lines, dots, and branch intersections).
44
452.  text_lines: `Vec<String>`: This represents the lines of the commit messages or other
46    textual information associated with each commit in the graph. Each `String`
47    in this vector corresponds to a line of text that is displayed alongside
48    the graph. This can include commit hashes, author information, commit
49    messages, branch names, and tags, depending on the formatting settings.
50    Some entries in this vector might be empty strings or correspond to
51    inserted blank lines for visual spacing.
52
533.  start_row: `Vec<usize>`: Starting row for commit in the `graph.commits` vector.
54*/
55pub type UnicodeGraphInfo = (Vec<String>, Vec<String>, Vec<usize>);
56
57/// Creates a text-based visual representation of a graph.
58pub fn print_unicode(graph: &GitGraph, settings: &Settings) -> Result<UnicodeGraphInfo, String> {
59    if graph.all_branches.is_empty() {
60        return Ok((vec![], vec![], vec![]));
61    }
62    let num_cols = 2 * graph
63        .all_branches
64        .iter()
65        .map(|b| b.visual.column.unwrap_or(0))
66        .max()
67        .unwrap()
68        + 1;
69
70    let head_idx = graph.indices.get(&graph.head.oid);
71
72    let inserts = get_inserts(graph, settings.compact);
73
74    let (indent1, indent2) = if let Some((_, ind1, ind2)) = settings.wrapping {
75        (" ".repeat(ind1.unwrap_or(0)), " ".repeat(ind2.unwrap_or(0)))
76    } else {
77        ("".to_string(), "".to_string())
78    };
79
80    let wrap_options = if let Some((width, _, _)) = settings.wrapping {
81        create_wrapping_options(width, &indent1, &indent2, num_cols + 4)?
82    } else {
83        None
84    };
85
86    // Compute commit text into text_lines and add blank rows
87    // if needed to match branch graph inserts.
88    let mut index_map = vec![];
89    let mut text_lines = vec![];
90    let mut offset = 0;
91    for (idx, info) in graph.commits.iter().enumerate() {
92        index_map.push(idx + offset);
93        let cnt_inserts = if let Some(inserts) = inserts.get(&idx) {
94            inserts
95                .iter()
96                .filter(|vec| {
97                    vec.iter().all(|occ| match occ {
98                        Occ::Commit(_, _) => false,
99                        Occ::Range(_, _, _, _) => true,
100                    })
101                })
102                .count()
103        } else {
104            0
105        };
106
107        let head = if head_idx == Some(&idx) {
108            Some(&graph.head)
109        } else {
110            None
111        };
112
113        let lines = format(
114            &settings.format,
115            graph,
116            info,
117            head,
118            settings.colored,
119            &wrap_options,
120        )?;
121
122        let num_lines = if lines.is_empty() { 0 } else { lines.len() - 1 };
123        let max_inserts = max(cnt_inserts, num_lines);
124        let add_lines = max_inserts - num_lines;
125
126        text_lines.extend(lines.into_iter().map(Some));
127        text_lines.extend((0..add_lines).map(|_| None));
128
129        offset += max_inserts;
130    }
131
132    let mut grid = Grid::new(
133        num_cols,
134        graph.commits.len() + offset,
135        GridCell {
136            character: SPACE,
137            color: WHITE,
138            pers: settings.branches.persistence.len() as u8 + 2,
139        },
140    );
141
142    // Compute branch lines in grid
143    for (idx, info) in graph.commits.iter().enumerate() {
144        let Some(trace) = info.branch_trace else {
145            continue;
146        };
147        let branch = &graph.all_branches[trace];
148        let column = branch.visual.column.unwrap();
149        let idx_map = index_map[idx];
150
151        let branch_color = branch.visual.term_color;
152
153        grid.set(
154            column * 2,
155            idx_map,
156            if info.is_merge { CIRCLE } else { DOT },
157            branch_color,
158            branch.persistence,
159        );
160        for p in 0..2 {
161            let parent = info.parents[p];
162            let Some(par_oid) = parent else {
163                continue;
164            };
165            let Some(par_idx) = graph.indices.get(&par_oid) else {
166                // Parent is outside scope of graph.indices
167                // so draw a vertical line to the bottom
168                let idx_bottom = grid.height;
169                vline(
170                    &mut grid,
171                    (idx_map, idx_bottom),
172                    column,
173                    branch_color,
174                    branch.persistence,
175                );
176                continue;
177            };
178
179            let par_idx_map = index_map[*par_idx];
180            let par_info = &graph.commits[*par_idx];
181            let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()];
182            let par_column = par_branch.visual.column.unwrap();
183
184            let (color, pers) = if info.is_merge {
185                (par_branch.visual.term_color, par_branch.persistence)
186            } else {
187                (branch_color, branch.persistence)
188            };
189
190            if branch.visual.column == par_branch.visual.column {
191                if par_idx_map > idx_map + 1 {
192                    vline(&mut grid, (idx_map, par_idx_map), column, color, pers);
193                }
194            } else {
195                let split_index = super::get_deviate_index(graph, idx, *par_idx);
196                let split_idx_map = index_map[split_index];
197                let insert_idx = find_insert_idx(&inserts[&split_index], idx, *par_idx).unwrap();
198                let idx_split = split_idx_map + insert_idx;
199
200                let is_secondary_merge = info.is_merge && p > 0;
201
202                let row123 = (idx_map, idx_split, par_idx_map);
203                let col12 = (column, par_column);
204                zig_zag_line(&mut grid, row123, col12, is_secondary_merge, color, pers);
205            }
206        }
207    }
208
209    if settings.reverse_commit_order {
210        text_lines.reverse();
211        grid.reverse();
212    }
213
214    let lines = print_graph(&settings.characters, &grid, text_lines, settings.colored);
215
216    Ok((lines.0, lines.1, index_map))
217}
218
219/// Create `textwrap::Options` from width and indent.
220fn create_wrapping_options<'a>(
221    width: Option<usize>,
222    indent1: &'a str,
223    indent2: &'a str,
224    graph_width: usize,
225) -> Result<Option<Options<'a>>, String> {
226    let wrapping = if let Some(width) = width {
227        Some(
228            textwrap::Options::new(width)
229                .initial_indent(indent1)
230                .subsequent_indent(indent2),
231        )
232    } else if atty::is(atty::Stream::Stdout) {
233        let width = crossterm::terminal::size()
234            .map_err(|err| err.to_string())?
235            .0 as usize;
236        let text_width = width.saturating_sub(graph_width);
237        if text_width < 40 {
238            // If too little space left for text, do not wrap at all
239            None
240        } else {
241            Some(
242                textwrap::Options::new(text_width)
243                    .initial_indent(indent1)
244                    .subsequent_indent(indent2),
245            )
246        }
247    } else {
248        None
249    };
250    Ok(wrapping)
251}
252
253/// Find the index of the insert that connects the two commits
254fn find_insert_idx(inserts: &[Vec<Occ>], child_idx: usize, parent_idx: usize) -> Option<usize> {
255    for (insert_idx, sub_entry) in inserts.iter().enumerate() {
256        for occ in sub_entry {
257            if let Occ::Range(i1, i2, _, _) = occ {
258                if *i1 == child_idx && *i2 == parent_idx {
259                    return Some(insert_idx);
260                }
261            }
262        }
263    }
264    None
265}
266
267/// Draw a line that connects two commits on different columns
268fn zig_zag_line(
269    grid: &mut Grid,
270    row123: (usize, usize, usize),
271    col12: (usize, usize),
272    is_merge: bool,
273    color: u8,
274    pers: u8,
275) {
276    let (row1, row2, row3) = row123;
277    let (col1, col2) = col12;
278    vline(grid, (row1, row2), col1, color, pers);
279    hline(grid, row2, (col2, col1), is_merge, color, pers);
280    vline(grid, (row2, row3), col2, color, pers);
281}
282
283/// Draws a vertical line
284fn vline(grid: &mut Grid, (from, to): (usize, usize), column: usize, color: u8, pers: u8) {
285    for i in (from + 1)..to {
286        let (curr, _, old_pers) = grid.get_tuple(column * 2, i);
287        let (new_col, new_pers) = if pers < old_pers {
288            (Some(color), Some(pers))
289        } else {
290            (None, None)
291        };
292        match curr {
293            DOT | CIRCLE => {}
294            HOR => {
295                grid.set_opt(column * 2, i, Some(CROSS), Some(color), Some(pers));
296            }
297            HOR_U | HOR_D => {
298                grid.set_opt(column * 2, i, Some(CROSS), Some(color), Some(pers));
299            }
300            CROSS | VER | VER_L | VER_R => grid.set_opt(column * 2, i, None, new_col, new_pers),
301            L_D | L_U => {
302                grid.set_opt(column * 2, i, Some(VER_L), new_col, new_pers);
303            }
304            R_D | R_U => {
305                grid.set_opt(column * 2, i, Some(VER_R), new_col, new_pers);
306            }
307            _ => {
308                grid.set_opt(column * 2, i, Some(VER), new_col, new_pers);
309            }
310        }
311    }
312}
313
314/// Draws a horizontal line
315fn hline(
316    grid: &mut Grid,
317    index: usize,
318    (from, to): (usize, usize),
319    merge: bool,
320    color: u8,
321    pers: u8,
322) {
323    if from == to {
324        return;
325    }
326    let from_2 = from * 2;
327    let to_2 = to * 2;
328    if from < to {
329        for column in (from_2 + 1)..to_2 {
330            if merge && column == to_2 - 1 {
331                grid.set(column, index, ARR_R, color, pers);
332            } else {
333                let (curr, _, old_pers) = grid.get_tuple(column, index);
334                let (new_col, new_pers) = if pers < old_pers {
335                    (Some(color), Some(pers))
336                } else {
337                    (None, None)
338                };
339                match curr {
340                    DOT | CIRCLE => {}
341                    VER => grid.set_opt(column, index, Some(CROSS), None, None),
342                    HOR | CROSS | HOR_U | HOR_D => {
343                        grid.set_opt(column, index, None, new_col, new_pers)
344                    }
345                    L_U | R_U => grid.set_opt(column, index, Some(HOR_U), new_col, new_pers),
346                    L_D | R_D => grid.set_opt(column, index, Some(HOR_D), new_col, new_pers),
347                    _ => {
348                        grid.set_opt(column, index, Some(HOR), new_col, new_pers);
349                    }
350                }
351            }
352        }
353
354        let (left, _, old_pers) = grid.get_tuple(from_2, index);
355        let (new_col, new_pers) = if pers < old_pers {
356            (Some(color), Some(pers))
357        } else {
358            (None, None)
359        };
360        match left {
361            DOT | CIRCLE => {}
362            VER => grid.set_opt(from_2, index, Some(VER_R), new_col, new_pers),
363            VER_L => grid.set_opt(from_2, index, Some(CROSS), None, None),
364            VER_R => {}
365            HOR | L_U => grid.set_opt(from_2, index, Some(HOR_U), new_col, new_pers),
366            _ => {
367                grid.set_opt(from_2, index, Some(R_D), new_col, new_pers);
368            }
369        }
370
371        let (right, _, old_pers) = grid.get_tuple(to_2, index);
372        let (new_col, new_pers) = if pers < old_pers {
373            (Some(color), Some(pers))
374        } else {
375            (None, None)
376        };
377        match right {
378            DOT | CIRCLE => {}
379            VER => grid.set_opt(to_2, index, Some(VER_L), None, None),
380            VER_L | HOR_U => grid.set_opt(to_2, index, None, new_col, new_pers),
381            HOR | R_U => grid.set_opt(to_2, index, Some(HOR_U), new_col, new_pers),
382            _ => {
383                grid.set_opt(to_2, index, Some(L_U), new_col, new_pers);
384            }
385        }
386    } else {
387        for column in (to_2 + 1)..from_2 {
388            if merge && column == to_2 + 1 {
389                grid.set(column, index, ARR_L, color, pers);
390            } else {
391                let (curr, _, old_pers) = grid.get_tuple(column, index);
392                let (new_col, new_pers) = if pers < old_pers {
393                    (Some(color), Some(pers))
394                } else {
395                    (None, None)
396                };
397                match curr {
398                    DOT | CIRCLE => {}
399                    VER => grid.set_opt(column, index, Some(CROSS), None, None),
400                    HOR | CROSS | HOR_U | HOR_D => {
401                        grid.set_opt(column, index, None, new_col, new_pers)
402                    }
403                    L_U | R_U => grid.set_opt(column, index, Some(HOR_U), new_col, new_pers),
404                    L_D | R_D => grid.set_opt(column, index, Some(HOR_D), new_col, new_pers),
405                    _ => {
406                        grid.set_opt(column, index, Some(HOR), new_col, new_pers);
407                    }
408                }
409            }
410        }
411
412        let (left, _, old_pers) = grid.get_tuple(to_2, index);
413        let (new_col, new_pers) = if pers < old_pers {
414            (Some(color), Some(pers))
415        } else {
416            (None, None)
417        };
418        match left {
419            DOT | CIRCLE => {}
420            VER => grid.set_opt(to_2, index, Some(VER_R), None, None),
421            VER_R => grid.set_opt(to_2, index, None, new_col, new_pers),
422            HOR | L_U => grid.set_opt(to_2, index, Some(HOR_U), new_col, new_pers),
423            _ => {
424                grid.set_opt(to_2, index, Some(R_U), new_col, new_pers);
425            }
426        }
427
428        let (right, _, old_pers) = grid.get_tuple(from_2, index);
429        let (new_col, new_pers) = if pers < old_pers {
430            (Some(color), Some(pers))
431        } else {
432            (None, None)
433        };
434        match right {
435            DOT | CIRCLE => {}
436            VER => grid.set_opt(from_2, index, Some(VER_L), new_col, new_pers),
437            VER_R => grid.set_opt(from_2, index, Some(CROSS), None, None),
438            VER_L => grid.set_opt(from_2, index, None, new_col, new_pers),
439            HOR | R_D => grid.set_opt(from_2, index, Some(HOR_D), new_col, new_pers),
440            _ => {
441                grid.set_opt(from_2, index, Some(L_D), new_col, new_pers);
442            }
443        }
444    }
445}
446
447/// Calculates required additional rows to visually connect commits that
448/// are not direct descendants in the main commit list. These "inserts"
449//  represent the horizontal lines in the graph.
450///
451/// # Arguments
452///
453/// * `graph`: A reference to the `GitGraph` structure containing the
454//             commit and branch information.
455/// * `compact`: A boolean indicating whether to use a compact layout,
456//               potentially merging some insertions with commits.
457///
458/// # Returns
459///
460/// A `HashMap` where the keys are the indices of commits in the
461/// `graph.commits` vector, and the values are vectors of vectors
462/// of `Occ`. Each inner vector represents a potential row of
463/// insertions needed *before* the commit at the key index. The
464/// `Occ` enum describes what occupies a cell in that row
465/// (either a commit or a range representing a connection).
466///
467fn get_inserts(graph: &GitGraph, compact: bool) -> HashMap<usize, Vec<Vec<Occ>>> {
468    // Initialize an empty HashMap to store the required insertions. The key is the commit
469    // index, and the value is a vector of rows, where each row is a vector of Occupations (`Occ`).
470    let mut inserts: HashMap<usize, Vec<Vec<Occ>>> = HashMap::new();
471
472    // First, for each commit, we initialize an entry in the `inserts`
473    // map with a single row containing the commit itself. This ensures
474    // that every commit has a position in the grid.
475    for (idx, info) in graph.commits.iter().enumerate() {
476        // Get the visual column assigned to the branch of this commit. Unwrap is safe here
477        // because `branch_trace` should always point to a valid branch with an assigned column
478        // for commits that are included in the filtered graph.
479        let column = graph.all_branches[info.branch_trace.unwrap()]
480            .visual
481            .column
482            .unwrap();
483
484        inserts.insert(idx, vec![vec![Occ::Commit(idx, column)]]);
485    }
486
487    // Now, iterate through the commits again to identify connections
488    // needed between parents that are not directly adjacent in the
489    // `graph.commits` list.
490    for (idx, info) in graph.commits.iter().enumerate() {
491        // If the commit has a branch trace (meaning it belongs to a visualized branch).
492        if let Some(trace) = info.branch_trace {
493            // Get the `BranchInfo` for the current commit's branch.
494            let branch = &graph.all_branches[trace];
495            // Get the visual column of the current commit's branch. Unwrap is safe as explained above.
496            let column = branch.visual.column.unwrap();
497
498            // Iterate through the two possible parents of the current commit.
499            for p in 0..2 {
500                let parent = info.parents[p];
501                let Some(par_oid) = parent else {
502                    continue;
503                };
504                // Try to find the index of the parent commit in the `graph.commits` vector.
505                if let Some(par_idx) = graph.indices.get(&par_oid) {
506                    let par_info = &graph.commits[*par_idx];
507                    let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()];
508                    let par_column = par_branch.visual.column.unwrap();
509                    // Determine the sorted range of columns between the current commit and its parent.
510                    let column_range = sorted(column, par_column);
511
512                    // If the column of the current commit is different from the column of its parent,
513                    // it means we need to draw a horizontal line (an "insert") to connect them.
514                    if column != par_column {
515                        // Find the index in the `graph.commits` list where the visual connection
516                        // should deviate from the parent's line. This helps in drawing the graph
517                        // correctly when branches diverge or merge.
518                        let split_index = super::get_deviate_index(graph, idx, *par_idx);
519                        // Access the entry in the `inserts` map for the `split_index`.
520                        match inserts.entry(split_index) {
521                            // If there's already an entry at this `split_index` (meaning other
522                            // insertions might be needed before this commit).
523                            Occupied(mut entry) => {
524                                // Find the first available row in the existing vector of rows
525                                // where the new range doesn't overlap with existing occupations.
526                                let mut insert_at = entry.get().len();
527                                for (insert_idx, sub_entry) in entry.get().iter().enumerate() {
528                                    let mut occ = false;
529                                    // Check for overlaps with existing `Occ` in the current row.
530                                    for other_range in sub_entry {
531                                        // Check if the current column range overlaps with the other range.
532                                        if other_range.overlaps(&column_range) {
533                                            match other_range {
534                                                // If the other occupation is a commit.
535                                                Occ::Commit(target_index, _) => {
536                                                    // In compact mode, we might allow overlap with the commit itself
537                                                    // for merge commits (specifically the second parent) to keep the
538                                                    // graph tighter.
539                                                    if !compact
540                                                        || !info.is_merge
541                                                        || idx != *target_index
542                                                        || p == 0
543                                                    {
544                                                        occ = true;
545                                                        break;
546                                                    }
547                                                }
548                                                // If the other occupation is a range (another connection).
549                                                Occ::Range(o_idx, o_par_idx, _, _) => {
550                                                    // Avoid overlap with connections between the same commits.
551                                                    if idx != *o_idx && par_idx != o_par_idx {
552                                                        occ = true;
553                                                        break;
554                                                    }
555                                                }
556                                            }
557                                        }
558                                    }
559                                    // If no overlap is found in this row, we can insert here.
560                                    if !occ {
561                                        insert_at = insert_idx;
562                                        break;
563                                    }
564                                }
565                                // Get a mutable reference to the vector of rows for this `split_index`.
566                                let vec = entry.get_mut();
567                                // If no suitable row was found, add a new row.
568                                if insert_at == vec.len() {
569                                    vec.push(vec![Occ::Range(
570                                        idx,
571                                        *par_idx,
572                                        column_range.0,
573                                        column_range.1,
574                                    )]);
575                                } else {
576                                    // Otherwise, insert the new range into the found row.
577                                    vec[insert_at].push(Occ::Range(
578                                        idx,
579                                        *par_idx,
580                                        column_range.0,
581                                        column_range.1,
582                                    ));
583                                }
584                            }
585                            // If there's no entry at this `split_index` yet.
586                            Vacant(entry) => {
587                                // Create a new entry with a single row containing the range.
588                                entry.insert(vec![vec![Occ::Range(
589                                    idx,
590                                    *par_idx,
591                                    column_range.0,
592                                    column_range.1,
593                                )]]);
594                            }
595                        }
596                    }
597                }
598            }
599        }
600    }
601
602    // Return the map of required insertions.
603    inserts
604}
605
606/// Creates the complete graph visualization, incl. formatter commits.
607fn print_graph(
608    characters: &Characters,
609    grid: &Grid,
610    text_lines: Vec<Option<String>>,
611    color: bool,
612) -> (Vec<String>, Vec<String>) {
613    let mut g_lines = vec![];
614    let mut t_lines = vec![];
615
616    for (row, line) in grid.data.chunks(grid.width).zip(text_lines.into_iter()) {
617        let mut g_out = String::new();
618        let mut t_out = String::new();
619
620        if color {
621            for cell in row {
622                let chars = cell.char(characters);
623                if cell.character == SPACE {
624                    write!(g_out, "{}", chars)
625                } else {
626                    write!(g_out, "{}", chars.to_string().fixed(cell.color))
627                }
628                .unwrap();
629            }
630        } else {
631            let str = row
632                .iter()
633                .map(|cell| cell.char(characters))
634                .collect::<String>();
635            write!(g_out, "{}", str).unwrap();
636        }
637
638        if let Some(line) = line {
639            write!(t_out, "{}", line).unwrap();
640        }
641
642        g_lines.push(g_out);
643        t_lines.push(t_out);
644    }
645
646    (g_lines, t_lines)
647}
648
649/// Format a commit.
650fn format(
651    format: &CommitFormat,
652    graph: &GitGraph,
653    info: &CommitInfo,
654    head: Option<&HeadInfo>,
655    color: bool,
656    wrapping: &Option<Options>,
657) -> Result<Vec<String>, String> {
658    let commit = graph
659        .repository
660        .find_commit(info.oid)
661        .map_err(|err| err.message().to_string())?;
662
663    let branch_str = format_branches(graph, info, head, color);
664
665    let hash_color = if color { Some(HASH_COLOR) } else { None };
666
667    crate::print::format::format(&commit, branch_str, wrapping, hash_color, format)
668}
669
670/// Format branches and tags.
671pub fn format_branches(
672    graph: &GitGraph,
673    info: &CommitInfo,
674    head: Option<&HeadInfo>,
675    color: bool,
676) -> String {
677    let curr_color = info
678        .branch_trace
679        .map(|branch_idx| &graph.all_branches[branch_idx].visual.term_color);
680
681    let mut branch_str = String::new();
682
683    let head_str = "HEAD ->";
684    if let Some(head) = head {
685        if !head.is_branch {
686            if color {
687                write!(branch_str, " {}", head_str.fixed(HEAD_COLOR))
688            } else {
689                write!(branch_str, " {}", head_str)
690            }
691            .unwrap();
692        }
693    }
694
695    if !info.branches.is_empty() {
696        write!(branch_str, " (").unwrap();
697
698        let branches = info.branches.iter().sorted_by_key(|br| {
699            if let Some(head) = head {
700                head.name != graph.all_branches[**br].name
701            } else {
702                false
703            }
704        });
705
706        for (idx, branch_index) in branches.enumerate() {
707            let branch = &graph.all_branches[*branch_index];
708            let branch_color = branch.visual.term_color;
709
710            if let Some(head) = head {
711                if idx == 0 && head.is_branch {
712                    if color {
713                        write!(branch_str, "{} ", head_str.fixed(14))
714                    } else {
715                        write!(branch_str, "{} ", head_str)
716                    }
717                    .unwrap();
718                }
719            }
720
721            if color {
722                write!(branch_str, "{}", &branch.name.fixed(branch_color))
723            } else {
724                write!(branch_str, "{}", &branch.name)
725            }
726            .unwrap();
727
728            if idx < info.branches.len() - 1 {
729                write!(branch_str, ", ").unwrap();
730            }
731        }
732        write!(branch_str, ")").unwrap();
733    }
734
735    if !info.tags.is_empty() {
736        write!(branch_str, " [").unwrap();
737        for (idx, tag_index) in info.tags.iter().enumerate() {
738            let tag = &graph.all_branches[*tag_index];
739            let tag_color = curr_color.unwrap_or(&tag.visual.term_color);
740
741            let tag_name = &tag.name[5..];
742            if color {
743                write!(branch_str, "{}", tag_name.fixed(*tag_color))
744            } else {
745                write!(branch_str, "{}", tag_name)
746            }
747            .unwrap();
748
749            if idx < info.tags.len() - 1 {
750                write!(branch_str, ", ").unwrap();
751            }
752        }
753        write!(branch_str, "]").unwrap();
754    }
755
756    branch_str
757}
758
759/// Occupied row ranges
760enum Occ {
761    /// Horizontal position of commit markers
762    // First  field (usize): The index of a commit within the graph.commits vector.
763    // Second field (usize): The visual column in the grid where this commit is located. This column is determined by the branch the commit belongs to.
764    // Purpose: This variant of Occ signifies that a specific row in the grid is occupied by a commit marker (dot or circle) at a particular column.
765    Commit(usize, usize), // index in Graph.commits, column
766
767    /// Horizontal line connecting two commits
768    // First  field (usize): The index of the starting commit of a visual connection (usually the child commit).
769    // Second field (usize): The index of the ending commit of a visual connection (usually the parent commit).
770    // Third  field (usize): The starting visual column of the range occupied by the connection line between the two commits. This is the minimum of the columns of the two connected commits.
771    // Fourth field (usize): The ending visual column of the range occupied by the connection line between the two commits. This is the maximum of the columns of the two connected commits.
772    // Purpose: This variant of Occ signifies that a range of columns in a particular row is occupied by a horizontal line segment connecting a commit to one of its parents. The range spans from the visual column of one commit to the visual column of the other.
773    Range(usize, usize, usize, usize), // ?child index, parent index, leftmost column, rightmost column
774}
775
776impl Occ {
777    fn overlaps(&self, (start, end): &(usize, usize)) -> bool {
778        match self {
779            Occ::Commit(_, col) => start <= col && end >= col,
780            Occ::Range(_, _, s, e) => s <= end && e >= start,
781        }
782    }
783}
784
785/// Sorts two numbers in ascending order
786fn sorted(v1: usize, v2: usize) -> (usize, usize) {
787    if v2 > v1 {
788        (v1, v2)
789    } else {
790        (v2, v1)
791    }
792}
793
794/// One cell in a [Grid]
795#[derive(Clone, Copy)]
796struct GridCell {
797    /// The symbol shown, encoded as in index into settings::Characters
798    character: u8,
799    /// Standard 8-bit terminal colour code
800    color: u8,
801    /// Persistence level. z-order, lower numbers take preceedence.
802    pers: u8,
803}
804
805impl GridCell {
806    pub fn char(&self, characters: &Characters) -> char {
807        characters.chars[self.character as usize]
808    }
809}
810
811/// Two-dimensional grid used to hold the graph layout.
812///
813/// This can be rendered as unicode text or as SVG.
814struct Grid {
815    width: usize,
816    height: usize,
817
818    /// Grid cells are stored in row-major order.
819    data: Vec<GridCell>,
820}
821
822impl Grid {
823    pub fn new(width: usize, height: usize, initial: GridCell) -> Self {
824        Grid {
825            width,
826            height,
827            data: vec![initial; width * height],
828        }
829    }
830
831    pub fn reverse(&mut self) {
832        self.data.reverse();
833    }
834    /// Turn a 2D coordinate into an index of Grid.data
835    pub fn index(&self, x: usize, y: usize) -> usize {
836        y * self.width + x
837    }
838    pub fn get_tuple(&self, x: usize, y: usize) -> (u8, u8, u8) {
839        let v = self.data[self.index(x, y)];
840        (v.character, v.color, v.pers)
841    }
842    pub fn set(&mut self, x: usize, y: usize, character: u8, color: u8, pers: u8) {
843        let idx = self.index(x, y);
844        self.data[idx] = GridCell {
845            character,
846            color,
847            pers,
848        };
849    }
850    pub fn set_opt(
851        &mut self,
852        x: usize,
853        y: usize,
854        character: Option<u8>,
855        color: Option<u8>,
856        pers: Option<u8>,
857    ) {
858        let idx = self.index(x, y);
859        let cell = &mut self.data[idx];
860        if let Some(character) = character {
861            cell.character = character;
862        }
863        if let Some(color) = color {
864            cell.color = color;
865        }
866        if let Some(pers) = pers {
867            cell.pers = pers;
868        }
869    }
870}