본문 바로가기
C.W.K.
Stream
Lesson 04 of 05 · published

키보드 단축키와 omnibox — Power-user trigger

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

Level 0Extension 입덕
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Track 3가 키보드 단축키 하나 wire 했어. Lesson 4가 trigger set 넓힘 — 여러 command, chrome://extensions/shortcuts의 re-bind page, Chrome 주소창을 ClipDeck search input으로 바꾸는 omnibox keyword. 각각 manifest 다섯 줄짜리 power-user move."

chrome.commands 복습

Track 3 Lesson 6가 commands.save-clip 소개. 같은 mechanism이 stable channel에서 extension 당 user-bindable 단축키 4 개까지 scale:

  • 각 command를 manifest.commands에 unique key, suggested_key, description와 함께 선언.
  • SW에서 chrome.commands.onCommand.addListener((name) => ...)로 listen.
  • User가 chrome://extensions/shortcuts 통해 어느 거든 re-bind 가능 — Chrome이 모든 extension의 command 거기 surface.
  • 특별 _execute_action command가 popup 열기. _execute_side_panel이 SW listener 없이 panel 열기.

Bind 된 거 진단

chrome.commands.getAll()이 각각의 active key와 함께 현재 set 반환. onInstalled handler에서 유용 — Chrome이 suggested key를 silent drop 했는지 (다른 extension 이나 Chrome 자체가 이미 combo 소유 시 일어남) 알도록 command 로깅:

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

shortcut이 비어 있다면 user가 아무것도 안 걸었거나, 설치할 때 다른 것과 충돌해서 풀린 거야. 처음 안내하는 popup에서 한 번쯤 알려 줘. 어디 가서 다시 걸 수 있는지 찾을 수 있게.

Omnibox API

chrome.omnibox가 Chrome 주소창을 extension의 input으로 바꿈. User가 (manifest에 선언된) keyword 타이핑, space, query — SW가 매 keystroke 받고 suggestion 반환.

세 field와 세 event:

  • Manifest: "omnibox": { "keyword": "clip" }.
  • chrome.omnibox.onInputStarted — user가 방금 keyword + space 타이핑. 일회성 setup fire.
  • chrome.omnibox.onInputChanged(text, suggest) — keyword 뒤 매 keystroke 마다 fire. ~5 entry까지 suggest([{ content, description }]) 호출.
  • chrome.omnibox.onInputEntered(text, disposition) — user가 entry 선택했거나 Enter 누름. disposition이 새 tab / 현재 tab 원하는지 알려 줘.

ClipDeck 엔 obvious 플레이: 주소창에 clip space 타이핑, 다음 검색어. suggestion이 매칭 clip을 source title로 list. 하나 선택하면 source URL 새 tab에 열기.

Omnibox 발견성

주소창 키워드는 진짜 고수용 기능이야. user가 스스로 찾아내는 일은 거의 없어. 그러니 이런 자리에서 알려 줘:

  • Popup help link ("주소창에 clip 타이핑하면 저장된 clip 검색").
  • Options page (Track 6).
  • Ship 한다면 first-install onboarding tab.

Omnibox UI를 over-engineer 안 함 — suggestion 다섯 개, 짧은 title, detail 한 줄. User가 속도 위해 주소창 골랐으니 그걸 보존.

Trigger ladder

ClipDeck이 이제 full ladder 가짐:

  • 매 page의 floating 버튼 (Track 3 Lesson 4) — 가장 discoverable, 가장 intrusive.
  • Ctrl+Shift+K 키보드 단축키 (Track 3 Lesson 6) — repeat user에 가장 빠름.
  • Toolbar icon click → popup → Save (이 track Lesson 2) — middle ground.
  • Right-click selection → 'Save to ClipDeck' (이 track Lesson 3) — in-the-moment discoverability.
  • 검색용 omnibox clip ... (이 lesson) — power-user 회수.

