diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-15 17:29:45 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-15 17:45:44 +0300 |
| commit | 0099c10881fd05ddecd3b535700d41eb3f42b633 (patch) | |
| tree | 0c05211953394414b35fe6930b5b45594294833a | |
| parent | 07261e09be60ac0c145685fd11f94351cc0e5ba6 (diff) | |
fix: use ctrl-click for captured pane links on macos
| -rw-r--r-- | docs/next/CHANGELOG.md | 2 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/configuration.mdx | 2 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/quick-start.mdx | 2 | ||||
| -rw-r--r-- | src/app/input/mod.rs | 12 | ||||
| -rw-r--r-- | src/app/input/terminal.rs | 74 | ||||
| -rw-r--r-- | src/raw_input.rs | 21 |
6 files changed, 99 insertions, 14 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 927e8d63..6dd0e9e2 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -57,7 +57,7 @@ This is a hotfix release for v0.6.9. See the v0.6.9 notes for the full feature r - Numeric keypad keys that send VT100 application-keypad escape sequences now enter their digits and operators instead of being dropped. (#493) - Codex panes now stay marked working when the live status header uses reasoning-summary text such as `Investigating code output` instead of the literal `Working` label. (#501) - Codex blocker detection now ignores stale prompt text outside the live prompt region, reducing false blocked states from old scrollback. -- Native pane URL clicks now use Cmd-click on macOS and Ctrl-click on other platforms. (#307) +- Pane URL clicks, including plugin link handlers and native URL opening, now use Ctrl-click on macOS too because captured terminal mouse reports do not expose Cmd-click separately from plain click. (#307) - Worktree open, create, and remove actions now work from bare repositories instead of assuming a normal checkout. (#497) - Pane mouse handling no longer sends empty PTY writes for mouse events that produce no terminal input. (#496) - Pane output now renders flag emoji and other multi-codepoint grapheme clusters as complete symbols instead of blank cells. (#243) diff --git a/docs/next/website/src/content/docs/configuration.mdx b/docs/next/website/src/content/docs/configuration.mdx index fbabb8ba..188d1237 100644 --- a/docs/next/website/src/content/docs/configuration.mdx +++ b/docs/next/website/src/content/docs/configuration.mdx @@ -299,7 +299,7 @@ accent = "cyan" `confirm_close` controls whether closing a workspace asks for confirmation. `prompt_new_tab_name` controls whether new tabs ask for a label first. -Set `mouse_capture = false` if you want your terminal to handle normal clicks, such as command-clicking URLs. With mouse capture enabled, Ctrl-click opens pane links when your terminal sends that modified click to Herdr; use Shift-Ctrl-click on Linux or Shift-Cmd-click on macOS for the terminal-native bypass path. +Set `mouse_capture = false` if you want your terminal to handle normal clicks, such as command-clicking URLs. With mouse capture enabled, Ctrl-click opens pane links when your terminal sends that modified click to Herdr. This includes macOS; Cmd-click is not reported separately from plain click while Herdr captures mouse input. Use Shift-Ctrl-click on Linux or Shift-Cmd-click on macOS for the terminal-native bypass path. Set `right_click_passthrough_modifier = "ctrl"` if you want Ctrl-right-click, hold, and drag gestures inside mouse-reporting pane apps to reach the app instead of opening Herdr's pane menu. The default is empty, which disables this passthrough. Supported modifiers are `ctrl`, `alt`, `cmd`, `super`, `meta`, and `hyper`; `shift` is rejected because many terminals reserve Shift+mouse for their own mouse bypass. diff --git a/docs/next/website/src/content/docs/quick-start.mdx b/docs/next/website/src/content/docs/quick-start.mdx index 26eaa9aa..a8f95e7c 100644 --- a/docs/next/website/src/content/docs/quick-start.mdx +++ b/docs/next/website/src/content/docs/quick-start.mdx @@ -19,7 +19,7 @@ When a session has no workspaces, Herdr opens one automatically. A workspace is Herdr is mouse-native, so start by clicking. Click panes, tabs, workspaces, and agents to focus them. Drag split borders to resize. Right-click for context menus, including splitting panes and creating tabs. Drag-select text to copy it to your clipboard; double-click a token to copy it directly. Copying does not require Ctrl+C. -Ctrl-click opens pane links when your terminal sends the modified click to Herdr. This works for OSC 8 hyperlinks and visible `http://` or `https://` URLs. The portable terminal-native fallback is Shift-Ctrl-click on Linux or Shift-Cmd-click on macOS. +Ctrl-click opens pane links when your terminal sends the modified click to Herdr. This works for OSC 8 hyperlinks and visible `http://` or `https://` URLs. On macOS, use Ctrl-click for Herdr-handled pane links while mouse capture is enabled; Cmd-click is only available through the terminal-native bypass path, such as Shift-Cmd-click or `ui.mouse_capture = false`. If you configure `ui.right_click_passthrough_modifier`, that modifier plus right-click sends right-click, hold, and drag gestures to mouse-reporting pane apps. diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs index 339f7293..cb4b0d46 100644 --- a/src/app/input/mod.rs +++ b/src/app/input/mod.rs @@ -23,23 +23,13 @@ enum WheelRouting { const WORKSPACE_DRAG_THRESHOLD: u16 = 1; const TAB_DRAG_THRESHOLD: u16 = 1; -#[cfg(target_os = "macos")] -fn modified_url_click_modifier() -> KeyModifiers { - KeyModifiers::SUPER -} - -#[cfg(not(target_os = "macos"))] fn modified_url_click_modifier() -> KeyModifiers { KeyModifiers::CONTROL } #[cfg(test)] #[test] -fn modified_url_click_modifier_matches_platform_primary_modifier() { - #[cfg(target_os = "macos")] - assert_eq!(modified_url_click_modifier(), KeyModifiers::SUPER); - - #[cfg(not(target_os = "macos"))] +fn modified_url_click_modifier_matches_terminal_mouse_reporting() { assert_eq!(modified_url_click_modifier(), KeyModifiers::CONTROL); } diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs index 60f2d4cc..e13412a2 100644 --- a/src/app/input/terminal.rs +++ b/src/app/input/terminal.rs @@ -261,6 +261,45 @@ mod tests { .is_some_and(crate::selection::Selection::is_visible)); } + #[cfg(unix)] + fn install_test_link_handler(app: &mut App) { + let plugin_root = std::env::temp_dir(); + app.state.installed_plugins = std::collections::HashMap::from([( + "example.links".to_string(), + crate::api::schema::InstalledPluginInfo { + plugin_id: "example.links".into(), + name: "Links".into(), + version: "0.1.0".into(), + min_herdr_version: "0.6.10".into(), + description: None, + manifest_path: plugin_root.join("herdr-plugin.toml").display().to_string(), + plugin_root: plugin_root.display().to_string(), + enabled: true, + platforms: None, + build: Vec::new(), + actions: vec![crate::api::schema::PluginManifestAction { + id: "open".into(), + title: "Open link".into(), + description: None, + contexts: Vec::new(), + platforms: None, + command: vec!["sh".into(), "-c".into(), ":".into()], + }], + events: Vec::new(), + panes: Vec::new(), + link_handlers: vec![crate::api::schema::PluginManifestLinkHandler { + id: "github-issue".into(), + title: "Open GitHub issue".into(), + pattern: "^https://github\\.com/[^/]+/[^/]+/(issues|pull)/[0-9]+$".into(), + action: "open".into(), + platforms: None, + }], + source: crate::api::schema::PluginSourceInfo::default(), + warnings: Vec::new(), + }, + )]); + } + #[tokio::test] async fn dragging_selection_above_pane_autoscrolls_and_extends_into_scrollback() { let mut app = app_for_mouse_test(); @@ -465,6 +504,41 @@ mod tests { assert!(app.selection_highlight_clear_deadline.is_none()); } + #[cfg(unix)] + #[tokio::test] + async fn ctrl_click_url_invokes_plugin_link_handler_but_super_click_does_not() { + let line = "see https://github.com/ogulcancelik/herdr/issues/398"; + let col = line.find("github").expect("url host") as u16; + + let (mut ctrl_app, ctrl_info) = app_with_screen_bytes(line.as_bytes()); + install_test_link_handler(&mut ctrl_app); + ctrl_app.handle_mouse(modified_mouse( + MouseEventKind::Down(MouseButton::Left), + ctrl_info.inner_rect.x + col, + ctrl_info.inner_rect.y, + KeyModifiers::CONTROL, + )); + + let ctrl_log = ctrl_app + .state + .plugin_command_logs + .last() + .expect("ctrl-click should start plugin link handler"); + assert_eq!(ctrl_log.plugin_id, "example.links"); + assert_eq!(ctrl_log.action_id.as_deref(), Some("open")); + + let (mut super_app, super_info) = app_with_screen_bytes(line.as_bytes()); + install_test_link_handler(&mut super_app); + super_app.handle_mouse(modified_mouse( + MouseEventKind::Down(MouseButton::Left), + super_info.inner_rect.x + col, + super_info.inner_rect.y, + KeyModifiers::SUPER, + )); + + assert!(super_app.state.plugin_command_logs.is_empty()); + } + #[tokio::test] async fn pane_cell_url_resolver_finds_visible_url() { let line = "see https://example.com/pr/307."; diff --git a/src/raw_input.rs b/src/raw_input.rs index fddcb3fd..346da476 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -818,6 +818,27 @@ mod tests { assert_eq!(mouse.kind, MouseEventKind::Down(MouseButton::Left)); assert_eq!(mouse.column, 19); assert_eq!(mouse.row, 9); + assert_eq!(mouse.modifiers, KeyModifiers::empty()); + } + + #[test] + fn parses_sgr_mouse_observable_modifiers() { + let cases = [ + (b"\x1b[<8;20;10M".as_slice(), KeyModifiers::ALT), + (b"\x1b[<16;20;10M".as_slice(), KeyModifiers::CONTROL), + ( + b"\x1b[<24;20;10M".as_slice(), + KeyModifiers::ALT | KeyModifiers::CONTROL, + ), + ]; + + for (input, expected) in cases { + let (RawInputEvent::Mouse(mouse), _) = extract_one_event(input).unwrap() else { + panic!("expected mouse"); + }; + assert_eq!(mouse.modifiers, expected); + assert!(!mouse.modifiers.contains(KeyModifiers::SUPER)); + } } #[test] |
