use crate::backend::{Backend, ClearType};
use crate::buffer::{Buffer, Cell};
use crate::layout::{Position, Rect, Size};
use crate::terminal::{CompletedFrame, Frame, TerminalOptions, Viewport};
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Terminal<B>
where
B: Backend,
{
backend: B,
buffers: [Buffer; 2],
current: usize,
hidden_cursor: bool,
viewport: Viewport,
viewport_area: Rect,
last_known_area: Rect,
last_known_cursor_pos: Position,
frame_count: usize,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Options {
pub viewport: Viewport,
}
impl<B> Drop for Terminal<B>
where
B: Backend,
{
fn drop(&mut self) {
if self.hidden_cursor {
#[allow(unused_variables)]
if let Err(err) = self.show_cursor() {
#[cfg(feature = "std")]
std::eprintln!("Failed to show the cursor: {err}");
}
}
}
}
impl<B> Terminal<B>
where
B: Backend,
{
pub fn new(backend: B) -> Result<Self, B::Error> {
Self::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)
}
pub fn with_options(mut backend: B, options: TerminalOptions) -> Result<Self, B::Error> {
let area = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?.into(),
Viewport::Fixed(area) => area,
};
let (viewport_area, cursor_pos) = match options.viewport {
Viewport::Fullscreen => (area, Position::ORIGIN),
Viewport::Inline(height) => {
compute_inline_size(&mut backend, height, area.as_size(), 0)?
}
Viewport::Fixed(area) => (area, area.as_position()),
};
Ok(Self {
backend,
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
viewport_area,
last_known_area: area,
last_known_cursor_pos: cursor_pos,
frame_count: 0,
})
}
pub const fn get_frame(&mut self) -> Frame<'_> {
let count = self.frame_count;
Frame {
cursor_position: None,
viewport_area: self.viewport_area,
buffer: self.current_buffer_mut(),
count,
}
}
pub const fn current_buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffers[self.current]
}
pub const fn backend(&self) -> &B {
&self.backend
}
pub const fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
pub fn flush(&mut self) -> Result<(), B::Error> {
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())
}
pub fn resize(&mut self, area: Rect) -> Result<(), B::Error> {
let next_area = match self.viewport {
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(_) | Viewport::Fullscreen => area,
};
self.set_viewport_area(next_area);
self.clear()?;
self.last_known_area = area;
Ok(())
}
fn set_viewport_area(&mut self, area: Rect) {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].resize(area);
self.viewport_area = area;
}
pub fn autoresize(&mut self) -> Result<(), B::Error> {
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
let area = self.size()?.into();
if area != self.last_known_area {
self.resize(area)?;
}
}
Ok(())
}
pub fn draw<F>(&mut self, render_callback: F) -> Result<CompletedFrame<'_>, B::Error>
where
F: FnOnce(&mut Frame),
{
self.try_draw(|frame| {
render_callback(frame);
Ok::<(), B::Error>(())
})
}
pub fn try_draw<F, E>(&mut self, render_callback: F) -> Result<CompletedFrame<'_>, B::Error>
where
F: FnOnce(&mut Frame) -> Result<(), E>,
E: Into<B::Error>,
{
self.autoresize()?;
let mut frame = self.get_frame();
render_callback(&mut frame).map_err(Into::into)?;
let cursor_position = frame.cursor_position;
self.flush()?;
match cursor_position {
None => self.hide_cursor()?,
Some(position) => {
self.show_cursor()?;
self.set_cursor_position(position)?;
}
}
self.swap_buffers();
self.backend.flush()?;
let completed_frame = CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_area,
count: self.frame_count,
};
self.frame_count = self.frame_count.wrapping_add(1);
Ok(completed_frame)
}
pub fn hide_cursor(&mut self) -> Result<(), B::Error> {
self.backend.hide_cursor()?;
self.hidden_cursor = true;
Ok(())
}
pub fn show_cursor(&mut self) -> Result<(), B::Error> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
#[deprecated = "use `get_cursor_position()` instead which returns `Result<Position>`"]
pub fn get_cursor(&mut self) -> Result<(u16, u16), B::Error> {
let Position { x, y } = self.get_cursor_position()?;
Ok((x, y))
}
#[deprecated = "use `set_cursor_position((x, y))` instead which takes `impl Into<Position>`"]
pub fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), B::Error> {
self.set_cursor_position(Position { x, y })
}
pub fn get_cursor_position(&mut self) -> Result<Position, B::Error> {
self.backend.get_cursor_position()
}
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<(), B::Error> {
let position = position.into();
self.backend.set_cursor_position(position)?;
self.last_known_cursor_pos = position;
Ok(())
}
pub fn clear(&mut self) -> Result<(), B::Error> {
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(_) => {
let area = self.viewport_area;
for y in area.top()..area.bottom() {
self.backend.set_cursor_position(Position { x: 0, y })?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
}
}
self.buffers[1 - self.current].reset();
Ok(())
}
pub fn swap_buffers(&mut self) {
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
}
pub fn size(&self) -> Result<Size, B::Error> {
self.backend.size()
}
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> Result<(), B::Error>
where
F: FnOnce(&mut Buffer),
{
match self.viewport {
#[cfg(feature = "scrolling-regions")]
Viewport::Inline(_) => self.insert_before_scrolling_regions(height, draw_fn),
#[cfg(not(feature = "scrolling-regions"))]
Viewport::Inline(_) => self.insert_before_no_scrolling_regions(height, draw_fn),
_ => Ok(()),
}
}
#[cfg(not(feature = "scrolling-regions"))]
fn insert_before_no_scrolling_regions(
&mut self,
height: u16,
draw_fn: impl FnOnce(&mut Buffer),
) -> Result<(), B::Error> {
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();
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();
while buffer_height + viewport_height > screen_height {
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;
}
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.clear()?;
Ok(())
}
#[cfg(feature = "scrolling-regions")]
fn insert_before_scrolling_regions(
&mut self,
mut height: u16,
draw_fn: impl FnOnce(&mut Buffer),
) -> Result<(), B::Error> {
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();
if self.viewport_area.height == self.last_known_area.height {
let mut first = true;
while !buffer.is_empty() {
buffer = if first {
self.draw_lines(0, 1, buffer)?
} else {
self.draw_lines_over_cleared(0, 1, buffer)?
};
first = false;
self.backend.scroll_region_up(0..1, 1)?;
}
let width = self.viewport_area.width as usize;
let top_line = self.buffers[1 - self.current].content[0..width].to_vec();
self.draw_lines_over_cleared(0, 1, &top_line)?;
return Ok(());
}
{
let viewport_top = self.viewport_area.top();
let viewport_bottom = self.viewport_area.bottom();
let screen_bottom = self.last_known_area.bottom();
if viewport_bottom < screen_bottom {
let to_draw = height.min(screen_bottom - viewport_bottom);
self.backend
.scroll_region_down(viewport_top..viewport_bottom + to_draw, to_draw)?;
buffer = self.draw_lines_over_cleared(viewport_top, to_draw, buffer)?;
self.set_viewport_area(Rect {
y: viewport_top + to_draw,
..self.viewport_area
});
height -= to_draw;
}
}
let viewport_top = self.viewport_area.top();
while height > 0 {
let to_draw = height.min(viewport_top);
self.backend.scroll_region_up(0..viewport_top, to_draw)?;
buffer = self.draw_lines_over_cleared(viewport_top - to_draw, to_draw, buffer)?;
height -= to_draw;
}
Ok(())
}
fn draw_lines<'a>(
&mut self,
y_offset: u16,
lines_to_draw: u16,
cells: &'a [Cell],
) -> Result<&'a [Cell], B::Error> {
let width: usize = self.last_known_area.width.into();
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
if lines_to_draw > 0 {
let iter = to_draw
.iter()
.enumerate()
.map(|(i, c)| ((i % width) as u16, y_offset + (i / width) as u16, c));
self.backend.draw(iter)?;
self.backend.flush()?;
}
Ok(remainder)
}
#[cfg(feature = "scrolling-regions")]
fn draw_lines_over_cleared<'a>(
&mut self,
y_offset: u16,
lines_to_draw: u16,
cells: &'a [Cell],
) -> Result<&'a [Cell], B::Error> {
let width: usize = self.last_known_area.width.into();
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
if lines_to_draw > 0 {
let area = Rect::new(0, y_offset, width as u16, y_offset + lines_to_draw);
let old = Buffer::empty(area);
let new = Buffer {
area,
content: to_draw.to_vec(),
};
self.backend.draw(old.diff(&new).into_iter())?;
self.backend.flush()?;
}
Ok(remainder)
}
#[cfg(not(feature = "scrolling-regions"))]
fn scroll_up(&mut self, lines_to_scroll: u16) -> Result<(), B::Error> {
if lines_to_scroll > 0 {
self.set_cursor_position(Position::new(
0,
self.last_known_area.height.saturating_sub(1),
))?;
self.backend.append_lines(lines_to_scroll)?;
}
Ok(())
}
}
fn compute_inline_size<B: Backend>(
backend: &mut B,
height: u16,
size: Size,
offset_in_previous_viewport: u16,
) -> Result<(Rect, Position), B::Error> {
let pos = backend.get_cursor_position()?;
let mut row = pos.y;
let max_height = size.height.min(height);
let lines_after_cursor = height
.saturating_sub(offset_in_previous_viewport)
.saturating_sub(1);
backend.append_lines(lines_after_cursor)?;
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
if missing_lines > 0 {
row = row.saturating_sub(missing_lines);
}
row = row.saturating_sub(offset_in_previous_viewport);
Ok((
Rect {
x: 0,
y: row,
width: size.width,
height: max_height,
},
pos,
))
}