dsh-vscode-mode 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -15
- package/lib/client.js +22666 -22351
- package/lib/client.js.map +1 -1
- package/lib/index.js +228 -12
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/index.ts +83 -13
- package/src/client/navHistory.ts +45 -0
- package/src/client/officialSidebar.ts +59 -1
- package/src/client/paths.ts +6 -0
- package/src/client/shortcutsOfficial.ts +26 -0
- package/src/client/state/mdPreviewCache.ts +74 -0
- package/src/client/state/navStateCache.ts +83 -0
- package/src/client/state/uiStatePersist.ts +562 -0
- package/src/client/styles/mcp.css +1 -19
- package/src/client/ui/EditorView.ts +65 -10
- package/src/client/ui/McpSettings.ts +26 -4
- package/src/paths.ts +59 -3
- package/src/rpc.ts +50 -3
- package/src/shared/rpc.ts +24 -0
- package/src/uiState.ts +165 -0
- package/src/client/shortcutRecorder.ts +0 -161
- package/src/client/ui/ShortcutSettings.ts +0 -274
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import type { OfficialBinding, OfficialModifier } from '../shared/keybindings.js'
|
|
2
|
-
import type { ShortcutsServiceLike } from './shortcutsOfficial.js'
|
|
3
|
-
|
|
4
|
-
export interface RecordHooks {
|
|
5
|
-
ready(): void
|
|
6
|
-
save(binding: OfficialBinding | null): void
|
|
7
|
-
cancel(): void
|
|
8
|
-
error(error: unknown): void
|
|
9
|
-
}
|
|
10
|
-
const recorderHost = globalThis as typeof globalThis & { __edrvRecording__?: WeakMap<ShortcutsServiceLike, Promise<void>> }
|
|
11
|
-
const leases = recorderHost.__edrvRecording__ ??= new WeakMap<ShortcutsServiceLike, Promise<void>>()
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Serialize native recorder state, including cleanup after a late ready response.
|
|
15
|
-
* @author ddj 2026年09月28号
|
|
16
|
-
* @param service Native/Web shortcut service.
|
|
17
|
-
* @param active Desired native interception state.
|
|
18
|
-
* @returns Completion of the state change; errors remain visible to the caller.
|
|
19
|
-
*/
|
|
20
|
-
function recordState(service: ShortcutsServiceLike, active: boolean): Promise<void> {
|
|
21
|
-
const prior = leases.get(service) ?? Promise.resolve()
|
|
22
|
-
const next = prior.catch(() => {}).then(() => service.recording(active))
|
|
23
|
-
leases.set(service, next)
|
|
24
|
-
return next
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Read physical modifiers without converting Control or Meta into logical primary.
|
|
29
|
-
* @author ddj 2026年09月28号
|
|
30
|
-
* @param event Keyboard event.
|
|
31
|
-
* @returns Modifiers in official canonical order.
|
|
32
|
-
*/
|
|
33
|
-
export function eventModifiers(event: KeyboardEvent): OfficialModifier[] {
|
|
34
|
-
const states = { control: event.ctrlKey, alt: event.altKey, shift: event.shiftKey, meta: event.metaKey }
|
|
35
|
-
return (Object.keys(states) as Array<keyof typeof states>).filter((key) => states[key])
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Physical key collection; native readiness and listeners are owned by startRecording. */
|
|
39
|
-
export class KeyCapture {
|
|
40
|
-
private held = new Set<string>()
|
|
41
|
-
private pending: OfficialBinding | null = null
|
|
42
|
-
private blocked = false
|
|
43
|
-
private dead = false
|
|
44
|
-
private composing = false
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Create a physical collector with official platform capabilities.
|
|
48
|
-
* @author ddj 2026年09月28号
|
|
49
|
-
* @param service Official binding validator and profile.
|
|
50
|
-
* @param hooks Completion/error handlers.
|
|
51
|
-
*/
|
|
52
|
-
constructor(private service: ShortcutsServiceLike, private hooks: RecordHooks) {}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Reset a gesture on blur or composition transitions.
|
|
56
|
-
* @author ddj 2026年09月28号
|
|
57
|
-
* @param composing Whether IME composition is currently active.
|
|
58
|
-
*/
|
|
59
|
-
reset(composing = false): void {
|
|
60
|
-
this.held.clear(); this.pending = null; this.blocked = false; this.dead = false
|
|
61
|
-
this.composing = composing
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Collect one physical press; commit only when a constituent key is released.
|
|
66
|
-
* @author ddj 2026年09月28号
|
|
67
|
-
* @param event Keyboard press; guarded IME/AltGraph input is left untouched.
|
|
68
|
-
*/
|
|
69
|
-
down(event: KeyboardEvent): void {
|
|
70
|
-
if (this.composing || event.isComposing || event.keyCode === 229 || event.getModifierState?.('AltGraph')) {
|
|
71
|
-
this.pending = null; this.held.clear(); return
|
|
72
|
-
}
|
|
73
|
-
const commandDead = this.service.runtime === 'web' && this.service.platform === 'macos'
|
|
74
|
-
&& event.code === 'KeyN' && event.metaKey && event.altKey && !event.ctrlKey && !event.shiftKey
|
|
75
|
-
if (event.key === 'Dead' && !commandDead) { this.reset(); this.dead = true; return }
|
|
76
|
-
if (this.dead) { this.dead = false; return }
|
|
77
|
-
event.preventDefault(); event.stopPropagation()
|
|
78
|
-
if (event.repeat) return
|
|
79
|
-
const modifiers = eventModifiers(event)
|
|
80
|
-
if (!modifiers.length && event.key === 'Escape') { this.hooks.cancel(); return }
|
|
81
|
-
if (!modifiers.length && (event.key === 'Backspace' || event.key === 'Delete')) { this.hooks.save(null); return }
|
|
82
|
-
if (/^(Control|Alt|Shift|Meta|AltGraph)(Left|Right)?$/u.test(event.code || event.key)) {
|
|
83
|
-
if (this.pending) this.reset()
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
const chords = this.service.runtime === 'desktop' && ['macos', 'windows'].includes(this.service.platform)
|
|
87
|
-
if (chords) this.held.add(event.code)
|
|
88
|
-
if (this.blocked) return
|
|
89
|
-
if (this.held.size > 2) {
|
|
90
|
-
this.pending = null; this.blocked = true; this.hooks.error('最多同时录制两个主键'); return
|
|
91
|
-
}
|
|
92
|
-
const codes = chords ? [...this.held] : [event.code]
|
|
93
|
-
try {
|
|
94
|
-
const probe = this.service.describeBinding({ code: codes[0], modifiers, ...(codes[1] ? { secondCode: codes[1] } : {}) })
|
|
95
|
-
if (!probe.binding || probe.issue) throw new Error(probe.issue ?? 'unsupported-key')
|
|
96
|
-
this.pending = probe.binding
|
|
97
|
-
} catch (error) { this.pending = null; this.hooks.error(error) }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Commit an accepted physical gesture once at release.
|
|
102
|
-
* @author ddj 2026年09月28号
|
|
103
|
-
* @param event Released key or modifier.
|
|
104
|
-
*/
|
|
105
|
-
up(event: KeyboardEvent): void {
|
|
106
|
-
this.held.delete(event.code)
|
|
107
|
-
const binding = this.pending
|
|
108
|
-
const modifier = event.code.replace(/(Left|Right)$/u, '').toLowerCase() as OfficialModifier
|
|
109
|
-
if (!binding || !(binding.code === event.code || binding.secondCode === event.code || binding.modifiers.includes(modifier))) return
|
|
110
|
-
this.pending = null
|
|
111
|
-
event.preventDefault(); event.stopPropagation()
|
|
112
|
-
this.hooks.save(binding)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Protect recording before listening, and restore interception on every exit path.
|
|
118
|
-
* @author ddj 2026年09月28号
|
|
119
|
-
* @param service Official service whose asynchronous native bridge must be ready.
|
|
120
|
-
* @param target Owning window, injectable for lifecycle tests.
|
|
121
|
-
* @param hooks Ready, save, cancel and failure callbacks.
|
|
122
|
-
* @returns Idempotent disposal, safe before readiness and after failure.
|
|
123
|
-
*/
|
|
124
|
-
export function startRecording(service: ShortcutsServiceLike, target: EventTarget, hooks: RecordHooks): () => void {
|
|
125
|
-
let disposed = false
|
|
126
|
-
let ready = false
|
|
127
|
-
/** @author ddj 2026年09月28号 @param binding Accepted binding; release interception before saving. */
|
|
128
|
-
const save = (binding: OfficialBinding | null): void => { dispose(); hooks.save(binding) }
|
|
129
|
-
/** @author ddj 2026年09月28号 Cancel capture and restore native interception. */
|
|
130
|
-
const cancel = (): void => { dispose(); hooks.cancel() }
|
|
131
|
-
const capture = new KeyCapture(service, { ...hooks, save, cancel })
|
|
132
|
-
/** @author ddj 2026年09月28号 @param event Physical key press. */
|
|
133
|
-
const down = (event: Event): void => { if (ready) capture.down(event as KeyboardEvent) }
|
|
134
|
-
/** @author ddj 2026年09月28号 @param event Physical key release. */
|
|
135
|
-
const up = (event: Event): void => { if (ready) capture.up(event as KeyboardEvent) }
|
|
136
|
-
/** @author ddj 2026年09月28号 Reset incomplete gesture on focus loss. */
|
|
137
|
-
const blur = (): void => capture.reset()
|
|
138
|
-
/** @author ddj 2026年09月28号 Guard the complete IME composition lifecycle. */
|
|
139
|
-
const compose = (): void => capture.reset(true)
|
|
140
|
-
const listeners: Array<[string, EventListener]> = [
|
|
141
|
-
['keydown', down], ['keyup', up], ['blur', blur], ['compositionstart', compose], ['compositionend', blur],
|
|
142
|
-
]
|
|
143
|
-
/** @author ddj 2026年09月28号 Release DOM listeners and serialize native restoration. */
|
|
144
|
-
const dispose = (): void => {
|
|
145
|
-
if (disposed) return
|
|
146
|
-
disposed = true; ready = false
|
|
147
|
-
for (const [name, handler] of listeners) target.removeEventListener(name, handler, { capture: true })
|
|
148
|
-
void recordState(service, false).catch(hooks.error)
|
|
149
|
-
}
|
|
150
|
-
/** @author ddj 2026年09月28号 Activate capture only after native acknowledgment. */
|
|
151
|
-
const activate = (): void => {
|
|
152
|
-
if (disposed) return
|
|
153
|
-
ready = true
|
|
154
|
-
for (const [name, handler] of listeners) target.addEventListener(name, handler, { capture: true })
|
|
155
|
-
hooks.ready()
|
|
156
|
-
}
|
|
157
|
-
/** @author ddj 2026年09月28号 @param error Native activation failure. */
|
|
158
|
-
const fail = (error: unknown): void => { if (!disposed) { dispose(); hooks.error(error) } }
|
|
159
|
-
void recordState(service, true).then(activate, fail)
|
|
160
|
-
return dispose
|
|
161
|
-
}
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* dsh-vscode-mode client — 独立「快捷键」设置区(与 VSCodeMode 平级,settings.section slot)。
|
|
4
|
-
* 子页一「官方快捷键」:官方 shortcuts 目录只读总览(官方 + 插件命令)+ 一键打开官方快捷键弹窗;
|
|
5
|
-
* 子页二「插件快捷键」:插件命令(edrv.*)行内录键编辑,持久化经官方 edit API
|
|
6
|
-
* (Desktop: userData/keybindings.json;Web: localStorage dsh.keybindings.v1),不再写插件设置。
|
|
7
|
-
* 录制期间调 service.recording(true):Desktop 原生键盘桥暂停拦截,否则录不到键。
|
|
8
|
-
* 样式复用 vsm-kb-* / vsm-panel-* / vsm-mcp-subtabs(styles/mcp.css 随 client bundle 注入)。
|
|
9
|
-
* 作者 ddj 2026年10月
|
|
10
|
-
*/
|
|
11
|
-
import React from 'react'
|
|
12
|
-
import { COMMANDS } from '../keybindings.js'
|
|
13
|
-
import { log } from '../log.js'
|
|
14
|
-
import { startRecording } from '../shortcutRecorder.js'
|
|
15
|
-
import { resetPluginKeys } from '../shortcutsOfficial.js'
|
|
16
|
-
import type { ShortcutsServiceLike, ShortcutCatalogRow } from '../shortcutsOfficial.js'
|
|
17
|
-
|
|
18
|
-
const settingsLog = log.child('shortcut-settings')
|
|
19
|
-
|
|
20
|
-
/** 目录行展示键位(keys 已按平台格式化;binding null = 未绑定)。 */
|
|
21
|
-
function keysText(row) {
|
|
22
|
-
if (!row || row.binding === null) return '未绑定'
|
|
23
|
-
return row.keys.join(' ')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** 插件命令行 = COMMANDS 目录序 × 官方目录行(行缺失时占位「未注册」)。 */
|
|
27
|
-
function catalogRowsOf(service) {
|
|
28
|
-
const byId = new Map(service.catalog.getSnapshot().map((row) => [row.id, row]))
|
|
29
|
-
return COMMANDS.map((command) => ({ id: command.id, label: command.label, row: byId.get(command.id) ?? null }))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 「快捷键」设置区主体:子页切换 + 服务就绪探测(晚到自愈,有界重试)。
|
|
34
|
-
* @author ddj 2026年10月
|
|
35
|
-
* @param props.getShortcuts 官方服务运行时探测函数(client/index.ts 注入)
|
|
36
|
-
*/
|
|
37
|
-
export function ShortcutSettings(props) {
|
|
38
|
-
const getShortcuts = props?.getShortcuts
|
|
39
|
-
const [service, setService] = React.useState(() => (typeof getShortcuts === 'function' ? getShortcuts() : null))
|
|
40
|
-
const [tab, setTab] = React.useState('official')
|
|
41
|
-
const [, setVersion] = React.useState(0)
|
|
42
|
-
|
|
43
|
-
// 服务就绪有界重试(服务晚到自愈;用尽保持提示文案)
|
|
44
|
-
React.useEffect(() => {
|
|
45
|
-
if (service) return undefined
|
|
46
|
-
let attempts = 0
|
|
47
|
-
let cancelled = false
|
|
48
|
-
const tick = () => {
|
|
49
|
-
if (cancelled) return
|
|
50
|
-
const found = typeof getShortcuts === 'function' ? getShortcuts() : null
|
|
51
|
-
if (found) {
|
|
52
|
-
setService(found)
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
if (attempts >= 15) return
|
|
56
|
-
attempts += 1
|
|
57
|
-
setTimeout(tick, 2000)
|
|
58
|
-
}
|
|
59
|
-
tick()
|
|
60
|
-
return () => {
|
|
61
|
-
cancelled = true
|
|
62
|
-
}
|
|
63
|
-
}, [service])
|
|
64
|
-
|
|
65
|
-
// 目录/配置变化即重渲染(官方 catalog/config 快照存储订阅)
|
|
66
|
-
React.useEffect(() => {
|
|
67
|
-
if (!service) return undefined
|
|
68
|
-
const bump = () => setVersion((v) => v + 1)
|
|
69
|
-
const offCatalog = service.catalog.subscribe(bump)
|
|
70
|
-
const offConfig = service.config.subscribe(bump)
|
|
71
|
-
return () => {
|
|
72
|
-
offCatalog()
|
|
73
|
-
offConfig()
|
|
74
|
-
}
|
|
75
|
-
}, [service])
|
|
76
|
-
|
|
77
|
-
let body
|
|
78
|
-
if (!service) {
|
|
79
|
-
body = React.createElement('div', { className: 'vsm-mcp-empty' },
|
|
80
|
-
'官方快捷键服务未就绪(正在探测;若长期无响应,说明当前 DSH 版本不含官方快捷键机制)。')
|
|
81
|
-
} else if (tab === 'plugin') {
|
|
82
|
-
body = React.createElement(PluginShortcutEditor, { service })
|
|
83
|
-
} else {
|
|
84
|
-
body = React.createElement(OfficialShortcutsPane, { service })
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return React.createElement('section', { className: 'vsm-general-page' },
|
|
88
|
-
React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null,
|
|
89
|
-
React.createElement('h2', null, '快捷键'),
|
|
90
|
-
React.createElement('p', null, '基于官方 shortcuts 机制:统一注册、冲突校验、按平台差异化默认值;Desktop 持久化到 userData/keybindings.json,Web 持久化到 localStorage(dsh.keybindings.v1)。'))),
|
|
91
|
-
React.createElement('nav', { className: 'vsm-mcp-subtabs' },
|
|
92
|
-
React.createElement('button', { className: tab === 'official' ? 'active' : '', onClick: () => setTab('official') }, '官方快捷键'),
|
|
93
|
-
React.createElement('button', { className: tab === 'plugin' ? 'active' : '', onClick: () => setTab('plugin') }, '插件快捷键')),
|
|
94
|
-
body,
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** 官方目录行(只读总览行)。 */
|
|
99
|
-
function OfficialRow({ row }) {
|
|
100
|
-
return React.createElement('div', { className: 'vsm-kb-row' },
|
|
101
|
-
React.createElement('div', { className: 'vsm-kb-label' },
|
|
102
|
-
React.createElement('span', null, row.label),
|
|
103
|
-
React.createElement('small', null, row.id),
|
|
104
|
-
row.modified && React.createElement('small', { className: 'vsm-kb-warn' }, '已自定义'),
|
|
105
|
-
row.issue && React.createElement('small', { className: 'vsm-kb-warn' }, '键位不受当前环境支持'),
|
|
106
|
-
row.conflicts.length > 0 && React.createElement('small', { className: 'vsm-kb-warn' }, '与 ' + row.conflicts.join('、') + ' 冲突')),
|
|
107
|
-
React.createElement('kbd', { 'aria-label': row?.aria, className: 'vsm-kb-key' + (row.conflicts.length ? ' warn' : '') }, keysText(row)),
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** 子页一:官方快捷键(弹窗入口 + 目录总览)。 */
|
|
112
|
-
function OfficialShortcutsPane({ service }) {
|
|
113
|
-
const [error, setError] = React.useState('')
|
|
114
|
-
const rows = service.catalog.getSnapshot()
|
|
115
|
-
const shortcut = rows.find((row) => row.id === 'shortcuts.open')
|
|
116
|
-
const hint = shortcut?.keys?.length ? '(' + shortcut.keys.join(' ') + ')' : ''
|
|
117
|
-
const config = service.config.getSnapshot()
|
|
118
|
-
const openOfficial = () => {
|
|
119
|
-
setError('')
|
|
120
|
-
if (!service.registry || typeof service.registry.invoke !== 'function') {
|
|
121
|
-
setError('官方弹窗入口不可用;可到 设置 → 通用 → 快捷键 打开')
|
|
122
|
-
return
|
|
123
|
-
}
|
|
124
|
-
try {
|
|
125
|
-
service.registry.invoke('shortcuts.open', {
|
|
126
|
-
region: 'page',
|
|
127
|
-
modal: null,
|
|
128
|
-
target: typeof document !== 'undefined' ? document.activeElement : null,
|
|
129
|
-
})
|
|
130
|
-
} catch (e) {
|
|
131
|
-
settingsLog.warn('打开官方快捷键弹窗失败:' + String(e))
|
|
132
|
-
setError('打开官方弹窗失败:' + String(e) + ';可到 设置 → 通用 → 快捷键 打开')
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return React.createElement(React.Fragment, null,
|
|
136
|
-
React.createElement('section', { className: 'vsm-panel' },
|
|
137
|
-
React.createElement('h3', { className: 'vsm-panel-title' }, '官方快捷键配置'),
|
|
138
|
-
React.createElement('div', { className: 'vsm-panel-body' },
|
|
139
|
-
React.createElement('p', null, '官方快捷键弹窗提供全部命令的查看、录键与恢复默认;也可从 设置 → 通用 → 快捷键 打开。下方为当前目录只读总览(含插件注册的命令)。'),
|
|
140
|
-
React.createElement('button', { className: 'vsm-primary vsm-small', onClick: openOfficial, 'aria-keyshortcuts': shortcut?.aria }, '打开官方快捷键设置' + hint),
|
|
141
|
-
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
142
|
-
config?.status === 'unreadable' && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' },
|
|
143
|
-
'快捷键配置读取失败(内容损坏或由更新版本写入),当前使用默认键位;请备份后清理配置,或升级 DSH。')),
|
|
144
|
-
),
|
|
145
|
-
React.createElement('section', { className: 'vsm-panel' },
|
|
146
|
-
React.createElement('h3', { className: 'vsm-panel-title' }, '命令目录(只读总览)'),
|
|
147
|
-
React.createElement('div', { className: 'vsm-panel-body' },
|
|
148
|
-
React.createElement('div', { className: 'vsm-kb-list' }, rows.map((row) => React.createElement(OfficialRow, { key: row.id, row }))),
|
|
149
|
-
),
|
|
150
|
-
),
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** 子页二:插件命令键位(行内录键 + 行级清除 + 恢复默认;写入走官方 edit API)。 */
|
|
155
|
-
function PluginShortcutEditor({ service }) {
|
|
156
|
-
const [nativeReady, setNativeReady] = React.useState(false)
|
|
157
|
-
const [recording, setRecording] = React.useState(null) // 正在录制的命令 id
|
|
158
|
-
const [busy, setBusy] = React.useState(false)
|
|
159
|
-
const [message, setMessage] = React.useState('')
|
|
160
|
-
const [error, setError] = React.useState('')
|
|
161
|
-
const rows = catalogRowsOf(service)
|
|
162
|
-
const config = service.config.getSnapshot()
|
|
163
|
-
|
|
164
|
-
const saveBinding = async (id, binding) => {
|
|
165
|
-
setBusy(true)
|
|
166
|
-
setError('')
|
|
167
|
-
setMessage('')
|
|
168
|
-
const buildEdit = () => (binding === null ? { type: 'reset', id } : { type: 'set', id, binding })
|
|
169
|
-
try {
|
|
170
|
-
const result = await service.edit(buildEdit(), service.config.getSnapshot().revision)
|
|
171
|
-
if (result.status === 'saved') {
|
|
172
|
-
setMessage('已保存')
|
|
173
|
-
} else if (result.status === 'stale') {
|
|
174
|
-
setError('快捷键配置已在其他窗口更新,请检查当前键位后重新录制')
|
|
175
|
-
} else if (result.status === 'conflict') {
|
|
176
|
-
// 官方按原因分类拒绝:issue = 键位本身不合法(如 web 白名单外),conflicts = 与其他命令撞键
|
|
177
|
-
if (result.issue) {
|
|
178
|
-
const reasons = {
|
|
179
|
-
'unsupported-browser': 'web 模式白名单外的组合(单修饰符组合在浏览器环境不可靠)',
|
|
180
|
-
'reserved': '官方保留键',
|
|
181
|
-
'unsupported-key': '官方不支持的键',
|
|
182
|
-
'modifier-required': '至少需要一个修饰键',
|
|
183
|
-
}
|
|
184
|
-
setError('键位被官方拒绝:' + (reasons[result.issue] ?? result.issue)
|
|
185
|
-
+ '。web 模式可用 Ctrl+Alt+X 风格组合;升级桌面外壳进入桌面模式后不受此限。')
|
|
186
|
-
} else {
|
|
187
|
-
setError('键位冲突:与 ' + (result.conflicts?.join('、') ?? '其他命令') + ' 冲突,请换一个组合')
|
|
188
|
-
}
|
|
189
|
-
} else if (result.status === 'not-ready') {
|
|
190
|
-
setError('快捷键配置尚未就绪,请稍后重试')
|
|
191
|
-
} else if (result.status === 'unreadable') {
|
|
192
|
-
setError('快捷键配置读取失败,编辑已禁用(见官方提示)')
|
|
193
|
-
} else {
|
|
194
|
-
setError('写入失败,请重试')
|
|
195
|
-
}
|
|
196
|
-
} catch (e) {
|
|
197
|
-
settingsLog.warn('快捷键保存失败:' + String(e))
|
|
198
|
-
setError('保存失败:' + String(e))
|
|
199
|
-
} finally {
|
|
200
|
-
setBusy(false)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const resetAll = async () => {
|
|
205
|
-
if (!window.confirm('恢复全部插件快捷键为默认值?(只影响本插件命令的键位覆盖)')) return
|
|
206
|
-
setBusy(true)
|
|
207
|
-
setError('')
|
|
208
|
-
setMessage('')
|
|
209
|
-
try {
|
|
210
|
-
const result = await resetPluginKeys(service)
|
|
211
|
-
if (result.failed) setError('已恢复 ' + result.reset + ' 条,' + result.failed + ' 恢复失败;请重试')
|
|
212
|
-
else setMessage('已恢复 ' + result.reset + ' 条插件快捷键')
|
|
213
|
-
} catch (e) {
|
|
214
|
-
settingsLog.warn('快捷键恢复默认失败:' + String(e))
|
|
215
|
-
setError('恢复失败:' + String(e))
|
|
216
|
-
} finally {
|
|
217
|
-
setBusy(false)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Own capture and native suspension for the selected command's entire lifetime.
|
|
223
|
-
* @author ddj 2026年09月28号
|
|
224
|
-
* @returns Cleanup on cancel, save, failure, service change and unmount.
|
|
225
|
-
*/
|
|
226
|
-
const recordEffect = () => {
|
|
227
|
-
if (!recording) return undefined
|
|
228
|
-
let mounted = true
|
|
229
|
-
setNativeReady(false)
|
|
230
|
-
/** @author ddj 2026年09月28号 Mark the native bridge ready to collect physical keys. */
|
|
231
|
-
const ready = () => { if (mounted) setNativeReady(true) }
|
|
232
|
-
/** @author ddj 2026年09月28号 Cancel capture without modifying preferences. */
|
|
233
|
-
const cancel = () => { if (mounted) setRecording(null) }
|
|
234
|
-
/** @author ddj 2026年09月28号 @param binding Accepted physical binding or reset. */
|
|
235
|
-
const save = (binding) => { if (mounted) { setRecording(null); void saveBinding(recording, binding) } }
|
|
236
|
-
/** @author ddj 2026年09月28号 @param reason Recorder failure to show while mounted. */
|
|
237
|
-
const fail = (reason) => { if (mounted) { setRecording(null); setError('录制失败:' + String(reason)) } }
|
|
238
|
-
const dispose = startRecording(service, window, { ready, cancel, save, error: fail })
|
|
239
|
-
return () => { mounted = false; dispose() }
|
|
240
|
-
}
|
|
241
|
-
React.useEffect(recordEffect, [recording, service])
|
|
242
|
-
|
|
243
|
-
const rowsView = rows.map(({ id, label, row }) => {
|
|
244
|
-
const recordingRow = recording === id
|
|
245
|
-
const display = recordingRow
|
|
246
|
-
? React.createElement('span', { className: 'vsm-kb-rec' }, nativeReady ? '按下组合键…(Esc 取消 / Backspace 恢复默认)' : '正在准备录制…')
|
|
247
|
-
: React.createElement('kbd', { 'aria-label': row?.aria, className: 'vsm-kb-key' + (row && row.conflicts.length ? ' warn' : '') }, keysText(row))
|
|
248
|
-
return React.createElement('div', { key: id, className: 'vsm-kb-row' + (recordingRow ? ' rec' : '') },
|
|
249
|
-
React.createElement('div', { className: 'vsm-kb-label' },
|
|
250
|
-
React.createElement('span', null, label),
|
|
251
|
-
row === null && React.createElement('small', { className: 'vsm-kb-warn' }, '未注册(官方服务未同步)'),
|
|
252
|
-
row && row.modified && React.createElement('small', { className: 'vsm-kb-warn' }, '已自定义'),
|
|
253
|
-
row && row.conflicts.length > 0 && React.createElement('small', { className: 'vsm-kb-warn' }, '与 ' + row.conflicts.join('、') + ' 冲突')),
|
|
254
|
-
display,
|
|
255
|
-
React.createElement('div', { className: 'vsm-kb-actions' },
|
|
256
|
-
recordingRow
|
|
257
|
-
? React.createElement('button', { onClick: () => setRecording(null) }, '取消')
|
|
258
|
-
: React.createElement(React.Fragment, null,
|
|
259
|
-
React.createElement('button', { disabled: busy || Boolean(recording) || config?.status !== 'ready', onClick: () => { setRecording(id); setMessage('') } }, '编辑'),
|
|
260
|
-
React.createElement('button', { disabled: busy || Boolean(recording) || config?.status !== 'ready', title: '清除该命令的键位(恢复默认)', onClick: () => void saveBinding(id, null) }, '清除'))),
|
|
261
|
-
)
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
return React.createElement('section', { className: 'vsm-panel' },
|
|
265
|
-
React.createElement('h3', { className: 'vsm-panel-title' }, '插件命令键位'),
|
|
266
|
-
React.createElement('div', { className: 'vsm-panel-body' },
|
|
267
|
-
React.createElement('div', { className: 'vsm-kb-list' }, rowsView)),
|
|
268
|
-
React.createElement('div', { className: 'vsm-panel-body vsm-kb-actionsbar' },
|
|
269
|
-
React.createElement('span', { className: 'vsm-kb-dirty' }, recording ? '录制中…' : ''),
|
|
270
|
-
message && React.createElement('span', { className: 'vsm-kb-message' }, message),
|
|
271
|
-
error && React.createElement('span', { className: 'vsm-kb-warn' }, error),
|
|
272
|
-
React.createElement('button', { className: 'vsm-small', disabled: busy || Boolean(recording) || config?.status !== 'ready', onClick: () => void resetAll() }, '恢复默认')),
|
|
273
|
-
)
|
|
274
|
-
}
|