2023-05-16 20:12:22 +00:00
|
|
|
//! A wrapper around the system's curses/ncurses library, exposing some lower-level functionality
|
|
|
|
//! that's not directly exposed in any of the popular ncurses crates.
|
|
|
|
//!
|
|
|
|
//! In addition to exposing the C library ffi calls, we also shim around some functionality that's
|
|
|
|
//! only made available via the the ncurses headers to C code via macro magic, such as polyfilling
|
|
|
|
//! missing capability strings to shoe-in missing support for certain terminal sequences.
|
|
|
|
//!
|
|
|
|
//! This is intentionally very bare bones and only implements the subset of curses functionality
|
|
|
|
//! used by fish
|
|
|
|
|
|
|
|
use self::sys::*;
|
|
|
|
use std::ffi::{CStr, CString};
|
|
|
|
use std::sync::Arc;
|
|
|
|
use std::sync::Mutex;
|
|
|
|
|
|
|
|
/// The [`Term`] singleton, providing a façade around the system curses library. Initialized via a
|
|
|
|
/// successful call to [`setup()`] and surfaced to the outside world via [`term()`].
|
|
|
|
///
|
|
|
|
/// It isn't guaranteed that fish will ever be able to successfully call `setup()`, so this must
|
|
|
|
/// remain an `Option` instead of returning `Term` by default and just panicking if [`term()`] was
|
|
|
|
/// called before `setup()`.
|
|
|
|
///
|
|
|
|
/// We can't just use an AtomicPtr<Arc<Term>> here because there's a race condition when the old Arc
|
|
|
|
/// gets dropped - we would obtain the current (non-null) value of `TERM` in [`term()`] but there's
|
|
|
|
/// no guarantee that a simultaneous call to [`setup()`] won't result in this refcount being
|
|
|
|
/// decremented to zero and the memory being reclaimed before we can clone it, since we can only
|
|
|
|
/// atomically *read* the value of the pointer, not clone the `Arc` it points to.
|
|
|
|
pub static TERM: Mutex<Option<Arc<Term>>> = Mutex::new(None);
|
|
|
|
|
|
|
|
/// Returns a reference to the global [`Term`] singleton or `None` if not preceded by a successful
|
|
|
|
/// call to [`curses::setup()`].
|
|
|
|
pub fn term() -> Option<Arc<Term>> {
|
|
|
|
TERM.lock()
|
|
|
|
.expect("Mutex poisoned!")
|
|
|
|
.as_ref()
|
|
|
|
.map(Arc::clone)
|
|
|
|
}
|
|
|
|
|
2023-05-07 22:14:19 +00:00
|
|
|
/// Convert a nul-terminated pointer, which must not be itself null, to a CString.
|
|
|
|
fn ptr_to_cstr(ptr: *const libc::c_char) -> CString {
|
|
|
|
assert!(!ptr.is_null());
|
|
|
|
unsafe { CStr::from_ptr(ptr).to_owned() }
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Convert a nul-terminated pointer to a CString, or None if the pointer is null.
|
|
|
|
fn try_ptr_to_cstr(ptr: *const libc::c_char) -> Option<CString> {
|
|
|
|
if ptr.is_null() {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(ptr_to_cstr(ptr))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-16 20:12:22 +00:00
|
|
|
/// Private module exposing system curses ffi.
|
|
|
|
mod sys {
|
|
|
|
pub const OK: i32 = 0;
|
|
|
|
pub const ERR: i32 = -1;
|
|
|
|
|
2023-05-07 22:14:19 +00:00
|
|
|
/// tputs callback argument type and the callback type itself.
|
|
|
|
/// N.B. The C++ had a check for TPUTS_USES_INT_ARG for the first parameter of tputs
|
|
|
|
/// which was to support OpenIndiana, which used a char.
|
|
|
|
pub type tputs_arg = libc::c_int;
|
|
|
|
pub type putc_t = extern "C" fn(tputs_arg) -> libc::c_int;
|
|
|
|
|
2023-05-16 20:12:22 +00:00
|
|
|
extern "C" {
|
|
|
|
/// The ncurses `cur_term` TERMINAL pointer.
|
|
|
|
pub static mut cur_term: *const core::ffi::c_void;
|
|
|
|
|
|
|
|
/// setupterm(3) is a low-level call to begin doing any sort of `term.h`/`curses.h` work.
|
|
|
|
/// It's called internally by ncurses's `initscr()` and `newterm()`, but the C++ code called
|
|
|
|
/// it directly from [`initialize_curses_using_fallbacks()`].
|
|
|
|
pub fn setupterm(
|
|
|
|
term: *const libc::c_char,
|
|
|
|
filedes: libc::c_int,
|
|
|
|
errret: *mut libc::c_int,
|
|
|
|
) -> libc::c_int;
|
|
|
|
|
|
|
|
/// Frees the `cur_term` TERMINAL pointer.
|
|
|
|
pub fn del_curterm(term: *const core::ffi::c_void) -> libc::c_int;
|
|
|
|
|
|
|
|
/// Checks for the presence of a termcap flag identified by the first two characters of
|
|
|
|
/// `id`.
|
|
|
|
pub fn tgetflag(id: *const libc::c_char) -> libc::c_int;
|
|
|
|
|
|
|
|
/// Checks for the presence and value of a number capability in the termcap/termconf
|
|
|
|
/// database. A return value of `-1` indicates not found.
|
|
|
|
pub fn tgetnum(id: *const libc::c_char) -> libc::c_int;
|
|
|
|
|
|
|
|
pub fn tgetstr(
|
|
|
|
id: *const libc::c_char,
|
|
|
|
area: *mut *mut libc::c_char,
|
|
|
|
) -> *const libc::c_char;
|
2023-05-07 22:14:19 +00:00
|
|
|
|
|
|
|
pub fn tparm(str: *const libc::c_char, ...) -> *const libc::c_char;
|
|
|
|
|
|
|
|
pub fn tputs(str: *const libc::c_char, affcnt: libc::c_int, putc: putc_t) -> libc::c_int;
|
2023-05-16 20:12:22 +00:00
|
|
|
}
|
|
|
|
}
|
2023-05-07 22:14:19 +00:00
|
|
|
pub use sys::tputs_arg as TputsArg;
|
2023-05-16 20:12:22 +00:00
|
|
|
|
|
|
|
/// The safe wrapper around curses functionality, initialized by a successful call to [`setup()`]
|
|
|
|
/// and obtained thereafter by calls to [`term()`].
|
|
|
|
///
|
|
|
|
/// An extant `Term` instance means the curses `TERMINAL *cur_term` pointer is non-null. Any
|
|
|
|
/// functionality that is normally performed using `cur_term` should be done via `Term` instead.
|
|
|
|
pub struct Term {
|
|
|
|
// String capabilities
|
2023-05-07 22:14:19 +00:00
|
|
|
pub enter_bold_mode: Option<CString>,
|
2023-05-16 20:12:22 +00:00
|
|
|
pub enter_italics_mode: Option<CString>,
|
|
|
|
pub exit_italics_mode: Option<CString>,
|
|
|
|
pub enter_dim_mode: Option<CString>,
|
2023-05-07 22:14:19 +00:00
|
|
|
pub enter_underline_mode: Option<CString>,
|
|
|
|
pub exit_underline_mode: Option<CString>,
|
|
|
|
pub enter_reverse_mode: Option<CString>,
|
|
|
|
pub enter_standout_mode: Option<CString>,
|
|
|
|
pub set_a_foreground: Option<CString>,
|
|
|
|
pub set_foreground: Option<CString>,
|
|
|
|
pub set_a_background: Option<CString>,
|
|
|
|
pub set_background: Option<CString>,
|
|
|
|
pub exit_attribute_mode: Option<CString>,
|
2023-05-16 20:12:22 +00:00
|
|
|
|
|
|
|
// Number capabilities
|
|
|
|
pub max_colors: Option<i32>,
|
|
|
|
|
|
|
|
// Flag/boolean capabilities
|
|
|
|
pub eat_newline_glitch: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Term {
|
|
|
|
/// Initialize a new `Term` instance, prepopulating the values of all the curses string
|
|
|
|
/// capabilities we care about in the process.
|
|
|
|
fn new() -> Self {
|
|
|
|
Term {
|
|
|
|
// String capabilities
|
2023-05-07 22:14:19 +00:00
|
|
|
enter_bold_mode: StringCap::new("md").lookup(),
|
2023-05-16 20:12:22 +00:00
|
|
|
enter_italics_mode: StringCap::new("ZH").lookup(),
|
|
|
|
exit_italics_mode: StringCap::new("ZR").lookup(),
|
|
|
|
enter_dim_mode: StringCap::new("mh").lookup(),
|
2023-05-07 22:14:19 +00:00
|
|
|
enter_underline_mode: StringCap::new("us").lookup(),
|
|
|
|
exit_underline_mode: StringCap::new("ue").lookup(),
|
|
|
|
enter_reverse_mode: StringCap::new("mr").lookup(),
|
|
|
|
enter_standout_mode: StringCap::new("so").lookup(),
|
|
|
|
set_a_foreground: StringCap::new("AF").lookup(),
|
|
|
|
set_foreground: StringCap::new("Sf").lookup(),
|
|
|
|
set_a_background: StringCap::new("AB").lookup(),
|
|
|
|
set_background: StringCap::new("Sb").lookup(),
|
|
|
|
exit_attribute_mode: StringCap::new("me").lookup(),
|
2023-05-16 20:12:22 +00:00
|
|
|
|
|
|
|
// Number capabilities
|
|
|
|
max_colors: NumberCap::new("Co").lookup(),
|
|
|
|
|
|
|
|
// Flag/boolean capabilities
|
|
|
|
eat_newline_glitch: FlagCap::new("xn").lookup(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
trait Capability {
|
|
|
|
type Result: Sized;
|
|
|
|
fn lookup(&self) -> Self::Result;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Capability for StringCap {
|
|
|
|
type Result = Option<CString>;
|
|
|
|
|
|
|
|
fn lookup(&self) -> Self::Result {
|
|
|
|
unsafe {
|
|
|
|
const NULL: *const i8 = core::ptr::null();
|
|
|
|
match sys::tgetstr(self.code.as_ptr(), core::ptr::null_mut()) {
|
|
|
|
NULL => None,
|
|
|
|
// termcap spec says nul is not allowed in terminal sequences and must be encoded;
|
|
|
|
// so the terminating NUL is the end of the string.
|
2023-05-07 22:14:19 +00:00
|
|
|
result => Some(ptr_to_cstr(result)),
|
2023-05-16 20:12:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Capability for NumberCap {
|
|
|
|
type Result = Option<i32>;
|
|
|
|
|
|
|
|
fn lookup(&self) -> Self::Result {
|
|
|
|
unsafe {
|
|
|
|
match tgetnum(self.0.as_ptr()) {
|
|
|
|
-1 => None,
|
|
|
|
n => Some(n),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Capability for FlagCap {
|
|
|
|
type Result = bool;
|
|
|
|
|
|
|
|
fn lookup(&self) -> Self::Result {
|
|
|
|
unsafe { tgetflag(self.0.as_ptr()) != 0 }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Calls the curses `setupterm()` function with the provided `$TERM` value `term` (or a null
|
|
|
|
/// pointer in case `term` is null) for the file descriptor `fd`. Returns a reference to the newly
|
|
|
|
/// initialized [`Term`] singleton on success or `None` if this failed.
|
|
|
|
///
|
|
|
|
/// The `configure` parameter may be set to a callback that takes an `&mut Term` reference to
|
|
|
|
/// override any capabilities before the `Term` is permanently made immutable.
|
|
|
|
///
|
|
|
|
/// Note that the `errret` parameter is provided to the function, meaning curses will not write
|
|
|
|
/// error output to stderr in case of failure.
|
|
|
|
///
|
|
|
|
/// Any existing references from `curses::term()` will be invalidated by this call!
|
|
|
|
pub fn setup<F>(term: Option<&CStr>, fd: i32, configure: F) -> Option<Arc<Term>>
|
|
|
|
where
|
|
|
|
F: Fn(&mut Term),
|
|
|
|
{
|
|
|
|
// For now, use the same TERM lock when using `cur_term` to prevent any race conditions in
|
|
|
|
// curses itself. We might split this to another lock in the future.
|
|
|
|
let mut global_term = TERM.lock().expect("Mutex poisoned!");
|
|
|
|
|
|
|
|
let result = unsafe {
|
|
|
|
// If cur_term is already initialized for a different $TERM value, calling setupterm() again
|
|
|
|
// will leak memory. Call del_curterm() first to free previously allocated resources.
|
|
|
|
let _ = sys::del_curterm(cur_term);
|
|
|
|
|
|
|
|
let mut err = 0;
|
|
|
|
if let Some(term) = term {
|
|
|
|
sys::setupterm(term.as_ptr(), fd, &mut err)
|
|
|
|
} else {
|
|
|
|
sys::setupterm(core::ptr::null(), fd, &mut err)
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Safely store the new Term instance or replace the old one. We have the lock so it's safe to
|
|
|
|
// drop the old TERM value and have its refcount decremented - no one will be cloning it.
|
|
|
|
if result == sys::OK {
|
|
|
|
// Create a new `Term` instance, prepopulate the capabilities we care about, and allow the
|
|
|
|
// caller to override any as needed.
|
|
|
|
let mut term = Term::new();
|
|
|
|
(configure)(&mut term);
|
|
|
|
|
|
|
|
let term = Arc::new(term);
|
|
|
|
*global_term = Some(term.clone());
|
|
|
|
Some(term)
|
|
|
|
} else {
|
|
|
|
*global_term = None;
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Resets the curses `cur_term` TERMINAL pointer. Subsequent calls to [`curses::term()`](term())
|
|
|
|
/// will return `None`.
|
|
|
|
pub fn reset() {
|
|
|
|
let mut term = TERM.lock().expect("Mutex poisoned!");
|
|
|
|
if term.is_some() {
|
|
|
|
unsafe {
|
|
|
|
// Ignore the result of del_curterm() as the only documented error is that
|
|
|
|
// `cur_term` was already null.
|
|
|
|
let _ = sys::del_curterm(cur_term);
|
|
|
|
sys::cur_term = core::ptr::null();
|
|
|
|
}
|
|
|
|
*term = None;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
|
|
struct Code {
|
|
|
|
/// The two-char termcap code for the capability, followed by a nul.
|
|
|
|
code: [u8; 3],
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Code {
|
|
|
|
/// `code` is the two-digit termcap code. See termcap(5) for a reference.
|
|
|
|
///
|
|
|
|
/// Panics if anything other than a two-ascii-character `code` is passed into the function. It
|
|
|
|
/// would take a hard-coded `[u8; 2]` parameter but that is less ergonomic. Since all our
|
|
|
|
/// termcap `Code`s are compile-time constants, the panic is a compile-time error, meaning
|
|
|
|
/// there's no harm to going this more ergonomic route.
|
|
|
|
const fn new(code: &str) -> Code {
|
|
|
|
let code = code.as_bytes();
|
|
|
|
if code.len() != 2 {
|
|
|
|
panic!("Invalid termcap code provided!");
|
|
|
|
}
|
|
|
|
Code {
|
|
|
|
code: [code[0], code[1], b'\0'],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The nul-terminated termcap id of the capability.
|
|
|
|
pub const fn as_ptr(&self) -> *const libc::c_char {
|
|
|
|
self.code.as_ptr().cast()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
|
|
struct StringCap {
|
|
|
|
code: Code,
|
|
|
|
}
|
|
|
|
impl StringCap {
|
|
|
|
const fn new(code: &str) -> Self {
|
|
|
|
StringCap {
|
|
|
|
code: Code::new(code),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
|
|
struct NumberCap(Code);
|
|
|
|
impl NumberCap {
|
|
|
|
const fn new(code: &str) -> Self {
|
|
|
|
NumberCap(Code::new(code))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
|
|
struct FlagCap(Code);
|
|
|
|
impl FlagCap {
|
|
|
|
const fn new(code: &str) -> Self {
|
|
|
|
FlagCap(Code::new(code))
|
|
|
|
}
|
|
|
|
}
|
2023-05-07 22:14:19 +00:00
|
|
|
|
|
|
|
/// Covers over tparm().
|
|
|
|
pub fn tparm1(cap: &CStr, param1: i32) -> Option<CString> {
|
|
|
|
// Take the lock because tparm races with del_curterm, etc.
|
|
|
|
let _term: std::sync::MutexGuard<Option<Arc<Term>>> = TERM.lock().unwrap();
|
|
|
|
assert!(!cap.to_bytes().is_empty());
|
|
|
|
let cap_ptr = cap.as_ptr() as *mut libc::c_char;
|
|
|
|
// Safety: we're trusting tparm here.
|
|
|
|
unsafe { try_ptr_to_cstr(tparm(cap_ptr, param1 as libc::c_int)) }
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Wrapper over tputs.
|
|
|
|
/// The caller is responsible for synchronization.
|
|
|
|
pub fn tputs(str: &CStr, affcnt: libc::c_int, putc: sys::putc_t) -> libc::c_int {
|
|
|
|
let str_ptr = str.as_ptr() as *mut libc::c_char;
|
|
|
|
// Safety: we're trusting tputs here.
|
|
|
|
unsafe { sys::tputs(str_ptr, affcnt, putc) }
|
|
|
|
}
|