각각 유지 비용 거의 0. 함께 모든 종류 user cover — power user, kbd-shy clicker, right-click 사람, omnibox 애호가. 각각 코드 몇 줄 세 받고 다른 audience 닿으니 다 ship.

값싼 통로 여러 개가 완벽한 통로 하나보다 더 많은 user 한테 닿아. 단축키, popup, 우클릭 메뉴, 주소창 키워드 — 하나같이 몇 줄이면 되고, 각자 다른 습관을 가진 사람을 잡아. 다 만들어 두고 고르는 건 user 한테 맡겨.
네 단축키 cap. Stable Chrome이 extension 당 user-bindable command 4 개 제한 강제. 더 필요하면 나머지를 user가 Karabiner, AutoHotkey 같은 거 통해 OS-level shortcut에 wire 할 suggestion으로 문서화. 다섯 등록 시도 안 함 — Chrome이 install 시 다섯 번째를 silent drop.

Code

manifest.json — custom command 셋 + 특별 _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 — 새 command wire 하고 active binding 로깅
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":
      // 이 track Lesson 5 의 구현.
      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 suggestion + selection handler·javascript
// background.js — omnibox: clip <query> 가 저장된 clip 검색
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))}`,
    }))
  );
});

async function resolveOmniboxUrl(text) {
  try {
    const url = new URL(text);
    if (["http:", "https:"].includes(url.protocol)) return url.href;
  } catch {}
  const query = text.trim().toLowerCase();
  const { clips = [] } = await chrome.storage.local.get("clips");
  const match = clips.find((c) =>
    c.text.toLowerCase().includes(query) ||
    (c.title || "").toLowerCase().includes(query));
  try {
    const url = new URL(match?.url || "");
    return ["http:", "https:"].includes(url.protocol) ? url.href : null;
  } catch {
    return null;
  }
}

chrome.omnibox.onInputEntered.addListener(async (text, disposition) => {
  const url = await resolveOmniboxUrl(text);
  if (!url) return;
  if (disposition === "newForegroundTab") {
    await chrome.tabs.create({ url, active: true });
  } else if (disposition === "newBackgroundTab") {
    await chrome.tabs.create({ url, active: false });
  } else {
    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
    if (tab?.id) await chrome.tabs.update(tab.id, { url });
  }
});

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

External links

Exercise

clipdeck/manifest.json의 commands update 하고 첫 번째 code block의 omnibox block 추가. clipdeck/background.js의 command listener를 두 번째 code block으로 교체. 세 번째 code block (omnibox handler)을 background.js에 추가. Reload. chrome://extensions/shortcuts 열고 4 ClipDeck command가 suggested key와 함께 나타나는지 확인 — conflict 보이면 하나 rebind. 각각 테스트: Ctrl+Shift+K가 clip 저장. Ctrl+Shift+P가 panel 열기. Ctrl+Shift+M이 SW console에 toggle-pause request 로깅. Ctrl+Shift+L이 popup 열기. 다음 주소창 click, clip space 타이핑, 저장한 clip 매칭하는 검색어 — suggestion 나타나야 함. 하나에 Enter 눌러 source URL 열기.
Hint
Shortcut이 chrome://extensions/shortcuts page에서 unbound 보이면, suggested combo가 다른 extension 이나 Chrome 자체 binding과 conflict — 다른 거 선택. Omnibox keyword가 user의 정확한 타이핑 문자열에 매칭. clip 이 suggestion trigger 안 하면 manifest entry가 "omnibox": { "keyword": "clip" } 인지 확인하고 extension reload. Suggestion 나타나는데 <match> 태그가 bold 안 되고 literal 렌더되면, 일부 Chrome 버전에선 정상 — description이 작은 XML formatting subset 지원, 렌더링은 다양. 상호작용 자체는 상관없이 동작.Raw query에서 Enter를 누르면 suggestion URL이 온다는 보장이 없어. Handler가 matching clip을 resolve하고 http(s)를 검증해야 해. Current, foreground, background disposition을 다 시험해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.