use super::cell::Cell;
use super::diff::Run;
use super::scroll::{ScrolledRuns, Shift};
use super::sgr::{build_incremental, build_reset, csi_n, cup, push_u32, resolve_pen, Pen};
use super::surface::Surface;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct PresenterOpts {
pub scroll_optimization: bool,
}
impl Default for PresenterOpts {
fn default() -> Self {
PresenterOpts {
scroll_optimization: true,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct PresentCaps {
pub color: ColorDepth,
pub sync_output_2026: bool,
pub hyperlinks: bool,
pub undercurl: bool,
pub underline_color: bool,
}
impl PresentCaps {
pub const BASELINE: PresentCaps = PresentCaps {
color: ColorDepth::Ansi16,
sync_output_2026: false,
hyperlinks: false,
undercurl: false,
underline_color: false,
};
pub const FULL: PresentCaps = PresentCaps {
color: ColorDepth::TrueColor,
sync_output_2026: true,
hyperlinks: true,
undercurl: true,
underline_color: true,
};
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ColorDepth {
TrueColor,
Xterm256,
Ansi16,
}
pub struct Presenter {
opts: PresenterOpts,
cursor: Option<(i32, i32)>,
pen: Option<Pen>,
open_link: String,
seq_inc: Vec<u8>,
seq_reset: Vec<u8>,
}
impl Default for Presenter {
fn default() -> Self {
Presenter::new()
}
}
impl Presenter {
pub fn new() -> Presenter {
Presenter::with_opts(PresenterOpts::default())
}
pub fn with_opts(opts: PresenterOpts) -> Presenter {
Presenter {
opts,
cursor: None,
pen: None,
open_link: String::new(),
seq_inc: Vec::new(),
seq_reset: Vec::new(),
}
}
pub fn opts(&self) -> PresenterOpts {
self.opts
}
pub fn invalidate(&mut self) {
self.cursor = None;
self.pen = None;
self.open_link.clear();
}
pub fn external_write(&mut self, out: &mut Vec<u8>, bytes: &[u8], at: crate::base::Point) {
if !self.open_link.is_empty() {
out.extend_from_slice(b"\x1b]8;;\x1b\\");
self.open_link.clear();
}
out.extend_from_slice(b"\x1b[0m");
cup(out, at.x, at.y);
out.extend_from_slice(bytes);
self.invalidate();
}
pub fn emit(&mut self, runs: &[Run], next: &Surface, caps: &PresentCaps, out: &mut Vec<u8>) {
self.emit_scrolled(ScrolledRuns::plain(runs), next, caps, out);
}
pub fn emit_scrolled(
&mut self,
frame: ScrolledRuns<'_>,
next: &Surface,
caps: &PresentCaps,
out: &mut Vec<u8>,
) {
let (shift, runs) = (frame.shift(), frame.runs());
if frame.is_empty() {
return;
}
if caps.sync_output_2026 {
out.extend_from_slice(b"\x1b[?2026h");
}
if let Some(shift) = shift {
self.emit_shift(&shift, caps, out);
}
for run in runs {
self.emit_run(run, next, caps, out);
}
self.close_link(caps, out);
out.extend_from_slice(b"\x1b[0m");
self.pen = Some(Pen::DEFAULT);
self.park_cursor(next, out);
if caps.sync_output_2026 {
out.extend_from_slice(b"\x1b[?2026l");
}
}
fn emit_shift(&mut self, shift: &Shift, caps: &PresentCaps, out: &mut Vec<u8>) {
self.close_link(caps, out);
out.extend_from_slice(b"\x1b[0m");
self.pen = Some(Pen::DEFAULT);
out.extend_from_slice(b"\x1b[");
push_u32(out, (shift.top + 1) as u32);
out.push(b';');
push_u32(out, shift.bottom as u32);
out.push(b'r');
csi_n(out, shift.n as u32, if shift.up { b'S' } else { b'T' });
out.extend_from_slice(b"\x1b[r");
self.cursor = Some((0, 0));
}
fn emit_run(&mut self, run: &Run, next: &Surface, caps: &PresentCaps, out: &mut Vec<u8>) {
let y = run.y;
let mut x = run.x;
let end = run.end();
while x < end && next.get(x, y).is_some_and(Cell::is_continuation) {
x += 1;
}
if x >= end {
return;
}
self.move_cursor(x, y, out);
while x < end {
let Some(&cell) = next.get(x, y) else {
break;
};
if cell.is_continuation() {
x += 1;
continue;
}
if self.cursor.is_none() {
self.move_cursor(x, y, out);
}
self.set_link(cell.link, next, caps, out);
self.set_pen(&cell, caps, out);
let width = cell.glyph.width().max(1);
if cell.is_wide_leader() && x + 1 >= next.width() {
out.push(b' ');
self.advance_cursor(1, next.width());
x += 1;
continue;
}
let s = next.glyph_str(&cell);
if s.is_empty() {
out.push(b' '); self.advance_cursor(width, next.width());
} else {
out.extend_from_slice(s.as_bytes());
if crate::text::is_risky_cluster(s) {
self.cursor = None;
} else {
self.advance_cursor(width, next.width());
}
}
x += width;
}
}
fn advance_cursor(&mut self, width: i32, surface_width: i32) {
if let Some((cx, cy)) = self.cursor {
let nx = cx + width;
self.cursor = if nx >= surface_width {
None
} else {
Some((nx, cy))
};
}
}
fn move_cursor(&mut self, x: i32, y: i32, out: &mut Vec<u8>) {
match self.cursor {
Some((cx, cy)) if cx == x && cy == y => {}
Some((cx, cy)) if cy == y => {
if x == 0 {
out.push(b'\r');
} else if x > cx {
csi_n(out, (x - cx) as u32, b'C');
} else {
csi_n(out, (cx - x) as u32, b'D');
}
}
Some((cx, cy)) if cx == x => {
if y > cy {
csi_n(out, (y - cy) as u32, b'B');
} else {
csi_n(out, (cy - y) as u32, b'A');
}
}
_ => cup(out, x, y),
}
self.cursor = Some((x, y));
}
fn park_cursor(&mut self, next: &Surface, out: &mut Vec<u8>) {
let y = (next.height() - 1).max(0);
self.move_cursor(0, y, out);
}
fn set_link(&mut self, link: u16, next: &Surface, caps: &PresentCaps, out: &mut Vec<u8>) {
if !caps.hyperlinks {
return;
}
let uri = next.link_uri(link).unwrap_or("");
if uri == self.open_link {
return;
}
if !self.open_link.is_empty() {
out.extend_from_slice(b"\x1b]8;;\x1b\\");
self.open_link.clear();
}
if !uri.is_empty() {
out.extend_from_slice(b"\x1b]8;id=");
push_u32(out, link as u32);
out.push(b';');
out.extend_from_slice(uri.as_bytes());
out.extend_from_slice(b"\x1b\\");
self.open_link.push_str(uri);
}
}
fn close_link(&mut self, caps: &PresentCaps, out: &mut Vec<u8>) {
if caps.hyperlinks && !self.open_link.is_empty() {
out.extend_from_slice(b"\x1b]8;;\x1b\\");
self.open_link.clear();
}
}
fn set_pen(&mut self, cell: &Cell, caps: &PresentCaps, out: &mut Vec<u8>) {
let target = resolve_pen(cell, caps);
match self.pen {
Some(cur) if cur == target => return,
Some(cur) => {
build_incremental(&cur, &target, &mut self.seq_inc);
build_reset(&target, &mut self.seq_reset);
let params: &[u8] = if self.seq_reset.len() < self.seq_inc.len() {
&self.seq_reset
} else {
&self.seq_inc
};
out.extend_from_slice(b"\x1b[");
out.extend_from_slice(params);
out.push(b'm');
}
None => {
build_reset(&target, &mut self.seq_reset);
out.extend_from_slice(b"\x1b[");
out.extend_from_slice(&self.seq_reset);
out.push(b'm');
}
}
self.pen = Some(target);
}
}
#[cfg(test)]
#[path = "present_tests.rs"]
mod tests;