use crate::core::buffer::{Buffer, Cell, CellStyle};
use crate::core::rect::Rect;
use crate::interaction::{
HitRegion, InteractionLayer, LogicalHit, PointerEvent, ScrollRowHit, SelectableSpan,
UiEventBatch, UiEventMapper, WidgetMetadata,
};
use crate::sanitize::char_display_width;
use crate::widgets::Widget;
use crossterm::{
cursor,
event::{
self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
Event,
},
execute, terminal,
};
use std::borrow::Cow;
use std::fmt;
use std::io::{self, Stdout, Write};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FrameTiming {
pub elapsed: Duration,
pub bytes_written: usize,
pub dirty_cells: usize,
pub areas: usize,
pub full_repaint: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NamedFrameTiming {
pub name: &'static str,
pub area: Option<Rect>,
pub timing: FrameTiming,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FrameDiagnostic {
pub name: Cow<'static, str>,
pub area: Option<Rect>,
pub elapsed: Duration,
}
impl FrameDiagnostic {
pub fn static_name(name: &'static str, area: Option<Rect>, elapsed: Duration) -> Self {
Self {
name: Cow::Borrowed(name),
area,
elapsed,
}
}
pub fn owned_name(name: impl Into<String>, area: Option<Rect>, elapsed: Duration) -> Self {
Self {
name: Cow::Owned(name.into()),
area,
elapsed,
}
}
pub fn name(&self) -> &str {
self.name.as_ref()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PresentStats {
pub bytes_written: usize,
pub dirty_cells: usize,
pub areas: usize,
pub full_repaint: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DiffText<'a> {
cells: &'a [Cell],
skip_cells: &'a [bool],
}
impl<'a> DiffText<'a> {
pub fn chars(self) -> DiffTextChars<'a> {
DiffTextChars {
cells: self.cells.iter(),
skip_cells: self.skip_cells.iter(),
}
}
pub fn cell_len(self) -> usize {
self.cells.len()
}
pub fn visible_cells(self) -> usize {
self.skip_cells.iter().filter(|skip| !**skip).count()
}
pub fn width(self) -> usize {
self.chars().map(|ch| char_display_width(ch).max(1)).sum()
}
pub fn is_empty(self) -> bool {
self.visible_cells() == 0
}
}
impl fmt::Display for DiffText<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for ch in self.chars() {
let mut buffer = [0; 4];
f.write_str(ch.encode_utf8(&mut buffer))?;
}
Ok(())
}
}
impl<'a> IntoIterator for DiffText<'a> {
type Item = char;
type IntoIter = DiffTextChars<'a>;
fn into_iter(self) -> Self::IntoIter {
self.chars()
}
}
#[derive(Debug, Clone)]
pub struct DiffTextChars<'a> {
cells: std::slice::Iter<'a, Cell>,
skip_cells: std::slice::Iter<'a, bool>,
}
impl Iterator for DiffTextChars<'_> {
type Item = char;
fn next(&mut self) -> Option<Self::Item> {
loop {
let cell = self.cells.next()?;
let skip = *self.skip_cells.next()?;
if !skip {
return Some(cell.ch);
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DiffRun<'a> {
pub x: u16,
pub y: u16,
pub style: CellStyle,
pub text: DiffText<'a>,
}
#[derive(Debug, Clone)]
pub struct DiffRuns<'a> {
front: &'a Buffer,
back: &'a Buffer,
width: usize,
height: usize,
areas: Option<&'a [Rect]>,
x: usize,
y: usize,
}
impl<'a> DiffRuns<'a> {
pub fn new(front: &'a Buffer, back: &'a Buffer, width: u16, height: u16) -> Self {
Self::with_areas(front, back, width, height, None)
}
pub fn with_areas(
front: &'a Buffer,
back: &'a Buffer,
width: u16,
height: u16,
areas: Option<&'a [Rect]>,
) -> Self {
Self {
front,
back,
width: (width as usize).min(front.width()).min(back.width()),
height: (height as usize).min(front.height()).min(back.height()),
areas,
x: 0,
y: 0,
}
}
fn contains(&self, x: usize, y: usize) -> bool {
self.areas
.map(|areas| areas.iter().any(|area| rect_contains(*area, x, y)))
.unwrap_or(true)
}
fn cell_changed(&self, x: usize, y: usize) -> bool {
let Some(back_cell) = self.back.get(x, y) else {
return false;
};
self.front.get(x, y) != Some(back_cell)
|| self.front.is_skip(x, y) != self.back.is_skip(x, y)
}
}
impl<'a> Iterator for DiffRuns<'a> {
type Item = DiffRun<'a>;
fn next(&mut self) -> Option<Self::Item> {
while self.y < self.height {
while self.x < self.width {
let start_x = self.x;
let y = self.y;
self.x += 1;
if !self.contains(start_x, y)
|| self.back.is_skip(start_x, y)
|| !self.cell_changed(start_x, y)
{
continue;
}
let back_cell = self.back.get(start_x, y)?;
let style = back_cell.style();
let mut end_x = start_x + char_display_width(back_cell.ch).max(1);
while end_x < self.width {
if !self.contains(end_x, y)
|| self.back.is_skip(end_x, y)
|| !self.cell_changed(end_x, y)
{
break;
}
let Some(cell) = self.back.get(end_x, y) else {
break;
};
if cell.style() != style {
break;
}
end_x += char_display_width(cell.ch).max(1);
}
self.x = end_x;
let row_start = y * self.back.width();
let start_idx = row_start + start_x;
let end_idx = row_start + end_x.min(self.back.width());
return Some(DiffRun {
x: start_x as u16,
y: y as u16,
style,
text: DiffText {
cells: &self.back.cells()[start_idx..end_idx],
skip_cells: &self.back.skip_cells()[start_idx..end_idx],
},
});
}
self.x = 0;
self.y += 1;
}
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PresentStrategy {
Diff,
Full,
Areas(Vec<Rect>),
DirtyBounds,
MarkedDirty,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerminalOptions {
pub mouse_capture: bool,
pub bracketed_paste: bool,
pub alternate_screen: bool,
pub hide_cursor: bool,
}
impl TerminalOptions {
pub const fn new() -> Self {
Self {
mouse_capture: false,
bracketed_paste: false,
alternate_screen: true,
hide_cursor: true,
}
}
}
impl Default for TerminalOptions {
fn default() -> Self {
Self::new()
}
}
pub struct Frame<'a> {
area: Rect,
buffer: &'a mut Buffer,
diagnostics: &'a mut Vec<FrameDiagnostic>,
interaction: &'a mut InteractionLayer,
dirty_regions: &'a mut Vec<Rect>,
frame_count: u64,
cursor_position: Option<(u16, u16)>,
}
impl<'a> Frame<'a> {
pub fn area(&self) -> Rect {
self.area
}
pub fn buffer(&mut self) -> &mut Buffer {
self.buffer
}
pub fn frame_count(&self) -> u64 {
self.frame_count
}
pub fn set_cursor_position(&mut self, x: u16, y: u16) {
self.cursor_position = Some((x, y));
}
pub fn cursor_position(&self) -> Option<(u16, u16)> {
self.cursor_position
}
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
where
W: Widget,
{
widget.render(&mut *self.buffer, area);
}
pub fn render_widget_timed<N, W>(&mut self, name: N, widget: W, area: Rect)
where
N: Into<Cow<'static, str>>,
W: Widget,
{
let start = Instant::now();
widget.render(&mut *self.buffer, area);
self.diagnostics.push(FrameDiagnostic {
name: name.into(),
area: Some(area),
elapsed: start.elapsed(),
});
}
pub fn time_named<N, R>(&mut self, name: N, f: impl FnOnce(&mut Self) -> R) -> R
where
N: Into<Cow<'static, str>>,
{
let start = Instant::now();
let result = f(self);
self.diagnostics.push(FrameDiagnostic {
name: name.into(),
area: None,
elapsed: start.elapsed(),
});
result
}
pub fn time_pane<R>(
&mut self,
name: impl Into<Cow<'static, str>>,
area: Rect,
f: impl FnOnce(&mut Self) -> R,
) -> R {
let start = Instant::now();
let result = f(self);
self.diagnostics.push(FrameDiagnostic {
name: name.into(),
area: Some(area),
elapsed: start.elapsed(),
});
result
}
pub fn diagnostics(&self) -> &[FrameDiagnostic] {
self.diagnostics
}
pub fn register_hit_region(&mut self, region: HitRegion) -> &HitRegion {
self.interaction.push_region(region)
}
pub fn register_metadata_region(&mut self, area: Rect, metadata: WidgetMetadata) -> &HitRegion {
self.interaction.push_metadata_region(area, metadata)
}
pub fn register_selectable_span(&mut self, span: SelectableSpan) -> &SelectableSpan {
self.interaction.push_selectable_span(span)
}
pub fn register_scroll_region(
&mut self,
id: impl Into<crate::interaction::WidgetId>,
viewport: Rect,
scroll_offset: usize,
rows: Vec<ScrollRowHit>,
) {
self.interaction
.push_scroll_region(id, viewport, scroll_offset, rows);
}
pub fn interaction_layer(&self) -> &InteractionLayer {
self.interaction
}
pub fn interaction_layer_mut(&mut self) -> &mut InteractionLayer {
self.interaction
}
pub fn mark_dirty(&mut self, area: Rect) {
if !area.is_empty() {
self.dirty_regions.push(area);
}
}
pub fn dirty_regions(&self) -> &[Rect] {
self.dirty_regions
}
}
pub struct Terminal {
stdout: Stdout,
width: u16,
height: u16,
front_buffer: Buffer,
back_buffer: Buffer,
hidden_cursor: bool,
mouse_capture: bool,
bracketed_paste: bool,
alternate_screen: bool,
restored: bool,
frame_count: u64,
cursor_position: Option<(u16, u16)>,
last_frame_timing: Option<FrameTiming>,
last_frame_diagnostics: Vec<FrameDiagnostic>,
interaction_layer: InteractionLayer,
last_dirty_regions: Vec<Rect>,
ui_event_mapper: UiEventMapper,
frame_timing_hook: Option<Box<dyn FnMut(FrameTiming) + Send + 'static>>,
}
impl fmt::Debug for Terminal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Terminal")
.field("width", &self.width)
.field("height", &self.height)
.field("hidden_cursor", &self.hidden_cursor)
.field("mouse_capture", &self.mouse_capture)
.field("bracketed_paste", &self.bracketed_paste)
.field("alternate_screen", &self.alternate_screen)
.field("restored", &self.restored)
.field("frame_count", &self.frame_count)
.field("cursor_position", &self.cursor_position)
.field("last_frame_timing", &self.last_frame_timing)
.field("last_frame_diagnostics", &self.last_frame_diagnostics)
.field("interaction_regions", &self.interaction_layer.regions.len())
.field("last_dirty_regions", &self.last_dirty_regions)
.finish()
}
}
impl Terminal {
pub fn init() -> io::Result<Self> {
Self::init_with(TerminalOptions::default())
}
pub fn init_with(options: TerminalOptions) -> io::Result<Self> {
let mut stdout = io::stdout();
terminal::enable_raw_mode()?;
if options.alternate_screen {
execute!(stdout, terminal::EnterAlternateScreen)?;
}
if options.hide_cursor {
execute!(stdout, cursor::Hide)?;
}
if options.mouse_capture {
execute!(stdout, EnableMouseCapture)?;
}
if options.bracketed_paste {
execute!(stdout, EnableBracketedPaste)?;
}
let (w, h) = terminal::size()?;
let front = Buffer::new(w as usize, h as usize);
let back = Buffer::new(w as usize, h as usize);
Ok(Self {
stdout,
width: w,
height: h,
front_buffer: front,
back_buffer: back,
hidden_cursor: options.hide_cursor,
mouse_capture: options.mouse_capture,
bracketed_paste: options.bracketed_paste,
alternate_screen: options.alternate_screen,
restored: false,
frame_count: 0,
cursor_position: None,
last_frame_timing: None,
last_frame_diagnostics: Vec::new(),
interaction_layer: InteractionLayer::new(),
last_dirty_regions: Vec::new(),
ui_event_mapper: UiEventMapper::new(),
frame_timing_hook: None,
})
}
pub fn restore(&mut self) -> io::Result<()> {
if self.restored {
return Ok(());
}
if self.bracketed_paste {
execute!(self.stdout, DisableBracketedPaste)?;
self.bracketed_paste = false;
}
if self.mouse_capture {
execute!(self.stdout, DisableMouseCapture)?;
self.mouse_capture = false;
}
if self.hidden_cursor {
execute!(self.stdout, cursor::Show)?;
self.hidden_cursor = false;
}
if self.alternate_screen {
execute!(self.stdout, terminal::LeaveAlternateScreen)?;
self.alternate_screen = false;
}
terminal::disable_raw_mode()?;
self.restored = true;
Ok(())
}
pub fn size(&self) -> Rect {
Rect::new(0, 0, self.width, self.height)
}
pub fn size_cached(&self) -> Rect {
self.size()
}
pub fn width(&self) -> usize {
self.width as usize
}
pub fn height(&self) -> usize {
self.height as usize
}
pub fn back_buffer(&mut self) -> &mut Buffer {
&mut self.back_buffer
}
pub fn front_buffer(&self) -> &Buffer {
&self.front_buffer
}
pub fn dirty_bounds(&self) -> Option<Rect> {
self.front_buffer.changed_bounds(&self.back_buffer)
}
pub fn last_frame_timing(&self) -> Option<FrameTiming> {
self.last_frame_timing
}
pub fn last_frame_diagnostics(&self) -> &[FrameDiagnostic] {
&self.last_frame_diagnostics
}
pub fn interaction_layer(&self) -> &InteractionLayer {
&self.interaction_layer
}
pub fn hit_test(&self, x: u16, y: u16) -> Option<&HitRegion> {
self.interaction_layer.hit_test(x, y)
}
pub fn scroll_hit_test(&self, x: u16, y: u16) -> Option<LogicalHit> {
self.interaction_layer.scroll_hit_test(x, y)
}
pub fn selectable_at(
&self,
x: u16,
y: u16,
) -> Option<(&SelectableSpan, crate::interaction::SelectionPoint)> {
self.interaction_layer.selectable_at(x, y)
}
pub fn last_dirty_regions(&self) -> &[Rect] {
&self.last_dirty_regions
}
pub fn handle_pointer_event(&mut self, event: PointerEvent) -> UiEventBatch {
self.ui_event_mapper
.handle_pointer_event(&self.interaction_layer, event)
}
pub fn set_frame_timing_hook<F>(&mut self, hook: F)
where
F: FnMut(FrameTiming) + Send + 'static,
{
self.frame_timing_hook = Some(Box::new(hook));
}
pub fn clear_frame_timing_hook(&mut self) {
self.frame_timing_hook = None;
}
fn record_frame_timing(&mut self, timing: FrameTiming) {
self.last_frame_timing = Some(timing);
if let Some(hook) = self.frame_timing_hook.as_mut() {
hook(timing);
}
}
pub fn draw<F>(&mut self, f: F) -> io::Result<()>
where
F: FnOnce(&mut Frame),
{
self.draw_timed(f).map(|_| ())
}
pub fn draw_timed<F>(&mut self, f: F) -> io::Result<FrameTiming>
where
F: FnOnce(&mut Frame),
{
self.draw_with_present_strategy(PresentStrategy::Diff, f)
}
pub fn draw_areas_timed<F>(&mut self, areas: &[Rect], f: F) -> io::Result<FrameTiming>
where
F: FnOnce(&mut Frame),
{
self.draw_with_present_strategy(PresentStrategy::Areas(areas.to_vec()), f)
}
pub fn draw_area_preserve_cursor_timed<F>(
&mut self,
area: Rect,
f: F,
) -> io::Result<FrameTiming>
where
F: FnOnce(&mut Frame),
{
let start = Instant::now();
let previous_cursor = self.cursor_position;
self.size_changed()?;
self.clear_area(area);
let mut diagnostics = Vec::new();
let mut interaction = InteractionLayer::new();
let mut dirty_regions = Vec::new();
{
let mut frame = Frame {
area,
buffer: &mut self.back_buffer,
diagnostics: &mut diagnostics,
interaction: &mut interaction,
dirty_regions: &mut dirty_regions,
frame_count: self.frame_count,
cursor_position: previous_cursor,
};
f(&mut frame);
self.cursor_position = frame.cursor_position.or(previous_cursor);
}
self.last_frame_diagnostics = diagnostics;
self.interaction_layer = interaction;
self.last_dirty_regions = dirty_regions;
let areas = [area];
let output = diff_output(
&self.front_buffer,
&self.back_buffer,
self.width,
self.height,
Some(&areas),
false,
);
let mut timing = self.write_present_output(output, start, Some(&areas))?;
self.back_buffer = self.front_buffer.clone();
if let Some((x, y)) = self.cursor_position {
self.set_cursor(x, y)?;
}
timing.elapsed = start.elapsed();
self.frame_count = self.frame_count.wrapping_add(1);
self.record_frame_timing(timing);
Ok(timing)
}
pub fn draw_area_preserve_cursor_named_timed<F>(
&mut self,
name: &'static str,
area: Rect,
f: F,
) -> io::Result<NamedFrameTiming>
where
F: FnOnce(&mut Frame),
{
let timing = self.draw_area_preserve_cursor_timed(area, f)?;
Ok(NamedFrameTiming {
name,
area: Some(area),
timing,
})
}
pub fn draw_with_present_strategy<F>(
&mut self,
strategy: PresentStrategy,
f: F,
) -> io::Result<FrameTiming>
where
F: FnOnce(&mut Frame),
{
let start = Instant::now();
self.size_changed()?;
let area_limited_draw = matches!(
&strategy,
PresentStrategy::Areas(_) | PresentStrategy::MarkedDirty
);
match &strategy {
PresentStrategy::Areas(areas) => {
for area in areas {
self.clear_area(*area);
}
}
PresentStrategy::MarkedDirty => {}
_ => self.clear(),
}
let mut diagnostics = Vec::new();
let mut interaction = InteractionLayer::new();
let mut dirty_regions = Vec::new();
{
let mut frame = Frame {
area: self.size(),
buffer: &mut self.back_buffer,
diagnostics: &mut diagnostics,
interaction: &mut interaction,
dirty_regions: &mut dirty_regions,
frame_count: self.frame_count,
cursor_position: None,
};
f(&mut frame);
self.cursor_position = frame.cursor_position;
}
self.last_frame_diagnostics = diagnostics;
let frame_dirty_regions = dirty_regions.clone();
self.interaction_layer = interaction;
self.last_dirty_regions = dirty_regions;
let mut copied_areas: Option<Vec<Rect>> = None;
let output = match strategy {
PresentStrategy::Diff => diff_output(
&self.front_buffer,
&self.back_buffer,
self.width,
self.height,
None,
false,
),
PresentStrategy::Full => PresentOutput {
ansi: self.back_buffer.to_ansi_string(),
dirty_cells: self.width as usize * self.height as usize,
areas: 1,
full_repaint: true,
},
PresentStrategy::Areas(areas) => {
copied_areas = Some(areas);
diff_output(
&self.front_buffer,
&self.back_buffer,
self.width,
self.height,
copied_areas.as_deref(),
false,
)
}
PresentStrategy::DirtyBounds => {
let areas = self.dirty_bounds().into_iter().collect::<Vec<_>>();
copied_areas = Some(areas);
diff_output(
&self.front_buffer,
&self.back_buffer,
self.width,
self.height,
copied_areas.as_deref(),
false,
)
}
PresentStrategy::MarkedDirty => {
copied_areas = Some(frame_dirty_regions);
diff_output(
&self.front_buffer,
&self.back_buffer,
self.width,
self.height,
copied_areas.as_deref(),
false,
)
}
};
let mut timing = self.write_present_output(output, start, copied_areas.as_deref())?;
if area_limited_draw {
self.back_buffer = self.front_buffer.clone();
}
if let Some((x, y)) = self.cursor_position {
self.set_cursor(x, y)?;
}
timing.elapsed = start.elapsed();
self.frame_count = self.frame_count.wrapping_add(1);
self.record_frame_timing(timing);
Ok(timing)
}
pub fn present(&mut self) -> io::Result<()> {
self.present_timed().map(|_| ())
}
pub fn present_timed(&mut self) -> io::Result<FrameTiming> {
let start = Instant::now();
let output = diff_output(
&self.front_buffer,
&self.back_buffer,
self.width,
self.height,
None,
false,
);
let timing = self.write_present_output(output, start, None)?;
self.record_frame_timing(timing);
Ok(timing)
}
pub fn present_direct(&mut self) -> io::Result<()> {
self.present_direct_timed().map(|_| ())
}
pub fn present_direct_timed(&mut self) -> io::Result<FrameTiming> {
let start = Instant::now();
let stats = write_diff_to(
&mut self.stdout,
&self.front_buffer,
&self.back_buffer,
self.width,
self.height,
None,
)?;
self.stdout.flush()?;
self.front_buffer = self.back_buffer.clone();
let timing = frame_timing_from_stats(start, stats);
self.record_frame_timing(timing);
Ok(timing)
}
pub fn present_full(&mut self) -> io::Result<()> {
self.present_full_timed().map(|_| ())
}
pub fn present_full_timed(&mut self) -> io::Result<FrameTiming> {
let start = Instant::now();
let output = PresentOutput {
ansi: self.back_buffer.to_ansi_string(),
dirty_cells: self.width as usize * self.height as usize,
areas: 1,
full_repaint: true,
};
let timing = self.write_present_output(output, start, None)?;
self.record_frame_timing(timing);
Ok(timing)
}
pub fn present_area(&mut self, area: Rect) -> io::Result<()> {
self.present_areas(&[area])
}
pub fn present_area_direct(&mut self, area: Rect) -> io::Result<()> {
self.present_areas_direct(&[area])
}
pub fn present_area_timed(&mut self, area: Rect) -> io::Result<FrameTiming> {
self.present_areas_timed(&[area])
}
pub fn present_area_direct_timed(&mut self, area: Rect) -> io::Result<FrameTiming> {
self.present_areas_direct_timed(&[area])
}
pub fn present_area_preserve_cursor_timed(&mut self, area: Rect) -> io::Result<FrameTiming> {
let start = Instant::now();
let output = diff_output(
&self.front_buffer,
&self.back_buffer,
self.width,
self.height,
Some(&[area]),
false,
);
let mut timing = self.write_present_output(output, start, Some(&[area]))?;
if let Some((x, y)) = self.cursor_position {
self.set_cursor(x, y)?;
}
timing.elapsed = start.elapsed();
self.record_frame_timing(timing);
Ok(timing)
}
pub fn present_area_named_timed(
&mut self,
name: &'static str,
area: Rect,
) -> io::Result<NamedFrameTiming> {
let timing = self.present_area_timed(area)?;
Ok(NamedFrameTiming {
name,
area: Some(area),
timing,
})
}
pub fn present_areas(&mut self, areas: &[Rect]) -> io::Result<()> {
self.present_areas_timed(areas).map(|_| ())
}
pub fn present_areas_direct(&mut self, areas: &[Rect]) -> io::Result<()> {
self.present_areas_direct_timed(areas).map(|_| ())
}
pub fn present_areas_timed(&mut self, areas: &[Rect]) -> io::Result<FrameTiming> {
let start = Instant::now();
let output = diff_output(
&self.front_buffer,
&self.back_buffer,
self.width,
self.height,
Some(areas),
false,
);
let timing = self.write_present_output(output, start, Some(areas))?;
self.record_frame_timing(timing);
Ok(timing)
}
pub fn present_areas_direct_timed(&mut self, areas: &[Rect]) -> io::Result<FrameTiming> {
let start = Instant::now();
let stats = write_diff_to(
&mut self.stdout,
&self.front_buffer,
&self.back_buffer,
self.width,
self.height,
Some(areas),
)?;
self.stdout.flush()?;
for area in areas {
self.front_buffer.copy_area_from(&self.back_buffer, *area);
}
let timing = frame_timing_from_stats(start, stats);
self.record_frame_timing(timing);
Ok(timing)
}
pub fn present_buffer(&mut self, buffer: &Buffer) -> io::Result<()> {
self.present_buffer_timed(buffer).map(|_| ())
}
pub fn present_buffer_timed(&mut self, buffer: &Buffer) -> io::Result<FrameTiming> {
self.present_buffer_inner_timed(buffer, None)
}
pub fn present_buffer_areas(&mut self, buffer: &Buffer, areas: &[Rect]) -> io::Result<()> {
self.present_buffer_areas_timed(buffer, areas).map(|_| ())
}
pub fn present_buffer_areas_timed(
&mut self,
buffer: &Buffer,
areas: &[Rect],
) -> io::Result<FrameTiming> {
self.present_buffer_inner_timed(buffer, Some(areas))
}
pub fn draw_full<F>(&mut self, f: F) -> io::Result<()>
where
F: FnOnce(&mut Frame),
{
self.draw_full_timed(f).map(|_| ())
}
pub fn draw_full_timed<F>(&mut self, f: F) -> io::Result<FrameTiming>
where
F: FnOnce(&mut Frame),
{
self.draw_with_present_strategy(PresentStrategy::Full, f)
}
fn write_present_output(
&mut self,
output: PresentOutput,
start: Instant,
copied_areas: Option<&[Rect]>,
) -> io::Result<FrameTiming> {
let bytes_written = output.ansi.len();
write!(self.stdout, "{}", output.ansi)?;
self.stdout.flush()?;
if let Some(areas) = copied_areas {
for area in areas {
self.front_buffer.copy_area_from(&self.back_buffer, *area);
}
} else {
self.front_buffer = self.back_buffer.clone();
}
Ok(FrameTiming {
elapsed: start.elapsed(),
bytes_written,
dirty_cells: output.dirty_cells,
areas: output.areas,
full_repaint: output.full_repaint,
})
}
fn present_buffer_inner_timed(
&mut self,
buffer: &Buffer,
areas: Option<&[Rect]>,
) -> io::Result<FrameTiming> {
let start = Instant::now();
self.size_changed()?;
self.ensure_present_buffer_size(buffer)?;
let stats = write_diff_to(
&mut self.stdout,
&self.front_buffer,
buffer,
self.width,
self.height,
areas,
)?;
self.stdout.flush()?;
if let Some(areas) = areas {
for area in areas {
self.front_buffer.copy_area_from(buffer, *area);
}
self.back_buffer = self.front_buffer.clone();
} else {
self.front_buffer = buffer.clone();
self.back_buffer = buffer.clone();
}
let timing = frame_timing_from_stats(start, stats);
self.record_frame_timing(timing);
Ok(timing)
}
fn ensure_present_buffer_size(&self, buffer: &Buffer) -> io::Result<()> {
if buffer.width() == self.width as usize && buffer.height() == self.height as usize {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"presented buffer size must match the terminal size",
))
}
}
pub fn clear(&mut self) {
self.back_buffer = Buffer::new(self.width as usize, self.height as usize);
}
pub fn clear_area(&mut self, area: Rect) {
self.back_buffer.clear(area);
}
pub fn hide_cursor(&mut self) -> io::Result<()> {
execute!(self.stdout, cursor::Hide)?;
self.hidden_cursor = true;
Ok(())
}
pub fn show_cursor(&mut self) -> io::Result<()> {
execute!(self.stdout, cursor::Show)?;
self.hidden_cursor = false;
Ok(())
}
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
execute!(self.stdout, cursor::MoveTo(x, y))?;
Ok(())
}
pub fn set_mouse_capture(&mut self, enabled: bool) -> io::Result<()> {
if enabled == self.mouse_capture {
return Ok(());
}
if enabled {
execute!(self.stdout, EnableMouseCapture)?;
} else {
execute!(self.stdout, DisableMouseCapture)?;
}
self.mouse_capture = enabled;
Ok(())
}
pub fn set_bracketed_paste(&mut self, enabled: bool) -> io::Result<()> {
if enabled == self.bracketed_paste {
return Ok(());
}
if enabled {
execute!(self.stdout, EnableBracketedPaste)?;
} else {
execute!(self.stdout, DisableBracketedPaste)?;
}
self.bracketed_paste = enabled;
Ok(())
}
pub fn poll_event(&self) -> io::Result<Option<Event>> {
if event::poll(std::time::Duration::from_millis(0))? {
Ok(Some(event::read()?))
} else {
Ok(None)
}
}
pub fn wait_event(&self) -> io::Result<Event> {
event::read()
}
pub fn flush(&mut self) -> io::Result<()> {
self.stdout.flush()
}
pub fn size_changed(&mut self) -> io::Result<bool> {
let (w, h) = terminal::size()?;
if w != self.width || h != self.height {
self.width = w;
self.height = h;
self.front_buffer.resize(w as usize, h as usize);
self.back_buffer.resize(w as usize, h as usize);
Ok(true)
} else {
Ok(false)
}
}
pub fn raw_output(&self) -> &Stdout {
&self.stdout
}
}
impl Drop for Terminal {
fn drop(&mut self) {
let _ = self.restore();
}
}
pub fn terminal_size() -> (u16, u16) {
terminal::size().unwrap_or((80, 24))
}
pub fn diff_runs<'a>(front: &'a Buffer, back: &'a Buffer, width: u16, height: u16) -> DiffRuns<'a> {
DiffRuns::new(front, back, width, height)
}
pub fn diff_runs_for_areas<'a>(
front: &'a Buffer,
back: &'a Buffer,
width: u16,
height: u16,
areas: &'a [Rect],
) -> DiffRuns<'a> {
DiffRuns::with_areas(front, back, width, height, Some(areas))
}
pub fn write_diff_to<W: Write>(
writer: &mut W,
front: &Buffer,
back: &Buffer,
width: u16,
height: u16,
areas: Option<&[Rect]>,
) -> io::Result<PresentStats> {
let mut writer = CountingWriter::new(writer);
let mut last_style = CellStyle::default();
let mut cursor_col: Option<usize> = None;
let mut cursor_row: Option<usize> = None;
let mut dirty_cells = 0usize;
for run in DiffRuns::with_areas(front, back, width, height, areas) {
let x = run.x as usize;
let y = run.y as usize;
if cursor_row != Some(y) || cursor_col != Some(x) {
write!(writer, "\x1b[{};{}H", y + 1, x + 1)?;
}
write_style_changes(&mut writer, &mut last_style, run.style)?;
write!(writer, "{}", run.text)?;
dirty_cells += run.text.visible_cells();
cursor_row = Some(y);
cursor_col = Some(x + run.text.width());
}
writer.write_all(b"\x1b[0m")?;
Ok(PresentStats {
bytes_written: writer.bytes_written(),
dirty_cells,
areas: areas.map(|areas| areas.len()).unwrap_or(1),
full_repaint: false,
})
}
pub fn write_full_to<W: Write>(
writer: &mut W,
buffer: &Buffer,
width: u16,
height: u16,
) -> io::Result<PresentStats> {
let mut writer = CountingWriter::new(writer);
let mut last_style = CellStyle::default();
let width = (width as usize).min(buffer.width());
let height = (height as usize).min(buffer.height());
for y in 0..height {
write!(writer, "\x1b[{};1H", y + 1)?;
for x in 0..width {
if buffer.is_skip(x, y) {
continue;
}
let Some(cell) = buffer.get(x, y) else {
continue;
};
write_style_changes(&mut writer, &mut last_style, cell.style())?;
write!(writer, "{}", cell.ch)?;
}
}
writer.write_all(b"\x1b[0m")?;
Ok(PresentStats {
bytes_written: writer.bytes_written(),
dirty_cells: width * height,
areas: 1,
full_repaint: true,
})
}
struct PresentOutput {
ansi: String,
dirty_cells: usize,
areas: usize,
full_repaint: bool,
}
#[cfg(test)]
fn diff_ansi_string(front: &Buffer, back: &Buffer, width: u16, height: u16) -> String {
diff_output(front, back, width, height, None, false).ansi
}
fn diff_output(
front: &Buffer,
back: &Buffer,
width: u16,
height: u16,
areas: Option<&[Rect]>,
full_repaint: bool,
) -> PresentOutput {
let mut output = Vec::with_capacity(width as usize * height as usize * 8);
let stats = write_diff_to(&mut output, front, back, width, height, areas)
.expect("writing diff to memory cannot fail");
PresentOutput {
ansi: String::from_utf8(output).expect("diff output is valid UTF-8"),
dirty_cells: stats.dirty_cells,
areas: stats.areas,
full_repaint,
}
}
fn frame_timing_from_stats(start: Instant, stats: PresentStats) -> FrameTiming {
FrameTiming {
elapsed: start.elapsed(),
bytes_written: stats.bytes_written,
dirty_cells: stats.dirty_cells,
areas: stats.areas,
full_repaint: stats.full_repaint,
}
}
fn write_style_changes<W: Write>(
writer: &mut W,
last_style: &mut CellStyle,
style: CellStyle,
) -> io::Result<()> {
if style.fg != last_style.fg {
write!(
writer,
"\x1b[38;2;{};{};{}m",
style.fg.r, style.fg.g, style.fg.b
)?;
last_style.fg = style.fg;
}
if style.bg != last_style.bg {
match style.bg {
Some(color) => write!(writer, "\x1b[48;2;{};{};{}m", color.r, color.g, color.b)?,
None => writer.write_all(b"\x1b[49m")?,
}
last_style.bg = style.bg;
}
if style.bold != last_style.bold {
writer.write_all(if style.bold {
b"\x1b[1m".as_slice()
} else {
b"\x1b[22m".as_slice()
})?;
last_style.bold = style.bold;
}
if style.italic != last_style.italic {
writer.write_all(if style.italic {
b"\x1b[3m".as_slice()
} else {
b"\x1b[23m".as_slice()
})?;
last_style.italic = style.italic;
}
if style.underlined != last_style.underlined {
writer.write_all(if style.underlined {
b"\x1b[4m".as_slice()
} else {
b"\x1b[24m".as_slice()
})?;
last_style.underlined = style.underlined;
}
Ok(())
}
struct CountingWriter<'a, W: Write> {
inner: &'a mut W,
bytes_written: usize,
}
impl<'a, W: Write> CountingWriter<'a, W> {
fn new(inner: &'a mut W) -> Self {
Self {
inner,
bytes_written: 0,
}
}
fn bytes_written(&self) -> usize {
self.bytes_written
}
}
impl<W: Write> Write for CountingWriter<'_, W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let written = self.inner.write(buf)?;
self.bytes_written += written;
Ok(written)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
fn rect_contains(area: Rect, x: usize, y: usize) -> bool {
x >= area.x as usize
&& x < area.right() as usize
&& y >= area.y as usize
&& y < area.bottom() as usize
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn terminal_options_default_is_safe() {
let options = TerminalOptions::default();
assert!(options.alternate_screen);
assert!(options.hide_cursor);
assert!(!options.mouse_capture);
}
fn visible_text(ansi: &str) -> String {
let mut visible = String::new();
let mut chars = ansi.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
if chars.peek() == Some(&'[') {
chars.next();
for code in chars.by_ref() {
if code.is_ascii_alphabetic() {
break;
}
}
}
continue;
}
visible.push(ch);
}
visible
}
#[test]
fn diff_repositions_non_contiguous_dirty_cells() {
let front = Buffer::new(12, 1);
let mut back = Buffer::new(12, 1);
back.set(
8,
0,
crate::core::buffer::Cell::new('A', crate::core::color::Color::WHITE, None),
);
back.set(
10,
0,
crate::core::buffer::Cell::new('B', crate::core::color::Color::WHITE, None),
);
let ansi = diff_ansi_string(&front, &back, 12, 1);
assert!(ansi.contains("\x1b[1;9H"));
assert!(ansi.contains("\x1b[1;11H"));
}
#[test]
fn diff_clears_old_wide_glyph_continuation_cell() {
let mut front = Buffer::new(4, 1);
let mut back = Buffer::new(4, 1);
front.set(
0,
0,
crate::core::buffer::Cell::new('ä¸', crate::core::color::Color::WHITE, None),
);
back.set(
0,
0,
crate::core::buffer::Cell::new('A', crate::core::color::Color::WHITE, None),
);
let ansi = diff_ansi_string(&front, &back, 4, 1);
assert!(ansi.contains("\x1b[1;1H"));
assert_eq!(visible_text(&ansi), "A ");
}
#[test]
fn diff_can_limit_output_to_dirty_rectangles() {
let front = Buffer::new(8, 1);
let mut back = Buffer::new(8, 1);
back.set(
1,
0,
crate::core::buffer::Cell::new('A', crate::core::color::Color::WHITE, None),
);
back.set(
6,
0,
crate::core::buffer::Cell::new('B', crate::core::color::Color::WHITE, None),
);
let output = diff_output(&front, &back, 8, 1, Some(&[Rect::new(5, 0, 3, 1)]), false);
assert_eq!(output.dirty_cells, 1);
assert!(!output.ansi.contains("A"));
assert!(output.ansi.contains("B"));
}
#[test]
fn diff_runs_group_contiguous_dirty_cells_by_style() {
let front = Buffer::new(6, 1);
let mut back = Buffer::new(6, 1);
let plain = crate::style::Style::new().fg(crate::core::color::Color::CYAN);
let bold = plain.bold();
back.set(0, 0, Cell::styled('A', plain));
back.set(1, 0, Cell::styled('B', plain));
back.set(2, 0, Cell::styled('C', bold));
let runs = diff_runs(&front, &back, 6, 1).collect::<Vec<_>>();
assert_eq!(runs.len(), 2);
assert_eq!(runs[0].x, 0);
assert_eq!(runs[0].y, 0);
assert_eq!(runs[0].style, CellStyle::from_style(plain));
assert_eq!(runs[0].text.to_string(), "AB");
assert_eq!(runs[1].x, 2);
assert_eq!(runs[1].style, CellStyle::from_style(bold));
assert_eq!(runs[1].text.to_string(), "C");
}
#[test]
fn write_diff_to_matches_string_diff_output() {
let front = Buffer::new(8, 1);
let mut back = Buffer::new(8, 1);
back.set(
1,
0,
Cell::styled(
'A',
crate::style::Style::new()
.fg(crate::core::color::Color::YELLOW)
.bold(),
),
);
back.set(
2,
0,
Cell::styled(
'B',
crate::style::Style::new()
.fg(crate::core::color::Color::YELLOW)
.bold(),
),
);
let expected = diff_ansi_string(&front, &back, 8, 1);
let mut output = Vec::new();
let stats = write_diff_to(&mut output, &front, &back, 8, 1, None).unwrap();
let output = String::from_utf8(output).unwrap();
assert_eq!(output, expected);
assert_eq!(stats.bytes_written, expected.len());
assert_eq!(stats.dirty_cells, 2);
}
#[test]
fn frame_records_named_widget_diagnostics() {
let mut buffer = Buffer::new(8, 3);
let mut diagnostics = Vec::new();
let mut interaction = InteractionLayer::new();
let mut dirty_regions = Vec::new();
let mut frame = Frame {
area: Rect::new(0, 0, 8, 3),
buffer: &mut buffer,
diagnostics: &mut diagnostics,
interaction: &mut interaction,
dirty_regions: &mut dirty_regions,
frame_count: 0,
cursor_position: None,
};
frame.render_widget_timed(
"panel",
crate::widgets::block::Block::bordered().title("p"),
Rect::new(0, 0, 8, 3),
);
frame.time_pane("inner", Rect::new(1, 1, 6, 1), |_| {});
assert_eq!(frame.diagnostics().len(), 2);
assert_eq!(frame.diagnostics()[0].name, "panel");
assert_eq!(frame.diagnostics()[0].area, Some(Rect::new(0, 0, 8, 3)));
assert_eq!(frame.diagnostics()[1].name, "inner");
}
}