1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
//! # Terminal
//!
//! The `terminal` module contains declarations that interact with the terminal. It provides
//! functionality for styling text and other characters as well as implementations for drawing
//! them through a [`Canvas`] and [`Window`].
//!
//! ## Examples
//!
//! The majority of applications do not interact with either of the [`Canvas`] or the [`Window`]
//! directly, but instead draw to the terminal using a [`Pencil`](crate::drawing::Pencil).
//!
//! An example of direct use of these elements can be seen in `window.rs` from the
//! [examples](https://github.com/lemunozm/ruscii/tree/master/examples) folder of the
//! [`ruscii`](https://github.com/lemunozm/ruscii) repository:
//!
//! ```rust,no_run
//! # use ruscii::terminal::{Window, Color, VisualElement};
//! #
//! fn main() {
//! let mut window = Window::default();
//! window.open();
//! println!("This is an open window");
//! std::thread::sleep(std::time::Duration::from_secs(2));
//!
//! let mut default = VisualElement::default();
//! default.background = Color::Red;
//! window.canvas_mut().set_default_element(&default);
//! window.clear();
//! window.draw();
//! println!("With a custom background color!");
//!
//! std::thread::sleep(std::time::Duration::from_secs(2));
//! window.close();
//! }
//! ```
use std::io::{self, BufWriter, Write};
use super::spatial::Vec2;
use crossterm as ct;
/// A set of common colors and a [`Color::Xterm`] value that allows you to pass an arbitrary ANSI
/// 8-bit color using its Xterm number (compatible with Windows 10 and most UNIX terminals).
///
/// # Example
///
/// For instance, to set a [`Pencil`](crate::drawing::Pencil)'s foreground color to Xterm color
/// DarkCyan (`#00af87`), you would do something similar to the following:
///
/// ```rust,no_run
/// # use ruscii::app::{App, State};
/// # use ruscii::drawing::Pencil;
/// # use ruscii::terminal::{Color, Window};
/// #
/// # let mut app = App::default();
/// #
/// # app.run(|app_state: &mut State, window: &mut Window| {
/// let mut pencil = Pencil::new(window.canvas_mut());
/// pencil.set_foreground(Color::Xterm(36));
/// # });
/// ```
///
/// For reference, see the
/// [256 Colors Cheat Sheet](https://www.ditig.com/256-colors-cheat-sheet).
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Color {
Black,
White,
Grey,
DarkGrey,
LightGrey,
Red,
Green,
Blue,
Cyan,
Yellow,
Magenta,
Xterm(u8),
}
impl Color {
/// Converts this [`Color`] to its corresponding Xterm number.
pub fn code(&self) -> u8 {
match *self {
Color::Black => 16,
Color::White => 231,
Color::Grey => 244,
Color::DarkGrey => 238,
Color::LightGrey => 250,
Color::Red => 196,
Color::Green => 46,
Color::Blue => 21,
Color::Cyan => 51,
Color::Yellow => 226,
Color::Magenta => 201,
Color::Xterm(code) => code,
}
}
}
/// The font weight.
///
/// Represents the boldness of text on the terminal screen.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Style {
Plain,
Bold,
}
/*
fn style_impl(style: Style) -> ct::style::Attribute {
match style {
Style::Plain => ct::style::Attribute::NoBold,
Style::Bold => ct::style::Attribute::Bold,
}
}
*/
/// Represents all the data needed to display a character on the terminal screen with text [`Style`]
/// and foreground and background [`Color`].
#[derive(Clone, Copy)]
pub struct VisualElement {
pub style: Style,
pub background: Color,
pub foreground: Color,
pub value: char,
}
impl Default for VisualElement {
/// Constructs a [`VisualElement`] with the default terminal styles.
fn default() -> Self {
Self {
style: Style::Plain,
background: Color::Black,
foreground: Color::White,
value: ' ',
}
}
}
/// An object that holds the data for a grid of [`VisualElement`]s for a single frame.
pub struct Canvas {
data: Vec<VisualElement>,
dimension: Vec2,
default_element: VisualElement,
}
impl Canvas {
/// Constructs a [`Canvas`] with the given `dimension` and each cell set to the given
/// [`VisualElement`] as a default.
pub fn new(dimension: Vec2, default_element: &VisualElement) -> Canvas {
let mut data = Vec::new();
data.resize((dimension.x * dimension.y) as usize, *default_element);
Canvas {
data,
dimension,
default_element: *default_element,
}
}
pub fn default_element(&self) -> &VisualElement {
&self.default_element
}
/// Sets the current [`Canvas`]'s default element, to which every cell is reset when
/// [`Canvas::clear`] is called.
pub fn set_default_element(&mut self, element: &VisualElement) {
self.default_element = *element;
}
pub fn dimension(&self) -> Vec2 {
self.dimension
}
/// Checks if the point represented by the given `pos` would be within the dimensions of the
/// [`Canvas`].
///
/// The dimensions are sizes while the `pos` are indices.
///
/// ```rust
/// # use ruscii::spatial::Vec2;
/// # use ruscii::terminal::{Canvas, VisualElement};
/// #
/// let canvas = Canvas::new(Vec2::xy(10, 20), &VisualElement::default());
/// let a = Vec2::xy(10, 20);
/// let b = Vec2::xy(9, 19);
///
/// assert!(!canvas.contains(a));
/// assert!(canvas.contains(b));
/// ```
///
/// A [`Vec2`] with any negative components will always evaluate `false`.
///
/// ```rust
/// # use ruscii::spatial::Vec2;
/// # use ruscii::terminal::{Canvas, VisualElement};
/// #
/// let canvas = Canvas::new(Vec2::xy(10, 20), &VisualElement::default());
/// let p = Vec2::xy(-1, -3);
///
/// assert!(!canvas.contains(p));
/// ```
pub fn contains(&self, pos: Vec2) -> bool {
0 <= pos.x && 0 <= pos.y && pos.x < self.dimension.x && pos.y < self.dimension.y
}
/// Returns a reference to the [`Canvas`] cell at the given `pos` if that `pos` is
/// within the [`Canvas`] dimensions, [`None`] otherwise.
pub fn elem(&self, pos: Vec2) -> Option<&VisualElement> {
if self.contains(pos) {
Some(&self.data[(pos.y * self.dimension.x + pos.x) as usize])
} else {
None
}
}
/// Returns a mutable reference to the [`Canvas`] cell at the given `pos` if that `pos` is
/// within the [`Canvas`] dimensions, [`None`] otherwise.
pub fn elem_mut(&mut self, pos: Vec2) -> Option<&mut VisualElement> {
if self.contains(pos) {
Some(&mut self.data[(pos.y * self.dimension.x + pos.x) as usize])
} else {
None
}
}
/// Clears all of the [`VisualElement`] cells in the grid by setting them to clones of the
/// default element.
pub fn clear(&mut self) {
self.fill(&self.default_element().clone());
}
/// Sets every cell to the given `elem`.
pub fn fill(&mut self, elem: &VisualElement) {
self.data.iter_mut().map(|x| *x = *elem).count();
}
/// Returns a reference to all the data the [`Canvas`] holds.
pub fn data(&self) -> &Vec<VisualElement> {
&self.data
}
}
/// An object that exposes a [`Canvas`] and can write the data within it to the standard output.
pub struct Window {
canvas: Canvas,
target: BufWriter<io::Stdout>,
}
impl Default for Window {
/// Constructs a [`Window`] with the automatically detected size and the target set to the
/// [`io::stdout`].
fn default() -> Self {
Self {
canvas: Canvas::new(size(), &VisualElement::default()),
target: BufWriter::with_capacity(
size().x as usize * size().y as usize * 50,
io::stdout(),
),
}
}
}
impl Window {
pub fn canvas(&self) -> &Canvas {
&self.canvas
}
pub fn canvas_mut(&mut self) -> &mut Canvas {
&mut self.canvas
}
pub fn size(&self) -> Vec2 {
self.canvas.dimension()
}
pub fn open(&mut self) {
ct::queue!(self.target, ct::terminal::EnterAlternateScreen).unwrap();
ct::queue!(self.target, ct::style::ResetColor).unwrap();
ct::queue!(
self.target,
ct::style::SetAttribute(ct::style::Attribute::Reset)
)
.unwrap();
ct::queue!(self.target, ct::cursor::Hide).unwrap();
self.clean_state();
self.raw_mode(true);
self.target.flush().unwrap();
}
pub fn raw_mode(&mut self, enable: bool) {
if enable {
ct::terminal::enable_raw_mode().unwrap();
} else {
ct::terminal::disable_raw_mode().unwrap();
}
}
pub fn close(&mut self) {
self.raw_mode(false);
ct::queue!(self.target, ct::cursor::Show).unwrap();
ct::queue!(
self.target,
ct::style::SetAttribute(ct::style::Attribute::Reset)
)
.unwrap();
ct::queue!(self.target, ct::style::ResetColor).unwrap();
ct::queue!(self.target, ct::terminal::LeaveAlternateScreen).unwrap();
self.target.flush().unwrap();
}
pub fn clear(&mut self) {
if self.canvas.dimension() != size() {
self.canvas = Canvas::new(size(), self.canvas.default_element());
} else {
self.canvas.fill(&self.canvas.default_element().clone());
}
}
pub fn draw(&mut self) {
self.clean_state();
let mut last_foreground = self.canvas.default_element().foreground;
let mut last_background = self.canvas.default_element().background;
//let mut last_style = self.canvas.default_element().style;
let target = &mut self.target;
for element in self.canvas.data().iter() {
/*
if last_style != element.style {
let term_attribute = style_impl(element.style);
ct::queue!(self.target, ct::style::SetAttribute(term_attribute)).unwrap();
last_style = element.style
}
*/
if last_foreground != element.foreground {
let term_color = ct::style::Color::AnsiValue(element.foreground.code());
ct::queue!(target, ct::style::SetForegroundColor(term_color)).unwrap();
last_foreground = element.foreground
}
if last_background != element.background {
let term_color = ct::style::Color::AnsiValue(element.background.code());
ct::queue!(target, ct::style::SetBackgroundColor(term_color)).unwrap();
last_background = element.background
}
ct::queue!(target, ct::style::Print(element.value)).unwrap();
}
self.clean_state();
self.target.flush().unwrap();
}
fn clean_state(&mut self) {
//ct::queue!(self.target, ct::style::SetAttribute(ct::style::Attribute::NoBold)).unwrap();
let term_foreground =
ct::style::Color::AnsiValue(self.canvas.default_element().foreground.code());
ct::queue!(self.target, ct::style::SetForegroundColor(term_foreground)).unwrap();
let term_background =
ct::style::Color::AnsiValue(self.canvas.default_element().background.code());
ct::queue!(self.target, ct::style::SetBackgroundColor(term_background)).unwrap();
ct::queue!(self.target, ct::cursor::MoveTo(0, 0)).unwrap();
}
}
/// Returns the detected size of the terminal.
pub fn size() -> Vec2 {
let (x, y) = ct::terminal::size().unwrap();
Vec2::xy(x, y)
}