C.W.K.
Stream
Lesson 04 of 05 · published

Keyboard Shortcuts and the Omnibox — Power-User Triggers

~12 min · commands, omnibox, keyboard, power-user

Level 0Extension Curious
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Track 3 wired one keyboard shortcut. Lesson 4 broadens the trigger set — multiple commands, the chrome://extensions/shortcuts re-binding page, and the omnibox keyword that turns Chrome's address bar into a ClipDeck search input. Power-user moves that take five lines of manifest each."

chrome.commands Recap

Track 3 Lesson 6 introduced commands.save-clip. The same mechanism scales to up to four user-bindable shortcuts per extension in the stable channel:

  • Declare each command in manifest.commands with a unique key, a suggested_key, and a description.
  • Listen with chrome.commands.onCommand.addListener((name) => ...) in the SW.
  • Users can re-bind any of them via chrome://extensions/shortcuts — Chrome surfaces every extension's commands there.
  • The special _execute_action command opens the popup; _execute_side_panel opens the panel without an SW listener.

Diagnosing What's Bound

chrome.commands.getAll() returns the current set with the active key for each. Useful in an onInstalled handler — log the commands so you know whether Chrome silently dropped your suggested key (it happens when another extension or Chrome itself already owns the combo):

const cmds = await chrome.commands.getAll();
for (const c of cmds) console.log(c.name, '→', c.shortcut || '(unbound)');

If shortcut is empty, the user (or the install conflict resolution) didn't bind anything. Surface a one-time hint in the onboarding popup so users can find the re-bind page.

The Omnibox API

chrome.omnibox turns Chrome's address bar into your extension's input. The user types a keyword (declared in the manifest), a space, then their query — your SW receives every keystroke and returns suggestions.

Three fields and three events:

  • Manifest: "omnibox": { "keyword": "clip" }.
  • chrome.omnibox.onInputStarted — user just typed the keyword + space; fire one-time setup.
  • chrome.omnibox.onInputChanged(text, suggest) — fired on every keystroke after the keyword. Call suggest([{ content, description }]) with up to ~5 entries.
  • chrome.omnibox.onInputEntered(text, disposition) — user picked an entry or hit Enter. disposition tells you whether they want a new tab, the current tab, etc.

For ClipDeck, the obvious play: type clip space in the address bar, then a search term; suggestions list matching clips by source title; selecting one opens the source URL in a new tab.

Discoverability of Omnibox

Omnibox is genuinely a power-user feature — most users will not discover it on their own. Surface it in:

  • The popup help link ("Type clip in the address bar to search saved clips").
  • The options page (Track 6).
  • A first-install onboarding tab if you ship one.

Don't over-engineer the omnibox UI — five suggestions, short titles, one detail line. The user picked their address bar for speed; preserve it.

The Trigger Ladder

ClipDeck now has the full ladder:

  • Floating button on every page (Track 3 Lesson 4) — most discoverable, most intrusive.
  • Keyboard shortcut Ctrl+Shift+K (Track 3 Lesson 6) — fastest for repeat users.
  • Toolbar icon click → popup → Save (this track Lesson 2) — middle ground.
  • Right-click selection → 'Save to ClipDeck' (this track Lesson 3) — in-the-moment discoverability.
  • Omnibox clip ... for search (this lesson) — power-user retrieval.

Each costs almost nothing to maintain. Together they cover every type of user — the power user, the kbd-shy clicker, the right-click person, the omnibox enthusiast. You ship them all because they each tax you a few lines of code and reach a different audience.

Multiple low-cost triggers reach more users than one perfect trigger. Keyboard, popup, context menu, omnibox — they all cost a few lines and each catches a different habit. Build them all; let the user pick.
The four-shortcut cap. Stable Chrome enforces a four-user-bindable-command limit per extension. If you need more, document the rest as suggestions the user can wire to OS-level shortcuts via Karabiner, AutoHotkey, or similar. Don't try to register five — Chrome will silently drop the fifth at install.

Code

