mirror of
https://github.com/wez/wezterm.git
synced 2024-12-24 13:52:55 +03:00
internalized password/auth UI for ssh
This is a bit of a large commit because it needed some plumbing: * Change mux creation to allow deferring associating any domains, and to change the default domain later in the lifetime of the program * De-bounce the empty mux detection to allow for transient windows during early startup * Implement a bridge between the termwiz client Surface and the frontend gui renderer so that we can render from termwiz to the gui. * Adjust the line editor logic so that the highlight_line method can change the length of the output. This enables replacing the input text with placeholders so that we can obscure password input
This commit is contained in:
parent
fda9671197
commit
6da7b3ecd0
@ -9,7 +9,8 @@ use failure::Fallible;
|
||||
use promise::{BasicExecutor, Executor, SpawnFunc};
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
mod glyphcache;
|
||||
mod quad;
|
||||
@ -30,12 +31,12 @@ pub fn is_opengl_enabled() -> bool {
|
||||
}
|
||||
|
||||
impl GuiFrontEnd {
|
||||
pub fn try_new_no_opengl(mux: &Rc<Mux>) -> Fallible<Rc<dyn FrontEnd>> {
|
||||
pub fn try_new_no_opengl() -> Fallible<Rc<dyn FrontEnd>> {
|
||||
USE_OPENGL.store(false, Ordering::Release);
|
||||
Self::try_new(mux)
|
||||
Self::try_new()
|
||||
}
|
||||
|
||||
pub fn try_new(_mux: &Rc<Mux>) -> Fallible<Rc<dyn FrontEnd>> {
|
||||
pub fn try_new() -> Fallible<Rc<dyn FrontEnd>> {
|
||||
let connection = Connection::init()?;
|
||||
let front_end = Rc::new(GuiFrontEnd { connection });
|
||||
Ok(front_end)
|
||||
@ -61,13 +62,40 @@ impl FrontEnd for GuiFrontEnd {
|
||||
}
|
||||
|
||||
fn run_forever(&self) -> Fallible<()> {
|
||||
// We run until we've run out of windows in the Mux.
|
||||
// When we're running ssh we have a transient window
|
||||
// or two during authentication and we want to de-bounce
|
||||
// our decision to quit until we're sure that we have
|
||||
// no windows, so we track it here.
|
||||
struct State {
|
||||
when: Option<Instant>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn mark(&mut self, is_empty: bool) {
|
||||
if is_empty {
|
||||
let now = Instant::now();
|
||||
if let Some(start) = self.when.as_ref() {
|
||||
let diff = now - *start;
|
||||
if diff > Duration::new(5, 0) {
|
||||
Connection::get().unwrap().terminate_message_loop();
|
||||
}
|
||||
} else {
|
||||
self.when = Some(now);
|
||||
}
|
||||
} else {
|
||||
self.when = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let state = Arc::new(Mutex::new(State { when: None }));
|
||||
|
||||
self.connection
|
||||
.schedule_timer(std::time::Duration::from_millis(200), move || {
|
||||
let mux = Mux::get().unwrap();
|
||||
mux.prune_dead_windows();
|
||||
if mux.is_empty() {
|
||||
Connection::get().unwrap().terminate_message_loop();
|
||||
}
|
||||
state.lock().unwrap().mark(mux.is_empty());
|
||||
});
|
||||
|
||||
self.connection.run_message_loop()
|
||||
|
@ -2,7 +2,6 @@ use crate::config::Config;
|
||||
use crate::font::FontConfiguration;
|
||||
use crate::mux::tab::Tab;
|
||||
use crate::mux::window::WindowId;
|
||||
use crate::mux::Mux;
|
||||
use downcast_rs::{impl_downcast, Downcast};
|
||||
use failure::{format_err, Error, Fallible};
|
||||
use lazy_static::lazy_static;
|
||||
@ -55,12 +54,12 @@ pub fn front_end() -> Option<Rc<dyn FrontEnd>> {
|
||||
}
|
||||
|
||||
impl FrontEndSelection {
|
||||
pub fn try_new(self, mux: &Rc<Mux>) -> Result<Rc<dyn FrontEnd>, Error> {
|
||||
pub fn try_new(self) -> Result<Rc<dyn FrontEnd>, Error> {
|
||||
let front_end = match self {
|
||||
FrontEndSelection::MuxServer => muxserver::MuxServerFrontEnd::try_new(mux),
|
||||
FrontEndSelection::Null => muxserver::MuxServerFrontEnd::new_null(mux),
|
||||
FrontEndSelection::Software => gui::GuiFrontEnd::try_new_no_opengl(mux),
|
||||
FrontEndSelection::OpenGL => gui::GuiFrontEnd::try_new(mux),
|
||||
FrontEndSelection::MuxServer => muxserver::MuxServerFrontEnd::try_new(),
|
||||
FrontEndSelection::Null => muxserver::MuxServerFrontEnd::new_null(),
|
||||
FrontEndSelection::Software => gui::GuiFrontEnd::try_new_no_opengl(),
|
||||
FrontEndSelection::OpenGL => gui::GuiFrontEnd::try_new(),
|
||||
}?;
|
||||
|
||||
EXECUTOR.lock().unwrap().replace(front_end.executor());
|
||||
|
@ -39,21 +39,22 @@ pub struct MuxServerFrontEnd {
|
||||
|
||||
impl MuxServerFrontEnd {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::new_ret_no_self))]
|
||||
fn new(mux: &Rc<Mux>, start_listener: bool) -> Result<Rc<dyn FrontEnd>, Error> {
|
||||
fn new(start_listener: bool) -> Result<Rc<dyn FrontEnd>, Error> {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
if start_listener {
|
||||
let mux = Mux::get().unwrap();
|
||||
spawn_listener(mux.config())?;
|
||||
}
|
||||
Ok(Rc::new(Self { tx, rx }))
|
||||
}
|
||||
|
||||
pub fn try_new(mux: &Rc<Mux>) -> Result<Rc<dyn FrontEnd>, Error> {
|
||||
Self::new(mux, true)
|
||||
pub fn try_new() -> Result<Rc<dyn FrontEnd>, Error> {
|
||||
Self::new(true)
|
||||
}
|
||||
|
||||
pub fn new_null(mux: &Rc<Mux>) -> Result<Rc<dyn FrontEnd>, Error> {
|
||||
Self::new(mux, false)
|
||||
pub fn new_null() -> Result<Rc<dyn FrontEnd>, Error> {
|
||||
Self::new(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
102
src/main.rs
102
src/main.rs
@ -2,6 +2,7 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use failure::{err_msg, format_err, Error, Fallible};
|
||||
use promise::Future;
|
||||
use std::ffi::OsString;
|
||||
use std::fs::DirBuilder;
|
||||
use std::io::{Read, Write};
|
||||
@ -10,6 +11,7 @@ use std::os::unix::fs::DirBuilderExt;
|
||||
use std::path::Path;
|
||||
use structopt::StructOpt;
|
||||
use tabout::{tabulate_output, Alignment, Column};
|
||||
use window::{Connection, ConnectionOps};
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
@ -23,8 +25,9 @@ mod mux;
|
||||
mod ratelim;
|
||||
mod server;
|
||||
mod ssh;
|
||||
mod termwiztermtab;
|
||||
|
||||
use crate::frontend::FrontEndSelection;
|
||||
use crate::frontend::{executor, front_end, FrontEndSelection};
|
||||
use crate::mux::domain::{Domain, LocalDomain};
|
||||
use crate::mux::Mux;
|
||||
use crate::server::client::{unix_connect_with_retry, Client};
|
||||
@ -287,39 +290,70 @@ impl SshParameters {
|
||||
}
|
||||
|
||||
fn run_ssh(config: Arc<config::Config>, opts: &SshCommand) -> Fallible<()> {
|
||||
let font_system = opts.font_system.unwrap_or(config.font_system);
|
||||
font_system.set_default();
|
||||
|
||||
let fontconfig = Rc::new(FontConfiguration::new(Arc::clone(&config), font_system));
|
||||
let cmd = if !opts.prog.is_empty() {
|
||||
let argv: Vec<&std::ffi::OsStr> = opts.prog.iter().map(|x| x.as_os_str()).collect();
|
||||
let mut builder = CommandBuilder::new(&argv[0]);
|
||||
builder.args(&argv[1..]);
|
||||
Some(builder)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let front_end_selection = opts.front_end.unwrap_or(config.front_end);
|
||||
let gui = front_end_selection.try_new()?;
|
||||
|
||||
let params = SshParameters::parse(&opts.user_at_host_and_port)?;
|
||||
let opts = opts.clone();
|
||||
|
||||
let sess = ssh::ssh_connect(¶ms.host_and_port, ¶ms.username)?;
|
||||
let pty_system = Box::new(portable_pty::ssh::SshSession::new(sess, &config.term));
|
||||
let domain: Arc<dyn Domain> = Arc::new(ssh::RemoteSshDomain::with_pty_system(
|
||||
&opts.user_at_host_and_port,
|
||||
&config,
|
||||
pty_system,
|
||||
));
|
||||
|
||||
let mux = Rc::new(mux::Mux::new(&config, &domain));
|
||||
// Set up the mux with no default domain; there's a good chance that
|
||||
// we'll need to show authentication UI and we don't want its domain
|
||||
// to become the default domain.
|
||||
let mux = Rc::new(mux::Mux::new(&config, None));
|
||||
Mux::set_mux(&mux);
|
||||
|
||||
let front_end = opts.front_end.unwrap_or(config.front_end);
|
||||
let gui = front_end.try_new(&mux)?;
|
||||
domain.attach()?;
|
||||
// Initiate an ssh connection; since that is a blocking process with
|
||||
// callbacks, we have to run it in another thread
|
||||
std::thread::spawn(move || {
|
||||
// Establish the connection; it may show UI for authentication
|
||||
let sess = match ssh::ssh_connect(¶ms.host_and_port, ¶ms.username) {
|
||||
Ok(sess) => sess,
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
Future::with_executor(executor(), move || {
|
||||
Connection::get().unwrap().terminate_message_loop();
|
||||
Ok(())
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let window_id = mux.new_empty_window();
|
||||
let tab = domain.spawn(PtySize::default(), cmd, window_id)?;
|
||||
gui.spawn_new_window(mux.config(), &fontconfig, &tab, window_id)?;
|
||||
// Now we have a connected session, set up the ssh domain and make it
|
||||
// the default domain
|
||||
Future::with_executor(executor(), move || {
|
||||
let gui = front_end().unwrap();
|
||||
|
||||
let font_system = opts.font_system.unwrap_or(config.font_system);
|
||||
font_system.set_default();
|
||||
|
||||
let fontconfig = Rc::new(FontConfiguration::new(Arc::clone(&config), font_system));
|
||||
let cmd = if !opts.prog.is_empty() {
|
||||
let argv: Vec<&std::ffi::OsStr> = opts.prog.iter().map(|x| x.as_os_str()).collect();
|
||||
let mut builder = CommandBuilder::new(&argv[0]);
|
||||
builder.args(&argv[1..]);
|
||||
Some(builder)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let pty_system = Box::new(portable_pty::ssh::SshSession::new(sess, &config.term));
|
||||
let domain: Arc<dyn Domain> = Arc::new(ssh::RemoteSshDomain::with_pty_system(
|
||||
&opts.user_at_host_and_port,
|
||||
&config,
|
||||
pty_system,
|
||||
));
|
||||
|
||||
let mux = Mux::get().unwrap();
|
||||
mux.add_domain(&domain);
|
||||
mux.set_default_domain(&domain);
|
||||
domain.attach()?;
|
||||
|
||||
let window_id = mux.new_empty_window();
|
||||
let tab = domain.spawn(PtySize::default(), cmd, window_id)?;
|
||||
gui.spawn_new_window(mux.config(), &fontconfig, &tab, window_id)?;
|
||||
Ok(())
|
||||
});
|
||||
});
|
||||
|
||||
gui.run_forever()
|
||||
}
|
||||
@ -338,11 +372,11 @@ fn run_serial(config: Arc<config::Config>, opts: &SerialCommand) -> Fallible<()>
|
||||
let pty_system = Box::new(portable_pty::serial::SerialTty::new(&opts.port));
|
||||
let domain: Arc<dyn Domain> =
|
||||
Arc::new(LocalDomain::with_pty_system("local", &config, pty_system));
|
||||
let mux = Rc::new(mux::Mux::new(&config, &domain));
|
||||
let mux = Rc::new(mux::Mux::new(&config, Some(domain.clone())));
|
||||
Mux::set_mux(&mux);
|
||||
|
||||
let front_end = opts.front_end.unwrap_or(config.front_end);
|
||||
let gui = front_end.try_new(&mux)?;
|
||||
let gui = front_end.try_new()?;
|
||||
domain.attach()?;
|
||||
|
||||
let window_id = mux.new_empty_window();
|
||||
@ -385,11 +419,11 @@ fn run_mux_client(config: Arc<config::Config>, opts: &ConnectCommand) -> Fallibl
|
||||
let fontconfig = Rc::new(FontConfiguration::new(Arc::clone(&config), font_system));
|
||||
|
||||
let domain: Arc<dyn Domain> = Arc::new(ClientDomain::new(client_config));
|
||||
let mux = Rc::new(mux::Mux::new(&config, &domain));
|
||||
let mux = Rc::new(mux::Mux::new(&config, Some(domain.clone())));
|
||||
Mux::set_mux(&mux);
|
||||
|
||||
let front_end = opts.front_end.unwrap_or(config.front_end);
|
||||
let gui = front_end.try_new(&mux)?;
|
||||
let gui = front_end.try_new()?;
|
||||
domain.attach()?;
|
||||
|
||||
if mux.is_empty() {
|
||||
@ -468,11 +502,11 @@ fn run_terminal_gui(config: Arc<config::Config>, opts: &StartCommand) -> Fallibl
|
||||
};
|
||||
|
||||
let domain: Arc<dyn Domain> = Arc::new(LocalDomain::new("local", &config)?);
|
||||
let mux = Rc::new(mux::Mux::new(&config, &domain));
|
||||
let mux = Rc::new(mux::Mux::new(&config, Some(domain.clone())));
|
||||
Mux::set_mux(&mux);
|
||||
|
||||
let front_end = opts.front_end.unwrap_or(config.front_end);
|
||||
let gui = front_end.try_new(&mux)?;
|
||||
let gui = front_end.try_new()?;
|
||||
domain.attach()?;
|
||||
|
||||
fn record_domain(mux: &Rc<Mux>, client: ClientDomain) -> Fallible<Arc<dyn Domain>> {
|
||||
|
@ -39,7 +39,7 @@ pub struct Mux {
|
||||
tabs: RefCell<HashMap<TabId, Rc<dyn Tab>>>,
|
||||
windows: RefCell<HashMap<WindowId, Window>>,
|
||||
config: Arc<Config>,
|
||||
default_domain: Arc<dyn Domain>,
|
||||
default_domain: RefCell<Option<Arc<dyn Domain>>>,
|
||||
domains: RefCell<HashMap<DomainId, Arc<dyn Domain>>>,
|
||||
domains_by_name: RefCell<HashMap<String, Arc<dyn Domain>>>,
|
||||
subscribers: RefCell<HashMap<usize, PollableSender<MuxNotification>>>,
|
||||
@ -134,21 +134,23 @@ thread_local! {
|
||||
}
|
||||
|
||||
impl Mux {
|
||||
pub fn new(config: &Arc<Config>, default_domain: &Arc<dyn Domain>) -> Self {
|
||||
pub fn new(config: &Arc<Config>, default_domain: Option<Arc<dyn Domain>>) -> Self {
|
||||
let mut domains = HashMap::new();
|
||||
domains.insert(default_domain.domain_id(), Arc::clone(default_domain));
|
||||
|
||||
let mut domains_by_name = HashMap::new();
|
||||
domains_by_name.insert(
|
||||
default_domain.domain_name().to_string(),
|
||||
Arc::clone(default_domain),
|
||||
);
|
||||
if let Some(default_domain) = default_domain.as_ref() {
|
||||
domains.insert(default_domain.domain_id(), Arc::clone(default_domain));
|
||||
|
||||
domains_by_name.insert(
|
||||
default_domain.domain_name().to_string(),
|
||||
Arc::clone(default_domain),
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
tabs: RefCell::new(HashMap::new()),
|
||||
windows: RefCell::new(HashMap::new()),
|
||||
config: Arc::clone(config),
|
||||
default_domain: Arc::clone(default_domain),
|
||||
default_domain: RefCell::new(default_domain),
|
||||
domains_by_name: RefCell::new(domains_by_name),
|
||||
domains: RefCell::new(domains),
|
||||
subscribers: RefCell::new(HashMap::new()),
|
||||
@ -167,8 +169,16 @@ impl Mux {
|
||||
subscribers.retain(|_, tx| tx.send(notification.clone()).is_ok());
|
||||
}
|
||||
|
||||
pub fn default_domain(&self) -> &Arc<dyn Domain> {
|
||||
&self.default_domain
|
||||
pub fn default_domain(&self) -> Arc<dyn Domain> {
|
||||
self.default_domain
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(Arc::clone)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn set_default_domain(&self, domain: &Arc<dyn Domain>) {
|
||||
*self.default_domain.borrow_mut() = Some(Arc::clone(domain));
|
||||
}
|
||||
|
||||
pub fn get_domain(&self, id: DomainId) -> Option<Arc<dyn Domain>> {
|
||||
@ -180,6 +190,9 @@ impl Mux {
|
||||
}
|
||||
|
||||
pub fn add_domain(&self, domain: &Arc<dyn Domain>) {
|
||||
if self.default_domain.borrow().is_none() {
|
||||
*self.default_domain.borrow_mut() = Some(Arc::clone(domain));
|
||||
}
|
||||
self.domains
|
||||
.borrow_mut()
|
||||
.insert(domain.domain_id(), Arc::clone(domain));
|
||||
|
82
src/ssh.rs
82
src/ssh.rs
@ -4,6 +4,7 @@ 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 failure::Error;
|
||||
use failure::{bail, format_err, Fallible};
|
||||
use portable_pty::cmdbuilder::CommandBuilder;
|
||||
@ -14,6 +15,10 @@ use std::net::TcpStream;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use termwiz::cell::{unicode_column_width, AttributeChange, Intensity};
|
||||
use termwiz::lineedit::*;
|
||||
use termwiz::surface::Change;
|
||||
use termwiz::terminal::*;
|
||||
|
||||
fn password_prompt(
|
||||
instructions: &str,
|
||||
@ -21,11 +26,58 @@ fn password_prompt(
|
||||
username: &str,
|
||||
remote_address: &str,
|
||||
) -> Option<String> {
|
||||
let title = format!("🔐 wezterm: SSH authentication");
|
||||
let text = format!(
|
||||
"SSH Authentication for {} @ {}\n{}\n{}",
|
||||
"🔐 SSH Authentication for {} @ {}\n{}\n{}",
|
||||
username, remote_address, instructions, prompt
|
||||
);
|
||||
tinyfiledialogs::password_box("wezterm", &text)
|
||||
|
||||
#[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> {
|
||||
let grapheme_count = unicode_column_width(line);
|
||||
let mut output = vec![];
|
||||
for _ in 0..grapheme_count {
|
||||
output.push(OutputElement::Text("🔑".to_string()));
|
||||
}
|
||||
output
|
||||
}
|
||||
}
|
||||
match 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("Password: ");
|
||||
|
||||
let mut host = PasswordPromptHost::default();
|
||||
if let Some(line) = editor.read_line(&mut host)? {
|
||||
Ok(line)
|
||||
} else {
|
||||
bail!("prompt cancelled");
|
||||
}
|
||||
})
|
||||
.wait()
|
||||
{
|
||||
Ok(p) => Some(p),
|
||||
Err(p) => {
|
||||
log::error!("failed to prompt for pw: {}", p);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn input_prompt(
|
||||
@ -34,11 +86,35 @@ fn input_prompt(
|
||||
username: &str,
|
||||
remote_address: &str,
|
||||
) -> Option<String> {
|
||||
let title = format!("🔐 wezterm: SSH authentication");
|
||||
let text = format!(
|
||||
"SSH Authentication for {} @ {}\n{}\n{}",
|
||||
username, remote_address, instructions, prompt
|
||||
);
|
||||
tinyfiledialogs::input_box("wezterm", &text, "")
|
||||
match 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);
|
||||
|
||||
let mut host = NopLineEditorHost::default();
|
||||
if let Some(line) = editor.read_line(&mut host)? {
|
||||
Ok(line)
|
||||
} else {
|
||||
bail!("prompt cancelled");
|
||||
}
|
||||
})
|
||||
.wait()
|
||||
{
|
||||
Ok(p) => Some(p),
|
||||
Err(p) => {
|
||||
log::error!("failed to prompt for pw: {}", p);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Prompt<'a> {
|
||||
|
445
src/termwiztermtab.rs
Normal file
445
src/termwiztermtab.rs
Normal file
@ -0,0 +1,445 @@
|
||||
//! a tab hosting a termwiz terminal applet
|
||||
//! The idea is to use these when wezterm needs to request
|
||||
//! input from the user as part of eg: setting up an ssh
|
||||
//! session.
|
||||
|
||||
use crate::font::FontConfiguration;
|
||||
use crate::frontend::{executor, front_end};
|
||||
use crate::mux::domain::{alloc_domain_id, Domain, DomainId, DomainState};
|
||||
use crate::mux::renderable::Renderable;
|
||||
use crate::mux::tab::{alloc_tab_id, Tab, TabId};
|
||||
use crate::mux::window::WindowId;
|
||||
use crate::mux::Mux;
|
||||
use crossbeam_channel::{unbounded as channel, Receiver, Sender};
|
||||
use failure::*;
|
||||
use filedescriptor::Pipe;
|
||||
use portable_pty::*;
|
||||
use promise::{Future, Promise};
|
||||
use std::borrow::Cow;
|
||||
use std::cell::RefCell;
|
||||
use std::cell::RefMut;
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use term::color::ColorPalette;
|
||||
use term::selection::SelectionRange;
|
||||
use term::{CursorPosition, KeyCode, KeyModifiers, Line, MouseEvent, TerminalHost};
|
||||
use termwiz::hyperlink::Hyperlink;
|
||||
use termwiz::input::{InputEvent, KeyEvent};
|
||||
use termwiz::surface::{Change, SequenceNo, Surface};
|
||||
use termwiz::terminal::ScreenSize;
|
||||
use termwiz::terminal::TerminalWaker;
|
||||
|
||||
struct RenderableInner {
|
||||
surface: Surface,
|
||||
selection_range: Arc<Mutex<Option<SelectionRange>>>,
|
||||
something_changed: Arc<AtomicBool>,
|
||||
highlight: Arc<Mutex<Option<Arc<Hyperlink>>>>,
|
||||
local_sequence: SequenceNo,
|
||||
dead: bool,
|
||||
render_rx: Receiver<Vec<Change>>,
|
||||
input_tx: Sender<InputEvent>,
|
||||
}
|
||||
|
||||
struct RenderableState {
|
||||
inner: RefCell<RenderableInner>,
|
||||
}
|
||||
|
||||
impl std::io::Write for RenderableState {
|
||||
fn write(&mut self, data: &[u8]) -> Result<usize, std::io::Error> {
|
||||
if let Ok(s) = std::str::from_utf8(data) {
|
||||
let paste = InputEvent::Paste(s.to_string());
|
||||
self.inner.borrow_mut().input_tx.send(paste).ok();
|
||||
}
|
||||
|
||||
Ok(data.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), std::io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderable for RenderableState {
|
||||
fn get_cursor_position(&self) -> CursorPosition {
|
||||
let (x, y) = self.inner.borrow().surface.cursor_position();
|
||||
CursorPosition { x, y: y as i64 }
|
||||
}
|
||||
|
||||
fn get_dirty_lines(&self) -> Vec<(usize, Cow<Line>, Range<usize>)> {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
let seq = inner.surface.current_seqno();
|
||||
inner.surface.flush_changes_older_than(seq);
|
||||
let selection = *inner.selection_range.lock().unwrap();
|
||||
inner.something_changed.store(false, Ordering::SeqCst);
|
||||
inner.local_sequence = seq;
|
||||
inner
|
||||
.surface
|
||||
.screen_lines()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, line)| {
|
||||
let r = match selection {
|
||||
None => 0..0,
|
||||
Some(sel) => sel.normalize().cols_for_row(idx as i32),
|
||||
};
|
||||
(idx, Cow::Owned(line.into_owned()), r)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn has_dirty_lines(&self) -> bool {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
|
||||
loop {
|
||||
match inner.render_rx.try_recv() {
|
||||
Ok(changes) => {
|
||||
inner.surface.add_changes(changes);
|
||||
}
|
||||
Err(err) => {
|
||||
if err.is_disconnected() {
|
||||
inner.dead = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inner.something_changed.load(Ordering::SeqCst) {
|
||||
return true;
|
||||
}
|
||||
inner.surface.has_changes(inner.local_sequence)
|
||||
}
|
||||
|
||||
fn make_all_lines_dirty(&mut self) {
|
||||
self.inner
|
||||
.borrow()
|
||||
.something_changed
|
||||
.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn clean_dirty_lines(&mut self) {}
|
||||
|
||||
fn current_highlight(&self) -> Option<Arc<Hyperlink>> {
|
||||
self.inner
|
||||
.borrow()
|
||||
.highlight
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn physical_dimensions(&self) -> (usize, usize) {
|
||||
let (cols, rows) = self.inner.borrow().surface.dimensions();
|
||||
(rows, cols)
|
||||
}
|
||||
}
|
||||
|
||||
struct TermWizTerminalDomain {
|
||||
domain_id: DomainId,
|
||||
}
|
||||
|
||||
impl TermWizTerminalDomain {
|
||||
pub fn new() -> Self {
|
||||
let domain_id = alloc_domain_id();
|
||||
Self { domain_id }
|
||||
}
|
||||
}
|
||||
|
||||
impl Domain for TermWizTerminalDomain {
|
||||
fn spawn(
|
||||
&self,
|
||||
_size: PtySize,
|
||||
_command: Option<CommandBuilder>,
|
||||
_window: WindowId,
|
||||
) -> Fallible<Rc<dyn Tab>> {
|
||||
bail!("cannot spawn tabs in a TermWizTerminalTab");
|
||||
}
|
||||
|
||||
fn domain_id(&self) -> DomainId {
|
||||
self.domain_id
|
||||
}
|
||||
|
||||
fn domain_name(&self) -> &str {
|
||||
"TermWizTerminalDomain"
|
||||
}
|
||||
fn attach(&self) -> Fallible<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn detach(&self) -> Fallible<()> {
|
||||
bail!("detach not implemented for TermWizTerminalDomain");
|
||||
}
|
||||
|
||||
fn state(&self) -> DomainState {
|
||||
DomainState::Attached
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TermWizTerminalTab {
|
||||
tab_id: TabId,
|
||||
domain_id: DomainId,
|
||||
renderable: RefCell<RenderableState>,
|
||||
reader: Pipe,
|
||||
}
|
||||
|
||||
impl Drop for TermWizTerminalTab {
|
||||
fn drop(&mut self) {
|
||||
log::error!("Dropping TermWizTerminalTab");
|
||||
}
|
||||
}
|
||||
|
||||
impl TermWizTerminalTab {
|
||||
fn new(domain_id: DomainId, inner: RenderableInner) -> Self {
|
||||
let tab_id = alloc_tab_id();
|
||||
let renderable = RefCell::new(RenderableState {
|
||||
inner: RefCell::new(inner),
|
||||
});
|
||||
let reader = Pipe::new().expect("Pipe::new failed");
|
||||
Self {
|
||||
tab_id,
|
||||
domain_id,
|
||||
renderable,
|
||||
reader,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tab for TermWizTerminalTab {
|
||||
fn tab_id(&self) -> TabId {
|
||||
self.tab_id
|
||||
}
|
||||
|
||||
fn renderer(&self) -> RefMut<dyn Renderable> {
|
||||
self.renderable.borrow_mut()
|
||||
}
|
||||
|
||||
fn get_title(&self) -> String {
|
||||
let renderable = self.renderable.borrow();
|
||||
let surface = &renderable.inner.borrow().surface;
|
||||
surface.title().to_string()
|
||||
}
|
||||
|
||||
fn send_paste(&self, text: &str) -> Fallible<()> {
|
||||
let paste = InputEvent::Paste(text.to_string());
|
||||
self.renderable
|
||||
.borrow_mut()
|
||||
.inner
|
||||
.borrow_mut()
|
||||
.input_tx
|
||||
.send(paste)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reader(&self) -> Fallible<Box<dyn std::io::Read + Send>> {
|
||||
Ok(Box::new(self.reader.read.try_clone()?))
|
||||
}
|
||||
|
||||
fn writer(&self) -> RefMut<dyn std::io::Write> {
|
||||
self.renderable.borrow_mut()
|
||||
}
|
||||
|
||||
fn resize(&self, size: PtySize) -> Fallible<()> {
|
||||
self.renderable
|
||||
.borrow()
|
||||
.inner
|
||||
.borrow_mut()
|
||||
.surface
|
||||
.resize(size.cols as usize, size.rows as usize);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn key_down(&self, key: KeyCode, modifiers: KeyModifiers) -> Fallible<()> {
|
||||
let event = InputEvent::Key(KeyEvent { key, modifiers });
|
||||
self.renderable
|
||||
.borrow_mut()
|
||||
.inner
|
||||
.borrow_mut()
|
||||
.input_tx
|
||||
.send(event)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mouse_event(&self, _event: MouseEvent, _host: &mut dyn TerminalHost) -> Fallible<()> {
|
||||
// FIXME: send mouse events through
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn advance_bytes(&self, _buf: &[u8], _host: &mut dyn TerminalHost) {
|
||||
panic!("advance_bytes is undefed for TermWizTerminalTab");
|
||||
}
|
||||
|
||||
fn is_dead(&self) -> bool {
|
||||
self.renderable.borrow().inner.borrow().dead
|
||||
}
|
||||
|
||||
fn palette(&self) -> ColorPalette {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn domain_id(&self) -> DomainId {
|
||||
self.domain_id
|
||||
}
|
||||
|
||||
fn selection_range(&self) -> Option<SelectionRange> {
|
||||
*self
|
||||
.renderable
|
||||
.borrow()
|
||||
.inner
|
||||
.borrow()
|
||||
.selection_range
|
||||
.lock()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TermWizTerminal {
|
||||
render_tx: Sender<Vec<Change>>,
|
||||
input_rx: Receiver<InputEvent>,
|
||||
screen_size: ScreenSize,
|
||||
}
|
||||
|
||||
impl TermWizTerminal {
|
||||
fn do_input_poll(&mut self, wait: Option<Duration>) -> Fallible<Option<InputEvent>> {
|
||||
if let Some(timeout) = wait {
|
||||
match self.input_rx.recv_timeout(timeout) {
|
||||
Ok(input) => Ok(Some(input)),
|
||||
Err(err) => {
|
||||
if err.is_timeout() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let input = self.input_rx.recv()?;
|
||||
Ok(Some(input))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl termwiz::terminal::Terminal for TermWizTerminal {
|
||||
fn set_raw_mode(&mut self) -> Fallible<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_cooked_mode(&mut self) -> Fallible<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enter_alternate_screen(&mut self) -> Fallible<()> {
|
||||
bail!("TermWizTerminalTab has no alt screen");
|
||||
}
|
||||
|
||||
fn exit_alternate_screen(&mut self) -> Fallible<()> {
|
||||
bail!("TermWizTerminalTab has no alt screen");
|
||||
}
|
||||
|
||||
fn get_screen_size(&mut self) -> Fallible<ScreenSize> {
|
||||
Ok(self.screen_size)
|
||||
}
|
||||
|
||||
fn set_screen_size(&mut self, _size: ScreenSize) -> Fallible<()> {
|
||||
bail!("TermWizTerminalTab cannot set screen size");
|
||||
}
|
||||
|
||||
fn render(&mut self, changes: &[Change]) -> Fallible<()> {
|
||||
self.render_tx.send(changes.to_vec())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Fallible<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn poll_input(&mut self, wait: Option<Duration>) -> Fallible<Option<InputEvent>> {
|
||||
self.do_input_poll(wait).map(|i| {
|
||||
if let Some(InputEvent::Resized { cols, rows }) = i.as_ref() {
|
||||
self.screen_size.cols = *cols;
|
||||
self.screen_size.rows = *rows;
|
||||
}
|
||||
i
|
||||
})
|
||||
}
|
||||
|
||||
fn waker(&self) -> TerminalWaker {
|
||||
// TODO: TerminalWaker assumes that we're a SystemTerminal but that
|
||||
// isn't the case here.
|
||||
panic!("TermWizTerminal::waker called!?");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// from the terminal window.
|
||||
/// When it completes its loop it will fulfil a promise and yield
|
||||
/// the return value from the function.
|
||||
pub fn run<T: Send + 'static, F: Send + 'static + Fn(TermWizTerminal) -> Fallible<T>>(
|
||||
width: usize,
|
||||
height: usize,
|
||||
f: F,
|
||||
) -> Future<T> {
|
||||
let (render_tx, render_rx) = channel();
|
||||
let (input_tx, input_rx) = channel();
|
||||
|
||||
let tw_term = TermWizTerminal {
|
||||
render_tx,
|
||||
input_rx,
|
||||
screen_size: ScreenSize {
|
||||
cols: width,
|
||||
rows: height,
|
||||
xpixel: 0,
|
||||
ypixel: 0,
|
||||
},
|
||||
};
|
||||
|
||||
let mut promise = Promise::new();
|
||||
let future = promise.get_future().expect("just made the promise");
|
||||
|
||||
Future::with_executor(executor(), move || {
|
||||
let mux = Mux::get().unwrap();
|
||||
|
||||
// TODO: make a singleton
|
||||
let domain: Arc<dyn Domain> = Arc::new(TermWizTerminalDomain::new());
|
||||
mux.add_domain(&domain);
|
||||
|
||||
let window_id = mux.new_empty_window();
|
||||
|
||||
let inner = RenderableInner {
|
||||
surface: Surface::new(width, height),
|
||||
highlight: Arc::new(Mutex::new(None)),
|
||||
local_sequence: 0,
|
||||
dead: false,
|
||||
something_changed: Arc::new(AtomicBool::new(false)),
|
||||
selection_range: Arc::new(Mutex::new(None)),
|
||||
input_tx,
|
||||
render_rx,
|
||||
};
|
||||
|
||||
let tab: Rc<dyn Tab> = Rc::new(TermWizTerminalTab::new(domain.domain_id(), inner));
|
||||
|
||||
mux.add_tab(&tab)?;
|
||||
mux.add_tab_to_window(&tab, window_id)?;
|
||||
|
||||
let fontconfig = Rc::new(FontConfiguration::new(
|
||||
Arc::clone(mux.config()),
|
||||
crate::font::FontSystemSelection::get_default(),
|
||||
));
|
||||
|
||||
let gui = front_end().unwrap();
|
||||
gui.spawn_new_window(mux.config(), &fontconfig, &tab, window_id)?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
promise.result(f(tw_term));
|
||||
});
|
||||
|
||||
future
|
||||
}
|
@ -163,18 +163,14 @@ impl<T: Terminal> LineEditor<T> {
|
||||
}
|
||||
changes.push(Change::AllAttributes(Default::default()));
|
||||
|
||||
let mut grapheme_count = 0;
|
||||
for ele in host.highlight_line(&self.line, self.cursor) {
|
||||
if let OutputElement::Text(ref t) = ele {
|
||||
grapheme_count += unicode_column_width(t);
|
||||
}
|
||||
changes.push(ele.into());
|
||||
}
|
||||
|
||||
// In order to position the terminal cursor at the right spot,
|
||||
// we need to compute how many graphemes away from the start of
|
||||
// the line the current insertion point is. We can do this by
|
||||
// slicing into the string and requesting its unicode width.
|
||||
// It might feel more right to count the number of graphemes in
|
||||
// the string, but this doesn't render correctly for glyphs that
|
||||
// are double-width. Nothing about unicode is easy :-/
|
||||
let grapheme_count = unicode_column_width(&self.line[0..self.cursor]);
|
||||
changes.push(Change::CursorPosition {
|
||||
x: Position::Absolute(prompt_width + grapheme_count),
|
||||
y: Position::NoChange,
|
||||
@ -313,6 +309,10 @@ impl<T: Terminal> LineEditor<T> {
|
||||
modifiers: Modifiers::NONE,
|
||||
}) => Some(Action::Move(Movement::ForwardChar(1))),
|
||||
InputEvent::Key(KeyEvent {
|
||||
key: KeyCode::Char(c),
|
||||
modifiers: Modifiers::SHIFT,
|
||||
})
|
||||
| InputEvent::Key(KeyEvent {
|
||||
key: KeyCode::Char(c),
|
||||
modifiers: Modifiers::NONE,
|
||||
}) => Some(Action::InsertChar(1, *c)),
|
||||
|
Loading…
Reference in New Issue
Block a user