1use std::io::{self, Write};
11
12use super::buffer::HeadingEntry;
13
14#[derive(Debug, Clone, Copy, Default)]
16pub struct Toc {
17 pub selected: usize,
19}
20
21impl Toc {
22 pub fn new(_headings: &[HeadingEntry]) -> Self {
24 Self::default()
25 }
26
27 pub fn step(&mut self, delta: isize, total: usize) {
29 if total == 0 {
30 self.selected = 0;
31 return;
32 }
33 let max = (total - 1) as isize;
34 let next = self.selected as isize + delta;
35 self.selected = next.clamp(0, max) as usize;
36 }
37
38 pub fn draw<W: Write>(
43 &self,
44 out: &mut W,
45 headings: &[HeadingEntry],
46 rows: usize,
47 ) -> io::Result<()> {
48 let top = self.selected.saturating_sub(rows / 2);
51 for row in 0..rows {
52 let idx = top + row;
53 if let Some(h) = headings.get(idx) {
54 let indent = " ".repeat(usize::from(h.level.saturating_sub(1)) * 2);
55 if idx == self.selected {
56 write!(out, "\x1b[7m{indent}{}\x1b[0m\r\n", h.text)?;
57 } else {
58 write!(out, "{indent}{}\r\n", h.text)?;
59 }
60 } else {
61 out.write_all(b"\r\n")?;
62 }
63 }
64 Ok(())
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 fn entries(n: usize) -> Vec<HeadingEntry> {
73 (0..n)
74 .map(|i| HeadingEntry {
75 level: 1,
76 text: format!("heading {i}"),
77 plain_offset: i * 10,
78 })
79 .collect()
80 }
81
82 #[test]
83 fn step_clamps_to_bounds() {
84 let hs = entries(3);
85 let mut t = Toc::new(&hs);
86 t.step(-5, hs.len());
87 assert_eq!(t.selected, 0);
88 t.step(10, hs.len());
89 assert_eq!(t.selected, 2);
90 }
91
92 #[test]
93 fn draw_marks_selected_entry_with_reverse_sgr() {
94 let hs = entries(3);
95 let mut t = Toc::new(&hs);
96 t.selected = 1;
97 let mut out = Vec::new();
98 t.draw(&mut out, &hs, 3).unwrap();
99 let s = String::from_utf8(out).unwrap();
100 assert!(s.contains("\x1b[7mheading 1"));
101 assert!(s.contains("heading 0"));
102 assert!(s.contains("heading 2"));
103 }
104}