From 1fe9e16f25e657eb3c09d5b4c0b51f6fc53be540 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Sat, 25 Jan 2020 15:10:34 -0800 Subject: [PATCH] refactor ssh connection ui into its own module I want to re-use this for the mux client --- src/connui.rs | 169 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/ssh.rs | 174 ++------------------------------------------------ 3 files changed, 175 insertions(+), 169 deletions(-) create mode 100644 src/connui.rs diff --git a/src/connui.rs b/src/connui.rs new file mode 100644 index 000000000..6774632f4 --- /dev/null +++ b/src/connui.rs @@ -0,0 +1,169 @@ +use crate::termwiztermtab; +use anyhow::bail; +use crossbeam::channel::{bounded, Receiver, Sender}; +use promise::Promise; +use std::time::Duration; +use termwiz::cell::unicode_column_width; +use termwiz::lineedit::*; +use termwiz::surface::Change; +use termwiz::terminal::*; + +#[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, 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), + /// Request input + Input { + prompt: String, + echo: bool, + respond: Promise, + }, + Close, +} + +struct ConnectionUIImpl { + term: termwiztermtab::TermWizTerminal, + rx: Receiver, +} + +impl ConnectionUIImpl { + fn run(&mut self) -> anyhow::Result<()> { + 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 { + 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 { + 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"); + } + } +} + +pub struct ConnectionUI { + tx: Sender, +} + +impl ConnectionUI { + pub fn new() -> Self { + let (tx, rx) = bounded(16); + promise::spawn::spawn_into_main_thread(termwiztermtab::run(70, 15, move |term| { + let mut ui = ConnectionUIImpl { term, rx }; + ui.run() + })); + Self { tx } + } + + pub fn title(&self, title: &str) { + self.output(vec![Change::Title(title.to_string())]); + } + + pub fn output(&self, changes: Vec) { + self.tx + .send(UIRequest::Output(changes)) + .expect("send to SShUI failed"); + } + + pub fn output_str(&self, s: &str) { + let s = s.replace("\n", "\r\n"); + self.output(vec![Change::Text(s)]); + } + + pub fn input(&self, prompt: &str) -> anyhow::Result { + 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 ConnectionUI failed"); + + future.wait() + } + + pub fn password(&self, prompt: &str) -> anyhow::Result { + 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 ConnectionUI failed"); + + future.wait() + } + + pub fn close(&self) { + self.tx + .send(UIRequest::Close) + .expect("send to ConnectionUI failed"); + } +} diff --git a/src/main.rs b/src/main.rs index a2e5eb8b6..9e6cea0b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ use structopt::StructOpt; use tabout::{tabulate_output, Alignment, Column}; mod config; +mod connui; mod frontend; mod keyassignment; mod localtab; diff --git a/src/ssh.rs b/src/ssh.rs index 29338040c..1b756fc59 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -1,12 +1,11 @@ +use crate::connui::ConnectionUI; use crate::localtab::LocalTab; use crate::mux::domain::{alloc_domain_id, Domain, DomainId, DomainState}; use crate::mux::tab::Tab; use crate::mux::window::WindowId; 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}; @@ -15,172 +14,8 @@ use std::io::Write; use std::net::TcpStream; use std::path::Path; use std::rc::Rc; -use std::time::Duration; -use termwiz::cell::unicode_column_width; -use termwiz::lineedit::*; -use termwiz::surface::Change; -use termwiz::terminal::*; -#[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, 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) - } -} - -enum UIRequest { - /// Display something - Output(Vec), - /// Request input - Input { - prompt: String, - echo: bool, - respond: Promise, - }, - Close, -} - -struct SshUIImpl { - term: termwiztermtab::TermWizTerminal, - rx: Receiver, -} - -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 { - 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 { - 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"); - } - } -} - -struct SshUI { - tx: Sender, -} - -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) { - 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 { - 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 { - 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 { +impl ssh2::KeyboardInteractivePrompt for ConnectionUI { fn prompt<'b>( &mut self, _username: &str, @@ -214,7 +49,7 @@ pub fn async_ssh_connect(remote_address: &str, username: &str) -> Future anyhow::Result { let mut sess = ssh2::Session::new()?; @@ -371,7 +206,8 @@ fn ssh_connect_with_ui( } pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result { - let mut ui = SshUI::new(); + let mut ui = ConnectionUI::new(); + ui.title("🔐 wezterm: SSH authentication"); let res = ssh_connect_with_ui(remote_address, username, &mut ui); match res { Ok(sess) => {