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 diff --git a/crates/egui-term/src/alacritty/mod.rs b/crates/egui-term/src/alacritty/mod.rs index 44f5c70..9db9cc3 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/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); 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/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..663c443 100644 --- a/crates/egui-term/src/view.rs +++ b/crates/egui-term/src/view.rs @@ -3,9 +3,11 @@ 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::index::Point; +use alacritty_terminal::grid::{Dimensions, Scroll}; +use alacritty_terminal::index::{Column, Line, Point}; use egui::ImeEvent; use egui::Widget; use egui::{Context, Event}; @@ -22,6 +24,7 @@ pub struct TerminalViewState { pub cursor_position: Option, // ime_enabled: bool, // ime_cursor_range: CursorRange, + pub scrollbar_state: ScrollbarState, } impl TerminalViewState { @@ -53,42 +56,86 @@ 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<'_> { 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(); + } - self.focus(&layout) - .resize(&layout) - .process_input(&mut state, &layout) - .show(&mut state, &layout, &painter); + let mut term = self + .focus(&layout) + .resize(&layout) + .process_input(&mut state, &layout); - state.store(ui.ctx(), widget_id); - 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); + } + + 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); + layout_opt = Some(layout); + }); + layout_opt.unwrap() } } @@ -208,11 +255,14 @@ impl<'a> TerminalView<'a> { modifiers, pos, } => { - if out_of_terminal(pos, layout) { - continue; - } + let new_pos = if out_of_terminal(pos, layout) { + pos.clamp(layout.rect.min, layout.rect.max) + } else { + 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); } diff --git a/nxshell/src/app.rs b/nxshell/src/app.rs index 1f5e5f5..7cf3e99 100644 --- a/nxshell/src/app.rs +++ b/nxshell/src/app.rs @@ -1,7 +1,8 @@ use crate::db::DbConn; use crate::errors::{error_toast, NxError}; use crate::ui::form::{AuthType, NxStateManager}; -use crate::ui::tab_view::Tab; +use crate::ui::side_panel::SidePanel; +use crate::ui::tab_view::{Tab, TabEvent}; use copypasta::ClipboardContext; use eframe::{egui, NativeOptions}; use egui::{Align2, CollapsingHeader, FontData, FontId, Id, TextEdit}; @@ -32,6 +33,15 @@ pub struct NxShellOptions { pub term_font: TerminalFont, pub term_font_size: f32, pub session_filter: String, + + pub side_panel: SidePanel, + + pub show_rename_view: Rc>, + pub renaming_tab_id: Option, + pub tab_events: Vec, + + pub search_start: bool, + pub search_regex: String, } impl NxShellOptions { @@ -54,6 +64,12 @@ impl Default for NxShellOptions { term_font: TerminalFont::new(font_setting), 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(), + search_start: false, + search_regex: String::default(), } } } @@ -118,25 +134,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); @@ -152,6 +181,8 @@ impl eframe::App for NxShell { self.tab_view(ctx); }); + self.rename_tab_view(ctx); + toasts.show(ctx); } } diff --git a/nxshell/src/ui/form/session.rs b/nxshell/src/ui/form/session.rs index b86d45f..d97a7b9 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(), )); @@ -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); diff --git a/nxshell/src/ui/menubar.rs b/nxshell/src/ui/menubar.rs index c66376c..1710462 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 @@ -67,7 +69,23 @@ 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.set_width(BTN_WIDTH); + ui.with_layout(egui::Layout::left_to_right(egui::Align::LEFT), |ui| { + 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(); + } + }); }); } } @@ -97,7 +115,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; +} diff --git a/nxshell/src/ui/tab_view/mod.rs b/nxshell/src/ui/tab_view/mod.rs index 7682e52..974cfa8 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 { @@ -115,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) @@ -154,6 +172,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(_)) } @@ -167,6 +202,10 @@ impl egui_dock::TabViewer for TabViewer<'_> { Ok(_) => true, } } + + fn scroll_bars(&self, _tab: &Self::Tab) -> [bool; 2] { + [false, false] + } } impl NxShell { @@ -174,7 +213,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, @@ -186,4 +225,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; + } + } + } + } + } }