use std::io::{self, Read, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalMode {
Raw,
Cooked,
Cbreak,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerminalSize {
pub cols: u16,
pub rows: u16,
}
impl Default for TerminalSize {
fn default() -> Self {
Self { cols: 80, rows: 24 }
}
}
impl TerminalSize {
#[must_use]
pub const fn new(cols: u16, rows: u16) -> Self {
Self { cols, rows }
}
}
#[derive(Debug, Clone)]
pub struct TerminalState {
pub mode: TerminalMode,
pub echo: bool,
pub canonical: bool,
}
impl Default for TerminalState {
fn default() -> Self {
Self {
mode: TerminalMode::Cooked,
echo: true,
canonical: true,
}
}
}
pub struct Terminal {
running: Arc<AtomicBool>,
mode: TerminalMode,
saved_state: Option<TerminalState>,
}
impl Terminal {
#[must_use]
pub fn new() -> Self {
Self {
running: Arc::new(AtomicBool::new(false)),
mode: TerminalMode::Cooked,
saved_state: None,
}
}
#[must_use]
pub fn is_running(&self) -> bool {
self.running.load(Ordering::SeqCst)
}
pub fn set_running(&self, running: bool) {
self.running.store(running, Ordering::SeqCst);
}
#[must_use]
pub fn running_flag(&self) -> Arc<AtomicBool> {
Arc::clone(&self.running)
}
#[must_use]
pub const fn mode(&self) -> TerminalMode {
self.mode
}
pub const fn set_mode(&mut self, mode: TerminalMode) {
self.mode = mode;
}
pub const fn save_state(&mut self) {
self.saved_state = Some(TerminalState {
mode: self.mode,
echo: true,
canonical: matches!(self.mode, TerminalMode::Cooked),
});
}
pub const fn restore_state(&mut self) {
if let Some(state) = self.saved_state.take() {
self.mode = state.mode;
}
}
pub fn size() -> io::Result<TerminalSize> {
let cols = std::env::var("COLUMNS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(80);
let rows = std::env::var("LINES")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(24);
Ok(TerminalSize::new(cols, rows))
}
#[must_use]
#[allow(unsafe_code)]
pub fn is_tty() -> bool {
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
unsafe { libc::isatty(std::io::stdin().as_raw_fd()) != 0 }
}
#[cfg(not(unix))]
{
false
}
}
}
impl Default for Terminal {
fn default() -> Self {
Self::new()
}
}
pub fn read_with_timeout(timeout_ms: u64) -> io::Result<Option<u8>> {
use std::time::{Duration, Instant};
let deadline = Instant::now() + Duration::from_millis(timeout_ms);
loop {
let mut buf = [0u8; 1];
match io::stdin().read(&mut buf) {
Ok(0) => return Ok(None),
Ok(_) => return Ok(Some(buf[0])),
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
if Instant::now() >= deadline {
return Ok(None);
}
std::thread::sleep(Duration::from_millis(10));
}
Err(e) => return Err(e),
}
}
}
pub fn write_immediate(data: &[u8]) -> io::Result<()> {
let mut stdout = io::stdout();
stdout.write_all(data)?;
stdout.flush()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn terminal_default() {
let term = Terminal::new();
assert!(!term.is_running());
assert_eq!(term.mode(), TerminalMode::Cooked);
}
#[test]
fn terminal_size_default() {
let size = TerminalSize::default();
assert_eq!(size.cols, 80);
assert_eq!(size.rows, 24);
}
#[test]
fn terminal_running_flag() {
let term = Terminal::new();
let flag = term.running_flag();
assert!(!term.is_running());
flag.store(true, Ordering::SeqCst);
assert!(term.is_running());
}
}