manifest.json — three custom commands + special _execute_action + omnibox keyword·json
{
  "commands": {
    "save-clip": {
      "suggested_key": { "default": "Ctrl+Shift+K", "mac": "Command+Shift+K" },
      "description": "Save the current text selection to ClipDeck"
    },
    "open-panel": {
      "suggested_key": { "default": "Ctrl+Shift+P", "mac": "Command+Shift+P" },
      "description": "Open the ClipDeck side panel"
    },
    "toggle-pause": {
      "suggested_key": { "default": "Ctrl+Shift+M", "mac": "Command+Shift+M" },
      "description": "Pause or resume ClipDeck capture on this tab"
    },
    "_execute_action": {
      "suggested_key": { "default": "Ctrl+Shift+L", "mac": "Command+Shift+L" },
      "description": "Open the ClipDeck popup"
    }
  },
  "omnibox": {
    "keyword": "clip"
  }
}
background.js — multi-command handler + onInstalled binding log·javascript
// background.js — wire the new commands and log the active bindings
chrome.commands.onCommand.addListener(async (command) => {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  if (!tab?.id) return;
  switch (command) {
    case "save-clip":
      try {
        await chrome.tabs.sendMessage(tab.id, { type: "captureSelection" });
      } catch { /* restricted page */ }
      return;
    case "open-panel":
      await chrome.sidePanel.open({ tabId: tab.id });
      return;
    case "toggle-pause":
      // Implementation in Lesson 5 of this track.
      console.log("[ClipDeck SW] toggle pause requested for tab", tab.id);
      return;
  }
});

chrome.runtime.onInstalled.addListener(async () => {
  const cmds = await chrome.commands.getAll();
  for (const c of cmds) {
    console.log("[ClipDeck SW] command", c.name, "→", c.shortcut || "(unbound)");
  }
});
background.js — omnibox suggestions + selection handler·javascript
// background.js — omnibox: clip <query> searches saved clips
chrome.omnibox.setDefaultSuggestion({
  description: "Search your ClipDeck clips — type to filter by title or content",
});

chrome.omnibox.onInputChanged.addListener(async (text, suggest) => {
  const query = text.trim().toLowerCase();
  if (!query) return;
  const { clips = [] } = await chrome.storage.local.get("clips");
  const matches = clips
    .filter((c) =>
      c.text.toLowerCase().includes(query) ||
      (c.title || "").toLowerCase().includes(query))
    .slice(0, 5);
  suggest(
    matches.map((c) => ({
      content: c.url,
      description: `<match>${escapeXml(c.title || c.url)}</match> — ${escapeXml(c.text.slice(0, 80))}`,
    }))
  );
});

chrome.omnibox.onInputEntered.addListener(async (text, disposition) => {
  // 'text' will be the picked suggestion's `content` (the URL).
  if (disposition === "newForegroundTab") {
    await chrome.tabs.create({ url: text });
  } else if (disposition === "currentTab") {
    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
    if (tab?.id) await chrome.tabs.update(tab.id, { url: text });
  }
});

function escapeXml(s) {
  return String(s).replace(/[&<>]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[ch]);
}

External links

Exercise

Update clipdeck/manifest.json's commands and add the omnibox block from the first code block. Replace clipdeck/background.js's command listener with the second code block. Add the third code block (omnibox handlers) to background.js. Reload. Open chrome://extensions/shortcuts and confirm all four ClipDeck commands appear with their suggested keys — rebind one if a conflict shows. Test each: Ctrl+Shift+K saves a clip; Ctrl+Shift+P opens the panel; Ctrl+Shift+M logs a toggle-pause request to the SW console; Ctrl+Shift+L opens the popup. Then click into the address bar, type clip space, then a search term that matches a clip you've saved — suggestions should appear; press Enter on one to open the source URL.
Hint
If a shortcut shows as unbound on the chrome://extensions/shortcuts page, your suggested combo conflicts with another extension or with Chrome's own bindings — pick a different one. The omnibox keyword is matched on the user's exact typed string; if clip does not trigger suggestions, confirm the manifest entry is "omnibox": { "keyword": "clip" } and reload the extension. If suggestions appear but the <match> tags render literally instead of bolding, that's normal in some Chrome versions — the description supports a small subset of XML formatting, but rendering varies. The interaction itself works regardless.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.