package input import "time" // Command is a semantic request; native callbacks never execute application work. type Command string const ( SelectPreviousSpeechSetting Command = "select-previous-speech-setting" SelectNextSpeechSetting Command = "select-next-speech-setting" DecreaseSpeechSetting Command = "decrease-speech-setting" IncreaseSpeechSetting Command = "increase-speech-setting" SaveSpeechSettings Command = "save-speech-settings" PresentTime Command = "present-time" PresentDate Command = "present-date" PresentClipboard Command = "present-clipboard" ShowSpeechHistory Command = "show-speech-history" ToggleSpeechMonitor Command = "toggle-speech-monitor" CycleSpeechMode Command = "cycle-speech-mode" CycleProgressBarOutput Command = "cycle-progress-bar-output" PresentVersion Command = "present-version" ToggleLearnMode Command = "toggle-learn-mode" ShowActions Command = "show-actions" InternalUIKey Command = "internal-ui-key" InternalUIDismiss Command = "internal-ui-dismiss" LearnKey Command = "learn-key" LearnDocumentation Command = "learn-documentation" LearnDefaultShortcuts Command = "learn-default-shortcuts" LearnApplicationShortcuts Command = "learn-application-shortcuts" StickyFocusMode Command = "sticky-focus-mode" StickyBrowseMode Command = "sticky-browse-mode" ToggleDocumentMode Command = "toggle-document-mode" DocumentNextHeading Command = "document-next-heading" DocumentPreviousHeading Command = "document-previous-heading" DocumentNextLink Command = "document-next-link" DocumentPreviousLink Command = "document-previous-link" DocumentNextButton Command = "document-next-button" DocumentPreviousButton Command = "document-previous-button" DocumentNextCheckBox Command = "document-next-check-box" DocumentPreviousCheckBox Command = "document-previous-check-box" DocumentNextList Command = "document-next-list" DocumentPreviousList Command = "document-previous-list" DocumentNextSeparator Command = "document-next-separator" DocumentPreviousSeparator Command = "document-previous-separator" DocumentNextHeadingAtLevel1 Command = "document-next-heading-at-level-1" DocumentPreviousHeadingAtLevel1 Command = "document-previous-heading-at-level-1" DocumentNextHeadingAtLevel2 Command = "document-next-heading-at-level-2" DocumentPreviousHeadingAtLevel2 Command = "document-previous-heading-at-level-2" DocumentNextHeadingAtLevel3 Command = "document-next-heading-at-level-3" DocumentPreviousHeadingAtLevel3 Command = "document-previous-heading-at-level-3" DocumentNextHeadingAtLevel4 Command = "document-next-heading-at-level-4" DocumentPreviousHeadingAtLevel4 Command = "document-previous-heading-at-level-4" DocumentNextHeadingAtLevel5 Command = "document-next-heading-at-level-5" DocumentPreviousHeadingAtLevel5 Command = "document-previous-heading-at-level-5" DocumentNextHeadingAtLevel6 Command = "document-next-heading-at-level-6" DocumentPreviousHeadingAtLevel6 Command = "document-previous-heading-at-level-6" DocumentNextBlockQuote Command = "document-next-block-quote" DocumentPreviousBlockQuote Command = "document-previous-block-quote" DocumentNextComboBox Command = "document-next-combo-box" DocumentPreviousComboBox Command = "document-previous-combo-box" DocumentNextEntry Command = "document-next-entry" DocumentPreviousEntry Command = "document-previous-entry" DocumentNextFormField Command = "document-next-form-field" DocumentPreviousFormField Command = "document-previous-form-field" DocumentNextImage Command = "document-next-image" DocumentPreviousImage Command = "document-previous-image" DocumentNextLandmark Command = "document-next-landmark" DocumentPreviousLandmark Command = "document-previous-landmark" DocumentNextListItem Command = "document-next-list-item" DocumentPreviousListItem Command = "document-previous-list-item" DocumentNextLiveRegion Command = "document-next-live-region" DocumentPreviousLiveRegion Command = "document-previous-live-region" DocumentNextParagraph Command = "document-next-paragraph" DocumentPreviousParagraph Command = "document-previous-paragraph" DocumentNextRadioButton Command = "document-next-radio-button" DocumentPreviousRadioButton Command = "document-previous-radio-button" DocumentNextTable Command = "document-next-table" DocumentPreviousTable Command = "document-previous-table" DocumentNextUnvisitedLink Command = "document-next-unvisited-link" DocumentPreviousUnvisitedLink Command = "document-previous-unvisited-link" DocumentNextVisitedLink Command = "document-next-visited-link" DocumentPreviousVisitedLink Command = "document-previous-visited-link" DocumentNextClickable Command = "document-next-clickable" DocumentPreviousClickable Command = "document-previous-clickable" DocumentNextChunk Command = "document-next-chunk" DocumentPreviousChunk Command = "document-previous-chunk" DocumentCaretNextCharacter Command = "document-caret-next-character" DocumentCaretPreviousCharacter Command = "document-caret-previous-character" DocumentCaretNextWord Command = "document-caret-next-word" DocumentCaretPreviousWord Command = "document-caret-previous-word" DocumentCaretNextLine Command = "document-caret-next-line" DocumentCaretPreviousLine Command = "document-caret-previous-line" DocumentCaretLineStart Command = "document-caret-line-start" DocumentCaretLineEnd Command = "document-caret-line-end" DocumentCaretDocumentStart Command = "document-caret-document-start" DocumentCaretDocumentEnd Command = "document-caret-document-end" DocumentTableLeft Command = "document-table-left" DocumentTableRight Command = "document-table-right" DocumentTableUp Command = "document-table-up" DocumentTableDown Command = "document-table-down" DocumentTableFirst Command = "document-table-first" DocumentTableLast Command = "document-table-last" DocumentActivate Command = "document-activate" ReadAll Command = "read-all" DisableTentacles Command = "disable-tentacles" Quit Command = "quit" Preferences Command = "preferences" ApplicationPreferences Command = "application-preferences" ChooseApplicationPreferences Command = "choose-application-preferences" PresentCellFormula Command = "present-cell-formula" WhereAmI Command = "where-am-i" PresentCharacterAttributes Command = "present-character-attributes" PresentLastNotification Command = "present-last-notification" PresentPreviousNotification Command = "present-previous-notification" PresentNextNotification Command = "present-next-notification" ShowNotificationList Command = "show-notification-list" ObjectParent Command = "object-parent" ObjectFirstChild Command = "object-first-child" ObjectPreviousSibling Command = "object-previous-sibling" ObjectNextSibling Command = "object-next-sibling" ReviewWindowStart Command = "review-window-start" ReviewWindowEnd Command = "review-window-end" ReviewPreviousLine Command = "review-previous-line" ReviewCurrentLine Command = "review-current-line" ReviewNextLine Command = "review-next-line" ReviewPreviousWord Command = "review-previous-word" ReviewCurrentWord Command = "review-current-word" ReviewSpellWord Command = "review-spell-word" ReviewPhoneticWord Command = "review-phonetic-word" ReviewNextWord Command = "review-next-word" ReviewPreviousCharacter Command = "review-previous-character" ReviewCurrentCharacter Command = "review-current-character" ReviewPhoneticCharacter Command = "review-phonetic-character" ReviewNextCharacter Command = "review-next-character" ReviewWordAbove Command = "review-word-above" ReviewWordBelow Command = "review-word-below" ReviewLineEnd Command = "review-line-end" ) type KeyboardLayout int const ( DesktopKeyboardLayout KeyboardLayout = 1 LaptopKeyboardLayout KeyboardLayout = 2 ) const ( returnKeySymbol uint32 = 0xff0d keypadEnterKeySymbol uint32 = 0xff8d leftKeySymbol uint32 = 0xff51 upKeySymbol uint32 = 0xff52 rightKeySymbol uint32 = 0xff53 downKeySymbol uint32 = 0xff54 leftControlKeySymbol uint32 = 0xffe3 rightControlKeySymbol uint32 = 0xffe4 leftShiftKeySymbol uint32 = 0xffe1 rightShiftKeySymbol uint32 = 0xffe2 controlModifier uint16 = 1 << 2 ) type CommandChord struct { Symbol uint32 Modifiers uint16 CthulhuModifier bool } type CommandBindings map[CommandChord]Command // CommandEvent retains the initiating X server time across asynchronous delivery. type CommandEvent struct { Command Command Timestamp uint32 Chord CommandChord KeyName string LearnModeStarted bool UISession uint64 Text string } type LockState struct { Name string Locked bool Timestamp uint32 } func (state LockState) Text() string { if state.Locked { return state.Name + " on" } return state.Name + " off" } func DefaultCommandBindings(layout KeyboardLayout) CommandBindings { bindings := CommandBindings{ {Symbol: 'a', Modifiers: 1, CthulhuModifier: true}: ShowActions, {Symbol: leftKeySymbol, CthulhuModifier: true}: SelectPreviousSpeechSetting, {Symbol: rightKeySymbol, CthulhuModifier: true}: SelectNextSpeechSetting, {Symbol: downKeySymbol, CthulhuModifier: true}: DecreaseSpeechSetting, {Symbol: upKeySymbol, CthulhuModifier: true}: IncreaseSpeechSetting, {Symbol: 's', Modifiers: controlModifier | 1, CthulhuModifier: true}: SaveSpeechSettings, {Symbol: 't', CthulhuModifier: true}: PresentTime, {Symbol: 'c', Modifiers: 1, CthulhuModifier: true}: PresentClipboard, {Symbol: 'h', Modifiers: controlModifier, CthulhuModifier: true}: ShowSpeechHistory, {Symbol: 'd', Modifiers: 1, CthulhuModifier: true}: ToggleSpeechMonitor, {Symbol: 's', CthulhuModifier: true}: CycleSpeechMode, {Symbol: 'u', CthulhuModifier: true}: CycleProgressBarOutput, {Symbol: 'v', Modifiers: 1, CthulhuModifier: true}: PresentVersion, {Symbol: 'a', CthulhuModifier: true}: ToggleDocumentMode, {Symbol: 'h', CthulhuModifier: true}: ToggleLearnMode, {Symbol: 0xffbe, CthulhuModifier: true}: ToggleLearnMode, {Symbol: 'h', Modifiers: 1, CthulhuModifier: true}: DocumentPreviousHeading, {Symbol: 'k', CthulhuModifier: true, Modifiers: controlModifier}: DocumentNextLink, {Symbol: 'k', Modifiers: controlModifier | 1, CthulhuModifier: true}: DocumentPreviousLink, {Symbol: 'q', CthulhuModifier: true}: Quit, {Symbol: 'q', Modifiers: 5, CthulhuModifier: true}: DisableTentacles, {Symbol: 'n', CthulhuModifier: true}: PresentLastNotification, {Symbol: 'n', Modifiers: controlModifier, CthulhuModifier: true}: ShowNotificationList, {Symbol: ' ', CthulhuModifier: true}: Preferences, {Symbol: ' ', Modifiers: controlModifier, CthulhuModifier: true}: ApplicationPreferences, {Symbol: ' ', Modifiers: 1, CthulhuModifier: true}: ApplicationPreferences, {Symbol: ' ', Modifiers: controlModifier | 1, CthulhuModifier: true}: ChooseApplicationPreferences, {Symbol: 'f', CthulhuModifier: true}: PresentCharacterAttributes, {Symbol: upKeySymbol, Modifiers: controlModifier, CthulhuModifier: true}: ObjectParent, {Symbol: downKeySymbol, Modifiers: controlModifier, CthulhuModifier: true}: ObjectFirstChild, {Symbol: leftKeySymbol, Modifiers: controlModifier, CthulhuModifier: true}: ObjectPreviousSibling, {Symbol: rightKeySymbol, Modifiers: controlModifier, CthulhuModifier: true}: ObjectNextSibling, } if layout == DesktopKeyboardLayout { bindings[CommandChord{Symbol: 'o', CthulhuModifier: true}] = OCRWindow bindings[CommandChord{Symbol: 0xffab}] = ReadAll // KP_Add bindings[CommandChord{Symbol: keypadEnterKeySymbol}] = WhereAmI bindings[CommandChord{Symbol: '=', CthulhuModifier: true}] = PresentCellFormula bindings[CommandChord{Symbol: 0xff95, CthulhuModifier: true}] = ReviewWindowStart bindings[CommandChord{Symbol: 0xff9a, CthulhuModifier: true}] = ReviewWindowEnd bindings[CommandChord{Symbol: 0xff96, CthulhuModifier: true}] = ReviewWordAbove bindings[CommandChord{Symbol: 0xff98, CthulhuModifier: true}] = ReviewWordBelow bindings[CommandChord{Symbol: 0xff9c, CthulhuModifier: true}] = ReviewLineEnd } else if layout == LaptopKeyboardLayout { bindings[CommandChord{Symbol: 'o', Modifiers: 1, CthulhuModifier: true}] = OCRWindow bindings[CommandChord{Symbol: 'u', Modifiers: 1, CthulhuModifier: true}] = CycleProgressBarOutput bindings[CommandChord{Symbol: ';', CthulhuModifier: true}] = ReadAll bindings[CommandChord{Symbol: returnKeySymbol, CthulhuModifier: true}] = WhereAmI bindings[CommandChord{Symbol: 'u', Modifiers: controlModifier, CthulhuModifier: true}] = ReviewWindowStart bindings[CommandChord{Symbol: 'o', Modifiers: controlModifier, CthulhuModifier: true}] = ReviewWindowEnd bindings[CommandChord{Symbol: 'j', Modifiers: controlModifier, CthulhuModifier: true}] = ReviewWordAbove bindings[CommandChord{Symbol: 'l', Modifiers: controlModifier, CthulhuModifier: true}] = ReviewWordBelow bindings[CommandChord{Symbol: 'm', Modifiers: controlModifier, CthulhuModifier: true}] = ReviewLineEnd } for _, binding := range []struct { desktop, laptop uint32 command Command }{ {0xff95, 'u', ReviewPreviousLine}, {0xff97, 'i', ReviewCurrentLine}, {0xff9a, 'o', ReviewNextLine}, {0xff96, 'j', ReviewPreviousWord}, {0xff9d, 'k', ReviewCurrentWord}, {0xff98, 'l', ReviewNextWord}, {0xff9c, 'm', ReviewPreviousCharacter}, {0xff99, ',', ReviewCurrentCharacter}, {0xff9b, '.', ReviewNextCharacter}, } { if layout == DesktopKeyboardLayout { bindings[CommandChord{Symbol: binding.desktop}] = binding.command } if layout == LaptopKeyboardLayout { bindings[CommandChord{Symbol: binding.laptop, CthulhuModifier: true}] = binding.command } } return bindings } // CommandRouter owns the pressed state of screen-reader modifiers and consumed // command keys. Commands are emitted on press; native owners retain the grab // independently until the chord releases, including during handler shutdown. type CommandRouter struct { now func() time.Time lastTimePress time.Time lastModePress time.Time modePresses int lastReviewPress time.Time lastReviewChord CommandChord reviewPresses int modifiers map[uint32]bool bindings CommandBindings down map[uint16]bool consumed map[uint16]bool } func NewCommandRouter(modifiers []uint32, bindings CommandBindings) *CommandRouter { router := &CommandRouter{ now: time.Now, modifiers: make(map[uint32]bool), bindings: make(CommandBindings, len(bindings)), down: make(map[uint16]bool), consumed: make(map[uint16]bool), } for _, symbol := range modifiers { router.modifiers[symbol] = true } for chord, command := range bindings { router.bindings[chord] = command } return router } func (router *CommandRouter) Handle(symbol uint32, code uint16, modifiers uint16, pressed bool) (bool, Command) { consume := false if !pressed { consume = router.down[code] || router.consumed[code] delete(router.down, code) delete(router.consumed, code) } else if router.modifiers[symbol] { router.down[code] = true consume = true } else if router.consumed[code] { consume = true } else { chord := CommandChord{ Symbol: symbol, Modifiers: modifiers &^ uint16(2|16), CthulhuModifier: len(router.down) != 0, } if command := router.bindings[chord]; command != "" { router.consumed[code] = true command = router.review_press(command, chord) if command == ToggleDocumentMode { now := router.now() if !router.lastModePress.IsZero() && now.Sub(router.lastModePress) >= 0 && now.Sub(router.lastModePress) <= 500*time.Millisecond { router.modePresses++ } else { router.modePresses = 1 } router.lastModePress = now switch router.modePresses { case 2: command = StickyFocusMode case 3: command = StickyBrowseMode router.lastModePress = time.Time{} } } else { router.lastModePress = time.Time{} } if command == PresentTime { now := router.now() if !router.lastTimePress.IsZero() && now.Sub(router.lastTimePress) >= 0 && now.Sub(router.lastTimePress) <= 500*time.Millisecond { command = PresentDate router.lastTimePress = time.Time{} } else { router.lastTimePress = now } } else { router.lastTimePress = time.Time{} } return true, command } router.lastTimePress = time.Time{} router.lastModePress = time.Time{} router.lastReviewPress = time.Time{} } return consume, "" } func (router *CommandRouter) IsModifier(symbol uint32) bool { switch symbol { case leftControlKeySymbol, rightControlKeySymbol, leftShiftKeySymbol, rightShiftKeySymbol, 0xffe7, 0xffe8, 0xffe9, 0xffea, 0xffeb, 0xffec, 0xffed, 0xffee: // Meta, Alt, Super, Hyper. return true } return router.modifiers[symbol] } // Chord reports the keys currently held before handling the next press. func (router *CommandRouter) Chord(symbol uint32, modifiers uint16) CommandChord { return CommandChord{Symbol: symbol, Modifiers: modifiers &^ uint16(2|16), CthulhuModifier: len(router.down) != 0} } func (router *CommandRouter) Reset() { clear(router.down) clear(router.consumed) } // Browse bindings are installed only while the runtime owns live web content. func BrowseCommandBindings(layout KeyboardLayout) CommandBindings { bindings := DefaultCommandBindings(layout) caretBindings := CommandBindings{ {Symbol: leftKeySymbol, Modifiers: 1 | 8}: DocumentTableLeft, {Symbol: rightKeySymbol, Modifiers: 1 | 8}: DocumentTableRight, {Symbol: upKeySymbol, Modifiers: 1 | 8}: DocumentTableUp, {Symbol: downKeySymbol, Modifiers: 1 | 8}: DocumentTableDown, {Symbol: 0xff50, Modifiers: 1 | 8}: DocumentTableFirst, {Symbol: 0xff57, Modifiers: 1 | 8}: DocumentTableLast, {Symbol: rightKeySymbol}: DocumentCaretNextCharacter, {Symbol: leftKeySymbol}: DocumentCaretPreviousCharacter, {Symbol: rightKeySymbol, Modifiers: controlModifier}: DocumentCaretNextWord, {Symbol: leftKeySymbol, Modifiers: controlModifier}: DocumentCaretPreviousWord, {Symbol: downKeySymbol}: DocumentCaretNextLine, {Symbol: upKeySymbol}: DocumentCaretPreviousLine, {Symbol: 0xff50}: DocumentCaretLineStart, {Symbol: 0xff57}: DocumentCaretLineEnd, {Symbol: 0xff50, Modifiers: controlModifier}: DocumentCaretDocumentStart, {Symbol: 0xff57, Modifiers: controlModifier}: DocumentCaretDocumentEnd, {Symbol: returnKeySymbol}: DocumentActivate, } for chord, command := range caretBindings { bindings[chord] = command } documentBindings := []struct { symbol uint32 next Command previous Command }{ {'h', DocumentNextHeading, DocumentPreviousHeading}, {'k', DocumentNextLink, DocumentPreviousLink}, {'b', DocumentNextButton, DocumentPreviousButton}, {'x', DocumentNextCheckBox, DocumentPreviousCheckBox}, {'l', DocumentNextList, DocumentPreviousList}, {'s', DocumentNextSeparator, DocumentPreviousSeparator}, {'q', DocumentNextBlockQuote, DocumentPreviousBlockQuote}, {'c', DocumentNextComboBox, DocumentPreviousComboBox}, {'e', DocumentNextEntry, DocumentPreviousEntry}, {'f', DocumentNextFormField, DocumentPreviousFormField}, {'g', DocumentNextImage, DocumentPreviousImage}, {'m', DocumentNextLandmark, DocumentPreviousLandmark}, {'i', DocumentNextListItem, DocumentPreviousListItem}, {'d', DocumentNextLiveRegion, DocumentPreviousLiveRegion}, {'p', DocumentNextParagraph, DocumentPreviousParagraph}, {'r', DocumentNextRadioButton, DocumentPreviousRadioButton}, {'t', DocumentNextTable, DocumentPreviousTable}, {'u', DocumentNextUnvisitedLink, DocumentPreviousUnvisitedLink}, {'v', DocumentNextVisitedLink, DocumentPreviousVisitedLink}, {'a', DocumentNextClickable, DocumentPreviousClickable}, {'o', DocumentNextChunk, DocumentPreviousChunk}, } for _, binding := range documentBindings { bindings[CommandChord{Symbol: binding.symbol}] = binding.next bindings[CommandChord{Symbol: binding.symbol, Modifiers: 1}] = binding.previous } headingLevels := []struct { level int next Command previous Command }{ {1, DocumentNextHeadingAtLevel1, DocumentPreviousHeadingAtLevel1}, {2, DocumentNextHeadingAtLevel2, DocumentPreviousHeadingAtLevel2}, {3, DocumentNextHeadingAtLevel3, DocumentPreviousHeadingAtLevel3}, {4, DocumentNextHeadingAtLevel4, DocumentPreviousHeadingAtLevel4}, {5, DocumentNextHeadingAtLevel5, DocumentPreviousHeadingAtLevel5}, {6, DocumentNextHeadingAtLevel6, DocumentPreviousHeadingAtLevel6}, } for _, binding := range headingLevels { symbol := uint32('0' + binding.level) bindings[CommandChord{Symbol: symbol}] = binding.next bindings[CommandChord{Symbol: symbol, Modifiers: 1}] = binding.previous } return bindings } // ReplaceBindings preserves multi-press timing across browse grab changes. // Native owners call this only after every consumed key has been released. func (router *CommandRouter) ReplaceBindings(bindings CommandBindings) { router.bindings = make(CommandBindings, len(bindings)) for chord, command := range bindings { router.bindings[chord] = command } }