diff --git a/emoji.go b/emoji.go index d90c942..3234228 100644 --- a/emoji.go +++ b/emoji.go @@ -69,14 +69,14 @@ func NewEmojiEngine() *EmojiEngine { } func (be *EmojiEngine) MatchString(s string) bool { - var lookup = FindPrefix(emojiTrie, s) + var lookup = FindPrefix(currentEmojiTrie(), s) return lookup != nil } func (be *EmojiEngine) Filter(s string) []string { var codePoints []string var keys []string - var lookup = FindPrefix(emojiTrie, s) + var lookup = FindPrefix(currentEmojiTrie(), s) for key := range lookup { keys = append(keys, key) } diff --git a/emoji_test.go b/emoji_test.go index e7d74be..4a153d1 100644 --- a/emoji_test.go +++ b/emoji_test.go @@ -23,7 +23,9 @@ import ( ) func TestEmojiFindResult(t *testing.T) { - emojiTrie, _ = loadEmojiOne(DictEmojiOne) + if trie, err := loadEmojiOne(DictEmojiOne); err == nil { + emojiTrie.Store(trie) + } var be = NewEmojiEngine() if be.MatchString(":'") != true { t.Errorf("Finding result for emoji :', expected true, got %v", be.MatchString(":'")) @@ -40,7 +42,9 @@ func TestEmojiFindResult(t *testing.T) { } func TestFilterEmoji(t *testing.T) { - emojiTrie, _ = loadEmojiOne(DictEmojiOne) + if trie, err := loadEmojiOne(DictEmojiOne); err == nil { + emojiTrie.Store(trie) + } var be = NewEmojiEngine() var grinnings = be.Filter(":')") if !inStringList(grinnings, "😂") { diff --git a/engine.go b/engine.go index 3533919..89da854 100644 --- a/engine.go +++ b/engine.go @@ -26,6 +26,7 @@ import ( "reflect" "strconv" "sync" + "sync/atomic" "github.com/BambooEngine/bamboo-core" ibus "github.com/BambooEngine/goibus" @@ -35,42 +36,62 @@ import ( "ibus-bamboo/ui" ) +// Locking convention: IBus invokes engine methods on D-Bus threads while a +// per-engine goroutine drains keyPressChan. Every method that touches the +// preeditor or composition state must hold e's mutex, except helpers +// documented as requiring the caller to hold it. Slow or blocking work +// (X11/Wayland lookups, GUI, notifications) stays outside the lock. type IBusBambooEngine struct { sync.Mutex IEngine - preeditor bamboo.IEngine - engineName string - config *config.Config - propList *ibus.PropList - englishMode bool - macroTable *MacroTable - wmClasses string - isInputModeLTOpened bool - isEmojiLTOpened bool - isInHexadecimal bool - emojiLookupTable *ibus.LookupTable - inputModeLookupTable *ibus.LookupTable - capabilities uint32 - keyPressDelay int - nFakeBackSpace int32 - isFirstTimeSendingBS bool - emoji *EmojiEngine - isSurroundingTextReady bool - lastKeyWithShift bool - lastCommitText int64 + preeditor bamboo.IEngine + engineName string + config *config.Config + propList *ibus.PropList + englishMode bool + macroTable *MacroTable + wmClasses string + isInputModeLTOpened bool + isEmojiLTOpened bool + isInHexadecimal bool + emojiLookupTable *ibus.LookupTable + inputModeLookupTable *ibus.LookupTable + capabilities uint32 + keyPressDelay int + nFakeBackSpace int32 + isFirstTimeSendingBS bool + emoji *EmojiEngine + surroundingTextReady bool + lastKeyWithShift bool + lastCommitText int64 // restore key strokes by pressing Shift + Space shouldRestoreKeyStrokes bool // enqueue key strokes to process later shouldEnqueuKeyStrokes bool + // per-engine key queue; replaces the former process-wide globals so + // concurrently created engines (Bamboo, BambooUs, variants, daemon + // re-initialization) can never consume each other's keystrokes. + keyPressChan chan [3]uint32 + keyQueueLen int32 // atomically counted pending queue items } func NewIbusBambooEngine(name string, cfg *config.Config, base IEngine, preeditor bamboo.IEngine) *IBusBambooEngine { - return &IBusBambooEngine{ + e := &IBusBambooEngine{ engineName: name, IEngine: base, preeditor: preeditor, config: cfg, } + // Initialized here (not in the async init) so no key/focus/property + // handler can observe a nil emoji engine or macro table on startup. + e.emoji = NewEmojiEngine() + e.macroTable = NewMacroTable(cfg.IBflags&config.IBautoCapitalizeMacro != 0) + if cfg.IBflags&config.IBmacroEnabled != 0 { + e.macroTable.Enable(name) + } + e.keyPressChan = make(chan [3]uint32, 100) + go e.keyPressCapturing() + return e } /* @@ -97,8 +118,10 @@ func (e *IBusBambooEngine) ProcessKeyEvent(keyVal uint32, keyCode uint32, state // fmt.Println("Ignore key-up event") return false, nil } + e.Lock() + defer e.Unlock() fmt.Printf("\n") - log.Printf(">>>>ProcessKeyEvent > %d | state %d keyVal 0x%04x | %c <<<<\n", len(keyPressChan), state, keyVal, rune(keyVal)) + log.Printf(">>>>ProcessKeyEvent > %d | state %d keyVal 0x%04x | %c <<<<\n", atomic.LoadInt32(&e.keyQueueLen), state, keyVal, rune(keyVal)) if ret, retValue := e.processShortcutKey(keyVal, keyCode, state); ret { return retValue, nil } @@ -110,83 +133,149 @@ func (e *IBusBambooEngine) ProcessKeyEvent(keyVal uint32, keyCode uint32, state func (e *IBusBambooEngine) FocusIn() *dbus.Error { log.Print("FocusIn.") + // Slow lookups stay outside the engine lock. var latestWm = e.getLatestWmClass() + e.Lock() e.checkWmClass(latestWm) - e.RegisterProperties(e.propList) + propList := e.propList + emojiShortcut := e.isShortcutKeyEnable(KSEmojiDialog) + ibflags := e.config.IBflags + e.surroundingTextReady = true + e.Unlock() + e.RegisterProperties(propList) e.RequireSurroundingText() - if e.isShortcutKeyEnable(KSEmojiDialog) && emojiTrie != nil && len(emojiTrie.Children) == 0 { - var err error - emojiTrie, err = loadEmojiOne(DictEmojiOne) - if err != nil { - panic(fmt.Sprintf("failed to load emojiTrie from %s: %s", DictEmojiOne, err)) - } - } - if e.config.IBflags&config.IBspellCheckWithDicts != 0 && len(dictionary) == 0 { - dictionary, _ = loadDictionary(DictVietnameseCm) - } - fmt.Printf("WM_CLASS=(%s)\n", e.getWmClass()) + e.ensureLazyData(emojiShortcut, ibflags) + fmt.Printf("WM_CLASS=(%s)\n", latestWm) return nil } func (e *IBusBambooEngine) FocusOut() *dbus.Error { log.Print("FocusOut.") + e.Lock() + defer e.Unlock() + // Commit pending preedit to the losing window now; otherwise the next + // FocusIn would commit it into the newly focused window. + e.resetBuffer() + e.resetFakeBackspace() + if e.isEmojiLTOpened { + e.closeEmojiCandidates() + } + if e.isInHexadecimal { + e.closeHexadecimalInput() + } + if e.isInputModeLTOpened { + e.closeInputModeCandidates() + } return nil } func (e *IBusBambooEngine) Reset() *dbus.Error { fmt.Print("Reset.\n") - if e.checkInputMode(config.PreeditIM) { - e.preeditor.Reset() - } + e.Lock() + defer e.Unlock() + // Reset must clear the composition buffer in every input mode; in + // backspace modes the buffer would otherwise leak into the next word. + e.preeditor.Reset() + e.resetFakeBackspace() + e.HidePreeditText() + e.HideAuxiliaryText() return nil } func (e *IBusBambooEngine) Enable() *dbus.Error { fmt.Print("Enable.") + e.Lock() + e.surroundingTextReady = true + e.Unlock() e.RequireSurroundingText() return nil } func (e *IBusBambooEngine) Disable() *dbus.Error { fmt.Print("Disable.") + e.Lock() + defer e.Unlock() + // Drop pending state without committing: the user explicitly stopped + // the engine, so nothing may leak into the next Enable. + e.preeditor.Reset() + e.resetFakeBackspace() + e.HidePreeditText() + e.HideAuxiliaryText() return nil } +// extractSurroundingString decodes the text payload of IBus +// SetSurroundingText, tolerating the shapes godbus may deliver: a plain +// string, a nested variant, the raw (text, attrs) tuple, or a goibus Text. +func extractSurroundingString(v interface{}) (string, bool) { + if v == nil { + return "", false + } + if s, ok := v.(string); ok { + return s, true + } + if vv, ok := v.(dbus.Variant); ok { + return extractSurroundingString(vv.Value()) + } + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.Slice, reflect.Array: + if rv.Len() > 2 { + return extractSurroundingString(rv.Index(2).Interface()) + } + case reflect.Struct: + // goibus.Text carries the string in its third field. + if rv.NumField() > 2 { + if f := rv.Field(2); f.Kind() == reflect.String { + return f.String(), true + } else if f.CanInterface() { + return extractSurroundingString(f.Interface()) + } + } + } + return "", false +} + // @method(in_signature="vuu") func (e *IBusBambooEngine) SetSurroundingText(text dbus.Variant, cursorPos uint32, anchorPos uint32) *dbus.Error { - if !e.isSurroundingTextReady { + e.Lock() + defer e.Unlock() + if !e.surroundingTextReady { //fmt.Println("Surrounding Text is not ready yet.") return nil } - e.Lock() - defer func() { - e.Unlock() - e.isSurroundingTextReady = false - if err := recover(); err != nil { - fmt.Println(err) - } - }() - if e.inBackspaceWhiteList() { - var str = reflect.ValueOf(reflect.ValueOf(text.Value()).Index(2).Interface()).String() - var s = []rune(str) - if len(s) < int(cursorPos) { - return nil - } - var cs = s[:cursorPos] - fmt.Println("Surrounding Text: ", string(cs)) - e.preeditor.Reset() - for i := len(cs) - 1; i >= 0; i-- { - // workaround for spell checking - if bamboo.IsPunctuationMark(cs[i]) && e.preeditor.CanProcessKey(cs[i]) { - cs[i] = ' ' - } - e.preeditor.ProcessKey(cs[i], bamboo.EnglishMode|bamboo.InReverseOrder) + e.surroundingTextReady = false + if anchorPos != cursorPos { + // A selection is active; don't guess the buffer from one side of it. + return nil + } + if !e.inBackspaceWhiteList() { + return nil + } + str, ok := extractSurroundingString(text.Value()) + if !ok { + return nil + } + var s = []rune(str) + if len(s) < int(cursorPos) { + return nil + } + var cs = s[:cursorPos] + fmt.Println("Surrounding Text: ", string(cs)) + e.preeditor.Reset() + for i := len(cs) - 1; i >= 0; i-- { + // workaround for spell checking + if bamboo.IsPunctuationMark(cs[i]) && e.preeditor.CanProcessKey(cs[i]) { + cs[i] = ' ' } + e.preeditor.ProcessKey(cs[i], bamboo.EnglishMode|bamboo.InReverseOrder) } return nil } func (e *IBusBambooEngine) PageUp() *dbus.Error { + e.Lock() + defer e.Unlock() if e.isEmojiLTOpened && e.emojiLookupTable.PageUp() { e.updateEmojiLookupTable() } @@ -197,6 +286,8 @@ func (e *IBusBambooEngine) PageUp() *dbus.Error { } func (e *IBusBambooEngine) PageDown() *dbus.Error { + e.Lock() + defer e.Unlock() if e.isEmojiLTOpened && e.emojiLookupTable.PageDown() { e.updateEmojiLookupTable() } @@ -207,6 +298,8 @@ func (e *IBusBambooEngine) PageDown() *dbus.Error { } func (e *IBusBambooEngine) CursorUp() *dbus.Error { + e.Lock() + defer e.Unlock() if e.isEmojiLTOpened && e.emojiLookupTable.CursorUp() { e.updateEmojiLookupTable() } @@ -217,6 +310,8 @@ func (e *IBusBambooEngine) CursorUp() *dbus.Error { } func (e *IBusBambooEngine) CursorDown() *dbus.Error { + e.Lock() + defer e.Unlock() if e.isEmojiLTOpened && e.emojiLookupTable.CursorDown() { e.updateEmojiLookupTable() } @@ -227,6 +322,8 @@ func (e *IBusBambooEngine) CursorDown() *dbus.Error { } func (e *IBusBambooEngine) CandidateClicked(index uint32, button uint32, state uint32) *dbus.Error { + e.Lock() + defer e.Unlock() if e.isEmojiLTOpened && e.updateCursorPosInEmojiTable(index) { e.commitEmojiCandidate() e.closeEmojiCandidates() @@ -239,7 +336,9 @@ func (e *IBusBambooEngine) CandidateClicked(index uint32, button uint32, state u } func (e *IBusBambooEngine) SetCapabilities(cap uint32) *dbus.Error { + e.Lock() e.capabilities = cap + e.Unlock() return nil } @@ -263,19 +362,27 @@ func (e *IBusBambooEngine) PropertyActivate(propName string, propState uint32) * } if propName == PropKeyConfiguration { ui.OpenGUI(e.engineName) + e.Lock() e.config = config.LoadConfig(e.engineName) + e.Unlock() return nil } if propName == PropKeyInputModeLookupTableShortcut { ui.OpenGUI(e.engineName) + e.Lock() e.config = config.LoadConfig(e.engineName) + e.Unlock() return nil } if propName == PropKeyMacroTable { ui.OpenGUI(e.engineName) + e.Lock() e.config = config.LoadConfig(e.engineName) + e.Unlock() return nil } + e.Lock() + defer e.Unlock() turnSpellChecking := func(on bool) { if on { @@ -323,7 +430,11 @@ func (e *IBusBambooEngine) PropertyActivate(propName string, propState uint32) * if propState == ibus.PROP_STATE_CHECKED { e.config.IBflags |= config.IBspellCheckWithDicts turnSpellChecking(true) - dictionary, _ = loadDictionary(DictVietnameseCm) + if d, err := loadDictionary(DictVietnameseCm); err != nil { + log.Printf("failed to load dictionary from %s: %s", DictVietnameseCm, err) + } else { + dictionary.Store(d) + } } else { e.config.IBflags &= ^config.IBspellCheckWithDicts } diff --git a/engine_backspace.go b/engine_backspace.go index cf40845..bfc1fe4 100644 --- a/engine_backspace.go +++ b/engine_backspace.go @@ -24,6 +24,7 @@ import ( "ibus-bamboo/config" "log" "strings" + "sync/atomic" "time" "unicode/utf8" @@ -31,17 +32,20 @@ import ( "github.com/godbus/dbus/v5" ) +// bsProcessKeyEvent, keyPressForwardHandler and keyPressHandler below must +// be called with the engine lock held (see IBusBambooEngine). + const BACKSPACE_INTERVAL = 0 func (e *IBusBambooEngine) bsProcessKeyEvent(keyVal uint32, keyCode uint32, state uint32) (bool, *dbus.Error) { if isMovementKey(keyVal) { e.preeditor.Reset() e.resetFakeBackspace() - e.isSurroundingTextReady = true + e.surroundingTextReady = true return false, nil } var keyRune = rune(keyVal) - if e.config.IBflags&config.IBmacroEnabled == 0 && len(keyPressChan) == 0 && e.getRawKeyLen() == 0 && !inKeyList(e.preeditor.GetInputMethod().AppendingKeys, keyRune) { + if e.config.IBflags&config.IBmacroEnabled == 0 && atomic.LoadInt32(&e.keyQueueLen) == 0 && e.getRawKeyLen() == 0 && !inKeyList(e.preeditor.GetInputMethod().AppendingKeys, keyRune) { e.updateLastKeyWithShift(keyVal, state) if e.preeditor.CanProcessKey(keyRune) && isValidState(state) { e.isFirstTimeSendingBS = true @@ -63,7 +67,7 @@ func (e *IBusBambooEngine) bsProcessKeyEvent(keyVal uint32, keyCode uint32, stat e.addFakeBackspace(-1) return false, nil } else { - sleep() + e.waitForQueueDrain() if e.getRawKeyLen() > 0 { if e.shouldFallbackToEnglish(true) { e.preeditor.RestoreLastWord(false) @@ -74,7 +78,7 @@ func (e *IBusBambooEngine) bsProcessKeyEvent(keyVal uint32, keyCode uint32, stat return false, nil } if keyVal == IBusTab { - sleep() + e.waitForQueueDrain() if ok, _ := e.getMacroText(); !ok { e.preeditor.Reset() return false, nil @@ -82,13 +86,18 @@ func (e *IBusBambooEngine) bsProcessKeyEvent(keyVal uint32, keyCode uint32, stat } isValidKey := isValidState(state) && e.isValidKeyVal(keyVal) if !isValidKey { - sleep() + e.waitForQueueDrain() return e.keyPressHandler(keyVal, keyCode, state), nil } } // if the main thread is busy processing, the keypress events come all mixed up - // so we enqueue these keypress events and process them sequentially on another thread - keyPressChan <- [3]uint32{keyVal, keyCode, state} + // so we enqueue these keypress events and process them sequentially on another thread. + // The engine lock is released across the send: the consumer needs it + // to drain, and the send may briefly block when the queue is full. + // No state is touched after re-locking, so this is safe. + e.Unlock() + e.enqueueKeyPress(keyVal, keyCode, state) + e.Lock() return true, nil } else { return e.keyPressHandler(keyVal, keyCode, state), nil @@ -103,7 +112,7 @@ func (e *IBusBambooEngine) keyPressForwardHandler(keyVal, keyCode, state uint32) } func (e *IBusBambooEngine) keyPressHandler(keyVal, keyCode, state uint32) bool { - // log.Printf(">>Backspace:ProcessKeyEvent > %c | keyCode 0x%04x keyVal 0x%04x | %d\n", rune(keyVal), keyCode, keyVal, len(keyPressChan)) + // log.Printf(">>Backspace:ProcessKeyEvent > %c | keyCode 0x%04x keyVal 0x%04x | %d\n", rune(keyVal), keyCode, keyVal, atomic.LoadInt32(&e.keyQueueLen)) defer e.updateLastKeyWithShift(keyVal, state) if e.keyPressDelay > 0 { time.Sleep(time.Duration(e.keyPressDelay) * time.Millisecond) @@ -200,8 +209,8 @@ func (e *IBusBambooEngine) updatePreviousTextInBatch(oldText, newText string, is } // isDirty means containing runes that are not committed var isDirty = false - for i := 0; i < len(keyPressChan); i++ { - var keyEvents = <-keyPressChan + for i := 0; i < len(e.keyPressChan); i++ { + var keyEvents = <-e.keyPressChan var keyVal, keyCode, state = keyEvents[0], keyEvents[1], keyEvents[2] isValidKey := isValidState(state) && e.isValidKeyVal(keyVal) if isValidKey { diff --git a/engine_emoji.go b/engine_emoji.go index 53d7fc3..d1e4599 100644 --- a/engine_emoji.go +++ b/engine_emoji.go @@ -147,12 +147,12 @@ func (e *IBusBambooEngine) emojiProcessKeyEvent(keyVal uint32, keyCode uint32, s func (e *IBusBambooEngine) updateCursorPosInEmojiTable(idx uint32) bool { pageSize := e.emojiLookupTable.PageSize - if idx > pageSize { + if idx >= pageSize { return false } page := e.emojiLookupTable.CursorPos / pageSize newPos := page*pageSize + idx - if int(newPos) > len(e.emojiLookupTable.Candidates) { + if int(newPos) >= len(e.emojiLookupTable.Candidates) { return false } e.emojiLookupTable.CursorPos = newPos diff --git a/engine_preedit.go b/engine_preedit.go index 3db5599..d70f90f 100644 --- a/engine_preedit.go +++ b/engine_preedit.go @@ -155,7 +155,8 @@ func (e *IBusBambooEngine) mustFallbackToEnglish() bool { return false } if e.config.IBflags&config.IBspellCheckWithDicts != 0 { - return !dictionary[vnSeq] + dict, _ := dictionary.Load().(map[string]bool) + return !dict[vnSeq] } return !e.preeditor.IsValid(true) } diff --git a/engine_regression_test.go b/engine_regression_test.go new file mode 100644 index 0000000..28f9329 --- /dev/null +++ b/engine_regression_test.go @@ -0,0 +1,272 @@ +package main + +import ( + "ibus-bamboo/config" + "sync" + "testing" + "time" + + "github.com/BambooEngine/bamboo-core" + ibus "github.com/BambooEngine/goibus" + "github.com/godbus/dbus/v5" +) + +func newRegressionEngine(inputMode int) (*IBusBambooEngine, *fakeEngine) { + fe := NewFakeEngine() + cfg := config.DefaultCfg() + cfg.DefaultInputMode = inputMode + im := bamboo.ParseInputMethod(cfg.InputMethodDefinitions, cfg.InputMethod) + return NewIbusBambooEngine("test", &cfg, fe, bamboo.NewEngine(im, cfg.Flags)), fe +} + +func TestResetClearsBackspaceBuffer(t *testing.T) { + e, _ := newRegressionEngine(config.SurroundingTextIM) + e.ProcessKeyEvent('d', 'd', 0) + e.ProcessKeyEvent('u', 'u', 0) + if e.getRawKeyLen() == 0 { + t.Fatalf("setup failed: buffer should be non-empty before Reset") + } + e.Reset() + if got := e.getRawKeyLen(); got != 0 { + t.Errorf("after Reset rawKeyLen = %d, want 0", got) + } +} + +func TestResetClearsPreeditBuffer(t *testing.T) { + e, fe := newRegressionEngine(config.PreeditIM) + e.ProcessKeyEvent('d', 'd', 0) + e.ProcessKeyEvent('u', 'u', 0) + e.Reset() + if got := e.getRawKeyLen(); got != 0 { + t.Errorf("after Reset rawKeyLen = %d, want 0", got) + } + if !fe.getHidePreeditText() { + t.Errorf("after Reset preedit should be hidden") + } +} + +func TestFocusOutCommitsToLosingWindow(t *testing.T) { + e, fe := newRegressionEngine(config.PreeditIM) + e.ProcessKeyEvent('d', 'd', 0) + e.ProcessKeyEvent('u', 'u', 0) + pending := fe.getPreeditText() + if pending == "" { + t.Fatalf("setup failed: expected pending preedit") + } + e.FocusOut() + if fe.getCommitText() != pending { + t.Errorf("after FocusOut commit = %q, want pending preedit %q", fe.getCommitText(), pending) + } + if got := e.getRawKeyLen(); got != 0 { + t.Errorf("after FocusOut rawKeyLen = %d, want 0", got) + } +} + +func TestFocusOutClearsBackspaceBuffer(t *testing.T) { + e, _ := newRegressionEngine(config.SurroundingTextIM) + e.ProcessKeyEvent('d', 'd', 0) + e.FocusOut() + if got := e.getRawKeyLen(); got != 0 { + t.Errorf("after FocusOut rawKeyLen = %d, want 0", got) + } +} + +func TestMovementHomeResetsBuffer(t *testing.T) { + e, _ := newRegressionEngine(config.SurroundingTextIM) + e.ProcessKeyEvent('d', 'd', 0) + if e.getRawKeyLen() == 0 { + t.Fatalf("setup failed: buffer should be non-empty") + } + if ret, _ := e.ProcessKeyEvent(IBusHome, 0, 0); ret { + t.Errorf("Home should be forwarded (return false), got true") + } + if got := e.getRawKeyLen(); got != 0 { + t.Errorf("after Home rawKeyLen = %d, want 0", got) + } +} + +func TestExtractSurroundingString(t *testing.T) { + if s, ok := extractSurroundingString("abc"); !ok || s != "abc" { + t.Errorf("plain string: got %q,%v", s, ok) + } + if s, ok := extractSurroundingString(dbus.MakeVariant("abc").Value()); !ok || s != "abc" { + t.Errorf("variant string: got %q,%v", s, ok) + } + if s, ok := extractSurroundingString([]interface{}{"IBusText", 0, "abc"}); !ok || s != "abc" { + t.Errorf("tuple shape: got %q,%v", s, ok) + } + if s, ok := extractSurroundingString(*ibus.NewText("abc")); !ok || s != "abc" { + t.Errorf("goibus.Text: got %q,%v", s, ok) + } + if _, ok := extractSurroundingString(42); ok { + t.Errorf("garbage shape should not decode") + } + if _, ok := extractSurroundingString(nil); ok { + t.Errorf("nil should not decode") + } +} + +func TestSetSurroundingTextRebuildsBuffer(t *testing.T) { + e, _ := newRegressionEngine(config.SurroundingTextIM) + e.surroundingTextReady = true + e.SetSurroundingText(dbus.MakeVariant(*ibus.NewText("hello")), 5, 5) + if got := e.getRawKeyLen(); got != 5 { + t.Errorf("after SetSurroundingText rawKeyLen = %d, want 5", got) + } + if e.surroundingTextReady { + t.Errorf("ready flag should be consumed") + } + // A selection (anchor != cursor) must be ignored. + e.surroundingTextReady = true + e.SetSurroundingText(dbus.MakeVariant(*ibus.NewText("hello world")), 5, 2) + if got := e.getRawKeyLen(); got != 5 { + t.Errorf("selection must be ignored, rawKeyLen = %d, want 5", got) + } +} + +func TestPerEngineQueueIsolation(t *testing.T) { + e1, fe1 := newRegressionEngine(config.SurroundingTextIM) + e2, fe2 := newRegressionEngine(config.SurroundingTextIM) + e1.shouldEnqueuKeyStrokes = true + e2.shouldEnqueuKeyStrokes = true + if ok, _ := e1.ProcessKeyEvent('d', 'd', 0); !ok { + t.Fatalf("key not accepted") + } + if ok, _ := e1.ProcessKeyEvent('u', 'u', 0); !ok { + t.Fatalf("key not accepted") + } + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + if fe1.getCommitText() == "du" { + break + } + time.Sleep(10 * time.Millisecond) + } + if fe1.getCommitText() != "du" { + t.Errorf("engine1 commit = %q, want %q", fe1.getCommitText(), "du") + } + if fe2.getCommitText() != "" { + t.Errorf("engine1 keystrokes leaked onto engine2: %q", fe2.getCommitText()) + } + if got := e2.getRawKeyLen(); got != 0 { + t.Errorf("engine2 buffer len = %d, want 0", got) + } +} + +func TestConcurrentEngineHammer(t *testing.T) { + e, _ := newRegressionEngine(config.SurroundingTextIM) + e.shouldEnqueuKeyStrokes = true + var wg sync.WaitGroup + keys := []rune(" engine ") + for g := 0; g < 3; g++ { + wg.Add(1) + go func(g int) { + defer wg.Done() + for i, k := range keys { + e.ProcessKeyEvent(uint32(k), uint32(k), 0) + if i%3 == 0 { + e.Reset() + } + } + e.ProcessKeyEvent(IBusBackSpace, 0, 0) + e.FocusOut() + e.SetCapabilities(0) + e.PageUp() + e.CandidateClicked(0, 1, 0) + }(g) + } + done := make(chan struct{}) + go func() { wg.Wait(); close(done) }() + select { + case <-done: + case <-time.After(20 * time.Second): + t.Fatalf("deadlock under concurrent engine use") + } +} + +func TestDetermineMacroCaseEmpty(t *testing.T) { + if got := determineMacroCase(""); got != VnCaseNoChange { + t.Errorf("determineMacroCase(\"\") = %d, want VnCaseNoChange", got) + } + if got := determineMacroCase("vn"); got != VnCaseAllSmall { + t.Errorf("determineMacroCase(vn) = %d, want VnCaseAllSmall", got) + } + if got := determineMacroCase("VN"); got != VnCaseAllCapital { + t.Errorf("determineMacroCase(VN) = %d, want VnCaseAllCapital", got) + } + if got := determineMacroCase("Vn"); got != VnCaseNoChange { + t.Errorf("determineMacroCase(Vn) = %d, want VnCaseNoChange", got) + } +} + +func TestEmojiCursorPosBounds(t *testing.T) { + e, _ := newRegressionEngine(config.PreeditIM) + lt := ibus.NewLookupTable() + lt.PageSize = uint32(EmojiMaxPageSize) + for i := 0; i < 10; i++ { + lt.AppendCandidate(string(rune('a' + i))) + } + lt.SetCursorPos(9) + e.emojiLookupTable = lt + if e.updateCursorPosInEmojiTable(1) { + t.Errorf("updateCursorPosInEmojiTable accepted out-of-range pos %d (len=%d)", lt.CursorPos, len(lt.Candidates)) + } + if lt.CursorPos != 9 { + t.Errorf("CursorPos = %d, want unchanged 9", lt.CursorPos) + } + if !e.updateCursorPosInEmojiTable(0) || lt.CursorPos != 9 { + t.Errorf("updateCursorPosInEmojiTable(0) = pos %d, want 9", lt.CursorPos) + } +} + +func TestUsIMDefaultHonorsWindowMapping(t *testing.T) { + fe := NewFakeEngine() + cfg := config.DefaultCfg() + cfg.DefaultInputMode = config.UsIM + cfg.InputModeMapping["test:test"] = config.PreeditIM + im := bamboo.ParseInputMethod(cfg.InputMethodDefinitions, cfg.InputMethod) + e := NewIbusBambooEngine("test", &cfg, fe, bamboo.NewEngine(im, cfg.Flags)) + e.wmClasses = "test:test" + if ret, _ := e.ProcessKeyEvent(IBusTilde, 0, IBusShiftMask); !ret || !e.isInputModeLTOpened { + t.Errorf("mapped Preedit window: input-mode switch ignored (ret=%v open=%v)", ret, e.isInputModeLTOpened) + } + + e2 := NewIbusBambooEngine("test", &cfg, fe, bamboo.NewEngine(im, cfg.Flags)) + e2.wmClasses = "other:other" + if ret, _ := e2.ProcessKeyEvent(IBusTilde, 0, IBusShiftMask); ret || e2.isInputModeLTOpened { + t.Errorf("unmapped UsIM window: switch should stay closed (ret=%v open=%v)", ret, e2.isInputModeLTOpened) + } + if ret, _ := e2.ProcessKeyEvent('d', 'd', 0); ret { + t.Errorf("unmapped UsIM window: keys should be forwarded (return false)") + } +} + +func TestMacroTableConcurrentAccess(t *testing.T) { + mt := NewMacroTable(false) + mt.mTable = map[string]string{"vn": "viet nam", "v": "v"} + var wg sync.WaitGroup + for g := 0; g < 4; g++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 200; i++ { + if mt.GetText("vn") == "" { + t.Errorf("macro text lost under concurrency") + return + } + mt.HasKey("vn") + mt.HasPrefix("v") + } + }() + } + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 10; i++ { + mt.Enable("test-race-nonexistent") + mt.Reload("test-race-nonexistent", false) + } + }() + wg.Wait() + mt.Disable() +} diff --git a/engine_test.go b/engine_test.go index 210e87e..3f4d9b8 100644 --- a/engine_test.go +++ b/engine_test.go @@ -158,11 +158,11 @@ func TestPreeditEngine(t *testing.T) { if ret != ev.canBeProcessed { t.Errorf("Is key can be processed? expected (%v), got (%v).", ev.canBeProcessed, ret) } - if ev.canBeProcessed && fe.preeditText != ev.expectedPreeditText { - t.Errorf("Preedit text, expected (%s), got (%s).", ev.expectedPreeditText, fe.preeditText) + if ev.canBeProcessed && fe.getPreeditText() != ev.expectedPreeditText { + t.Errorf("Preedit text, expected (%s), got (%s).", ev.expectedPreeditText, fe.getPreeditText()) } - if !ev.canBeProcessed && ev.expectedCommitText != fe.commitText { - t.Errorf("Commit text, expected (%s), got (%s).", ev.expectedCommitText, fe.commitText) + if !ev.canBeProcessed && ev.expectedCommitText != fe.getCommitText() { + t.Errorf("Commit text, expected (%s), got (%s).", ev.expectedCommitText, fe.getCommitText()) } } }) @@ -276,8 +276,8 @@ func TestBsEngine(t *testing.T) { if ret != ev.canBeProcessed { t.Errorf("Is key can be processed? expected (%v), got (%v).", ev.canBeProcessed, ret) } - if fe.commitText != ev.expectedCommitText { - t.Errorf("Commit text, expected (%s), got (%s).", ev.expectedCommitText, fe.commitText) + if fe.getCommitText() != ev.expectedCommitText { + t.Errorf("Commit text, expected (%s), got (%s).", ev.expectedCommitText, fe.getCommitText()) } } }) diff --git a/engine_utils.go b/engine_utils.go index 017d85b..95bb17a 100644 --- a/engine_utils.go +++ b/engine_utils.go @@ -24,9 +24,11 @@ import ( "ibus-bamboo/config" "ibus-bamboo/ui" "io/ioutil" + "log" "os" "strconv" "strings" + "sync" "sync/atomic" "time" "unicode" @@ -37,12 +39,55 @@ import ( "github.com/godbus/dbus/v5" ) -var dictionary = map[string]bool{} -var emojiTrie = NewTrie() +// Shared lookup data, published immutably via atomic.Value so FocusIn on +// any engine instance can load them without racing key processing. +var dictionary atomic.Value // map[string]bool +var emojiTrie atomic.Value // *TrieNode +var emojiLoadMu sync.Mutex +var dictLoadMu sync.Mutex -func GetIBusEngineCreator() func(*dbus.Conn, string) dbus.ObjectPath { - go keyPressCapturing() +func init() { + emojiTrie.Store(NewTrie()) +} +// currentEmojiTrie returns the loaded emoji trie, or an empty one when the +// data file has not been (or could not be) loaded yet. +func currentEmojiTrie() *TrieNode { + if trie, ok := emojiTrie.Load().(*TrieNode); ok && trie != nil { + return trie + } + return NewTrie() +} + +// ensureLazyData loads the emoji trie and the spelling dictionary on first +// use. Failures are logged, never fatal: the engine keeps working with +// the corresponding feature effectively disabled. +func (e *IBusBambooEngine) ensureLazyData(emojiShortcut bool, ibflags uint) { + if emojiShortcut && len(currentEmojiTrie().Children) == 0 { + emojiLoadMu.Lock() + if len(currentEmojiTrie().Children) == 0 { + if trie, err := loadEmojiOne(DictEmojiOne); err != nil { + log.Printf("failed to load emoji data from %s: %s", DictEmojiOne, err) + } else { + emojiTrie.Store(trie) + } + } + emojiLoadMu.Unlock() + } + if ibflags&config.IBspellCheckWithDicts != 0 { + dictLoadMu.Lock() + if d, _ := dictionary.Load().(map[string]bool); len(d) == 0 { + if loaded, err := loadDictionary(DictVietnameseCm); err != nil { + log.Printf("failed to load dictionary from %s: %s", DictVietnameseCm, err) + } else { + dictionary.Store(loaded) + } + } + dictLoadMu.Unlock() + } +} + +func GetIBusEngineCreator() func(*dbus.Conn, string) dbus.ObjectPath { return func(conn *dbus.Conn, ngName string) dbus.ObjectPath { var ngGroupName = strings.Split(ngName, "::")[0] var engineName = strings.ToLower(ngGroupName) @@ -51,7 +96,7 @@ func GetIBusEngineCreator() func(*dbus.Conn, string) dbus.ObjectPath { var objectPath = dbus.ObjectPath(fmt.Sprintf("/org/freedesktop/IBus/Engine/%s/%d", engineName, time.Now().UnixNano())) var inputMethod = bamboo.ParseInputMethod(cfg.InputMethodDefinitions, cfg.InputMethod) baseEngine := ibus.BaseEngine(conn, objectPath) - var engine = NewIbusBambooEngine(engineName, config.LoadConfig(engineName), &baseEngine, bamboo.NewEngine(inputMethod, cfg.Flags)) + var engine = NewIbusBambooEngine(engineName, cfg, &baseEngine, bamboo.NewEngine(inputMethod, cfg.Flags)) engine.propList = GetPropListByConfig(cfg) engine.shouldEnqueuKeyStrokes = true ibus.PublishEngine(conn, objectPath, engine) @@ -76,22 +121,17 @@ func (e *IBusBambooEngine) isShortcutKeyEnable(ski uint) bool { } func (e *IBusBambooEngine) init() { - initConfigFiles(e.engineName) - e.emoji = NewEmojiEngine() - if e.macroTable == nil { - e.macroTable = NewMacroTable(e.config.IBflags&config.IBautoCapitalizeMacro != 0) - if e.config.IBflags&config.IBmacroEnabled != 0 { - e.macroTable.Enable(e.engineName) - } + // Config file setup must not crash the daemon when the home directory + // is missing or read-only; the engine still works with defaults. + if err := initConfigFiles(e.engineName); err != nil { + log.Printf("initConfigFiles: %s", err) } - keyPressHandler = e.keyPressForwardHandler } -func initConfigFiles(engineName string) { +func initConfigFiles(engineName string) error { if sta, err := os.Stat(config.GetConfigDir(engineName)); err != nil || !sta.IsDir() { - err = os.Mkdir(config.GetConfigDir(engineName), 0777) - if err != nil { - panic(err) + if err := os.Mkdir(config.GetConfigDir(engineName), 0777); err != nil { + return err } } macroPath := config.GetMacroPath(engineName) @@ -99,33 +139,39 @@ func initConfigFiles(engineName string) { sampleFile := getEngineSubFile(sampleMactabFile) sample, err := ioutil.ReadFile(sampleFile) if err != nil { - panic(err) + return err } - err = ioutil.WriteFile(macroPath, sample, 0644) - if err != nil { - panic(err) + if err := ioutil.WriteFile(macroPath, sample, 0644); err != nil { + return err } } + return nil } -var keyPressHandler = func(keyVal, keyCode, state uint32) {} -var keyPressChan = make(chan [3]uint32, 100) -var lenKeyChan int32 - -func keyPressCapturing() { - for keyEvents := range keyPressChan { - atomic.StoreInt32(&lenKeyChan, int32(len(keyPressChan))) - +// keyPressCapturing drains this engine's own queue sequentially. The lock +// is taken per key, never across channel operations, so a producer blocked +// on a full queue can always make progress via a lock-free receive here. +func (e *IBusBambooEngine) keyPressCapturing() { + for keyEvents := range e.keyPressChan { var keyVal, keyCode, state = keyEvents[0], keyEvents[1], keyEvents[2] - keyPressHandler(keyVal, keyCode, state) - - atomic.AddInt32(&lenKeyChan, -1) + e.Lock() + e.keyPressForwardHandler(keyVal, keyCode, state) + e.Unlock() + atomic.AddInt32(&e.keyQueueLen, -1) } } -var sleep = func() { +func (e *IBusBambooEngine) enqueueKeyPress(keyVal, keyCode, state uint32) { + atomic.AddInt32(&e.keyQueueLen, 1) + e.keyPressChan <- [3]uint32{keyVal, keyCode, state} +} + +// waitForQueueDrain pauses until keys queued ahead of the current one are +// handled. It only performs bounded atomic loads, so it is safe to call +// with the engine lock held. +func (e *IBusBambooEngine) waitForQueueDrain() { var i = 0 - for i < 10 && atomic.LoadInt32(&lenKeyChan) > 0 { + for i < 10 && atomic.LoadInt32(&e.keyQueueLen) > 0 { i++ time.Sleep(5 * time.Millisecond) } @@ -197,7 +243,9 @@ func (e *IBusBambooEngine) processShortcutKey(keyVal, keyCode, state uint32) (bo return true, e.hexadecimalProcessKeyEvent(keyVal, keyCode, state) } - if e.config.DefaultInputMode == config.UsIM { + // An engine whose default mode is UsIM (e.g. BambooUs) forwards everything, + // but a per-window mapping to a Vietnamese mode still takes effect. + if e.config.DefaultInputMode == config.UsIM && e.checkInputMode(config.UsIM) { return true, false } if e.isShortcutKeyPressed(keyVal, state, KSRestoreKeyStrokes) { @@ -208,7 +256,8 @@ func (e *IBusBambooEngine) processShortcutKey(keyVal, keyCode, state uint32) (bo // fmt.Println("===Process shortcut for input method switcher") if e.isShortcutKeyPressed(keyVal, state, KSViEnSwitch) { e.englishMode = !e.englishMode - notify(e.englishMode) + // Asynchronous: the notification round-trip must not stall input. + go notify(e.englishMode) e.resetBuffer() return true, true } @@ -424,11 +473,16 @@ func (e *IBusBambooEngine) getCommitText(keyVal, keyCode, state uint32) (newText } else { newText = e.getProcessedString(bamboo.VietnameseMode) } - if fullSeq := e.preeditor.GetProcessedString(bamboo.VietnameseMode); len(fullSeq) > 0 && rune(fullSeq[len(fullSeq)-1]) == keyRune { + // Compare last runes, not last bytes: custom input methods may + // define multi-byte appending keys. + if fullSeq := []rune(e.preeditor.GetProcessedString(bamboo.VietnameseMode)); len(fullSeq) > 0 && fullSeq[len(fullSeq)-1] == keyRune { // [[ => [ var ret = e.getPreeditString() - var lastRune = rune(ret[len(ret)-1]) - var isWordBreakRune = bamboo.IsWordBreakSymbol(lastRune) + var retRunes = []rune(ret) + if len(retRunes) == 0 { + return ret, false + } + var isWordBreakRune = bamboo.IsWordBreakSymbol(retRunes[len(retRunes)-1]) // TODO: THIS IS A HACK if isWordBreakRune { e.preeditor.RemoveLastChar(false) diff --git a/fake_engine.go b/fake_engine.go index eb9ce42..6d39537 100644 --- a/fake_engine.go +++ b/fake_engine.go @@ -1,6 +1,8 @@ package main import ( + "sync" + ibus "github.com/BambooEngine/goibus" "github.com/godbus/dbus/v5" ) @@ -47,7 +49,10 @@ type IEngine interface { RequireSurroundingText() } +// fakeEngine is used by tests; its assertions may run on a different +// goroutine than the engine's key queue consumer, so state is guarded. type fakeEngine struct { + mu sync.Mutex commitText string preeditText string committed bool @@ -58,6 +63,24 @@ type fakeEngine struct { forwardKeyEvent [3]uint32 } +func (e *fakeEngine) getCommitText() string { + e.mu.Lock() + defer e.mu.Unlock() + return e.commitText +} + +func (e *fakeEngine) getPreeditText() string { + e.mu.Lock() + defer e.mu.Unlock() + return e.preeditText +} + +func (e *fakeEngine) getHidePreeditText() bool { + e.mu.Lock() + defer e.mu.Unlock() + return e.isHidePreeditText +} + func NewFakeEngine() *fakeEngine { return &fakeEngine{} } @@ -92,6 +115,8 @@ func (e *fakeEngine) FocusOut() *dbus.Error { } func (e *fakeEngine) Reset() *dbus.Error { + e.mu.Lock() + defer e.mu.Unlock() e.isReset = true return nil } @@ -153,19 +178,27 @@ func (e *fakeEngine) Destroy() *dbus.Error { // @signal(signature="v") func (e *fakeEngine) CommitText(text *ibus.Text) { + e.mu.Lock() + defer e.mu.Unlock() e.commitText += text.Text } // @signal(signature="uuu") func (e *fakeEngine) ForwardKeyEvent(keyval uint32, keycode uint32, state uint32) { + e.mu.Lock() + defer e.mu.Unlock() e.forwardKeyEvent = [3]uint32{keyval, keycode, state} } // @signal(signature="vubu") func (e *fakeEngine) UpdatePreeditText(text *ibus.Text, cursor_pos uint32, visible bool) { + e.mu.Lock() + defer e.mu.Unlock() e.preeditText = text.Text } func (e *fakeEngine) UpdatePreeditTextWithMode(text *ibus.Text, cursor_pos uint32, visible bool, mode uint32) { + e.mu.Lock() + defer e.mu.Unlock() e.preeditText = text.Text } @@ -175,6 +208,8 @@ func (e *fakeEngine) ShowPreeditText() { // @signal() func (e *fakeEngine) HidePreeditText() { + e.mu.Lock() + defer e.mu.Unlock() e.preeditText = "" e.isHidePreeditText = true } @@ -185,11 +220,15 @@ func (e *fakeEngine) UpdateAuxiliaryText(text *ibus.Text, visible bool) { // @signal() func (e *fakeEngine) ShowAuxiliaryText() { + e.mu.Lock() + defer e.mu.Unlock() e.isHideAuxiliaryText = false } // @signal() func (e *fakeEngine) HideAuxiliaryText() { + e.mu.Lock() + defer e.mu.Unlock() e.isHideAuxiliaryText = true } @@ -199,11 +238,15 @@ func (e *fakeEngine) UpdateLookupTable(lookup_table *ibus.LookupTable, visible b // @signal() func (e *fakeEngine) ShowLookupTable() { + e.mu.Lock() + defer e.mu.Unlock() e.isHideLookupTable = false } // @signal() func (e *fakeEngine) HideLookupTable() { + e.mu.Lock() + defer e.mu.Unlock() e.isHideLookupTable = true } diff --git a/ibus_const.go b/ibus_const.go index d12cef4..6b4d8b0 100644 --- a/ibus_const.go +++ b/ibus_const.go @@ -60,6 +60,7 @@ const ( ) const ( IBusTab = 0xff09 + IBusHome = 0xff50 IBusEnd = 0xff57 IBusColon = 0x03a IBusLeft = 0xFF51 diff --git a/mactab.go b/mactab.go index f853e51..01ee3b7 100644 --- a/mactab.go +++ b/mactab.go @@ -31,6 +31,7 @@ import ( type MacroTable struct { sync.RWMutex enable bool + watching bool autoCapitalizeMacro bool mTable map[string]string } @@ -45,6 +46,8 @@ func (e *MacroTable) LoadFromFile(macroFileName string) error { return err } defer f.Close() + e.Lock() + defer e.Unlock() e.mTable = map[string]string{} rd := bufio.NewReader(f) for { @@ -69,11 +72,15 @@ func (e *MacroTable) LoadFromFile(macroFileName string) error { } func (e *MacroTable) Reload(engineName string, autoCapitalizeMacro bool) { + e.Lock() e.autoCapitalizeMacro = autoCapitalizeMacro + e.Unlock() e.Enable(engineName) } func (e *MacroTable) GetText(key string) string { + e.RLock() + defer e.RUnlock() if e.autoCapitalizeMacro { key = strings.ToLower(key) } @@ -81,6 +88,8 @@ func (e *MacroTable) GetText(key string) string { } func (e *MacroTable) HasKey(key string) bool { + e.RLock() + defer e.RUnlock() if e.autoCapitalizeMacro { key = strings.ToLower(key) } @@ -88,6 +97,8 @@ func (e *MacroTable) HasKey(key string) bool { } func (e *MacroTable) HasPrefix(key string) bool { + e.RLock() + defer e.RUnlock() if e.mTable[key] != "" { return true } @@ -99,15 +110,35 @@ func (e *MacroTable) HasPrefix(key string) bool { return false } +func (e *MacroTable) isEnabled() bool { + e.RLock() + defer e.RUnlock() + return e.enable +} + func (e *MacroTable) Enable(engineName string) { + e.Lock() e.enable = true + alreadyWatching := e.watching + if !alreadyWatching { + e.watching = true + } + e.Unlock() + if alreadyWatching { + return + } go func() { + defer func() { + e.Lock() + e.watching = false + e.Unlock() + }() modTime := time.Now() efPath := config.GetMacroPath(engineName) - for e.enable { + for e.isEnabled() { if sta, _ := os.Stat(efPath); sta != nil { if newModeTime := sta.ModTime(); !newModeTime.Equal(modTime) { modTime = newModeTime @@ -120,6 +151,8 @@ func (e *MacroTable) Enable(engineName string) { } func (e *MacroTable) Disable() { + e.Lock() + defer e.Unlock() e.enable = false e.mTable = map[string]string{} } diff --git a/utils.go b/utils.go index 5e6e0ec..c38d3bd 100644 --- a/utils.go +++ b/utils.go @@ -85,6 +85,9 @@ func getEngineSubFile(fileName string) string { func determineMacroCase(str string) uint8 { var chars = []rune(str) + if len(chars) == 0 { + return VnCaseNoChange + } if unicode.IsLower(chars[0]) { return VnCaseAllSmall } else { @@ -180,26 +183,25 @@ func loadDictionary(dataFiles ...string) (map[string]bool, error) { if err != nil { return nil, err } - rd := bufio.NewReader(f) - for { - line, _, err := rd.ReadLine() - if err != nil { - break - } - if len(line) == 0 { + scanner := bufio.NewScanner(f) + scanner.Buffer(make([]byte, 64*1024), 1024*1024) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { continue } - var tmp = []byte(strings.ToLower(string(line))) - data[string(tmp)] = true - //bamboo.AddTrie(rootWordTrie, []rune(string(line)), false) + data[strings.ToLower(line)] = true } f.Close() + if err := scanner.Err(); err != nil { + return nil, err + } } return data, nil } func isMovementKey(keyVal uint32) bool { - var list = []uint32{IBusLeft, IBusRight, IBusUp, IBusDown, IBusPageDown, IBusPageUp, IBusEnd} + var list = []uint32{IBusLeft, IBusRight, IBusUp, IBusDown, IBusPageDown, IBusPageUp, IBusHome, IBusEnd} for _, item := range list { if item == keyVal { return true