diff --git a/Cargo.toml b/Cargo.toml index 3b006ef..2f7a8ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ keywords = ["terminal", "egui"] [workspace.dependencies] alacritty_terminal = { git = "https://github.com/alacritty/alacritty" } anyhow = "1" +bytesize = "1" +camino = "1" catppuccin-egui = { git = "https://github.com/iamazy/catppuccin-egui", branch = "egui30" } chrono = "0.4" copypasta = "0.10" @@ -34,6 +36,7 @@ egui_form = "0.5" egui-phosphor = "0.9" egui-theme-switch = "0.2.3" egui-toast = "0.16" +file-format = "0.26" garde = "0.22" homedir = "0.3" indexmap = "2" @@ -48,10 +51,9 @@ serde = "1" signal-hook = "0.3" smol = "2" thiserror = "2" +time = "0.3" tracing = "0.1" tracing-subscriber = "0.3" -uuid = "1" -validator = "0.20" wezterm-ssh = { git = "https://github.com/iamazy/wezterm.git", branch = "nxssh" } windows = "0.59" diff --git a/crates/egui-term/Cargo.toml b/crates/egui-term/Cargo.toml index e5edf22..0380bc1 100644 --- a/crates/egui-term/Cargo.toml +++ b/crates/egui-term/Cargo.toml @@ -13,13 +13,19 @@ keywords.workspace = true [dependencies] alacritty_terminal.workspace = true anyhow.workspace = true +bytesize.workspace = true +camino.workspace = true copypasta.workspace = true egui.workspace = true +egui_extras.workspace = true +file-format.workspace = true +homedir.workspace = true open.workspace = true parking_lot.workspace = true polling.workspace = true smol.workspace = true thiserror.workspace = true +time.workspace = true tracing.workspace = true wezterm-ssh = { workspace = true, features = ["vendored-openssl"] } diff --git a/crates/egui-term/examples/custom_bindings.rs b/crates/egui-term/examples/custom_bindings.rs index 4bf5cd4..34a527c 100644 --- a/crates/egui-term/examples/custom_bindings.rs +++ b/crates/egui-term/examples/custom_bindings.rs @@ -1,5 +1,5 @@ use copypasta::ClipboardContext; -use egui::{Id, Key, Modifiers, Vec2}; +use egui::{Key, Modifiers, Vec2}; use egui_term::{ generate_bindings, Binding, BindingAction, InputKind, KeyboardBinding, PtyEvent, TermMode, Terminal, TerminalContext, TerminalFont, TerminalOptions, TerminalTheme, TerminalView, @@ -11,7 +11,6 @@ pub struct App { terminal_font: TerminalFont, terminal_theme: TerminalTheme, multi_exec: bool, - active_id: Option, clipboard: ClipboardContext, pty_proxy_receiver: Receiver<(u64, PtyEvent)>, custom_terminal_bindings: Vec<(Binding, BindingAction)>, @@ -67,7 +66,6 @@ impl App { terminal_theme: TerminalTheme::default(), terminal_font: TerminalFont::default(), multi_exec: false, - active_id: None, clipboard: ClipboardContext::new().unwrap(), pty_proxy_receiver, custom_terminal_bindings, @@ -83,13 +81,14 @@ impl eframe::App for App { } egui::CentralPanel::default().show(ctx, |ui| { - let term_ctx = TerminalContext::new(&mut self.terminal_backend, &mut self.clipboard); + let term_ctx = + TerminalContext::new(&mut self.terminal_backend, &mut self.clipboard, &mut false); let term_opt = TerminalOptions { font: &mut self.terminal_font, multi_exec: &mut self.multi_exec, theme: &mut self.terminal_theme, default_font_size: 14., - active_tab_id: &mut self.active_id, + active_tab_id: None, }; let terminal = TerminalView::new(ui, term_ctx, term_opt) .set_focus(true) diff --git a/crates/egui-term/examples/sftp.rs b/crates/egui-term/examples/sftp.rs new file mode 100644 index 0000000..ce96f3c --- /dev/null +++ b/crates/egui-term/examples/sftp.rs @@ -0,0 +1,59 @@ +use std::io::Read; + +use egui_term::TermError; +use tracing::{error, trace}; +use wezterm_ssh::{Config, Session, SessionEvent}; + +fn main() -> Result<(), TermError> { + let mut config = Config::new(); + config.add_default_config_files(); + let config = config.for_host("127.0.0.1"); + smol::block_on(async move { + let (session, events) = Session::connect(config)?; + + while let Ok(event) = events.recv().await { + match event { + SessionEvent::Banner(banner) => { + if let Some(banner) = banner { + trace!("{}", banner); + } + } + SessionEvent::HostVerify(verify) => { + verify.answer(true).await?; + } + SessionEvent::Authenticate(auth) => { + // login with ssh config, so no answers needed + auth.answer(vec![]).await?; + } + SessionEvent::HostVerificationFailed(failed) => { + error!("host verification failed: {failed}"); + return Err(TermError::HostVerification(failed)); + } + SessionEvent::Error(err) => { + error!("ssh login error: {err}"); + return Err(TermError::Box(err.into())); + } + SessionEvent::Authenticated => break, + } + } + + let mut exec_ret = session.exec("pwd", None).await.unwrap(); + + let mut s = String::new(); + exec_ret.stdout.read_to_string(&mut s).unwrap(); + + let sftp = session.sftp(); + match sftp.read_dir(s.trim()).await { + Ok(entries) => { + for (path, _meta) in entries { + println!("path: {}", path.as_path()) + } + } + Err(err) => println!("{err}"), + } + + Ok(()) + })?; + + Ok(()) +} diff --git a/crates/egui-term/examples/tabs.rs b/crates/egui-term/examples/tabs.rs index eb30b09..3bc0306 100644 --- a/crates/egui-term/examples/tabs.rs +++ b/crates/egui-term/examples/tabs.rs @@ -1,6 +1,5 @@ use copypasta::ClipboardContext; use eframe::glow; -use egui::Id; use egui_term::{ PtyEvent, Terminal, TerminalContext, TerminalFont, TerminalOptions, TerminalTheme, TerminalView, }; @@ -14,7 +13,6 @@ pub struct App { command_receiver: Receiver<(u64, PtyEvent)>, tab_manager: TabManager, multi_exec: bool, - active_tab: Option, clipboard: ClipboardContext, } @@ -26,7 +24,6 @@ impl App { command_receiver, tab_manager: TabManager::new(), multi_exec: false, - active_tab: None, clipboard: ClipboardContext::new().unwrap(), } } @@ -73,13 +70,14 @@ impl eframe::App for App { egui::CentralPanel::default().show(ctx, |ui| { if let Some(tab) = self.tab_manager.get_active() { - let term_ctx = TerminalContext::new(&mut tab.backend, &mut self.clipboard); + let term_ctx = + TerminalContext::new(&mut tab.backend, &mut self.clipboard, &mut false); let term_opt = TerminalOptions { font: &mut tab.font, multi_exec: &mut self.multi_exec, theme: &mut tab.theme, default_font_size: 14., - active_tab_id: &mut self.active_tab, + active_tab_id: None, }; let terminal = TerminalView::new(ui, term_ctx, term_opt) .set_focus(true) diff --git a/crates/egui-term/examples/themes.rs b/crates/egui-term/examples/themes.rs index 4082c75..56f3344 100644 --- a/crates/egui-term/examples/themes.rs +++ b/crates/egui-term/examples/themes.rs @@ -1,5 +1,5 @@ use copypasta::ClipboardContext; -use egui::{Id, Vec2}; +use egui::Vec2; use egui_term::{ ColorPalette, PtyEvent, Terminal, TerminalContext, TerminalFont, TerminalOptions, TerminalTheme, TerminalView, @@ -11,7 +11,6 @@ pub struct App { terminal_font: TerminalFont, terminal_theme: TerminalTheme, multi_exec: bool, - active_id: Option, clipboard: ClipboardContext, pty_proxy_receiver: Receiver<(u64, PtyEvent)>, } @@ -25,7 +24,6 @@ impl App { Self { terminal_backend, multi_exec: false, - active_id: None, clipboard: ClipboardContext::new().unwrap(), terminal_font: TerminalFont::default(), terminal_theme: TerminalTheme::default(), @@ -98,13 +96,14 @@ impl eframe::App for App { }); egui::CentralPanel::default().show(ctx, |ui| { - let term_ctx = TerminalContext::new(&mut self.terminal_backend, &mut self.clipboard); + let term_ctx = + TerminalContext::new(&mut self.terminal_backend, &mut self.clipboard, &mut false); let term_opt = TerminalOptions { font: &mut self.terminal_font, multi_exec: &mut self.multi_exec, theme: &mut self.terminal_theme, default_font_size: 14., - active_tab_id: &mut self.active_id, + active_tab_id: None, }; let terminal = TerminalView::new(ui, term_ctx, term_opt) .set_focus(true) diff --git a/crates/egui-term/src/alacritty/mod.rs b/crates/egui-term/src/alacritty/mod.rs index 44f5c70..90d0f2f 100644 --- a/crates/egui-term/src/alacritty/mod.rs +++ b/crates/egui-term/src/alacritty/mod.rs @@ -1,5 +1,6 @@ +use crate::display::SftpExplorer; use crate::errors::TermError; -use crate::ssh::{Pty, SshOptions}; +use crate::ssh::{SshOptions, SshSession}; use crate::types::Size; use alacritty_terminal::event::{Event, EventListener, Notify, OnResize, WindowSize}; use alacritty_terminal::event_loop::{EventLoop, Msg, Notifier}; @@ -22,6 +23,7 @@ use std::path::PathBuf; use std::sync::mpsc::Sender; use std::sync::{mpsc, Arc}; use tracing::debug; +use wezterm_ssh::Session; pub type PtyEvent = Event; @@ -138,6 +140,8 @@ pub enum TermType { pub struct Terminal { pub id: u64, + pub session: Option, + pub sftp_explorer: Option, pub url_regex: RegexSearch, pub term: Arc>>, pub size: TerminalSize, @@ -152,37 +156,6 @@ impl PartialEq for Terminal { } impl Terminal { - pub fn new( - id: u64, - app_context: egui::Context, - term_type: TermType, - term_size: TerminalSize, - pty_event_proxy_sender: Sender<(u64, PtyEvent)>, - ) -> Result { - match term_type { - TermType::Regular { working_directory } => { - let opts = Options { - working_directory, - ..Default::default() - }; - Self::new_with_pty( - id, - app_context, - term_size, - tty::new(&opts, term_size.into(), id)?, - pty_event_proxy_sender, - ) - } - TermType::Ssh { options } => Self::new_with_pty( - id, - app_context, - term_size, - Pty::new(options)?, - pty_event_proxy_sender, - ), - } - } - pub fn new_regular( id: u64, app_context: egui::Context, @@ -214,6 +187,42 @@ impl Terminal { ) } + fn new( + id: u64, + app_context: egui::Context, + term_type: TermType, + term_size: TerminalSize, + pty_event_proxy_sender: Sender<(u64, PtyEvent)>, + ) -> Result { + match term_type { + TermType::Regular { working_directory } => { + let opts = Options { + working_directory, + ..Default::default() + }; + Self::new_with_pty( + id, + app_context, + term_size, + tty::new(&opts, term_size.into(), id)?, + pty_event_proxy_sender, + ) + } + TermType::Ssh { options } => { + let session = SshSession::new(options)?; + let mut term = Self::new_with_pty( + id, + app_context, + term_size, + session.pty()?, + pty_event_proxy_sender, + )?; + term.session = Some(session); + Ok(term) + } + } + } + fn new_with_pty( id: u64, app_context: egui::Context, @@ -254,6 +263,8 @@ impl Terminal { debug!("create a terminal backend: {id}"); Ok(Self { id, + session: None, + sftp_explorer: None, url_regex, term, size: term_size, @@ -272,27 +283,43 @@ impl Drop for Terminal { pub struct TerminalContext<'a> { pub id: u64, pub terminal: MutexGuard<'a, Term>, + pub session: Option<&'a SshSession>, + pub sftp_explorer: &'a mut Option, pub url_regex: &'a mut RegexSearch, pub size: &'a mut TerminalSize, pub notifier: &'a mut Notifier, pub hovered_hyperlink: &'a mut Option, pub clipboard: &'a mut ClipboardContext, + pub show_sftp_window: &'a mut bool, } impl<'a> TerminalContext<'a> { - pub fn new(terminal: &'a mut Terminal, clipboard: &'a mut ClipboardContext) -> Self { + pub fn new( + terminal: &'a mut Terminal, + clipboard: &'a mut ClipboardContext, + show_sftp_window: &'a mut bool, + ) -> Self { let term = terminal.term.lock(); Self { id: terminal.id, terminal: term, + session: terminal.session.as_ref(), + sftp_explorer: &mut terminal.sftp_explorer, url_regex: &mut terminal.url_regex, size: &mut terminal.size, notifier: &mut terminal.notifier, hovered_hyperlink: &mut terminal.hovered_hyperlink, + show_sftp_window, clipboard, } } + pub fn open_sftp(&mut self, session: &Session) -> Result<(), TermError> { + let sftp = session.sftp(); + *self.sftp_explorer = Some(SftpExplorer::new(sftp)?); + Ok(()) + } + pub fn term_mode(&self) -> TermMode { *self.terminal.mode() } diff --git a/crates/egui-term/src/display/mod.rs b/crates/egui-term/src/display/mod.rs index 56ecef8..b0b6144 100644 --- a/crates/egui-term/src/display/mod.rs +++ b/crates/egui-term/src/display/mod.rs @@ -1,5 +1,7 @@ #![allow(dead_code)] mod color; +mod sftp; +pub use sftp::SftpExplorer; use crate::display::color::HOVERED_HYPERLINK_COLOR; use crate::view::TerminalViewState; @@ -15,6 +17,8 @@ use egui::{ Painter, Pos2, Rect, Response, Vec2, WidgetText, }; use egui::{Shape, Stroke}; +use tracing::error; +use wezterm_ssh::Session; impl TerminalView<'_> { pub fn show(self, state: &mut TerminalViewState, layout: &Response, painter: &Painter) { @@ -162,6 +166,13 @@ impl TerminalView<'_> { ui.separator(); // select all btn self.select_all_btn(ui, width); + + if let Some(session) = self.term_ctx.session { + ui.separator(); + + // sftp + self.sftp_btn(session, ui, width); + } }); }); } @@ -208,6 +219,20 @@ impl TerminalView<'_> { ui.close_menu(); } } + + fn sftp_btn(&mut self, session: &Session, ui: &mut egui::Ui, btn_width: f32) { + let sftp_btn = context_btn("Sftp", btn_width, None); + if ui.add(sftp_btn).clicked() { + match self.term_ctx.open_sftp(session) { + Ok(_) => { + *self.term_ctx.show_sftp_window = true; + self.options.surrender_focus(); + } + Err(err) => error!("opening sftp error: {err}"), + } + ui.close_menu(); + } + } } fn context_btn<'a>( diff --git a/crates/egui-term/src/display/sftp.rs b/crates/egui-term/src/display/sftp.rs new file mode 100644 index 0000000..279c339 --- /dev/null +++ b/crates/egui-term/src/display/sftp.rs @@ -0,0 +1,273 @@ +use crate::{TermError, TerminalView}; +use camino::Utf8PathBuf; +use egui::{Align2, CentralPanel, Context, Layout, TopBottomPanel, Window}; +use egui_extras::TableBuilder; +use file_format::FileFormat; +use homedir::my_home; +use time::Duration; +use wezterm_ssh::{FilePermissions, FileType, Metadata, Sftp}; + +pub struct Entry { + pub path: Utf8PathBuf, + meta: Metadata, +} + +pub struct SftpExplorer { + pub sftp: Sftp, + pub current_path: String, + pub entries: Vec, + previous_path: Vec, + forward_path: Vec, +} + +impl SftpExplorer { + pub fn new(sftp: Sftp) -> Result { + let current_path = match my_home()? { + Some(home) => home, + None => { + return Err(TermError::Any(anyhow::anyhow!( + "cannot find home directory" + ))) + } + }; + let current_path = match current_path.to_str() { + Some(path) => path.to_owned(), + None => { + return Err(TermError::Any(anyhow::anyhow!( + "cannot convert path to unicode string" + ))) + } + }; + let entries = smol::block_on(async { sftp.read_dir(¤t_path).await })?; + let entries = entries + .into_iter() + .map(|(path, meta)| Entry { path, meta }) + .collect(); + Ok(Self { + sftp, + current_path, + entries, + previous_path: vec![], + forward_path: vec![], + }) + } +} + +impl TerminalView<'_> { + pub fn show_sftp_window(&mut self, ctx: &Context) { + if let Some(explorer) = self.term_ctx.sftp_explorer { + Window::new("Sftp Window") + .open(self.term_ctx.show_sftp_window) + .max_width(1000.) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .show(ctx, |ui| { + TopBottomPanel::bottom("sftp_bottom_panel").show_inside(ui, |ui| { + ui.with_layout(Layout::right_to_left(egui::Align::TOP), |_ui| {}); + }); + + CentralPanel::default().show_inside(ui, |ui| { + egui::ScrollArea::both() + .auto_shrink([false; 2]) + .show(ui, |ui| { + let text_size = + egui::TextStyle::Body.resolve(ui.style()).size + 10.0; + + TableBuilder::new(ui) + .column(egui_extras::Column::initial(300.0)) + .column(egui_extras::Column::initial(100.0)) + .column(egui_extras::Column::initial(100.0)) + .column(egui_extras::Column::initial(100.0)) + .column(egui_extras::Column::initial(100.0)) + .column(egui_extras::Column::remainder()) + .resizable(true) + .striped(true) + .header(20.0, |mut header| { + header.col(|ui| { + ui.strong("Name"); + }); + header.col(|ui| { + ui.strong("Type"); + }); + header.col(|ui| { + ui.strong("Size"); + }); + header.col(|ui| { + ui.strong("Last accessed"); + }); + header.col(|ui| { + ui.strong("Last modified"); + }); + header.col(|ui| { + ui.strong("Permissions"); + }); + }) + .body(|body| { + body.rows(text_size, explorer.entries.len(), |mut row| { + let row_index = row.index(); + + if let Some(entry) = explorer.entries.get(row_index) { + let file_name = + entry.path.file_name().unwrap_or_default(); + let entry_type = match entry.meta.ty { + FileType::File => { + let mut file_type = "File".to_string(); + if let Ok(t) = + FileFormat::from_file(&entry.path) + { + if let Some(short_name) = t.short_name() + { + file_type = + format!("{} File", short_name); + } + } + file_type + } + FileType::Dir => "Folder".to_string(), + FileType::Symlink => "Symlink".to_string(), + FileType::Other => "Other".to_string(), + }; + + row.col(|ui| { + let _entry_label = { + ui.push_id(file_name, |ui| { + ui.with_layout( + Layout::left_to_right( + egui::Align::Min, + ), + |ui| { + if ui + .selectable_label( + false, file_name, + ) + .clicked() + { + } + }, + ) + }) + .inner + }; + }); + row.col(|ui| { + ui.with_layout( + Layout::left_to_right(egui::Align::Min), + |ui| { + ui.label(entry_type); + }, + ); + }); + + row.col(|ui| { + if let Some(size) = entry.meta.size { + ui.with_layout( + Layout::left_to_right(egui::Align::Min), + |ui| { + ui.label(bytesize::to_string( + size, false, + )); + }, + ); + } + }); + + row.col(|ui| { + if let Some(accessed) = entry.meta.accessed { + ui.with_layout( + Layout::left_to_right(egui::Align::Min), + |ui| { + ui.label(duration_to_string( + Duration::milliseconds( + accessed as i64, + ), + )); + }, + ); + } + }); + + row.col(|ui| { + if let Some(modified) = entry.meta.modified { + ui.with_layout( + Layout::left_to_right(egui::Align::Min), + |ui| { + ui.label(duration_to_string( + Duration::milliseconds( + modified as i64, + ), + )); + }, + ); + } + }); + + row.col(|ui| { + if let Some(permissions) = + entry.meta.permissions + { + ui.with_layout( + Layout::left_to_right(egui::Align::Min), + |ui| { + ui.label(to_rwx_string( + permissions, + )); + }, + ); + } + }); + } + }); + }); + }); + }); + }); + } + + if !*self.term_ctx.show_sftp_window { + *self.term_ctx.sftp_explorer = None; + } + } +} + +pub fn duration_to_string(duration: Duration) -> String { + if duration.whole_weeks() >= 1 { + format!("{} weeks ago", duration.whole_weeks()) + } else if duration.whole_days() >= 1 { + format!("{} days ago", duration.whole_days()) + } else if duration.whole_hours() >= 1 { + format!("{} hours ago", duration.whole_days()) + } else if duration.whole_minutes() >= 1 { + format!("{} minutes ago", duration.whole_minutes()) + } else { + format!("{} seconds ago", duration.whole_seconds()) + } +} + +pub fn to_rwx_string(permission: FilePermissions) -> String { + fn perms_to_str(read: bool, write: bool, exec: bool) -> String { + [ + if read { 'r' } else { '-' }, + if write { 'w' } else { '-' }, + if exec { 'x' } else { '-' }, + ] + .iter() + .collect() + } + format!( + "{}{}{}", + perms_to_str( + permission.owner_read, + permission.owner_write, + permission.owner_exec + ), + perms_to_str( + permission.group_read, + permission.group_write, + permission.group_exec + ), + perms_to_str( + permission.other_read, + permission.other_write, + permission.other_exec + ), + ) +} diff --git a/crates/egui-term/src/errors.rs b/crates/egui-term/src/errors.rs index ca41bf6..9ca1076 100644 --- a/crates/egui-term/src/errors.rs +++ b/crates/egui-term/src/errors.rs @@ -1,4 +1,5 @@ -use wezterm_ssh::HostVerificationFailed; +use homedir::GetHomeError; +use wezterm_ssh::{HostVerificationFailed, SftpChannelError}; #[derive(Debug, thiserror::Error)] pub enum TermError { @@ -10,4 +11,8 @@ pub enum TermError { HostVerification(HostVerificationFailed), #[error("{0}")] Io(#[from] std::io::Error), + #[error("{0}")] + GetHome(#[from] GetHomeError), + #[error("{0}")] + SftpChannel(#[from] SftpChannelError), } diff --git a/crates/egui-term/src/input/mod.rs b/crates/egui-term/src/input/mod.rs index e4f5bd3..98151e3 100644 --- a/crates/egui-term/src/input/mod.rs +++ b/crates/egui-term/src/input/mod.rs @@ -50,7 +50,14 @@ impl TerminalView<'_> { ))), Some(BindingAction::Copy) => { let content = self.term_ctx.selection_content(); - Some(InputAction::WriteToClipboard(content)) + + // if multi_exec is enabled, when copy selected terminal content, only one terminal content copied, + // and others copy empty content, which make copy_text in memory always be blank + if *self.options.multi_exec && content.is_empty() { + None + } else { + Some(InputAction::WriteToClipboard(content)) + } } Some(BindingAction::ResetFontSize) => { self.reset_font_size(self.options.default_font_size); diff --git a/crates/egui-term/src/lib.rs b/crates/egui-term/src/lib.rs index 26e1956..b9ce537 100644 --- a/crates/egui-term/src/lib.rs +++ b/crates/egui-term/src/lib.rs @@ -12,6 +12,7 @@ mod view; pub use alacritty::{PtyEvent, TermType, Terminal, TerminalContext}; pub use alacritty_terminal::term::TermMode; pub use bindings::{Binding, BindingAction, InputKind, KeyboardBinding}; +pub use errors::TermError; pub use font::{FontSettings, TerminalFont}; pub use ssh::{Authentication, SshOptions}; pub use theme::{ColorPalette, TerminalTheme}; diff --git a/crates/egui-term/src/ssh/mod.rs b/crates/egui-term/src/ssh/mod.rs index 2297f63..4234582 100644 --- a/crates/egui-term/src/ssh/mod.rs +++ b/crates/egui-term/src/ssh/mod.rs @@ -5,6 +5,7 @@ use alacritty_terminal::tty::{ChildEvent, EventedPty, EventedReadWrite}; use anyhow::Context; use polling::{Event, PollMode, Poller}; use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; use std::sync::Arc; use tracing::{error, trace}; use wezterm_ssh::{ @@ -194,7 +195,9 @@ impl OnResize for Pty { } } -impl Pty { +pub struct SshSession(Session); + +impl SshSession { pub fn new(opts: SshOptions) -> Result { let mut config = Config::new(); @@ -249,47 +252,66 @@ impl Pty { SessionEvent::Authenticated => break, } } + Ok(SshSession(session)) + }) + } - // FIXME: set in settings - let mut env = HashMap::new(); - env.insert("LANG".to_string(), "en_US.UTF-8".to_string()); - env.insert("LC_COLLATE".to_string(), "C".to_string()); - - let (pty, child) = session - .request_pty("xterm-256color", PtySize::default(), None, Some(env)) - .await?; - - #[cfg(unix)] - { - // Prepare signal handling before spawning child. - let (signals, sig_id) = { - let (sender, recv) = UnixStream::pair()?; - - // Register the recv end of the pipe for SIGCHLD. - let sig_id = pipe::register(consts::SIGCHLD, sender)?; - recv.set_nonblocking(true)?; - (recv, sig_id) - }; - - Ok(Pty { - pty, - child, - signals, - sig_id, - }) - } + pub fn pty(&self) -> Result { + // FIXME: set in settings + let mut env = HashMap::new(); + env.insert("LANG".to_string(), "en_US.UTF-8".to_string()); + env.insert("LC_COLLATE".to_string(), "C".to_string()); - #[cfg(windows)] - { - let listener = TcpListener::bind("127.0.0.1:0")?; - let signals = TcpStream::connect(listener.local_addr()?)?; - Ok(Pty { - pty, - child, - signals, - }) - } - }) + let (pty, child) = smol::block_on(self.request_pty( + "xterm-256color", + PtySize::default(), + None, + Some(env), + ))?; + + #[cfg(unix)] + { + // Prepare signal handling before spawning child. + let (signals, sig_id) = { + let (sender, recv) = UnixStream::pair()?; + + // Register the recv end of the pipe for SIGCHLD. + let sig_id = pipe::register(consts::SIGCHLD, sender)?; + recv.set_nonblocking(true)?; + (recv, sig_id) + }; + + Ok(Pty { + pty, + child, + signals, + sig_id, + }) + } + + #[cfg(windows)] + { + let listener = TcpListener::bind("127.0.0.1:0")?; + let signals = TcpStream::connect(listener.local_addr()?)?; + Ok(Pty { + pty, + child, + signals, + }) + } + } +} + +impl Deref for SshSession { + type Target = Session; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SshSession { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } diff --git a/crates/egui-term/src/view.rs b/crates/egui-term/src/view.rs index e707e57..a8b1bb0 100644 --- a/crates/egui-term/src/view.rs +++ b/crates/egui-term/src/view.rs @@ -52,7 +52,15 @@ pub struct TerminalOptions<'a> { pub font: &'a mut TerminalFont, pub multi_exec: &'a mut bool, pub theme: &'a mut TerminalTheme, - pub active_tab_id: &'a mut Option, + pub active_tab_id: Option<&'a mut Id>, +} + +impl TerminalOptions<'_> { + pub fn surrender_focus(&mut self) { + if let Some(active_tab_id) = self.active_tab_id.as_mut() { + **active_tab_id = Id::NULL; + } + } } impl Widget for TerminalView<'_> { @@ -63,13 +71,20 @@ impl Widget for TerminalView<'_> { let mut state = TerminalViewState::load(ui.ctx(), widget_id); if layout.contains_pointer() { - *self.options.active_tab_id = Some(self.widget_id); + if let Some(tab_id) = self.options.active_tab_id.as_mut() { + **tab_id = self.widget_id; + } layout.ctx.set_cursor_icon(CursorIcon::Text); } else { layout.ctx.set_cursor_icon(CursorIcon::Default); } - if self.options.active_tab_id.is_none() { + if self + .options + .active_tab_id + .as_ref() + .is_some_and(|id| **id == Id::NULL) + { self.has_focus = false; } @@ -82,6 +97,8 @@ impl Widget for TerminalView<'_> { ui.close_menu(); } + self.show_sftp_window(ui.ctx()); + self.focus(&layout) .resize(&layout) .process_input(&mut state, &layout) @@ -165,8 +182,11 @@ impl<'a> TerminalView<'a> { if !layout.has_focus() { return self; } - if self.options.active_tab_id != &Some(self.widget_id) && !*self.options.multi_exec { - return self; + + if let Some(tab_id) = self.options.active_tab_id.as_ref() { + if **tab_id != self.widget_id && !*self.options.multi_exec { + return self; + } } let modifiers = layout.ctx.input(|i| i.modifiers); @@ -208,9 +228,6 @@ impl<'a> TerminalView<'a> { modifiers, pos, } => { - if out_of_terminal(pos, layout) { - continue; - } if let Some(action) = self.button_click(state, layout, button, pos, &modifiers, pressed) { @@ -249,10 +266,3 @@ impl<'a> TerminalView<'a> { self } } - -fn out_of_terminal(pos: Pos2, layout: &Response) -> bool { - !(pos.x > layout.rect.min.x - && pos.x < layout.rect.max.x - && pos.y > layout.rect.min.y - && pos.y < layout.rect.max.y) -} diff --git a/nxshell/Cargo.toml b/nxshell/Cargo.toml index 24f29da..000c953 100644 --- a/nxshell/Cargo.toml +++ b/nxshell/Cargo.toml @@ -37,7 +37,6 @@ rusqlite = { workspace = true, features = ["bundled"] } thiserror.workspace = true tracing.workspace = true tracing-subscriber = { workspace = true, features = ["env-filter"] } -uuid = { workspace = true, features = ["v4"] } [target.'cfg(windows)'.dependencies] windows = { workspace = true, features = ["Win32_System_Threading"] } diff --git a/nxshell/src/app.rs b/nxshell/src/app.rs index 1f5e5f5..d450d77 100644 --- a/nxshell/src/app.rs +++ b/nxshell/src/app.rs @@ -27,8 +27,8 @@ pub struct NxShellOptions { /// /// 1. When the mouse cursor leaves the terminal, it still influences the state of the current /// terminal's selection. - /// 2. When it is None, all tabs lose focus, and you can iteract with the other UI components. - pub active_tab_id: Option, + /// 2. When it is Id::NULL, all tabs lose focus, and you can iteract with the other UI components. + pub active_tab_id: Id, pub term_font: TerminalFont, pub term_font_size: f32, pub session_filter: String, @@ -36,7 +36,7 @@ pub struct NxShellOptions { impl NxShellOptions { pub fn surrender_focus(&mut self) { - self.active_tab_id = None; + self.active_tab_id = Id::NULL; } } @@ -49,7 +49,7 @@ impl Default for NxShellOptions { Self { show_add_session_modal: Rc::new(RefCell::new(false)), show_dock_panel: false, - active_tab_id: None, + active_tab_id: Id::NULL, multi_exec: false, term_font: TerminalFont::new(font_setting), term_font_size, diff --git a/nxshell/src/ui/tab_view/mod.rs b/nxshell/src/ui/tab_view/mod.rs index 7682e52..84c2959 100644 --- a/nxshell/src/ui/tab_view/mod.rs +++ b/nxshell/src/ui/tab_view/mod.rs @@ -55,6 +55,7 @@ impl Tab { terminal, terminal_theme: TerminalTheme::default(), term_type: typ, + show_sftp_window: false, }), }) } @@ -108,13 +109,17 @@ impl egui_dock::TabViewer for TabViewer<'_> { fn ui(&mut self, ui: &mut Ui, tab: &mut Self::Tab) { match &mut tab.inner { TabInner::Term(tab) => { - let term_ctx = TerminalContext::new(&mut tab.terminal, self.clipboard); + let term_ctx = TerminalContext::new( + &mut tab.terminal, + self.clipboard, + &mut tab.show_sftp_window, + ); let term_opt = TerminalOptions { font: &mut self.options.term_font, multi_exec: &mut self.options.multi_exec, theme: &mut tab.terminal_theme, default_font_size: self.options.term_font_size, - active_tab_id: &mut self.options.active_tab_id, + active_tab_id: Some(&mut self.options.active_tab_id), }; let terminal = TerminalView::new(ui, term_ctx, term_opt) diff --git a/nxshell/src/ui/tab_view/terminal.rs b/nxshell/src/ui/tab_view/terminal.rs index b109601..d054100 100644 --- a/nxshell/src/ui/tab_view/terminal.rs +++ b/nxshell/src/ui/tab_view/terminal.rs @@ -5,4 +5,5 @@ pub struct TerminalTab { pub terminal_theme: TerminalTheme, pub terminal: Terminal, pub term_type: TermType, + pub show_sftp_window: bool, }