import { VimMode } from '../neovim/types' ; import { $ } from '../../common/utils' ; import { State } from '../neovim/state' ; import { ipcMain } from 'electron' ; import { Invokables } from '../../common/ipc' ; import { InputType } from '../../common/types' ; type OnKeyFn = ( inputKeys : string , inputType : InputType ) => void ; const modifiers = [ 'Alt' , 'Shift' , 'Meta' , 'Control' ] ; const isStandardAscii = ( key ) => key . charCodeAt ( 0 ) > 32 && key . charCodeAt ( 0 ) < 127 ; const handleMods = ( { ctrlKey , shiftKey , metaKey , altKey , key , } ) => { const mods = [ ] ; const onlyShift = shiftKey && ! ctrlKey && ! metaKey && ! altKey ; const notCmdOrCtrl = ! metaKey && ! ctrlKey ; const macOSUnicode = ( process . platform === 'darwin' && altKey && notCmdOrCtrl ) || ( process . platform === 'darwin' && altKey && shiftKey && notCmdOrCtrl ) ; if ( onlyShift && isStandardAscii ( key ) && key . length === 1 ) return mods ; if ( macOSUnicode ) return mods ; if ( ctrlKey ) mods . push ( 'C' ) ; if ( shiftKey ) mods . push ( 'S' ) ; if ( metaKey ) mods . push ( 'D' ) ; if ( altKey ) mods . push ( 'A' ) ; return mods ; } ; const toVimKey = ( key ) : string => { if ( key === 'Backspace' ) return 'BS' ; if ( key === '<' ) return 'LT' ; if ( key === 'Escape' ) return 'Esc' ; if ( key === 'Delete' ) return 'Del' ; if ( key === ' ' ) return 'Space' ; if ( key === 'ArrowUp' ) return 'Up' ; if ( key === 'ArrowDown' ) return 'Down' ; if ( key === 'ArrowLeft' ) return 'Left' ; if ( key === 'ArrowRight' ) return 'Right' ; else return key ; } ; const isUpper = ( char ) => char . toLowerCase ( ) !== char ; const bypassEmptyMod = ( key ) => ( modifiers . includes ( key ) ? '' : key ) ; const wrapKey = ( key ) : string => key . length > 1 && isUpper ( key [ 0 ] ) ? `<${ key }>` : key ; const combineModsWithKey = ( mods , key ) => mods . length ? `${ mods }-${ key }` : key ; const joinModsWithDash = ( mods ) => mods . join ( '-' ) ; const mapMods = $ < string > ( handleMods , joinModsWithDash ) ; const mapKey = $ < string > ( bypassEmptyMod , toVimKey ) ; const formatInput = $ < string > ( combineModsWithKey , wrapKey ) ; const isNotChar = ( e ) : boolean => { if ( e . key . length === 1 && ! e . ctrlKey && ! e . metaKey && ! e . altKey && ! e . shiftKey ) return false ; if ( process . platform !== 'darwin' && ( ( e . shiftKey && e . altKey ) || e . altKey ) && e . key . length === 1 && ! e . ctrlKey && ! e . metaKey ) return true ; if ( ( e . shiftKey || e . metaKey || e . altKey ) && ! e . ctrlKey && e . key . length === 1 ) return false ; return true ; } ; const Input = ( nvimStateRef , nvimInput , onWinFocus , onWinBlur ) => { const globalShortcuts = new Map < string , ( ) => void > ( ) ; let shouldClearEscapeOnNextAppFocus = false ; let lastEscapeTimestamp = 0 ; let windowHasFocus = true ; let keyListener = ( ) => { } ; let isCapturing = true ; let sendInputToVim = true ; let previousKeyWasDead = false ; let keyIsDead = false ; const workaroundForDeadKeyBeingPressedTwiceInARowOnMacOS = ( e ) : boolean => { if ( e . key === 'Dead' && ! previousKeyWasDead ) { keyIsDead = true ; previousKeyWasDead = false ; return false ; } if ( previousKeyWasDead ) ( previousKeyWasDead = false ) , ( keyIsDead = e . key === 'Dead' ) ; return true ; } ; const sendToVim = ( inputKeys ) => { if ( inputKeys . length === 1 ) inputKeys = inputKeys . replace ( '<' , '<LT>' ) ; if ( globalShortcuts . has ( inputKeys ) ) return globalShortcuts . get ( inputKeys ) ! ( ) ; if ( inputKeys === '<S-Space>' ) return nvimInput ( '<space>' ) ; if ( inputKeys . length > 1 && ! inputKeys . startsWith ( '<' ) ) { return inputKeys . split ( '' ) . forEach ( ( k ) => nvimInput ( k ) ) ; } if ( inputKeys . toLowerCase ( ) === '<esc>' ) lastEscapeTimestamp = Date . now ( ) ; nvimInput ( inputKeys ) ; } ; const sendKeys = async ( e , inputType ) => { const key = bypassEmptyMod ( e . key ) ; if ( ! key ) { const inputKey = e . data ; if ( ! inputKey ) return ; if ( sendInputToVim ) return sendToVim ( inputKey ) ; keyListener ( inputKey , inputType ) ; return ; } const inputKeys = formatInput ( mapMods ( e ) , mapKey ( e . key ) ) ; if ( sendInputToVim ) return sendToVim ( inputKeys ) ; keyListener ( inputKeys , inputType ) ; } ; const keydownHandler = ( e ) => { if ( ! windowHasFocus || ! isCapturing ) return ; sendKeys ( e , InputType . Down ) ; } ; ipcMain . handle ( Invokables . documentOnInput , ( _evt , keyEvent ) => { if ( process . platform === 'linux' || process . platform === 'win32' ) { keydownHandler ( keyEvent ) ; } else { if ( ! previousKeyWasDead && keyIsDead ) { keyIsDead = false ; previousKeyWasDead = true ; return ; } keydownHandler ( keyEvent ) ; } } ) ; ipcMain . handle ( Invokables . documentOnKeydown , ( _evt , keyEvent ) => { if ( process . platform === 'linux' || process . platform === 'win32' ) { if ( isNotChar ( keyEvent ) ) { keydownHandler ( keyEvent ) ; } } else { if ( isNotChar ( keyEvent ) && workaroundForDeadKeyBeingPressedTwiceInARowOnMacOS ( keyEvent ) ) { keydownHandler ( keyEvent ) ; } } } ) ; onWinFocus ( ( ) => { windowHasFocus = true ; if ( shouldClearEscapeOnNextAppFocus ) { nvimInput ( '<enter>' ) ; shouldClearEscapeOnNextAppFocus = false ; } } ) ; onWinBlur ( ( ) => { windowHasFocus = false ; const lastEscapeFromNow = Date . now ( ) - lastEscapeTimestamp ; const isTerminalMode = nvimStateRef . state . mode === VimMode . Terminal ; const fixTermEscape = isTerminalMode && lastEscapeFromNow < 25 ; if ( fixTermEscape ) shouldClearEscapeOnNextAppFocus = true ; } ) ; return { focus : ( ) => { isCapturing = true ; } , blur : ( ) => { isCapturing = false ; } , stealInput : ( onKeyFn ) => { sendInputToVim = false ; keyListener = onKeyFn ; } , restoreInput : ( ) => ( sendInputToVim = true ) , registerOneTimeUseShortcuts : ( shortcuts , cb ) => { const done = ( shortcut ) => { shortcuts . forEach ( ( s ) => globalShortcuts . delete ( s ) ) ; cb ( shortcut ) ; } ; shortcuts . forEach ( ( s ) => globalShortcuts . set ( s , ( ) => done ( s ) ) ) ; } , registerGlobalShortcuts : ( shortcuts ) => { shortcuts . forEach ( ( [ s , cb ] ) => globalShortcuts . set ( s , cb ) ) ; } , unregisterGlobalShortcuts : ( shortcuts ) => { shortcuts . forEach ( ( s ) => globalShortcuts . delete ( s ) ) ; } , } ; } ; export default Input ; export type Input = ReturnType < typeof Input > ;