fix: handle capital keybindings from enhanced keyboard events (#322)

Fixes #286

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
This commit is contained in:
Matt Van Horn
2026-08-20 16:59:30 +01:00
committed by GitHub
parent 5ca8722bae
commit e0328d982e
2 changed files with 140 additions and 0 deletions
+128
View File
@@ -9,8 +9,136 @@ import (
"github.com/bjarneo/cliamp/ipc"
"github.com/bjarneo/cliamp/playlist"
"github.com/bjarneo/cliamp/provider"
)
type interactionBrowseProvider struct {
commandsTestProvider
}
func (p interactionBrowseProvider) Artists() ([]provider.ArtistInfo, error) { return nil, nil }
func (p interactionBrowseProvider) ArtistAlbums(string) ([]provider.AlbumInfo, error) {
return nil, nil
}
func keybindingTestModel() Model {
local := commandsTestProvider{name: "Local"}
return Model{
playlist: playlist.New(),
provider: local,
providers: []ProviderEntry{
{Key: "local", Name: "Local", Provider: local},
{Key: "yt", Name: "YouTube", Provider: commandsTestProvider{name: "YouTube"}},
},
}
}
func TestHandleKeyEnhancedShiftYSelectsYouTubeProvider(t *testing.T) {
m := keybindingTestModel()
msg := tea.KeyPressMsg{Code: 'y', ShiftedCode: 'Y', Mod: tea.ModShift}
if got := msg.String(); got != "shift+y" {
t.Fatalf("enhanced Shift+Y string = %q, want shift+y", got)
}
m.handleKey(msg)
if got := m.provider.Name(); got != "YouTube" {
t.Fatalf("active provider = %q, want YouTube", got)
}
if m.lyrics.visible {
t.Fatal("lyrics.visible = true after enhanced Shift+Y, want false")
}
}
func TestHandleKeyEnhancedShiftNOpensProviderBrowser(t *testing.T) {
browse := interactionBrowseProvider{commandsTestProvider{name: "Navidrome"}}
m := keybindingTestModel()
m.providers = append(m.providers, ProviderEntry{Key: "navidrome", Name: "Navidrome", Provider: browse})
msg := tea.KeyPressMsg{Code: 'n', ShiftedCode: 'N', Mod: tea.ModShift}
m.handleKey(msg)
if !m.navBrowser.visible {
t.Fatal("navBrowser.visible = false after enhanced Shift+N, want true")
}
if got := m.navBrowser.prov.Name(); got != "Navidrome" {
t.Fatalf("browser provider = %q, want Navidrome", got)
}
}
func TestHandleKeyEnhancedShiftLetterReachesActiveTextInput(t *testing.T) {
m := keybindingTestModel()
m.search.active = true
m.handleKey(tea.KeyPressMsg{Code: 'a', ShiftedCode: 'A', Mod: tea.ModShift})
if got := m.search.query; got != "A" {
t.Fatalf("search query = %q, want A", got)
}
}
func TestHandleKeyPreservesExistingLetterRepresentations(t *testing.T) {
t.Run("lowercase toggles lyrics", func(t *testing.T) {
m := keybindingTestModel()
m.handleKey(tea.KeyPressMsg{Code: 'y', Text: "y"})
if !m.lyrics.visible {
t.Fatal("lyrics.visible = false after lowercase y, want true")
}
if got := m.provider.Name(); got != "Local" {
t.Fatalf("active provider = %q, want Local", got)
}
})
t.Run("uppercase text selects provider", func(t *testing.T) {
m := keybindingTestModel()
msg := tea.KeyPressMsg{Code: 'y', ShiftedCode: 'Y', Text: "Y", Mod: tea.ModShift}
if got := msg.String(); got != "Y" {
t.Fatalf("uppercase text string = %q, want Y", got)
}
m.handleKey(msg)
if got := m.provider.Name(); got != "YouTube" {
t.Fatalf("active provider = %q, want YouTube", got)
}
})
}
func TestHandleKeyPreservesUnrelatedModifiedKeys(t *testing.T) {
tests := []struct {
name string
msg tea.KeyPressMsg
want string
}{
{name: "shifted non-letter", msg: tea.KeyPressMsg{Code: '1', ShiftedCode: '!', Mod: tea.ModShift}, want: "shift+1"},
{name: "control shifted letter", msg: tea.KeyPressMsg{Code: 'y', ShiftedCode: 'Y', Mod: tea.ModCtrl | tea.ModShift}, want: "ctrl+shift+y"},
{name: "alt shifted letter", msg: tea.KeyPressMsg{Code: 'y', ShiftedCode: 'Y', Mod: tea.ModAlt | tea.ModShift}, want: "alt+shift+y"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := keybindingTestModel()
if got := tt.msg.String(); got != tt.want {
t.Fatalf("key string = %q, want %q", got, tt.want)
}
if got := normalizeShiftedLetter(tt.msg).String(); got != tt.want {
t.Fatalf("normalized key string = %q, want %q", got, tt.want)
}
m.handleKey(tt.msg)
if got := m.provider.Name(); got != "Local" {
t.Fatalf("active provider = %q, want Local", got)
}
if m.lyrics.visible || m.navBrowser.visible {
t.Fatalf("unexpected action: lyrics.visible=%v navBrowser.visible=%v", m.lyrics.visible, m.navBrowser.visible)
}
})
}
}
func TestGlobalHelpOpensOverActiveTextInput(t *testing.T) {
m := Model{search: searchState{active: true, query: "jazz"}}
+12
View File
@@ -175,8 +175,20 @@ func (m *Model) providerToBottom() {
m.providerMaybeAdjustScroll()
}
func normalizeShiftedLetter(msg tea.KeyPressMsg) tea.KeyPressMsg {
if msg.Text != "" || msg.Mod != tea.ModShift ||
msg.Code < 'a' || msg.Code > 'z' ||
msg.ShiftedCode < 'A' || msg.ShiftedCode > 'Z' {
return msg
}
msg.Text = string(msg.ShiftedCode)
return msg
}
// handleKey processes a single key press and returns an optional command.
func (m *Model) handleKey(msg tea.KeyPressMsg) tea.Cmd {
msg = normalizeShiftedLetter(msg)
if msg.String() == "ctrl+c" {
return m.quit()
}