use std::io::{self, Write};
use super::buffer::HeadingEntry;
#[derive(Debug, Clone, Copy, Default)]
pub struct Toc {
pub selected: usize,
}
impl Toc {
pub fn new(_headings: &[HeadingEntry]) -> Self {
Self::default()
}
pub fn step(&mut self, delta: isize, total: usize) {
if total == 0 {
self.selected = 0;
return;
}
let max = (total - 1) as isize;
let next = self.selected as isize + delta;
self.selected = next.clamp(0, max) as usize;
}
pub fn draw<W: Write>(
&self,
out: &mut W,
headings: &[HeadingEntry],
rows: usize,
) -> io::Result<()> {
let top = self.selected.saturating_sub(rows / 2);
for row in 0..rows {
let idx = top + row;
if let Some(h) = headings.get(idx) {
let indent = " ".repeat(usize::from(h.level.saturating_sub(1)) * 2);
if idx == self.selected {
write!(out, "\x1b[7m{indent}{}\x1b[0m\r\n", h.text)?;
} else {
write!(out, "{indent}{}\r\n", h.text)?;
}
} else {
out.write_all(b"\r\n")?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entries(n: usize) -> Vec<HeadingEntry> {
(0..n)
.map(|i| HeadingEntry {
level: 1,
text: format!("heading {i}"),
plain_offset: i * 10,
})
.collect()
}
#[test]
fn step_clamps_to_bounds() {
let hs = entries(3);
let mut t = Toc::new(&hs);
t.step(-5, hs.len());
assert_eq!(t.selected, 0);
t.step(10, hs.len());
assert_eq!(t.selected, 2);
}
#[test]
fn draw_marks_selected_entry_with_reverse_sgr() {
let hs = entries(3);
let mut t = Toc::new(&hs);
t.selected = 1;
let mut out = Vec::new();
t.draw(&mut out, &hs, 3).unwrap();
let s = String::from_utf8(out).unwrap();
assert!(s.contains("\x1b[7mheading 1"));
assert!(s.contains("heading 0"));
assert!(s.contains("heading 2"));
}
}