use crate::clear::*;
use crate::cli::Cli;
use crate::cursor::*;
use crate::error::Error;
use crate::keys::Key;
use crate::result::Result;
use crate::CrLf;
use crate::UnicodeString;
use cfg_if::cfg_if;
use futures::*;
pub use pad::PadStr;
use regex::Regex;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, LockResult, Mutex, MutexGuard};
use workflow_core::channel::{unbounded, Channel, DuplexChannel, Receiver, Sender};
use workflow_core::task::spawn;
use workflow_log::log_error;
const DEFAULT_PARA_WIDTH: usize = 80;
pub struct Modifiers {
pub alt: bool,
pub shift: bool,
pub ctrl: bool,
pub meta: bool,
}
pub type LinkMatcherHandlerFn = Arc<Box<(dyn Fn(Modifiers, &str))>>;
#[derive(Debug, Clone)]
pub enum Event {
Copy,
Paste,
}
pub type EventHandlerFn = Arc<Box<(dyn Fn(Event))>>;
mod options;
pub use options::Options;
pub use options::TargetElement;
pub mod bindings;
pub mod xterm;
pub use xterm::{Theme, ThemeOption};
cfg_if! {
if #[cfg(target_arch = "wasm32")] {
use crate::terminal::xterm::Xterm as Interface;
} else if #[cfg(termion)] {
pub mod termion;
use crate::terminal::termion::Termion as Interface;
} else {
pub mod crossterm;
use crate::terminal::crossterm::Crossterm as Interface;
pub use crate::terminal::crossterm::{disable_raw_mode,init_panic_hook};
}
}
#[derive(Debug)]
pub struct Inner {
pub buffer: UnicodeString,
history: Vec<UnicodeString>,
pub cursor: usize,
history_index: usize,
}
impl Default for Inner {
fn default() -> Self {
Self::new()
}
}
impl Inner {
pub fn new() -> Self {
Inner {
buffer: UnicodeString::default(),
history: vec![],
cursor: 0,
history_index: 0,
}
}
pub fn reset_line_buffer(&mut self) {
self.buffer.clear();
self.cursor = 0;
}
}
#[derive(Clone)]
struct UserInput {
prompt: Arc<Mutex<Option<String>>>,
buffer: Arc<Mutex<UnicodeString>>,
enabled: Arc<AtomicBool>,
secret: Arc<AtomicBool>,
kbhit: Arc<AtomicBool>,
terminate: Arc<AtomicBool>,
sender: Sender<String>,
receiver: Receiver<String>,
}
impl UserInput {
pub fn new() -> Self {
let (sender, receiver) = unbounded();
UserInput {
prompt: Arc::new(Mutex::new(None)),
buffer: Arc::new(Mutex::new(UnicodeString::default())),
enabled: Arc::new(AtomicBool::new(false)),
secret: Arc::new(AtomicBool::new(false)),
kbhit: Arc::new(AtomicBool::new(false)),
terminate: Arc::new(AtomicBool::new(false)),
sender,
receiver,
}
}
pub fn get_prompt(&self) -> Option<String> {
self.prompt.lock().unwrap().clone()
}
pub fn get_buffer(&self) -> String {
self.buffer.lock().unwrap().clone().to_string()
}
pub fn open(&self, secret: bool, kbhit: bool, prompt: Option<String>) -> Result<()> {
*self.prompt.lock().unwrap() = prompt;
self.enabled.store(true, Ordering::SeqCst);
self.secret.store(secret, Ordering::SeqCst);
self.kbhit.store(kbhit, Ordering::SeqCst);
self.terminate.store(false, Ordering::SeqCst);
Ok(())
}
pub fn close(&self) -> Result<()> {
let s = {
self.prompt.lock().unwrap().take();
let mut buffer = self.buffer.lock().unwrap();
let s = buffer.clone();
buffer.clear();
s
};
self.enabled.store(false, Ordering::SeqCst);
self.terminate.store(true, Ordering::SeqCst);
self.sender.try_send(s.to_string()).unwrap();
Ok(())
}
pub async fn capture(
&self,
secret: bool,
kbhit: bool,
prompt: Option<String>,
term: &Arc<Terminal>,
) -> Result<String> {
self.open(secret, kbhit, prompt)?;
let term = term.clone();
let terminate = self.terminate.clone();
cfg_if! {
if #[cfg(target_arch = "wasm32")] {
workflow_core::task::dispatch(async move {
let _result = term.term().intake(&terminate).await;
});
} else {
workflow_core::task::spawn(async move {
let _result = term.term().intake(&terminate).await;
});
}
}
let string = self.receiver.recv().await?;
Ok(string)
}
fn is_enabled(&self) -> bool {
self.enabled.load(Ordering::SeqCst)
}
fn is_secret(&self) -> bool {
self.secret.load(Ordering::SeqCst)
}
fn is_kbhit(&self) -> bool {
self.kbhit.load(Ordering::SeqCst)
}
fn ingest(&self, key: Key, term: &Arc<Terminal>) -> Result<()> {
match key {
Key::Ctrl('c') => {
self.close()?;
term.abort();
}
Key::Char(ch) => {
self.buffer.lock().unwrap().push(ch);
if !self.is_secret() {
term.write(ch);
}
if self.is_kbhit() {
term.crlf();
self.close()?;
}
}
Key::Backspace => {
self.buffer.lock().unwrap().pop();
if !self.is_secret() {
term.write("\x08 \x08");
}
}
Key::Enter => {
term.crlf();
self.close()?;
}
_ => {}
}
Ok(())
}
#[allow(dead_code)]
pub fn inject<S: ToString>(&self, text: S, term: &Terminal) -> Result<()> {
self.inject_impl(text.to_string().into(), term)?;
Ok(())
}
fn inject_impl(&self, text: UnicodeString, term: &Terminal) -> Result<()> {
let mut buffer = self.buffer.lock().unwrap();
if !self.is_secret() {
text.iter().for_each(|ch| {
term.write(ch);
});
}
buffer.extend(text);
Ok(())
}
}
#[derive(Clone)]
pub struct Terminal {
inner: Arc<Mutex<Inner>>,
pub running: Arc<AtomicBool>,
pub prompt: Arc<Mutex<String>>,
pub term: Arc<Interface>,
pub handler: Arc<dyn Cli>,
pub terminate: Arc<AtomicBool>,
user_input: UserInput,
pub pipe_raw: Channel<String>,
pub pipe_crlf: Channel<String>,
pub pipe_ctl: DuplexChannel<()>,
pub para_width: Arc<AtomicUsize>,
}
impl Terminal {
pub fn try_new(handler: Arc<dyn Cli>, prompt: &str) -> Result<Self> {
let term = Arc::new(Interface::try_new()?);
let terminal = Self {
inner: Arc::new(Mutex::new(Inner::new())),
running: Arc::new(AtomicBool::new(false)),
prompt: Arc::new(Mutex::new(prompt.to_string())),
term,
handler,
terminate: Arc::new(AtomicBool::new(false)),
user_input: UserInput::new(),
pipe_raw: Channel::unbounded(),
pipe_crlf: Channel::unbounded(),
pipe_ctl: DuplexChannel::oneshot(),
para_width: Arc::new(AtomicUsize::new(DEFAULT_PARA_WIDTH)),
};
Ok(terminal)
}
pub fn try_new_with_options(
handler: Arc<dyn Cli>,
options: Options,
) -> Result<Self> {
let term = Arc::new(Interface::try_new_with_options(&options)?);
let terminal = Self {
inner: Arc::new(Mutex::new(Inner::new())),
running: Arc::new(AtomicBool::new(false)),
prompt: Arc::new(Mutex::new(options.prompt())),
term,
handler,
terminate: Arc::new(AtomicBool::new(false)),
user_input: UserInput::new(),
pipe_raw: Channel::unbounded(),
pipe_crlf: Channel::unbounded(),
pipe_ctl: DuplexChannel::oneshot(),
para_width: Arc::new(AtomicUsize::new(DEFAULT_PARA_WIDTH)),
};
Ok(terminal)
}
pub async fn init(self: &Arc<Self>) -> Result<()> {
self.term.init(self).await?;
self.handler.clone().init(self)?;
Ok(())
}
pub fn inner(&self) -> LockResult<MutexGuard<'_, Inner>> {
self.inner.lock()
}
pub fn history(&self) -> Vec<UnicodeString> {
let data = self.inner().unwrap();
data.history.clone()
}
pub fn reset_line_buffer(&self) {
self.inner().unwrap().reset_line_buffer();
}
pub fn get_prompt(&self) -> String {
if let Some(prompt) = self.handler.prompt() {
prompt
} else {
self.prompt.lock().unwrap().clone()
}
}
pub fn prompt(&self) {
let mut data = self.inner().unwrap();
data.cursor = 0;
data.buffer.clear();
self.term().write(self.get_prompt());
}
pub fn crlf(&self) {
self.term().write("\n\r".to_string());
}
pub fn write<S>(&self, s: S)
where
S: ToString,
{
self.term().write(s);
}
pub fn writeln<S>(&self, s: S)
where
S: ToString,
{
if self.is_running() {
if self.user_input.is_enabled() {
if let Some(prompt) = self.user_input.get_prompt() {
self.write(format!("{}{}\n\r", ClearLine, s.to_string()));
self.write(prompt);
if !self.user_input.secret.load(Ordering::SeqCst) {
self.write(self.user_input.get_buffer());
}
}
} else {
self.write(format!("{}\n\r", s.to_string()));
}
} else {
self.write(format!("{}{}\n\r", ClearLine, s.to_string()));
let data = self.inner().unwrap();
let p = format!("{}{}", self.get_prompt(), data.buffer);
self.write(p);
let l = data.buffer.len() - data.cursor;
for _ in 0..l {
self.write("\x08".to_string());
}
}
}
pub fn refresh_prompt(&self) {
if !self.is_running() {
self.write(format!("{}", ClearLine));
let data = self.inner().unwrap();
let p = format!("{}{}", self.get_prompt(), data.buffer);
self.write(p);
let l = data.buffer.len() - data.cursor;
for _ in 0..l {
self.write("\x08".to_string());
}
}
}
pub fn para<S>(&self, text: S)
where
S: Into<String>,
{
let width = self
.term()
.cols()
.unwrap_or_else(|| self.para_width.load(Ordering::SeqCst));
let options = textwrap::Options::new(width).line_ending(textwrap::LineEnding::CRLF);
textwrap::wrap(text.into().as_str(), options)
.into_iter()
.for_each(|line| self.writeln(line));
}
pub fn para_with_options<'a, S, Opt>(&self, width_or_options: Opt, text: S)
where
S: Into<String>,
Opt: Into<textwrap::Options<'a>>,
{
textwrap::wrap(text.into().crlf().as_str(), width_or_options.into())
.into_iter()
.for_each(|line| self.writeln(line));
}
pub fn help<S: ToString, H: ToString>(
&self,
list: &[(S, H)],
separator: Option<&str>,
) -> Result<()> {
let mut list = list
.iter()
.map(|(cmd, help)| (cmd.to_string(), help.to_string()))
.collect::<Vec<_>>();
list.sort_by_key(|(cmd, _)| cmd.to_string());
let separator = separator.unwrap_or(" ");
let term_width: usize = self.cols().unwrap_or(80);
let cmd_width = list.iter().map(|(c, _)| c.len()).fold(0, |a, b| a.max(b)) + 2;
let help_width = term_width - cmd_width - 2 - 4 - separator.len();
let cmd_space = "".pad_to_width(cmd_width);
self.writeln("");
for (cmd, help) in list {
let mut first = true;
let options =
textwrap::Options::new(help_width).line_ending(textwrap::LineEnding::CRLF);
textwrap::wrap(help.as_str(), options)
.into_iter()
.for_each(|line| {
if first {
self.writeln(format!(
"{:>4}{}{}{}",
"",
cmd.pad_to_width(cmd_width),
separator,
line
));
first = false;
} else {
self.writeln(format!("{:>4}{cmd_space}{}{}", "", separator, line));
}
});
}
self.writeln("");
Ok(())
}
pub fn term(&self) -> Arc<Interface> {
Arc::clone(&self.term)
}
async fn pipe_start(self: &Arc<Self>) -> Result<()> {
let self_ = self.clone();
spawn(async move {
loop {
select! {
_ = self_.pipe_ctl.request.receiver.recv().fuse() => {
break;
},
raw = self_.pipe_raw.receiver.recv().fuse() => {
raw.map(|text|self_.write(text)).unwrap_or_else(|err|log_error!("Error writing from raw pipe: {err}"));
},
text = self_.pipe_crlf.receiver.recv().fuse() => {
text.map(|text|self_.writeln(text)).unwrap_or_else(|err|log_error!("Error writing from crlf pipe: {err}"));
},
}
}
self_
.pipe_ctl
.response
.sender
.send(())
.await
.unwrap_or_else(|err| log_error!("Error posting shutdown ctl: {err}"));
});
Ok(())
}
async fn pipe_stop(self: &Arc<Self>) -> Result<()> {
self.pipe_ctl.signal(()).await?;
Ok(())
}
fn pipe_abort(self: &Arc<Self>) -> Result<()> {
self.pipe_ctl.request.try_send(())?;
Ok(())
}
pub async fn run(self: &Arc<Self>) -> Result<()> {
self.pipe_start().await?;
self.term().run().await
}
pub async fn exit(self: &Arc<Self>) {
self.terminate.store(true, Ordering::SeqCst);
self.pipe_stop().await.unwrap_or_else(|err| panic!("{err}"));
self.term.exit();
}
pub fn abort(self: &Arc<Self>) {
self.terminate.store(true, Ordering::SeqCst);
self.pipe_abort().unwrap_or_else(|err| panic!("{err}"));
self.term.exit();
}
pub async fn ask(self: &Arc<Terminal>, secret: bool, prompt: &str) -> Result<String> {
self.reset_line_buffer();
self.term().write(prompt.to_string());
self.user_input
.capture(secret, false, Some(prompt.to_string()), self)
.await
}
pub async fn kbhit(self: &Arc<Terminal>, prompt: Option<&str>) -> Result<String> {
self.reset_line_buffer();
if let Some(prompt) = prompt {
self.term().write(prompt.to_string());
}
self.user_input
.capture(true, true, prompt.map(String::from), self)
.await
}
pub fn inject_unicode_string(&self, text: UnicodeString) -> Result<()> {
let mut data = self.inner()?;
self.inject_impl(&mut data, text)?;
Ok(())
}
pub fn inject<S: ToString>(&self, text: S) -> Result<()> {
let mut data = self.inner()?;
self.inject_impl(&mut data, text.to_string().into())
}
fn inject_impl(&self, data: &mut Inner, text: UnicodeString) -> Result<()> {
if self.user_input.is_enabled() {
self.user_input.inject_impl(text, self)
} else {
let len = text.len();
data.buffer.insert(data.cursor, text);
self.trail(data.cursor, &data.buffer, true, false, len);
data.cursor += len;
Ok(())
}
}
pub fn inject_char(&self, ch: char) -> Result<()> {
let mut data = self.inner()?;
self.inject_char_impl(&mut data, ch)?;
Ok(())
}
fn inject_char_impl(&self, data: &mut Inner, ch: char) -> Result<()> {
data.buffer.insert_char(data.cursor, ch);
self.trail(data.cursor, &data.buffer, true, false, 1);
data.cursor += 1;
Ok(())
}
async fn ingest(self: &Arc<Terminal>, key: Key, _term_key: String) -> Result<()> {
if self.user_input.is_enabled() {
self.user_input.ingest(key, self)?;
return Ok(());
}
match key {
Key::Backspace => {
let mut data = self.inner()?;
if data.cursor == 0 {
return Ok(());
}
self.write("\x08".to_string());
data.cursor -= 1;
let idx = data.cursor;
data.buffer.remove(idx);
self.trail(data.cursor, &data.buffer, true, true, 0);
}
Key::ArrowUp => {
let mut data = self.inner()?;
if data.history_index == 0 {
return Ok(());
}
let current_buffer = data.buffer.clone();
let index = data.history_index;
if data.history.len() <= index {
data.history.push(current_buffer);
} else {
data.history[index] = current_buffer;
}
data.history_index -= 1;
data.buffer = data.history[data.history_index].clone();
self.write(format!("{}{}{}", ClearLine, self.get_prompt(), data.buffer));
data.cursor = data.buffer.len();
}
Key::ArrowDown => {
let mut data = self.inner()?;
let len = data.history.len();
if data.history_index >= len {
return Ok(());
}
let index = data.history_index;
data.history[index] = data.buffer.clone();
data.history_index += 1;
if data.history_index == len {
data.buffer.clear();
} else {
data.buffer = data.history[data.history_index].clone();
}
self.write(format!("{}{}{}", ClearLine, self.get_prompt(), data.buffer));
data.cursor = data.buffer.len();
}
Key::ArrowLeft => {
let mut data = self.inner()?;
if data.cursor == 0 {
return Ok(());
}
data.cursor -= 1;
self.write(Left(1));
}
Key::ArrowRight => {
let mut data = self.inner()?;
if data.cursor < data.buffer.len() {
data.cursor += 1;
self.write(Right(1));
}
}
Key::Enter => {
let cmd = {
let mut data = self.inner()?;
let buffer = data.buffer.clone();
let length = data.history.len();
data.buffer.clear();
data.cursor = 0;
if !buffer.is_empty() {
let cmd = buffer.clone();
if length == 0 || !data.history[length - 1].is_empty() {
data.history_index = length;
} else {
data.history_index = length - 1;
}
let index = data.history_index;
if length <= index {
data.history.push(buffer);
} else {
data.history[index] = buffer;
}
data.history_index += 1;
Some(cmd)
} else {
None
}
};
self.crlf();
if let Some(cmd) = cmd {
self.running.store(true, Ordering::SeqCst);
self.exec(cmd).await.ok();
self.running.store(false, Ordering::SeqCst);
} else {
self.prompt();
}
}
Key::Alt(_c) => {
return Ok(());
}
Key::Ctrl('c') => {
cfg_if! {
if #[cfg(not(target_arch = "wasm32"))] {
self.exit().await;
}
}
return Ok(());
}
Key::Ctrl(_c) => {
return Ok(());
}
Key::Char(ch) => {
self.inject_char(ch)?;
}
_ => {
return Ok(());
}
}
Ok(())
}
fn trail(
&self,
cursor: usize,
buffer: &UnicodeString,
rewind: bool,
erase_last: bool,
offset: usize,
) {
let mut tail = UnicodeString::from(&buffer.0[cursor..]); if erase_last {
tail.push(' ');
}
self.write(&tail);
if rewind {
let mut l = tail.len();
if offset > 0 {
l -= offset;
}
for _ in 0..l {
self.write("\x08"); }
}
}
#[inline]
pub fn is_running(&self) -> bool {
self.running.load(Ordering::SeqCst)
}
pub async fn exec<S: ToString>(self: &Arc<Terminal>, cmd: S) -> Result<()> {
if let Err(err) = self
.handler
.clone()
.digest(self.clone(), cmd.to_string())
.await
{
self.writeln(err);
}
if self.terminate.load(Ordering::SeqCst) {
self.term().exit();
} else {
self.prompt();
}
Ok(())
}
pub fn set_theme(&self, _theme: Theme) -> Result<()> {
#[cfg(target_arch = "wasm32")]
self.term.set_theme(_theme)?;
Ok(())
}
pub fn update_theme(&self) -> Result<()> {
#[cfg(target_arch = "wasm32")]
self.term.update_theme()?;
Ok(())
}
pub fn clipboard_copy(&self) -> Result<()> {
#[cfg(target_arch = "wasm32")]
self.term.clipboard_copy()?;
Ok(())
}
pub fn clipboard_paste(&self) -> Result<()> {
#[cfg(target_arch = "wasm32")]
self.term.clipboard_paste()?;
Ok(())
}
pub fn increase_font_size(&self) -> Result<Option<f64>> {
self.term.increase_font_size()
}
pub fn decrease_font_size(&self) -> Result<Option<f64>> {
self.term.decrease_font_size()
}
pub fn set_font_size(&self, font_size: f64) -> Result<()> {
self.term.set_font_size(font_size)
}
pub fn get_font_size(&self) -> Result<Option<f64>> {
self.term.get_font_size()
}
pub fn cols(&self) -> Option<usize> {
self.term.cols()
}
pub async fn select<T>(self: &Arc<Terminal>, prompt: &str, list: &[T]) -> Result<Option<T>>
where
T: std::fmt::Display + Clone, {
if list.is_empty() {
Ok(None)
} else if list.len() == 1 {
Ok(list.first().cloned())
} else {
let mut selection = None;
while selection.is_none() {
list.iter().enumerate().for_each(|(seq, item)| {
self.writeln(format!("{seq}: {item}"));
});
let text = self
.ask(
false,
&format!("{prompt} [{}..{}] or <enter> to abort: ", 0, list.len() - 1),
)
.await?
.trim()
.to_string();
if text.is_empty() {
self.writeln("aborting...");
return Err(Error::UserAbort);
} else {
match text.parse::<usize>() {
Ok(seq) if seq < list.len() => selection = list.get(seq).cloned(),
_ => {}
};
}
}
Ok(selection)
}
}
pub fn register_event_handler(self: &Arc<Self>, _handler: EventHandlerFn) -> Result<()> {
#[cfg(target_arch = "wasm32")]
self.term.register_event_handler(_handler)?;
Ok(())
}
pub fn register_link_matcher(
&self,
_regexp: &js_sys::RegExp,
_handler: LinkMatcherHandlerFn,
) -> Result<()> {
cfg_if! {
if #[cfg(target_arch = "wasm32")] {
self.term.register_link_matcher(_regexp, _handler)?;
}
}
Ok(())
}
}
pub fn parse(s: &str) -> Vec<String> {
let regex = Regex::new(r"\s+").unwrap();
let s = regex.replace_all(s.trim(), " ");
s.split(' ').map(|s| s.to_string()).collect::<Vec<String>>()
}