use base::{Height, Style, Width, Window, WindowBuffer};
use ndarray::Axis;
use raw_tty::TtyWithGuard;
use std::io;
use std::io::{StdoutLock, Write};
use std::os::unix::io::AsRawFd;
use termion;
use nix::sys::signal::{killpg, pthread_sigmask, SigSet, SigmaskHow, SIGCONT, SIGTSTP};
use nix::unistd::getpgrp;
pub struct Terminal<'a, T = StdoutLock<'a>>
where
T: AsRawFd + Write,
{
values: WindowBuffer,
old_values: WindowBuffer,
terminal: TtyWithGuard<T>,
size_has_changed_since_last_present: bool,
bell_to_emit: bool,
_phantom: ::std::marker::PhantomData<&'a ()>,
}
impl<'a, T: Write + AsRawFd> Terminal<'a, T> {
pub fn new(sink: T) -> io::Result<Self> {
let mut terminal = TtyWithGuard::new(sink)?;
terminal.set_raw_mode()?;
let mut term = Terminal {
values: WindowBuffer::new(Width::new(0).unwrap(), Height::new(0).unwrap()),
old_values: WindowBuffer::new(Width::new(0).unwrap(), Height::new(0).unwrap()),
terminal,
size_has_changed_since_last_present: true,
bell_to_emit: false,
_phantom: Default::default(),
};
term.enter_tui()?;
Ok(term)
}
pub fn handle_sigtstp(&mut self) -> io::Result<()> {
self.leave_tui()?;
let mut stop_and_cont = SigSet::empty();
stop_and_cont.add(SIGCONT);
stop_and_cont.add(SIGTSTP);
pthread_sigmask(SigmaskHow::SIG_UNBLOCK, Some(&stop_and_cont), None)?;
killpg(getpgrp(), SIGTSTP)?;
pthread_sigmask(SigmaskHow::SIG_BLOCK, Some(&stop_and_cont), None)?;
self.enter_tui()
}
fn enter_tui(&mut self) -> io::Result<()> {
write!(
self.terminal,
"{}{}",
termion::screen::ToAlternateScreen,
termion::cursor::Hide
)?;
self.terminal.set_raw_mode()?;
self.terminal.flush()?;
Ok(())
}
fn leave_tui(&mut self) -> io::Result<()> {
write!(
self.terminal,
"{}{}",
termion::screen::ToMainScreen,
termion::cursor::Show
)?;
self.terminal.modify_mode(|m| m)?; self.terminal.flush()?;
Ok(())
}
pub fn on_main_screen<R, F: FnOnce() -> R>(&mut self, f: F) -> io::Result<R> {
self.leave_tui()?;
let res = f();
self.enter_tui()?;
Ok(res)
}
pub fn create_root_window(&mut self) -> Window {
let (x, y) = termion::terminal_size().expect("get terminal size");
let x = Width::new(x as i32).unwrap();
let y = Height::new(y as i32).unwrap();
if x != self.values.as_window().get_width() || y != self.values.as_window().get_height() {
self.size_has_changed_since_last_present = true;
self.values = WindowBuffer::new(x, y);
} else {
self.values.as_window().clear();
}
self.values.as_window()
}
pub fn emit_bell(&mut self) {
self.bell_to_emit = true;
}
pub fn present(&mut self) {
let mut current_style = Style::default();
let mut num_potentially_unchanged_lines = self.old_values.storage().dim().0;
if self.size_has_changed_since_last_present {
write!(self.terminal, "{}", termion::clear::All).expect("clear");
self.size_has_changed_since_last_present = false;
num_potentially_unchanged_lines = 0;
}
if self.bell_to_emit {
write!(self.terminal, "\x07").expect("emit bell");
self.bell_to_emit = false;
}
for (y, line) in self.values.storage().axis_iter(Axis(0)).enumerate() {
if y < num_potentially_unchanged_lines
&& self.old_values.storage().subview(Axis(0), y) == line
{
continue;
}
write!(
self.terminal,
"{}",
termion::cursor::Goto(1, (y + 1) as u16)
)
.expect("move cursor");
let mut buffer = String::with_capacity(line.len());
for c in line.iter() {
if c.style != current_style {
current_style.set_terminal_attributes(&mut self.terminal);
write!(self.terminal, "{}", buffer).expect("write buffer");
buffer.clear();
current_style = c.style;
}
let grapheme_cluster = match c.grapheme_cluster.as_str() {
c @ "\t" | c @ "\n" | c @ "\r" | c @ "\0" => {
panic!("Invalid grapheme cluster written to terminal: {:?}", c)
}
x => x,
};
buffer.push_str(grapheme_cluster);
}
current_style.set_terminal_attributes(&mut self.terminal);
write!(self.terminal, "{}", buffer).expect("write leftover buffer contents");
}
let _ = self.terminal.flush();
self.old_values = self.values.clone();
}
}
impl<'a, T: Write + AsRawFd> Drop for Terminal<'a, T> {
fn drop(&mut self) {
let _ = self.leave_tui();
}
}
pub mod test {
use super::super::{
GraphemeCluster, Height, Style, StyleModifier, StyledGraphemeCluster, Width, Window,
WindowBuffer,
};
#[derive(PartialEq)]
pub struct FakeTerminal {
values: WindowBuffer,
}
impl FakeTerminal {
pub fn with_size((w, h): (u32, u32)) -> Self {
FakeTerminal {
values: WindowBuffer::new(
Width::new(w as i32).unwrap(),
Height::new(h as i32).unwrap(),
),
}
}
pub fn from_str(
(w, h): (u32, u32),
description: &str,
) -> Result<Self, ::ndarray::ShapeError> {
let mut tiles = Vec::<StyledGraphemeCluster>::new();
let mut style = Style::plain();
for c in GraphemeCluster::all_from_str(description) {
if c.as_str() == "*" {
style = StyleModifier::new()
.bold(crate::base::BoolModifyMode::Toggle)
.apply(style);
continue;
}
if c.as_str() == " " || c.as_str() == "\n" {
continue;
}
tiles.push(StyledGraphemeCluster::new(c, style));
}
Ok(FakeTerminal {
values: WindowBuffer::from_storage(::ndarray::Array2::from_shape_vec(
(h as usize, w as usize),
tiles,
)?),
})
}
pub fn assert_looks_like(&self, string_description: &str) {
assert_eq!(format!("{:?}", self), string_description);
}
pub fn create_root_window(&mut self) -> Window {
self.values.as_window()
}
}
impl ::std::fmt::Debug for FakeTerminal {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
let raw_values = self.values.storage();
for r in 0..raw_values.dim().0 {
for c in 0..raw_values.dim().1 {
let c = raw_values.get((r, c)).expect("debug: in bounds");
if c.style.format().bold {
write!(f, "*{}*", c.grapheme_cluster.as_str())?;
} else {
write!(f, "{}", c.grapheme_cluster.as_str())?;
}
}
if r != raw_values.dim().0 - 1 {
write!(f, "|")?;
}
}
Ok(())
}
}
}