mod buffer;
use crate::{
color::{self, Color, Color2},
coord,
coord::{Coord, Vec2},
error::Error,
screen::buffer::ScreenBuffer,
stdio,
stdio::{restore_screen, save_screen, LockedStdout, Stdout},
string::{TermGrapheme, TermString},
style::Style,
terminal::Shared,
tile::{self, Tile},
};
use std::{
fmt::Write,
sync::atomic::{AtomicBool, Ordering::*},
time::Duration,
};
use tokio::{
io,
sync::{Mutex, MutexGuard, Notify},
task,
time,
};
#[derive(Debug)]
pub(crate) struct ScreenData {
min_size: Vec2,
frame_time: Duration,
cleanedup: AtomicBool,
stdout: Stdout,
buffer: Mutex<ScreenBuffer>,
notifier: Notify,
}
impl ScreenData {
pub fn new(size: Vec2, min_size: Vec2, frame_time: Duration) -> Self {
let corrected_size = if size.x >= min_size.x && size.y >= min_size.y {
size
} else {
min_size
};
Self {
min_size,
frame_time,
cleanedup: AtomicBool::new(false),
stdout: Stdout::new(),
buffer: Mutex::new(ScreenBuffer::blank(corrected_size)),
notifier: Notify::new(),
}
}
pub fn notify(&self) {
self.notifier.notify_waiters()
}
async fn subscribe(&self) {
self.notifier.notified().await
}
pub async fn lock<'this>(&'this self) -> Screen<'this> {
Screen::new(self).await
}
pub async fn setup(&self) -> Result<(), Error> {
let mut buf = String::new();
save_screen(&mut buf)?;
write!(
buf,
"{}{}{}{}",
crossterm::style::SetBackgroundColor(
crossterm::style::Color::Black
),
crossterm::style::SetForegroundColor(
crossterm::style::Color::White
),
crossterm::cursor::Hide,
crossterm::terminal::Clear(crossterm::terminal::ClearType::All),
)?;
self.stdout.write_and_flush(buf.as_bytes()).await?;
Ok(())
}
pub async fn cleanup(&self) -> Result<(), Error> {
task::block_in_place(|| crossterm::terminal::disable_raw_mode())?;
let mut buf = String::new();
write!(buf, "{}", crossterm::cursor::Show)?;
restore_screen(&mut buf)?;
self.stdout.write_and_flush(buf.as_bytes()).await?;
self.cleanedup.store(true, Release);
Ok(())
}
}
impl Drop for ScreenData {
fn drop(&mut self) {
if !self.cleanedup.load(Relaxed) {
let _ = crossterm::terminal::disable_raw_mode();
let mut buf = String::new();
write!(buf, "{}", crossterm::cursor::Show)
.ok()
.and_then(|_| stdio::restore_screen(&mut buf).ok())
.map(|_| println!("{}", buf));
}
}
}
#[cold]
#[inline(never)]
fn out_of_bounds(point: Vec2, size: Vec2) -> ! {
panic!(
"Point x: {}, y: {} out of screen size x: {}, y: {}",
point.x, point.y, size.x, size.y
)
}
#[derive(Debug)]
pub struct Screen<'terminal> {
data: &'terminal ScreenData,
buffer: MutexGuard<'terminal, ScreenBuffer>,
}
impl<'terminal> Screen<'terminal> {
pub(crate) async fn new<'param>(
data: &'param ScreenData,
) -> Screen<'terminal>
where
'param: 'terminal,
{
Self { data, buffer: data.buffer.lock().await }
}
pub fn size(&self) -> Vec2 {
self.buffer.size()
}
pub fn valid_size(&self) -> bool {
self.buffer.valid
}
pub fn min_size(&self) -> Vec2 {
self.data.min_size
}
pub fn set<T>(&mut self, point: Vec2, updater: T)
where
T: tile::Updater,
{
let index = self
.buffer
.make_index(point)
.unwrap_or_else(|| out_of_bounds(point, self.buffer.size()));
updater.update(&mut self.buffer.curr[index]);
if self.buffer.old[index] != self.buffer.curr[index] {
self.buffer.changed.insert(point);
} else {
self.buffer.changed.remove(&point);
}
}
pub fn get(&self, point: Vec2) -> &Tile {
let index = self
.buffer
.make_index(point)
.unwrap_or_else(|| out_of_bounds(point, self.buffer.size()));
&self.buffer.curr[index]
}
pub fn clear(&mut self, background: Color) {
let size = self.buffer.size();
let tile = Tile {
colors: Color2 { background, ..Color2::default() },
grapheme: TermGrapheme::space(),
};
for y in 0 .. size.y {
for x in 0 .. size.x {
self.set(Vec2 { x, y }, tile.clone());
}
}
}
pub fn styled_text<C>(
&mut self,
tstring: &TermString,
style: Style<C>,
) -> Coord
where
C: color::Updater,
{
let mut len = tstring.count_graphemes();
let mut slice = tstring.index(..);
let screen_size = self.buffer.size();
let size = style.make_size(screen_size);
let mut cursor = Vec2 { x: 0, y: style.top_margin };
let mut is_inside = cursor.y - style.top_margin < size.y;
while len > 0 && is_inside {
is_inside = cursor.y - style.top_margin + 1 < size.y;
let width = coord::to_index(size.x);
let pos = self.find_break_pos(width, len, size, &slice, is_inside);
cursor.x = size.x - coord::from_index(pos);
cursor.x = cursor.x + style.left_margin - style.right_margin;
cursor.x = cursor.x * style.align_numer / style.align_denom;
slice = slice.index(.. pos);
self.write_styled_slice(&slice, &style, &mut cursor);
if pos != len && !is_inside {
self.set(cursor, |tile: &mut Tile| {
let grapheme = TermGrapheme::new_lossy("…");
let colors = style.colors.update(tile.colors);
*tile = Tile { grapheme, colors };
});
}
cursor.y += 1;
len -= pos;
}
cursor.y
}
fn find_break_pos(
&self,
width: usize,
total_graphemes: usize,
term_size: Vec2,
slice: &TermString,
is_inside: bool,
) -> usize {
if width <= slice.len() {
let mut pos = slice
.index(.. coord::to_index(term_size.x))
.iter()
.rev()
.position(|grapheme| grapheme == TermGrapheme::space())
.map_or(width, |rev| total_graphemes - rev);
if !is_inside {
pos -= 1;
}
pos
} else {
total_graphemes
}
}
fn write_styled_slice<C>(
&mut self,
slice: &TermString,
style: &Style<C>,
cursor: &mut Vec2,
) where
C: color::Updater,
{
for grapheme in slice {
self.set(*cursor, |tile: &mut Tile| {
tile.grapheme = grapheme;
tile.colors = style.colors.update(tile.colors);
});
cursor.x += 1;
}
}
pub(crate) async fn check_resize(
&mut self,
new_size: Vec2,
guard: &mut Option<LockedStdout<'terminal>>,
) -> io::Result<()> {
let min_size = self.data.min_size;
if new_size.x < min_size.x || new_size.y < min_size.y {
if guard.is_none() {
self.buffer.valid = false;
let mut stdout = self.data.stdout.lock().await;
self.ask_resize(&mut stdout, min_size).await?;
*guard = Some(stdout);
}
} else {
let mut stdout = match guard.take() {
Some(stdout) => stdout,
None => self.data.stdout.lock().await,
};
self.buffer.valid = true;
self.resize(new_size, &mut stdout).await?;
}
Ok(())
}
async fn ask_resize(
&mut self,
stdout: &mut LockedStdout<'terminal>,
min_size: Vec2,
) -> io::Result<()> {
let buf = format!(
"{}{}RESIZE {}x{}",
crossterm::terminal::Clear(crossterm::terminal::ClearType::All),
crossterm::cursor::MoveTo(0, 0),
min_size.x,
min_size.y,
);
stdout.write_and_flush(buf.as_bytes()).await?;
Ok(())
}
async fn resize(
&mut self,
new_size: Vec2,
stdout: &mut LockedStdout<'terminal>,
) -> io::Result<()> {
let buf = format!(
"{}{}{}",
crossterm::style::SetForegroundColor(
crossterm::style::Color::White
),
crossterm::style::SetBackgroundColor(
crossterm::style::Color::Black
),
crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
);
stdout.write_and_flush(buf.as_bytes()).await?;
self.buffer.resize(new_size);
Ok(())
}
pub(crate) async fn render(
&mut self,
buf: &mut String,
) -> Result<(), Error> {
let screen_size = self.buffer.size();
buf.clear();
let mut colors = Color2::default();
let mut cursor = Vec2 { x: 0, y: 0 };
self.render_init_term(buf, colors, cursor)?;
for &coord in self.buffer.changed.iter() {
self.render_tile(
buf,
&mut colors,
&mut cursor,
screen_size,
coord,
)?;
}
if let Some(mut stdout) = self.data.stdout.try_lock() {
stdout.write_and_flush(buf.as_bytes()).await?;
}
self.buffer.next_tick();
Ok(())
}
fn render_init_term(
&self,
buf: &mut String,
colors: Color2,
cursor: Vec2,
) -> Result<(), Error> {
write!(
buf,
"{}{}{}",
crossterm::style::SetForegroundColor(
colors.foreground.to_crossterm()
),
crossterm::style::SetBackgroundColor(
colors.background.to_crossterm()
),
crossterm::cursor::MoveTo(
coord::to_crossterm(cursor.x),
coord::to_crossterm(cursor.y)
),
)?;
Ok(())
}
fn render_tile(
&self,
buf: &mut String,
colors: &mut Color2,
cursor: &mut Vec2,
screen_size: Vec2,
coord: Vec2,
) -> Result<(), Error> {
if *cursor != coord {
write!(
buf,
"{}",
crossterm::cursor::MoveTo(
coord::to_crossterm(coord.x),
coord::to_crossterm(coord.y)
)
)?;
}
*cursor = coord;
let tile = self.get(*cursor);
if colors.background != tile.colors.background {
let color = tile.colors.background.to_crossterm();
write!(buf, "{}", crossterm::style::SetBackgroundColor(color))?;
}
if colors.foreground != tile.colors.foreground {
let color = tile.colors.foreground.to_crossterm();
write!(buf, "{}", crossterm::style::SetForegroundColor(color))?;
}
*colors = tile.colors;
write!(buf, "{}", tile.grapheme)?;
if cursor.x <= screen_size.x {
cursor.x += 1;
}
Ok(())
}
}
pub(crate) async fn renderer(shared: &Shared) -> Result<(), Error> {
let mut interval = time::interval(shared.screen().frame_time);
let mut buf = String::new();
loop {
{
let _guard = shared.service_guard().await?;
let mut screen = shared.screen().lock().await;
screen.render(&mut buf).await?;
}
tokio::select! {
_ = interval.tick() => (),
_ = shared.screen().subscribe() => break,
};
}
Ok(())
}