diff --git a/browser/src/components/keyboard/index.tsx b/browser/src/components/keyboard/index.tsx index ed994fdb..bf3b9d6c 100644 --- a/browser/src/components/keyboard/index.tsx +++ b/browser/src/components/keyboard/index.tsx @@ -6,6 +6,7 @@ import { getOperatingSystem } from '@/libs/browser'; import { device } from '@/libs/device'; import { KeyboardReport } from '@/libs/keyboard/keyboard.ts'; import { isModifier } from '@/libs/keyboard/keymap.ts'; +import { learnFromKeyEvent } from '@/libs/keyboard/layouts.ts'; interface AltGrState { active: boolean; @@ -69,6 +70,10 @@ export const Keyboard = () => { } pressedKeys.current.add(code); + + // Learn character mappings for paste feature + learnFromKeyEvent(event); + await handleKeyEvent({ type: 'keydown', code }); } diff --git a/browser/src/components/menu/keyboard/paste.tsx b/browser/src/components/menu/keyboard/paste.tsx index ce0dff90..2a59cc70 100644 --- a/browser/src/components/menu/keyboard/paste.tsx +++ b/browser/src/components/menu/keyboard/paste.tsx @@ -3,8 +3,62 @@ import { ClipboardIcon } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { device } from '@/libs/device'; -import { CharCodes, ShiftChars } from '@/libs/keyboard/charCodes.ts'; -import { getModifierBit } from '@/libs/keyboard/keymap.ts'; +import { getLayoutById, initLayoutDetection, LayoutMap } from '@/libs/keyboard/layouts.ts'; +import { ModifierBits } from '@/libs/keyboard/keymap.ts'; + +// Initialize layout detection early +initLayoutDetection(); + +// Paste text as keystrokes using the specified keyboard layout +export async function pasteText(text: string, layoutId: string = 'auto'): Promise { + const layout: LayoutMap = getLayoutById(layoutId); + + // Release all keys first to ensure clean state + await device.sendKeyboardData([0, 0, 0, 0, 0, 0, 0, 0]); + await new Promise((r) => setTimeout(r, 50)); + + for (const char of text) { + const mapping = layout[char]; + if (!mapping) { + console.warn(`No mapping for character: '${char}' (code ${char.charCodeAt(0)})`); + continue; + } + + let modifier = 0; + if (mapping.shift) { + modifier |= ModifierBits.LeftShift; + } + if (mapping.altGr) { + // AltGr is typically Right Alt + modifier |= ModifierBits.RightAlt; + } + + // For modified keys (Shift/AltGr), press modifier first, then key + // This is more compatible with Windows login screen + if (modifier !== 0) { + await device.sendKeyboardData([modifier, 0, 0, 0, 0, 0, 0, 0]); + await new Promise((r) => setTimeout(r, 20)); + } + + // Press key (with modifier held) + await device.sendKeyboardData([modifier, 0, mapping.code, 0, 0, 0, 0, 0]); + await new Promise((r) => setTimeout(r, 50)); + + // Release key (modifier still held) + if (modifier !== 0) { + await device.sendKeyboardData([modifier, 0, 0, 0, 0, 0, 0, 0]); + await new Promise((r) => setTimeout(r, 15)); + } + + // Release modifier + await device.sendKeyboardData([0, 0, 0, 0, 0, 0, 0, 0]); + if (mapping.altGr) { + await new Promise((r) => setTimeout(r, 20)); + await device.sendKeyboardData([0, 0, 0, 0, 0, 0, 0, 0]); + } + await new Promise((r) => setTimeout(r, 30)); + } +} export const Paste = () => { const { t } = useTranslation(); @@ -17,22 +71,7 @@ export const Paste = () => { try { const text = await navigator.clipboard.readText(); if (!text) return; - - for (const char of text) { - const ascii = char.charCodeAt(0); - - const code = CharCodes[ascii]; - if (!code) continue; - - let modifier = 0; - if ((ascii >= 65 && ascii <= 90) || ShiftChars[ascii]) { - modifier |= getModifierBit('ShiftLeft'); - } - - await send(modifier, code); - await new Promise((r) => setTimeout(r, 50)); - await send(0, 0); - } + await pasteText(text); } catch (e) { console.log(e); } finally { @@ -40,11 +79,6 @@ export const Paste = () => { } } - async function send(modifier: number, code: number): Promise { - const keys = [modifier, 0, code, 0, 0, 0, 0, 0]; - await device.sendKeyboardData(keys); - } - return (
{ const { t } = useTranslation(); @@ -14,6 +16,10 @@ export const Settings = () => { const content = (
+ + + +
{ + const { t } = useTranslation(); + const [layout, setLayout] = useAtom(targetKeyboardLayoutAtom); + + const options = Object.entries(LAYOUTS).map(([id, { name }]) => ({ + value: id, + label: name, + })); + + function handleChange(value: string) { + setLayout(value); + storage.setTargetKeyboardLayout(value); + } + + return ( +
+ + {t('settings.keyboardLayout.title')}: +