aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-server
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-08 21:59:39 +0200
committeruser <user@clank>2026-08-08 21:59:39 +0200
commite8699796afd3c92162d933d45c3181926f28e9ef (patch)
tree2a81afa936429b62b7ae3bcae01ac755602ca627 /zellij-server
parentsl0p: ignore gap/padding when a pane draws no frame (diff)
downloadsl0ppij-e8699796afd3c92162d933d45c3181926f28e9ef.tar.gz
sl0ppij-e8699796afd3c92162d933d45c3181926f28e9ef.tar.xz
sl0ppij-e8699796afd3c92162d933d45c3181926f28e9ef.zip
sl0p: double-click a pane title to rename it
Double-clicking the top edge of a pane's frame focuses that pane and switches the client into RenamePane mode. Everything needed already existed; this is wiring, not new machinery: - grid.rs already tracks repeated clicks (Click, 400ms threshold). It is now pub and reused rather than inventing a second notion of 'double click' that could drift from the one used for text selection. A frame click never reaches the grid, so Tab tracks frame clicks per client separately. - pane.position_is_on_frame() / get_edge_at_position() already give the hit edge, and mouse_handler already computes both. - Screen::change_mode() already calls store_pane_name() when entering RenamePane, so issuing ChangeMode(RenamePane) does the whole setup exactly as the keybind path does. No rename logic is duplicated. Placement in determine_mouse_action matters twice: - AFTER the floating 'start moving' branch, so floating panes keep dragging by their title (that is their move handle). Tiled panes only for now. - BEFORE the resize branch, which would otherwise consume the click as a top-edge resize drag. Only PaneEdge::Top renames; corners stay resize handles. The pane is focused first because RenamePane acts on the ACTIVE pane, so renaming an unfocused pane would otherwise rename the wrong one. Tests cover all three: double-click on Top renames, single click on Top still resizes, and double-click on any other edge or corner does not rename. Baselines held: zellij-server 1424/30 (3 new tests), zellij-utils 435/5.
Diffstat (limited to 'zellij-server')
-rw-r--r--zellij-server/src/panes/grid.rs8
-rw-r--r--zellij-server/src/tab/mod.rs5
-rw-r--r--zellij-server/src/tab/mouse_handler.rs119
3 files changed, 129 insertions, 3 deletions
diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs
index 7c921176..62ef0e10 100644
--- a/zellij-server/src/panes/grid.rs
+++ b/zellij-server/src/panes/grid.rs
@@ -823,10 +823,14 @@ impl CompiledHighlight {
}
}
-const CLICK_TIME_THRESHOLD: u128 = 400; // Doherty Threshold
+pub const CLICK_TIME_THRESHOLD: u128 = 400; // Doherty Threshold
+/// Tracks repeated clicks at the same position within CLICK_TIME_THRESHOLD.
+/// Public so the frame/title double-click handling can reuse the exact same
+/// notion of "double click" as terminal text selection, rather than inventing a
+/// second threshold that could drift.
#[derive(Clone, Debug, Default)]
-struct Click {
+pub struct Click {
position_and_time: Option<(Position, std::time::Instant)>,
count: usize,
}
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index ec088422..bb02d4d1 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -261,6 +261,10 @@ pub(crate) struct Tab {
mouse_hover_effects: bool,
focus_follows_mouse: bool,
mouse_click_through: bool,
+ /// Repeated clicks on pane FRAMES, per client. Separate from the grid's own
+ /// click tracking (which is for selecting text inside a pane) because a
+ /// click that lands on the frame never reaches the grid.
+ frame_clicks: HashMap<ClientId, crate::panes::grid::Click>,
currently_marking_pane_group: Rc<RefCell<HashMap<ClientId, bool>>>,
connected_clients_in_app: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client
// the below are the configured values - the ones that will be set if and when the web server
@@ -1028,6 +1032,7 @@ impl Tab {
mouse_hover_effects,
focus_follows_mouse,
mouse_click_through,
+ frame_clicks: HashMap::new(),
connected_clients_in_app,
web_server_ip,
web_server_port,
diff --git a/zellij-server/src/tab/mouse_handler.rs b/zellij-server/src/tab/mouse_handler.rs
index 288e58af..99d39bbb 100644
--- a/zellij-server/src/tab/mouse_handler.rs
+++ b/zellij-server/src/tab/mouse_handler.rs
@@ -1,5 +1,5 @@
use std::time::Instant;
-use zellij_utils::data::{Direction, Resize, ResizeStrategy};
+use zellij_utils::data::{Direction, InputMode, Resize, ResizeStrategy};
use zellij_utils::errors::prelude::*;
use zellij_utils::input::mouse::{MouseEvent, MouseEventType};
use zellij_utils::pane_size::PaneGeom;
@@ -101,6 +101,10 @@ impl MouseEffect {
#[derive(Debug, Clone, PartialEq)]
enum MouseAction {
+ /// double-click on a pane's title row -> start renaming it
+ StartRenamingPane {
+ pane_id: PaneId,
+ },
GroupToggle(PaneId),
GroupAdd(PaneId),
Ungroup,
@@ -227,6 +231,8 @@ struct MouseEventContext {
selecting_with_mouse: bool,
pane_being_moved: bool,
clicked_pane: Option<ClickedPaneDetails>,
+ /// this press is the 2nd click of a double-click on a pane frame
+ frame_double_click: bool,
pinned_selectable: Option<PaneId>,
pinned_unselectable: Option<PaneId>,
focus_follows_mouse: bool,
@@ -456,6 +462,25 @@ impl MouseHandler {
(None, None)
};
+ // Record repeated presses on a pane frame so a double-click on the
+ // title row can be distinguished from a plain click (which focuses, or
+ // starts a resize drag). Only plain left presses on a frame count.
+ let frame_double_click = if event.left
+ && event.event_type == MouseEventType::Press
+ && !event.ctrl
+ && !event.alt
+ && clicked_pane.map(|d| d.on_frame).unwrap_or(false)
+ {
+ let tracker = tab.frame_clicks.entry(client_id).or_default();
+ tracker.record_click(event.position);
+ tracker.is_double_click()
+ } else {
+ if let Some(tracker) = tab.frame_clicks.get_mut(&client_id) {
+ tracker.reset();
+ }
+ false
+ };
+
Ok(MouseEventContext {
pane_id_at_position,
active_pane_id,
@@ -464,6 +489,7 @@ impl MouseHandler {
selecting_with_mouse: tab.selecting_with_mouse_in_pane.is_some(),
pane_being_moved: tab.floating_panes.pane_is_being_moved_with_mouse(),
clicked_pane,
+ frame_double_click,
pinned_selectable,
pinned_unselectable,
focus_follows_mouse: tab.focus_follows_mouse,
@@ -737,6 +763,25 @@ impl MouseHandler {
}
Ok(MouseEffect::group_toggle(pane_id))
},
+ MouseAction::StartRenamingPane { pane_id } => {
+ // Focus the pane first: RenamePane mode acts on the ACTIVE pane
+ // (Screen::change_mode calls store_pane_name() on it), so
+ // renaming an unfocused pane would otherwise rename the wrong
+ // one. Then ask Screen to switch this client into RenamePane,
+ // which is the same path the keybind takes.
+ clear_hover_for_client(tab, client_id);
+ tab.focus_pane_with_id(pane_id, false, false, client_id)
+ .with_context(err_context)?;
+ tab.senders
+ .send_to_screen(ScreenInstruction::ChangeMode(
+ InputMode::RenamePane,
+ None,
+ client_id,
+ None,
+ ))
+ .with_context(err_context)?;
+ Ok(MouseEffect::state_changed())
+ },
MouseAction::GroupAdd(pane_id) => Ok(MouseEffect::group_add(pane_id)),
MouseAction::Ungroup => Ok(MouseEffect::ungroup()),
MouseAction::StartResize {
@@ -1431,6 +1476,16 @@ impl MouseHandler {
});
}
+ // A double-click on the pane's title row starts a rename.
+ // Checked before the resize branch, which would otherwise
+ // consume the click as a top-edge resize drag. Corners are
+ // deliberately excluded: they are resize handles.
+ if ctx.frame_double_click && details.edge == Some(PaneEdge::Top) {
+ return Ok(MouseAction::StartRenamingPane {
+ pane_id: details.pane_id,
+ });
+ }
+
if let Some(edge) = details.edge {
return Ok(MouseAction::StartResize {
pane_id: details.pane_id,
@@ -1838,6 +1893,7 @@ mod tests {
fn mouse_event_context(mouse_scroll_resize: bool) -> MouseEventContext {
MouseEventContext {
+ frame_double_click: false,
pane_id_at_position: Some(PaneId::Terminal(1)),
active_pane_id: Some(PaneId::Terminal(1)),
floating_visible: false,
@@ -1853,6 +1909,67 @@ mod tests {
}
}
+ fn frame_click_context(frame_double_click: bool, edge: Option<PaneEdge>) -> MouseEventContext {
+ let mut ctx = mouse_event_context(false);
+ ctx.frame_double_click = frame_double_click;
+ ctx.clicked_pane = Some(ClickedPaneDetails {
+ pane_id: PaneId::Terminal(1),
+ on_frame: true,
+ frame_intercepted: false,
+ edge,
+ is_floating: false,
+ terminal_wants_mouse: false,
+ });
+ ctx
+ }
+
+ #[test]
+ fn double_click_on_pane_title_starts_rename() {
+ let ctx = frame_click_context(true, Some(PaneEdge::Top));
+ let event = MouseEvent::new_left_press_event(Position::new(1, 1));
+ assert_eq!(
+ MouseHandler::determine_mouse_action(&event, &ctx).unwrap(),
+ MouseAction::StartRenamingPane {
+ pane_id: PaneId::Terminal(1)
+ }
+ );
+ }
+
+ #[test]
+ fn single_click_on_pane_title_still_resizes() {
+ // the rename must not steal the ordinary top-edge resize drag
+ let ctx = frame_click_context(false, Some(PaneEdge::Top));
+ let event = MouseEvent::new_left_press_event(Position::new(1, 1));
+ assert!(matches!(
+ MouseHandler::determine_mouse_action(&event, &ctx).unwrap(),
+ MouseAction::StartResize { .. }
+ ));
+ }
+
+ #[test]
+ fn double_click_on_frame_corners_and_sides_does_not_rename() {
+ // corners are resize handles; only the title row renames
+ for edge in [
+ PaneEdge::TopLeft,
+ PaneEdge::TopRight,
+ PaneEdge::Left,
+ PaneEdge::Right,
+ PaneEdge::Bottom,
+ PaneEdge::BottomLeft,
+ PaneEdge::BottomRight,
+ ] {
+ let ctx = frame_click_context(true, Some(edge));
+ let event = MouseEvent::new_left_press_event(Position::new(1, 1));
+ let action = MouseHandler::determine_mouse_action(&event, &ctx).unwrap();
+ assert!(
+ !matches!(action, MouseAction::StartRenamingPane { .. }),
+ "edge {:?} must not trigger rename, got {:?}",
+ edge,
+ action
+ );
+ }
+ }
+
#[test]
fn disabled_ctrl_scroll_does_not_fall_through_to_regular_scrolling() {
let context = mouse_event_context(false);