From 2a702d29c29a9cf0561a0f526f34ef5d57c92392 Mon Sep 17 00:00:00 2001 From: gongyh Date: Thu, 5 Jun 2025 21:26:47 +0800 Subject: [PATCH 01/13] fix emtpy password --- nxshell/src/ui/form/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxshell/src/ui/form/session.rs b/nxshell/src/ui/form/session.rs index b86d45f..ee45d00 100644 --- a/nxshell/src/ui/form/session.rs +++ b/nxshell/src/ui/form/session.rs @@ -144,7 +144,7 @@ impl NxShell { fn submit_session(&mut self, ctx: &Context, session: &mut SessionState) -> Result<(), NxError> { let (auth, secret_key, secret_data) = match session.auth_type { AuthType::Password => { - if session.username.trim().is_empty() || session.auth_data.trim().is_empty() { + if session.username.trim().is_empty() || session.auth_data.is_empty() { return Err(NxError::Plain( "`username` and `password` cannot be empty in `Password` mode".to_string(), )); From d7179e9c394eddd187df5605231d3ce3483e759a Mon Sep 17 00:00:00 2001 From: gongyh Date: Mon, 9 Jun 2025 10:08:04 +0800 Subject: [PATCH 02/13] length of host url limit to 128 --- nxshell/src/ui/form/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxshell/src/ui/form/session.rs b/nxshell/src/ui/form/session.rs index ee45d00..d97a7b9 100644 --- a/nxshell/src/ui/form/session.rs +++ b/nxshell/src/ui/form/session.rs @@ -245,7 +245,7 @@ impl NxShell { match session.auth_type { AuthType::Password => { FormField::new(form, "host") - .ui(ui, host_edit.char_limit(15).desired_width(150.)); + .ui(ui, host_edit.char_limit(128).desired_width(250.)); } AuthType::Config => { FormField::new(form, "host").ui(ui, host_edit); From 1f2dbf86c9bd9ac946f613b4968fd3abb44fb486 Mon Sep 17 00:00:00 2001 From: gongyh Date: Mon, 9 Jun 2025 14:12:18 +0800 Subject: [PATCH 03/13] Close&Show SidePanel --- nxshell/src/app.rs | 47 ++++++++++++++++++++++---------- nxshell/src/ui/menubar.rs | 17 +++++++++++- nxshell/src/ui/mod.rs | 1 + nxshell/src/ui/side_panel/mod.rs | 21 ++++++++++++++ 4 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 nxshell/src/ui/side_panel/mod.rs diff --git a/nxshell/src/app.rs b/nxshell/src/app.rs index 1f5e5f5..35e8b12 100644 --- a/nxshell/src/app.rs +++ b/nxshell/src/app.rs @@ -1,6 +1,7 @@ use crate::db::DbConn; use crate::errors::{error_toast, NxError}; use crate::ui::form::{AuthType, NxStateManager}; +use crate::ui::side_panel::SidePanel; use crate::ui::tab_view::Tab; use copypasta::ClipboardContext; use eframe::{egui, NativeOptions}; @@ -32,6 +33,8 @@ pub struct NxShellOptions { pub term_font: TerminalFont, pub term_font_size: f32, pub session_filter: String, + + pub side_panel: SidePanel, } impl NxShellOptions { @@ -54,6 +57,7 @@ impl Default for NxShellOptions { term_font: TerminalFont::new(font_setting), term_font_size, session_filter: String::default(), + side_panel: SidePanel::new(true), } } } @@ -118,25 +122,38 @@ impl eframe::App for NxShell { egui::TopBottomPanel::top("main_top_panel").show(ctx, |ui| { self.menubar(ui); }); - egui::SidePanel::right("main_right_panel") - .resizable(true) - .width_range(200.0..=300.0) - .show(ctx, |ui| { - ui.horizontal(|ui| { - ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { - ui.label("Sessions"); + + if self.opts.side_panel.show_right_panel { + let side_panel_response = egui::SidePanel::right("main_right_panel") + .resizable(true) + .width_range(self.opts.side_panel.min_panel_width..=SidePanel::MAX_WIDTH) + .default_width(SidePanel::DEFAULT_WIDTH) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| { + ui.label("Sessions"); + }); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Max), |ui| { + if ui.button("X").clicked() { + self.opts.side_panel.show_right_panel = false; + } + }); }); - // TODO: add close menu - // ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { - // ui.label("Sessions"); - // }); + self.search_sessions(ui); + ui.separator(); + self.list_sessions(ctx, ui, &mut toasts); }); - self.search_sessions(ui); - ui.separator(); - self.list_sessions(ctx, ui, &mut toasts); - }); + if side_panel_response.response.rect.width() <= SidePanel::CLOSE_WIDTH { + self.opts.side_panel.show_right_panel = false; + self.opts.side_panel.min_panel_width = SidePanel::DEFAULT_WIDTH; + } else { + self.opts.side_panel.min_panel_width = SidePanel::MIN_WIDTH; + } + } + egui::TopBottomPanel::bottom("main_bottom_panel").show(ctx, |ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { global_theme_switch(ui); diff --git a/nxshell/src/ui/menubar.rs b/nxshell/src/ui/menubar.rs index c66376c..50561f8 100644 --- a/nxshell/src/ui/menubar.rs +++ b/nxshell/src/ui/menubar.rs @@ -22,6 +22,8 @@ impl NxShell { self.session_menu(ui); // Window window_menu(ui); + // View + self.view_menu(ui); // Tools self.tools_menu(ui); // Help @@ -70,6 +72,19 @@ impl NxShell { ui.add(Checkbox::new(&mut self.opts.multi_exec, "Multi Exec")); }); } + + fn view_menu(&mut self, ui: &mut egui::Ui) { + ui.menu_button("View", |ui| { + ui.set_width(BTN_WIDTH); + ui.menu_button("Panes", |ui| { + let session_btn = Button::new("Sessions").min_size((BTN_WIDTH, 0.).into()); + if ui.add(session_btn).clicked() { + self.opts.side_panel.show_right_panel = true; + ui.close_menu(); + } + }); + }); + } } impl NxShell { @@ -97,7 +112,7 @@ impl NxShell { ctx: &egui::Context, session: Session, ) -> Result<(), NxError> { - let auth = match AuthType::from(session.auth_type) { + let auth: Authentication = match AuthType::from(session.auth_type) { AuthType::Password => { let key = SecretKey::from_slice(&session.secret_key)?; let auth_data = orion_open(&key, &session.secret_data)?; diff --git a/nxshell/src/ui/mod.rs b/nxshell/src/ui/mod.rs index 3fff304..a07a1e1 100644 --- a/nxshell/src/ui/mod.rs +++ b/nxshell/src/ui/mod.rs @@ -1,3 +1,4 @@ pub mod form; pub mod menubar; +pub mod side_panel; pub mod tab_view; diff --git a/nxshell/src/ui/side_panel/mod.rs b/nxshell/src/ui/side_panel/mod.rs new file mode 100644 index 0000000..ab24a81 --- /dev/null +++ b/nxshell/src/ui/side_panel/mod.rs @@ -0,0 +1,21 @@ +#[derive(Debug, Clone)] +pub struct SidePanel { + pub show_right_panel: bool, + pub min_panel_width: f32, +} + +impl SidePanel { + pub fn new(is_show: bool) -> Self { + Self { + show_right_panel: is_show, + min_panel_width: 0.0, + } + } +} + +impl SidePanel { + pub const DEFAULT_WIDTH: f32 = 200.0; + pub const MIN_WIDTH: f32 = 0.0; + pub const MAX_WIDTH: f32 = 600.0; + pub const CLOSE_WIDTH: f32 = 100.0; +} From b9778219edb5afaf17e686aa6acd8cec09c549cb Mon Sep 17 00:00:00 2001 From: gongyh Date: Wed, 11 Jun 2025 07:13:50 +0800 Subject: [PATCH 04/13] set tools menu size --- nxshell/src/ui/menubar.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nxshell/src/ui/menubar.rs b/nxshell/src/ui/menubar.rs index 50561f8..6ec52a7 100644 --- a/nxshell/src/ui/menubar.rs +++ b/nxshell/src/ui/menubar.rs @@ -69,7 +69,12 @@ impl NxShell { fn tools_menu(&mut self, ui: &mut egui::Ui) { ui.menu_button("Tools", |ui| { - ui.add(Checkbox::new(&mut self.opts.multi_exec, "Multi Exec")); + ui.with_layout(egui::Layout::left_to_right(egui::Align::LEFT), |ui| { + ui.add_sized( + (BTN_WIDTH, 0.), + Checkbox::new(&mut self.opts.multi_exec, "Multi Exec"), + ); + }); }); } From 251a4fcd45532d91479959a5b4fefcf3a0d5a15b Mon Sep 17 00:00:00 2001 From: gongyh Date: Wed, 11 Jun 2025 07:55:41 +0800 Subject: [PATCH 05/13] size --- nxshell/src/ui/menubar.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nxshell/src/ui/menubar.rs b/nxshell/src/ui/menubar.rs index 6ec52a7..1710462 100644 --- a/nxshell/src/ui/menubar.rs +++ b/nxshell/src/ui/menubar.rs @@ -69,11 +69,9 @@ impl NxShell { fn tools_menu(&mut self, ui: &mut egui::Ui) { ui.menu_button("Tools", |ui| { + ui.set_width(BTN_WIDTH); ui.with_layout(egui::Layout::left_to_right(egui::Align::LEFT), |ui| { - ui.add_sized( - (BTN_WIDTH, 0.), - Checkbox::new(&mut self.opts.multi_exec, "Multi Exec"), - ); + ui.add(Checkbox::new(&mut self.opts.multi_exec, "Multi Exec")); }); }); } From 92cd813b986f043cc5c1ceffe260ab2190231b61 Mon Sep 17 00:00:00 2001 From: gongyh Date: Wed, 11 Jun 2025 07:55:57 +0800 Subject: [PATCH 06/13] action --- .github/workflows/build.yaml | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index cd8f6c2..8fe5da1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -14,33 +14,11 @@ jobs: fail-fast: false matrix: include: - # Linux - - os: ubuntu-latest - name: linux - arch: x86_64 - target: x86_64-unknown-linux-gnu - - os: ubuntu-latest - name: linux - arch: aarch64 - target: aarch64-unknown-linux-gnu # Windows - os: windows-latest name: windows arch: x86_64 target: x86_64-pc-windows-msvc - - os: windows-latest - name: windows - arch: aarch64 - target: aarch64-pc-windows-msvc - # MacOS - - os: macos-latest - name: macos - arch: x86_64 - target: x86_64-apple-darwin - - os: macos-latest - name: macos - arch: aarch64 - target: aarch64-apple-darwin steps: - uses: actions/checkout@v4 - name: Install Rust From 383fd45946e55c518004b103ada59bea9f65e474 Mon Sep 17 00:00:00 2001 From: gongyh Date: Sat, 14 Jun 2025 12:45:41 +0800 Subject: [PATCH 07/13] rename Tab View --- nxshell/src/app.rs | 11 ++- nxshell/src/ui/tab_view/mod.rs | 121 ++++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 3 deletions(-) diff --git a/nxshell/src/app.rs b/nxshell/src/app.rs index 35e8b12..ca9d3bd 100644 --- a/nxshell/src/app.rs +++ b/nxshell/src/app.rs @@ -2,7 +2,7 @@ use crate::db::DbConn; use crate::errors::{error_toast, NxError}; use crate::ui::form::{AuthType, NxStateManager}; use crate::ui::side_panel::SidePanel; -use crate::ui::tab_view::Tab; +use crate::ui::tab_view::{Tab, TabEvent}; use copypasta::ClipboardContext; use eframe::{egui, NativeOptions}; use egui::{Align2, CollapsingHeader, FontData, FontId, Id, TextEdit}; @@ -35,6 +35,10 @@ pub struct NxShellOptions { pub session_filter: String, pub side_panel: SidePanel, + + pub show_rename_view: Rc>, + pub renaming_tab_id: Option, + pub tab_events: Vec, } impl NxShellOptions { @@ -58,6 +62,9 @@ impl Default for NxShellOptions { term_font_size, session_filter: String::default(), side_panel: SidePanel::new(true), + show_rename_view: Rc::new(RefCell::new(false)), + renaming_tab_id: None, + tab_events: Vec::new(), } } } @@ -169,6 +176,8 @@ impl eframe::App for NxShell { self.tab_view(ctx); }); + self.rename_tab_view(ctx); + toasts.show(ctx); } } diff --git a/nxshell/src/ui/tab_view/mod.rs b/nxshell/src/ui/tab_view/mod.rs index 7682e52..af577ee 100644 --- a/nxshell/src/ui/tab_view/mod.rs +++ b/nxshell/src/ui/tab_view/mod.rs @@ -5,8 +5,8 @@ use crate::app::{NxShell, NxShellOptions}; use crate::consts::GLOBAL_COUNTER; use crate::ui::tab_view::session::SessionList; use copypasta::ClipboardContext; -use egui::{Label, Response, Sense, Ui}; -use egui_dock::{DockArea, Style}; +use egui::{Label, Order, Response, Sense, Ui}; +use egui_dock::{node_index::NodeIndex, surface_index::SurfaceIndex, DockArea, Style}; use egui_phosphor::regular::{DRONE, NUMPAD}; use egui_term::{ Authentication, PtyEvent, TermType, Terminal, TerminalContext, TerminalOptions, TerminalTheme, @@ -18,6 +18,13 @@ use std::sync::mpsc::Sender; use terminal::TerminalTab; use tracing::error; +const TAB_BTN_WIDTH: f32 = 100.0; + +#[derive(Debug, Clone)] +pub enum TabEvent { + Rename(u64), // tab id +} + #[derive(PartialEq)] enum TabInner { Term(TerminalTab), @@ -28,6 +35,8 @@ enum TabInner { pub struct Tab { inner: TabInner, id: u64, + custom_title: Option, + rename_buffer: String, } impl Tab { @@ -56,6 +65,8 @@ impl Tab { terminal_theme: TerminalTheme::default(), term_type: typ, }), + custom_title: None, + rename_buffer: String::new(), }) } @@ -65,6 +76,8 @@ impl Tab { Self { id, inner: TabInner::SessionList(SessionList {}), + custom_title: None, + rename_buffer: String::new(), } } } @@ -79,6 +92,9 @@ impl egui_dock::TabViewer for TabViewer<'_> { type Tab = Tab; fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText { + if let Some(title) = &tab.custom_title { + return title.clone().into(); + } let tab_id = tab.id(); match &mut tab.inner { TabInner::Term(term) => match term.term_type { @@ -154,6 +170,23 @@ impl egui_dock::TabViewer for TabViewer<'_> { } } + fn context_menu( + &mut self, + ui: &mut Ui, + tab: &mut Self::Tab, + _surface: SurfaceIndex, + _node: NodeIndex, + ) { + ui.set_width(TAB_BTN_WIDTH); + let rename_btn_response = ui.button("Rename Tab"); + if rename_btn_response.clicked() { + self.options.tab_events.push(TabEvent::Rename(tab.id())); + ui.close_menu(); + } + + ui.separator(); + } + fn closeable(&mut self, tab: &mut Self::Tab) -> bool { matches!(&mut tab.inner, TabInner::Term(_)) } @@ -186,4 +219,88 @@ impl NxShell { ); } } + + pub fn rename_tab_view(&mut self, ctx: &egui::Context) { + if let Some(tab_id) = self.opts.renaming_tab_id { + if let Some((_, tab)) = self + .dock_state + .iter_all_tabs_mut() + .find(|(_, tab)| tab.id() == tab_id) + { + let popup_id = egui::Id::new(format!("rename_tab_{}", tab_id)); + let mut close_popup = false; + + self.opts.surrender_focus(); + egui::Area::new("modal_mask".into()) + .order(egui::Order::Middle) + .interactable(true) + .show(ctx, |ui| { + let screen_rect = ui.ctx().screen_rect(); + let painter = ui.painter(); + painter.rect_filled(screen_rect, 0.0, egui::Color32::from_black_alpha(96)); + ui.allocate_rect(screen_rect, egui::Sense::click_and_drag()); + }); + + egui::Window::new("Rename Tab View") + .id(popup_id) + .title_bar(true) + .collapsible(false) + .resizable(false) + .order(Order::Foreground) + .open(&mut self.opts.show_rename_view.borrow_mut()) + .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) + .show(ctx, |ui| { + ui.label("Please input a new name for the tab:"); + let text_id = egui::Id::new(format!("rename_tab_text_{}", tab_id)); + + ui.add(egui::TextEdit::singleline(&mut tab.rename_buffer).id(text_id)); + ui.memory_mut(|mem| mem.request_focus(text_id)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { + if ui.button("Cancel").clicked() { + ui.set_width(50.0); + tab.rename_buffer.clear(); + close_popup = true; + } + + ui.add_space(20.0); + + if ui.button("OK").clicked() { + ui.set_width(50.0); + if !tab.rename_buffer.is_empty() { + tab.custom_title = Some(tab.rename_buffer.clone()); + } + tab.rename_buffer.clear(); + close_popup = true; + } + }); + if ui.input(|i| i.key_pressed(egui::Key::Enter)) { + if !tab.rename_buffer.is_empty() { + tab.custom_title = Some(tab.rename_buffer.clone()); + } + tab.rename_buffer.clear(); + close_popup = true; + } else if ui.input(|i| i.key_pressed(egui::Key::Escape)) { + tab.rename_buffer.clear(); + close_popup = true; + } + }); + if close_popup || !*self.opts.show_rename_view.borrow() { + self.opts.renaming_tab_id = None; + *self.opts.show_rename_view.borrow_mut() = false; + tab.rename_buffer.clear(); + } + } + } else { + self.opts.renaming_tab_id = None; + + if let Some(event) = self.opts.tab_events.pop() { + match event { + TabEvent::Rename(tab_id) => { + self.opts.renaming_tab_id = Some(tab_id); + *self.opts.show_rename_view.borrow_mut() = true; + } + } + } + } + } } From 3f281076a4d280b21d3612c6867e54776b81cf7a Mon Sep 17 00:00:00 2001 From: gongyh Date: Sun, 15 Jun 2025 19:03:10 +0800 Subject: [PATCH 08/13] collapse the window --- nxshell/src/ui/tab_view/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxshell/src/ui/tab_view/mod.rs b/nxshell/src/ui/tab_view/mod.rs index af577ee..09c9c7c 100644 --- a/nxshell/src/ui/tab_view/mod.rs +++ b/nxshell/src/ui/tab_view/mod.rs @@ -207,7 +207,7 @@ impl NxShell { if self.opts.show_dock_panel { DockArea::new(&mut self.dock_state) .show_add_buttons(false) - .show_leaf_collapse_buttons(false) + .show_leaf_collapse_buttons(true) .style(Style::from_egui(ctx.style().as_ref())) .show( ctx, From b8dff44f39eed2a033981622bcbda0760b9cde0b Mon Sep 17 00:00:00 2001 From: gongyh Date: Sun, 22 Jun 2025 19:44:05 +0800 Subject: [PATCH 09/13] fix shortcut for copy&paste --- crates/egui-term/src/display/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/egui-term/src/display/mod.rs b/crates/egui-term/src/display/mod.rs index 56ecef8..959d69c 100644 --- a/crates/egui-term/src/display/mod.rs +++ b/crates/egui-term/src/display/mod.rs @@ -172,7 +172,7 @@ impl TerminalView<'_> { #[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)); + let copy_btn: Button<'_> = 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); @@ -182,7 +182,7 @@ impl TerminalView<'_> { fn paste_btn(&mut self, ui: &mut egui::Ui, btn_width: f32) { #[cfg(not(target_os = "macos"))] - let paste_shortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::V); + 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); @@ -198,7 +198,7 @@ impl TerminalView<'_> { 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); + let select_all_shortcut = KeyboardShortcut::new(Modifiers::CTRL | Modifiers::SHIFT, 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); From c62587651d8ba48aa5df43788b9d1d9fc0e09857 Mon Sep 17 00:00:00 2001 From: gongyh Date: Sat, 5 Jul 2025 23:11:13 +0800 Subject: [PATCH 10/13] feat: add scroll bar --- crates/egui-term/src/lib.rs | 2 + crates/egui-term/src/scroll_bar.rs | 104 +++++++++++++++++++++++++++++ crates/egui-term/src/view.rs | 79 +++++++++++++++------- nxshell/src/ui/tab_view/mod.rs | 4 ++ 4 files changed, 164 insertions(+), 25 deletions(-) create mode 100644 crates/egui-term/src/scroll_bar.rs diff --git a/crates/egui-term/src/lib.rs b/crates/egui-term/src/lib.rs index 26e1956..fcd299a 100644 --- a/crates/egui-term/src/lib.rs +++ b/crates/egui-term/src/lib.rs @@ -4,6 +4,7 @@ mod display; mod errors; mod font; mod input; +mod scroll_bar; mod ssh; mod theme; mod types; @@ -13,6 +14,7 @@ pub use alacritty::{PtyEvent, TermType, Terminal, TerminalContext}; pub use alacritty_terminal::term::TermMode; pub use bindings::{Binding, BindingAction, InputKind, KeyboardBinding}; pub use font::{FontSettings, TerminalFont}; +pub use scroll_bar::{InteractiveScrollbar, ScrollbarState}; pub use ssh::{Authentication, SshOptions}; pub use theme::{ColorPalette, TerminalTheme}; pub use view::{TerminalOptions, TerminalView}; diff --git a/crates/egui-term/src/scroll_bar.rs b/crates/egui-term/src/scroll_bar.rs new file mode 100644 index 0000000..baafd42 --- /dev/null +++ b/crates/egui-term/src/scroll_bar.rs @@ -0,0 +1,104 @@ +use egui::{Color32, NumExt, Pos2, Rect, Sense, Ui, Vec2}; + +#[derive(Clone)] +pub struct ScrollbarState { + pub scroll_pixels: f32, +} + +impl Default for ScrollbarState { + fn default() -> Self { + Self { scroll_pixels: 0.0 } + } +} + +pub struct InteractiveScrollbar { + pub first_row_pos: f32, + pub new_first_row_pos: Option, +} + +impl InteractiveScrollbar { + pub fn new() -> Self { + Self { + first_row_pos: 0.0, + new_first_row_pos: None, + } + } + + pub fn set_first_row_pos(&mut self, row: f32) { + self.first_row_pos = row; + } + + pub const WIDTH: f32 = 16.0; + pub const MARGIN: f32 = 0.0; +} + +impl InteractiveScrollbar { + pub fn ui(&mut self, total_height: f32, ui: &mut Ui) { + let mut position: f32; + let scrollbar_width = InteractiveScrollbar::WIDTH; + let margin = InteractiveScrollbar::MARGIN; + + let available_rect = ui.available_rect_before_wrap(); + let height = available_rect.bottom() - available_rect.top(); + let y_min = available_rect.top() + margin; + let scrollbar_rect = Rect::from_min_size( + Pos2::new(available_rect.right() - scrollbar_width - margin, y_min), + Vec2::new(scrollbar_width, height), + ); + + let ratio = (height / total_height).min(1.0); + let slider_height = (height * ratio).at_least(64.0); + let max_value = total_height - height; + let max_scroll_top = height - slider_height; + let scroll_pos = max_scroll_top - self.first_row_pos * max_scroll_top / max_value; + let slider_rect = Rect::from_min_size( + scrollbar_rect.min + Vec2::new(0.0, scroll_pos), + Vec2::new(scrollbar_width, slider_height), + ); + + ui.painter().rect_filled( + scrollbar_rect, + 0.0, + Color32::BLACK, //from_gray(100) + ); + + ui.painter().rect_filled( + slider_rect, + 0.0, + Color32::DARK_GRAY, //from_gray(200) + ); + + let response = ui.allocate_rect(slider_rect, Sense::click_and_drag()); + + let scrollbar_response = ui.allocate_rect(scrollbar_rect, Sense::click()); + + if response.dragged() { + if let Some(pos) = response.hover_pos() { + let new_position = pos.y - scrollbar_rect.top(); + position = new_position.clamp(0.0, height); + let new_first_row_pos = max_value - position * max_value / max_scroll_top; + self.new_first_row_pos = Some(new_first_row_pos); + } + } + + if scrollbar_response.clicked() { + if let Some(click_pos) = scrollbar_response.interact_pointer_pos() { + let click_y = click_pos.y - scrollbar_rect.top(); + position = click_y.clamp(0.0, height); + let new_first_row_pos = max_value - position * max_value / max_scroll_top; + self.new_first_row_pos = Some(new_first_row_pos); + } + } + + // mouse wheel + /* + let scroll_delta = ui.input(|i| i.smooth_scroll_delta.y); + if scroll_delta != 0.0 { + self.state.position += scroll_delta * 1.0; + self.state.position = self.state.position.clamp(0.0, height); + } + */ + + ui.ctx().request_repaint(); + } +} diff --git a/crates/egui-term/src/view.rs b/crates/egui-term/src/view.rs index e707e57..a595811 100644 --- a/crates/egui-term/src/view.rs +++ b/crates/egui-term/src/view.rs @@ -3,8 +3,10 @@ use crate::bindings::Binding; use crate::bindings::{BindingAction, Bindings, InputKind}; use crate::font::TerminalFont; use crate::input::InputAction; +use crate::scroll_bar::{InteractiveScrollbar, ScrollbarState}; use crate::theme::TerminalTheme; use crate::types::Size; +use alacritty_terminal::grid::{Dimensions, Scroll}; use alacritty_terminal::index::Point; use egui::ImeEvent; use egui::Widget; @@ -22,6 +24,7 @@ pub struct TerminalViewState { pub cursor_position: Option, // ime_enabled: bool, // ime_cursor_range: CursorRange, + pub scrollbar_state: ScrollbarState, } impl TerminalViewState { @@ -57,38 +60,64 @@ pub struct TerminalOptions<'a> { impl Widget for TerminalView<'_> { fn ui(mut self, ui: &mut egui::Ui) -> Response { - let (layout, painter) = ui.allocate_painter(self.size, egui::Sense::click()); - let widget_id = self.widget_id; let mut state = TerminalViewState::load(ui.ctx(), widget_id); + let mut layout_opt = None; - if layout.contains_pointer() { - *self.options.active_tab_id = Some(self.widget_id); - layout.ctx.set_cursor_icon(CursorIcon::Text); - } else { - layout.ctx.set_cursor_icon(CursorIcon::Default); - } + ui.horizontal(|ui| { + let size_p = Vec2::new(self.size.x - InteractiveScrollbar::WIDTH, self.size.y); + let (layout, painter) = ui.allocate_painter(size_p, egui::Sense::click()); - if self.options.active_tab_id.is_none() { - self.has_focus = false; - } + if layout.contains_pointer() { + *self.options.active_tab_id = Some(self.widget_id); + layout.ctx.set_cursor_icon(CursorIcon::Text); + } else { + layout.ctx.set_cursor_icon(CursorIcon::Default); + } - // context menu - if let Some(pos) = state.cursor_position { - self.context_menu(pos, &layout, ui); - } - if ui.input(|input_state| input_state.pointer.primary_clicked()) { - state.cursor_position = None; - ui.close_menu(); - } + if self.options.active_tab_id.is_none() { + self.has_focus = false; + } + + // context menu + if let Some(pos) = state.cursor_position { + self.context_menu(pos, &layout, ui); + } + if ui.input(|input_state| input_state.pointer.primary_clicked()) { + state.cursor_position = None; + ui.close_menu(); + } + + let mut term = self + .focus(&layout) + .resize(&layout) + .process_input(&mut state, &layout); + + let grid = term.term_ctx.terminal.grid_mut(); + let total_lines = grid.total_lines() as f32; + let display_offset = grid.display_offset() as f32; + let cell_height = term.term_ctx.size.cell_height as f32; + let total_height = cell_height * total_lines; + let display_offset_pos = display_offset * cell_height; + + let mut scrollbar = InteractiveScrollbar::new(); + scrollbar.set_first_row_pos(display_offset_pos); + scrollbar.ui(total_height, ui); + if let Some(new_first_row_pos) = scrollbar.new_first_row_pos { + let total_row_pos = new_first_row_pos + state.scrollbar_state.scroll_pixels; + let new_pos = total_row_pos / cell_height; + state.scrollbar_state.scroll_pixels = total_row_pos % cell_height; + let line_diff = new_pos - display_offset; + let line_delta = Scroll::Delta(line_diff.ceil() as i32); + grid.scroll_display(line_delta); + } - self.focus(&layout) - .resize(&layout) - .process_input(&mut state, &layout) - .show(&mut state, &layout, &painter); + term.show(&mut state, &layout, &painter); - state.store(ui.ctx(), widget_id); - layout + state.store(ui.ctx(), widget_id); + layout_opt = Some(layout); + }); + layout_opt.unwrap() } } diff --git a/nxshell/src/ui/tab_view/mod.rs b/nxshell/src/ui/tab_view/mod.rs index 09c9c7c..1f3d34d 100644 --- a/nxshell/src/ui/tab_view/mod.rs +++ b/nxshell/src/ui/tab_view/mod.rs @@ -200,6 +200,10 @@ impl egui_dock::TabViewer for TabViewer<'_> { Ok(_) => true, } } + + fn scroll_bars(&self, _tab: &Self::Tab) -> [bool; 2] { + [false, false] + } } impl NxShell { From 02655b45f5859b22289f28c0176dc1f7d256b561 Mon Sep 17 00:00:00 2001 From: gongyh Date: Sun, 6 Jul 2025 15:00:53 +0800 Subject: [PATCH 11/13] fix: select out-of-terminal --- crates/egui-term/src/view.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/egui-term/src/view.rs b/crates/egui-term/src/view.rs index a595811..5804030 100644 --- a/crates/egui-term/src/view.rs +++ b/crates/egui-term/src/view.rs @@ -237,11 +237,16 @@ impl<'a> TerminalView<'a> { modifiers, pos, } => { + let new_pos: Pos2; + if out_of_terminal(pos, layout) { - continue; + new_pos = pos.clamp(layout.rect.min, layout.rect.max); + } else { + new_pos = pos; } + if let Some(action) = - self.button_click(state, layout, button, pos, &modifiers, pressed) + self.button_click(state, layout, button, new_pos, &modifiers, pressed) { input_actions.push(action); } From a94bb0a6be8fe881ae38811ce8e1611dfe0219a7 Mon Sep 17 00:00:00 2001 From: gongyh Date: Mon, 7 Jul 2025 23:42:56 +0800 Subject: [PATCH 12/13] search --- crates/egui-term/src/alacritty/mod.rs | 1 + crates/egui-term/src/bindings.rs | 3 +++ crates/egui-term/src/input/mod.rs | 10 ++++++++++ crates/egui-term/src/view.rs | 27 ++++++++++++++++++++------- nxshell/src/app.rs | 5 +++++ nxshell/src/ui/tab_view/mod.rs | 2 ++ 6 files changed, 41 insertions(+), 7 deletions(-) diff --git a/crates/egui-term/src/alacritty/mod.rs b/crates/egui-term/src/alacritty/mod.rs index 44f5c70..d8441a8 100644 --- a/crates/egui-term/src/alacritty/mod.rs +++ b/crates/egui-term/src/alacritty/mod.rs @@ -323,6 +323,7 @@ impl<'a> TerminalContext<'a> { BackendCommand::MouseReport(button, modifiers, point, pressed) => { self.mouse_report(button, modifiers, point, pressed); } + _ => { } }; } diff --git a/crates/egui-term/src/bindings.rs b/crates/egui-term/src/bindings.rs index 71ca8e4..447a0bb 100644 --- a/crates/egui-term/src/bindings.rs +++ b/crates/egui-term/src/bindings.rs @@ -15,6 +15,7 @@ pub enum BindingAction { DecreaseFontSize, Char(char), Esc(String), + Search, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -334,6 +335,7 @@ fn platform_keyboard_bindings() -> Vec<(Binding, BindingAction)> { Equals, Modifiers::MAC_CMD; BindingAction::IncreaseFontSize; Plus, Modifiers::MAC_CMD; BindingAction::IncreaseFontSize; Minus, Modifiers::MAC_CMD; BindingAction::DecreaseFontSize; + F, Modifiers::MAC_CMD; BindingAction::Search; ) } @@ -348,6 +350,7 @@ fn platform_keyboard_bindings() -> Vec<(Binding, BindingAction)> { Equals, Modifiers::CTRL; BindingAction::IncreaseFontSize; Plus, Modifiers::CTRL; BindingAction::IncreaseFontSize; Minus, Modifiers::CTRL; BindingAction::DecreaseFontSize; + F, Modifiers::CTRL; BindingAction::Search; ) } diff --git a/crates/egui-term/src/input/mod.rs b/crates/egui-term/src/input/mod.rs index e4f5bd3..2c92ea3 100644 --- a/crates/egui-term/src/input/mod.rs +++ b/crates/egui-term/src/input/mod.rs @@ -67,6 +67,11 @@ impl TerminalView<'_> { Some(BindingAction::SelectAll) => { Some(InputAction::BackendCall(BackendCommand::SelectAll)) } + Some(BindingAction::Search) => { + let content = self.term_ctx.selection_content(); + self.set_search_regex(content); + None + } _ => None, } } @@ -82,6 +87,11 @@ impl TerminalView<'_> { } } + fn set_search_regex(&mut self, str_regex: String) { + *self.options.search_start = true; + *self.options.search_regex = str_regex; + } + pub fn mouse_wheel_input( &mut self, state: &mut TerminalViewState, diff --git a/crates/egui-term/src/view.rs b/crates/egui-term/src/view.rs index 5804030..3064120 100644 --- a/crates/egui-term/src/view.rs +++ b/crates/egui-term/src/view.rs @@ -7,7 +7,7 @@ use crate::scroll_bar::{InteractiveScrollbar, ScrollbarState}; use crate::theme::TerminalTheme; use crate::types::Size; use alacritty_terminal::grid::{Dimensions, Scroll}; -use alacritty_terminal::index::Point; +use alacritty_terminal::index::{Point, Line, Column}; use egui::ImeEvent; use egui::Widget; use egui::{Context, Event}; @@ -56,6 +56,8 @@ pub struct TerminalOptions<'a> { pub multi_exec: &'a mut bool, pub theme: &'a mut TerminalTheme, pub active_tab_id: &'a mut Option, + pub search_start: &'a mut bool, + pub search_regex: &'a mut String, } impl Widget for TerminalView<'_> { @@ -112,6 +114,19 @@ impl Widget for TerminalView<'_> { grid.scroll_display(line_delta); } + if *term.options.search_start { + let mut start_pos = Point::new(Line(0), Column(0)); + let regex = term.term_ctx.terminal.inline_search_right( start_pos, term.options.search_regex); + match regex { + Ok(point) => { + println!("point: {}, {}", point.line, term.options.search_regex); + } + Err(_point1) => { + println!("search error: {}", term.options.search_regex); + } + } + } + term.show(&mut state, &layout, &painter); state.store(ui.ctx(), widget_id); @@ -237,13 +252,11 @@ impl<'a> TerminalView<'a> { modifiers, pos, } => { - let new_pos: Pos2; - - if out_of_terminal(pos, layout) { - new_pos = pos.clamp(layout.rect.min, layout.rect.max); + let new_pos = if out_of_terminal(pos, layout) { + pos.clamp(layout.rect.min, layout.rect.max) } else { - new_pos = pos; - } + pos + }; if let Some(action) = self.button_click(state, layout, button, new_pos, &modifiers, pressed) diff --git a/nxshell/src/app.rs b/nxshell/src/app.rs index ca9d3bd..7cf3e99 100644 --- a/nxshell/src/app.rs +++ b/nxshell/src/app.rs @@ -39,6 +39,9 @@ pub struct NxShellOptions { pub show_rename_view: Rc>, pub renaming_tab_id: Option, pub tab_events: Vec, + + pub search_start: bool, + pub search_regex: String, } impl NxShellOptions { @@ -65,6 +68,8 @@ impl Default for NxShellOptions { show_rename_view: Rc::new(RefCell::new(false)), renaming_tab_id: None, tab_events: Vec::new(), + search_start: false, + search_regex: String::default(), } } } diff --git a/nxshell/src/ui/tab_view/mod.rs b/nxshell/src/ui/tab_view/mod.rs index 1f3d34d..974cfa8 100644 --- a/nxshell/src/ui/tab_view/mod.rs +++ b/nxshell/src/ui/tab_view/mod.rs @@ -131,6 +131,8 @@ impl egui_dock::TabViewer for TabViewer<'_> { theme: &mut tab.terminal_theme, default_font_size: self.options.term_font_size, active_tab_id: &mut self.options.active_tab_id, + search_start: &mut self.options.search_start, + search_regex: &mut self.options.search_regex, }; let terminal = TerminalView::new(ui, term_ctx, term_opt) From cb68f0b32e0a938df388febec006ff6b182b8b30 Mon Sep 17 00:00:00 2001 From: gongyh Date: Mon, 7 Jul 2025 23:44:32 +0800 Subject: [PATCH 13/13] search --- crates/egui-term/src/alacritty/mod.rs | 2 +- crates/egui-term/src/view.rs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/egui-term/src/alacritty/mod.rs b/crates/egui-term/src/alacritty/mod.rs index d8441a8..9db9cc3 100644 --- a/crates/egui-term/src/alacritty/mod.rs +++ b/crates/egui-term/src/alacritty/mod.rs @@ -323,7 +323,7 @@ impl<'a> TerminalContext<'a> { BackendCommand::MouseReport(button, modifiers, point, pressed) => { self.mouse_report(button, modifiers, point, pressed); } - _ => { } + _ => {} }; } diff --git a/crates/egui-term/src/view.rs b/crates/egui-term/src/view.rs index 3064120..663c443 100644 --- a/crates/egui-term/src/view.rs +++ b/crates/egui-term/src/view.rs @@ -7,7 +7,7 @@ use crate::scroll_bar::{InteractiveScrollbar, ScrollbarState}; use crate::theme::TerminalTheme; use crate::types::Size; use alacritty_terminal::grid::{Dimensions, Scroll}; -use alacritty_terminal::index::{Point, Line, Column}; +use alacritty_terminal::index::{Column, Line, Point}; use egui::ImeEvent; use egui::Widget; use egui::{Context, Event}; @@ -116,7 +116,10 @@ impl Widget for TerminalView<'_> { if *term.options.search_start { let mut start_pos = Point::new(Line(0), Column(0)); - let regex = term.term_ctx.terminal.inline_search_right( start_pos, term.options.search_regex); + let regex = term + .term_ctx + .terminal + .inline_search_right(start_pos, term.options.search_regex); match regex { Ok(point) => { println!("point: {}, {}", point.line, term.options.search_regex);