use std::cmp::Ordering;
#[derive(Debug, Copy, Clone)]
pub struct PixelSize {
pub x: u32,
pub y: u32,
}
impl PixelSize {
pub fn from_xy((x, y): (u32, u32)) -> Self {
Self { x, y }
}
}
impl PartialEq for PixelSize {
fn eq(&self, other: &Self) -> bool {
matches!(self.partial_cmp(other), Some(Ordering::Equal))
}
}
impl PartialOrd for PixelSize {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.x == other.x && self.y == other.y {
Some(Ordering::Equal)
} else if self.x < other.x && self.y < other.y {
Some(Ordering::Less)
} else if self.x > other.x && self.y > other.y {
Some(Ordering::Greater)
} else {
None
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct TerminalSize {
pub columns: u16,
pub rows: u16,
pub pixels: Option<PixelSize>,
pub cell: Option<PixelSize>,
}
impl Default for TerminalSize {
fn default() -> Self {
TerminalSize {
columns: 80,
rows: 24,
pixels: None,
cell: None,
}
}
}
#[cfg(unix)]
mod implementation {
use rustix::termios::{tcgetwinsize, Winsize};
use tracing::{event, Level};
use crate::TerminalSize;
use std::fs::File;
use std::io::Result;
use std::path::Path;
use super::PixelSize;
fn ctermid() -> &'static Path {
Path::new("/dev/tty")
}
fn from_cterm() -> Result<Winsize> {
let tty = File::open(ctermid())?;
tcgetwinsize(&tty).map_err(Into::into)
}
pub fn from_terminal() -> Option<TerminalSize> {
let winsize = from_cterm()
.map_err(|error| {
event!(
Level::ERROR,
"Failed to read terminal size from controlling terminal: {}",
error
);
error
})
.ok()?;
if winsize.ws_row == 0 || winsize.ws_col == 0 {
event!(
Level::WARN,
"Invalid terminal size returned, columns or rows were 0: {:?}",
winsize
);
None
} else {
let mut terminal_size = TerminalSize {
columns: winsize.ws_col,
rows: winsize.ws_row,
pixels: None,
cell: None,
};
if winsize.ws_xpixel != 0 && winsize.ws_ypixel != 0 {
let pixels = PixelSize {
x: winsize.ws_xpixel as u32,
y: winsize.ws_ypixel as u32,
};
terminal_size.pixels = Some(pixels);
terminal_size.cell = Some(PixelSize {
x: pixels.x / terminal_size.columns as u32,
y: pixels.y / terminal_size.rows as u32,
});
};
Some(terminal_size)
}
}
}
#[cfg(windows)]
mod implementation {
use terminal_size::{terminal_size, Height, Width};
use super::TerminalSize;
pub fn from_terminal() -> Option<TerminalSize> {
terminal_size().map(|(Width(columns), Height(rows))| TerminalSize {
rows,
columns,
pixels: None,
cell: None,
})
}
}
impl TerminalSize {
pub fn from_env() -> Option<Self> {
let columns = std::env::var("COLUMNS")
.ok()
.and_then(|value| value.parse::<u16>().ok());
let rows = std::env::var("LINES")
.ok()
.and_then(|value| value.parse::<u16>().ok());
match (columns, rows) {
(Some(columns), Some(rows)) => Some(Self {
columns,
rows,
pixels: None,
cell: None,
}),
_ => None,
}
}
pub fn from_terminal() -> Option<Self> {
implementation::from_terminal()
}
pub fn detect() -> Option<Self> {
Self::from_terminal().or_else(Self::from_env)
}
pub fn with_max_columns(&self, max_columns: u16) -> Self {
let pixels = match (self.pixels, self.cell) {
(Some(pixels), Some(cell)) => Some(PixelSize {
x: cell.x * max_columns as u32,
y: pixels.y,
}),
_ => None,
};
Self {
columns: max_columns,
rows: self.rows,
pixels,
cell: self.cell,
}
}
}