From 3e1de24dba35757e63f0da5f0410faceea14aba2 Mon Sep 17 00:00:00 2001 From: iamazy Date: Wed, 27 Aug 2025 01:31:38 +0800 Subject: [PATCH 1/4] chore: use builtin context menu --- crates/egui-term/src/display/mod.rs | 39 ++++++++++++----------------- crates/egui-term/src/input/mod.rs | 7 ------ crates/egui-term/src/view.rs | 13 +--------- 3 files changed, 17 insertions(+), 42 deletions(-) diff --git a/crates/egui-term/src/display/mod.rs b/crates/egui-term/src/display/mod.rs index 188bf9b..232b200 100644 --- a/crates/egui-term/src/display/mod.rs +++ b/crates/egui-term/src/display/mod.rs @@ -11,8 +11,8 @@ use alacritty_terminal::vte::ansi::{Color, NamedColor}; use copypasta::ClipboardProvider; use egui::epaint::RectShape; use egui::{ - Align2, Area, Button, Color32, CornerRadius, CursorIcon, Id, Key, KeyboardShortcut, Modifiers, - Painter, Pos2, Rect, Response, Vec2, WidgetText, + Align2, Button, CornerRadius, CursorIcon, Key, KeyboardShortcut, Modifiers, Painter, Pos2, + Rect, Response, Vec2, WidgetText, }; use egui::{Shape, Stroke}; @@ -148,24 +148,19 @@ impl TerminalView<'_> { } impl TerminalView<'_> { - pub fn context_menu(&mut self, pos: Pos2, layout: &Response, ui: &mut egui::Ui) { - Area::new(Id::new(format!("context_menu_{:?}", self.id()))) - .fixed_pos(pos) - .order(egui::Order::Foreground) - .show(ui.ctx(), |ui| { - egui::Frame::popup(ui.style()).show(ui, |ui| { - let width = 200.; - ui.set_width(width); - // copy btn - self.copy_btn(ui, layout, width); - // paste btn - self.paste_btn(ui, width); - - ui.separator(); - // select all btn - self.select_all_btn(ui, width); - }); - }); + pub fn context_menu(&mut self, layout: &Response) { + layout.context_menu(|ui| { + let width = 200.; + ui.set_width(width); + // copy btn + self.copy_btn(ui, layout, width); + // paste btn + self.paste_btn(ui, width); + + ui.separator(); + // select all btn + self.select_all_btn(ui, width); + }); } fn copy_btn(&mut self, ui: &mut egui::Ui, layout: &Response, btn_width: f32) { @@ -217,9 +212,7 @@ fn context_btn<'a>( width: f32, shortcut: Option, ) -> Button<'a> { - let mut btn = Button::new(text) - .fill(Color32::TRANSPARENT) - .min_size((width, 0.).into()); + let mut btn = Button::new(text).min_size((width, 0.).into()); if let Some(shortcut) = shortcut { btn = btn.shortcut_text(shortcut); } diff --git a/crates/egui-term/src/input/mod.rs b/crates/egui-term/src/input/mod.rs index ae9a85f..061a993 100644 --- a/crates/egui-term/src/input/mod.rs +++ b/crates/egui-term/src/input/mod.rs @@ -133,10 +133,6 @@ impl TerminalView<'_> { PointerButton::Primary => { self.left_button_click(state, layout, position, modifiers, pressed) } - PointerButton::Secondary => { - state.context_menu_position = Some(position); - None - } _ => None, } } @@ -149,9 +145,6 @@ impl TerminalView<'_> { modifiers: &Modifiers, pressed: bool, ) -> Option { - if state.context_menu_position.is_some() { - return None; - } let terminal_mode = self.term_ctx.terminal.mode(); if terminal_mode.intersects(TermMode::MOUSE_MODE) { Some(InputAction::BackendCall(BackendCommand::MouseReport( diff --git a/crates/egui-term/src/view.rs b/crates/egui-term/src/view.rs index 8882a81..48c8198 100644 --- a/crates/egui-term/src/view.rs +++ b/crates/egui-term/src/view.rs @@ -23,7 +23,6 @@ pub struct TerminalViewState { // for terminal pub mouse_point: Point, pub mouse_position: Option, - pub context_menu_position: Option, pub cursor_position: Option, pub scrollbar_state: ScrollbarState, } @@ -79,17 +78,7 @@ impl Widget for TerminalView<'_> { self.has_focus = false; } - // context menu - if let Some(pos) = state.context_menu_position { - if is_in_terminal(pos, layout.rect) { - self.context_menu(pos, &layout, ui); - } - } - - if ui.input(|input_state| input_state.pointer.primary_clicked()) { - state.context_menu_position = None; - ui.close(); - } + self.context_menu(&layout); let background = self.theme().get_color(Color::Named(NamedColor::Background)); From df6d8dd2e8eb397c0fd509cb027a827492c0106b Mon Sep 17 00:00:00 2001 From: iamazy Date: Thu, 28 Aug 2025 06:26:31 +0800 Subject: [PATCH 2/4] feat: use builtin context menu --- crates/egui-term/src/input/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/egui-term/src/input/mod.rs b/crates/egui-term/src/input/mod.rs index 061a993..9151c30 100644 --- a/crates/egui-term/src/input/mod.rs +++ b/crates/egui-term/src/input/mod.rs @@ -145,6 +145,9 @@ impl TerminalView<'_> { modifiers: &Modifiers, pressed: bool, ) -> Option { + if layout.context_menu_opened() { + return None; + } let terminal_mode = self.term_ctx.terminal.mode(); if terminal_mode.intersects(TermMode::MOUSE_MODE) { Some(InputAction::BackendCall(BackendCommand::MouseReport( From a3bbf6d6bd1336285be9ed8a55e836a098b8d322 Mon Sep 17 00:00:00 2001 From: iamazy Date: Thu, 28 Aug 2025 06:37:34 +0800 Subject: [PATCH 3/4] chore: reuse toast --- nxshell/src/app.rs | 20 ++++++++++---------- nxshell/src/ui/form/session.rs | 5 ++--- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/nxshell/src/app.rs b/nxshell/src/app.rs index 32424fe..e5742cd 100644 --- a/nxshell/src/app.rs +++ b/nxshell/src/app.rs @@ -66,6 +66,7 @@ pub struct NxShell { pub clipboard: ClipboardContext, pub db: DbConn, pub opts: NxShellOptions, + pub toasts: Toasts, } impl NxShell { @@ -89,6 +90,9 @@ impl NxShell { ..Default::default() }, state_manager, + toasts: Toasts::new() + .anchor(Align2::CENTER_CENTER, (10.0, 10.0)) + .direction(egui::Direction::TopDown), }) } @@ -112,10 +116,6 @@ impl eframe::App for NxShell { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.recv_event(); - let mut toasts = Toasts::new() - .anchor(Align2::CENTER_CENTER, (10.0, 10.0)) - .direction(egui::Direction::TopDown); - egui::TopBottomPanel::top("main_top_panel").show(ctx, |ui| { self.menubar(ui); }); @@ -131,7 +131,7 @@ impl eframe::App for NxShell { self.search_sessions(ui); ui.separator(); - self.list_sessions(ctx, ui, &mut toasts); + self.list_sessions(ctx, ui); }); egui::TopBottomPanel::bottom("main_bottom_panel").show(ctx, |ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { @@ -141,14 +141,14 @@ impl eframe::App for NxShell { if *self.opts.show_add_session_modal.borrow() { self.opts.surrender_focus(); - self.show_add_session_window(ctx, &mut toasts); + self.show_add_session_window(ctx); } egui::CentralPanel::default().show(ctx, |_ui| { self.tab_view(ctx); }); - toasts.show(ctx); + self.toasts.show(ctx); } } @@ -165,7 +165,7 @@ impl NxShell { } } - fn list_sessions(&mut self, ctx: &egui::Context, ui: &mut egui::Ui, toasts: &mut Toasts) { + fn list_sessions(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) { if let Some(sessions) = self.state_manager.sessions.take() { for (group, sessions) in sessions.iter() { CollapsingHeader::new(group) @@ -183,12 +183,12 @@ impl NxShell { if let Err(err) = self.add_shell_tab_with_secret(ctx, session) { - toasts.add(error_toast(err.to_string())); + self.toasts.add(error_toast(err.to_string())); } } Ok(None) => {} Err(err) => { - toasts.add(error_toast(err.to_string())); + self.toasts.add(error_toast(err.to_string())); } } } else if response.secondary_clicked() { diff --git a/nxshell/src/ui/form/session.rs b/nxshell/src/ui/form/session.rs index b86d45f..9d90fb1 100644 --- a/nxshell/src/ui/form/session.rs +++ b/nxshell/src/ui/form/session.rs @@ -8,7 +8,6 @@ use egui::{ use egui_form::garde::GardeReport; use egui_form::{Form, FormField}; use egui_term::{Authentication, SshOptions, TermType}; -use egui_toast::Toasts; use garde::Validate; use orion::aead::{seal, SecretKey}; use std::fmt::Display; @@ -91,7 +90,7 @@ impl SessionState { } impl NxShell { - pub fn show_add_session_window(&mut self, ctx: &Context, toasts: &mut Toasts) { + pub fn show_add_session_window(&mut self, ctx: &Context) { let session_id = Id::new(SessionState::id()); let mut session_state = SessionState::load(ctx, session_id); @@ -114,7 +113,7 @@ impl NxShell { Ok(_) => should_close = true, Err(err) => { error!("failed to add session: {err}"); - toasts.add(error_toast(err.to_string())); + self.toasts.add(error_toast(err.to_string())); } } } From 2801a1d9dd5166210d1a2dfe9f2a52c711cc0afa Mon Sep 17 00:00:00 2001 From: iamazy Date: Thu, 28 Aug 2025 06:48:08 +0800 Subject: [PATCH 4/4] chore: clean code --- crates/egui-term/examples/custom_bindings.rs | 1 - crates/egui-term/examples/tabs.rs | 5 +- crates/egui-term/examples/themes.rs | 1 - crates/egui-term/src/display/mod.rs | 78 +------------------- crates/egui-term/src/lib.rs | 1 + crates/egui-term/src/ui/menu.rs | 75 +++++++++++++++++++ crates/egui-term/src/ui/mod.rs | 1 + crates/egui-term/src/view.rs | 2 +- nxshell/src/ui/tab_view/mod.rs | 5 +- 9 files changed, 83 insertions(+), 86 deletions(-) create mode 100644 crates/egui-term/src/ui/menu.rs create mode 100644 crates/egui-term/src/ui/mod.rs diff --git a/crates/egui-term/examples/custom_bindings.rs b/crates/egui-term/examples/custom_bindings.rs index 4bf5cd4..2c7c36e 100644 --- a/crates/egui-term/examples/custom_bindings.rs +++ b/crates/egui-term/examples/custom_bindings.rs @@ -92,7 +92,6 @@ impl eframe::App for App { active_tab_id: &mut self.active_id, }; let terminal = TerminalView::new(ui, term_ctx, term_opt) - .set_focus(true) .add_bindings(self.custom_terminal_bindings.clone()) .set_size(Vec2::new(ui.available_width(), ui.available_height())); diff --git a/crates/egui-term/examples/tabs.rs b/crates/egui-term/examples/tabs.rs index eb30b09..99dfd0d 100644 --- a/crates/egui-term/examples/tabs.rs +++ b/crates/egui-term/examples/tabs.rs @@ -81,9 +81,8 @@ impl eframe::App for App { default_font_size: 14., active_tab_id: &mut self.active_tab, }; - let terminal = TerminalView::new(ui, term_ctx, term_opt) - .set_focus(true) - .set_size(ui.available_size()); + let terminal = + TerminalView::new(ui, term_ctx, term_opt).set_size(ui.available_size()); ui.add(terminal); } diff --git a/crates/egui-term/examples/themes.rs b/crates/egui-term/examples/themes.rs index 4082c75..c382023 100644 --- a/crates/egui-term/examples/themes.rs +++ b/crates/egui-term/examples/themes.rs @@ -107,7 +107,6 @@ impl eframe::App for App { active_tab_id: &mut self.active_id, }; let terminal = TerminalView::new(ui, term_ctx, term_opt) - .set_focus(true) .set_size(Vec2::new(ui.available_width(), ui.available_height())); ui.add(terminal); diff --git a/crates/egui-term/src/display/mod.rs b/crates/egui-term/src/display/mod.rs index 232b200..22da4e2 100644 --- a/crates/egui-term/src/display/mod.rs +++ b/crates/egui-term/src/display/mod.rs @@ -8,12 +8,8 @@ use alacritty_terminal::grid::GridCell; use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::TermMode; use alacritty_terminal::vte::ansi::{Color, NamedColor}; -use copypasta::ClipboardProvider; use egui::epaint::RectShape; -use egui::{ - Align2, Button, CornerRadius, CursorIcon, Key, KeyboardShortcut, Modifiers, Painter, Pos2, - Rect, Response, Vec2, WidgetText, -}; +use egui::{Align2, CornerRadius, CursorIcon, Painter, Pos2, Rect, Response, Vec2}; use egui::{Shape, Stroke}; impl TerminalView<'_> { @@ -146,75 +142,3 @@ impl TerminalView<'_> { painter.extend(shapes); } } - -impl TerminalView<'_> { - pub fn context_menu(&mut self, layout: &Response) { - layout.context_menu(|ui| { - let width = 200.; - ui.set_width(width); - // copy btn - self.copy_btn(ui, layout, width); - // paste btn - self.paste_btn(ui, width); - - ui.separator(); - // select all btn - self.select_all_btn(ui, width); - }); - } - - fn copy_btn(&mut self, ui: &mut egui::Ui, layout: &Response, btn_width: f32) { - #[cfg(not(target_os = "macos"))] - let copy_shortcut = KeyboardShortcut::new(Modifiers::CTRL | Modifiers::SHIFT, Key::C); - #[cfg(target_os = "macos")] - let copy_shortcut = KeyboardShortcut::new(Modifiers::MAC_CMD, Key::C); - let copy_shortcut = ui.ctx().format_shortcut(©_shortcut); - let copy_btn = context_btn("Copy", btn_width, Some(copy_shortcut)); - if ui.add(copy_btn).clicked() { - let data = self.term_ctx.selection_content(); - layout.ctx.copy_text(data); - ui.close(); - } - } - - fn paste_btn(&mut self, ui: &mut egui::Ui, btn_width: f32) { - #[cfg(not(target_os = "macos"))] - let paste_shortcut = KeyboardShortcut::new(Modifiers::CTRL | Modifiers::SHIFT, Key::V); - #[cfg(target_os = "macos")] - let paste_shortcut = KeyboardShortcut::new(Modifiers::MAC_CMD, Key::V); - let paste_shortcut = ui.ctx().format_shortcut(&paste_shortcut); - let paste_btn = context_btn("Paste", btn_width, Some(paste_shortcut)); - if ui.add(paste_btn).clicked() { - if let Ok(data) = self.term_ctx.clipboard.get_contents() { - self.term_ctx.write_data(data.into_bytes()); - self.term_ctx.terminal.selection = None; - } - ui.close(); - } - } - - fn select_all_btn(&mut self, ui: &mut egui::Ui, btn_width: f32) { - #[cfg(not(target_os = "macos"))] - let select_all_shortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::A); - #[cfg(target_os = "macos")] - let select_all_shortcut = KeyboardShortcut::new(Modifiers::MAC_CMD, Key::A); - let select_all_shortcut = ui.ctx().format_shortcut(&select_all_shortcut); - let select_all_btn = context_btn("Select All", btn_width, Some(select_all_shortcut)); - if ui.add(select_all_btn).clicked() { - self.term_ctx.select_all(); - ui.close(); - } - } -} - -fn context_btn<'a>( - text: impl Into, - width: f32, - shortcut: Option, -) -> Button<'a> { - let mut btn = Button::new(text).min_size((width, 0.).into()); - if let Some(shortcut) = shortcut { - btn = btn.shortcut_text(shortcut); - } - btn -} diff --git a/crates/egui-term/src/lib.rs b/crates/egui-term/src/lib.rs index fcd299a..5349d6a 100644 --- a/crates/egui-term/src/lib.rs +++ b/crates/egui-term/src/lib.rs @@ -8,6 +8,7 @@ mod scroll_bar; mod ssh; mod theme; mod types; +mod ui; mod view; pub use alacritty::{PtyEvent, TermType, Terminal, TerminalContext}; diff --git a/crates/egui-term/src/ui/menu.rs b/crates/egui-term/src/ui/menu.rs new file mode 100644 index 0000000..68daf73 --- /dev/null +++ b/crates/egui-term/src/ui/menu.rs @@ -0,0 +1,75 @@ +use crate::TerminalView; +use copypasta::ClipboardProvider; +use egui::{Button, Key, KeyboardShortcut, Modifiers, Response, WidgetText}; + +impl TerminalView<'_> { + pub fn context_menu(&mut self, layout: &Response) { + layout.context_menu(|ui| { + let width = 200.; + ui.set_width(width); + // copy btn + self.copy_btn(ui, layout, width); + // paste btn + self.paste_btn(ui, width); + + ui.separator(); + // select all btn + self.select_all_btn(ui, width); + }); + } + + fn copy_btn(&mut self, ui: &mut egui::Ui, layout: &Response, btn_width: f32) { + #[cfg(not(target_os = "macos"))] + let copy_shortcut = KeyboardShortcut::new(Modifiers::CTRL | Modifiers::SHIFT, Key::C); + #[cfg(target_os = "macos")] + let copy_shortcut = KeyboardShortcut::new(Modifiers::MAC_CMD, Key::C); + let copy_shortcut = ui.ctx().format_shortcut(©_shortcut); + let copy_btn = context_btn("Copy", btn_width, Some(copy_shortcut)); + if ui.add(copy_btn).clicked() { + let data = self.term_ctx.selection_content(); + layout.ctx.copy_text(data); + ui.close(); + } + } + + fn paste_btn(&mut self, ui: &mut egui::Ui, btn_width: f32) { + #[cfg(not(target_os = "macos"))] + let paste_shortcut = KeyboardShortcut::new(Modifiers::CTRL | Modifiers::SHIFT, Key::V); + #[cfg(target_os = "macos")] + let paste_shortcut = KeyboardShortcut::new(Modifiers::MAC_CMD, Key::V); + let paste_shortcut = ui.ctx().format_shortcut(&paste_shortcut); + let paste_btn = context_btn("Paste", btn_width, Some(paste_shortcut)); + if ui.add(paste_btn).clicked() { + if let Ok(data) = self.term_ctx.clipboard.get_contents() { + self.term_ctx.write_data(data.into_bytes()); + self.term_ctx.terminal.selection = None; + } + ui.close(); + } + } + + fn select_all_btn(&mut self, ui: &mut egui::Ui, btn_width: f32) { + #[cfg(not(target_os = "macos"))] + let select_all_shortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::A); + #[cfg(target_os = "macos")] + let select_all_shortcut = KeyboardShortcut::new(Modifiers::MAC_CMD, Key::A); + let select_all_shortcut = ui.ctx().format_shortcut(&select_all_shortcut); + let select_all_btn = context_btn("Select All", btn_width, Some(select_all_shortcut)); + if ui.add(select_all_btn).clicked() { + self.term_ctx.select_all(); + ui.close(); + } + } +} + +fn context_btn<'a>( + text: impl Into, + width: f32, + shortcut: Option, +) -> Button<'a> { + let mut btn = Button::new(text).min_size((width, 0.).into()); + if let Some(shortcut) = shortcut { + btn = btn.shortcut_text(shortcut); + } + btn +} diff --git a/crates/egui-term/src/ui/mod.rs b/crates/egui-term/src/ui/mod.rs new file mode 100644 index 0000000..c57668d --- /dev/null +++ b/crates/egui-term/src/ui/mod.rs @@ -0,0 +1 @@ +mod menu; diff --git a/crates/egui-term/src/view.rs b/crates/egui-term/src/view.rs index 48c8198..f8925d8 100644 --- a/crates/egui-term/src/view.rs +++ b/crates/egui-term/src/view.rs @@ -139,7 +139,7 @@ impl<'a> TerminalView<'a> { Self { widget_id, - has_focus: false, + has_focus: true, size: ui.available_size(), term_ctx, options, diff --git a/nxshell/src/ui/tab_view/mod.rs b/nxshell/src/ui/tab_view/mod.rs index 502fadd..c4671cd 100644 --- a/nxshell/src/ui/tab_view/mod.rs +++ b/nxshell/src/ui/tab_view/mod.rs @@ -118,9 +118,8 @@ impl egui_dock::TabViewer for TabViewer<'_> { active_tab_id: &mut self.options.active_tab_id, }; - let terminal = TerminalView::new(ui, term_ctx, term_opt) - .set_focus(true) - .set_size(ui.available_size()); + let terminal = + TerminalView::new(ui, term_ctx, term_opt).set_size(ui.available_size()); ui.add(terminal); } TabInner::SessionList(_list) => {