refactor(tui): move to Bubble Tea, Lip Gloss and Bubbles v2

Switch imports to the charm.land v2 modules. Return tea.View from Model.View,
set alt screen and window title there, use KeyPressMsg and the "space" key
name, and call SetWidth on text inputs.
This commit is contained in:
2026-10-04 10:12:33 +01:00
parent 750e43501c
commit 22f8f7a008
10 changed files with 101 additions and 83 deletions
+35 -29
View File
@@ -7,9 +7,9 @@ import (
"strings"
"time"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/lukaszraczylo/lolcathost/internal/client"
"github.com/lukaszraczylo/lolcathost/internal/config"
@@ -143,7 +143,7 @@ func NewModel(socketPath string) *Model {
searchInput := textinput.New()
searchInput.Placeholder = "Search..."
searchInput.CharLimit = 100
searchInput.Width = 50
searchInput.SetWidth(50)
return &Model{
client: client.New(socketPath),
@@ -161,7 +161,6 @@ func NewModel(socketPath string) *Model {
func (m *Model) Init() tea.Cmd {
return tea.Batch(
m.connect(),
tea.SetWindowTitle("lolcathost"),
m.tick(),
m.checkForUpdate(),
)
@@ -330,9 +329,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if searchWidth > 60 {
searchWidth = 60
}
m.searchInput.Width = searchWidth
m.searchInput.SetWidth(searchWidth)
case tea.KeyMsg:
case tea.KeyPressMsg:
cmd := m.handleKey(msg)
if cmd != nil {
cmds = append(cmds, cmd)
@@ -505,7 +504,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
func (m *Model) handleKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handleKey(msg tea.KeyPressMsg) tea.Cmd {
// Global keys
switch msg.String() {
case "ctrl+c":
@@ -535,7 +534,7 @@ func (m *Model) handleKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *Model) handleListKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handleListKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "q":
return tea.Quit
@@ -549,7 +548,7 @@ func (m *Model) handleListKey(msg tea.KeyMsg) tea.Cmd {
m.list.MoveUp()
case "down", "j":
m.list.MoveDown()
case " ", "enter":
case "space", "enter":
return m.toggleSelected()
case "n":
m.mode = ViewForm
@@ -587,7 +586,7 @@ func (m *Model) handleListKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *Model) handleFormKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handleFormKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "esc":
m.mode = ViewList
@@ -615,7 +614,7 @@ func (m *Model) handleFormKey(msg tea.KeyMsg) tea.Cmd {
return m.form.Update(msg)
}
func (m *Model) handlePresetKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handlePresetKey(msg tea.KeyPressMsg) tea.Cmd {
// Handle based on preset picker mode
switch m.presetPicker.Mode() {
case PresetModeSelect:
@@ -630,7 +629,7 @@ func (m *Model) handlePresetKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *Model) handlePresetSelectKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handlePresetSelectKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "esc", "q":
m.mode = ViewList
@@ -652,7 +651,7 @@ func (m *Model) handlePresetSelectKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *Model) handlePresetFormKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handlePresetFormKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "esc":
m.presetPicker.CancelForm()
@@ -690,7 +689,7 @@ func (m *Model) handlePresetFormKey(msg tea.KeyMsg) tea.Cmd {
return m.presetPicker.Update(msg)
}
func (m *Model) handlePresetPickerKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handlePresetPickerKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "esc":
m.presetPicker.ClosePicker()
@@ -700,13 +699,13 @@ func (m *Model) handlePresetPickerKey(msg tea.KeyMsg) tea.Cmd {
m.presetPicker.PickerMoveUp()
case "down", "j":
m.presetPicker.PickerMoveDown()
case " ":
case "space":
m.presetPicker.TogglePickerSelection()
}
return nil
}
func (m *Model) handlePresetDeleteKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handlePresetDeleteKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "y", "Y":
if preset := m.presetPicker.Selected(); preset != "" {
@@ -719,7 +718,7 @@ func (m *Model) handlePresetDeleteKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *Model) handleGroupKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handleGroupKey(msg tea.KeyPressMsg) tea.Cmd {
// Handle based on group picker mode
switch m.groupPicker.Mode() {
case GroupModeSelect:
@@ -732,7 +731,7 @@ func (m *Model) handleGroupKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *Model) handleGroupSelectKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handleGroupSelectKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "esc", "q":
m.mode = ViewList
@@ -750,7 +749,7 @@ func (m *Model) handleGroupSelectKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *Model) handleGroupFormKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handleGroupFormKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "esc":
m.groupPicker.CancelForm()
@@ -770,7 +769,7 @@ func (m *Model) handleGroupFormKey(msg tea.KeyMsg) tea.Cmd {
return m.groupPicker.Update(msg)
}
func (m *Model) handleGroupDeleteKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handleGroupDeleteKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "y", "Y":
if group := m.groupPicker.Selected(); group != "" {
@@ -783,7 +782,7 @@ func (m *Model) handleGroupDeleteKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *Model) handleBackupKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handleBackupKey(msg tea.KeyPressMsg) tea.Cmd {
switch m.backupPicker.Mode() {
case BackupModeSelect:
return m.handleBackupSelectKey(msg)
@@ -793,7 +792,7 @@ func (m *Model) handleBackupKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *Model) handleBackupSelectKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handleBackupSelectKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "esc", "q":
m.mode = ViewList
@@ -821,7 +820,7 @@ func (m *Model) handleBackupSelectKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *Model) handleBackupRestoreKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handleBackupRestoreKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "y", "Y":
if backup := m.backupPicker.Selected(); backup != "" {
@@ -834,7 +833,7 @@ func (m *Model) handleBackupRestoreKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *Model) handleHelpKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handleHelpKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "esc", "q", "?":
m.mode = ViewList
@@ -842,7 +841,7 @@ func (m *Model) handleHelpKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *Model) handleSearchKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handleSearchKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "esc":
m.mode = ViewList
@@ -860,7 +859,7 @@ func (m *Model) handleSearchKey(msg tea.KeyMsg) tea.Cmd {
return cmd
}
func (m *Model) handleConfirmDeleteKey(msg tea.KeyMsg) tea.Cmd {
func (m *Model) handleConfirmDeleteKey(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "y", "Y":
alias := m.pendingDeleteAlias
@@ -900,7 +899,14 @@ func (m *Model) setSuccess(msg string) {
}
// View renders the UI.
func (m *Model) View() string {
func (m *Model) View() tea.View {
v := tea.NewView(m.render())
v.AltScreen = true
v.WindowTitle = "lolcathost"
return v
}
func (m *Model) render() string {
var sb strings.Builder
// Title with version
@@ -1142,7 +1148,7 @@ func RunWithVersion(socketPath, version, githubOwner, githubRepo string) error {
m.version = version
m.githubOwner = githubOwner
m.githubRepo = githubRepo
p := tea.NewProgram(m, tea.WithAltScreen())
p := tea.NewProgram(m)
_, err := p.Run()
return err
+25
View File
@@ -0,0 +1,25 @@
package tui
import (
"testing"
tea "charm.land/bubbletea/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestModel_View_UsesAltScreenAndTitle(t *testing.T) {
m := NewModel("/nonexistent.sock")
v := m.View()
assert.True(t, v.AltScreen)
assert.Equal(t, "lolcathost", v.WindowTitle)
assert.Contains(t, v.Content, "lolcathost - Host Management")
}
func TestModel_SpaceKeyIsNamedSpace(t *testing.T) {
msg := tea.KeyPressMsg{Code: tea.KeySpace, Text: " "}
require.Equal(t, "space", msg.String())
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"charm.land/lipgloss/v2"
"github.com/lukaszraczylo/lolcathost/internal/protocol"
)
+4 -4
View File
@@ -5,8 +5,8 @@ import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"github.com/lukaszraczylo/lolcathost/internal/config"
)
@@ -127,14 +127,14 @@ func (f *Form) SetSize(width, height int) {
inputWidth := min(50, width-10)
for i := range f.fields {
f.fields[i].Width = inputWidth
f.fields[i].SetWidth(inputWidth)
}
}
// Update handles input events.
func (f *Form) Update(msg tea.Msg) tea.Cmd {
switch msg := msg.(type) {
case tea.KeyMsg:
case tea.KeyPressMsg:
// Handle group dropdown navigation
if f.focus == FieldGroup {
switch msg.String() {
+4 -4
View File
@@ -4,8 +4,8 @@ package tui
import (
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
)
// GroupMode represents the group view mode.
@@ -53,7 +53,7 @@ func (g *GroupPicker) SetGroups(groups []string) {
func (g *GroupPicker) SetSize(width, height int) {
g.width = width
g.height = height
g.input.Width = min(50, width-10)
g.input.SetWidth(min(50, width-10))
}
// MoveUp moves the cursor up.
@@ -126,7 +126,7 @@ func (g *GroupPicker) CancelForm() {
}
// Update handles input events for form mode.
func (g *GroupPicker) Update(msg tea.KeyMsg) tea.Cmd {
func (g *GroupPicker) Update(msg tea.KeyPressMsg) tea.Cmd {
var cmd tea.Cmd
g.input, cmd = g.input.Update(msg)
return cmd
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"charm.land/lipgloss/v2"
"charm.land/lipgloss/v2/table"
"github.com/lukaszraczylo/lolcathost/internal/protocol"
)
+4 -4
View File
@@ -4,8 +4,8 @@ package tui
import (
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"github.com/lukaszraczylo/lolcathost/internal/protocol"
)
@@ -108,7 +108,7 @@ func (p *PresetPicker) SetSize(width, height int) {
inputWidth := min(60, width-10)
for i := range p.fields {
p.fields[i].Width = inputWidth
p.fields[i].SetWidth(inputWidth)
}
}
@@ -223,7 +223,7 @@ func (p *PresetPicker) CancelForm() {
}
// Update handles input events for form mode.
func (p *PresetPicker) Update(msg tea.KeyMsg) tea.Cmd {
func (p *PresetPicker) Update(msg tea.KeyPressMsg) tea.Cmd {
switch msg.String() {
case "tab", "down":
p.nextField()
+1 -1
View File
@@ -4,7 +4,7 @@ package tui
import (
"strings"
"github.com/charmbracelet/lipgloss"
"charm.land/lipgloss/v2"
)
// Colors - matching kportal style, optimized for dark terminals