mirror of
https://github.com/wez/wezterm.git
synced 2024-12-25 06:12:16 +03:00
344543260e
The release notes are generally slightly taller than the default window size.
440 lines
14 KiB
Rust
440 lines
14 KiB
Rust
use crate::termwiztermtab;
|
|
use anyhow::{anyhow, bail, Context as _};
|
|
use crossbeam::channel::{bounded, Receiver, Sender};
|
|
use promise::Promise;
|
|
use std::sync::Mutex;
|
|
use std::time::{Duration, Instant};
|
|
use termwiz::cell::{unicode_column_width, CellAttributes};
|
|
use termwiz::lineedit::*;
|
|
use termwiz::surface::{Change, Position};
|
|
use termwiz::terminal::*;
|
|
use unicode_segmentation::UnicodeSegmentation;
|
|
|
|
#[derive(Default)]
|
|
struct PasswordPromptHost {
|
|
history: BasicHistory,
|
|
}
|
|
impl LineEditorHost for PasswordPromptHost {
|
|
fn history(&mut self) -> &mut dyn History {
|
|
&mut self.history
|
|
}
|
|
|
|
// Rewrite the input so that we can obscure the password
|
|
// characters when output to the terminal widget
|
|
fn highlight_line(&self, line: &str, cursor_position: usize) -> (Vec<OutputElement>, usize) {
|
|
let placeholder = "🔑";
|
|
let grapheme_count = unicode_column_width(line);
|
|
let mut output = vec![];
|
|
for _ in 0..grapheme_count {
|
|
output.push(OutputElement::Text(placeholder.to_string()));
|
|
}
|
|
(output, unicode_column_width(placeholder) * cursor_position)
|
|
}
|
|
}
|
|
|
|
pub enum UIRequest {
|
|
/// Display something
|
|
Output(Vec<Change>),
|
|
/// Request input
|
|
Input {
|
|
prompt: String,
|
|
echo: bool,
|
|
respond: Promise<String>,
|
|
},
|
|
/// Sleep with a progress bar
|
|
Sleep {
|
|
reason: String,
|
|
duration: Duration,
|
|
respond: Promise<()>,
|
|
},
|
|
Close,
|
|
}
|
|
|
|
struct ConnectionUIImpl {
|
|
term: termwiztermtab::TermWizTerminal,
|
|
rx: Receiver<UIRequest>,
|
|
}
|
|
|
|
#[derive(PartialEq, Eq)]
|
|
enum CloseStatus {
|
|
Explicit,
|
|
Implicit,
|
|
}
|
|
|
|
impl ConnectionUIImpl {
|
|
fn run(&mut self) -> anyhow::Result<CloseStatus> {
|
|
loop {
|
|
match self.rx.recv_timeout(Duration::from_millis(200)) {
|
|
Ok(UIRequest::Close) => return Ok(CloseStatus::Explicit),
|
|
Ok(UIRequest::Output(changes)) => self.term.render(&changes)?,
|
|
Ok(UIRequest::Input {
|
|
prompt,
|
|
echo: true,
|
|
mut respond,
|
|
}) => {
|
|
respond.result(self.input_prompt(&prompt));
|
|
}
|
|
Ok(UIRequest::Input {
|
|
prompt,
|
|
echo: false,
|
|
mut respond,
|
|
}) => {
|
|
respond.result(self.password_prompt(&prompt));
|
|
}
|
|
Ok(UIRequest::Sleep {
|
|
reason,
|
|
duration,
|
|
mut respond,
|
|
}) => {
|
|
respond.result(self.sleep(&reason, duration));
|
|
}
|
|
Err(err) if err.is_timeout() => {}
|
|
Err(err) => bail!("recv_timeout: {}", err),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn password_prompt(&mut self, prompt: &str) -> anyhow::Result<String> {
|
|
let mut editor = LineEditor::new(&mut self.term);
|
|
editor.set_prompt(prompt);
|
|
|
|
let mut host = PasswordPromptHost::default();
|
|
if let Some(line) = editor.read_line(&mut host)? {
|
|
Ok(line)
|
|
} else {
|
|
bail!("password entry was cancelled");
|
|
}
|
|
}
|
|
|
|
fn input_prompt(&mut self, prompt: &str) -> anyhow::Result<String> {
|
|
let mut editor = LineEditor::new(&mut self.term);
|
|
editor.set_prompt(prompt);
|
|
|
|
let mut host = NopLineEditorHost::default();
|
|
if let Some(line) = editor.read_line(&mut host)? {
|
|
Ok(line)
|
|
} else {
|
|
bail!("prompt cancelled");
|
|
}
|
|
}
|
|
|
|
fn sleep(&mut self, reason: &str, duration: Duration) -> anyhow::Result<()> {
|
|
let start = Instant::now();
|
|
let deadline = start + duration;
|
|
let mut last_draw = None;
|
|
|
|
loop {
|
|
let now = Instant::now();
|
|
if now >= deadline {
|
|
break;
|
|
}
|
|
|
|
// Render a progress bar underneath the countdown text by reversing
|
|
// out the text for the elapsed portion of time.
|
|
let remain = deadline - now;
|
|
let term_width = self.term.get_screen_size().map(|s| s.cols).unwrap_or(80);
|
|
let prog_width = term_width as u128 * (duration.as_millis() - remain.as_millis())
|
|
/ duration.as_millis();
|
|
let prog_width = prog_width as usize;
|
|
let message = format!("{} ({:.0?})", reason, remain);
|
|
|
|
let mut reversed_string = String::new();
|
|
let mut default_string = String::new();
|
|
let mut col = 0;
|
|
for grapheme in message.graphemes(true) {
|
|
// Once we've passed the elapsed column, full up the string
|
|
// that we'll render with default attributes instead.
|
|
if col > prog_width {
|
|
default_string.push_str(grapheme);
|
|
} else {
|
|
reversed_string.push_str(grapheme);
|
|
}
|
|
col += 1;
|
|
}
|
|
|
|
// If we didn't reach the elapsed column yet (really short text!),
|
|
// we need to pad out the reversed string.
|
|
while col < prog_width {
|
|
reversed_string.push(' ');
|
|
col += 1;
|
|
}
|
|
|
|
let combined = format!("{}{}", reversed_string, default_string);
|
|
|
|
if last_draw.is_none() || last_draw.as_ref().unwrap() != &combined {
|
|
self.term.render(&[
|
|
Change::CursorPosition {
|
|
x: Position::Absolute(0),
|
|
y: Position::Relative(0),
|
|
},
|
|
Change::AllAttributes(CellAttributes::default().set_reverse(true).clone()),
|
|
Change::Text(reversed_string),
|
|
Change::AllAttributes(CellAttributes::default()),
|
|
Change::Text(default_string),
|
|
])?;
|
|
last_draw.replace(combined);
|
|
}
|
|
|
|
// We use poll_input rather than a raw sleep here so that
|
|
// eg: resize events can be processed and reflected in the
|
|
// dimensions reported at the top of the loop.
|
|
// We're using a sub-second value for the delay here for a
|
|
// slightly smoother progress bar.
|
|
self.term
|
|
.poll_input(Some(remain.min(Duration::from_millis(50))))?;
|
|
}
|
|
|
|
let message = format!("{} (done)\r\n", reason);
|
|
self.term.render(&[
|
|
Change::CursorPosition {
|
|
x: Position::Absolute(0),
|
|
y: Position::Relative(0),
|
|
},
|
|
Change::Text(message),
|
|
])?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct HeadlessImpl {
|
|
rx: Receiver<UIRequest>,
|
|
}
|
|
|
|
impl HeadlessImpl {
|
|
fn run(&mut self) -> anyhow::Result<()> {
|
|
loop {
|
|
match self.rx.recv_timeout(Duration::from_millis(200)) {
|
|
Ok(UIRequest::Close) => break,
|
|
Ok(UIRequest::Output(changes)) => {
|
|
log::trace!("Output: {:?}", changes);
|
|
}
|
|
Ok(UIRequest::Input { mut respond, .. }) => {
|
|
respond.result(Err(anyhow!("Input requested from headless context")));
|
|
}
|
|
Ok(UIRequest::Sleep {
|
|
mut respond,
|
|
reason,
|
|
duration,
|
|
}) => {
|
|
log::error!("{} (sleeping for {:?})", reason, duration);
|
|
std::thread::sleep(duration);
|
|
respond.result(Ok(()));
|
|
}
|
|
Err(err) if err.is_timeout() => {}
|
|
Err(err) => bail!("recv_timeout: {}", err),
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct ConnectionUI {
|
|
tx: Sender<UIRequest>,
|
|
}
|
|
|
|
impl ConnectionUI {
|
|
pub fn new() -> Self {
|
|
let enable_close_delay = true;
|
|
Self::with_dimensions(80, 24, enable_close_delay)
|
|
}
|
|
|
|
pub fn with_dimensions(width: usize, height: usize, enable_close_delay: bool) -> Self {
|
|
let (tx, rx) = bounded(16);
|
|
promise::spawn::spawn_into_main_thread(termwiztermtab::run(width, height, move |term| {
|
|
let mut ui = ConnectionUIImpl { term, rx };
|
|
let status = ui.run().unwrap_or_else(|e| {
|
|
log::error!("while running ConnectionUI loop: {:?}", e);
|
|
CloseStatus::Implicit
|
|
});
|
|
|
|
if enable_close_delay && status == CloseStatus::Implicit {
|
|
ui.sleep(
|
|
"(this window will close automatically)",
|
|
Duration::new(120, 0),
|
|
)
|
|
.ok();
|
|
}
|
|
Ok(())
|
|
}));
|
|
Self { tx }
|
|
}
|
|
|
|
pub fn new_with_no_close_delay() -> Self {
|
|
let enable_close_delay = false;
|
|
Self::with_dimensions(80, 24, enable_close_delay)
|
|
}
|
|
|
|
pub fn new_headless() -> Self {
|
|
let (tx, rx) = bounded(16);
|
|
std::thread::spawn(move || {
|
|
let mut ui = HeadlessImpl { rx };
|
|
ui.run()
|
|
});
|
|
Self { tx }
|
|
}
|
|
|
|
pub fn run_and_log_error<T, F>(&self, f: F) -> anyhow::Result<T>
|
|
where
|
|
F: FnOnce() -> anyhow::Result<T>,
|
|
{
|
|
match f() {
|
|
Err(e) => {
|
|
let what = format!("\r\nFailed: {:?}\r\n", e);
|
|
log::error!("{}", what);
|
|
self.output_str(&what);
|
|
Err(e)
|
|
}
|
|
result => result,
|
|
}
|
|
}
|
|
|
|
pub async fn async_run_and_log_error<T, F>(&self, f: F) -> anyhow::Result<T>
|
|
where
|
|
F: std::future::Future<Output = anyhow::Result<T>>,
|
|
{
|
|
match f.await {
|
|
Err(e) => {
|
|
let what = format!("\r\nFailed: {:?}\r\n", e);
|
|
self.output_str(&what);
|
|
Err(e)
|
|
}
|
|
result => result,
|
|
}
|
|
}
|
|
|
|
pub fn title(&self, title: &str) {
|
|
self.output(vec![Change::Title(title.to_string())]);
|
|
}
|
|
|
|
pub fn output(&self, changes: Vec<Change>) {
|
|
self.tx.send(UIRequest::Output(changes)).ok();
|
|
}
|
|
|
|
pub fn output_str(&self, s: &str) {
|
|
let s = s.replace("\n", "\r\n");
|
|
self.output(vec![Change::Text(s)]);
|
|
}
|
|
|
|
/// Sleep (blocking!) for the specified duration, but updates
|
|
/// the UI with the reason and a count down during that time.
|
|
pub fn sleep_with_reason(&self, reason: &str, duration: Duration) -> anyhow::Result<()> {
|
|
let mut promise = Promise::new();
|
|
let future = promise.get_future().unwrap();
|
|
|
|
self.tx
|
|
.send(UIRequest::Sleep {
|
|
reason: reason.to_string(),
|
|
duration,
|
|
respond: promise,
|
|
})
|
|
.context("send to ConnectionUI failed")?;
|
|
|
|
future.wait()
|
|
}
|
|
|
|
/// Crack a multi-line prompt into an optional preamble and the prompt
|
|
/// text on the final line. This is needed because the line editor
|
|
/// is only designed for a single line prompt; a multi-line prompt
|
|
/// messes up the cursor positioning.
|
|
fn split_multi_line_prompt(s: &str) -> (Option<String>, String) {
|
|
let text = s.replace("\n", "\r\n");
|
|
let bits: Vec<&str> = text.rsplitn(2, "\r\n").collect();
|
|
|
|
if bits.len() == 2 {
|
|
(Some(format!("{}\r\n", bits[1])), bits[0].to_owned())
|
|
} else {
|
|
(None, text)
|
|
}
|
|
}
|
|
|
|
pub fn input(&self, prompt: &str) -> anyhow::Result<String> {
|
|
let mut promise = Promise::new();
|
|
let future = promise.get_future().unwrap();
|
|
|
|
let (preamble, prompt) = Self::split_multi_line_prompt(prompt);
|
|
if let Some(preamble) = preamble {
|
|
self.output(vec![Change::Text(preamble)]);
|
|
}
|
|
|
|
self.tx
|
|
.send(UIRequest::Input {
|
|
prompt,
|
|
echo: true,
|
|
respond: promise,
|
|
})
|
|
.context("send to ConnectionUI failed")?;
|
|
|
|
future.wait()
|
|
}
|
|
|
|
pub fn password(&self, prompt: &str) -> anyhow::Result<String> {
|
|
let mut promise = Promise::new();
|
|
let future = promise.get_future().unwrap();
|
|
|
|
let (preamble, prompt) = Self::split_multi_line_prompt(prompt);
|
|
if let Some(preamble) = preamble {
|
|
self.output(vec![Change::Text(preamble)]);
|
|
}
|
|
|
|
self.tx
|
|
.send(UIRequest::Input {
|
|
prompt,
|
|
echo: false,
|
|
respond: promise,
|
|
})
|
|
.context("send to ConnectionUI failed")?;
|
|
|
|
future.wait()
|
|
}
|
|
|
|
pub fn close(&self) {
|
|
self.tx.send(UIRequest::Close).ok();
|
|
}
|
|
|
|
pub fn test_alive(&self) -> bool {
|
|
if !self.tx.send(UIRequest::Output(vec![])).is_ok() {
|
|
return false;
|
|
}
|
|
std::thread::sleep(Duration::from_millis(50));
|
|
self.tx.send(UIRequest::Output(vec![])).is_ok()
|
|
}
|
|
}
|
|
|
|
lazy_static::lazy_static! {
|
|
static ref ERROR_WINDOW: Mutex<Option<ConnectionUI>> = Mutex::new(None);
|
|
}
|
|
|
|
fn get_error_window() -> ConnectionUI {
|
|
let mut err = ERROR_WINDOW.lock().unwrap();
|
|
if let Some(ui) = err.as_ref().map(|ui| ui.clone()) {
|
|
ui.output_str("\n");
|
|
if ui.test_alive() {
|
|
return ui;
|
|
}
|
|
}
|
|
|
|
let ui = ConnectionUI::new_with_no_close_delay();
|
|
ui.title("wezterm Configuration Error");
|
|
err.replace(ui.clone());
|
|
ui
|
|
}
|
|
|
|
/// If the GUI has been started, pops up a window with the supplied error
|
|
/// message framed as a configuration error.
|
|
/// If there is no GUI front end, generates a toast notification instead.
|
|
pub fn show_configuration_error_message(err: &str) {
|
|
log::error!("While (re)loading configuration: {}", err);
|
|
if crate::frontend::has_gui_front_end() {
|
|
let ui = get_error_window();
|
|
|
|
let mut wrapped = textwrap::fill(&err, 78);
|
|
wrapped.push_str("\n");
|
|
ui.output_str(&wrapped);
|
|
} else {
|
|
crate::toast_notification("Wezterm Configuration", &err);
|
|
}
|
|
}
|