From 03c26a05e06c49e6c46e044dbb848de98309050e Mon Sep 17 00:00:00 2001 From: hunchulchoi Date: Thu, 4 Jun 2026 18:29:10 +0900 Subject: [PATCH] feat: add Korean keyboard layout support for keybindings --- pkg/gui/keybindings.go | 86 ++++++++++++++++++++++++++++++++++++- pkg/gui/keybindings_test.go | 75 ++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 pkg/gui/keybindings_test.go diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 8e106ad5..aa6e96ea 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -3,6 +3,7 @@ package gui import ( "fmt" + "github.com/gdamore/tcell/v2" "github.com/jesseduffield/gocui" ) @@ -609,7 +610,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { } } - return bindings + return translateKeybindingsToKorean(bindings) } func (gui *Gui) keybindings(g *gocui.Gui) error { @@ -633,3 +634,86 @@ func wrappedHandler(f func() error) func(*gocui.Gui, *gocui.View) error { return f() } } + +type koreanKey struct { + key rune + modifier gocui.Modifier +} + +var englishToKorean = map[rune]koreanKey{ + // Lowercase + 'q': {key: 'ㅂ', modifier: gocui.ModNone}, + 'w': {key: 'ㅈ', modifier: gocui.ModNone}, + 'e': {key: 'ㄷ', modifier: gocui.ModNone}, + 'r': {key: 'ㄱ', modifier: gocui.ModNone}, + 't': {key: 'ㅅ', modifier: gocui.ModNone}, + 'y': {key: 'ㅛ', modifier: gocui.ModNone}, + 'u': {key: 'ㅕ', modifier: gocui.ModNone}, + 'i': {key: 'ㅑ', modifier: gocui.ModNone}, + 'o': {key: 'ㅐ', modifier: gocui.ModNone}, + 'p': {key: 'ㅔ', modifier: gocui.ModNone}, + 'a': {key: 'ㅁ', modifier: gocui.ModNone}, + 's': {key: 'ㄴ', modifier: gocui.ModNone}, + 'd': {key: 'ㅇ', modifier: gocui.ModNone}, + 'f': {key: 'ㄹ', modifier: gocui.ModNone}, + 'g': {key: 'ㅎ', modifier: gocui.ModNone}, + 'h': {key: 'ㅗ', modifier: gocui.ModNone}, + 'j': {key: 'ㅓ', modifier: gocui.ModNone}, + 'k': {key: 'ㅏ', modifier: gocui.ModNone}, + 'l': {key: 'ㅣ', modifier: gocui.ModNone}, + 'z': {key: 'ㅋ', modifier: gocui.ModNone}, + 'x': {key: 'ㅌ', modifier: gocui.ModNone}, + 'c': {key: 'ㅊ', modifier: gocui.ModNone}, + 'v': {key: 'ㅍ', modifier: gocui.ModNone}, + 'b': {key: 'ㅠ', modifier: gocui.ModNone}, + 'n': {key: 'ㅜ', modifier: gocui.ModNone}, + 'm': {key: 'ㅡ', modifier: gocui.ModNone}, + + // Uppercase (Double Consonants - ModNone) + 'Q': {key: 'ㅃ', modifier: gocui.ModNone}, + 'W': {key: 'ㅉ', modifier: gocui.ModNone}, + 'E': {key: 'ㄸ', modifier: gocui.ModNone}, + 'R': {key: 'ㄲ', modifier: gocui.ModNone}, + 'T': {key: 'ㅆ', modifier: gocui.ModNone}, + 'O': {key: 'ㅒ', modifier: gocui.ModNone}, + 'P': {key: 'ㅖ', modifier: gocui.ModNone}, + + // Uppercase (No Double Consonant - ModShift) + 'A': {key: 'ㅁ', modifier: gocui.Modifier(tcell.ModShift)}, + 'S': {key: 'ㄴ', modifier: gocui.Modifier(tcell.ModShift)}, + 'D': {key: 'ㅇ', modifier: gocui.Modifier(tcell.ModShift)}, + 'F': {key: 'ㄹ', modifier: gocui.Modifier(tcell.ModShift)}, + 'G': {key: 'ㅎ', modifier: gocui.Modifier(tcell.ModShift)}, + 'H': {key: 'ㅗ', modifier: gocui.Modifier(tcell.ModShift)}, + 'J': {key: 'ㅓ', modifier: gocui.Modifier(tcell.ModShift)}, + 'K': {key: 'ㅏ', modifier: gocui.Modifier(tcell.ModShift)}, + 'L': {key: 'ㅣ', modifier: gocui.Modifier(tcell.ModShift)}, + 'Z': {key: 'ㅋ', modifier: gocui.Modifier(tcell.ModShift)}, + 'X': {key: 'ㅌ', modifier: gocui.Modifier(tcell.ModShift)}, + 'C': {key: 'ㅊ', modifier: gocui.Modifier(tcell.ModShift)}, + 'V': {key: 'ㅍ', modifier: gocui.Modifier(tcell.ModShift)}, + 'B': {key: 'ㅠ', modifier: gocui.Modifier(tcell.ModShift)}, + 'N': {key: 'ㅜ', modifier: gocui.Modifier(tcell.ModShift)}, + 'M': {key: 'ㅡ', modifier: gocui.Modifier(tcell.ModShift)}, + 'Y': {key: 'ㅛ', modifier: gocui.Modifier(tcell.ModShift)}, + 'U': {key: 'ㅕ', modifier: gocui.Modifier(tcell.ModShift)}, + 'I': {key: 'ㅑ', modifier: gocui.Modifier(tcell.ModShift)}, +} + +func translateKeybindingsToKorean(bindings []*Binding) []*Binding { + var translated []*Binding + for _, binding := range bindings { + if r, ok := binding.Key.(rune); ok { + if kor, exists := englishToKorean[r]; exists { + translated = append(translated, &Binding{ + ViewName: binding.ViewName, + Handler: binding.Handler, + Key: kor.key, + Modifier: binding.Modifier | kor.modifier, + Description: binding.Description, + }) + } + } + } + return append(bindings, translated...) +} diff --git a/pkg/gui/keybindings_test.go b/pkg/gui/keybindings_test.go new file mode 100644 index 00000000..ee3b7ae4 --- /dev/null +++ b/pkg/gui/keybindings_test.go @@ -0,0 +1,75 @@ +package gui + +import ( + "testing" + + "github.com/gdamore/tcell/v2" + "github.com/jesseduffield/gocui" + "github.com/stretchr/testify/assert" +) + +func TestTranslateKeybindingsToKorean(t *testing.T) { + inputBindings := []*Binding{ + { + ViewName: "services", + Key: 's', + Modifier: gocui.ModNone, + Handler: nil, + }, + { + ViewName: "services", + Key: 'S', + Modifier: gocui.ModNone, + Handler: nil, + }, + { + ViewName: "services", + Key: 'l', + Modifier: gocui.ModNone, + Handler: nil, + }, + { + ViewName: "global", + Key: gocui.KeyCtrlC, + Modifier: gocui.ModNone, + Handler: nil, + }, + } + + result := translateKeybindingsToKorean(inputBindings) + + // The original bindings must remain intact, and new ones should be appended + assert.Len(t, result, len(inputBindings)+3) + + // Verify original bindings are still present and unaltered + for i, b := range inputBindings { + assert.Equal(t, b.Key, result[i].Key) + assert.Equal(t, b.Modifier, result[i].Modifier) + } + + // Verify translated Korean bindings are appended correctly + // 's' -> 'ㄴ' (gocui.ModNone) + // 'S' -> 'ㄴ' (gocui.Modifier(tcell.ModShift)) + // 'l' -> 'ㅣ' (gocui.ModNone) + + // We expect translated bindings to be appended: + foundS := false + foundShiftS := false + foundL := false + + for _, b := range result[len(inputBindings):] { + if b.Key == 'ㄴ' && b.Modifier == gocui.ModNone { + foundS = true + } + if b.Key == 'ㄴ' && b.Modifier == gocui.Modifier(tcell.ModShift) { + foundShiftS = true + } + if b.Key == 'ㅣ' && b.Modifier == gocui.ModNone { + foundL = true + } + } + + assert.True(t, foundS, "Expected to find translated Korean binding for 's'") + assert.True(t, foundShiftS, "Expected to find translated Korean binding for 'S'") + assert.True(t, foundL, "Expected to find translated Korean binding for 'l'") +}