460 lines
14 KiB
Rust
460 lines
14 KiB
Rust
use crate::{Error, theme};
|
|
|
|
pub use crossterm::{
|
|
cursor::SetCursorStyle as CursorStyle, event::Event as TerminalEvent, style::Color,
|
|
};
|
|
|
|
use crossterm::{
|
|
ExecutableCommand, QueueableCommand, SynchronizedUpdate, cursor, event, style, terminal,
|
|
};
|
|
use std::{
|
|
borrow::Borrow,
|
|
io::{self, StdoutLock, Write as _},
|
|
panic,
|
|
time::Duration,
|
|
};
|
|
|
|
#[derive(Copy, Clone, PartialEq)]
|
|
struct Cell {
|
|
c: char,
|
|
fg: Color,
|
|
bg: Color,
|
|
}
|
|
|
|
impl Default for Cell {
|
|
fn default() -> Self {
|
|
Self {
|
|
c: ' ',
|
|
fg: Color::Reset,
|
|
bg: Color::Reset,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents an area of the terminal window
|
|
#[derive(Copy, Clone, Default)]
|
|
pub struct Area {
|
|
origin: [u16; 2],
|
|
size: [u16; 2],
|
|
}
|
|
|
|
impl Area {
|
|
pub fn size(&self) -> [usize; 2] {
|
|
self.size.map(|e| e as usize)
|
|
}
|
|
|
|
pub fn contains(&self, pos: [isize; 2]) -> Option<[isize; 2]> {
|
|
if (self.origin[0] as isize..self.origin[0] as isize + self.size[0] as isize)
|
|
.contains(&pos[0])
|
|
&& (self.origin[1] as isize..self.origin[1] as isize + self.size[1] as isize)
|
|
.contains(&pos[1])
|
|
{
|
|
Some([
|
|
pos[0] - self.origin[0] as isize,
|
|
pos[1] - self.origin[1] as isize,
|
|
])
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Rect<'a> {
|
|
pub fg: Color,
|
|
pub bg: Color,
|
|
area: Area,
|
|
fb: &'a mut Framebuffer,
|
|
has_focus: bool,
|
|
}
|
|
|
|
impl<'a> Rect<'a> {
|
|
fn get_mut(&mut self, pos: [usize; 2]) -> Option<&mut Cell> {
|
|
if pos[0] < self.size()[0] && pos[1] < self.size()[1] {
|
|
let offs = [
|
|
self.area.origin[0] as usize + pos[0],
|
|
self.area.origin[1] as usize + pos[1],
|
|
];
|
|
Some(&mut self.fb.cells[offs[1] * self.fb.size[0] as usize + offs[0]])
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn with<R>(&mut self, f: impl FnOnce(&mut Rect<'_>) -> R) -> R {
|
|
f(self)
|
|
}
|
|
|
|
pub fn rect(&mut self, origin: [usize; 2], size: [usize; 2]) -> Rect<'_> {
|
|
Rect {
|
|
area: Area {
|
|
origin: [
|
|
self.area.origin[0] + origin[0] as u16,
|
|
self.area.origin[1] + origin[1] as u16,
|
|
],
|
|
size: [
|
|
size[0].min((self.area.size[0] as usize).saturating_sub(origin[0])) as u16,
|
|
size[1].min((self.area.size[1] as usize).saturating_sub(origin[1])) as u16,
|
|
],
|
|
},
|
|
fg: self.fg,
|
|
bg: self.bg,
|
|
fb: self.fb,
|
|
has_focus: self.has_focus,
|
|
}
|
|
}
|
|
|
|
pub fn with_border(&mut self, theme: &theme::BorderTheme, title: Option<&str>) -> Rect<'_> {
|
|
let edge = self.size().map(|e| e.saturating_sub(1));
|
|
for col in 0..edge[0] {
|
|
self.get_mut([col, 0]).map(|c| {
|
|
c.c = theme.top;
|
|
c.fg = theme.fg;
|
|
});
|
|
self.get_mut([col, edge[1]]).map(|c| {
|
|
c.c = theme.bottom;
|
|
c.fg = theme.fg;
|
|
});
|
|
}
|
|
for row in 0..edge[1] {
|
|
self.get_mut([0, row]).map(|c| {
|
|
c.c = theme.left;
|
|
c.fg = theme.fg;
|
|
});
|
|
self.get_mut([edge[0], row]).map(|c| {
|
|
c.c = theme.right;
|
|
c.fg = theme.fg;
|
|
});
|
|
}
|
|
self.get_mut([0, 0]).map(|c| {
|
|
c.c = theme.top_left;
|
|
c.fg = theme.fg;
|
|
});
|
|
self.get_mut([edge[0], 0]).map(|c| {
|
|
c.c = theme.top_right;
|
|
c.fg = theme.fg;
|
|
});
|
|
self.get_mut([0, edge[1]]).map(|c| {
|
|
c.c = theme.bottom_left;
|
|
c.fg = theme.fg;
|
|
});
|
|
self.get_mut([edge[0], edge[1]]).map(|c| {
|
|
c.c = theme.bottom_right;
|
|
c.fg = theme.fg;
|
|
});
|
|
if let Some(title) = title {
|
|
for (i, c) in [theme.join_right, ' ']
|
|
.into_iter()
|
|
.chain(title.chars())
|
|
.chain([' ', theme.join_left])
|
|
.enumerate()
|
|
{
|
|
self.get_mut([2 + i, 0]).map(|cell| {
|
|
cell.fg = theme.fg;
|
|
cell.c = c
|
|
});
|
|
}
|
|
}
|
|
self.rect([1, 1], self.size().map(|e| e.saturating_sub(2)))
|
|
}
|
|
|
|
pub fn with_fg(&mut self, fg: Color) -> Rect<'_> {
|
|
Rect {
|
|
fg,
|
|
bg: self.bg,
|
|
area: self.area,
|
|
fb: self.fb,
|
|
has_focus: self.has_focus,
|
|
}
|
|
}
|
|
|
|
pub fn with_bg(&mut self, bg: Color) -> Rect<'_> {
|
|
Rect {
|
|
fg: self.fg,
|
|
bg,
|
|
area: self.area,
|
|
fb: self.fb,
|
|
has_focus: self.has_focus,
|
|
}
|
|
}
|
|
|
|
/// `with_bg`, but only if background color is not already set.
|
|
pub fn with_bg_preference(&mut self, bg: Color) -> Rect<'_> {
|
|
self.with_bg(if self.bg == Color::Reset { bg } else { self.bg })
|
|
}
|
|
|
|
pub fn with_focus(&mut self, focus: bool) -> Rect<'_> {
|
|
Rect {
|
|
fg: self.fg,
|
|
bg: self.bg,
|
|
area: self.area,
|
|
fb: self.fb,
|
|
has_focus: self.has_focus && focus,
|
|
}
|
|
}
|
|
|
|
pub fn has_focus(&self) -> bool {
|
|
self.has_focus
|
|
}
|
|
|
|
pub fn area(&self) -> Area {
|
|
self.area
|
|
}
|
|
|
|
pub fn size(&self) -> [usize; 2] {
|
|
self.area.size.map(|e| e as usize)
|
|
}
|
|
|
|
pub fn fill(&mut self, c: char) -> Rect<'_> {
|
|
for row in 0..self.size()[1] {
|
|
for col in 0..self.size()[0] {
|
|
let cell = Cell {
|
|
c,
|
|
fg: self.fg,
|
|
bg: self.bg,
|
|
};
|
|
if let Some(c) = self.get_mut([col, row]) {
|
|
*c = cell;
|
|
}
|
|
}
|
|
}
|
|
self.rect([0, 0], self.size())
|
|
}
|
|
|
|
pub fn text(&mut self, origin: [isize; 2], text: &str) -> Rect<'_> {
|
|
for (idx, c) in text.chars().enumerate() {
|
|
if (0..self.size()[0] as isize).contains(&(origin[0] + idx as isize)) && origin[1] >= 0
|
|
{
|
|
let cell = Cell {
|
|
c: *c.borrow(),
|
|
fg: self.fg,
|
|
bg: self.bg,
|
|
};
|
|
if let Some(c) =
|
|
self.get_mut([(origin[0] + idx as isize) as usize, origin[1] as usize])
|
|
{
|
|
*c = cell;
|
|
}
|
|
}
|
|
}
|
|
self.rect([0, 0], self.size())
|
|
}
|
|
|
|
pub fn set_cursor(&mut self, cursor: [isize; 2], style: CursorStyle) -> Rect<'_> {
|
|
if self.has_focus
|
|
&& (0..=self.size()[0] as isize).contains(&cursor[0])
|
|
&& (0..self.size()[1] as isize).contains(&cursor[1])
|
|
{
|
|
self.fb.cursor = Some((
|
|
[
|
|
self.area.origin[0] + cursor[0] as u16,
|
|
self.area.origin[1] + cursor[1] as u16,
|
|
],
|
|
style,
|
|
));
|
|
}
|
|
self.rect([0, 0], self.size())
|
|
}
|
|
|
|
pub fn set_title(&mut self, title: String) {
|
|
self.fb.title = title;
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct Framebuffer {
|
|
size: [u16; 2],
|
|
cells: Vec<Cell>,
|
|
cursor: Option<([u16; 2], CursorStyle)>,
|
|
title: String,
|
|
}
|
|
|
|
impl Framebuffer {
|
|
pub fn rect(&mut self) -> Rect<'_> {
|
|
Rect {
|
|
fg: Color::Reset,
|
|
bg: Color::Reset,
|
|
area: Area {
|
|
origin: [0, 0],
|
|
size: self.size,
|
|
},
|
|
fb: self,
|
|
has_focus: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Terminal<'a> {
|
|
stdout: StdoutLock<'a>,
|
|
size: [u16; 2],
|
|
fb: [Framebuffer; 2],
|
|
bell: bool,
|
|
}
|
|
|
|
impl<'a> Terminal<'a> {
|
|
fn enter(mut stdout: impl io::Write) {
|
|
let _ = terminal::enable_raw_mode();
|
|
let _ = stdout.execute(terminal::EnterAlternateScreen);
|
|
let _ = stdout.execute(terminal::DisableLineWrap);
|
|
let _ = stdout.execute(event::EnableMouseCapture);
|
|
}
|
|
|
|
fn leave(mut stdout: impl io::Write) {
|
|
let _ = terminal::disable_raw_mode();
|
|
let _ = stdout.execute(terminal::LeaveAlternateScreen);
|
|
let _ = stdout.execute(terminal::EnableLineWrap);
|
|
let _ = stdout.execute(cursor::Show);
|
|
let _ = stdout.execute(event::DisableMouseCapture);
|
|
}
|
|
|
|
pub fn with<T>(
|
|
f: impl FnOnce(&mut Self) -> Result<T, Error> + panic::UnwindSafe,
|
|
) -> Result<T, Error> {
|
|
let size = terminal::window_size()?;
|
|
|
|
Self::enter(io::stdout().lock());
|
|
|
|
let mut this = Self {
|
|
stdout: io::stdout().lock(),
|
|
size: [size.columns, size.rows],
|
|
fb: [Framebuffer::default(), Framebuffer::default()],
|
|
bell: false,
|
|
};
|
|
|
|
let hook = panic::take_hook();
|
|
panic::set_hook(Box::new(move |panic| {
|
|
Self::leave(io::stdout().lock());
|
|
hook(panic);
|
|
}));
|
|
let res = f(&mut this);
|
|
|
|
Self::leave(io::stdout().lock());
|
|
|
|
res
|
|
}
|
|
|
|
pub fn set_size(&mut self, size: [u16; 2]) {
|
|
self.size = size;
|
|
}
|
|
|
|
pub fn ring_bell(&mut self) {
|
|
self.bell = true;
|
|
}
|
|
|
|
pub fn update(&mut self, render: impl FnOnce(&mut Rect)) {
|
|
// Reset framebuffer
|
|
if self.fb[0].size != self.size {
|
|
self.fb[0].size = self.size;
|
|
self.fb[0].cells.resize(
|
|
self.size[0] as usize * self.size[1] as usize,
|
|
Cell::default(),
|
|
);
|
|
}
|
|
self.fb[0].cursor = None;
|
|
|
|
render(&mut self.fb[0].rect());
|
|
|
|
self.stdout
|
|
.sync_update(|stdout| {
|
|
if self.bell {
|
|
self.bell = false;
|
|
stdout.queue(style::Print('\x07')).unwrap();
|
|
}
|
|
|
|
if self.fb[0].title != self.fb[1].title {
|
|
stdout.queue(terminal::SetTitle(&self.fb[0].title)).unwrap();
|
|
}
|
|
|
|
let mut cursor_pos = [0, 0];
|
|
let mut fg = Color::Reset;
|
|
let mut bg = Color::Reset;
|
|
stdout
|
|
.queue(cursor::MoveTo(cursor_pos[0], cursor_pos[1]))
|
|
.unwrap()
|
|
.queue(style::SetForegroundColor(fg))
|
|
.unwrap()
|
|
.queue(style::SetBackgroundColor(bg))
|
|
.unwrap()
|
|
.queue(cursor::Hide)
|
|
.unwrap();
|
|
|
|
// Write out changes
|
|
for row in 0..self.size[1] {
|
|
for col in 0..self.size[0] {
|
|
let pos = row as usize * self.size[0] as usize + col as usize;
|
|
let cell = self.fb[0].cells[pos];
|
|
|
|
let changed =
|
|
self.fb[0].size != self.fb[1].size || cell != self.fb[1].cells[pos];
|
|
|
|
if changed {
|
|
if cursor_pos != [col, row] {
|
|
// Minimise the work done to move the cursor around
|
|
if cursor_pos[1] == row {
|
|
stdout.queue(cursor::MoveToColumn(col)).unwrap();
|
|
} else if cursor_pos[0] == col {
|
|
stdout.queue(cursor::MoveToRow(row)).unwrap();
|
|
} else {
|
|
stdout.queue(cursor::MoveTo(col, row)).unwrap();
|
|
}
|
|
cursor_pos = [col, row];
|
|
}
|
|
if fg != cell.fg {
|
|
fg = cell.fg;
|
|
stdout.queue(style::SetForegroundColor(fg)).unwrap();
|
|
}
|
|
if bg != cell.bg {
|
|
bg = cell.bg;
|
|
stdout.queue(style::SetBackgroundColor(bg)).unwrap();
|
|
}
|
|
|
|
// Convert non-printable chars
|
|
let c = match self.fb[0].cells[pos].c {
|
|
c if c.is_whitespace() => ' ',
|
|
c if c.is_control() => {
|
|
char::from_u32(9216 + c as u32).unwrap_or('?')
|
|
}
|
|
c => c,
|
|
};
|
|
stdout.queue(style::Print(c)).unwrap();
|
|
|
|
// Move cursor
|
|
cursor_pos[0] +=
|
|
unicode_display_width::width(c.encode_utf8(&mut [0; 4])) as u16;
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(([col, row], style)) = self.fb[0].cursor {
|
|
stdout
|
|
.queue(cursor::MoveTo(col, row))
|
|
.unwrap()
|
|
.queue(style)
|
|
.unwrap()
|
|
.queue(cursor::Show)
|
|
.unwrap();
|
|
} else {
|
|
stdout.queue(cursor::Hide).unwrap();
|
|
}
|
|
})
|
|
.unwrap();
|
|
|
|
self.stdout.flush().unwrap();
|
|
|
|
// Switch front and back buffers
|
|
self.fb.swap(0, 1);
|
|
}
|
|
|
|
// Get the next pending event, if one is available.
|
|
pub fn get_event(&mut self) -> Option<TerminalEvent> {
|
|
if event::poll(Duration::ZERO).ok()? {
|
|
event::read().ok()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
// Wait for the given duration or until an event arrives.
|
|
pub fn wait_at_least(&mut self, dur: Duration) {
|
|
event::poll(dur).unwrap();
|
|
}
|
|
}
|