fryingpan 0.1.2

A TUI library based on pancurses.
Documentation
/*!
A TUI library based on pancurses.

## Example
```
use fryingpan::{FryingPan, Input, Panel, Point, A_ITALIC, init_pair, COLOR_BLUE, color_pair, COLOR_WHITE};
use std::{thread::sleep, time::Duration};

const BLUE_WHITE: i16 = 1;
const SLEEP_TIME: Duration = Duration::from_millis(50);

fn main() {
    // cook pancake and init colors
    let pancake = FryingPan::default()
        .hide_cursor(true)
        .no_delay(true)
        .color(true)
        .build()
        .unwrap();
    init_pair(BLUE_WHITE, COLOR_BLUE, COLOR_WHITE);

    // make panel,                   position v   v size
    let mut panel = Panel::new(Point::from(1, 1), 10, 30);
    panel.draw_box();

    // printing does not move the cursor
    panel.mv_print(Point::from(1, 0), "╴pancakes╶");
    panel.mv_print(Point::from(1, 1), "hello");

    // the cursor will be within bounds, unwrap is fine
    panel.attr_on(A_ITALIC).unwrap();
    panel.attr_on(color_pair(BLUE_WHITE)).unwrap();

    loop {
        if let Some(Input::Character('q')) = pancake.get_char() {
            break;
        }

        pancake.erase();
        pancake.render_panel(&panel);
        pancake.refresh();

        sleep(SLEEP_TIME);
    }

    pancake.quit();
}
```
*/

use grid::Grid;
use pancurses::COLOR_PAIR;
use pancurses::{endwin, start_color, Window};
use std::fmt::Display;
use thiserror::Error;

/// Input enum, re-exported from pancurses.
pub use pancurses::Input;

/// Builder struct for the `Pancake` struct.
#[derive(Debug, Default)]
pub struct FryingPan {
    colors: bool,
    hide_cursor: bool,
    no_delay: bool,
}
// cast iron pans are the best.

impl FryingPan {
    /// Whether to try to enable colors in the final pancake.
    pub fn color(&mut self, enabled: bool) -> &mut FryingPan {
        self.colors = enabled;
        self
    }

    /// Whether to hide the cursor.
    pub fn hide_cursor(&mut self, enabled: bool) -> &mut FryingPan {
        self.hide_cursor = enabled;
        self
    }

    /// Whether to continue execution on `get_char` instead of stopping the thread and waiting.
    pub fn no_delay(&mut self, enabled: bool) -> &mut FryingPan {
        self.no_delay = enabled;
        self
    }

    /// Build everything.
    ///
    /// ## Errors
    /// Errors if terminal does not support color and color was enabled as an option
    pub fn build(&mut self) -> Result<Pancake, Error> {
        let win = pancurses::initscr();
        if self.colors && pancurses::has_colors() {
            start_color();
        } else if self.colors {
            return Err(Error::TermNoColor);
        }
        if self.hide_cursor {
            pancurses::curs_set(0);
        }
        if self.no_delay {
            win.nodelay(self.no_delay);
        }
        win.keypad(true);
        Ok(Pancake {
            win,
        })
    }
}

/// Central data structure.
pub struct Pancake {
    win: Window,
}

impl Pancake {
    /// Renders a panel to the terminal buffer.
    pub fn render_panel(&self, panel: &Panel) {
        for y in 0..panel.grid.rows() {
            for (x, ch) in panel.grid.iter_row(y).enumerate() {
                for a in panel.attr.get(y, x).unwrap() {
                    self.win.attron(*a);
                }
                self.win.mvaddstr(
                    (panel.position.y + y) as i32,
                    (panel.position.x + x) as i32,
                    ch.to_string(),
                );
                for a in panel.attr.get(y, x).unwrap() {
                    self.win.attroff(*a);
                }
            }
        }
    }

    /// Read a single character from the terminal. In `no_delay` mode and if no key is pressed,
    /// returns None.
    pub fn get_char(&self) -> Option<Input> {
        self.win.getch()
    }

    /// Erase the terminal buffer. Call before rendering.
    pub fn erase(&self) {
        self.win.erase();
    }

    /// Copy the terminal buffer to the terminal. Call after rendering.
    pub fn refresh(&self) {
        self.win.refresh();
    }

    /// Gracefully shutdown and return terminal to default state.
    pub fn quit(&self) {
        endwin();
    }
}

/// A panel.
pub struct Panel {
    /// The current position of this panel.
    pub position: Point,
    /// The current position of this panel's cursor.
    pub cursor: Point,
    grid: Grid<char>,
    attr: Grid<Vec<chtype>>,
}

