#![warn(missing_docs)]
use core::{
convert::{TryFrom, TryInto},
mem::replace,
num::NonZeroU8,
ops::*,
sync::atomic::{AtomicBool, Ordering},
};
use std::panic::PanicInfo;
mod curses_common;
use curses_common::*;
#[cfg(unix)]
mod ncurses;
#[cfg(unix)]
use ncurses::*;
#[cfg(windows)]
mod pdcurses;
#[cfg(windows)]
use pdcurses::*;
macro_rules! unsafe_call_result {
($name:literal, $func:ident($($tree:tt)*)) => {
if ERR == unsafe { $func($($tree)*) } {
Err($name)
} else {
Ok(())
}
}
}
macro_rules! unsafe_always_ok {
($func:ident($($tree:tt)*)) => {{
let ret = unsafe { $func($($tree)*) };
debug_assert!(ret != ERR);
}}
}
macro_rules! unsafe_void {
($func:ident($($tree:tt)*)) => {{
let _: () = unsafe { $func($($tree)*) };
}}
}
type PanicHook = Box<dyn Fn(&PanicInfo) + Sync + Send + 'static>;
#[repr(C)]
pub struct Curses {
ptr: *mut WINDOW,
old_hook: PanicHook,
}
static CURSES_ACTIVE: AtomicBool = AtomicBool::new(false);
impl Drop for Curses {
fn drop(&mut self) {
unsafe_always_ok!(def_prog_mode());
let _ = unsafe { endwin() };
CURSES_ACTIVE.store(false, Ordering::SeqCst);
if !std::thread::panicking() {
std::panic::set_hook(replace(&mut self.old_hook, Box::new(|_| {})));
}
}
}
impl Curses {
pub fn init() -> Self {
if CURSES_ACTIVE
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
let old_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|panic_info| {
unsafe_always_ok!(def_prog_mode());
let _ = unsafe_call_result!("", endwin());
eprintln!("{}", panic_info);
}));
if unsafe { isendwin() } {
let mut w = Self { ptr: unsafe { stdscr }, old_hook };
w.refresh().unwrap();
w
} else {
let win = Self { ptr: unsafe { initscr() }, old_hook };
assert!(!win.ptr.is_null());
let _ = unsafe_call_result!("", start_color());
let _ = unsafe_call_result!("", keypad(win.ptr, true));
unsafe_call_result!("", cbreak()).expect("Couldn't set `cbreak` mode.");
win
}
} else {
panic!("Curses is already active.")
}
}
pub fn refresh(&mut self) -> Result<(), &'static str> {
unsafe_call_result!("refresh", wrefresh(self.ptr))
}
pub fn set_echo(&mut self, echoing: bool) -> Result<(), &'static str> {
if echoing {
unsafe_call_result!("set_echo", echo())
} else {
unsafe_call_result!("set_echo", noecho())
}
}
pub fn get_cursor_position(&self) -> Position {
let x = unsafe { getcurx(self.ptr) as u32 };
let y = unsafe { getcury(self.ptr) as u32 };
Position { x, y }
}
pub fn get_terminal_size(&self) -> TerminalSize {
let x_count = unsafe { getmaxx(self.ptr) as u32 };
let y_count = unsafe { getmaxy(self.ptr) as u32 };
TerminalSize { x_count, y_count }
}
pub fn move_cursor(&mut self, p: Position) -> Result<(), &'static str> {
unsafe_call_result!("move_cursor", wmove(self.ptr, p.y as _, p.x as _))
}
pub fn print_ch<C: Into<CursesGlyph>>(
&mut self, c: C,
) -> Result<(), &'static str> {
unsafe_call_result!("print_ch", waddch(self.ptr, c.into().as_chtype()))
}
pub fn print_str(&mut self, s: &str) -> Result<(), &'static str> {
unsafe_call_result!(
"print_str",
waddnstr(self.ptr, s.as_ptr().cast(), s.len().try_into().unwrap())
)
}
pub fn insert_ch<C: Into<CursesGlyph>>(
&mut self, c: C,
) -> Result<(), &'static str> {
unsafe_call_result!("insert_ch", winsch(self.ptr, c.into().as_chtype()))
}
pub fn delete_ch(&mut self) -> Result<(), &'static str> {
unsafe_call_result!("delete_ch", wdelch(self.ptr))
}
pub fn copy_glyphs(&mut self, s: &[CursesGlyph]) -> Result<(), &'static str> {
unsafe_call_result!(
"copy_glyphs",
waddchnstr(self.ptr, s.as_ptr().cast(), s.len().try_into().unwrap())
)
}
pub fn clear(&mut self) -> Result<(), &'static str> {
unsafe_call_result!("clear", wclear(self.ptr))
}
pub fn set_attributes(
&mut self, attr: Attributes, on: bool,
) -> Result<(), &'static str> {
let attr: i32 = ((attr.0 as u32) << 16) as i32;
if on {
unsafe_call_result!("set_attributes", wattron(self.ptr, attr))
} else {
unsafe_call_result!("set_attributes", wattroff(self.ptr, attr))
}
}
pub fn set_terminal_size(
&mut self, size: TerminalSize,
) -> Result<(), &'static str> {
unsafe_call_result!(
"resize_term",
resize_term(size.y_count as _, size.x_count as _)
)
}
pub fn set_timeout(&mut self, time: i32) {
unsafe_void!(wtimeout(self.ptr, time))
}
pub fn poll_events(&mut self) -> Option<CursesKey> {
const ERR_U32: u32 = ERR as u32;
const KEY_F64: u32 = KEY_F0 + 64;
match (unsafe { wgetch(self.ptr) }) as u32 {
ERR_U32 => None,
ascii if (ascii <= u8::MAX as u32) => Some(CursesKey::Ascii(ascii as u8)),
#[cfg(windows)]
KEY_A1 => Some(CursesKey::Home),
#[cfg(windows)]
KEY_A2 => Some(CursesKey::ArrowUp),
#[cfg(windows)]
KEY_A3 => Some(CursesKey::PageUp),
#[cfg(windows)]
KEY_B1 => Some(CursesKey::ArrowLeft),
#[cfg(windows)]
KEY_B3 => Some(CursesKey::ArrowRight),
#[cfg(windows)]
KEY_C1 => Some(CursesKey::End),
#[cfg(windows)]
KEY_C2 => Some(CursesKey::ArrowDown),
#[cfg(windows)]
KEY_C3 => Some(CursesKey::PageDown),
#[cfg(windows)]
PADENTER => Some(CursesKey::Enter),
#[cfg(windows)]
PADSLASH => Some(CursesKey::Ascii(b'/')),
#[cfg(windows)]
PADSTAR => Some(CursesKey::Ascii(b'*')),
#[cfg(windows)]
PADMINUS => Some(CursesKey::Ascii(b'-')),
#[cfg(windows)]
PADPLUS => Some(CursesKey::Ascii(b'+')),
KEY_BACKSPACE => Some(CursesKey::Backspace),
KEY_UP => Some(CursesKey::ArrowUp),
KEY_DOWN => Some(CursesKey::ArrowDown),
KEY_LEFT => Some(CursesKey::ArrowLeft),
KEY_RIGHT => Some(CursesKey::ArrowRight),
KEY_IC => Some(CursesKey::Insert),
KEY_DC => Some(CursesKey::Delete),
KEY_HOME => Some(CursesKey::Home),
KEY_END => Some(CursesKey::End),
KEY_PPAGE => Some(CursesKey::PageUp),
KEY_NPAGE => Some(CursesKey::PageDown),
KEY_B2 => Some(CursesKey::Keypad5NoNumlock),
KEY_RESIZE => {
unsafe { resize_term(0, 0) };
Some(CursesKey::TerminalResized)
}
KEY_ENTER => Some(CursesKey::Enter),
f if (f >= KEY_F0 && f <= KEY_F64) => {
Some(CursesKey::Function((f - KEY_F0) as u8))
}
other => Some(CursesKey::UnknownKey(other)),
}
}
pub fn un_get_event(
&mut self, event: Option<CursesKey>,
) -> Result<(), &'static str> {
let ev: u32 = match event {
None => ERR as u32,
Some(CursesKey::Ascii(ascii)) => ascii as u32,
Some(CursesKey::Function(f)) => KEY_F0 + (f as u32),
Some(CursesKey::Enter) => KEY_ENTER,
Some(CursesKey::Backspace) => KEY_BACKSPACE,
Some(CursesKey::ArrowUp) => KEY_UP,
Some(CursesKey::ArrowDown) => KEY_DOWN,
Some(CursesKey::ArrowLeft) => KEY_LEFT,
Some(CursesKey::ArrowRight) => KEY_RIGHT,
Some(CursesKey::Insert) => KEY_IC,
Some(CursesKey::Delete) => KEY_DC,
Some(CursesKey::Home) => KEY_HOME,
Some(CursesKey::End) => KEY_END,
Some(CursesKey::PageUp) => KEY_PPAGE,
Some(CursesKey::PageDown) => KEY_NPAGE,
Some(CursesKey::Keypad5NoNumlock) => KEY_B2,
Some(CursesKey::TerminalResized) => KEY_RESIZE,
Some(CursesKey::UnknownKey(u)) => u,
};
unsafe_call_result!("un_get_event", ungetch(ev as i32))
}
pub fn flush_events(&mut self) -> Result<(), &'static str> {
unsafe_call_result!("flush_events", flushinp())
}
pub fn shell_mode<'a>(&'a mut self) -> Result<CursesShell<'a>, &'static str> {
let _ = self.refresh();
unsafe_always_ok!(def_prog_mode());
unsafe_call_result!("shell_mode", endwin())
.map(move |_| CursesShell { win: self })
}
pub fn has_color(&self) -> bool {
unsafe { has_colors() }
}
pub fn can_change_colors(&self) -> bool {
unsafe { can_change_color() }
}
pub fn get_max_color_id_inclusive(&self) -> Option<ColorID> {
let colors = unsafe { COLORS };
if colors > 0 {
Some(ColorID(unsafe { (COLORS - 1).try_into().unwrap_or(u8::MAX) }))
} else {
None
}
}
pub fn get_max_color_pair_inclusive(&self) -> Option<ColorPair> {
NonZeroU8::new(unsafe { (COLOR_PAIRS - 1).try_into().unwrap_or(u8::MAX) })
.map(ColorPair)
}
pub fn set_color_id_rgb(
&mut self, c: ColorID, [r, g, b]: [f32; 3],
) -> Result<(), &'static str> {
let r_i16 = (r.max(0.0).min(1.0) * 1000.0) as i16;
let g_i16 = (g.max(0.0).min(1.0) * 1000.0) as i16;
let b_i16 = (b.max(0.0).min(1.0) * 1000.0) as i16;
unsafe_call_result!(
"set_color_id_rgb",
init_color(c.0.into(), r_i16, g_i16, b_i16)
)
}
pub fn get_color_id_rgb(&self, c: ColorID) -> Result<[f32; 3], &'static str> {
let mut r_i16 = 0;
let mut g_i16 = 0;
let mut b_i16 = 0;
unsafe_call_result!(
"get_color_id_rgb",
color_content(c.0.into(), &mut r_i16, &mut g_i16, &mut b_i16)
)
.map(|_| {
let r = r_i16 as f32 / 1000.0;
let g = g_i16 as f32 / 1000.0;
let b = b_i16 as f32 / 1000.0;
[r, g, b]
})
}
pub fn set_color_pair_content(
&mut self, pair: ColorPair, fg: ColorID, bg: ColorID,
) -> Result<(), &'static str> {
unsafe_call_result!(
"set_color_pair_content",
init_pair(pair.0.get().into(), fg.0.into(), bg.0.into())
)
}
pub fn get_color_pair_content(
&self, c: ColorID,
) -> Result<(ColorID, ColorID), &'static str> {
let mut f_i16 = 0;
let mut b_i16 = 0;
unsafe_call_result!(
"get_color_pair_content",
pair_content(c.0.into(), &mut f_i16, &mut b_i16)
)
.and_then(|_| match (u8::try_from(f_i16), u8::try_from(b_i16)) {
(Ok(f), Ok(b)) => Ok((ColorID(f), ColorID(b))),
_ => Err("get_color_pair_content"),
})
}
pub fn set_active_color_pair(
&mut self, opt_pair: Option<ColorPair>,
) -> Result<(), &'static str> {
let p = opt_pair.map(|cp| cp.0.get()).unwrap_or(0).into();
unsafe_call_result!(
"set_active_color_pair",
wcolor_set(self.ptr, p, core::ptr::null_mut())
)
}
pub fn set_scrollable(&mut self, yes: bool) -> Result<(), &'static str> {
unsafe_call_result!("set_scrollable", scrollok(self.ptr, yes))
}
pub fn set_scroll_region(
&mut self, top: u32, bottom: u32,
) -> Result<(), &'static str> {
unsafe_call_result!(
"set_scroll_region",
wsetscrreg(self.ptr, top as i32, bottom as i32)
)
}
pub fn scroll(&mut self, n: i32) -> Result<(), &'static str> {
unsafe_call_result!("scroll", wscrl(self.ptr, n))
}
pub fn set_cursor_visibility(
&mut self, vis: CursorVisibility,
) -> Result<CursorVisibility, &'static str> {
let old = unsafe { curs_set(vis as i32) };
Ok(match old {
0 => CursorVisibility::Invisible,
1 => CursorVisibility::Normal,
2 => CursorVisibility::VeryVisible,
_ => return Err("set_cursor_visibility"),
})
}
pub fn set_background<C: Into<CursesGlyph>>(
&mut self, c: C,
) -> Result<(), &'static str> {
unsafe_call_result!("set_background", wbkgd(self.ptr, c.into().as_chtype()))
}
pub fn get_background(&self) -> CursesGlyph {
CursesGlyph::from(unsafe { getbkgd(self.ptr) })
}
}
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct Position {
pub x: u32,
pub y: u32,
}
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct TerminalSize {
pub x_count: u32,
pub y_count: u32,
}
#[derive(Debug, Clone, Copy)]
#[repr(C, align(4))]
pub struct CursesGlyph {
pub ascii: u8,
pub opt_color_pair: Option<ColorPair>,
pub attributes: Attributes,
}
impl From<u8> for CursesGlyph {
fn from(ascii: u8) -> Self {
Self { ascii, opt_color_pair: None, attributes: Attributes(0) }
}
}
impl From<char> for CursesGlyph {
fn from(ch: char) -> Self {
let ascii = ch as u8;
Self { ascii, opt_color_pair: None, attributes: Attributes(0) }
}
}
impl From<chtype> for CursesGlyph {
fn from(cht: chtype) -> Self {
unsafe { core::mem::transmute(cht) }
}
}
impl CursesGlyph {
fn as_chtype(self) -> chtype {
unsafe { core::mem::transmute(self) }
}
}
#[repr(i32)]
pub enum CursorVisibility {
Invisible = 0,
Normal = 1,
VeryVisible = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct ColorID(pub u8);
#[allow(missing_docs)]
impl ColorID {
pub const BLACK: ColorID = ColorID(COLOR_BLACK as u8);
pub const RED: ColorID = ColorID(COLOR_RED as u8);
pub const GREEN: ColorID = ColorID(COLOR_GREEN as u8);
pub const YELLOW: ColorID = ColorID(COLOR_YELLOW as u8);
pub const BLUE: ColorID = ColorID(COLOR_BLUE as u8);
pub const MAGENTA: ColorID = ColorID(COLOR_MAGENTA as u8);
pub const CYAN: ColorID = ColorID(COLOR_CYAN as u8);
pub const WHITE: ColorID = ColorID(COLOR_WHITE as u8);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct ColorPair(pub NonZeroU8);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct Attributes(pub u16);
impl Attributes {
pub const STANDOUT: Attributes = if cfg!(unix) {
Attributes(1 << 0)
} else {
Attributes(Attributes::REVERSE.0 | Attributes::BOLD.0)
};
pub const UNDERLINE: Attributes = if cfg!(unix) {
Attributes(1 << 1)
} else {
Attributes((0x00100000 >> 16) as u16)
};
pub const REVERSE: Attributes = if cfg!(unix) {
Attributes(1 << 2)
} else {
Attributes((0x00200000 >> 16) as u16)
};
pub const BLINK: Attributes = if cfg!(unix) {
Attributes(1 << 3)
} else {
Attributes((0x00400000 >> 16) as u16)
};
pub const DIM: Attributes =
if cfg!(unix) { Attributes(1 << 4) } else { Attributes(0) };
pub const BOLD: Attributes = if cfg!(unix) {
Attributes(1 << 5)
} else {
Attributes((0x00800000 >> 16) as u16)
};
pub const ALT_CHAR_SET: Attributes = if cfg!(unix) {
Attributes(1 << 6)
} else {
Attributes((0x00010000 >> 16) as u16)
};
pub const INVIS: Attributes =
if cfg!(unix) { Attributes(1 << 7) } else { Attributes(0) };
pub const ITALIC: Attributes = if cfg!(unix) {
Attributes(1 << 15)
} else {
Attributes((0x00080000 >> 16) as u16)
};
}
impl BitAnd for Attributes {
type Output = Self;
#[inline]
fn bitand(self, rhs: Self) -> Self {
Self(self.0 & rhs.0)
}
}
impl BitAndAssign for Attributes {
#[inline]
fn bitand_assign(&mut self, rhs: Self) {
self.0 &= rhs.0
}
}
impl BitOr for Attributes {
type Output = Self;
#[inline]
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl BitOrAssign for Attributes {
#[inline]
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0
}
}
impl BitXor for Attributes {
type Output = Self;
#[inline]
fn bitxor(self, rhs: Self) -> Self {
Self(self.0 ^ rhs.0)
}
}
impl BitXorAssign for Attributes {
#[inline]
fn bitxor_assign(&mut self, rhs: Self) {
self.0 ^= rhs.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursesKey {
Ascii(u8),
TerminalResized,
Enter,
Backspace,
ArrowUp,
ArrowDown,
ArrowLeft,
ArrowRight,
Insert,
Delete,
Home,
End,
PageUp,
PageDown,
Keypad5NoNumlock,
Function(u8),
UnknownKey(u32),
}
impl CursesKey {
pub const fn from_ascii(ascii: u8) -> Self {
CursesKey::Ascii(ascii)
}
}
#[repr(transparent)]
pub struct CursesShell<'a> {
win: &'a mut Curses,
}
impl<'a> Drop for CursesShell<'a> {
fn drop(&mut self) {
unsafe_call_result!("refresh", wrefresh(self.win.ptr)).unwrap();
}
}
impl<'a> Deref for CursesShell<'a> {
type Target = Curses;
#[inline]
fn deref(&self) -> &Self::Target {
self.win
}
}
macro_rules! acs_getter {
($fn_name:ident, $ch:expr, $d:expr) => {
#[doc = $d]
#[cfg(unix)]
pub fn $fn_name(&self) -> CursesGlyph {
let c: char = $ch;
CursesGlyph {
ascii: unsafe { (*acs_map.as_ptr().add(c as u8 as usize)) as u8 },
opt_color_pair: None,
attributes: Attributes::ALT_CHAR_SET,
}
}
#[doc = $d]
#[cfg(windows)]
pub fn $fn_name(&self) -> CursesGlyph {
let c: char = $ch;
CursesGlyph {
ascii: c as u8,
opt_color_pair: None,
attributes: Attributes::ALT_CHAR_SET,
}
}
};
}
impl Curses {
acs_getter!(acs_block, '0', "Solid square block, but sometimes a hash.");
acs_getter!(acs_board, 'h', "Board of squares, often just a hash.");
acs_getter!(acs_btee, 'v', "Bottom T");
acs_getter!(acs_bullet, '~', "Bullet point");
acs_getter!(acs_ckboard, 'a', "Checkerboard, usually like a 50% stipple");
acs_getter!(acs_darrow, '.', "Down arrow");
acs_getter!(acs_degree, 'f', "Degree symbol (like with an angle)");
acs_getter!(acs_diamond, '`', "Diamond");
acs_getter!(acs_gequal, 'z', "Greater-than or equal to.");
acs_getter!(acs_hline, 'q', "Horizontal line");
acs_getter!(acs_lantern, 'i', "Lantern symbol");
acs_getter!(acs_larrow, ',', "Left arrow");
acs_getter!(acs_lequal, 'y', "Less-than or equal to.");
acs_getter!(acs_llcorner, 'm', "Lower left corner of a box.");
acs_getter!(acs_lrcorner, 'j', "Lower right corner of a box.");
acs_getter!(acs_ltee, 't', "Left T");
acs_getter!(acs_nequal, '|', "Not-equal to.");
acs_getter!(acs_pi, '{', "Pi");
acs_getter!(acs_plminus, 'g', "Plus/Minus");
acs_getter!(acs_plus, 'n', "Plus shaped \"line\" in all four directions");
acs_getter!(acs_rarrow, '+', "Right arrow");
acs_getter!(acs_rtee, 'u', "Right T");
acs_getter!(acs_s1, 'o', "Horizontal Scanline 1");
acs_getter!(acs_s3, 'p', "Horizontal Scanline 3");
acs_getter!(acs_s7, 'r', "Horizontal Scanline 7");
acs_getter!(acs_s9, 's', "Horizontal Scanline 9");
acs_getter!(acs_sterling, '}', "British pounds sterling.");
acs_getter!(acs_ttee, 'w', "Top T");
acs_getter!(acs_uarrow, '-', "Up arrow");
acs_getter!(acs_ulcorner, 'l', "Upper left corner of a box.");
acs_getter!(acs_urcorner, 'k', "Upper right corner of a box.");
acs_getter!(acs_vline, 'x', "Vertical line");
}