1
1
mirror of https://github.com/wez/wezterm.git synced 2024-10-28 09:22:19 +03:00

connui: add a sleep_with_reason method

This is used to indicate timeout/retries during connection establishment
and also to count down to the automatic window close.

The UI will render a progress bar underneath the reason text to show
the passage of time, as well as counting down the provided duration.
This commit is contained in:
Wez Furlong 2020-03-08 09:45:39 -07:00
parent 73c0c02ffb
commit e7d8068ad9
3 changed files with 133 additions and 16 deletions

View File

@ -2,11 +2,12 @@ use crate::termwiztermtab;
use anyhow::{anyhow, bail, Context as _}; use anyhow::{anyhow, bail, Context as _};
use crossbeam::channel::{bounded, Receiver, Sender}; use crossbeam::channel::{bounded, Receiver, Sender};
use promise::Promise; use promise::Promise;
use std::time::Duration; use std::time::{Duration, Instant};
use termwiz::cell::unicode_column_width; use termwiz::cell::{unicode_column_width, CellAttributes};
use termwiz::lineedit::*; use termwiz::lineedit::*;
use termwiz::surface::Change; use termwiz::surface::{Change, Position};
use termwiz::terminal::*; use termwiz::terminal::*;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Default)] #[derive(Default)]
struct PasswordPromptHost { struct PasswordPromptHost {
@ -39,6 +40,12 @@ pub enum UIRequest {
echo: bool, echo: bool,
respond: Promise<String>, respond: Promise<String>,
}, },
/// Sleep with a progress bar
Sleep {
reason: String,
duration: Duration,
respond: Promise<()>,
},
Close, Close,
} }
@ -67,6 +74,13 @@ impl ConnectionUIImpl {
}) => { }) => {
respond.result(self.password_prompt(&prompt)); 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) if err.is_timeout() => {}
Err(err) => bail!("recv_timeout: {}", err), Err(err) => bail!("recv_timeout: {}", err),
} }
@ -97,6 +111,78 @@ impl ConnectionUIImpl {
bail!("prompt cancelled"); bail!("prompt cancelled");
} }
} }
fn sleep(&mut self, reason: &str, duration: Duration) -> anyhow::Result<()> {
let start = Instant::now();
let deadline = start + duration;
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;
}
self.term.render(&[
Change::CursorPosition {
x: Position::Absolute(0),
y: Position::NoChange,
},
Change::AllAttributes(CellAttributes::default().set_reverse(true).clone()),
Change::Text(reversed_string),
Change::AllAttributes(CellAttributes::default()),
Change::Text(default_string),
])?;
// 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))))
.ok();
}
let message = format!("{} (done)\r\n", reason);
self.term.render(&[
Change::CursorPosition {
x: Position::Absolute(0),
y: Position::NoChange,
},
Change::Text(message),
])?;
Ok(())
}
} }
struct HeadlessImpl { struct HeadlessImpl {
@ -114,6 +200,15 @@ impl HeadlessImpl {
Ok(UIRequest::Input { mut respond, .. }) => { Ok(UIRequest::Input { mut respond, .. }) => {
respond.result(Err(anyhow!("Input requested from headless context"))); 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) if err.is_timeout() => {}
Err(err) => bail!("recv_timeout: {}", err), Err(err) => bail!("recv_timeout: {}", err),
} }
@ -136,7 +231,11 @@ impl ConnectionUI {
if let Err(e) = ui.run() { if let Err(e) = ui.run() {
log::error!("while running ConnectionUI loop: {:?}", e); log::error!("while running ConnectionUI loop: {:?}", e);
} }
std::thread::sleep(Duration::new(10, 0)); ui.sleep(
"(this window will close automatically)",
Duration::new(10, 0),
)
.ok();
Ok(()) Ok(())
})); }));
Self { tx } Self { tx }
@ -164,6 +263,23 @@ impl ConnectionUI {
self.output(vec![Change::Text(s)]); 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 /// Crack a multi-line prompt into an optional preamble and the prompt
/// text on the final line. This is needed because the line editor /// text on the final line. This is needed because the line editor
/// is only designed for a single line prompt; a multi-line prompt /// is only designed for a single line prompt; a multi-line prompt

View File

@ -699,13 +699,12 @@ impl Client {
let mut ui = ConnectionUI::new(); let mut ui = ConnectionUI::new();
ui.title("wezterm: Reconnecting..."); ui.title("wezterm: Reconnecting...");
ui.output_str(&format!(
"client disconnected {}; will reconnect in {:?}\n",
e, backoff
));
loop { loop {
std::thread::sleep(backoff); ui.sleep_with_reason(
&format!("client disconnected {}; will reconnect", e),
backoff,
)
.ok();
match reconnectable.connect(false, &mut ui) { match reconnectable.connect(false, &mut ui) {
Ok(_) => { Ok(_) => {
backoff = BASE_INTERVAL; backoff = BASE_INTERVAL;

View File

@ -263,12 +263,14 @@ impl Tab for TermWizTerminalTab {
} }
fn resize(&self, size: PtySize) -> anyhow::Result<()> { fn resize(&self, size: PtySize) -> anyhow::Result<()> {
self.renderable let renderable = self.renderable.borrow();
.borrow() let mut inner = renderable.inner.borrow_mut();
.inner
.borrow_mut() inner.surface.resize(size.cols as usize, size.rows as usize);
.surface inner.input_tx.send(InputEvent::Resized {
.resize(size.cols as usize, size.rows as usize); rows: size.rows as usize,
cols: size.cols as usize,
})?;
Ok(()) Ok(())
} }