feat: add Korean keyboard layout support for keybindings

This commit is contained in:
hunchulchoi 2026-06-04 18:29:10 +09:00
parent 3396d26d8b
commit 03c26a05e0
2 changed files with 160 additions and 1 deletions

View file

@ -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...)
}

View file

@ -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'")
}