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

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

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

Level 0Extension 입덕
0 XP0/54 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 (또는 install conflict resolution) 가 아무것도 bind 안 함. Onboarding popup 에 일회성 hint surface 해서 user 가 re-bind page 찾을 수 있게.

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 발견성

Omnibox 가 진짜 power-user feature — 대부분 user 가 스스로 발견 안 함. 이런 데서 surface:

  • 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.

여러 저비용 trigger 가 하나의 완벽한 trigger 보다 더 많은 user 닿음. 키보드 / popup / context menu / omnibox — 다 몇 줄 들고 각자 다른 습관 잡음. 다 만들어; 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))}`,
    }))
  );
});

chrome.omnibox.onInputEntered.addListener(async (text, disposition) => {
  // 'text' 가 picked suggestion 의 `content` (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

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 지원, 렌더링은 다양. 상호작용 자체는 상관없이 동작.

Progress

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

댓글 0

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

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