use std::io;
use crate::{backend::ClearType, prelude::*};
#[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_size: Rect,
last_known_cursor_pos: (u16, u16),
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 {
if let Err(err) = self.show_cursor() {
eprintln!("Failed to show the cursor: {err}");
}
}
}
}
impl<B> Terminal<B>
where
B: Backend,
{
pub fn new(backend: B) -> io::Result<Self> {
Self::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)
}
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Self> {
let size = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
Viewport::Fixed(area) => area,
};
let (viewport_area, cursor_pos) = match options.viewport {
Viewport::Fullscreen => (size, (0, 0)),
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
Viewport::Fixed(area) => (area, (area.left(), area.top())),
};
Ok(Self {
backend,
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
viewport_area,
last_known_size: size,
last_known_cursor_pos: cursor_pos,
frame_count: 0,
})
}
pub 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 fn current_buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffers[self.current]
}
pub const fn backend(&self) -> &B {
&self.backend
}
pub fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
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 = (*col, *row);
}
self.backend.draw(updates.into_iter())
}
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
let next_area = match self.viewport {
Viewport::Fullscreen => size,
Viewport::Inline(height) => {
let offset_in_previous_viewport = self
.last_known_cursor_pos
.1
.saturating_sub(self.viewport_area.top());
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
}
Viewport::Fixed(area) => area,
};
self.set_viewport_area(next_area);
self.clear()?;
self.last_known_size = size;
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) -> io::Result<()> {
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
let size = self.size()?;
if size != self.last_known_size {
self.resize(size)?;
}
};
Ok(())
}
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Frame),
{
self.autoresize()?;
let mut frame = self.get_frame();
f(&mut frame);
let cursor_position = frame.cursor_position;
self.flush()?;
match cursor_position {
None => self.hide_cursor()?,
Some((x, y)) => {
self.show_cursor()?;
self.set_cursor(x, y)?;
}
}
self.swap_buffers();
self.backend.flush()?;
let completed_frame = CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_size,
count: self.frame_count,
};
self.frame_count = self.frame_count.wrapping_add(1);
Ok(completed_frame)
}
pub fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()?;
self.hidden_cursor = true;
Ok(())
}
pub fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
self.backend.get_cursor()
}
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)?;
self.last_known_cursor_pos = (x, y);
Ok(())
}
pub fn clear(&mut self) -> io::Result<()> {
match self.viewport {
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
self.backend
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
Viewport::Fixed(area) => {
for row in area.top()..area.bottom() {
self.backend.set_cursor(0, row)?;
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) -> io::Result<Rect> {
self.backend.size()
}
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
where
F: FnOnce(&mut Buffer),
{
if !matches!(self.viewport, Viewport::Inline(_)) {
return Ok(());
}
self.clear()?;
let viewport_at_bottom = self.last_known_size.bottom() - self.viewport_area.height;
self.set_viewport_area(Rect {
y: self
.viewport_area
.y
.saturating_add(height)
.min(viewport_at_bottom),
..self.viewport_area
});
let area = Rect {
x: self.viewport_area.left(),
y: 0,
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
let max_chunk_size = (self.viewport_area.top() * area.width).into();
for buffer_content_chunk in buffer.content.chunks(max_chunk_size) {
let chunk_size = buffer_content_chunk.len() as u16 / area.width;
self.backend
.append_lines(self.viewport_area.height.saturating_sub(1) + chunk_size)?;
let iter = buffer_content_chunk.iter().enumerate().map(|(i, c)| {
let (x, y) = buffer.pos_of(i);
(
x,
self.viewport_area.top().saturating_sub(chunk_size) + y,
c,
)
});
self.backend.draw(iter)?;
self.backend.flush()?;
self.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
}
Ok(())
}
}
fn compute_inline_size<B: Backend>(
backend: &mut B,
height: u16,
size: Rect,
offset_in_previous_viewport: u16,
) -> io::Result<(Rect, (u16, u16))> {
let pos = backend.get_cursor()?;
let mut row = pos.1;
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,
))
}