use std::io::{self, Write};
use std::ops::Range;
use crossterm::{cursor, queue, style::Print, terminal};
use regex::Regex;
use crate::buffer::LineBuffer;
use crate::flags::Flags;
use crate::layout::Layout;
pub struct Viewport {
pub top: usize,
pub rows: u16,
pub cols: u16,
}
impl Viewport {
pub fn content_rows(&self) -> usize {
(self.rows as usize).saturating_sub(1)
}
}
const SGR_RESET: &str = "\x1b[0m";
const HL_ON: &str = "\x1b[7m";
const HL_OFF: &str = "\x1b[27m";
pub fn draw(
buf: &LineBuffer,
layout: &Layout,
vp: &Viewport,
_flags: &Flags,
status: &str,
highlight: Option<&Regex>,
) -> anyhow::Result<()> {
let mut out = io::stdout().lock();
queue!(
out,
terminal::Clear(terminal::ClearType::All),
cursor::MoveTo(0, 0),
Print(SGR_RESET),
)?;
let content_rows = vp.content_rows();
for i in 0..content_rows {
let seg_idx = vp.top + i;
let Some(seg) = layout.segment(seg_idx) else { break };
let line = &buf.lines()[seg.line_idx];
let highlights: Vec<Range<usize>> = match highlight {
Some(re) => re.find_iter(line).map(|m| m.range()).collect(),
None => Vec::new(),
};
let rendered = render_segment(line, seg.byte_start..seg.byte_end, &highlights);
queue!(out, cursor::MoveTo(0, i as u16), Print(rendered))?;
let next_is_same_line = layout
.segment(seg_idx + 1)
.is_some_and(|n| n.line_idx == seg.line_idx);
if !next_is_same_line {
queue!(out, Print(SGR_RESET))?;
}
}
queue!(
out,
cursor::MoveTo(0, content_rows as u16),
Print(SGR_RESET),
terminal::Clear(terminal::ClearType::CurrentLine),
Print(status),
)?;
out.flush()?;
Ok(())
}
fn render_segment(line: &str, range: Range<usize>, highlights: &[Range<usize>]) -> String {
let s = &line[range.start..range.end];
let mut out = String::with_capacity(s.len() + 16);
let mut in_hl = false;
let mut chars = s.char_indices().peekable();
while let Some((local_i, c)) = chars.next() {
let abs_i = range.start + local_i;
let want_hl = highlights.iter().any(|r| r.contains(&abs_i));
if want_hl && !in_hl {
out.push_str(HL_ON);
in_hl = true;
} else if !want_hl && in_hl {
out.push_str(HL_OFF);
in_hl = false;
}
if c == '\x1b' {
if let Some(&(_, '[')) = chars.peek() {
out.push(c);
let (_, br) = chars.next().unwrap();
out.push(br);
while let Some(&(_, p)) = chars.peek() {
let v = p as u32;
if (0x20..=0x3F).contains(&v) {
let (_, p) = chars.next().unwrap();
out.push(p);
} else {
break;
}
}
if let Some(&(_, p)) = chars.peek() {
let v = p as u32;
if (0x40..=0x7E).contains(&v) {
let (_, p) = chars.next().unwrap();
out.push(p);
}
}
continue;
}
}
out.push(c);
}
if in_hl {
out.push_str(HL_OFF);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_full_segment_unchanged() {
let s = "hello";
assert_eq!(render_segment(s, 0..5, &[]), "hello");
}
#[test]
fn renders_partial_segment() {
let s = "0123456789";
assert_eq!(render_segment(s, 0..5, &[]), "01234");
assert_eq!(render_segment(s, 5..10, &[]), "56789");
}
#[test]
fn highlights_match_with_sgr() {
let s = "hello";
let r = render_segment(s, 0..5, &[2..4]);
assert_eq!(r, format!("he{}ll{}o", HL_ON, HL_OFF));
}
#[test]
fn highlight_uses_absolute_byte_offsets() {
let s = "0123456789";
let r = render_segment(s, 5..10, &[6..8]);
assert_eq!(r, format!("5{}67{}89", HL_ON, HL_OFF));
}
#[test]
fn sgr_passes_through_in_segment() {
let s = "\x1b[31mhi\x1b[0m";
assert_eq!(render_segment(s, 0..s.len(), &[]), s);
}
#[test]
fn highlight_closed_at_segment_end() {
let s = "hello";
let r = render_segment(s, 0..5, &[3..10]);
assert!(r.ends_with(HL_OFF));
}
}