impl Panel {
    /// Create a new empty panel with the position specified and the size specified.
    pub fn new(position: Point, rows: usize, cols: usize) -> Self {
        let mut grid = Grid::new(rows, cols);
        grid.fill(' ');
        let mut attr = Grid::new(rows, cols);
        attr.fill(Vec::new());
        Panel {
            position,
            grid,
            attr,
            cursor: Point::new(),
        }
    }

    /// Write a string to the current cursor location. Does not wrap text. If cursor is out of
    /// bounds, does nothing.
    pub fn print<T>(&mut self, string: T)
    where
        T: Display,
    {
        let string = string.to_string();
        for (i, ch) in string.chars().enumerate() {
            if let Some(c) = self.grid.get_mut(self.cursor.y, self.cursor.x + i) {
                *c = ch;
            }
        }
    }

    /// Move the cursor to the specified location and call `print`.
    pub fn mv_print<T>(&mut self, position: Point, string: T)
    where
        T: Display,
    {
        self.cursor = position;
        self.print(string);
    }

    /// Enable an attribute for the character under the cursor.
    pub fn attr_on(&mut self, attr: chtype) -> Result<(), Error> {
        if let Some(vec) = self.attr.get_mut(self.cursor.y, self.cursor.x) {
            vec.push(attr);
            Ok(())
        } else {
            Err(Error::OutOfBoundsIndex)
        }
    }

    /// Disable an attribute for the character under the cursor.
    pub fn attr_off(&mut self, attr: chtype) -> Result<(), Error> {
        if let Some(vec) = self.attr.get_mut(self.cursor.y, self.cursor.x) {
            let mut indices = Vec::new();
            for (i, a) in vec.iter().enumerate() {
                if *a == attr {
                    indices.push(i);
                }
            }
            for index in indices {
                vec.remove(index);
            }
            Ok(())
        } else {
            Err(Error::OutOfBoundsIndex)
        }
    }

    /// Erase the character grid and the attribute grid.
    pub fn erase(&mut self) {
        self.grid.fill(' ');
        self.attr.fill(Vec::new());
    }

    /// Draw a box around the edge of the panel. This will overwrite things.
    pub fn draw_box(&mut self) {
        let cols = self.grid.cols() - 1;
        let rows = self.grid.rows() - 1;
        for ch in self.grid.iter_row_mut(0) {
            *ch = ''
        }
        for ch in self.grid.iter_row_mut(rows) {
            *ch = ''
        }
        for ch in self.grid.iter_col_mut(0) {
            *ch = ''
        }
        for ch in self.grid.iter_col_mut(cols) {
            *ch = ''
        }
        *self.grid.get_mut(0, 0).unwrap() = '';
        *self.grid.get_mut(rows, 0).unwrap() = '';
        *self.grid.get_mut(0, cols).unwrap() = '';
        *self.grid.get_mut(rows, cols).unwrap() = '';
    }
}

/// A point in 2D space.
pub struct Point {
    pub x: usize,
    pub y: usize,
}

impl Point {
    /// Create a new point with `x: 0` and `y: 0`.
    pub fn new() -> Self {
        Point { y: 0, x: 0 }
    }
    /// Create a new point with the provided coordinates.
    pub fn from(x: usize, y: usize) -> Self {
        Point { x, y }
    }
}

impl Default for Point {
    fn default() -> Self {
        Point::new()
    }
}

/// Errors used by this library.
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum Error {
    /// Terminal does not support color, but color was enabled as an option.
    #[error("Terminal does not support color")]
    TermNoColor,
    /// Cursor index is out of bounds.
    #[error("Cursor index out of bounds")]
    OutOfBoundsIndex,
}

/// Makes text blink.
pub use pancurses::A_BLINK;
/// Makes text bold.
pub use pancurses::A_BOLD;
/// Makes text dim.
pub use pancurses::A_DIM;
/// Makes text italic.
pub use pancurses::A_ITALIC;
/// Makes text underlined.
pub use pancurses::A_UNDERLINE;

pub use pancurses::COLOR_BLACK;
pub use pancurses::COLOR_BLUE;
pub use pancurses::COLOR_CYAN;
pub use pancurses::COLOR_GREEN;
pub use pancurses::COLOR_MAGENTA;
pub use pancurses::COLOR_RED;
pub use pancurses::COLOR_WHITE;
pub use pancurses::COLOR_YELLOW;

/// Change the definition of a color pair. Does not do anything if colors are disabled.
///
pub use pancurses::init_pair;

/// Converts a color pair ID into an attribute.
pub fn color_pair(color_pair: i16) -> u32 {
    COLOR_PAIR(color_pair as u32)
}

/// Contains information about a character and/or an attribute.
pub use pancurses::chtype;