mirror of
https://github.com/wez/wezterm.git
synced 2024-12-23 21:32:13 +03:00
ssh: use a single window for authenticating a session
This makes it so that we preserve context while showing the connection status and authentication prompts.
This commit is contained in:
parent
1ef95b917a
commit
fd8f28960f
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -539,6 +539,20 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-channel 0.4.0",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.3.9"
|
||||
@ -3401,7 +3415,7 @@ dependencies = [
|
||||
"core-foundation 0.7.0",
|
||||
"core-graphics 0.19.0",
|
||||
"core-text 15.0.0",
|
||||
"crossbeam-channel 0.3.9",
|
||||
"crossbeam",
|
||||
"daemonize",
|
||||
"dirs 1.0.5",
|
||||
"downcast-rs",
|
||||
|
@ -21,7 +21,7 @@ base64 = "0.10"
|
||||
base91 = { path = "base91" }
|
||||
rangeset = { path = "rangeset" }
|
||||
bitflags = "1.0"
|
||||
crossbeam-channel = "0.3"
|
||||
crossbeam = "0.7"
|
||||
dirs = "1.0"
|
||||
downcast-rs = "1.0"
|
||||
euclid = "0.20"
|
||||
|
@ -6,7 +6,7 @@ use crate::mux::window::WindowId;
|
||||
use crate::mux::Mux;
|
||||
use crate::server::listener::spawn_listener;
|
||||
use anyhow::{bail, Error};
|
||||
use crossbeam_channel::{unbounded as channel, Receiver};
|
||||
use crossbeam::channel::{unbounded as channel, Receiver};
|
||||
use log::info;
|
||||
use promise::*;
|
||||
use std::rc::Rc;
|
||||
|
@ -9,7 +9,7 @@ use crate::server::tab::ClientTab;
|
||||
use crate::server::UnixStream;
|
||||
use crate::ssh::ssh_connect;
|
||||
use anyhow::{anyhow, bail, Context, Error};
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use crossbeam::channel::TryRecvError;
|
||||
use filedescriptor::{pollfd, AsRawSocketDescriptor};
|
||||
use log::info;
|
||||
use portable_pty::{CommandBuilder, NativePtySystem, PtySystem};
|
||||
|
@ -4,7 +4,7 @@ use crate::mux::{Mux, MuxNotification, MuxSubscriber};
|
||||
use crate::server::codec::*;
|
||||
use crate::server::pollable::*;
|
||||
use anyhow::{anyhow, bail, Context, Error};
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use crossbeam::channel::TryRecvError;
|
||||
use log::error;
|
||||
use portable_pty::PtySize;
|
||||
use promise::spawn::spawn_into_main_thread;
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::server::UnixStream;
|
||||
use anyhow::Error;
|
||||
use crossbeam_channel::{unbounded as channel, Receiver, Sender, TryRecvError};
|
||||
use crossbeam::channel::{unbounded as channel, Receiver, Sender, TryRecvError};
|
||||
use filedescriptor::*;
|
||||
use std::cell::RefCell;
|
||||
use std::io::{Read, Write};
|
||||
|
292
src/ssh.rs
292
src/ssh.rs
@ -6,6 +6,7 @@ use crate::mux::Mux;
|
||||
use crate::termwiztermtab;
|
||||
use anyhow::{anyhow, bail, Context, Error};
|
||||
use async_trait::async_trait;
|
||||
use crossbeam::channel::{bounded, Receiver, Sender};
|
||||
use portable_pty::cmdbuilder::CommandBuilder;
|
||||
use portable_pty::{PtySize, PtySystem};
|
||||
use promise::{Future, Promise};
|
||||
@ -14,41 +15,24 @@ use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use termwiz::cell::{unicode_column_width, AttributeChange, Intensity};
|
||||
use std::time::Duration;
|
||||
use termwiz::cell::unicode_column_width;
|
||||
use termwiz::lineedit::*;
|
||||
use termwiz::surface::Change;
|
||||
use termwiz::terminal::*;
|
||||
|
||||
fn password_prompt(
|
||||
instructions: &str,
|
||||
prompt: &str,
|
||||
username: &str,
|
||||
remote_address: &str,
|
||||
) -> Option<String> {
|
||||
let title = "🔐 wezterm: SSH authentication".to_string();
|
||||
let text = format!(
|
||||
"🔐 SSH Authentication for {} @ {}\n{}\n",
|
||||
username, remote_address, instructions
|
||||
)
|
||||
.replace("\n", "\r\n");
|
||||
let prompt = prompt.to_string();
|
||||
|
||||
#[derive(Default)]
|
||||
struct PasswordPromptHost {
|
||||
#[derive(Default)]
|
||||
struct PasswordPromptHost {
|
||||
history: BasicHistory,
|
||||
}
|
||||
impl LineEditorHost for PasswordPromptHost {
|
||||
}
|
||||
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) {
|
||||
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![];
|
||||
@ -57,53 +41,73 @@ fn password_prompt(
|
||||
}
|
||||
(output, unicode_column_width(placeholder) * cursor_position)
|
||||
}
|
||||
}
|
||||
match promise::spawn::block_on(termwiztermtab::run(60, 10, move |mut term| {
|
||||
term.render(&[
|
||||
// Change::Attribute(AttributeChange::Intensity(Intensity::Bold)),
|
||||
Change::Title(title.to_string()),
|
||||
Change::Text(text.to_string()),
|
||||
Change::Attribute(AttributeChange::Intensity(Intensity::Normal)),
|
||||
])?;
|
||||
}
|
||||
|
||||
let mut editor = LineEditor::new(term);
|
||||
editor.set_prompt(&format!("{}: ", prompt));
|
||||
enum UIRequest {
|
||||
/// Display something
|
||||
Output(Vec<Change>),
|
||||
/// Request input
|
||||
Input {
|
||||
prompt: String,
|
||||
echo: bool,
|
||||
respond: Promise<String>,
|
||||
},
|
||||
Close,
|
||||
}
|
||||
|
||||
struct SshUIImpl {
|
||||
term: termwiztermtab::TermWizTerminal,
|
||||
rx: Receiver<UIRequest>,
|
||||
}
|
||||
|
||||
impl SshUIImpl {
|
||||
fn run(&mut self) -> anyhow::Result<()> {
|
||||
let title = "🔐 wezterm: SSH authentication".to_string();
|
||||
self.term.render(&[Change::Title(title)])?;
|
||||
|
||||
loop {
|
||||
match self.rx.recv_timeout(Duration::from_millis(200)) {
|
||||
Ok(UIRequest::Close) => break,
|
||||
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));
|
||||
}
|
||||
Err(err) if err.is_timeout() => {}
|
||||
Err(err) => bail!("recv_timeout: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::new(2, 0));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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!("prompt cancelled");
|
||||
}
|
||||
})) {
|
||||
Ok(p) => Some(p),
|
||||
Err(p) => {
|
||||
log::error!("failed to prompt for pw: {}", p);
|
||||
None
|
||||
bail!("password entry was cancelled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn input_prompt(
|
||||
instructions: &str,
|
||||
prompt: &str,
|
||||
username: &str,
|
||||
remote_address: &str,
|
||||
) -> Option<String> {
|
||||
let title = "🔐 wezterm: SSH authentication".to_string();
|
||||
let text = format!(
|
||||
"SSH Authentication for {} @ {}\n{}\n{}\n",
|
||||
username, remote_address, instructions, prompt
|
||||
)
|
||||
.replace("\n", "\r\n");
|
||||
match promise::spawn::block_on(termwiztermtab::run(60, 10, move |mut term| {
|
||||
term.render(&[
|
||||
Change::Title(title.to_string()),
|
||||
Change::Text(text.to_string()),
|
||||
Change::Attribute(AttributeChange::Intensity(Intensity::Normal)),
|
||||
])?;
|
||||
|
||||
let mut editor = LineEditor::new(term);
|
||||
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)? {
|
||||
@ -111,21 +115,72 @@ fn input_prompt(
|
||||
} else {
|
||||
bail!("prompt cancelled");
|
||||
}
|
||||
})) {
|
||||
Ok(p) => Some(p),
|
||||
Err(p) => {
|
||||
log::error!("failed to prompt for pw: {}", p);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Prompt<'a> {
|
||||
username: &'a str,
|
||||
remote_address: &'a str,
|
||||
struct SshUI {
|
||||
tx: Sender<UIRequest>,
|
||||
}
|
||||
|
||||
impl<'a> ssh2::KeyboardInteractivePrompt for Prompt<'a> {
|
||||
impl SshUI {
|
||||
fn new() -> Self {
|
||||
let (tx, rx) = bounded(16);
|
||||
promise::spawn::spawn_into_main_thread(termwiztermtab::run(70, 15, move |term| {
|
||||
let mut ui = SshUIImpl { term, rx };
|
||||
ui.run()
|
||||
}));
|
||||
Self { tx }
|
||||
}
|
||||
|
||||
fn output(&self, changes: Vec<Change>) {
|
||||
self.tx
|
||||
.send(UIRequest::Output(changes))
|
||||
.expect("send to SShUI failed");
|
||||
}
|
||||
|
||||
fn output_str(&self, s: &str) {
|
||||
let s = s.replace("\n", "\r\n");
|
||||
self.output(vec![Change::Text(s)]);
|
||||
}
|
||||
|
||||
fn input(&self, prompt: &str) -> anyhow::Result<String> {
|
||||
let mut promise = Promise::new();
|
||||
let future = promise.get_future().unwrap();
|
||||
|
||||
self.tx
|
||||
.send(UIRequest::Input {
|
||||
prompt: prompt.replace("\n", "\r\n"),
|
||||
echo: true,
|
||||
respond: promise,
|
||||
})
|
||||
.expect("send to SshUI failed");
|
||||
|
||||
future.wait()
|
||||
}
|
||||
|
||||
fn password(&self, prompt: &str) -> anyhow::Result<String> {
|
||||
let mut promise = Promise::new();
|
||||
let future = promise.get_future().unwrap();
|
||||
|
||||
self.tx
|
||||
.send(UIRequest::Input {
|
||||
prompt: prompt.replace("\n", "\r\n"),
|
||||
echo: false,
|
||||
respond: promise,
|
||||
})
|
||||
.expect("send to SshUI failed");
|
||||
|
||||
future.wait()
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
self.tx
|
||||
.send(UIRequest::Close)
|
||||
.expect("send to SshUI failed");
|
||||
}
|
||||
}
|
||||
|
||||
impl ssh2::KeyboardInteractivePrompt for SshUI {
|
||||
fn prompt<'b>(
|
||||
&mut self,
|
||||
_username: &str,
|
||||
@ -135,14 +190,13 @@ impl<'a> ssh2::KeyboardInteractivePrompt for Prompt<'a> {
|
||||
prompts
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let func = if p.echo {
|
||||
input_prompt
|
||||
self.output_str(&format!("{}\n", instructions));
|
||||
if p.echo {
|
||||
self.input(&p.text)
|
||||
} else {
|
||||
password_prompt
|
||||
};
|
||||
|
||||
func(instructions, &p.text, &self.username, &self.remote_address)
|
||||
.unwrap_or_else(String::new)
|
||||
self.password(&p.text)
|
||||
}
|
||||
.unwrap_or_else(|_| String::new())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@ -157,7 +211,11 @@ pub fn async_ssh_connect(remote_address: &str, username: &str) -> Future<ssh2::S
|
||||
future
|
||||
}
|
||||
|
||||
pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2::Session> {
|
||||
fn ssh_connect_with_ui(
|
||||
remote_address: &str,
|
||||
username: &str,
|
||||
ui: &mut SshUI,
|
||||
) -> anyhow::Result<ssh2::Session> {
|
||||
let mut sess = ssh2::Session::new()?;
|
||||
|
||||
let (remote_address, remote_host_name, port) = {
|
||||
@ -170,8 +228,11 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
|
||||
}
|
||||
};
|
||||
|
||||
ui.output_str(&format!("Connecting to {}\n", remote_address));
|
||||
|
||||
let tcp = TcpStream::connect(&remote_address)
|
||||
.with_context(|| format!("ssh connecting to {}", remote_address))?;
|
||||
ui.output_str("Connected OK!\n");
|
||||
tcp.set_nodelay(true)?;
|
||||
sess.set_tcp_stream(tcp);
|
||||
sess.handshake()
|
||||
@ -221,38 +282,22 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
|
||||
match known_hosts.check_port(&remote_host_name, port, key) {
|
||||
CheckResult::Match => {}
|
||||
CheckResult::NotFound => {
|
||||
let message = format!(
|
||||
"SSH host {} is not yet trusted.\r\n\
|
||||
{:?} Fingerprint: {}.\r\n\
|
||||
Trust and continue connecting?\r\n",
|
||||
ui.output_str(&format!(
|
||||
"SSH host {} is not yet trusted.\n\
|
||||
{:?} Fingerprint: {}.\n\
|
||||
Trust and continue connecting?\n",
|
||||
remote_address, key_type, fingerprint
|
||||
);
|
||||
));
|
||||
|
||||
let allow =
|
||||
promise::spawn::block_on(termwiztermtab::run(80, 10, move |mut term| {
|
||||
let title = "🔐 wezterm: SSH authentication".to_string();
|
||||
term.render(&[Change::Title(title), Change::Text(message.to_string())])?;
|
||||
|
||||
let mut editor = LineEditor::new(term);
|
||||
editor.set_prompt("Enter [Y/n]> ");
|
||||
|
||||
let mut host = NopLineEditorHost::default();
|
||||
loop {
|
||||
let line = match editor.read_line(&mut host) {
|
||||
Ok(Some(line)) => line,
|
||||
Ok(None) | Err(_) => return Ok(false),
|
||||
};
|
||||
let line = ui.input("Enter [Y/n]> ")?;
|
||||
|
||||
match line.as_ref() {
|
||||
"y" | "Y" | "yes" | "YES" => return Ok(true),
|
||||
"n" | "N" | "no" | "NO" => return Ok(false),
|
||||
"y" | "Y" | "yes" | "YES" => break,
|
||||
"n" | "N" | "no" | "NO" => bail!("user declined to trust host"),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}))?;
|
||||
|
||||
if !allow {
|
||||
bail!("user declined to trust host");
|
||||
}
|
||||
|
||||
known_hosts
|
||||
.add(remote_host_name, key, &remote_address, key_type.into())
|
||||
@ -263,11 +308,11 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
|
||||
.with_context(|| format!("writing known_hosts file {}", file.display()))?;
|
||||
}
|
||||
CheckResult::Mismatch => {
|
||||
termwiztermtab::message_box_ok(&format!(
|
||||
ui.output_str(&format!(
|
||||
"🛑 host key mismatch for ssh server {}.\n\
|
||||
Got fingerprint {} instead of expected value from known_hosts\n\
|
||||
file {}.\n\
|
||||
Refusing to connect.",
|
||||
Refusing to connect.\n",
|
||||
remote_address,
|
||||
fingerprint,
|
||||
file.display()
|
||||
@ -275,7 +320,7 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
|
||||
bail!("host mismatch, man in the middle attack?!");
|
||||
}
|
||||
CheckResult::Failure => {
|
||||
termwiztermtab::message_box_ok("🛑 Failed to load and check known ssh hosts");
|
||||
ui.output_str("🛑 Failed to load and check known ssh hosts\n");
|
||||
bail!("failed to check the known hosts");
|
||||
}
|
||||
}
|
||||
@ -295,24 +340,24 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
|
||||
if !sess.authenticated() && methods.contains("publickey") {
|
||||
if let Err(err) = sess.userauth_agent(&username) {
|
||||
log::info!("while attempting agent auth: {}", err);
|
||||
} else if sess.authenticated() {
|
||||
ui.output_str("publickey auth successful!\n");
|
||||
}
|
||||
}
|
||||
|
||||
if !sess.authenticated() && methods.contains("password") {
|
||||
let pass = password_prompt("", "Password", username, &remote_address)
|
||||
.ok_or_else(|| anyhow!("password entry was cancelled"))?;
|
||||
ui.output_str(&format!(
|
||||
"Password authentication for {}@{}\n",
|
||||
username, remote_address
|
||||
));
|
||||
let pass = ui.password("Password: ")?;
|
||||
if let Err(err) = sess.userauth_password(username, &pass) {
|
||||
log::error!("while attempting password auth: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if !sess.authenticated() && methods.contains("keyboard-interactive") {
|
||||
let mut prompt = Prompt {
|
||||
username,
|
||||
remote_address: &remote_address,
|
||||
};
|
||||
|
||||
if let Err(err) = sess.userauth_keyboard_interactive(&username, &mut prompt) {
|
||||
if let Err(err) = sess.userauth_keyboard_interactive(&username, ui) {
|
||||
log::error!("while attempting keyboard-interactive auth: {}", err);
|
||||
}
|
||||
}
|
||||
@ -325,6 +370,21 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
|
||||
Ok(sess)
|
||||
}
|
||||
|
||||
pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2::Session> {
|
||||
let mut ui = SshUI::new();
|
||||
let res = ssh_connect_with_ui(remote_address, username, &mut ui);
|
||||
match res {
|
||||
Ok(sess) => {
|
||||
ui.close();
|
||||
Ok(sess)
|
||||
}
|
||||
Err(err) => {
|
||||
ui.output_str(&format!("\nFailed: {}", err));
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RemoteSshDomain {
|
||||
pty_system: Box<dyn PtySystem>,
|
||||
id: DomainId,
|
||||
|
@ -13,7 +13,7 @@ use crate::mux::window::WindowId;
|
||||
use crate::mux::Mux;
|
||||
use anyhow::{bail, Error};
|
||||
use async_trait::async_trait;
|
||||
use crossbeam_channel::{unbounded as channel, Receiver, Sender};
|
||||
use crossbeam::channel::{unbounded as channel, Receiver, Sender};
|
||||
use filedescriptor::Pipe;
|
||||
use portable_pty::*;
|
||||
use rangeset::RangeSet;
|
||||
@ -370,6 +370,48 @@ impl termwiz::terminal::Terminal for TermWizTerminal {
|
||||
}
|
||||
}
|
||||
|
||||
impl termwiz::terminal::Terminal for &mut TermWizTerminal {
|
||||
fn set_raw_mode(&mut self) -> anyhow::Result<()> {
|
||||
(**self).set_raw_mode()
|
||||
}
|
||||
|
||||
fn set_cooked_mode(&mut self) -> anyhow::Result<()> {
|
||||
(**self).set_cooked_mode()
|
||||
}
|
||||
|
||||
fn enter_alternate_screen(&mut self) -> anyhow::Result<()> {
|
||||
(**self).enter_alternate_screen()
|
||||
}
|
||||
|
||||
fn exit_alternate_screen(&mut self) -> anyhow::Result<()> {
|
||||
(**self).exit_alternate_screen()
|
||||
}
|
||||
|
||||
fn get_screen_size(&mut self) -> anyhow::Result<ScreenSize> {
|
||||
(**self).get_screen_size()
|
||||
}
|
||||
|
||||
fn set_screen_size(&mut self, size: ScreenSize) -> anyhow::Result<()> {
|
||||
(**self).set_screen_size(size)
|
||||
}
|
||||
|
||||
fn render(&mut self, changes: &[Change]) -> anyhow::Result<()> {
|
||||
(**self).render(changes)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> anyhow::Result<()> {
|
||||
(**self).flush()
|
||||
}
|
||||
|
||||
fn poll_input(&mut self, wait: Option<Duration>) -> anyhow::Result<Option<InputEvent>> {
|
||||
(**self).poll_input(wait)
|
||||
}
|
||||
|
||||
fn waker(&self) -> TerminalWaker {
|
||||
(**self).waker()
|
||||
}
|
||||
}
|
||||
|
||||
/// This function spawns a thread and constructs a GUI window with an
|
||||
/// associated termwiz Terminal object to execute the provided function.
|
||||
/// The function is expected to run in a loop to manage input and output
|
||||
@ -378,7 +420,7 @@ impl termwiz::terminal::Terminal for TermWizTerminal {
|
||||
/// the return value from the function.
|
||||
pub async fn run<
|
||||
T: Send + 'static,
|
||||
F: Send + 'static + Fn(TermWizTerminal) -> anyhow::Result<T>,
|
||||
F: Send + 'static + FnOnce(TermWizTerminal) -> anyhow::Result<T>,
|
||||
>(
|
||||
width: usize,
|
||||
height: usize,
|
||||
@ -457,6 +499,7 @@ pub async fn run<
|
||||
result
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn message_box_ok(message: &str) {
|
||||
let title = "wezterm";
|
||||
let message = message.to_string();
|
||||
@ -497,7 +540,7 @@ pub fn show_configuration_error_message(err: &str) {
|
||||
])
|
||||
.map_err(Error::msg)?;
|
||||
|
||||
let mut editor = LineEditor::new(term);
|
||||
let mut editor = LineEditor::new(&mut term);
|
||||
editor.set_prompt("(press enter to close this window)");
|
||||
|
||||
let mut host = NopLineEditorHost::default();
|
||||
|
Loading…
Reference in New Issue
Block a user