mirror of
https://github.com/ratatui-org/ratatui
synced 2025-02-16 14:08:44 +00:00
feat: add metrics (wip)
Adds a variety of metrics to the terminal and backend modules. Run the metrics example to see the metrics in action. ```shell cargo run --example metrics cargo run --example metrics --release ```
This commit is contained in:
parent
b13e2f9473
commit
5673bb5039
8 changed files with 761 additions and 263 deletions
|
@ -40,6 +40,8 @@ time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
|||
unicode-segmentation = "1.10"
|
||||
unicode-truncate = "1"
|
||||
unicode-width = "0.1.13"
|
||||
metrics = { version = "0.23.0", git = "https://github.com/joshka/metrics.git", branch = "jm/derive-debug" }
|
||||
quanta = "0.12.3"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
# termion is not supported on Windows
|
||||
|
@ -55,6 +57,7 @@ fakeit = "1.1"
|
|||
font8x8 = "0.3.1"
|
||||
futures = "0.3.30"
|
||||
indoc = "2"
|
||||
metrics-util = { version = "0.17.0", git = "https://github.com/joshka/metrics.git", branch = "jm/derive-debug" }
|
||||
octocrab = "0.39.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
|
|
222
examples/metrics.rs
Normal file
222
examples/metrics.rs
Normal file
|
@ -0,0 +1,222 @@
|
|||
use std::{
|
||||
sync::{atomic::Ordering, Arc},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||
use metrics::{Counter, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit};
|
||||
use metrics_util::{
|
||||
registry::{AtomicStorage, Registry},
|
||||
Summary,
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Stylize,
|
||||
widgets::{Row, Table, Widget},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let recorder = MetricsRecorder::new();
|
||||
let recorder_widget = recorder.widget();
|
||||
recorder.install();
|
||||
let terminal = ratatui::init();
|
||||
let app = App::new(recorder_widget);
|
||||
let result = app.run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct App {
|
||||
should_quit: bool,
|
||||
recorder_widget: RecorderWidget,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new(recorder_widget: RecorderWidget) -> Self {
|
||||
Self {
|
||||
should_quit: false,
|
||||
recorder_widget,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
let mut last_frame = Instant::now();
|
||||
let frame_duration = Duration::from_secs_f64(1.0 / 60.0);
|
||||
while !self.should_quit {
|
||||
if last_frame.elapsed() >= frame_duration {
|
||||
last_frame = Instant::now();
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
}
|
||||
self.handle_events(frame_duration.saturating_sub(last_frame.elapsed()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let [top, main] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(frame.area());
|
||||
let title = if cfg!(debug_assertions) {
|
||||
"Metrics Example (debug)"
|
||||
} else {
|
||||
"Metrics Example (release)"
|
||||
};
|
||||
frame.render_widget(title.blue().into_centered_line(), top);
|
||||
frame.render_widget(&self.recorder_widget, main);
|
||||
}
|
||||
|
||||
fn handle_events(&mut self, timeout: Duration) -> Result<()> {
|
||||
if !event::poll(timeout)? {
|
||||
return Ok(());
|
||||
}
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => self.on_key_press(key),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_key_press(&mut self, key: event::KeyEvent) {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => self.should_quit = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MetricsRecorder {
|
||||
metrics: Arc<Metrics>,
|
||||
}
|
||||
|
||||
impl MetricsRecorder {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn widget(&self) -> RecorderWidget {
|
||||
RecorderWidget {
|
||||
metrics: Arc::clone(&self.metrics),
|
||||
}
|
||||
}
|
||||
|
||||
fn install(self) {
|
||||
metrics::set_global_recorder(self).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Metrics {
|
||||
registry: Registry<Key, AtomicStorage>,
|
||||
}
|
||||
|
||||
impl Default for Metrics {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
registry: Registry::atomic(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
fn counter(&self, key: &Key) -> Counter {
|
||||
self.registry
|
||||
.get_or_create_counter(key, |c| Counter::from_arc(c.clone()))
|
||||
}
|
||||
|
||||
fn gauge(&self, key: &Key) -> Gauge {
|
||||
self.registry
|
||||
.get_or_create_gauge(key, |g| Gauge::from_arc(g.clone()))
|
||||
}
|
||||
|
||||
fn histogram(&self, key: &Key) -> Histogram {
|
||||
self.registry
|
||||
.get_or_create_histogram(key, |h| Histogram::from_arc(h.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RecorderWidget {
|
||||
metrics: Arc<Metrics>,
|
||||
}
|
||||
|
||||
impl Widget for &RecorderWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut lines = Vec::<(Key, String)>::new();
|
||||
self.metrics.registry.visit_counters(|key, counter| {
|
||||
let value = counter.load(Ordering::SeqCst);
|
||||
lines.push((key.clone(), value.to_string()));
|
||||
});
|
||||
self.metrics.registry.visit_gauges(|key, gauge| {
|
||||
let value = gauge.load(Ordering::SeqCst);
|
||||
lines.push((key.clone(), value.to_string()));
|
||||
});
|
||||
self.metrics.registry.visit_histograms(|key, histogram| {
|
||||
let mut summary = Summary::with_defaults();
|
||||
for data in histogram.data() {
|
||||
summary.add(data);
|
||||
}
|
||||
if summary.is_empty() {
|
||||
lines.push((key.clone(), "empty".to_string()));
|
||||
} else {
|
||||
let min = Duration::from_secs_f64(summary.min());
|
||||
let max = Duration::from_secs_f64(summary.max());
|
||||
let p50 = Duration::from_secs_f64(summary.quantile(0.5).unwrap());
|
||||
let p90 = Duration::from_secs_f64(summary.quantile(0.9).unwrap());
|
||||
let p99 = Duration::from_secs_f64(summary.quantile(0.99).unwrap());
|
||||
let line = format!(
|
||||
"min={min:.2?} max={max:.2?} p50={p50:.2?} p90={p90:.2?} p99={p99:.2?}"
|
||||
);
|
||||
lines.push((key.clone(), line));
|
||||
}
|
||||
});
|
||||
lines.sort();
|
||||
let rows = lines
|
||||
.iter()
|
||||
.map(|(key, line)| Row::new([key.name(), line]))
|
||||
.enumerate()
|
||||
.map(|(i, row)| {
|
||||
if (i % 2) == 0 {
|
||||
row.on_dark_gray()
|
||||
} else {
|
||||
row.on_black()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Table::new(rows, [Constraint::Length(40), Constraint::Fill(1)]).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
impl Recorder for MetricsRecorder {
|
||||
fn describe_counter(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
|
||||
// todo!()
|
||||
}
|
||||
|
||||
fn describe_gauge(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
|
||||
// todo!()
|
||||
}
|
||||
|
||||
fn describe_histogram(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
|
||||
// todo!()
|
||||
}
|
||||
|
||||
fn register_counter(&self, key: &Key, metadata: &Metadata<'_>) -> Counter {
|
||||
self.metrics.counter(key)
|
||||
}
|
||||
|
||||
fn register_gauge(&self, key: &Key, metadata: &Metadata<'_>) -> Gauge {
|
||||
self.metrics.gauge(key)
|
||||
}
|
||||
|
||||
fn register_histogram(&self, key: &Key, metadata: &Metadata<'_>) -> Histogram {
|
||||
self.metrics.histogram(key)
|
||||
}
|
||||
}
|
|
@ -100,12 +100,14 @@
|
|||
//! [Backend Comparison]:
|
||||
//! https://ratatui.rs/concepts/backends/comparison/
|
||||
//! [Ratatui Website]: https://ratatui.rs
|
||||
use std::io;
|
||||
use std::{io, sync::LazyLock};
|
||||
|
||||
use metrics::{Counter, Histogram};
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{
|
||||
buffer::Cell,
|
||||
counter, duration_histogram,
|
||||
layout::{Position, Size},
|
||||
};
|
||||
|
||||
|
@ -127,6 +129,89 @@ pub use self::termwiz::TermwizBackend;
|
|||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
|
||||
static METRICS: LazyLock<Metrics> = LazyLock::new(Metrics::new);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Metrics {
|
||||
pub clear_region_count: Counter,
|
||||
pub clear_region_duration: Histogram,
|
||||
pub draw_count: Counter,
|
||||
pub draw_duration: Histogram,
|
||||
pub append_lines_count: Counter,
|
||||
pub append_lines_duration: Histogram,
|
||||
pub hide_cursor_duration: Histogram,
|
||||
pub show_cursor_duration: Histogram,
|
||||
pub get_cursor_position_duration: Histogram,
|
||||
pub set_cursor_position_duration: Histogram,
|
||||
pub size_duration: Histogram,
|
||||
pub window_size_duration: Histogram,
|
||||
pub flush_count: Counter,
|
||||
pub flush_duration: Histogram,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
clear_region_count: counter!(
|
||||
"ratatui.backend.clear_region.count",
|
||||
"Number of times a region of the backend buffer was cleared"
|
||||
),
|
||||
clear_region_duration: duration_histogram!(
|
||||
"ratatui.backend.clear.time",
|
||||
"Time spent clearing the backend buffer"
|
||||
),
|
||||
draw_count: counter!(
|
||||
"ratatui.backend.draw.count",
|
||||
"Number of times the backend buffer was drawn to the terminal"
|
||||
),
|
||||
draw_duration: duration_histogram!(
|
||||
"ratatui.backend.draw.time",
|
||||
"Time spent drawing the backend buffer to the terminal"
|
||||
),
|
||||
hide_cursor_duration: duration_histogram!(
|
||||
"ratatui.backend.hide_cursor.time",
|
||||
"Time spent hiding the cursor in the backend"
|
||||
),
|
||||
show_cursor_duration: duration_histogram!(
|
||||
"ratatui.backend.show_cursor.time",
|
||||
"Time spent showing the cursor in the backend"
|
||||
),
|
||||
get_cursor_position_duration: duration_histogram!(
|
||||
"ratatui.backend.get_cursor_position.time",
|
||||
"Time spent getting the cursor position from the backend"
|
||||
),
|
||||
set_cursor_position_duration: duration_histogram!(
|
||||
"ratatui.backend.set_cursor_position.time",
|
||||
"Time spent setting the cursor position in the backend"
|
||||
),
|
||||
append_lines_count: counter!(
|
||||
"ratatui.backend.append_lines.count",
|
||||
"Number of times lines were appended to the backend buffer"
|
||||
),
|
||||
append_lines_duration: duration_histogram!(
|
||||
"ratatui.backend.append_lines.time",
|
||||
"Time spent appending lines to the backend buffer"
|
||||
),
|
||||
size_duration: duration_histogram!(
|
||||
"ratatui.backend.size.time",
|
||||
"Time spent getting the size of the backend buffer"
|
||||
),
|
||||
window_size_duration: duration_histogram!(
|
||||
"ratatui.backend.window_size.time",
|
||||
"Time spent getting the window size of the backend buffer"
|
||||
),
|
||||
flush_count: counter!(
|
||||
"ratatui.backend.flush.count",
|
||||
"Number of times the backend buffer was flushed to the terminal"
|
||||
),
|
||||
flush_duration: duration_histogram!(
|
||||
"ratatui.backend.flush.time",
|
||||
"Time spent flushing the backend buffer to the terminal"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum representing the different types of clearing operations that can be performed
|
||||
/// on the terminal screen.
|
||||
#[derive(Debug, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
use std::io::{self, Write};
|
||||
|
||||
use crossterm::cursor;
|
||||
#[cfg(feature = "underline-color")]
|
||||
use crossterm::style::SetUnderlineColor;
|
||||
|
||||
use super::METRICS;
|
||||
use crate::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
|
@ -20,6 +22,7 @@ use crate::{
|
|||
terminal::{self, Clear},
|
||||
},
|
||||
layout::{Position, Size},
|
||||
metrics::HistogramExt,
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
|
@ -154,78 +157,89 @@ where
|
|||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
#[cfg(feature = "underline-color")]
|
||||
let mut underline_color = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<Position> = None;
|
||||
for (x, y, cell) in content {
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
|
||||
queue!(self.writer, MoveTo(x, y))?;
|
||||
}
|
||||
last_pos = Some(Position { x, y });
|
||||
if cell.modifier != modifier {
|
||||
let diff = ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier,
|
||||
};
|
||||
diff.queue(&mut self.writer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg || cell.bg != bg {
|
||||
queue!(
|
||||
self.writer,
|
||||
SetColors(Colors::new(cell.fg.into(), cell.bg.into()))
|
||||
)?;
|
||||
fg = cell.fg;
|
||||
bg = cell.bg;
|
||||
}
|
||||
METRICS.draw_count.increment(1);
|
||||
METRICS.draw_duration.measure_duration(|| {
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
#[cfg(feature = "underline-color")]
|
||||
if cell.underline_color != underline_color {
|
||||
let color = CColor::from(cell.underline_color);
|
||||
queue!(self.writer, SetUnderlineColor(color))?;
|
||||
underline_color = cell.underline_color;
|
||||
let mut underline_color = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<Position> = None;
|
||||
for (x, y, cell) in content {
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
|
||||
queue!(self.writer, MoveTo(x, y))?;
|
||||
}
|
||||
last_pos = Some(Position { x, y });
|
||||
if cell.modifier != modifier {
|
||||
let diff = ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier,
|
||||
};
|
||||
diff.queue(&mut self.writer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg || cell.bg != bg {
|
||||
queue!(
|
||||
self.writer,
|
||||
SetColors(Colors::new(cell.fg.into(), cell.bg.into()))
|
||||
)?;
|
||||
fg = cell.fg;
|
||||
bg = cell.bg;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if cell.underline_color != underline_color {
|
||||
let color = CColor::from(cell.underline_color);
|
||||
queue!(self.writer, SetUnderlineColor(color))?;
|
||||
underline_color = cell.underline_color;
|
||||
}
|
||||
|
||||
queue!(self.writer, Print(cell.symbol()))?;
|
||||
}
|
||||
|
||||
queue!(self.writer, Print(cell.symbol()))?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
return queue!(
|
||||
self.writer,
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetUnderlineColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset),
|
||||
);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
return queue!(
|
||||
self.writer,
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset),
|
||||
);
|
||||
#[cfg(feature = "underline-color")]
|
||||
return queue!(
|
||||
self.writer,
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetUnderlineColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset),
|
||||
);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
return queue!(
|
||||
self.writer,
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset),
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
execute!(self.writer, Hide)
|
||||
METRICS
|
||||
.hide_cursor_duration
|
||||
.measure_duration(|| execute!(self.writer, Hide))
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
execute!(self.writer, Show)
|
||||
METRICS
|
||||
.show_cursor_duration
|
||||
.measure_duration(|| execute!(self.writer, Show))
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
crossterm::cursor::position()
|
||||
.map(|(x, y)| Position { x, y })
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
METRICS.get_cursor_position_duration.measure_duration(|| {
|
||||
cursor::position()
|
||||
.map(|(x, y)| Position { x, y })
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
let Position { x, y } = position.into();
|
||||
execute!(self.writer, MoveTo(x, y))
|
||||
METRICS.set_cursor_position_duration.measure_duration(|| {
|
||||
let Position { x, y } = position.into();
|
||||
execute!(self.writer, MoveTo(x, y))
|
||||
})
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
|
@ -233,48 +247,63 @@ where
|
|||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
execute!(
|
||||
self.writer,
|
||||
Clear(match clear_type {
|
||||
ClearType::All => crossterm::terminal::ClearType::All,
|
||||
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
|
||||
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
|
||||
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
|
||||
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
queue!(self.writer, Print("\n"))?;
|
||||
}
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
let (width, height) = terminal::size()?;
|
||||
Ok(Size { width, height })
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
let crossterm::terminal::WindowSize {
|
||||
columns,
|
||||
rows,
|
||||
width,
|
||||
height,
|
||||
} = terminal::window_size()?;
|
||||
Ok(WindowSize {
|
||||
columns_rows: Size {
|
||||
width: columns,
|
||||
height: rows,
|
||||
},
|
||||
pixels: Size { width, height },
|
||||
METRICS.clear_region_count.increment(1);
|
||||
METRICS.clear_region_duration.measure_duration(|| {
|
||||
execute!(
|
||||
self.writer,
|
||||
Clear(match clear_type {
|
||||
ClearType::All => crossterm::terminal::ClearType::All,
|
||||
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
|
||||
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
|
||||
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
|
||||
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
METRICS.append_lines_count.increment(1);
|
||||
METRICS.append_lines_duration.measure_duration(|| {
|
||||
for _ in 0..n {
|
||||
queue!(self.writer, Print("\n"))?;
|
||||
}
|
||||
self.writer.flush()
|
||||
})
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
METRICS
|
||||
.size_duration
|
||||
.measure_duration(|| terminal::size().map(Size::from))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
METRICS
|
||||
.window_size_duration
|
||||
.measure_duration(|| terminal::window_size().map(WindowSize::from))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
METRICS.flush_count.increment(1);
|
||||
METRICS
|
||||
.flush_duration
|
||||
.measure_duration(|| self.writer.flush())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crossterm::terminal::WindowSize> for WindowSize {
|
||||
fn from(value: crossterm::terminal::WindowSize) -> Self {
|
||||
Self {
|
||||
columns_rows: Size {
|
||||
width: value.columns,
|
||||
height: value.rows,
|
||||
},
|
||||
pixels: Size {
|
||||
width: value.width,
|
||||
height: value.height,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
use std::{
|
||||
fmt,
|
||||
ops::{Index, IndexMut},
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use metrics::Histogram;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{buffer::Cell, layout::Position, prelude::*};
|
||||
use crate::{
|
||||
buffer::Cell, duration_histogram, layout::Position, metrics::HistogramExt, prelude::*,
|
||||
};
|
||||
|
||||
/// A buffer that maps to the desired content of the terminal after the draw call
|
||||
///
|
||||
|
@ -70,6 +74,23 @@ pub struct Buffer {
|
|||
pub content: Vec<Cell>,
|
||||
}
|
||||
|
||||
static METRICS: LazyLock<Metrics> = LazyLock::new(Metrics::new);
|
||||
|
||||
struct Metrics {
|
||||
diff_duration: Histogram,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
diff_duration: duration_histogram!(
|
||||
"ratatui.buffer.diff.time",
|
||||
"Time spent diffing buffers"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
#[must_use]
|
||||
|
@ -464,27 +485,32 @@ impl Buffer {
|
|||
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
|
||||
/// ```
|
||||
pub fn diff<'a>(&self, other: &'a Self) -> Vec<(u16, u16, &'a Cell)> {
|
||||
let previous_buffer = &self.content;
|
||||
let next_buffer = &other.content;
|
||||
METRICS.diff_duration.measure_duration(|| {
|
||||
let previous_buffer = &self.content;
|
||||
let next_buffer = &other.content;
|
||||
|
||||
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
||||
// Cells invalidated by drawing/replacing preceding multi-width characters:
|
||||
let mut invalidated: usize = 0;
|
||||
// Cells from the current buffer to skip due to preceding multi-width characters taking
|
||||
// their place (the skipped cells should be blank anyway), or due to per-cell-skipping:
|
||||
let mut to_skip: usize = 0;
|
||||
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
||||
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
let (x, y) = self.pos_of(i);
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
||||
// Cells invalidated by drawing/replacing preceding multi-width characters:
|
||||
let mut invalidated: usize = 0;
|
||||
// Cells from the current buffer to skip due to preceding multi-width characters taking
|
||||
// their place (the skipped cells should be blank anyway), or due to per-cell-skipping:
|
||||
let mut to_skip: usize = 0;
|
||||
for (i, (current, previous)) in
|
||||
next_buffer.iter().zip(previous_buffer.iter()).enumerate()
|
||||
{
|
||||
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
let (x, y) = self.pos_of(i);
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
}
|
||||
|
||||
to_skip = current.symbol().width().saturating_sub(1);
|
||||
|
||||
let affected_width =
|
||||
std::cmp::max(current.symbol().width(), previous.symbol().width());
|
||||
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
|
||||
}
|
||||
|
||||
to_skip = current.symbol().width().saturating_sub(1);
|
||||
|
||||
let affected_width = std::cmp::max(current.symbol().width(), previous.symbol().width());
|
||||
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
|
||||
}
|
||||
updates
|
||||
updates
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -310,6 +310,7 @@ pub use termwiz;
|
|||
pub mod backend;
|
||||
pub mod buffer;
|
||||
pub mod layout;
|
||||
pub(crate) mod metrics;
|
||||
pub mod prelude;
|
||||
pub mod style;
|
||||
pub mod symbols;
|
||||
|
|
46
src/metrics.rs
Normal file
46
src/metrics.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
use metrics::Histogram;
|
||||
|
||||
/// A helper macro that registers and describes a counter
|
||||
#[macro_export]
|
||||
macro_rules! counter {
|
||||
($name:expr, $description:expr $(,)?) => {{
|
||||
::metrics::describe_counter!($name, ::metrics::Unit::Count, $description);
|
||||
::metrics::counter!($name)
|
||||
}};
|
||||
}
|
||||
|
||||
/// A helper macro that registers and describes a histogram that tracks durations.
|
||||
#[macro_export]
|
||||
macro_rules! duration_histogram {
|
||||
($name:expr, $description:expr $(,)?) => {{
|
||||
::metrics::describe_histogram!($name, ::metrics::Unit::Seconds, $description);
|
||||
::metrics::histogram!($name)
|
||||
}};
|
||||
}
|
||||
|
||||
/// A helper macro that registers and describes a histogram that tracks bytes.
|
||||
#[macro_export]
|
||||
macro_rules! bytes_histogram {
|
||||
($name:expr, $description:expr $(,)?) => {{
|
||||
::metrics::describe_histogram!($name, ::metrics::Unit::Bytes, $description);
|
||||
::metrics::histogram!($name)
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) trait HistogramExt {
|
||||
fn measure_duration<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: FnOnce() -> R;
|
||||
}
|
||||
|
||||
impl HistogramExt for Histogram {
|
||||
fn measure_duration<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: FnOnce() -> R,
|
||||
{
|
||||
let start = quanta::Instant::now();
|
||||
let result = f();
|
||||
self.record(start.elapsed().as_secs_f64());
|
||||
result
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
use std::io;
|
||||
use std::{io, sync::LazyLock};
|
||||
|
||||
use metrics::{Counter, Histogram};
|
||||
|
||||
use crate::{
|
||||
backend::ClearType, buffer::Cell, prelude::*, CompletedFrame, TerminalOptions, Viewport,
|
||||
backend::ClearType, buffer::Cell, counter, duration_histogram, metrics::HistogramExt,
|
||||
prelude::*, CompletedFrame, TerminalOptions, Viewport,
|
||||
};
|
||||
|
||||
/// An interface to interact and draw [`Frame`]s on the user's terminal.
|
||||
|
@ -84,6 +87,69 @@ pub struct Options {
|
|||
pub viewport: Viewport,
|
||||
}
|
||||
|
||||
static METRICS: LazyLock<Metrics> = LazyLock::new(Metrics::new);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Metrics {
|
||||
pub clear_duration: Histogram,
|
||||
pub draw_callback_duration: Histogram,
|
||||
pub draw_count: Counter,
|
||||
pub draw_duration: Histogram,
|
||||
pub flush_duration: Histogram,
|
||||
pub flush_count: Counter,
|
||||
pub resize_duration: Histogram,
|
||||
pub resize_count: Counter,
|
||||
pub insert_before_count: Counter,
|
||||
pub insert_before_duration: Histogram,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
clear_duration: duration_histogram!(
|
||||
"ratatui.terminal.clear.time",
|
||||
"Time spent clearing the terminal buffer"
|
||||
),
|
||||
draw_callback_duration: duration_histogram!(
|
||||
"ratatui.terminal.draw.callback.time",
|
||||
"Time spent calling the draw callback (application code)"
|
||||
),
|
||||
draw_count: counter!(
|
||||
"ratatui.terminal.draw.count",
|
||||
"Number of times the terminal buffer was drawn to the backend"
|
||||
),
|
||||
draw_duration: duration_histogram!(
|
||||
"ratatui.terminal.draw.time",
|
||||
"Time spent drawing the terminal buffer to the backend"
|
||||
),
|
||||
flush_duration: duration_histogram!(
|
||||
"ratatui.terminal.flush.time",
|
||||
"Time spent flushing the terminal buffer to the terminal"
|
||||
),
|
||||
flush_count: counter!(
|
||||
"ratatui.terminal.flush.count",
|
||||
"Number of times the terminal buffer was flushed to the terminal"
|
||||
),
|
||||
resize_duration: duration_histogram!(
|
||||
"ratatui.terminal.resize.time",
|
||||
"Time spent resizing the terminal buffer"
|
||||
),
|
||||
resize_count: counter!(
|
||||
"ratatui.terminal.resize.count",
|
||||
"Number of times the terminal buffer was resized"
|
||||
),
|
||||
insert_before_count: counter!(
|
||||
"ratatui.terminal.insert_before.count",
|
||||
"Number of times content was inserted before the viewport"
|
||||
),
|
||||
insert_before_duration: duration_histogram!(
|
||||
"ratatui.terminal.insert_before.time",
|
||||
"Time spent inserting content before the viewport"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
|
@ -190,13 +256,16 @@ where
|
|||
/// Obtains a difference between the previous and the current buffer and passes it to the
|
||||
/// current backend for drawing.
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
if let Some((col, row, _)) = updates.last() {
|
||||
self.last_known_cursor_pos = Position { x: *col, y: *row };
|
||||
}
|
||||
self.backend.draw(updates.into_iter())
|
||||
METRICS.flush_count.increment(1);
|
||||
METRICS.flush_duration.measure_duration(|| {
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
if let Some((col, row, _)) = updates.last() {
|
||||
self.last_known_cursor_pos = Position { x: *col, y: *row };
|
||||
}
|
||||
self.backend.draw(updates.into_iter())
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates the Terminal so that internal buffers match the requested area.
|
||||
|
@ -204,28 +273,31 @@ where
|
|||
/// Requested area will be saved to remain consistent when rendering. This leads to a full clear
|
||||
/// of the screen.
|
||||
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
|
||||
let next_area = match self.viewport {
|
||||
Viewport::Fullscreen => area,
|
||||
Viewport::Inline(height) => {
|
||||
let offset_in_previous_viewport = self
|
||||
.last_known_cursor_pos
|
||||
.y
|
||||
.saturating_sub(self.viewport_area.top());
|
||||
compute_inline_size(
|
||||
&mut self.backend,
|
||||
height,
|
||||
area.as_size(),
|
||||
offset_in_previous_viewport,
|
||||
)?
|
||||
.0
|
||||
}
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
self.set_viewport_area(next_area);
|
||||
self.clear()?;
|
||||
METRICS.resize_count.increment(1);
|
||||
METRICS.resize_duration.measure_duration(|| {
|
||||
let next_area = match self.viewport {
|
||||
Viewport::Fullscreen => area,
|
||||
Viewport::Inline(height) => {
|
||||
let offset_in_previous_viewport = self
|
||||
.last_known_cursor_pos
|
||||
.y
|
||||
.saturating_sub(self.viewport_area.top());
|
||||
compute_inline_size(
|
||||
&mut self.backend,
|
||||
height,
|
||||
area.as_size(),
|
||||
offset_in_previous_viewport,
|
||||
)?
|
||||
.0
|
||||
}
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
self.set_viewport_area(next_area);
|
||||
self.clear()?;
|
||||
|
||||
self.last_known_area = area;
|
||||
Ok(())
|
||||
self.last_known_area = area;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn set_viewport_area(&mut self, area: Rect) {
|
||||
|
@ -377,45 +449,50 @@ where
|
|||
F: FnOnce(&mut Frame) -> Result<(), E>,
|
||||
E: Into<io::Error>,
|
||||
{
|
||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// and the terminal (if growing), which may OOB.
|
||||
self.autoresize()?;
|
||||
METRICS.draw_count.increment(1);
|
||||
METRICS.draw_duration.measure_duration(|| {
|
||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between
|
||||
// widgets and the terminal (if growing), which may OOB.
|
||||
self.autoresize()?;
|
||||
|
||||
let mut frame = self.get_frame();
|
||||
let mut frame = self.get_frame();
|
||||
|
||||
render_callback(&mut frame).map_err(Into::into)?;
|
||||
METRICS
|
||||
.draw_callback_duration
|
||||
.measure_duration(|| render_callback(&mut frame).map_err(Into::into))?;
|
||||
|
||||
// We can't change the cursor position right away because we have to flush the frame to
|
||||
// stdout first. But we also can't keep the frame around, since it holds a &mut to
|
||||
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
|
||||
let cursor_position = frame.cursor_position;
|
||||
// We can't change the cursor position right away because we have to flush the frame to
|
||||
// stdout first. But we also can't keep the frame around, since it holds a &mut to
|
||||
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
|
||||
let cursor_position = frame.cursor_position;
|
||||
|
||||
// Draw to stdout
|
||||
self.flush()?;
|
||||
// Draw to stdout
|
||||
self.flush()?;
|
||||
|
||||
match cursor_position {
|
||||
None => self.hide_cursor()?,
|
||||
Some(position) => {
|
||||
self.show_cursor()?;
|
||||
self.set_cursor_position(position)?;
|
||||
match cursor_position {
|
||||
None => self.hide_cursor()?,
|
||||
Some(position) => {
|
||||
self.show_cursor()?;
|
||||
self.set_cursor_position(position)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.swap_buffers();
|
||||
self.swap_buffers();
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
|
||||
let completed_frame = CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.last_known_area,
|
||||
count: self.frame_count,
|
||||
};
|
||||
let completed_frame = CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.last_known_area,
|
||||
count: self.frame_count,
|
||||
};
|
||||
|
||||
// increment frame count before returning from draw
|
||||
self.frame_count = self.frame_count.wrapping_add(1);
|
||||
// increment frame count before returning from draw
|
||||
self.frame_count = self.frame_count.wrapping_add(1);
|
||||
|
||||
Ok(completed_frame)
|
||||
Ok(completed_frame)
|
||||
})
|
||||
}
|
||||
|
||||
/// Hides the cursor.
|
||||
|
@ -465,23 +542,25 @@ where
|
|||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
match self.viewport {
|
||||
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
|
||||
Viewport::Inline(_) => {
|
||||
self.backend
|
||||
.set_cursor_position(self.viewport_area.as_position())?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
Viewport::Fixed(area) => {
|
||||
for y in area.top()..area.bottom() {
|
||||
self.backend.set_cursor_position(Position { x: 0, y })?;
|
||||
METRICS.clear_duration.measure_duration(|| {
|
||||
match self.viewport {
|
||||
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
|
||||
Viewport::Inline(_) => {
|
||||
self.backend
|
||||
.set_cursor_position(self.viewport_area.as_position())?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
Viewport::Fixed(area) => {
|
||||
for y in area.top()..area.bottom() {
|
||||
self.backend.set_cursor_position(Position { x: 0, y })?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Clears the inactive buffer and swaps it with the current buffer
|
||||
|
@ -573,89 +652,96 @@ where
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
// The approach of this function is to first render all of the lines to insert into a
|
||||
// temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
|
||||
// this buffer onto the screen.
|
||||
let area = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.viewport_area.width,
|
||||
height,
|
||||
};
|
||||
let mut buffer = Buffer::empty(area);
|
||||
draw_fn(&mut buffer);
|
||||
let mut buffer = buffer.content.as_slice();
|
||||
METRICS.insert_before_count.increment(1);
|
||||
METRICS.insert_before_duration.measure_duration(|| {
|
||||
// The approach of this function is to first render all of the lines to insert into a
|
||||
// temporary buffer, and then to loop drawing chunks from the buffer to the screen.
|
||||
// drawing this buffer onto the screen.
|
||||
let area = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.viewport_area.width,
|
||||
height,
|
||||
};
|
||||
let mut buffer = Buffer::empty(area);
|
||||
draw_fn(&mut buffer);
|
||||
let mut buffer = buffer.content.as_slice();
|
||||
|
||||
// Use i32 variables so we don't have worry about overflowed u16s when adding, or about
|
||||
// negative results when subtracting.
|
||||
let mut drawn_height: i32 = self.viewport_area.top().into();
|
||||
let mut buffer_height: i32 = height.into();
|
||||
let viewport_height: i32 = self.viewport_area.height.into();
|
||||
let screen_height: i32 = self.last_known_area.height.into();
|
||||
// Use i32 variables so we don't have worry about overflowed u16s when adding, or about
|
||||
// negative results when subtracting.
|
||||
let mut drawn_height: i32 = self.viewport_area.top().into();
|
||||
let mut buffer_height: i32 = height.into();
|
||||
let viewport_height: i32 = self.viewport_area.height.into();
|
||||
let screen_height: i32 = self.last_known_area.height.into();
|
||||
|
||||
// The algorithm here is to loop, drawing large chunks of text (up to a screen-full at a
|
||||
// time), until the remainder of the buffer plus the viewport fits on the screen. We choose
|
||||
// this loop condition because it guarantees that we can write the remainder of the buffer
|
||||
// with just one call to Self::draw_lines().
|
||||
while buffer_height + viewport_height > screen_height {
|
||||
// We will draw as much of the buffer as possible on this iteration in order to make
|
||||
// forward progress. So we have:
|
||||
// The algorithm here is to loop, drawing large chunks of text (up to a screen-full at a
|
||||
// time), until the remainder of the buffer plus the viewport fits on the screen. We
|
||||
// choose this loop condition because it guarantees that we can write the
|
||||
// remainder of the buffer with just one call to Self::draw_lines().
|
||||
while buffer_height + viewport_height > screen_height {
|
||||
// We will draw as much of the buffer as possible on this iteration in order to make
|
||||
// forward progress. So we have:
|
||||
//
|
||||
// to_draw = min(buffer_height, screen_height)
|
||||
//
|
||||
// We may need to scroll the screen up to make room to draw. We choose the minimal
|
||||
// possible scroll amount so we don't end up with the viewport sitting in the middle
|
||||
// of the screen when this function is done. The amount to scroll by
|
||||
// is:
|
||||
//
|
||||
// scroll_up = max(0, drawn_height + to_draw - screen_height)
|
||||
//
|
||||
// We want `scroll_up` to be enough so that, after drawing, we have used the whole
|
||||
// screen (drawn_height - scroll_up + to_draw = screen_height). However, there might
|
||||
// already be enough room on the screen to draw without scrolling (drawn_height +
|
||||
// to_draw <= screen_height). In this case, we just don't scroll at all.
|
||||
let to_draw = buffer_height.min(screen_height);
|
||||
let scroll_up = 0.max(drawn_height + to_draw - screen_height);
|
||||
self.scroll_up(scroll_up as u16)?;
|
||||
buffer =
|
||||
self.draw_lines((drawn_height - scroll_up) as u16, to_draw as u16, buffer)?;
|
||||
drawn_height += to_draw - scroll_up;
|
||||
buffer_height -= to_draw;
|
||||
}
|
||||
|
||||
// There is now enough room on the screen for the remaining buffer plus the viewport,
|
||||
// though we may still need to scroll up some of the existing text first. It's possible
|
||||
// that by this point we've drained the buffer, but we may still need to scroll up to
|
||||
// make room for the viewport.
|
||||
//
|
||||
// to_draw = min(buffer_height, screen_height)
|
||||
// We want to scroll up the exact amount that will leave us completely filling the
|
||||
// screen. However, it's possible that the viewport didn't start on the
|
||||
// bottom of the screen and the added lines weren't enough to push it all
|
||||
// the way to the bottom. We deal with this case by just ensuring that our
|
||||
// scroll amount is non-negative.
|
||||
//
|
||||
// We may need to scroll the screen up to make room to draw. We choose the minimal
|
||||
// possible scroll amount so we don't end up with the viewport sitting in the middle of
|
||||
// the screen when this function is done. The amount to scroll by is:
|
||||
//
|
||||
// scroll_up = max(0, drawn_height + to_draw - screen_height)
|
||||
//
|
||||
// We want `scroll_up` to be enough so that, after drawing, we have used the whole
|
||||
// screen (drawn_height - scroll_up + to_draw = screen_height). However, there might
|
||||
// already be enough room on the screen to draw without scrolling (drawn_height +
|
||||
// to_draw <= screen_height). In this case, we just don't scroll at all.
|
||||
let to_draw = buffer_height.min(screen_height);
|
||||
let scroll_up = 0.max(drawn_height + to_draw - screen_height);
|
||||
// We want:
|
||||
// screen_height = drawn_height - scroll_up + buffer_height + viewport_height
|
||||
// Or, equivalently:
|
||||
// scroll_up = drawn_height + buffer_height + viewport_height - screen_height
|
||||
let scroll_up = 0.max(drawn_height + buffer_height + viewport_height - screen_height);
|
||||
self.scroll_up(scroll_up as u16)?;
|
||||
buffer = self.draw_lines((drawn_height - scroll_up) as u16, to_draw as u16, buffer)?;
|
||||
drawn_height += to_draw - scroll_up;
|
||||
buffer_height -= to_draw;
|
||||
}
|
||||
self.draw_lines(
|
||||
(drawn_height - scroll_up) as u16,
|
||||
buffer_height as u16,
|
||||
buffer,
|
||||
)?;
|
||||
drawn_height += buffer_height - scroll_up;
|
||||
|
||||
// There is now enough room on the screen for the remaining buffer plus the viewport,
|
||||
// though we may still need to scroll up some of the existing text first. It's possible
|
||||
// that by this point we've drained the buffer, but we may still need to scroll up to make
|
||||
// room for the viewport.
|
||||
//
|
||||
// We want to scroll up the exact amount that will leave us completely filling the screen.
|
||||
// However, it's possible that the viewport didn't start on the bottom of the screen and
|
||||
// the added lines weren't enough to push it all the way to the bottom. We deal with this
|
||||
// case by just ensuring that our scroll amount is non-negative.
|
||||
//
|
||||
// We want:
|
||||
// screen_height = drawn_height - scroll_up + buffer_height + viewport_height
|
||||
// Or, equivalently:
|
||||
// scroll_up = drawn_height + buffer_height + viewport_height - screen_height
|
||||
let scroll_up = 0.max(drawn_height + buffer_height + viewport_height - screen_height);
|
||||
self.scroll_up(scroll_up as u16)?;
|
||||
self.draw_lines(
|
||||
(drawn_height - scroll_up) as u16,
|
||||
buffer_height as u16,
|
||||
buffer,
|
||||
)?;
|
||||
drawn_height += buffer_height - scroll_up;
|
||||
self.set_viewport_area(Rect {
|
||||
y: drawn_height as u16,
|
||||
..self.viewport_area
|
||||
});
|
||||
|
||||
self.set_viewport_area(Rect {
|
||||
y: drawn_height as u16,
|
||||
..self.viewport_area
|
||||
});
|
||||
// Clear the viewport off the screen. We didn't clear earlier for two reasons. First, it
|
||||
// wasn't necessary because the buffer we drew out of isn't sparse, so it overwrote
|
||||
// whatever was on the screen. Second, there is a weird bug with tmux where a full
|
||||
// screen clear plus immediate scrolling causes some garbage to go into the
|
||||
// scrollback.
|
||||
self.clear()?;
|
||||
|
||||
// Clear the viewport off the screen. We didn't clear earlier for two reasons. First, it
|
||||
// wasn't necessary because the buffer we drew out of isn't sparse, so it overwrote
|
||||
// whatever was on the screen. Second, there is a weird bug with tmux where a full screen
|
||||
// clear plus immediate scrolling causes some garbage to go into the scrollback.
|
||||
self.clear()?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Draw lines at the given vertical offset. The slice of cells must contain enough cells
|
||||
|
|
Loading…
Add table
Reference in a new issue