// ==UserScript==
// @name DeepSeek ShortCuts
// @name:zh-CN DeepSeek 快捷键
// @name:zh-TW DeepSeek 快捷鍵
// @description Keyboard Shortcuts For DeepSeek | Support Custom Shortcut Keys
// @description:zh-CN 为DeepSeek提供快捷键支持 | 支持自定义快捷键
// @description:zh-TW 為DeepSeek提供快捷鍵支援 | 支援自定義快捷鍵
// @version 1.5.0
// @icon https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/DeepSeekShortcutsIcon.svg
// @author 念柚
// @namespace https://github.com/MiPoNianYou/UserScripts
// @supportURL https://github.com/MiPoNianYou/UserScripts/issues
// @license GPL-3.0
// @match https://chat.deepseek.com/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function () {
"use strict";
const UI_SETTINGS = {
FONT_STACK: "-apple-system, BlinkMacSystemFont, system-ui, sans-serif",
ANIMATION_DURATION_MS: 350,
ANIMATION_EASING_POP_OUT: "cubic-bezier(0.34, 1.56, 0.64, 1)",
ANIMATION_EASING_STANDARD_INTERACTIVE: "cubic-bezier(0, 0, 0.58, 1)",
BREATHING_ANIMATION_DURATION: "2.2s",
DEBOUNCE_DELAY_MS: 150,
};
const STORAGE_KEYS = {
CUSTOM_SHORTCUTS_PREFIX: "dsk_custom_shortcuts_",
};
const ELEMENT_SELECTORS = {
REGENERATE_BUTTON: {
selector: ".ds-icon-button",
filterText: "#重新生成",
},
CONTINUE_BUTTON: {
selector: ".ds-button",
filterText: "继续生成",
},
STOP_GENERATING_BUTTON: {
selector: "._7436101",
position: "first",
},
LAST_COPY_BUTTON: {
parentSelector: "div._4f9bf79.d7dc56a8",
parentPosition: "last",
selector: "._965abe9 .ds-icon-button",
childPosition: "first",
},
LAST_EDIT_BUTTON: {
parentSelector: "._9663006",
parentPosition: "last",
selector: "._78e0558 .ds-icon-button",
childPosition: "last",
},
DEEP_THINK_MODE_BUTTON: {
selector: ".ds-button span",
filterText: "深度思考",
},
SEARCH_MODE_BUTTON: {
selector: ".ds-button span",
filterText: "联网搜索",
},
UPLOAD_FILE_BUTTON: {
selector: ".f02f0e25",
position: "first",
},
NEW_CHAT_BUTTON: {
selector: "._217e214",
position: "first",
},
TOGGLE_SIDEBAR_BUTTON: {
selector: ".ds-icon-button",
filterText: "svg #打开边栏0730, svg #折叠边栏0730",
},
CURRENT_CHAT_MENU_BUTTON: {
parentSelector: "._83421f9.b64fb9ae",
parentPosition: "last",
selector: "._2090548",
childPosition: "first",
},
};
const ELEMENT_IDS = {
HELP_PANEL: "dsk-help-panel",
HELP_PANEL_ANIMATE_IN: "dsk-help-panel-animate-in",
HELP_PANEL_ANIMATE_OUT: "dsk-help-panel-animate-out",
};
const CSS_CLASSES = {
HELP_PANEL_VISIBLE: "dsk-help-panel--visible",
HELP_PANEL_CLOSE_BUTTON: "dsk-help-panel-close-button",
HELP_PANEL_TITLE: "dsk-help-panel-title",
HELP_PANEL_CONTENT: "dsk-help-panel-content",
HELP_PANEL_ROW: "dsk-help-panel-row",
HELP_PANEL_KEY: "dsk-help-panel-key",
HELP_PANEL_KEY_DISPLAY: "dsk-help-panel-key--display",
HELP_PANEL_KEY_SETTING: "dsk-help-panel-key--setting",
HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT:
"dsk-help-panel-key--configurable-highlight",
HELP_PANEL_KEY_LISTENING: "dsk-help-panel-key--listening",
HELP_PANEL_KEY_INVALID_SHAKE: "dsk-key-invalid-shake",
HELP_PANEL_DESCRIPTION: "dsk-help-panel-description",
HELP_PANEL_WARNING: "dsk-help-panel-warning",
};
const UI_STRINGS = {
HELP_PANEL_TITLE: "快捷按键指北",
HELP_PANEL_WARNING_TEXT: "⚠️ 脚本依UA自动适配快捷键 篡改UA或致功能异常",
CUSTOMIZE_SHORTCUTS_LABEL: "自定义快捷键",
SETTINGS_BUTTON_TEXT: "设置自定按键",
FINISH_CUSTOMIZING_BUTTON_TEXT: "完成自定设置",
PRESS_NEW_SHORTCUT_TEXT: "请按下快捷键",
KEY_CONFLICT_TEXT_PREFIX: "键 「",
KEY_CONFLICT_TEXT_SUFFIX: "」 已被使用",
INVALID_MODIFIER_TEXT_PREFIX: "请按 ",
INVALID_MODIFIER_TEXT_SUFFIX: " + 字母/数字",
};
let currentKeybindingConfig = {
MODIFIERS: (() => {
const isMac = /Macintosh|Mac OS X/i.test(navigator.userAgent);
return {
CHARACTER_DISPLAY: isMac ? "Control" : "Alt",
EVENT_PROPERTY: isMac ? "ctrlKey" : "altKey",
};
})(),
SHORTCUTS: [
{
id: "regenerate",
key: "R",
description: "重新生成回答",
selectorConfig: ELEMENT_SELECTORS.REGENERATE_BUTTON,
},
{
id: "continueGenerating",
key: "C",
description: "继续生成回答",
selectorConfig: ELEMENT_SELECTORS.CONTINUE_BUTTON,
},
{
id: "stopGenerating",
key: "Q",
description: "中断当前生成",
selectorConfig: ELEMENT_SELECTORS.STOP_GENERATING_BUTTON,
},
{
id: "copyLastResponse",
key: "K",
description: "复制末条回答",
selectorConfig: ELEMENT_SELECTORS.LAST_COPY_BUTTON,
},
{
id: "editLastQuery",
key: "E",
description: "编辑末次提问",
selectorConfig: ELEMENT_SELECTORS.LAST_EDIT_BUTTON,
},
{
id: "deepThinkMode",
key: "D",
description: "深度思考模式",
selectorConfig: ELEMENT_SELECTORS.DEEP_THINK_MODE_BUTTON,
},
{
id: "searchMode",
key: "S",
description: "联网搜索模式",
selectorConfig: ELEMENT_SELECTORS.SEARCH_MODE_BUTTON,
},
{
id: "uploadFile",
key: "U",
description: "上传本地文件",
selectorConfig: ELEMENT_SELECTORS.UPLOAD_FILE_BUTTON,
},
{
id: "newChat",
key: "N",
description: "新建对话窗口",
selectorConfig: ELEMENT_SELECTORS.NEW_CHAT_BUTTON,
},
{
id: "toggleSidebar",
key: "T",
description: "切换开关边栏",
selectorConfig: ELEMENT_SELECTORS.TOGGLE_SIDEBAR_BUTTON,
},
{
id: "currentChatMenu",
key: "I",
description: "当前对话菜单",
selectorConfig: ELEMENT_SELECTORS.CURRENT_CHAT_MENU_BUTTON,
},
{
id: "toggleHelpPanel",
key: "H",
description: "快捷按键帮助",
actionIdentifier: "toggleHelpPanel",
isSpecialAction: true,
},
{
id: "settingsEntry",
key: null,
description: UI_STRINGS.CUSTOMIZE_SHORTCUTS_LABEL,
isSettingsEntry: true,
actionIdentifier: "toggleCustomizationMode",
nonConfigurable: true,
},
],
};
let helpPanelElement = null;
let keydownEventListener = null;
let isCustomizingShortcuts = false;
let activeCustomizationTarget = null;
const shortcutDisplaySpansMap = new Map();
let panelCloseTimer = null;
function injectUserInterfaceStyles() {
const styles = `
:root {
--ctp-frappe-rosewater: #f2d5cf;
--ctp-frappe-flamingo: #eebebe;
--ctp-frappe-pink: #f4b8e4;
--ctp-frappe-mauve: #ca9ee6;
--ctp-frappe-red: #e78284;
--ctp-frappe-maroon: #ea999c;
--ctp-frappe-peach: #ef9f76;
--ctp-frappe-yellow: #e5c890;
--ctp-frappe-green: #a6d189;
--ctp-frappe-teal: #81c8be;
--ctp-frappe-sky: #99d1db;
--ctp-frappe-sapphire: #85c1dc;
--ctp-frappe-blue: #8caaee;
--ctp-frappe-lavender: #babbf1;
--ctp-frappe-text: #c6d0f5;
--ctp-frappe-subtext1: #b5bfe2;
--ctp-frappe-subtext0: #a5adce;
--ctp-frappe-overlay2: #949cbb;
--ctp-frappe-overlay1: #838ba7;
--ctp-frappe-overlay0: #737994;
--ctp-frappe-surface2: #626880;
--ctp-frappe-surface1: #51576d;
--ctp-frappe-surface0: #414559;
--ctp-frappe-base: #303446;
--ctp-frappe-mantle: #292c3c;
--ctp-frappe-crust: #232634;
--ctp-frappe-crust-rgb: 35, 38, 52;
--dsk-panel-bg: rgba(41, 44, 60, 0.85);
--dsk-panel-border: rgba(65, 69, 89, 0.5);
--dsk-panel-shadow:
0 1px 3px rgba(var(--ctp-frappe-crust-rgb), 0.12),
0 6px 16px rgba(var(--ctp-frappe-crust-rgb), 0.10),
0 12px 28px rgba(var(--ctp-frappe-crust-rgb), 0.08);
--dsk-text-primary: var(--ctp-frappe-text);
--dsk-text-secondary: var(--ctp-frappe-subtext0);
--dsk-key-bg: var(--ctp-frappe-surface0);
--dsk-key-border: var(--ctp-frappe-surface1);
--dsk-key-setting-text: var(--ctp-frappe-blue);
--dsk-key-setting-hover-bg: var(--ctp-frappe-surface1);
--dsk-key-breathing-highlight-color: var(--ctp-frappe-mauve);
--dsk-key-listening-border: var(--ctp-frappe-green);
--dsk-key-listening-bg: color-mix(in srgb, var(--dsk-key-bg) 85%, var(--ctp-frappe-green) 15%);
--dsk-key-invalid-shake-color: var(--ctp-frappe-red);
--dsk-warning-bg: rgba(65, 69, 89, 0.5);
--dsk-warning-border: var(--ctp-frappe-surface1);
--dsk-warning-text: var(--ctp-frappe-yellow);
--dsk-scrollbar-thumb: var(--ctp-frappe-overlay0);
--dsk-scrollbar-thumb-hover: var(--ctp-frappe-overlay1);
--dsk-close-button-bg: var(--ctp-frappe-red);
--dsk-close-button-hover-bg: color-mix(in srgb, var(--ctp-frappe-red) 80%, var(--ctp-frappe-crust) 20%);
--dsk-close-button-symbol: rgba(var(--ctp-frappe-crust-rgb), 0.7);
}
@keyframes dsk-opacity-breathing-effect {
0%, 100% {
opacity: 0;
}
50% {
opacity: 0.25;
}
}
@keyframes dsk-border-breathing-effect {
0%, 100% {
border-color: var(--dsk-key-border);
}
50% {
border-color: var(--dsk-key-breathing-highlight-color);
}
}
@keyframes dsk-invalid-shake-effect {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-3px); }
20%, 40%, 60%, 80% { transform: translateX(3px); }
}
@keyframes ${ELEMENT_IDS.HELP_PANEL_ANIMATE_IN} {
0% {
transform: translate(-50%, -50%) scale(0.88);
opacity: 0;
}
100% {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
}
@keyframes ${ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT} {
0% {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
100% {
transform: translate(-50%, -50%) scale(0.9);
opacity: 0;
}
}
#${ELEMENT_IDS.HELP_PANEL} {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.88);
opacity: 0;
visibility: hidden;
z-index: 2147483647;
min-width: 300px;
max-width: 480px;
padding: 24px;
border: 1px solid var(--dsk-panel-border);
border-radius: 16px;
background-color: var(--dsk-panel-bg);
color: var(--dsk-text-primary);
font-family: ${UI_SETTINGS.FONT_STACK};
font-size: 14px;
font-weight: 500;
line-height: 1.5;
box-shadow: var(--dsk-panel-shadow);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
display: flex;
flex-direction: column;
pointer-events: none;
}
#${ELEMENT_IDS.HELP_PANEL}.${CSS_CLASSES.HELP_PANEL_VISIBLE} {
pointer-events: auto;
}
.${CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON} {
position: absolute;
top: 14px;
left: 14px;
width: 12px;
height: 12px;
padding: 0;
border: none;
border-radius: 50%;
background-color: var(--dsk-close-button-bg);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE},
transform 0.15s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE};
appearance: none;
-webkit-appearance: none;
outline: none;
}
.${CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}::before {
content: '✕';
display: block;
color: transparent;
font-size: 10px;
font-weight: bold;
line-height: 12px;
text-align: center;
transition: color 0.1s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE};
}
.${CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}:hover {
background-color: var(--dsk-close-button-hover-bg);
}
.${CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}:hover::before {
color: var(--dsk-close-button-symbol);
}
.${CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}:active {
filter: brightness(0.85);
transform: scale(0.9);
}
.${CSS_CLASSES.HELP_PANEL_TITLE} {
margin: 0 0 18px 0;
padding-top: 8px;
color: var(--dsk-text-primary);
font-size: 17px;
font-weight: 600;
text-align: center;
flex-shrink: 0;
}
.${CSS_CLASSES.HELP_PANEL_CONTENT} {
flex-grow: 1;
overflow-y: auto;
max-height: 60vh;
margin-right: -12px;
padding-right: 12px;
scrollbar-width: thin;
scrollbar-color: var(--dsk-scrollbar-thumb) transparent;
}
.${CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar {
width: 6px;
}
.${CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar-track {
background: transparent;
margin: 4px 0;
}
.${CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar-thumb {
background-color: var(--dsk-scrollbar-thumb);
border-radius: 3px;
transition: background-color 0.15s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE};
}
.${CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar-thumb:hover {
background-color: var(--dsk-scrollbar-thumb-hover);
}
.${CSS_CLASSES.HELP_PANEL_ROW} {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 6px 2px;
}
.${CSS_CLASSES.HELP_PANEL_CONTENT} > .${CSS_CLASSES.HELP_PANEL_ROW}:last-child {
margin-bottom: 0;
}
.${CSS_CLASSES.HELP_PANEL_KEY} {
min-width: 95px;
padding: 5px 10px;
margin-left: 18px;
background-color: var(--dsk-key-bg);
border: 1px solid var(--dsk-key-border);
border-radius: 6px;
box-shadow: 0 1px 1px rgba(0,0,0,0.08), inset 0 1px 1px rgba(255,255,255,0.03);
color: var(--dsk-text-primary);
font-family: inherit;
font-size: 13px;
font-weight: 500;
text-align: center;
flex-shrink: 0;
transition: background-color 0.15s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE},
border-color 0.15s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE},
color 0.15s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE};
cursor: default;
position: relative;
}
.${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_DISPLAY} {
}
.${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_SETTING} {
color: var(--dsk-key-setting-text);
cursor: pointer;
}
.${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}:hover {
background-color: var(--dsk-key-setting-hover-bg);
border-color: var(--ctp-frappe-overlay0);
}
.${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT} {
animation: dsk-border-breathing-effect ${UI_SETTINGS.BREATHING_ANIMATION_DURATION} infinite ease-in-out;
cursor: pointer;
}
.${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT}::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: inherit;
background-color: var(--dsk-key-breathing-highlight-color);
opacity: 0;
z-index: 0;
pointer-events: none;
animation: dsk-opacity-breathing-effect ${UI_SETTINGS.BREATHING_ANIMATION_DURATION} infinite ease-in-out;
}
.${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_LISTENING} {
border-color: var(--dsk-key-listening-border) !important;
background-color: var(--dsk-key-listening-bg) !important;
color: var(--ctp-frappe-green) !important;
animation: none !important;
cursor: default !important;
}
.${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_LISTENING}::after {
animation: none !important;
opacity: 0 !important;
}
.${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_INVALID_SHAKE} {
animation: dsk-invalid-shake-effect 0.5s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_DEFAULT};
border-color: var(--dsk-key-invalid-shake-color) !important;
color: var(--dsk-key-invalid-shake-color) !important;
}
.${CSS_CLASSES.HELP_PANEL_DESCRIPTION} {
flex-grow: 1;
padding-right: 10px;
color: var(--dsk-text-secondary);
font-size: 13.5px;
}
.${CSS_CLASSES.HELP_PANEL_WARNING} {
margin-top: 20px;
padding: 12px 16px;
background-color: var(--dsk-warning-bg);
border: 1px solid var(--dsk-warning-border);
border-radius: 10px;
color: var(--dsk-warning-text);
font-size: 12.5px;
font-weight: 500;
line-height: 1.45;
text-align: center;
flex-shrink: 0;
}
`;
try {
GM_addStyle(styles);
} catch (e) {
const styleElement = document.createElement("style");
styleElement.textContent = styles;
(document.head || document.documentElement).appendChild(styleElement);
}
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function getElementByConfig(config) {
if (!config || !config.selector) return null;
const {
selector,
filterText,
position = "first",
parentSelector,
parentPosition = "last",
childPosition = "first",
} = config;
let targetElements = [];
if (parentSelector) {
const parents = Array.from(document.querySelectorAll(parentSelector));
if (parents.length === 0) return null;
const parentIndex = parentPosition === "last" ? parents.length - 1 : 0;
const targetParent = parents[parentIndex];
if (!targetParent) return null;
targetElements = Array.from(targetParent.querySelectorAll(selector));
} else {
targetElements = Array.from(document.querySelectorAll(selector));
}
if (targetElements.length === 0) return null;
if (filterText) {
const filters = filterText.split(",").map((f) => f.trim());
const foundElement = targetElements.find((element) =>
filters.some(
(ft) =>
element.textContent?.includes(ft) ||
(ft.startsWith("svg #") &&
element.querySelector(ft.replace("svg ", "")))
)
);
return foundElement || null;
} else {
const index =
position === "last"
? targetElements.length - 1
: childPosition === "last"
? targetElements.length - 1
: 0;
return targetElements[index] || null;
}
}
function triggerElementClick(elementConfig) {
const element = getElementByConfig(elementConfig);
if (element && typeof element.click === "function") {
element.click();
return true;
}
return false;
}
function formatShortcutForDisplay(shortcutKey) {
if (!shortcutKey) return "---";
return `${
currentKeybindingConfig.MODIFIERS.CHARACTER_DISPLAY
} + ${shortcutKey.toUpperCase()}`;
}
function updateShortcutDisplay(shortcutId, newKey) {
const spanElement = shortcutDisplaySpansMap.get(shortcutId);
if (spanElement) {
spanElement.textContent = newKey
? formatShortcutForDisplay(newKey)
: UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
}
}
function saveCustomShortcut(shortcutId, newKey) {
const shortcutToUpdate = currentKeybindingConfig.SHORTCUTS.find(
(s) => s.id === shortcutId
);
if (shortcutToUpdate) {
shortcutToUpdate.key = newKey.toUpperCase();
try {
GM_setValue(
`${STORAGE_KEYS.CUSTOM_SHORTCUTS_PREFIX}${shortcutId}`,
shortcutToUpdate.key
);
} catch (e) {}
if (keydownEventListener) {
window.removeEventListener("keydown", keydownEventListener, true);
}
keydownEventListener = createKeyboardEventHandler();
window.addEventListener("keydown", keydownEventListener, true);
}
}
function loadCustomShortcuts() {
currentKeybindingConfig.SHORTCUTS.forEach((shortcut) => {
if (shortcut.nonConfigurable || shortcut.isSettingsEntry) return;
try {
const savedKey = GM_getValue(
`${STORAGE_KEYS.CUSTOM_SHORTCUTS_PREFIX}${shortcut.id}`,
shortcut.key
);
if (
savedKey &&
typeof savedKey === "string" &&
savedKey.match(/^[a-zA-Z0-9]$/i)
) {
shortcut.key = savedKey.toUpperCase();
}
} catch (e) {}
});
}
function setListeningState(shortcutId, spanElement, isListening) {
if (isListening) {
if (
activeCustomizationTarget &&
activeCustomizationTarget.spanElement !== spanElement
) {
const prevShortcut = currentKeybindingConfig.SHORTCUTS.find(
(s) => s.id === activeCustomizationTarget.shortcutId
);
activeCustomizationTarget.spanElement.textContent =
formatShortcutForDisplay(prevShortcut?.key);
activeCustomizationTarget.spanElement.classList.remove(
CSS_CLASSES.HELP_PANEL_KEY_LISTENING
);
activeCustomizationTarget.spanElement.classList.remove(
CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
);
if (isCustomizingShortcuts) {
activeCustomizationTarget.spanElement.classList.add(
CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
);
}
}
activeCustomizationTarget = { shortcutId, spanElement };
spanElement.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
shortcutDisplaySpansMap.forEach((s) => {
s.classList.remove(CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT);
});
spanElement.classList.add(CSS_CLASSES.HELP_PANEL_KEY_LISTENING);
spanElement.classList.remove(
CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
);
} else {
if (
activeCustomizationTarget &&
activeCustomizationTarget.shortcutId === shortcutId
) {
activeCustomizationTarget = null;
}
spanElement.classList.remove(CSS_CLASSES.HELP_PANEL_KEY_LISTENING);
const currentKey = currentKeybindingConfig.SHORTCUTS.find(
(s) => s.id === shortcutId
)?.key;
spanElement.textContent = formatShortcutForDisplay(currentKey);
if (isCustomizingShortcuts) {
shortcutDisplaySpansMap.forEach((s, id) => {
const cfg = currentKeybindingConfig.SHORTCUTS.find(
(sc) => sc.id === id
);
if (cfg && !cfg.nonConfigurable && !cfg.isSettingsEntry) {
s.classList.add(CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT);
}
});
}
}
}
function toggleCustomizationMode(settingsButtonSpan) {
isCustomizingShortcuts = !isCustomizingShortcuts;
if (activeCustomizationTarget) {
setListeningState(
activeCustomizationTarget.shortcutId,
activeCustomizationTarget.spanElement,
false
);
}
if (isCustomizingShortcuts) {
settingsButtonSpan.textContent =
UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT;
shortcutDisplaySpansMap.forEach((span, id) => {
const shortcut = currentKeybindingConfig.SHORTCUTS.find(
(s) => s.id === id
);
if (
shortcut &&
!shortcut.nonConfigurable &&
!shortcut.isSettingsEntry
) {
span.classList.add(CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT);
}
});
} else {
settingsButtonSpan.textContent = UI_STRINGS.SETTINGS_BUTTON_TEXT;
shortcutDisplaySpansMap.forEach((span) => {
span.classList.remove(
CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
);
span.classList.remove(CSS_CLASSES.HELP_PANEL_KEY_LISTENING);
});
}
}
function createHelpPanelElement() {
if (helpPanelElement && document.body.contains(helpPanelElement)) {
const settingsButton = helpPanelElement.querySelector(
`.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
);
if (settingsButton) {
settingsButton.textContent = isCustomizingShortcuts
? UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT
: UI_STRINGS.SETTINGS_BUTTON_TEXT;
}
shortcutDisplaySpansMap.forEach((span, id) => {
const shortcut = currentKeybindingConfig.SHORTCUTS.find(
(s) => s.id === id
);
if (shortcut) {
span.textContent = formatShortcutForDisplay(shortcut.key);
span.classList.remove(
CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT,
CSS_CLASSES.HELP_PANEL_KEY_LISTENING
);
if (
isCustomizingShortcuts &&
!shortcut.isSettingsEntry &&
!shortcut.nonConfigurable
) {
if (
activeCustomizationTarget &&
activeCustomizationTarget.shortcutId === id
) {
span.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
span.classList.add(CSS_CLASSES.HELP_PANEL_KEY_LISTENING);
} else {
span.classList.add(
CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
);
}
}
}
});
return helpPanelElement;
}
shortcutDisplaySpansMap.clear();
const panel = document.createElement("div");
panel.id = ELEMENT_IDS.HELP_PANEL;
const closeButton = document.createElement("button");
closeButton.className = CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON;
closeButton.setAttribute("aria-label", "Close help panel");
closeButton.addEventListener("click", (event) => {
event.stopPropagation();
if (isCustomizingShortcuts) {
const settingsBtnInPanel = panel.querySelector(
`.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
);
if (settingsBtnInPanel) toggleCustomizationMode(settingsBtnInPanel);
}
closeHelpPanel();
});
const titleElement = document.createElement("h3");
titleElement.className = CSS_CLASSES.HELP_PANEL_TITLE;
titleElement.textContent = UI_STRINGS.HELP_PANEL_TITLE;
const contentContainer = document.createElement("div");
contentContainer.className = CSS_CLASSES.HELP_PANEL_CONTENT;
currentKeybindingConfig.SHORTCUTS.forEach((shortcut) => {
const row = document.createElement("div");
row.className = CSS_CLASSES.HELP_PANEL_ROW;
const descriptionSpan = document.createElement("span");
descriptionSpan.className = CSS_CLASSES.HELP_PANEL_DESCRIPTION;
descriptionSpan.textContent = shortcut.description;
const keySpan = document.createElement("span");
keySpan.className = CSS_CLASSES.HELP_PANEL_KEY;
if (shortcut.isSettingsEntry) {
keySpan.textContent = isCustomizingShortcuts
? UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT
: UI_STRINGS.SETTINGS_BUTTON_TEXT;
keySpan.classList.add(CSS_CLASSES.HELP_PANEL_KEY_SETTING);
keySpan.addEventListener("click", () => {
toggleCustomizationMode(keySpan);
});
} else {
keySpan.textContent = formatShortcutForDisplay(shortcut.key);
keySpan.classList.add(CSS_CLASSES.HELP_PANEL_KEY_DISPLAY);
shortcutDisplaySpansMap.set(shortcut.id, keySpan);
if (!shortcut.nonConfigurable) {
keySpan.addEventListener("click", () => {
if (
isCustomizingShortcuts &&
(!activeCustomizationTarget ||
activeCustomizationTarget.shortcutId !== shortcut.id)
) {
setListeningState(shortcut.id, keySpan, true);
} else if (
isCustomizingShortcuts &&
activeCustomizationTarget &&
activeCustomizationTarget.shortcutId === shortcut.id
) {
setListeningState(shortcut.id, keySpan, false);
}
});
}
if (isCustomizingShortcuts && !shortcut.nonConfigurable) {
if (
activeCustomizationTarget &&
activeCustomizationTarget.shortcutId === shortcut.id
) {
keySpan.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
keySpan.classList.add(CSS_CLASSES.HELP_PANEL_KEY_LISTENING);
} else {
keySpan.classList.add(
CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
);
}
}
}
row.appendChild(descriptionSpan);
row.appendChild(keySpan);
contentContainer.appendChild(row);
});
const warningElement = document.createElement("div");
warningElement.className = CSS_CLASSES.HELP_PANEL_WARNING;
warningElement.textContent = UI_STRINGS.HELP_PANEL_WARNING_TEXT;
panel.appendChild(closeButton);
panel.appendChild(titleElement);
panel.appendChild(contentContainer);
panel.appendChild(warningElement);
helpPanelElement = panel;
document.body.appendChild(panel);
return panel;
}
function closeHelpPanel() {
if (
helpPanelElement &&
helpPanelElement.classList.contains(CSS_CLASSES.HELP_PANEL_VISIBLE)
) {
helpPanelElement.style.animation = `${ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT} ${UI_SETTINGS.ANIMATION_DURATION_MS}ms ${UI_SETTINGS.ANIMATION_EASING_POP_OUT} forwards`;
clearTimeout(panelCloseTimer);
panelCloseTimer = setTimeout(() => {
if (helpPanelElement) {
helpPanelElement.classList.remove(CSS_CLASSES.HELP_PANEL_VISIBLE);
helpPanelElement.style.animation = "";
helpPanelElement.style.opacity = "0";
helpPanelElement.style.visibility = "hidden";
}
window.removeEventListener(
"click",
handlePanelInteractionOutside,
true
);
window.removeEventListener("keydown", handlePanelEscapeKey, true);
}, UI_SETTINGS.ANIMATION_DURATION_MS);
}
}
function handlePanelInteractionOutside(event) {
if (
helpPanelElement &&
helpPanelElement.classList.contains(CSS_CLASSES.HELP_PANEL_VISIBLE) &&
!helpPanelElement.contains(event.target) &&
parseFloat(getComputedStyle(helpPanelElement).opacity) === 1
) {
if (isCustomizingShortcuts) {
const settingsButton = helpPanelElement.querySelector(
`.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
);
if (settingsButton) toggleCustomizationMode(settingsButton);
}
closeHelpPanel();
}
}
function handlePanelEscapeKey(event) {
if (event.key === "Escape") {
if (
helpPanelElement &&
helpPanelElement.classList.contains(CSS_CLASSES.HELP_PANEL_VISIBLE) &&
parseFloat(getComputedStyle(helpPanelElement).opacity) === 1
) {
event.preventDefault();
event.stopPropagation();
if (activeCustomizationTarget) {
setListeningState(
activeCustomizationTarget.shortcutId,
activeCustomizationTarget.spanElement,
false
);
} else if (isCustomizingShortcuts) {
const settingsButton = helpPanelElement.querySelector(
`.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
);
if (settingsButton) toggleCustomizationMode(settingsButton);
}
closeHelpPanel();
}
}
}
function toggleHelpPanelVisibility() {
if (!helpPanelElement || !document.body.contains(helpPanelElement)) {
helpPanelElement = createHelpPanelElement();
} else {
const settingsButton = helpPanelElement.querySelector(
`.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
);
if (settingsButton) {
settingsButton.textContent = isCustomizingShortcuts
? UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT
: UI_STRINGS.SETTINGS_BUTTON_TEXT;
}
shortcutDisplaySpansMap.forEach((span, id) => {
const shortcut = currentKeybindingConfig.SHORTCUTS.find(
(s) => s.id === id
);
if (shortcut) {
span.textContent = formatShortcutForDisplay(shortcut.key);
span.classList.remove(
CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT,
CSS_CLASSES.HELP_PANEL_KEY_LISTENING
);
if (
isCustomizingShortcuts &&
!shortcut.isSettingsEntry &&
!shortcut.nonConfigurable
) {
if (
activeCustomizationTarget &&
activeCustomizationTarget.shortcutId === id
) {
span.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
span.classList.add(CSS_CLASSES.HELP_PANEL_KEY_LISTENING);
} else {
span.classList.add(
CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
);
}
}
}
});
}
clearTimeout(panelCloseTimer);
const isCurrentlyVisibleByClass = helpPanelElement.classList.contains(
CSS_CLASSES.HELP_PANEL_VISIBLE
);
let currentOpacity = 0;
if (
isCurrentlyVisibleByClass ||
helpPanelElement.style.animationName ===
ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT
) {
currentOpacity = parseFloat(getComputedStyle(helpPanelElement).opacity);
}
const isAnimatingOut =
helpPanelElement.style.animationName ===
ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT;
if (isCurrentlyVisibleByClass && currentOpacity > 0.01 && !isAnimatingOut) {
if (isCustomizingShortcuts && !activeCustomizationTarget) {
const settingsButton = helpPanelElement.querySelector(
`.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
);
if (settingsButton) toggleCustomizationMode(settingsButton);
}
closeHelpPanel();
} else {
helpPanelElement.style.animation = "";
helpPanelElement.style.opacity = "0";
helpPanelElement.style.visibility = "hidden";
requestAnimationFrame(() => {
helpPanelElement.classList.add(CSS_CLASSES.HELP_PANEL_VISIBLE);
helpPanelElement.style.visibility = "visible";
helpPanelElement.style.animation = `${ELEMENT_IDS.HELP_PANEL_ANIMATE_IN} ${UI_SETTINGS.ANIMATION_DURATION_MS}ms ${UI_SETTINGS.ANIMATION_EASING_POP_OUT} forwards`;
});
setTimeout(() => {
window.addEventListener("click", handlePanelInteractionOutside, true);
window.addEventListener("keydown", handlePanelEscapeKey, true);
}, 0);
}
}
const debouncedToggleHelpPanelVisibility = debounce(
toggleHelpPanelVisibility,
UI_SETTINGS.DEBOUNCE_DELAY_MS
);
function createKeyboardEventHandler() {
const specialActionHandlers = {
toggleHelpPanel: debouncedToggleHelpPanelVisibility,
toggleCustomizationMode: () => {
if (helpPanelElement) {
const settingsButton = helpPanelElement.querySelector(
`.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
);
if (settingsButton) toggleCustomizationMode(settingsButton);
}
},
};
const shortcutActionMap = {};
currentKeybindingConfig.SHORTCUTS.forEach((shortcut) => {
if (shortcut.key && !shortcut.isSettingsEntry) {
const lowerKey = shortcut.key.toLowerCase();
if (
shortcut.actionIdentifier &&
specialActionHandlers[shortcut.actionIdentifier]
) {
shortcutActionMap[lowerKey] =
specialActionHandlers[shortcut.actionIdentifier];
} else if (shortcut.selectorConfig) {
shortcutActionMap[lowerKey] = () =>
triggerElementClick(shortcut.selectorConfig);
}
}
});
return function handleKeyDown(event) {
if (event.key === "Escape") {
return;
}
if (activeCustomizationTarget) {
event.preventDefault();
event.stopPropagation();
const newKey = event.key;
const targetSpan = activeCustomizationTarget.spanElement;
if (
newKey &&
newKey.length === 1 &&
!event.ctrlKey &&
!event.altKey &&
!event.shiftKey &&
!event.metaKey &&
!["Control", "Alt", "Shift", "Meta"].includes(newKey)
) {
if (newKey.match(/^[a-zA-Z0-9]$/i)) {
const conflictingShortcut = currentKeybindingConfig.SHORTCUTS.find(
(s) =>
s.key &&
s.key.toLowerCase() === newKey.toLowerCase() &&
s.id !== activeCustomizationTarget.shortcutId
);
if (conflictingShortcut) {
targetSpan.textContent = `${
UI_STRINGS.KEY_CONFLICT_TEXT_PREFIX
}${newKey.toUpperCase()}${UI_STRINGS.KEY_CONFLICT_TEXT_SUFFIX}`;
setTimeout(() => {
if (
activeCustomizationTarget &&
activeCustomizationTarget.spanElement === targetSpan
) {
targetSpan.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
}
}, 2000);
return;
}
saveCustomShortcut(activeCustomizationTarget.shortcutId, newKey);
updateShortcutDisplay(activeCustomizationTarget.shortcutId, newKey);
setListeningState(
activeCustomizationTarget.shortcutId,
targetSpan,
false
);
} else {
targetSpan.classList.add(CSS_CLASSES.HELP_PANEL_KEY_INVALID_SHAKE);
setTimeout(() => {
if (
activeCustomizationTarget &&
activeCustomizationTarget.spanElement === targetSpan
) {
targetSpan.classList.remove(
CSS_CLASSES.HELP_PANEL_KEY_INVALID_SHAKE
);
}
}, 500);
}
} else if (!["Control", "Alt", "Shift", "Meta"].includes(newKey)) {
if (event[currentKeybindingConfig.MODIFIERS.EVENT_PROPERTY]) {
const conflictingShortcut = currentKeybindingConfig.SHORTCUTS.find(
(s) =>
s.key &&
s.key.toLowerCase() === newKey.toLowerCase() &&
s.id !== activeCustomizationTarget.shortcutId
);
if (conflictingShortcut) {
targetSpan.textContent = `${
UI_STRINGS.KEY_CONFLICT_TEXT_PREFIX
}${newKey.toUpperCase()}${UI_STRINGS.KEY_CONFLICT_TEXT_SUFFIX}`;
setTimeout(() => {
if (
activeCustomizationTarget &&
activeCustomizationTarget.spanElement === targetSpan
) {
targetSpan.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
}
}, 2000);
return;
}
saveCustomShortcut(activeCustomizationTarget.shortcutId, newKey);
updateShortcutDisplay(activeCustomizationTarget.shortcutId, newKey);
setListeningState(
activeCustomizationTarget.shortcutId,
targetSpan,
false
);
} else {
targetSpan.textContent = `${UI_STRINGS.INVALID_MODIFIER_TEXT_PREFIX}${currentKeybindingConfig.MODIFIERS.CHARACTER_DISPLAY}${UI_STRINGS.INVALID_MODIFIER_TEXT_SUFFIX}`;
setTimeout(() => {
if (
activeCustomizationTarget &&
activeCustomizationTarget.spanElement === targetSpan
) {
targetSpan.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
}
}, 2000);
}
}
return;
}
if (isCustomizingShortcuts) {
return;
}
if (!event[currentKeybindingConfig.MODIFIERS.EVENT_PROPERTY]) {
return;
}
const pressedKey = event.key.toLowerCase();
const actionToExecute = shortcutActionMap[pressedKey];
if (typeof actionToExecute === "function") {
actionToExecute();
event.preventDefault();
event.stopPropagation();
}
};
}
function initializeScript() {
loadCustomShortcuts();
injectUserInterfaceStyles();
keydownEventListener = createKeyboardEventHandler();
window.addEventListener("keydown", keydownEventListener, true);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initializeScript, {
once: true,
});
} else {
initializeScript();
}
})();