Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/devtools/client/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import { useCopy } from '~/composables/editor'
import { setupFrameNav } from '~/composables/frame-nav'
import { WS_DEBOUNCE_TIME } from '~/composables/rpc'
import { registerCommands } from '~/composables/state-commands'
import { useDevToolsOptions } from '~/composables/storage-options'
import { isDevtoolsShortcut, watchShortcutLayoutMap } from '../src/runtime/shared/shortcut'
import { wsConnectedOnce } from './composables/rpc'
import { useSchemaInput } from './composables/state-schema'
import { useDevToolsOptions } from './composables/storage-options'
import { setupClientRPC } from './setup/client-rpc'

import 'floating-vue/dist/style.css'
Expand Down Expand Up @@ -83,8 +84,10 @@ watchEffect(() => {
document.documentElement.classList.toggle('light', !isDark)
})

watchShortcutLayoutMap()

useEventListener('keydown', (e) => {
if (e.code === 'KeyD' && e.altKey) {
if (isDevtoolsShortcut(e)) {
client.value?.devtools.close()
e.preventDefault()
}
Expand Down
30 changes: 24 additions & 6 deletions packages/devtools/src/runtime/plugins/view/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { computed, markRaw, nextTick, reactive, ref, shallowReactive, shallowRef
import { useAppConfig } from '#imports'

import { initTimelineMetrics } from '../../function-metrics-helpers'
import { isDevtoolsShortcut, watchShortcutLayoutMap } from '../../shared/shortcut'

// Host controls must update the visible Devframe viewer context, rather than
// Vite's separate dock-registration context. Target the shared-frame anchor
Expand All @@ -27,6 +28,14 @@ function getDevframeContext() {
return getDevframeClientContext() as any
}

function closeDevtoolsPanel() {
const ctx = getDevframeContext()
if (!ctx)
return
ctx.panel.session.open = false
void ctx.docks.switchEntry(null)
}

const clientRef = shallowRef<NuxtDevtoolsHostClient>()

export { clientRef as client }
Expand Down Expand Up @@ -57,13 +66,21 @@ export async function setupDevToolsClient({
devtools: {
toggle() {
const ctx = getDevframeContext()
if (ctx)
ctx.docks.toggleEntry(NUXT_DOCK_ANCHOR_ID)
if (!ctx)
return
if (!ctx.panel.session.open) {
ctx.panel.session.open = true
void ctx.docks.switchEntry(NUXT_DOCK_ANCHOR_ID)
}
else if (ctx.docks.selected?.id === NUXT_DOCK_ANCHOR_ID) {
closeDevtoolsPanel()
}
else {
void ctx.docks.switchEntry(NUXT_DOCK_ANCHOR_ID)
}
},
close() {
const ctx = getDevframeContext()
if (ctx)
ctx.panel.session.open = false
closeDevtoolsPanel()
},
open() {
const ctx = getDevframeContext()
Expand Down Expand Up @@ -205,8 +222,9 @@ export async function setupDevToolsClient({
clientRef.value = client

// Shortcut to toggle devtools (opens Vite DevTools panel with Nuxt DevTools entry)
watchShortcutLayoutMap()
window.addEventListener('keydown', (e) => {
if (e.code === 'KeyD' && e.altKey && e.shiftKey)
if (isDevtoolsShortcut(e, { shift: true }))
client.devtools.toggle()
})
}
Expand Down
125 changes: 125 additions & 0 deletions packages/devtools/src/runtime/shared/shortcut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// DevTools is toggled with `Shift + Alt + D`. `KeyboardEvent.code` describes
// the physical key position, so it only matches the letter D on a QWERTY
// layout. Users on Workman, Colemak, etc. press a different physical key, and
// macOS rewrites the produced character while Option is held (`d` -> `∂`,
// with Shift -> `Î`). Resolve the keys that actually type `d` in the active
// layout and fall back to the produced key and the legacy key code.

interface KeyboardLayoutMapLike {
forEach: (callback: (key: string, code: string) => void) => void
}

interface KeyboardLike {
getLayoutMap: () => Promise<KeyboardLayoutMapLike>
addEventListener?: (type: 'layoutchange', listener: () => void) => void
}

export interface DevtoolsShortcutEvent {
altKey: boolean
shiftKey: boolean
ctrlKey?: boolean
metaKey?: boolean
code?: string
key?: string
keyCode?: number
}

export interface DevtoolsShortcutOptions {
/** Require Shift to be held. Opening requires it; closing from the panel does not. */
shift?: boolean
/** Physical codes that type `d` in the active layout. Exposed for testing. */
codes?: ReadonlySet<string>
}

const D_KEY_CODE = 68
const QWERTY_D_CODE = 'KeyD'

const shortcutCodes = new Set([QWERTY_D_CODE])

function resetToQwertyShortcut() {
shortcutCodes.clear()
shortcutCodes.add(QWERTY_D_CODE)
}

export function resolveShortcutCodes(layout: KeyboardLayoutMapLike): Set<string> {
const codes = new Set<string>()
layout.forEach((key, code) => {
if (key.toLowerCase() === 'd')
codes.add(code)
})
return codes
}

function getKeyboard(): KeyboardLike | undefined {
if (typeof navigator === 'undefined')
return
return (navigator as Navigator & { keyboard?: KeyboardLike }).keyboard
}

let watching = false
let refreshRequest = 0

export function watchShortcutLayoutMap(): void {
if (watching)
return
watching = true

const keyboard = getKeyboard()
if (!keyboard?.getLayoutMap)
return

const refresh = () => {
const request = ++refreshRequest
shortcutCodes.clear()
try {
keyboard.getLayoutMap()
.then((layout) => {
if (request !== refreshRequest)
return
const codes = resolveShortcutCodes(layout)
if (!codes.size) {
resetToQwertyShortcut()
return
}
shortcutCodes.clear()
for (const code of codes)
shortcutCodes.add(code)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
.catch(() => {
if (request === refreshRequest)
resetToQwertyShortcut()
})
}
catch {
// Blocked by a permissions policy. `code`, `key` and `keyCode` matching
// still work without the layout map.
resetToQwertyShortcut()
}
}

refresh()
keyboard.addEventListener?.('layoutchange', refresh)
}

export function isDevtoolsShortcut(
event: DevtoolsShortcutEvent,
options: DevtoolsShortcutOptions = {},
): boolean {
if (!event.altKey)
return false
if (options.shift && !event.shiftKey)
return false
if (event.ctrlKey || event.metaKey)
return false

const codes = options.codes ?? shortcutCodes
if (event.code && codes.has(event.code))
return true

if (event.key?.toLowerCase() === 'd')
return true

// Deprecated, but it identifies the key produced by the active layout and
// ignores modifiers, which matters on macOS where Option rewrites `key`.
return event.keyCode === D_KEY_CODE
}
Loading
Loading