use std::cell::RefCell;
use std::sync::mpsc::{Sender, channel};
use std::time::Duration;
thread_local! {
static SINK: RefCell<Option<Sender<Request>>> = const { RefCell::new(None) };
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Level {
Success,
Warn,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PromptKind {
Confirm,
Input {
placeholder: Option<String>,
},
Select {
items: Vec<String>,
allow_other: bool,
},
MultiSelect {
items: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Answer {
Bool(bool),
Text(String),
Many(Vec<String>),
}
pub enum Request {
Prompt {
kind: PromptKind,
prompt: String,
error: Option<String>,
reply: Sender<Option<Answer>>,
},
Message {
level: Level,
text: String,
},
Spinner(Option<String>),
Suspend(Sender<()>),
Resume,
}
#[derive(Debug)]
pub struct Cancelled;
impl std::fmt::Display for Cancelled {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Cancelled")
}
}
impl std::error::Error for Cancelled {}
pub fn install(sender: Sender<Request>) {
SINK.with(|s| *s.borrow_mut() = Some(sender));
}
pub fn uninstall() {
SINK.with(|s| *s.borrow_mut() = None);
}
pub fn active() -> bool {
SINK.with(|s| s.borrow().is_some())
}
fn send(request: Request) -> bool {
SINK.with(|s| match s.borrow().as_ref() {
Some(sender) => sender.send(request).is_ok(),
None => false,
})
}
pub fn message(level: Level, text: &str) {
send(Request::Message {
level,
text: text.to_string(),
});
}
pub fn spinner(text: Option<&str>) {
send(Request::Spinner(text.map(str::to_string)));
}
pub fn prompt(kind: PromptKind, prompt: &str, error: Option<String>) -> anyhow::Result<Answer> {
let (reply, answer) = channel();
let sent = send(Request::Prompt {
kind,
prompt: prompt.to_string(),
error,
reply,
});
match answer.recv() {
Ok(Some(answer)) if sent => Ok(answer),
_ => Err(Cancelled.into()),
}
}
pub struct Suspended;
impl Drop for Suspended {
fn drop(&mut self) {
send(Request::Resume);
}
}
pub fn suspend() -> anyhow::Result<Option<Suspended>> {
if !active() {
return Ok(None);
}
let (ack, restored) = channel();
if !send(Request::Suspend(ack)) {
return Ok(None);
}
restored
.recv_timeout(Duration::from_secs(5))
.map_err(|_| anyhow::anyhow!("the TUI did not hand over the terminal"))?;
Ok(Some(Suspended))
}
#[cfg(test)]
#[path = "ui_test.rs"]
mod tests;