From f533f72586d4f931297dd84b81e1159e7438a6f1 Mon Sep 17 00:00:00 2001 From: thecentinol Date: Wed, 22 Jul 2026 14:58:46 +0200 Subject: [PATCH 01/14] feat(events): define keybinding modal events --- internal/events/ui.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internal/events/ui.go b/internal/events/ui.go index 43e4834..a43b326 100644 --- a/internal/events/ui.go +++ b/internal/events/ui.go @@ -1,6 +1,8 @@ package events import ( + tea "charm.land/bubbletea/v2" + nm "github.com/thecentinol/tuwi/internal/networkmanager" ) @@ -12,3 +14,21 @@ type PasswordResultMsg struct { Cancelled bool Password string } + +type OpenKeybindsModalMsg struct{} + +type CloseKeybindsModalMsg struct{} + +// returns OpenKeybindsModalMsg{} +func OpenKeybindsModal() tea.Cmd { + return func() tea.Msg { + return OpenKeybindsModalMsg{} + } +} + +// returns CloseKeybindsModalMsg{} +func CloseKeybindsModal() tea.Cmd { + return func() tea.Msg { + return CloseKeybindsModalMsg{} + } +} From 09c23594ddad372feeeddad30df39abac7c87baf Mon Sep 17 00:00:00 2001 From: thecentinol Date: Wed, 22 Jul 2026 15:06:17 +0200 Subject: [PATCH 02/14] feat(keybindings): define keybindings for keybindings modal --- internal/keybindings/keybindings.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/internal/keybindings/keybindings.go b/internal/keybindings/keybindings.go index 9108249..540f0ae 100644 --- a/internal/keybindings/keybindings.go +++ b/internal/keybindings/keybindings.go @@ -26,7 +26,7 @@ func DefaultKeybindings() Keybindings { ViewName: "", Group: globalGroup, Keys: []string{"1"}, - HelpKey: "", + HelpKey: "1", HelpDescription: "", Description: "Switch focus to saved wifi connections pane", }, @@ -34,7 +34,7 @@ func DefaultKeybindings() Keybindings { ViewName: "", Group: globalGroup, Keys: []string{"2"}, - HelpKey: "", + HelpKey: "2", HelpDescription: "", Description: "Switch focus to available wifi networks pane", }, @@ -128,6 +128,24 @@ func DefaultKeybindings() Keybindings { Description: "Go to the bottom of the table", }, + // Keybindings modal + OpenKeybindingsModal: Binding{ + ViewName: "keybindings-modal", + Group: "keybindings", + Keys: []string{"?"}, + HelpKey: "?", + HelpDescription: "keybindings", + Description: "Open keybindings modal", + }, + CloseKeybindingsModal: Binding{ + ViewName: "keybindings-modal", + Group: "keybindings", + Keys: []string{"esc"}, + HelpKey: "escape", + HelpDescription: "close", + Description: "Close keybindings modal", + }, + // Error modal ErrorDismiss: Binding{ ViewName: errModalView, @@ -189,6 +207,10 @@ type Keybindings struct { GotoTop Binding GotoBottom Binding + // Keybindings modal + OpenKeybindingsModal Binding + CloseKeybindingsModal Binding + // Error modal ErrorDismiss Binding From 1064f4cf04f9c9c2b549a90b3a010fac99c6f057 Mon Sep 17 00:00:00 2001 From: thecentinol Date: Wed, 22 Jul 2026 15:08:32 +0200 Subject: [PATCH 03/14] refactor(keybindings): separate utility functions from keybindings definitions --- internal/keybindings/keybindings.go | 30 +-------------- internal/keybindings/utils.go | 57 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 29 deletions(-) create mode 100644 internal/keybindings/utils.go diff --git a/internal/keybindings/keybindings.go b/internal/keybindings/keybindings.go index 540f0ae..f378659 100644 --- a/internal/keybindings/keybindings.go +++ b/internal/keybindings/keybindings.go @@ -1,7 +1,7 @@ package keybindings import ( - "charm.land/bubbles/v2/key" +// "charm.land/bubbles/v2/key" ) const ( @@ -232,31 +232,3 @@ type Binding struct { HelpDescription string // the help text for bubbles key.Binding.Help Description string // the longer full description displayed in the keybindings modal } - -// converts the keybind to a bubbles keybinding -func (b Binding) ToBubbles() key.Binding { - return key.NewBinding( - key.WithKeys(b.Keys...), - key.WithHelp(b.HelpKey, b.HelpDescription), - ) -} - -// returns all keybindings -func (k Keybindings) All() []Binding { - return []Binding{ - k.FocusedWifiSaved, k.FocusedWifiAvailable, k.Quit, k.WifiConnectSaved, k.WifiDisconnect, - k.WifiForget, k.WifiAutoConnect, k.WifiScan, k.WifiConnectAvailable, k.LineUp, k.LineDown, - k.GotoTop, k.GotoBottom, k.ErrorDismiss, k.PasswordSubmit, k.PasswordCancel, - } -} - -// get keybindings by ViewName -func (k Keybindings) ByView(viewName string) []Binding { - var binds []Binding - for _, b := range k.All() { - if b.ViewName == viewName { - binds = append(binds, b) - } - } - return binds -} diff --git a/internal/keybindings/utils.go b/internal/keybindings/utils.go new file mode 100644 index 0000000..2a9ca42 --- /dev/null +++ b/internal/keybindings/utils.go @@ -0,0 +1,57 @@ +package keybindings + +import ( + "charm.land/bubbles/v2/key" +) + +// converts a single keybind to a bubbles keybinding +func (b Binding) ToBubbles() key.Binding { + return key.NewBinding( + key.WithKeys(b.Keys...), + key.WithHelp(b.HelpKey, b.HelpDescription), + ) +} + +func ToBubblesBatch(keys []Binding) []key.Binding { + var binds []key.Binding + + for _, k := range keys { + bind := key.NewBinding( + key.WithKeys(k.Keys...), + key.WithHelp(k.HelpKey, k.Description), + ) + binds = append(binds, bind) + } + + return binds +} + +// returns all keybindings +func (k Keybindings) All() []Binding { + return []Binding{ + k.FocusedWifiSaved, k.FocusedWifiAvailable, k.Quit, k.WifiConnectSaved, k.WifiDisconnect, + k.WifiForget, k.WifiAutoConnect, k.WifiScan, k.WifiConnectAvailable, k.LineUp, k.LineDown, + k.GotoTop, k.GotoBottom, k.ErrorDismiss, k.PasswordSubmit, k.PasswordCancel, + } +} + +// get keybindings by ViewName +func (k Keybindings) ByView(viewName string) []Binding { + var binds []Binding + for _, b := range k.All() { + if b.ViewName == viewName { + binds = append(binds, b) + } + } + return binds +} + +func (k Keybindings) ByGroup(group string) []Binding { + var binds []Binding + for _, kb := range k.All() { + if kb.Group == group { + binds = append(binds, kb) + } + } + return binds +} From fd503a959d3c2c418d0ea39304942ad3d7e582ff Mon Sep 17 00:00:00 2001 From: thecentinol Date: Wed, 22 Jul 2026 15:09:20 +0200 Subject: [PATCH 04/14] feat(theme): define keybinding modal theme --- internal/ui/theme/default.go | 6 ++++++ internal/ui/theme/rose_pine.go | 12 ++++++++++++ internal/ui/theme/theme.go | 18 +++++++++++++----- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/internal/ui/theme/default.go b/internal/ui/theme/default.go index 334ebc4..00b5572 100644 --- a/internal/ui/theme/default.go +++ b/internal/ui/theme/default.go @@ -34,6 +34,12 @@ var Default = Theme{ PromptBlurred: lipgloss.Color("238"), Cursor: lipgloss.White, }, + KeybindsModal: KeybindsModalTheme{ + Border: lipgloss.Green, + BorderContent: lipgloss.Green, + KeybindsText: lipgloss.White, + DescriptionText: lipgloss.Color("#6e6a86"), + }, Help: HelpTheme{ Ellipsis: nil, ShortKey: lipgloss.White, diff --git a/internal/ui/theme/rose_pine.go b/internal/ui/theme/rose_pine.go index 134c6b7..0daf741 100644 --- a/internal/ui/theme/rose_pine.go +++ b/internal/ui/theme/rose_pine.go @@ -77,6 +77,12 @@ var RosePine = Theme{ Cursor: lipgloss.Color(foam), }, + KeybindsModal: KeybindsModalTheme{ + Border: lipgloss.Color(foam), + BorderContent: lipgloss.Color(foam), + KeybindsText: lipgloss.Color(iris), + DescriptionText: lipgloss.White, + }, Help: HelpTheme{ Ellipsis: nil, ShortKey: lipgloss.Color(gold), @@ -128,6 +134,12 @@ var RosePineMoon = Theme{ Cursor: lipgloss.Color(moonFoam), }, + KeybindsModal: KeybindsModalTheme{ + Border: lipgloss.Color(foam), + BorderContent: lipgloss.Color(foam), + KeybindsText: lipgloss.Color(iris), + DescriptionText: lipgloss.White, + }, Help: HelpTheme{ Ellipsis: nil, ShortKey: lipgloss.Color(moonGold), diff --git a/internal/ui/theme/theme.go b/internal/ui/theme/theme.go index b9c4387..ed5ad0e 100644 --- a/internal/ui/theme/theme.go +++ b/internal/ui/theme/theme.go @@ -13,11 +13,12 @@ type Theme struct { Success color.Color - Table TableTheme - Error ErrorTheme - Password PasswordTheme - Input InputTheme - Help HelpTheme + Table TableTheme + Error ErrorTheme + Password PasswordTheme + Input InputTheme + KeybindsModal KeybindsModalTheme + Help HelpTheme } type TableTheme struct { @@ -55,6 +56,13 @@ type InputTheme struct { Cursor color.Color } +type KeybindsModalTheme struct { + Border, + BorderContent, + KeybindsText, + DescriptionText color.Color +} + // the help view type HelpTheme struct { Ellipsis, From c1be4ff381a7e392222f78cb39ddb8f6d98dac67 Mon Sep 17 00:00:00 2001 From: thecentinol Date: Thu, 23 Jul 2026 06:39:00 +0200 Subject: [PATCH 05/14] feat(keybindings): add grouping method for bindings --- internal/keybindings/keybindings.go | 5 +++ internal/keybindings/utils.go | 69 +++++++++++++++++++++++++++-- internal/ui/theme/default.go | 1 + internal/ui/theme/rose_pine.go | 2 + internal/ui/theme/theme.go | 1 + 5 files changed, 74 insertions(+), 4 deletions(-) diff --git a/internal/keybindings/keybindings.go b/internal/keybindings/keybindings.go index f378659..21cf7f8 100644 --- a/internal/keybindings/keybindings.go +++ b/internal/keybindings/keybindings.go @@ -232,3 +232,8 @@ type Binding struct { HelpDescription string // the help text for bubbles key.Binding.Help Description string // the longer full description displayed in the keybindings modal } + +type BindingGroup struct { + Title string + Bindings []Binding +} diff --git a/internal/keybindings/utils.go b/internal/keybindings/utils.go index 2a9ca42..ce5a496 100644 --- a/internal/keybindings/utils.go +++ b/internal/keybindings/utils.go @@ -4,7 +4,7 @@ import ( "charm.land/bubbles/v2/key" ) -// converts a single keybind to a bubbles keybinding +// converts a single keybinding to a bubbles keybinding func (b Binding) ToBubbles() key.Binding { return key.NewBinding( key.WithKeys(b.Keys...), @@ -12,6 +12,7 @@ func (b Binding) ToBubbles() key.Binding { ) } +// converts multiple keybindings to a bubbles keybindings func ToBubblesBatch(keys []Binding) []key.Binding { var binds []key.Binding @@ -26,16 +27,75 @@ func ToBubblesBatch(keys []Binding) []key.Binding { return binds } -// returns all keybindings +// Returns all keybindings func (k Keybindings) All() []Binding { return []Binding{ k.FocusedWifiSaved, k.FocusedWifiAvailable, k.Quit, k.WifiConnectSaved, k.WifiDisconnect, k.WifiForget, k.WifiAutoConnect, k.WifiScan, k.WifiConnectAvailable, k.LineUp, k.LineDown, - k.GotoTop, k.GotoBottom, k.ErrorDismiss, k.PasswordSubmit, k.PasswordCancel, + k.GotoTop, k.GotoBottom, k.OpenKeybindingsModal, k.CloseKeybindingsModal, k.ErrorDismiss, + k.PasswordSubmit, k.PasswordCancel, } } -// get keybindings by ViewName +// Get all Keybindings and sort them by Group +func (k Keybindings) AllGrouped() []BindingGroup { + var groups []BindingGroup + var globalGroup BindingGroup + var wifiGroup BindingGroup + var tableGroup BindingGroup + var kbGroup BindingGroup + var errGroup BindingGroup + var passwordGroup BindingGroup + + for _, b := range k.All() { + if b.Group == "global" { + globalGroup = BindingGroup{ + Title: b.Group, + Bindings: k.ByGroup("global"), + } + } + if b.Group == "wifi" { + wifiGroup = BindingGroup{ + Title: b.Group, + Bindings: k.ByGroup("wifi"), + } + } + if b.Group == "table" { + tableGroup = BindingGroup{ + Title: b.Group, + Bindings: k.ByGroup("table"), + } + } + if b.Group == "keybindings" { + kbGroup = BindingGroup{ + Title: b.Group, + Bindings: k.ByGroup("keybindings"), + } + } + if b.Group == "error" { + errGroup = BindingGroup{ + Title: b.Group, + Bindings: k.ByGroup("error"), + } + } + if b.Group == "password" { + passwordGroup = BindingGroup{ + Title: b.Group, + Bindings: k.ByGroup("password"), + } + } + } + + groups = append(groups, globalGroup) + groups = append(groups, wifiGroup) + groups = append(groups, tableGroup) + groups = append(groups, kbGroup) + groups = append(groups, errGroup) + groups = append(groups, passwordGroup) + return groups +} + +// Get keybindings by ViewName func (k Keybindings) ByView(viewName string) []Binding { var binds []Binding for _, b := range k.All() { @@ -46,6 +106,7 @@ func (k Keybindings) ByView(viewName string) []Binding { return binds } +// Get keybindings by Group func (k Keybindings) ByGroup(group string) []Binding { var binds []Binding for _, kb := range k.All() { diff --git a/internal/ui/theme/default.go b/internal/ui/theme/default.go index 00b5572..8a83a7f 100644 --- a/internal/ui/theme/default.go +++ b/internal/ui/theme/default.go @@ -37,6 +37,7 @@ var Default = Theme{ KeybindsModal: KeybindsModalTheme{ Border: lipgloss.Green, BorderContent: lipgloss.Green, + GroupText: lipgloss.Green, KeybindsText: lipgloss.White, DescriptionText: lipgloss.Color("#6e6a86"), }, diff --git a/internal/ui/theme/rose_pine.go b/internal/ui/theme/rose_pine.go index 0daf741..ea04339 100644 --- a/internal/ui/theme/rose_pine.go +++ b/internal/ui/theme/rose_pine.go @@ -80,6 +80,7 @@ var RosePine = Theme{ KeybindsModal: KeybindsModalTheme{ Border: lipgloss.Color(foam), BorderContent: lipgloss.Color(foam), + GroupText: lipgloss.Color(foam), KeybindsText: lipgloss.Color(iris), DescriptionText: lipgloss.White, }, @@ -137,6 +138,7 @@ var RosePineMoon = Theme{ KeybindsModal: KeybindsModalTheme{ Border: lipgloss.Color(foam), BorderContent: lipgloss.Color(foam), + GroupText: lipgloss.Color(foam), KeybindsText: lipgloss.Color(iris), DescriptionText: lipgloss.White, }, diff --git a/internal/ui/theme/theme.go b/internal/ui/theme/theme.go index ed5ad0e..5caed68 100644 --- a/internal/ui/theme/theme.go +++ b/internal/ui/theme/theme.go @@ -59,6 +59,7 @@ type InputTheme struct { type KeybindsModalTheme struct { Border, BorderContent, + GroupText, KeybindsText, DescriptionText color.Color } From c6b369896e0be9a1846d379b632c1b14eb11da3e Mon Sep 17 00:00:00 2001 From: thecentinol Date: Thu, 23 Jul 2026 07:13:41 +0200 Subject: [PATCH 06/14] refactor(theme): change keybindings KeybindsText color --- internal/ui/theme/default.go | 4 ++-- internal/ui/theme/rose_pine.go | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/ui/theme/default.go b/internal/ui/theme/default.go index 8a83a7f..7fc24ef 100644 --- a/internal/ui/theme/default.go +++ b/internal/ui/theme/default.go @@ -38,8 +38,8 @@ var Default = Theme{ Border: lipgloss.Green, BorderContent: lipgloss.Green, GroupText: lipgloss.Green, - KeybindsText: lipgloss.White, - DescriptionText: lipgloss.Color("#6e6a86"), + KeybindsText: lipgloss.Color("#038B5A"), + DescriptionText: lipgloss.White, }, Help: HelpTheme{ Ellipsis: nil, diff --git a/internal/ui/theme/rose_pine.go b/internal/ui/theme/rose_pine.go index ea04339..0e51176 100644 --- a/internal/ui/theme/rose_pine.go +++ b/internal/ui/theme/rose_pine.go @@ -81,7 +81,7 @@ var RosePine = Theme{ Border: lipgloss.Color(foam), BorderContent: lipgloss.Color(foam), GroupText: lipgloss.Color(foam), - KeybindsText: lipgloss.Color(iris), + KeybindsText: lipgloss.Color(gold), DescriptionText: lipgloss.White, }, Help: HelpTheme{ @@ -136,10 +136,10 @@ var RosePineMoon = Theme{ Cursor: lipgloss.Color(moonFoam), }, KeybindsModal: KeybindsModalTheme{ - Border: lipgloss.Color(foam), - BorderContent: lipgloss.Color(foam), - GroupText: lipgloss.Color(foam), - KeybindsText: lipgloss.Color(iris), + Border: lipgloss.Color(moonFoam), + BorderContent: lipgloss.Color(moonFoam), + GroupText: lipgloss.Color(moonFoam), + KeybindsText: lipgloss.Color(moonGold), DescriptionText: lipgloss.White, }, Help: HelpTheme{ From c6f7fe1737b81df311908a28366f3f3ec9fc4a4d Mon Sep 17 00:00:00 2001 From: thecentinol Date: Sat, 25 Jul 2026 22:12:31 +0200 Subject: [PATCH 07/14] refactor(keybindings): define global cancel binding & resolve linter issues --- internal/keybindings/keybindings.go | 61 ++++++++++++++--------------- internal/keybindings/utils.go | 54 ++++++++++++------------- 2 files changed, 56 insertions(+), 59 deletions(-) diff --git a/internal/keybindings/keybindings.go b/internal/keybindings/keybindings.go index 21cf7f8..5a3762b 100644 --- a/internal/keybindings/keybindings.go +++ b/internal/keybindings/keybindings.go @@ -1,9 +1,5 @@ package keybindings -import ( -// "charm.land/bubbles/v2/key" -) - const ( // ViewNames's wifiSavedView = "wifi-saved" @@ -15,8 +11,13 @@ const ( globalGroup = "global" wifiGroup = "wifi" tableGroup = "table" + keybindsGroup = "keybindings" errModalGroup = "error" passwordGroup = "password" + + // keys + keyEnter = "enter" + keyEsc = "esc" ) func DefaultKeybindings() Keybindings { @@ -38,6 +39,14 @@ func DefaultKeybindings() Keybindings { HelpDescription: "", Description: "Switch focus to available wifi networks pane", }, + Cancel: Binding{ + ViewName: "", + Group: "global", + Keys: []string{keyEsc}, + HelpKey: keyEsc, + HelpDescription: "cancel", + Description: "Cancel", + }, Quit: Binding{ ViewName: "", Group: globalGroup, @@ -51,8 +60,8 @@ func DefaultKeybindings() Keybindings { WifiConnectSaved: Binding{ ViewName: wifiSavedView, Group: wifiGroup, - Keys: []string{"enter"}, - HelpKey: "Enter", + Keys: []string{keyEnter}, + HelpKey: keyEnter, HelpDescription: "connect", Description: "Connect to a saved wifi connection", }, @@ -61,7 +70,7 @@ func DefaultKeybindings() Keybindings { Group: wifiGroup, Keys: []string{"d"}, HelpKey: "d", - HelpDescription: "diconnect", + HelpDescription: "disconnect", Description: "Disconnect from current wifi network", }, WifiForget: Binding{ @@ -91,14 +100,13 @@ func DefaultKeybindings() Keybindings { WifiConnectAvailable: Binding{ ViewName: wifiAvailableView, Group: wifiGroup, - Keys: []string{"enter"}, - HelpKey: "enter", + Keys: []string{keyEnter}, + HelpKey: keyEnter, HelpDescription: "connect", Description: "Connect to a new wifi network", }, - // table navigation - // ViewName is left nil because multiple models will use a table. + // Navigation LineUp: Binding{ Group: tableGroup, Keys: []string{"k", "up"}, @@ -135,23 +143,20 @@ func DefaultKeybindings() Keybindings { Keys: []string{"?"}, HelpKey: "?", HelpDescription: "keybindings", - Description: "Open keybindings modal", + Description: "Open keybindings menu", }, CloseKeybindingsModal: Binding{ - ViewName: "keybindings-modal", - Group: "keybindings", - Keys: []string{"esc"}, - HelpKey: "escape", + Keys: []string{keyEsc}, + HelpKey: keyEsc, HelpDescription: "close", - Description: "Close keybindings modal", }, // Error modal ErrorDismiss: Binding{ ViewName: errModalView, Group: errModalGroup, - Keys: []string{"esc", "enter"}, - HelpKey: "esc", + Keys: []string{keyEsc, keyEnter}, + HelpKey: keyEsc, HelpDescription: "dismiss", Description: "Dismiss the error modal", }, @@ -163,24 +168,16 @@ func DefaultKeybindings() Keybindings { Keys: []string{"ctrl+t"}, HelpKey: "ctrl+t", HelpDescription: "hide/show password", - Description: "Toggle the password inputs hidden state", + Description: "Toggle the password input's hidden state", }, PasswordSubmit: Binding{ ViewName: passwordView, Group: passwordGroup, - Keys: []string{"enter"}, - HelpKey: "enter", + Keys: []string{keyEnter}, + HelpKey: keyEnter, HelpDescription: "submit", Description: "Submit password", }, - PasswordCancel: Binding{ - ViewName: passwordView, - Group: passwordGroup, - Keys: []string{"esc"}, - HelpKey: "esc", - HelpDescription: "cancel", - Description: "Cancel", - }, } } @@ -191,6 +188,7 @@ type Keybindings struct { // Global FocusedWifiSaved Binding FocusedWifiAvailable Binding + Cancel Binding Quit Binding // WiFi @@ -201,7 +199,7 @@ type Keybindings struct { WifiScan Binding WifiConnectAvailable Binding - // Table navigation + // Navigation LineUp Binding LineDown Binding GotoTop Binding @@ -217,7 +215,6 @@ type Keybindings struct { // Password modal PasswordVisibility Binding PasswordSubmit Binding - PasswordCancel Binding } type Binding struct { diff --git a/internal/keybindings/utils.go b/internal/keybindings/utils.go index ce5a496..e7d6188 100644 --- a/internal/keybindings/utils.go +++ b/internal/keybindings/utils.go @@ -14,7 +14,7 @@ func (b Binding) ToBubbles() key.Binding { // converts multiple keybindings to a bubbles keybindings func ToBubblesBatch(keys []Binding) []key.Binding { - var binds []key.Binding + binds := make([]key.Binding, 0, len(keys)) for _, k := range keys { bind := key.NewBinding( @@ -33,65 +33,65 @@ func (k Keybindings) All() []Binding { k.FocusedWifiSaved, k.FocusedWifiAvailable, k.Quit, k.WifiConnectSaved, k.WifiDisconnect, k.WifiForget, k.WifiAutoConnect, k.WifiScan, k.WifiConnectAvailable, k.LineUp, k.LineDown, k.GotoTop, k.GotoBottom, k.OpenKeybindingsModal, k.CloseKeybindingsModal, k.ErrorDismiss, - k.PasswordSubmit, k.PasswordCancel, + k.PasswordSubmit, k.Cancel, } } // Get all Keybindings and sort them by Group func (k Keybindings) AllGrouped() []BindingGroup { - var groups []BindingGroup - var globalGroup BindingGroup - var wifiGroup BindingGroup - var tableGroup BindingGroup - var kbGroup BindingGroup - var errGroup BindingGroup - var passwordGroup BindingGroup + groups := make([]BindingGroup, 0, 6) + var global BindingGroup + var wifi BindingGroup + var table BindingGroup + var kb BindingGroup + var err BindingGroup + var password BindingGroup for _, b := range k.All() { - if b.Group == "global" { - globalGroup = BindingGroup{ + if b.Group == globalGroup { + global = BindingGroup{ Title: b.Group, Bindings: k.ByGroup("global"), } } - if b.Group == "wifi" { - wifiGroup = BindingGroup{ + if b.Group == wifiGroup { + wifi = BindingGroup{ Title: b.Group, Bindings: k.ByGroup("wifi"), } } - if b.Group == "table" { - tableGroup = BindingGroup{ + if b.Group == tableGroup { + table = BindingGroup{ Title: b.Group, Bindings: k.ByGroup("table"), } } - if b.Group == "keybindings" { - kbGroup = BindingGroup{ + if b.Group == keybindsGroup { + kb = BindingGroup{ Title: b.Group, Bindings: k.ByGroup("keybindings"), } } - if b.Group == "error" { - errGroup = BindingGroup{ + if b.Group == errModalGroup { + err = BindingGroup{ Title: b.Group, Bindings: k.ByGroup("error"), } } - if b.Group == "password" { - passwordGroup = BindingGroup{ + if b.Group == passwordGroup { + password = BindingGroup{ Title: b.Group, Bindings: k.ByGroup("password"), } } } - groups = append(groups, globalGroup) - groups = append(groups, wifiGroup) - groups = append(groups, tableGroup) - groups = append(groups, kbGroup) - groups = append(groups, errGroup) - groups = append(groups, passwordGroup) + groups = append(groups, global) + groups = append(groups, wifi) + groups = append(groups, table) + groups = append(groups, kb) + groups = append(groups, err) + groups = append(groups, password) return groups } From bcf191af3d66dfb5ae7c6c5561e2cc64367b3e38 Mon Sep 17 00:00:00 2001 From: thecentinol Date: Sat, 25 Jul 2026 22:15:38 +0200 Subject: [PATCH 08/14] refactor(ui): use global cancel binding to close password modal --- internal/ui/components/password_modal.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ui/components/password_modal.go b/internal/ui/components/password_modal.go index eaddb82..adaebb9 100644 --- a/internal/ui/components/password_modal.go +++ b/internal/ui/components/password_modal.go @@ -75,7 +75,7 @@ func (p PasswordModel) Update(msg tea.Msg) (PasswordModel, tea.Cmd) { password := p.Input.Value() p.Input.Reset() return p, handlePasswordSubmit(password) - case key.Matches(msg, p.keys.PasswordCancel.ToBubbles()): + case key.Matches(msg, p.keys.Cancel.ToBubbles()): p.Input.Reset() return p, handlePasswordClose() } @@ -101,7 +101,7 @@ func (p PasswordModel) HelpView() []key.Binding { help := []key.Binding{ p.keys.PasswordVisibility.ToBubbles(), p.keys.PasswordSubmit.ToBubbles(), - p.keys.PasswordCancel.ToBubbles(), + p.keys.Cancel.ToBubbles(), } return help } From eaa60feeb3605c7fbf272bc57494131252f284a1 Mon Sep 17 00:00:00 2001 From: thecentinol Date: Sat, 25 Jul 2026 22:33:16 +0200 Subject: [PATCH 09/14] refactor(keybindings): use global cancel binding to close keybindings modal --- internal/keybindings/keybindings.go | 9 ++------- internal/keybindings/utils.go | 8 ++++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/internal/keybindings/keybindings.go b/internal/keybindings/keybindings.go index 5a3762b..b2f3119 100644 --- a/internal/keybindings/keybindings.go +++ b/internal/keybindings/keybindings.go @@ -145,11 +145,6 @@ func DefaultKeybindings() Keybindings { HelpDescription: "keybindings", Description: "Open keybindings menu", }, - CloseKeybindingsModal: Binding{ - Keys: []string{keyEsc}, - HelpKey: keyEsc, - HelpDescription: "close", - }, // Error modal ErrorDismiss: Binding{ @@ -206,8 +201,8 @@ type Keybindings struct { GotoBottom Binding // Keybindings modal - OpenKeybindingsModal Binding - CloseKeybindingsModal Binding + // NOTE: Cancel is used to close the modal + OpenKeybindingsModal Binding // Error modal ErrorDismiss Binding diff --git a/internal/keybindings/utils.go b/internal/keybindings/utils.go index e7d6188..175f365 100644 --- a/internal/keybindings/utils.go +++ b/internal/keybindings/utils.go @@ -30,10 +30,10 @@ func ToBubblesBatch(keys []Binding) []key.Binding { // Returns all keybindings func (k Keybindings) All() []Binding { return []Binding{ - k.FocusedWifiSaved, k.FocusedWifiAvailable, k.Quit, k.WifiConnectSaved, k.WifiDisconnect, - k.WifiForget, k.WifiAutoConnect, k.WifiScan, k.WifiConnectAvailable, k.LineUp, k.LineDown, - k.GotoTop, k.GotoBottom, k.OpenKeybindingsModal, k.CloseKeybindingsModal, k.ErrorDismiss, - k.PasswordSubmit, k.Cancel, + k.FocusedWifiSaved, k.FocusedWifiAvailable, k.Cancel, k.Quit, k.WifiConnectSaved, + k.WifiDisconnect, k.WifiForget, k.WifiAutoConnect, k.WifiScan, k.WifiConnectAvailable, + k.LineUp, k.LineDown, k.GotoTop, k.GotoBottom, k.OpenKeybindingsModal, k.ErrorDismiss, + k.PasswordVisibility, k.PasswordSubmit, } } From 2769edb221a7b6c89ad7cdd460eef77f38e13d29 Mon Sep 17 00:00:00 2001 From: thecentinol Date: Sun, 26 Jul 2026 02:19:05 +0200 Subject: [PATCH 10/14] refactor(keybindings): improve grouping, descriptions & Keybindings struct field names --- internal/keybindings/keybindings.go | 77 ++++++++++++++--------------- internal/keybindings/utils.go | 32 +++++------- internal/ui/components/table.go | 8 +-- 3 files changed, 52 insertions(+), 65 deletions(-) diff --git a/internal/keybindings/keybindings.go b/internal/keybindings/keybindings.go index b2f3119..bec2266 100644 --- a/internal/keybindings/keybindings.go +++ b/internal/keybindings/keybindings.go @@ -1,21 +1,21 @@ package keybindings const ( - // ViewNames's + // ViewName + globalView = "global" wifiSavedView = "wifi-saved" wifiAvailableView = "wifi-available" errModalView = "error-modal" passwordView = "password-modal" - // Groups + // Group globalGroup = "global" wifiGroup = "wifi" - tableGroup = "table" - keybindsGroup = "keybindings" - errModalGroup = "error" - passwordGroup = "password" + navGroup = "navigation" + errModalGroup = "error modal" + passwordGroup = "password modal" - // keys + // Keys keyEnter = "enter" keyEsc = "esc" ) @@ -24,7 +24,7 @@ func DefaultKeybindings() Keybindings { return Keybindings{ // Global FocusedWifiSaved: Binding{ - ViewName: "", + ViewName: globalView, Group: globalGroup, Keys: []string{"1"}, HelpKey: "1", @@ -32,7 +32,7 @@ func DefaultKeybindings() Keybindings { Description: "Switch focus to saved wifi connections pane", }, FocusedWifiAvailable: Binding{ - ViewName: "", + ViewName: globalView, Group: globalGroup, Keys: []string{"2"}, HelpKey: "2", @@ -40,15 +40,23 @@ func DefaultKeybindings() Keybindings { Description: "Switch focus to available wifi networks pane", }, Cancel: Binding{ - ViewName: "", - Group: "global", + ViewName: globalView, + Group: globalGroup, Keys: []string{keyEsc}, HelpKey: keyEsc, HelpDescription: "cancel", Description: "Cancel", }, + OpenKeybindingsModal: Binding{ + ViewName: globalView, + Group: globalGroup, + Keys: []string{"?"}, + HelpKey: "?", + HelpDescription: "keybindings", + Description: "Open keybindings menu", + }, Quit: Binding{ - ViewName: "", + ViewName: globalView, Group: globalGroup, Keys: []string{"q", "ctrl+c"}, HelpKey: "q", @@ -86,7 +94,7 @@ func DefaultKeybindings() Keybindings { Group: wifiGroup, Keys: []string{"a"}, HelpKey: "a", - HelpDescription: "toggle auto-connect", + HelpDescription: "auto-connect", Description: "Toggle auto-connect state for the selected wifi connection", }, WifiScan: Binding{ @@ -107,43 +115,33 @@ func DefaultKeybindings() Keybindings { }, // Navigation - LineUp: Binding{ - Group: tableGroup, + Up: Binding{ + Group: navGroup, Keys: []string{"k", "up"}, HelpKey: "↑", - HelpDescription: "move up", + HelpDescription: "up", Description: "Move up by one line", }, - LineDown: Binding{ - Group: tableGroup, + Down: Binding{ + Group: navGroup, Keys: []string{"j", "down"}, HelpKey: "", - HelpDescription: "move down", + HelpDescription: "down", Description: "Move down by one line", }, GotoTop: Binding{ - Group: tableGroup, + Group: navGroup, Keys: []string{"t"}, HelpKey: "t", - HelpDescription: "move to top", - Description: "Go to the top of the table", + HelpDescription: "top", + Description: "Move/Scroll to top", }, GotoBottom: Binding{ - Group: tableGroup, + Group: navGroup, Keys: []string{"b"}, HelpKey: "b", - HelpDescription: "move to bottom", - Description: "Go to the bottom of the table", - }, - - // Keybindings modal - OpenKeybindingsModal: Binding{ - ViewName: "keybindings-modal", - Group: "keybindings", - Keys: []string{"?"}, - HelpKey: "?", - HelpDescription: "keybindings", - Description: "Open keybindings menu", + HelpDescription: "bottom", + Description: "Move/Scroll to bottom", }, // Error modal @@ -184,6 +182,7 @@ type Keybindings struct { FocusedWifiSaved Binding FocusedWifiAvailable Binding Cancel Binding + OpenKeybindingsModal Binding // NOTE: Cancel is used to close the modal Quit Binding // WiFi @@ -195,15 +194,11 @@ type Keybindings struct { WifiConnectAvailable Binding // Navigation - LineUp Binding - LineDown Binding + Up Binding + Down Binding GotoTop Binding GotoBottom Binding - // Keybindings modal - // NOTE: Cancel is used to close the modal - OpenKeybindingsModal Binding - // Error modal ErrorDismiss Binding diff --git a/internal/keybindings/utils.go b/internal/keybindings/utils.go index 175f365..c5bf9d2 100644 --- a/internal/keybindings/utils.go +++ b/internal/keybindings/utils.go @@ -32,7 +32,7 @@ func (k Keybindings) All() []Binding { return []Binding{ k.FocusedWifiSaved, k.FocusedWifiAvailable, k.Cancel, k.Quit, k.WifiConnectSaved, k.WifiDisconnect, k.WifiForget, k.WifiAutoConnect, k.WifiScan, k.WifiConnectAvailable, - k.LineUp, k.LineDown, k.GotoTop, k.GotoBottom, k.OpenKeybindingsModal, k.ErrorDismiss, + k.Up, k.Down, k.GotoTop, k.GotoBottom, k.OpenKeybindingsModal, k.ErrorDismiss, k.PasswordVisibility, k.PasswordSubmit, } } @@ -42,8 +42,7 @@ func (k Keybindings) AllGrouped() []BindingGroup { groups := make([]BindingGroup, 0, 6) var global BindingGroup var wifi BindingGroup - var table BindingGroup - var kb BindingGroup + var nav BindingGroup var err BindingGroup var password BindingGroup @@ -51,45 +50,38 @@ func (k Keybindings) AllGrouped() []BindingGroup { if b.Group == globalGroup { global = BindingGroup{ Title: b.Group, - Bindings: k.ByGroup("global"), + Bindings: k.ByGroup(globalGroup), } } - if b.Group == wifiGroup { - wifi = BindingGroup{ + if b.Group == navGroup { + nav = BindingGroup{ Title: b.Group, - Bindings: k.ByGroup("wifi"), + Bindings: k.ByGroup(navGroup), } } - if b.Group == tableGroup { - table = BindingGroup{ - Title: b.Group, - Bindings: k.ByGroup("table"), - } - } - if b.Group == keybindsGroup { - kb = BindingGroup{ + if b.Group == wifiGroup { + wifi = BindingGroup{ Title: b.Group, - Bindings: k.ByGroup("keybindings"), + Bindings: k.ByGroup(wifiGroup), } } if b.Group == errModalGroup { err = BindingGroup{ Title: b.Group, - Bindings: k.ByGroup("error"), + Bindings: k.ByGroup(errModalGroup), } } if b.Group == passwordGroup { password = BindingGroup{ Title: b.Group, - Bindings: k.ByGroup("password"), + Bindings: k.ByGroup(passwordGroup), } } } groups = append(groups, global) + groups = append(groups, nav) groups = append(groups, wifi) - groups = append(groups, table) - groups = append(groups, kb) groups = append(groups, err) groups = append(groups, password) return groups diff --git a/internal/ui/components/table.go b/internal/ui/components/table.go index 085cd72..d7fde5e 100644 --- a/internal/ui/components/table.go +++ b/internal/ui/components/table.go @@ -69,9 +69,9 @@ func (t *TableModel) Update(msg tea.Msg) (TableModel, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: switch { - case key.Matches(msg, t.keys.LineUp.ToBubbles()): + case key.Matches(msg, t.keys.Up.ToBubbles()): t.table.MoveUp(1) - case key.Matches(msg, t.keys.LineDown.ToBubbles()): + case key.Matches(msg, t.keys.Down.ToBubbles()): t.table.MoveDown(1) case key.Matches(msg, t.keys.GotoTop.ToBubbles()): t.table.GotoTop() @@ -90,8 +90,8 @@ func (t *TableModel) View() tea.View { func (t TableModel) HelpView() []key.Binding { return []key.Binding{ - t.keys.LineUp.ToBubbles(), - t.keys.LineDown.ToBubbles(), + t.keys.Up.ToBubbles(), + t.keys.Down.ToBubbles(), t.keys.GotoTop.ToBubbles(), t.keys.GotoBottom.ToBubbles(), } From 408e90b991e54971a5ac7826353485cfc14d31ec Mon Sep 17 00:00:00 2001 From: thecentinol Date: Sun, 26 Jul 2026 02:49:55 +0200 Subject: [PATCH 11/14] refactor(keybindings): split wifi grouping by saved and available --- internal/keybindings/keybindings.go | 39 ++++++++++++++++------------- internal/keybindings/utils.go | 18 +++++++++---- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/internal/keybindings/keybindings.go b/internal/keybindings/keybindings.go index bec2266..c225bfb 100644 --- a/internal/keybindings/keybindings.go +++ b/internal/keybindings/keybindings.go @@ -9,11 +9,12 @@ const ( passwordView = "password-modal" // Group - globalGroup = "global" - wifiGroup = "wifi" - navGroup = "navigation" - errModalGroup = "error modal" - passwordGroup = "password modal" + globalGroup = "global" + wifiSavedGroup = "saved connections" + wifiAvailableGroup = "available networks" + navGroup = "navigation" + errModalGroup = "error modal" + passwordGroup = "password modal" // Keys keyEnter = "enter" @@ -64,54 +65,56 @@ func DefaultKeybindings() Keybindings { Description: "Quit the application", }, - // WiFi + // WiFi saved WifiConnectSaved: Binding{ ViewName: wifiSavedView, - Group: wifiGroup, + Group: wifiSavedGroup, Keys: []string{keyEnter}, HelpKey: keyEnter, HelpDescription: "connect", - Description: "Connect to a saved wifi connection", + Description: "Connect", }, WifiDisconnect: Binding{ ViewName: wifiSavedView, - Group: wifiGroup, + Group: wifiSavedGroup, Keys: []string{"d"}, HelpKey: "d", HelpDescription: "disconnect", - Description: "Disconnect from current wifi network", + Description: "Disconnect from current WiFi network", }, WifiForget: Binding{ ViewName: wifiSavedView, - Group: wifiGroup, + Group: wifiSavedGroup, Keys: []string{"f"}, HelpKey: "f", HelpDescription: "forget", - Description: "Remove the selected wifi connection", + Description: "Remove the selected WiFi connection", }, WifiAutoConnect: Binding{ ViewName: wifiSavedView, - Group: wifiGroup, + Group: wifiSavedGroup, Keys: []string{"a"}, HelpKey: "a", HelpDescription: "auto-connect", - Description: "Toggle auto-connect state for the selected wifi connection", + Description: "Toggle auto-connect on/off for the selected WiFi connection", }, + + // WiFi available WifiScan: Binding{ ViewName: wifiAvailableView, - Group: wifiGroup, + Group: wifiAvailableGroup, Keys: []string{"s"}, HelpKey: "s", HelpDescription: "Scan", - Description: "Scan for nearby wifi networks", + Description: "Scan for nearby WiFi networks", }, WifiConnectAvailable: Binding{ ViewName: wifiAvailableView, - Group: wifiGroup, + Group: wifiAvailableGroup, Keys: []string{keyEnter}, HelpKey: keyEnter, HelpDescription: "connect", - Description: "Connect to a new wifi network", + Description: "Connect", }, // Navigation diff --git a/internal/keybindings/utils.go b/internal/keybindings/utils.go index c5bf9d2..8f06a99 100644 --- a/internal/keybindings/utils.go +++ b/internal/keybindings/utils.go @@ -41,7 +41,8 @@ func (k Keybindings) All() []Binding { func (k Keybindings) AllGrouped() []BindingGroup { groups := make([]BindingGroup, 0, 6) var global BindingGroup - var wifi BindingGroup + var wifiSaved BindingGroup + var wifiAvailable BindingGroup var nav BindingGroup var err BindingGroup var password BindingGroup @@ -59,10 +60,16 @@ func (k Keybindings) AllGrouped() []BindingGroup { Bindings: k.ByGroup(navGroup), } } - if b.Group == wifiGroup { - wifi = BindingGroup{ + if b.Group == wifiSavedGroup { + wifiSaved = BindingGroup{ Title: b.Group, - Bindings: k.ByGroup(wifiGroup), + Bindings: k.ByGroup(wifiSavedGroup), + } + } + if b.Group == wifiAvailableGroup { + wifiAvailable = BindingGroup{ + Title: b.Group, + Bindings: k.ByGroup(wifiAvailableGroup), } } if b.Group == errModalGroup { @@ -81,7 +88,8 @@ func (k Keybindings) AllGrouped() []BindingGroup { groups = append(groups, global) groups = append(groups, nav) - groups = append(groups, wifi) + groups = append(groups, wifiSaved) + groups = append(groups, wifiAvailable) groups = append(groups, err) groups = append(groups, password) return groups From 998fe10bd8f19b19ba29f41a13965d654b5fde57 Mon Sep 17 00:00:00 2001 From: thecentinol Date: Sun, 26 Jul 2026 15:38:02 +0200 Subject: [PATCH 12/14] feat(ui): add keybindings modal --- internal/keybindings/keybindings.go | 6 +- internal/ui/keybindings_modal/model.go | 159 +++++++++++++++++++++++++ internal/ui/root/model.go | 92 ++++++++++---- 3 files changed, 234 insertions(+), 23 deletions(-) create mode 100644 internal/ui/keybindings_modal/model.go diff --git a/internal/keybindings/keybindings.go b/internal/keybindings/keybindings.go index c225bfb..c94d638 100644 --- a/internal/keybindings/keybindings.go +++ b/internal/keybindings/keybindings.go @@ -1,15 +1,17 @@ package keybindings const ( + global = "global" + // ViewName - globalView = "global" + globalView = global wifiSavedView = "wifi-saved" wifiAvailableView = "wifi-available" errModalView = "error-modal" passwordView = "password-modal" // Group - globalGroup = "global" + globalGroup = global wifiSavedGroup = "saved connections" wifiAvailableGroup = "available networks" navGroup = "navigation" diff --git a/internal/ui/keybindings_modal/model.go b/internal/ui/keybindings_modal/model.go new file mode 100644 index 0000000..f18727e --- /dev/null +++ b/internal/ui/keybindings_modal/model.go @@ -0,0 +1,159 @@ +package keybindingsmodal + +import ( + "strings" + + "charm.land/bubbles/v2/help" + "charm.land/bubbles/v2/key" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + + "github.com/thecentinol/tuwi/internal/events" + kb "github.com/thecentinol/tuwi/internal/keybindings" + comp "github.com/thecentinol/tuwi/internal/ui/components" + "github.com/thecentinol/tuwi/internal/ui/theme" +) + +type Model struct { + theme *theme.Theme + vp viewport.Model + + keys kb.Keybindings + help help.Model + + Height, Width int + X, Y int + IsFocused bool +} + +func NewKeybindsModal(theme *theme.Theme, keys kb.Keybindings) Model { + m := Model{ + theme: theme, + vp: viewport.New(), + keys: keys, + help: help.New(), + } + + m.renderContent() + + return m +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyPressMsg: + switch { + case key.Matches(msg, m.keys.Up.ToBubbles()): + m.vp.ScrollUp(3) + + case key.Matches(msg, m.keys.Down.ToBubbles()): + m.vp.ScrollDown(3) + + case key.Matches(msg, m.keys.GotoTop.ToBubbles()): + m.vp.GotoTop() + return m, nil + + case key.Matches(msg, m.keys.GotoBottom.ToBubbles()): + m.vp.GotoBottom() + return m, nil + + case key.Matches(msg, m.keys.Cancel.ToBubbles()): + return m, events.CloseKeybindsModal() + } + } + + m.vp, cmd = m.vp.Update(msg) + return m, cmd +} + +func (m Model) View() tea.View { + container := comp.NewBorderContent( + m.theme.KeybindsModal.Border, + m.theme.KeybindsModal.BorderContent, + m.Height, + ) + + return tea.NewView(container.Render("Keybindings", m.vp.View(), m.Width)) +} + +func (m Model) HelpView() []key.Binding { + // Note: OpenKeybindingsModal is excluded because it is handled + // by the root model's HelpView. + + // Override default help descriptions to provide modal-specific context. + cancel := m.keys.Cancel.ToBubbles() + scrollUp := m.keys.Up.ToBubbles() + scrollDown := m.keys.Down.ToBubbles() + cancel.SetHelp(m.keys.Cancel.HelpKey, "Close/Cancel") + scrollUp.SetHelp(m.keys.Up.HelpKey, "Scroll up") + scrollDown.SetHelp(m.keys.Down.HelpKey, "Scroll down") + + binds := []key.Binding{ + scrollUp, + scrollDown, + m.keys.GotoTop.ToBubbles(), + m.keys.GotoBottom.ToBubbles(), + cancel, + } + return binds +} + +func (m *Model) SetSize(width, height int) { + m.Width = width + m.Height = height + m.vp.SetWidth(width) + m.vp.SetHeight(height) +} + +func (m *Model) renderContent() { + var builder strings.Builder + + titleStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(m.theme.KeybindsModal.GroupText). + MarginBottom(1) + + groupStyle := lipgloss.NewStyle(). + PaddingLeft(5). + MarginBottom(2) + + groups := m.keys.AllGrouped() + + // styling the bindings + help := m.help + help.Styles.FullKey = lipgloss.NewStyle(). + Foreground(m.theme.KeybindsModal.KeybindsText) + + help.Styles.FullDesc = lipgloss.NewStyle(). + Foreground(m.theme.KeybindsModal.DescriptionText) + + for _, group := range groups { + title := titleStyle.Render("── " + group.Title + " ──") + + bindings := help.FullHelpView([][]key.Binding{ + kb.ToBubblesBatch(group.Bindings), + }) + + section := groupStyle.Render( + lipgloss.JoinVertical( + lipgloss.Left, + title, + bindings, + ), + ) + + // WriteString returns a nil error, the `revive` linter will complain about not handling the + // the error's for the below WriteString's. So just like the liberals, we tell em to shut up. + builder.WriteString(section) //nolint:revive + builder.WriteString("\n") //nolint:revive + } + + m.vp.SetContent(builder.String()) +} diff --git a/internal/ui/root/model.go b/internal/ui/root/model.go index 97626bf..4889127 100644 --- a/internal/ui/root/model.go +++ b/internal/ui/root/model.go @@ -11,6 +11,7 @@ import ( "github.com/thecentinol/tuwi/internal/keybindings" nm "github.com/thecentinol/tuwi/internal/networkmanager" comp "github.com/thecentinol/tuwi/internal/ui/components" + kbmodal "github.com/thecentinol/tuwi/internal/ui/keybindings_modal" "github.com/thecentinol/tuwi/internal/ui/theme" wifiui "github.com/thecentinol/tuwi/internal/ui/wifi" wifidomain "github.com/thecentinol/tuwi/internal/wifi" @@ -18,12 +19,12 @@ import ( type Focused string -// make sure these match Keybindings ViewName's const ( - FocusSaved Focused = "wifi-saved" - FocusAvailable Focused = "wifi-available" - FocusPasswordModal Focused = "password-modal" - FocusErrorModal Focused = "error-modal" + FocusWifiSaved Focused = "wifi-saved" + FocusWifiAvailable Focused = "wifi-available" + FocusKeybindingsModal Focused = "keybindings-modal" + FocusPasswordModal Focused = "password-modal" + FocusErrorModal Focused = "error-modal" ) type Model struct { @@ -39,6 +40,9 @@ type Model struct { wifiAvailable wifiui.AvailableModel selectedNetwork *nm.AccessPoint + keybindingsModal kbmodal.Model + showKeybindingsModal bool + passwordModal comp.PasswordModel showPasswordModal bool @@ -49,16 +53,18 @@ type Model struct { func NewModel(client *dbus.Client, keys keybindings.Keybindings) Model { th := theme.Default state := &wifidomain.State{} + return Model{ - Client: client, - focus: FocusSaved, - help: help.New(), - wifiSaved: wifiui.NewWifiSavedModel(client, state, keys, &th), - wifiAvailable: wifiui.NewWifiAvailableModel(client, state, keys, &th), - passwordModal: comp.NewPasswordModal(&th, keys), - errorModal: comp.NewErrorModal(&th, keys), - keys: keys, - theme: &th, + Client: client, + focus: FocusWifiSaved, + help: help.New(), + wifiSaved: wifiui.NewWifiSavedModel(client, state, keys, &th), + wifiAvailable: wifiui.NewWifiAvailableModel(client, state, keys, &th), + keybindingsModal: kbmodal.NewKeybindsModal(&th, keys), + passwordModal: comp.NewPasswordModal(&th, keys), + errorModal: comp.NewErrorModal(&th, keys), + keys: keys, + theme: &th, } } @@ -83,6 +89,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.height = msg.Height m.sizeComponents() + case events.OpenKeybindsModalMsg: + m.showKeybindingsModal = true + return m, m.keybindingsModal.Init() + + case events.CloseKeybindsModalMsg: + m.showKeybindingsModal = false + // TODO: get previous focus before the keybindingsModal was opened and switch back to that. + m.focus = FocusWifiAvailable + case events.ShowPasswordModalMsg: m.showPasswordModal = true m.selectedNetwork = msg.Network @@ -125,10 +140,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.errorModal, cmd = m.errorModal.Update(msg) cmds = append(cmds, cmd) } + if m.showKeybindingsModal { + m.keybindingsModal, cmd = m.keybindingsModal.Update(msg) + cmds = append(cmds, cmd) + } } - m.wifiSaved.IsFocused = m.focus == FocusSaved - m.wifiAvailable.IsFocused = m.focus == FocusAvailable + m.wifiSaved.IsFocused = m.focus == FocusWifiSaved + m.wifiAvailable.IsFocused = m.focus == FocusWifiAvailable + m.keybindingsModal.IsFocused = m.focus == FocusKeybindingsModal return m, tea.Batch(cmds...) } @@ -137,6 +157,7 @@ func (m Model) View() tea.View { var view tea.View wifiSavedView := m.wifiSaved.View().Content wifiAvailableView := m.wifiAvailable.View().Content + kbView := m.keybindingsModal.View().Content helpView := m.HelpView() passwordView := m.passwordModal.View().Content errorModalView := m.errorModal.View().Content @@ -175,6 +196,15 @@ func (m Model) View() tea.View { layers = append(layers, errorModal) } + if m.showKeybindingsModal && !m.showErrorModal && !m.showPasswordModal { + kbModal := lipgloss.NewLayer(kbView). + Z(1). + X(m.keybindingsModal.X). + Y(m.keybindingsModal.Y) + + layers = append(layers, kbModal) + } + comp := lipgloss.NewCompositor(layers...) view.SetContent(comp.Render()) view.BackgroundColor = m.theme.BG @@ -184,7 +214,7 @@ func (m Model) View() tea.View { func (m *Model) sizeComponents() { halfWidth := m.width / 2 - // halfHeight := m.height / 2 + halfHeight := m.height / 2 helpHeight := 2 modalWidth := m.width / 3 tableHeight := m.height - helpHeight - 2 @@ -199,6 +229,11 @@ func (m *Model) sizeComponents() { m.wifiAvailable.Table.SetWidth(halfWidth) m.wifiAvailable.Table.SetHeight(tableHeight) + // Keybindings modal + m.keybindingsModal.SetSize(halfWidth, (m.height*3)/4) + m.keybindingsModal.X = halfWidth - (m.keybindingsModal.Width / 2) + m.keybindingsModal.Y = halfHeight - (m.keybindingsModal.Height / 2) + // Password Modal iconWidth := 2 m.passwordModal.Width = modalWidth @@ -232,11 +267,18 @@ func (m *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { case m.showErrorModal: m.errorModal, cmd = m.errorModal.Update(msg) + case key.Matches(msg, m.keys.OpenKeybindingsModal.ToBubbles()): + m.focus = FocusKeybindingsModal + return events.OpenKeybindsModal() + + case m.showKeybindingsModal: + m.keybindingsModal, cmd = m.keybindingsModal.Update(msg) + case key.Matches(msg, m.keys.FocusedWifiSaved.ToBubbles()): - m.focus = FocusSaved + m.focus = FocusWifiSaved case key.Matches(msg, m.keys.FocusedWifiAvailable.ToBubbles()): - m.focus = FocusAvailable + m.focus = FocusWifiAvailable case m.wifiSaved.IsFocused: m.wifiSaved, cmd = m.wifiSaved.Update(msg) @@ -254,14 +296,22 @@ func (m Model) HelpView() string { if m.showPasswordModal { help = append(help, m.passwordModal.HelpView()...) } - if m.focus == FocusSaved { + if m.showKeybindingsModal { + help = append(help, m.keybindingsModal.HelpView()...) + } + if m.focus == FocusWifiSaved { help = append(help, m.wifiSaved.HelpView()...) } - if m.focus == FocusAvailable && !m.showPasswordModal { + if m.focus == FocusWifiAvailable && !m.showPasswordModal { help = append(help, m.wifiAvailable.HelpView()...) } help = append(help, m.keys.Quit.ToBubbles()) + + if !m.showKeybindingsModal { + help = append(help, m.keys.OpenKeybindingsModal.ToBubbles()) + } + render := m.help render.Styles.Ellipsis = lipgloss.NewStyle(). Foreground(m.theme.Help.Ellipsis) From ebe549eabe406aca1f6ac41f52ccd597076abba0 Mon Sep 17 00:00:00 2001 From: thecentinol Date: Mon, 27 Jul 2026 15:40:48 +0200 Subject: [PATCH 13/14] fix(ui): typo in keybindings modal renderContent() comments --- internal/ui/keybindings_modal/model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ui/keybindings_modal/model.go b/internal/ui/keybindings_modal/model.go index f18727e..53c1cbb 100644 --- a/internal/ui/keybindings_modal/model.go +++ b/internal/ui/keybindings_modal/model.go @@ -150,7 +150,7 @@ func (m *Model) renderContent() { ) // WriteString returns a nil error, the `revive` linter will complain about not handling the - // the error's for the below WriteString's. So just like the liberals, we tell em to shut up. + // error's for the below WriteString's. So just like the liberals, we tell em to shut up. builder.WriteString(section) //nolint:revive builder.WriteString("\n") //nolint:revive } From 55abffc8497ecf5cb1b78adc675a2bb26a27f06c Mon Sep 17 00:00:00 2001 From: thecentinol Date: Mon, 27 Jul 2026 15:42:42 +0200 Subject: [PATCH 14/14] refactor(ui): improve keybindings HelpView description for Cancel --- internal/ui/keybindings_modal/model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ui/keybindings_modal/model.go b/internal/ui/keybindings_modal/model.go index 53c1cbb..29ae8e5 100644 --- a/internal/ui/keybindings_modal/model.go +++ b/internal/ui/keybindings_modal/model.go @@ -91,7 +91,7 @@ func (m Model) HelpView() []key.Binding { cancel := m.keys.Cancel.ToBubbles() scrollUp := m.keys.Up.ToBubbles() scrollDown := m.keys.Down.ToBubbles() - cancel.SetHelp(m.keys.Cancel.HelpKey, "Close/Cancel") + cancel.SetHelp(m.keys.Cancel.HelpKey, "Close") scrollUp.SetHelp(m.keys.Up.HelpKey, "Scroll up") scrollDown.SetHelp(m.keys.Down.HelpKey, "Scroll down")