use std::{
any::Any,
borrow::Cow,
str::FromStr,
sync::mpsc as std_mpsc,
time::{Duration, Instant},
};
#[cfg(not(feature = "global"))]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "history")]
use std::sync::{Arc, RwLock, RwLockReadGuard};
#[cfg(feature = "completion")]
use std::num::NonZeroU32;
use bitflags::bitflags;
use crossterm::event::Event;
use crossterm::style::{
Attribute as CtAttribute, Attributes as CtAttributes, Color as CtColor,
};
use tokio::sync::mpsc as tokio_mpsc;
mod line;
pub use line::*;
mod term;
mod worker;
use term::*;
#[cfg(unix)]
mod unix_util;
#[cfg(feature = "history")]
mod history;
#[cfg(feature = "history")]
pub use history::*;
#[cfg(feature = "completion")]
mod completion;
#[cfg(feature = "completion")]
pub use completion::*;
#[cfg(feature = "capture-stderr")]
mod stderr_capture;
const ESCAPE_DELAY: Duration = Duration::new(0, 1000000000 / 24);
struct DummyError {}
type LifeOrDeath = std::result::Result<(), DummyError>;
impl From<std::io::Error> for DummyError {
fn from(_: std::io::Error) -> DummyError {
DummyError {}
}
}
impl<T> From<tokio_mpsc::error::SendError<T>> for DummyError {
fn from(_: tokio_mpsc::error::SendError<T>) -> DummyError {
DummyError {}
}
}
impl<T> From<std_mpsc::SendError<T>> for DummyError {
fn from(_: std_mpsc::SendError<T>) -> DummyError {
DummyError {}
}
}
impl From<std_mpsc::RecvError> for DummyError {
fn from(_: std_mpsc::RecvError) -> DummyError {
DummyError {}
}
}
impl From<std_mpsc::RecvTimeoutError> for DummyError {
fn from(_: std_mpsc::RecvTimeoutError) -> DummyError {
DummyError {}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
pub enum Color {
Black = 0,
Red = 1,
Green = 2,
Yellow = 3,
Blue = 4,
Cyan = 5,
Magenta = 6,
White = 7,
}
impl Color {
fn to_crossterm(self) -> CtColor {
match self {
Color::Black => CtColor::Black,
Color::Red => CtColor::DarkRed,
Color::Green => CtColor::DarkGreen,
Color::Yellow => CtColor::DarkYellow,
Color::Blue => CtColor::DarkBlue,
Color::Cyan => CtColor::DarkCyan,
Color::Magenta => CtColor::DarkMagenta,
Color::White => CtColor::Grey,
}
}
fn to_atari16_bright(self) -> u8 {
match self {
Color::Black => 8,
Color::Red => 1,
Color::Green => 2,
Color::Yellow => 13,
Color::Blue => 4,
Color::Cyan => 9,
Color::Magenta => 12,
Color::White => 0,
}
}
fn to_atari16_dim(self) -> u8 {
match self {
Color::Black => 15,
Color::Red => 3,
Color::Green => 5,
Color::Yellow => 11,
Color::Blue => 6,
Color::Cyan => 10,
Color::Magenta => 14,
Color::White => 7,
}
}
fn to_atari4(self) -> u8 {
match self {
Color::Black => 15,
Color::Red => 1,
Color::Green => 2,
Color::Yellow => 2,
Color::Blue => 3,
Color::Cyan => 2,
Color::Magenta => 1,
Color::White => 0,
}
}
}
bitflags! {
#[cfg_attr(feature="serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Style: u32 {
const PLAIN = 0;
const BOLD = 1 << 0;
const DIM = 1 << 1;
const UNDERLINE = 1 << 2;
const INVERSE = 1 << 3;
#[doc(alias="INVERSE")]
const REVERSE = 1 << 3;
const ITALIC = 1 << 4;
}
}
impl Style {
fn as_crossterm(&self) -> CtAttributes {
let mut ret = CtAttributes::default();
if self.contains(Style::BOLD) {
ret.set(CtAttribute::Bold)
}
if self.contains(Style::DIM) {
ret.set(CtAttribute::Dim)
}
if self.contains(Style::UNDERLINE) {
ret.set(CtAttribute::Underlined)
}
if self.contains(Style::INVERSE) {
ret.set(CtAttribute::Reverse)
}
if self.contains(Style::ITALIC) {
ret.set(CtAttribute::Italic)
}
ret
}
}
pub struct Output {
tx: std_mpsc::Sender<Request>,
}
pub struct OutputOnly(Output);
pub struct InputOutput {
output: Output,
rx: tokio_mpsc::UnboundedReceiver<Response>,
#[cfg(feature = "history")]
history: Arc<RwLock<History>>,
death_count: u32,
}
const MAX_DEATH_COUNT: u32 = 9;
enum Request {
Output(Line),
#[cfg(feature = "wrap")]
OutputWrapped(Line),
OutputEcho(Line),
Status(Option<Line>),
Notice(Line, Duration),
Prompt {
line: Option<Line>,
input_allowed: bool,
clear_input: bool,
},
SuspendAndRun(Box<dyn FnMut() + Send>),
Bell,
Die,
RawInput(String),
Heartbeat,
CrosstermEvent(crossterm::event::Event),
Custom(Box<dyn Any + Send>),
#[cfg(feature = "history")]
BumpHistory,
#[cfg(feature = "completion")]
SetCompletor(Option<Box<dyn Completor>>),
#[cfg(feature = "capture-stderr")]
StderrLine(String),
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Response {
Input(String),
Dead,
Quit,
Discarded(String),
Finish,
Info,
Break,
Escape,
Swap,
Custom(Box<dyn Any + Send>),
Unknown(u8),
}
impl Response {
pub fn as_unknown(&self) -> u8 {
match self {
&Response::Input(_) => 10,
&Response::Discarded(_) => 7,
&Response::Custom(_) => 0,
&Response::Quit => 3,
&Response::Finish => 4,
&Response::Info => 20,
&Response::Dead | &Response::Break => 28,
&Response::Escape => 27,
&Response::Swap => 24,
&Response::Unknown(x) => x,
}
}
}
impl Output {
fn send(&self, thing: Request) {
self.tx.send(thing).expect("Liso output has stopped");
}
pub fn println<T>(&self, line: T)
where
T: Into<Line>,
{
self.send(Request::Output(line.into()))
}
#[cfg(feature = "wrap")]
pub fn wrapln<T>(&self, line: T)
where
T: Into<Line>,
{
self.send(Request::OutputWrapped(line.into()))
}
pub fn echoln<T>(&self, line: T)
where
T: Into<Line>,
{
self.send(Request::OutputEcho(line.into()))
}
pub fn status<T>(&self, line: Option<T>)
where
T: Into<Line>,
{
self.send(Request::Status(line.map(T::into)))
}
pub fn remove_status(&self) {
self.send(Request::Status(None))
}
pub fn notice<T>(&self, line: T, max_duration: Duration)
where
T: Into<Line>,
{
self.send(Request::Notice(line.into(), max_duration))
}
pub fn prompt<T>(&self, line: T, input_allowed: bool, clear_input: bool)
where
T: Into<Line>,
{
let line: Line = line.into();
self.send(Request::Prompt {
line: if line.is_empty() { None } else { Some(line) },
input_allowed,
clear_input,
})
}
#[deprecated = "Use `prompt` with a blank line instead."]
#[doc(hidden)]
pub fn remove_prompt(&self, input_allowed: bool, clear_input: bool) {
self.send(Request::Prompt {
line: None,
input_allowed,
clear_input,
})
}
pub fn bell(&self) {
self.send(Request::Bell)
}
pub fn suspend_and_run<F: 'static + FnMut() + Send>(&self, f: F) {
self.send(Request::SuspendAndRun(Box::new(f)))
}
pub fn clone_output(&self) -> OutputOnly {
OutputOnly(Output {
tx: self.tx.clone(),
})
}
#[deprecated = "Use `clone_output` instead."]
#[doc(hidden)]
pub fn clone_sender(&self) -> OutputOnly {
self.clone_output()
}
pub fn send_custom<T: Any + Send>(&self, value: T) {
self.send(Request::Custom(Box::new(value)))
}
pub fn send_custom_box(&self, value: Box<dyn Any + Send>) {
self.send(Request::Custom(value))
}
#[cfg(feature = "completion")]
pub fn set_completor(&self, completor: Option<Box<dyn Completor>>) {
self.send(Request::SetCompletor(completor))
}
}
impl Drop for InputOutput {
fn drop(&mut self) {
#[cfg(feature = "global")]
{
*LISO_OUTPUT_TX.lock() = None;
}
self.actually_blocking_die();
#[cfg(not(feature = "global"))]
LISO_IS_ACTIVE.store(false, Ordering::Release);
#[cfg(feature = "capture-stderr")]
stderr_capture::wait_until_not_captured();
}
}
impl core::ops::Deref for InputOutput {
type Target = Output;
fn deref(&self) -> &Output {
&self.output
}
}
impl InputOutput {
#[allow(clippy::new_without_default)]
pub fn new() -> InputOutput {
let we_are_alone;
#[cfg(feature = "global")]
let mut global_lock = LISO_OUTPUT_TX.lock();
#[cfg(feature = "global")]
{
we_are_alone = global_lock.is_none();
}
#[cfg(not(feature = "global"))]
match LISO_IS_ACTIVE.compare_exchange(
false,
true,
Ordering::Acquire,
Ordering::Relaxed,
) {
Ok(_) => we_are_alone = true,
Err(_) => we_are_alone = false,
}
if !we_are_alone {
panic!(
"Tried to have multiple `liso::InputOutput` instances \
active at the same time!"
)
}
let (request_tx, request_rx) = std_mpsc::channel();
let (response_tx, response_rx) = tokio_mpsc::unbounded_channel();
let request_tx_clone = request_tx.clone();
#[cfg(feature = "history")]
let history = Arc::new(RwLock::new(History::new()));
#[cfg(feature = "history")]
let history_clone = history.clone();
std::thread::Builder::new()
.name("Liso output thread".to_owned())
.spawn(move || {
#[cfg(feature = "history")]
let _ = worker::worker(
request_tx_clone,
request_rx,
response_tx,
history_clone,
);
#[cfg(not(feature = "history"))]
let _ =
worker::worker(request_tx_clone, request_rx, response_tx);
})
.unwrap();
#[cfg(feature = "global")]
{
*global_lock = Some(request_tx.clone());
}
InputOutput {
output: Output { tx: request_tx },
rx: response_rx,
death_count: 0,
#[cfg(feature = "history")]
history,
}
}
pub async fn die(mut self) {
if self.output.tx.send(Request::Die).is_err() {
return;
}
loop {
if let Response::Dead = self.read_async().await {
break;
}
}
}
fn actually_blocking_die(&mut self) {
if self.output.tx.send(Request::Die).is_err() {
return;
}
loop {
match self.try_read() {
None => std::thread::yield_now(),
Some(Response::Dead) => break,
_ => (),
}
}
}
pub fn blocking_die(mut self) {
self.actually_blocking_die()
}
fn report_death(&mut self) {
self.death_count = self.death_count.saturating_add(1);
if self.death_count >= MAX_DEATH_COUNT {
panic!("Client program is looping forever despite receiving `Response::Dead` {} times. Program bug!", MAX_DEATH_COUNT);
}
}
pub async fn read_async(&mut self) -> Response {
match self.rx.recv().await {
None => {
self.report_death();
Response::Dead
}
Some(x) => x,
}
}
#[deprecated = "Use `read_async` instead."]
#[doc(hidden)]
pub async fn read(&mut self) -> Response {
self.read_async().await
}
pub fn read_timeout(&mut self, timeout: Duration) -> Option<Response> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.expect(
"Couldn't create temporary Tokio runtime for `read_timeout`",
);
rt.block_on(async {
let timeout = tokio::time::timeout(timeout, self.rx.recv());
match timeout.await {
Ok(None) => {
self.report_death();
Some(Response::Dead)
}
Ok(Some(x)) => Some(x),
Err(_) => None,
}
})
}
pub fn read_deadline(&mut self, deadline: Instant) -> Option<Response> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.expect(
"Couldn't create temporary Tokio runtime for `read_deadline`",
);
rt.block_on(async {
let timeout = tokio::time::timeout_at(
tokio::time::Instant::from_std(deadline),
self.rx.recv(),
);
match timeout.await {
Ok(None) => {
self.report_death();
Some(Response::Dead)
}
Ok(Some(x)) => Some(x),
Err(_) => None,
}
})
}
pub fn read_blocking(&mut self) -> Response {
match self.rx.blocking_recv() {
None => {
self.report_death();
Response::Dead
}
Some(x) => x,
}
}
#[deprecated = "Use `read_blocking` instead."]
#[doc(hidden)]
pub fn blocking_read(&mut self) -> Response {
self.read_blocking()
}
pub fn try_read(&mut self) -> Option<Response> {
use tokio::sync::mpsc::error::TryRecvError;
match self.rx.try_recv() {
Ok(x) => Some(x),
Err(TryRecvError::Disconnected) => {
self.report_death();
Some(Response::Dead)
}
Err(TryRecvError::Empty) => None,
}
}
#[cfg(feature = "history")]
pub fn swap_history(&self, mut history: History) -> History {
let mut lock = self.history.write().unwrap();
std::mem::swap(&mut history, &mut *lock);
drop(lock);
let _ = self.tx.send(Request::BumpHistory);
history
}
#[cfg(feature = "history")]
pub fn read_history(&self) -> RwLockReadGuard<History> {
self.history.read().unwrap()
}
}
impl core::ops::Deref for OutputOnly {
type Target = Output;
fn deref(&self) -> &Output {
&self.0
}
}
impl Clone for OutputOnly {
fn clone(&self) -> OutputOnly {
self.clone_output()
}
}
#[cfg(feature = "wrap")]
fn convert_subset_slice_to_range(outer: &str, inner: &str) -> (usize, usize) {
if inner.is_empty() {
return (0, 0);
}
let outer_start = outer.as_ptr() as usize;
let outer_end = outer_start.checked_add(outer.len()).unwrap();
let inner_start = inner.as_ptr() as usize;
let inner_end = inner_start.checked_add(inner.len()).unwrap();
assert!(inner_start >= outer_start);
assert!(inner_end <= outer_end);
(inner_start - outer_start, inner_end - outer_start)
}
#[macro_export]
#[doc(hidden)]
macro_rules! color {
(Black) => {
Some($crate::Color::Black)
};
(Red) => {
Some($crate::Color::Red)
};
(Green) => {
Some($crate::Color::Green)
};
(Yellow) => {
Some($crate::Color::Yellow)
};
(Blue) => {
Some($crate::Color::Blue)
};
(Cyan) => {
Some($crate::Color::Cyan)
};
(Magenta) => {
Some($crate::Color::Magenta)
};
(White) => {
Some($crate::Color::White)
};
(none) => {
None
};
(black) => {
Some($crate::Color::Black)
};
(red) => {
Some($crate::Color::Red)
};
(green) => {
Some($crate::Color::Green)
};
(yellow) => {
Some($crate::Color::Yellow)
};
(blue) => {
Some($crate::Color::Blue)
};
(cyan) => {
Some($crate::Color::Cyan)
};
(magenta) => {
Some($crate::Color::Magenta)
};
(white) => {
Some($crate::Color::White)
};
(none) => {
None
};
($other:expr) => {
$other
};
}
#[macro_export]
macro_rules! liso_add {
($line:ident, reset, $($rest:tt)*) => {
$line.reset_all();
$crate::liso_add!($line, $($rest)*);
};
($line:ident, reset) => {
$line.reset_all();
};
($line:ident, fg = $color:tt, $($rest:tt)*) => {
$line.set_fg_color($crate::color!($color));
$crate::liso_add!($line, $($rest)*);
};
($line:ident, fg = $color:tt) => {
$line.set_fg_color($crate::color!($color));
};
($line:ident, bg = $color:tt, $($rest:tt)*) => {
$line.set_bg_color($crate::color!($color));
$crate::liso_add!($line, $($rest)*);
};
($line:ident, bg = $color:tt) => {
$line.set_bg_color($crate::color!($color));
};
($line:ident, fg = $color:expr, $($rest:tt)*) => {
$line.set_fg_color($color);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, fg = $color:expr) => {
$line.set_fg_color($color);
};
($line:ident, bg = $color:expr, $($rest:tt)*) => {
$line.set_bg_color($color);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, bg = $color:expr) => {
$line.set_bg_color($color);
};
($line:ident, plain $($rest:tt)*) => {
$line.set_style($crate::Style::PLAIN);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, bold $($rest:tt)*) => {
$line.set_style($crate::Style::BOLD);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, dim $($rest:tt)*) => {
$line.set_style($crate::Style::DIM);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, underline $($rest:tt)*) => {
$line.set_style($crate::Style::UNDERLINE);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, inverse $($rest:tt)*) => {
$line.set_style($crate::Style::INVERSE);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, reverse $($rest:tt)*) => {
$line.set_style($crate::Style::INVERSE);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, italic $($rest:tt)*) => {
$line.set_style($crate::Style::ITALIC);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, +bold $($rest:tt)*) => {
$line.activate_style($crate::Style::BOLD);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, +dim $($rest:tt)*) => {
$line.activate_style($crate::Style::DIM);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, +underline $($rest:tt)*) => {
$line.activate_style($crate::Style::UNDERLINE);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, +inverse $($rest:tt)*) => {
$line.activate_style($crate::Style::INVERSE);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, +reverse $($rest:tt)*) => {
$line.activate_style($crate::Style::INVERSE);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, +italic $($rest:tt)*) => {
$line.activate_style($crate::Style::ITALIC);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, -bold $($rest:tt)*) => {
$line.deactivate_style($crate::Style::BOLD);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, -dim $($rest:tt)*) => {
$line.deactivate_style($crate::Style::DIM);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, -underline $($rest:tt)*) => {
$line.deactivate_style($crate::Style::UNDERLINE);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, -inverse $($rest:tt)*) => {
$line.deactivate_style($crate::Style::INVERSE);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, -reverse $($rest:tt)*) => {
$line.deactivate_style($crate::Style::INVERSE);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, -italic $($rest:tt)*) => {
$line.deactivate_style($crate::Style::ITALIC);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, ^bold $($rest:tt)*) => {
$line.toggle_style($crate::Style::BOLD);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, ^dim $($rest:tt)*) => {
$line.toggle_style($crate::Style::DIM);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, ^underline $($rest:tt)*) => {
$line.toggle_style($crate::Style::UNDERLINE);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, ^inverse $($rest:tt)*) => {
$line.toggle_style($crate::Style::INVERSE);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, ^reverse $($rest:tt)*) => {
$line.toggle_style($crate::Style::INVERSE);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, ^italic $($rest:tt)*) => {
$line.toggle_style($crate::Style::ITALIC);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, ansi $expr:expr, $($rest:tt)*) => {
$line.add_ansi_text($expr);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, ansi $expr:expr) => {
$line.add_ansi_text($expr);
};
($line:ident, $expr:expr, $($rest:tt)*) => {
$line.add_text($expr);
$crate::liso_add!($line, $($rest)*);
};
($line:ident, $expr:expr) => {
$line.add_text($expr);
};
($line:ident,, $($rest:tt)*) => {
$crate::liso_add!($line, $($rest)*);
};
($line:ident$(,)*) => {
};
}
#[macro_export]
macro_rules! liso {
($($rest:tt)*) => {
{
let mut line = $crate::Line::new();
$crate::liso_add!(line, $($rest)*,);
line
}
};
}
#[deprecated = "This type was renamed to `InputOutput` to improve clarity.\n\
To continue using this name without warnings, try `use \
liso::InputOutput as IO;`"]
#[doc(hidden)]
pub type IO = InputOutput;
#[deprecated = "This type was split into `Output` and `OutputOnly` to improve \
clarity.\nReplace with `&Output` or `OutputOnly` as needed."]
#[doc(hidden)]
pub type Sender = OutputOnly;
#[cfg(not(feature = "global"))]
static LISO_IS_ACTIVE: AtomicBool = AtomicBool::new(false);
#[cfg(feature = "global")]
static LISO_OUTPUT_TX: parking_lot::Mutex<Option<std_mpsc::Sender<Request>>> =
parking_lot::Mutex::new(None);
#[cfg(feature = "global")]
pub fn output() -> OutputOnly {
match &*LISO_OUTPUT_TX.lock() {
None => {
panic!("liso::output() called with no liso::InputOutput alive")
}
Some(x) => OutputOnly(Output { tx: x.clone() }),
}
}
#[cfg(feature = "global")]
#[macro_export]
macro_rules! println {
($($rest:tt)*) => {
$crate::output().println(liso!($($rest)*))
}
}
#[cfg(all(feature = "global", feature = "wrap"))]
#[macro_export]
macro_rules! wrapln {
($($rest:tt)*) => {
$crate::output().wrapln(liso!($($rest)*))
}
}