From 6e25a15bd583b37c45ff119815913c2df8157510 Mon Sep 17 00:00:00 2001 From: otomist Date: Thu, 7 Aug 2025 18:39:02 -0400 Subject: [PATCH 1/4] color link tags --- content_scripts/link_hints.js | 46 +++++++++++++++++++++++++++++++++++ content_scripts/vimium.css | 22 +++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/content_scripts/link_hints.js b/content_scripts/link_hints.js index e30a9abac..ceaa0ffe6 100644 --- a/content_scripts/link_hints.js +++ b/content_scripts/link_hints.js @@ -484,6 +484,52 @@ class LinkHintsMode { // Note that Vimium's CSS is user-customizable. We're adding the "vimiumHintMarker" class here // for users to customize. See further comments about this in vimium.css. el.className = "vimium-reset internal-vimium-hint-marker vimiumHintMarker"; + + // Classify the element so we can style different hint types differently (buttons, links, + // external links). We only attach these classes for local markers actually rendered here. + try { + const target = localHint.element; + if (target) { + const tag = target.tagName?.toLowerCase?.() || ""; + const role = target.getAttribute?.("role")?.toLowerCase?.(); + let typeClass = null; + + // Helper to decide if an is external (different domain). + const classifyAnchor = () => { + if (!target.href) return "vimium-hint-type-link"; // No href => treat as normal link. + let linkHostname; + try { + linkHostname = new URL(target.href, document.baseURI).hostname || ""; + } catch (_) { + linkHostname = ""; // Malformed; treat as same-domain link. + } + const norm = (h) => h.replace(/^www\./, ""); + if (linkHostname && norm(linkHostname) && norm(linkHostname) !== norm(location.hostname || "")) { + return "vimium-hint-type-external"; + } + return "vimium-hint-type-link"; + }; + + if ( + tag === "button" || + (tag === "input" && ["button", "submit", "reset"].includes((target.getAttribute("type") || "").toLowerCase())) || + role === "button" + ) { + typeClass = "vimium-hint-type-button"; + } else if (tag === "a" || role === "link") { + typeClass = classifyAnchor(); + } else if (target.href && tag !== "area") { // Generic clickable with href (e.g.
+ typeClass = classifyAnchor(); + } + + // Apply class if determined. + if (typeClass) { + el.classList.add(typeClass); + } + } + } catch (_) { + // Swallow any classification errors; hint rendering must not break. + } Object.assign(marker, { element: el, localHint, diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css index 9182d7255..8e1c0bb86 100644 --- a/content_scripts/vimium.css +++ b/content_scripts/vimium.css @@ -118,6 +118,28 @@ div.internal-vimium-hint-marker { z-index: 2147483647; } +/* Hint type variants: these classes are added dynamically in link_hints.js. + * We keep them additive so user-defined CSS can override them easily. */ +.internal-vimium-hint-marker.vimium-hint-type-button { + background: linear-gradient(to bottom, #d0ebff 0%, #74c0fc 100%); + border-color: #339af0; +} +.internal-vimium-hint-marker.vimium-hint-type-link { + background: linear-gradient(to bottom, #e6ffe6 0%, #9bde83 100%); + border-color: #4caf50; +} +.internal-vimium-hint-marker.vimium-hint-type-external { + background: linear-gradient(to bottom, #ffe6f1 0%, #ff87b5 100%); + border-color: #ff3d7f; +} + +/* Active state tweaks keep foreground legible */ +.internal-vimium-hint-marker.vimium-hint-type-external span, +.internal-vimium-hint-marker.vimium-hint-type-button span, +.internal-vimium-hint-marker.vimium-hint-type-link span { + color: #302505; +} + div.internal-vimium-hint-marker span { color: #302505; font-family: Helvetica, Arial, sans-serif; From 70ad79c18b51460329f63997bb8e7b4149f95014 Mon Sep 17 00:00:00 2001 From: otomist Date: Sat, 9 Aug 2025 18:28:04 -0400 Subject: [PATCH 2/4] key binding for copying from a tag --- background_scripts/all_commands.js | 22 +++++++++ background_scripts/commands.js | 5 ++ content_scripts/link_hints.js | 79 +++++++++++++++++++++++++++++- content_scripts/mode_normal.js | 4 ++ 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/background_scripts/all_commands.js b/background_scripts/all_commands.js index 5abab2a16..587d23708 100644 --- a/background_scripts/all_commands.js +++ b/background_scripts/all_commands.js @@ -229,6 +229,28 @@ const allCommands = [ advanced: true, }, + { + name: "LinkHints.activateModeToCopyCodeBlock", + desc: "Copy a code block's text to the clipboard", + group: "navigation", + advanced: true, + noRepeat: true, + }, + { + name: "LinkHints.activateModeToCopyParagraph", + desc: "Copy a paragraph's text to the clipboard", + group: "navigation", + advanced: true, + noRepeat: true, + }, + { + name: "LinkHints.activateModeToCopySpan", + desc: "Copy a span's text to the clipboard", + group: "navigation", + advanced: true, + noRepeat: true, + }, + { name: "goPrevious", desc: "Follow the link labeled previous or <", diff --git a/background_scripts/commands.js b/background_scripts/commands.js index 11673f22a..2f8d067ae 100644 --- a/background_scripts/commands.js +++ b/background_scripts/commands.js @@ -431,6 +431,11 @@ const defaultKeyMappings = { "i": "enterInsertMode", "v": "enterVisualMode", "V": "enterVisualLineMode", + + // Copy element text (prefix c): cc (code), cp (paragraph), cs (span) + "cc": "LinkHints.activateModeToCopyCodeBlock", + "cp": "LinkHints.activateModeToCopyParagraph", + "cs": "LinkHints.activateModeToCopySpan", // Link hints "f": "LinkHints.activateMode", diff --git a/content_scripts/link_hints.js b/content_scripts/link_hints.js index fa5478e23..a5ae908ed 100644 --- a/content_scripts/link_hints.js +++ b/content_scripts/link_hints.js @@ -146,6 +146,47 @@ const FOCUS_LINK = { }, }; +// Generic copy modes (selector-based). Each provides customSelectors array for hint gathering. +function copyElementTextActivator(label) { + return function (el) { + if (!el) return; + let text = el.innerText || el.textContent || ""; + if (!text.trim()) { + HUD.show(`No ${label} text to yank.`, 1500); + return; + } + HUD.copyToClipboard(text); + const preview = text.replace(/\s+/g, " ").trim().slice(0, 40); + HUD.show(`Yanked ${label}: ${preview}${text.length > 40 ? "..." : ""}`, 2000); + }; +} + +const COPY_CODE_BLOCK = { + name: "code", + indicator: "Copy code block", + customSelectors: [ + "pre", + "code", + ".hljs", + "div.highlight pre", + "div.highlight code", + "[data-code-block]", + ], + linkActivator: copyElementTextActivator("code"), +}; +const COPY_PARAGRAPH_BLOCK = { + name: "para", + indicator: "Copy paragraph", + customSelectors: ["p"], + linkActivator: copyElementTextActivator("paragraph"), +}; +const COPY_SPAN_BLOCK = { + name: "span", + indicator: "Copy span", + customSelectors: ["span"], + linkActivator: copyElementTextActivator("span"), +}; + const availableModes = [ OPEN_IN_CURRENT_TAB, OPEN_IN_NEW_BG_TAB, @@ -157,6 +198,9 @@ const availableModes = [ COPY_LINK_TEXT, HOVER_LINK, FOCUS_LINK, + COPY_CODE_BLOCK, + COPY_PARAGRAPH_BLOCK, + COPY_SPAN_BLOCK, ]; const HintCoordinator = { @@ -222,14 +266,19 @@ const HintCoordinator = { getHintDescriptors({ modeIndex, requestedByHelpDialog }, _sender) { if (!DomUtils.isReady() || DomUtils.windowIsTooSmall()) return []; - const requireHref = [COPY_LINK_URL, OPEN_INCOGNITO].includes(availableModes[modeIndex]); + const mode = availableModes[modeIndex]; + const requireHref = [COPY_LINK_URL, OPEN_INCOGNITO].includes(mode); // If link hints is launched within the help dialog, then we only offer hints from that frame. // This improves the usability of the help dialog on the options page (particularly for // selecting command names). if (requestedByHelpDialog && !globalThis.isVimiumHelpDialog) { this.localHints = []; } else { - this.localHints = LocalHints.getLocalHints(requireHref); + if (mode.customSelectors) { + this.localHints = LocalHints.getHintsForSelectors(mode.customSelectors); + } else { + this.localHints = LocalHints.getLocalHints(requireHref); + } } this.localHintDescriptors = this.localHints.map(({ linkText }, localIndex) => ( new HintDescriptor({ @@ -1594,4 +1643,30 @@ Object.assign(globalThis, { AlphabetHints, FilterHints, WaitForEnter, + COPY_CODE_BLOCK, + COPY_PARAGRAPH_BLOCK, + COPY_SPAN_BLOCK, }); +// Generic selector-based helper for custom copy modes. +LocalHints.getHintsForSelectors = function (selectors) { + if (!document.documentElement) return []; + let elements = []; + try { + elements = selectors.flatMap((sel) => Array.from(document.querySelectorAll(sel))); + } catch (_) { + return []; + } + elements = elements.filter((el) => !elements.some((other) => (other !== el) && other.contains(el))); + const hints = []; + for (const el of elements) { + const rect = DomUtils.getVisibleClientRect(el, true); + if (!rect) continue; + if (rect.width < 10 || rect.height < 10) continue; + let text = (el.innerText || el.textContent || "").trim(); + if (!text) continue; + let caption = text.split(/\n/)[0]; + if (caption.length > 80) caption = caption.slice(0, 77) + "..."; + hints.push(new LocalHint({ element: el, rect, linkText: caption, showLinkText: true })); + } + return hints; +}; diff --git a/content_scripts/mode_normal.js b/content_scripts/mode_normal.js index eaa649c3f..d33515bcb 100644 --- a/content_scripts/mode_normal.js +++ b/content_scripts/mode_normal.js @@ -353,6 +353,10 @@ const NormalModeCommands = { "LinkHints.activateModeToOpenIncognito": LinkHints.activateModeToOpenIncognito.bind(LinkHints), "LinkHints.activateModeToDownloadLink": LinkHints.activateModeToDownloadLink.bind(LinkHints), "LinkHints.activateModeToCopyLinkUrl": LinkHints.activateModeToCopyLinkUrl.bind(LinkHints), + // Custom: copy element text via hinting (prefix 'c'). + "LinkHints.activateModeToCopyCodeBlock": function () { HintCoordinator.prepareToActivateMode(COPY_CODE_BLOCK, function () {}); }, + "LinkHints.activateModeToCopyParagraph": function () { HintCoordinator.prepareToActivateMode(COPY_PARAGRAPH_BLOCK, function () {}); }, + "LinkHints.activateModeToCopySpan": function () { HintCoordinator.prepareToActivateMode(COPY_SPAN_BLOCK, function () {}); }, "Vomnibar.activate": Vomnibar.activate.bind(Vomnibar), "Vomnibar.activateInNewTab": Vomnibar.activateInNewTab.bind(Vomnibar), From 8c5f5ac69c5ec920a66d890945311f82c9e1c368 Mon Sep 17 00:00:00 2001 From: otomist Date: Sat, 9 Aug 2025 18:35:28 -0400 Subject: [PATCH 3/4] copy a tags and copy and with c* --- background_scripts/all_commands.js | 14 ++++++++++++++ background_scripts/commands.js | 6 ++++-- content_scripts/link_hints.js | 27 +++++++++++++++++++++++++++ content_scripts/mode_normal.js | 2 ++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/background_scripts/all_commands.js b/background_scripts/all_commands.js index 587d23708..52967633e 100644 --- a/background_scripts/all_commands.js +++ b/background_scripts/all_commands.js @@ -250,6 +250,20 @@ const allCommands = [ advanced: true, noRepeat: true, }, + { + name: "LinkHints.activateModeToCopyAnchor", + desc: "Copy an anchor's text to the clipboard", + group: "navigation", + advanced: true, + noRepeat: true, + }, + { + name: "LinkHints.activateModeToCopyAny", + desc: "Copy text from various element types (a, p, span, code, pre)", + group: "navigation", + advanced: true, + noRepeat: true, + }, { name: "goPrevious", diff --git a/background_scripts/commands.js b/background_scripts/commands.js index 2f8d067ae..0fb681422 100644 --- a/background_scripts/commands.js +++ b/background_scripts/commands.js @@ -431,11 +431,13 @@ const defaultKeyMappings = { "i": "enterInsertMode", "v": "enterVisualMode", "V": "enterVisualLineMode", - - // Copy element text (prefix c): cc (code), cp (paragraph), cs (span) + + // Copy element text (prefix c): cc (code), cp (paragraph), cs (span), ca (anchor), c* (any) "cc": "LinkHints.activateModeToCopyCodeBlock", "cp": "LinkHints.activateModeToCopyParagraph", "cs": "LinkHints.activateModeToCopySpan", + "ca": "LinkHints.activateModeToCopyAnchor", + "c*": "LinkHints.activateModeToCopyAny", // Link hints "f": "LinkHints.activateMode", diff --git a/content_scripts/link_hints.js b/content_scripts/link_hints.js index a5ae908ed..dd250a096 100644 --- a/content_scripts/link_hints.js +++ b/content_scripts/link_hints.js @@ -186,6 +186,29 @@ const COPY_SPAN_BLOCK = { customSelectors: ["span"], linkActivator: copyElementTextActivator("span"), }; +const COPY_ANCHOR_BLOCK = { + name: "alink", + indicator: "Copy link text", + customSelectors: ["a"], + linkActivator: copyElementTextActivator("link"), +}; +const COPY_ANY_BLOCK = { + name: "any", + indicator: "Copy element", + // Union of existing selector groups (anchor, span, paragraph, code family). + customSelectors: [ + "a", + "span", + "p", + "pre", + "code", + ".hljs", + "div.highlight pre", + "div.highlight code", + "[data-code-block]", + ], + linkActivator: copyElementTextActivator("element"), +}; const availableModes = [ OPEN_IN_CURRENT_TAB, @@ -201,6 +224,8 @@ const availableModes = [ COPY_CODE_BLOCK, COPY_PARAGRAPH_BLOCK, COPY_SPAN_BLOCK, + COPY_ANCHOR_BLOCK, + COPY_ANY_BLOCK, ]; const HintCoordinator = { @@ -1646,6 +1671,8 @@ Object.assign(globalThis, { COPY_CODE_BLOCK, COPY_PARAGRAPH_BLOCK, COPY_SPAN_BLOCK, + COPY_ANCHOR_BLOCK, + COPY_ANY_BLOCK, }); // Generic selector-based helper for custom copy modes. LocalHints.getHintsForSelectors = function (selectors) { diff --git a/content_scripts/mode_normal.js b/content_scripts/mode_normal.js index d33515bcb..a955d8804 100644 --- a/content_scripts/mode_normal.js +++ b/content_scripts/mode_normal.js @@ -357,6 +357,8 @@ const NormalModeCommands = { "LinkHints.activateModeToCopyCodeBlock": function () { HintCoordinator.prepareToActivateMode(COPY_CODE_BLOCK, function () {}); }, "LinkHints.activateModeToCopyParagraph": function () { HintCoordinator.prepareToActivateMode(COPY_PARAGRAPH_BLOCK, function () {}); }, "LinkHints.activateModeToCopySpan": function () { HintCoordinator.prepareToActivateMode(COPY_SPAN_BLOCK, function () {}); }, + "LinkHints.activateModeToCopyAnchor": function () { HintCoordinator.prepareToActivateMode(COPY_ANCHOR_BLOCK, function () {}); }, + "LinkHints.activateModeToCopyAny": function () { HintCoordinator.prepareToActivateMode(COPY_ANY_BLOCK, function () {}); }, "Vomnibar.activate": Vomnibar.activate.bind(Vomnibar), "Vomnibar.activateInNewTab": Vomnibar.activateInNewTab.bind(Vomnibar), From 0d654f9f67402a8013c85fba60cccaf3e9313306 Mon Sep 17 00:00:00 2001 From: otomist Date: Sun, 10 Aug 2025 19:07:41 -0400 Subject: [PATCH 4/4] fix hint position fter scroll --- content_scripts/link_hints.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/content_scripts/link_hints.js b/content_scripts/link_hints.js index dd250a096..19523744c 100644 --- a/content_scripts/link_hints.js +++ b/content_scripts/link_hints.js @@ -1685,6 +1685,7 @@ LocalHints.getHintsForSelectors = function (selectors) { } elements = elements.filter((el) => !elements.some((other) => (other !== el) && other.contains(el))); const hints = []; + const { top: viewportTop, left: viewportLeft } = DomUtils.getViewportTopLeft(); for (const el of elements) { const rect = DomUtils.getVisibleClientRect(el, true); if (!rect) continue; @@ -1693,6 +1694,9 @@ LocalHints.getHintsForSelectors = function (selectors) { if (!text) continue; let caption = text.split(/\n/)[0]; if (caption.length > 80) caption = caption.slice(0, 77) + "..."; + // Adjust to document coordinates (LinkHints expects rect relative to full document, not viewport). + rect.top += viewportTop; + rect.left += viewportLeft; hints.push(new LocalHint({ element: el, rect, linkText: caption, showLinkText: true })); } return hints;