From 7aeca8adf74fbd2cbfa597877f79578e8bfdad74 Mon Sep 17 00:00:00 2001
From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Date: Sun, 26 Jul 2026 18:33:36 +0800
Subject: [PATCH] fix(links): open links on Ctrl+click on Windows and Linux
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The link modifier was `mods.platform`, which gpui maps to ⌘ on macOS but
to the Win/Super key elsewhere — a key the OS mostly swallows, so on
Windows and Linux neither the hover underline nor click-to-open could be
triggered at all. `config.json`'s own docs already promised "⌘/Ctrl-click";
this is the implementation catching up.
Use `Modifiers::secondary()` (⌘ on macOS, Ctrl elsewhere) at all three
sites: the click handler, the hover probe, and the app-level modifier
tracking that pushes the state down to background panes. Not
`platform || control` — that would steal ⌃-click on macOS, where it means
"right click".
Ctrl+click still falls through to mouse-tracking TUIs when there's no
link under the cursor, since `open_link_at` reports whether it consumed
the click.
Fixes #183
---
docs/features.md | 4 ++--
src/terminal/element.rs | 9 ++++++---
src/ui/hints.rs | 5 +++--
src/ui/settings.rs | 21 ++++++++++++++++-----
4 files changed, 27 insertions(+), 12 deletions(-)
diff --git a/docs/features.md b/docs/features.md
index 625cb074..4249caf5 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -17,7 +17,7 @@
- **Tabs & splits** — always open in the current directory
- **Repo-grouped sidebar** — the left tab sidebar groups rows under a header per git repository, non-repo tabs in a trailing *Scratch* section; branch switches and in-repo `cd`s never move a row (`sidebar_grouping` in `config.json`: `repo` default, `none` for a flat list)
- **Command palette** ⌘ P · scrollback search ⌘ F
-- **⌘-click links** · desktop notifications · copy on select (opt-in, Settings → Terminal → Clipboard)
+- **⌘/Ctrl-click links** (⌘ on macOS, Ctrl on Windows/Linux) · desktop notifications · copy on select (opt-in, Settings → Terminal → Clipboard)
- **Smart double-click selection** — double-click grabs the whole URL, file path, bracket/quote pair, or dictionary-segmented CJK word under the cursor; Shift-click extends a selection (toggle in Settings → Terminal → Mouse; word separators via `word_separators` in `config.json`)
- **Eight themes, plus your own** — YAML seed themes with solid, gradient, or image backgrounds; iTerm2 `.itermcolors` import; in-app color editor with a background-image picker
- **Sync with system** — Settings → Appearance; pick separate light and dark themes and tty7 follows the OS appearance live (`theme_follow_system`, `theme_preset_light` / `theme_preset_dark` in `config.json`)
@@ -48,7 +48,7 @@ and SFTP without shelling out to `ssh`. There is no system-ssh compat mode.
- **`~/.ssh/config` aliases** — type one to connect (resolved natively — common fields, best-effort — over russh), or import them as profiles in Settings
- **GUI auth** — in-pane sheets for password, key passphrase, 2FA, and host-key confirmation (new vs. changed)
- **Built-in SFTP** — a slide-in file panel: browse, upload / download, rename / delete / chmod, drag to Finder
-- **Port forwarding** — Local / Remote / Dynamic, preconfigured or added live, plus ⌘-click `localhost:PORT` to auto-forward
+- **Port forwarding** — Local / Remote / Dynamic, preconfigured or added live, plus ⌘/Ctrl-click `localhost:PORT` to auto-forward
- **Jump hosts & proxies** — multi-hop via profile references or `ProxyJump`, ProxyCommand, SOCKS5 / HTTP
| Entry point | Connects via |
diff --git a/src/terminal/element.rs b/src/terminal/element.rs
index d54fe708..6d8e173d 100644
--- a/src/terminal/element.rs
+++ b/src/terminal/element.rs
@@ -1189,8 +1189,11 @@ impl TerminalElement {
let button = ev.button;
let clicks = ev.click_count;
view.update(cx, |v, cx| {
- // Cmd+click opens a URL under the cursor.
- let link_modifier = mods.platform || v.link_modifier_down();
+ // Secondary+click (⌘ on macOS, Ctrl on Windows/Linux) opens a
+ // URL under the cursor. Not the raw platform key: that's Win/
+ // Super off macOS, which the OS mostly swallows — and every
+ // other terminal there opens links on Ctrl+click.
+ let link_modifier = mods.secondary() || v.link_modifier_down();
if link_modifier && button == MouseButton::Left && v.open_link_at(col, row, cx) {
return;
}
@@ -1244,7 +1247,7 @@ impl TerminalElement {
if !mods.shift {
v.mouse_motion(col, row, &mods);
}
- let include_files = mods.platform || v.link_modifier_down();
+ let include_files = mods.secondary() || v.link_modifier_down();
v.hover_link_at(col, row, include_files, cx);
} else {
v.clear_hovered_link(cx);
diff --git a/src/ui/hints.rs b/src/ui/hints.rs
index ca86f68e..030c5bda 100644
--- a/src/ui/hints.rs
+++ b/src/ui/hints.rs
@@ -44,7 +44,7 @@ impl Tty7App {
cx: &mut Context,
) {
let m = &ev.modifiers;
- self.set_link_modifier(m.platform, cx);
+ self.set_link_modifier(m.secondary(), cx);
// Mirror `on_key_down`'s chord test: reject the other platform-ish key
// (⌃ on macOS, Win/Super elsewhere), Alt, and Shift, so only the bare
@@ -79,7 +79,8 @@ impl Tty7App {
.detach();
}
- /// Push the platform-modifier state down to every pane's link tracking.
+ /// Push the secondary-modifier state (⌘ on macOS, Ctrl elsewhere — the
+ /// same key that opens a link on click) down to every pane's link tracking.
/// Every tab, not just the active one: `on_modifiers_changed` only fires on
/// the frontmost window state, so a background tab that saw "⌘ down" but
/// never the matching release would keep a stale `true` — and a stale
diff --git a/src/ui/settings.rs b/src/ui/settings.rs
index 7c4c8e17..76e3aad5 100644
--- a/src/ui/settings.rs
+++ b/src/ui/settings.rs
@@ -467,7 +467,7 @@ pub(crate) struct SettingsState {
pub(crate) shell_args_input: Entity,
/// Custom working-directory path (used when the strategy is `Custom`).
pub(crate) wd_path_input: Entity,
- /// Command template run when ⌘-clicking a file link (Links section). Empty
+ /// Command template run when ⌘/Ctrl-clicking a file link (Links section). Empty
/// clears the override, restoring the built-in "open in default app".
pub(crate) link_file_command_input: Entity,
/// Mouse-scroll multiplier slider (Terminal section).
@@ -622,6 +622,14 @@ pub(crate) struct Recording {
/// unlikely real font name.
pub(crate) const FONT_DEFAULT_LABEL: &str = "Default (match primary)";
+/// How the link-click modifier is spelled in the Links copy. It's gpui's
+/// `secondary` (see `Modifiers::secondary`), so it must read ⌘ on macOS and
+/// Ctrl on Windows/Linux — same split `key_tokens` uses for keycaps.
+#[cfg(target_os = "macos")]
+const LINK_MODIFIER_LABEL: &str = "⌘";
+#[cfg(not(target_os = "macos"))]
+const LINK_MODIFIER_LABEL: &str = "Ctrl";
+
/// Humanize a CamelCase action name for display: "CloseActiveTab" → "Close
/// Active Tab".
pub(crate) fn humanize_action(action: &str) -> String {
@@ -3093,7 +3101,7 @@ impl Tty7App {
.child(self.section_header("Links", cx))
.child(self.settings_row(
"Detect URLs",
- "Underline links on hover and open them on ⌘-click.",
+ format!("Underline links on hover and open them on {LINK_MODIFIER_LABEL}-click."),
link_switch,
cx,
))
@@ -3105,9 +3113,12 @@ impl Tty7App {
))
.child(self.settings_row(
"Open files with",
- "Command run when ⌘-clicking a file link, instead of the default app. \
- Use {path}, {line}, {column}; a flag whose value is absent is dropped \
- (e.g. herdr edit {path} --line={line}). Empty uses the default app.",
+ format!(
+ "Command run when {LINK_MODIFIER_LABEL}-clicking a file link, instead of \
+ the default app. Use {{path}}, {{line}}, {{column}}; a flag whose value \
+ is absent is dropped (e.g. herdr edit {{path}} --line={{line}}). Empty \
+ uses the default app."
+ ),
link_file_command_control,
cx,
))