From f7287b3ef6a8a9f6781c7156662460d24c04a6d8 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sun, 4 Oct 2026 10:14:28 +0100 Subject: [PATCH] refactor(ui): migrate to bubbletea v2 and lipgloss v2 Switch imports to charm.land/bubbletea/v2 and charm.land/lipgloss/v2. View now returns tea.View with AltScreen set, key handling uses KeyPressMsg, and colour fields use image/color. --- go.mod | 15 +- go.sum | 37 ++-- internal/ui/bubbletea_ui.go | 37 ++-- internal/ui/generate.go | 50 +++--- internal/ui/generate_test.go | 22 +-- internal/ui/handlers_test.go | 68 ++++---- internal/ui/wizard_commands.go | 2 +- internal/ui/wizard_handlers.go | 18 +- internal/ui/wizard_handlers_extended_test.go | 170 +++++++++---------- internal/ui/wizard_manual_namespace_test.go | 8 +- internal/ui/wizard_styles.go | 2 +- internal/ui/wizard_views_test.go | 18 +- 12 files changed, 223 insertions(+), 224 deletions(-) diff --git a/go.mod b/go.mod index 652b411..0d26d44 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/lukaszraczylo/kportal go 1.26.0 require ( - github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/lipgloss v1.1.0 + charm.land/bubbletea/v2 v2.0.10 + charm.land/lipgloss/v2 v2.0.6 github.com/fsnotify/fsnotify v1.10.1 github.com/go-logr/logr v1.4.4 github.com/grandcat/zeroconf v1.0.0 @@ -18,17 +18,17 @@ require ( ) require ( - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect github.com/charmbracelet/x/ansi v0.11.8 // indirect - github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fxamacker/cbor/v2 v2.9.4 // indirect github.com/go-openapi/jsonpointer v1.0.2 // indirect github.com/go-openapi/jsonreference v1.0.3 // indirect @@ -49,16 +49,12 @@ require ( github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/lucasb-eyer/go-colorful v1.4.1 // indirect - github.com/mattn/go-isatty v0.0.24 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.30 // indirect github.com/miekg/dns v1.1.73 // indirect github.com/moby/spdystream v0.5.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.10 // indirect @@ -68,6 +64,7 @@ require ( go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/net v0.59.0 // indirect golang.org/x/oauth2 v0.37.0 // indirect + golang.org/x/sync v0.23.0 // indirect golang.org/x/sys v0.48.0 // indirect golang.org/x/term v0.46.0 // indirect golang.org/x/text v0.42.0 // indirect diff --git a/go.sum b/go.sum index da17567..780f931 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,27 @@ +charm.land/bubbletea/v2 v2.0.10 h1:oolvo20VBpI0PfqE7iFjkZ1bx0WpmXGfnKz5Yldjq5o= +charm.land/bubbletea/v2 v2.0.10/go.mod h1:QOatcnhOjYIfxzUSTz6raF7Ex4R/rIuHa3SnBdCCpMc= +charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ= +charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 h1:rdnVWKgJpTVXKuKuJyxDJ+NFJdUaUqGvyGy61OcvlbA= +github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro= github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ= github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0= -github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= -github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= -github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= @@ -30,8 +32,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.9.4 h1:xwjVlxEMR3S605oUlgBjKLTTeGFciYPGYCtF/35LKGo= @@ -93,10 +93,6 @@ github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBql github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lukaszraczylo/oss-telemetry v0.2.5 h1:W6Qgwp7IY4yBRO361MmnEAmM59motdQpbAFAx8v6fJM= github.com/lukaszraczylo/oss-telemetry v0.2.5/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ= -github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= -github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk= github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= @@ -110,12 +106,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -160,7 +152,6 @@ golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= diff --git a/internal/ui/bubbletea_ui.go b/internal/ui/bubbletea_ui.go index 8759870..ce67671 100644 --- a/internal/ui/bubbletea_ui.go +++ b/internal/ui/bubbletea_ui.go @@ -21,13 +21,14 @@ package ui import ( "fmt" + "image/color" "log" "strings" "sync" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/lipgloss/table" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + "charm.land/lipgloss/v2/table" "github.com/lukaszraczylo/kportal/internal/config" "github.com/lukaszraczylo/kportal/internal/k8s" ) @@ -173,7 +174,7 @@ func (ui *BubbleTeaUI) SetUpdateAvailable(version, url string) { // Start starts the bubbletea application func (ui *BubbleTeaUI) Start() error { m := model{ui: ui} - ui.program = tea.NewProgram(m, tea.WithAltScreen()) + ui.program = tea.NewProgram(m) _, err := ui.program.Run() return err } @@ -325,7 +326,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.termHeight = msg.Height return m, nil - case tea.KeyMsg: + case tea.KeyPressMsg: // Route based on current view mode switch viewMode { case ViewModeMain: @@ -390,7 +391,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m model) View() string { +func (m model) View() tea.View { + v := tea.NewView(m.view()) + v.AltScreen = true + return v +} + +func (m model) view() string { m.ui.mu.RLock() viewMode := m.ui.viewMode deleteConfirming := m.ui.deleteConfirming @@ -438,13 +445,13 @@ func (m model) View() string { // mainViewColors holds the color palette for the main view type mainViewColors struct { - header lipgloss.Color - active lipgloss.Color - warning lipgloss.Color - errorColor lipgloss.Color - muted lipgloss.Color - selectedBg lipgloss.Color - selectedFg lipgloss.Color + header color.Color + active color.Color + warning color.Color + errorColor color.Color + muted color.Color + selectedBg color.Color + selectedFg color.Color } // defaultMainViewColors returns the default color palette @@ -526,7 +533,7 @@ func (m model) getTermDimensions() (width, height int) { } // renderTitle renders the title bar with version and optional update notification -func (m model) renderTitle(headerColor lipgloss.Color) string { +func (m model) renderTitle(headerColor color.Color) string { var b strings.Builder titleStyle := lipgloss.NewStyle(). @@ -552,7 +559,7 @@ func (m model) renderTitle(headerColor lipgloss.Color) string { // renderEmptyMessage renders the message shown when no forwards are configured. // It includes an actionable hint so a first-time user knows how to proceed. -func (m model) renderEmptyMessage(mutedColor lipgloss.Color) string { +func (m model) renderEmptyMessage(mutedColor color.Color) string { mutedStyle := lipgloss.NewStyle().Foreground(mutedColor) hintStyle := lipgloss.NewStyle().Foreground(highlightColor) return mutedStyle.Render("No forwards configured") + "\n\n" + diff --git a/internal/ui/generate.go b/internal/ui/generate.go index c58787d..339ef82 100644 --- a/internal/ui/generate.go +++ b/internal/ui/generate.go @@ -9,7 +9,7 @@ import ( "sync" "time" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/lukaszraczylo/kportal/internal/config" ) @@ -323,13 +323,13 @@ func (m *GenerateModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.step = GenerateStepDone return m, tea.Quit - case tea.KeyMsg: + case tea.KeyPressMsg: return m.handleKey(msg) } return m, nil } -func (m *GenerateModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *GenerateModel) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { if m.loading { // Allow only ctrl+c / esc while loading switch msg.String() { @@ -354,12 +354,12 @@ func (m *GenerateModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { // ---------- Namespace step ---------- -func (m *GenerateModel) handleNamespaceKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *GenerateModel) handleNamespaceKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { if m.nsFiltering { - switch msg.Type { - case tea.KeyEnter, tea.KeyEsc: + switch msg.Code { + case tea.KeyEnter, tea.KeyEscape: m.nsFiltering = false - if msg.Type == tea.KeyEsc { + if msg.Code == tea.KeyEscape { m.nsFilter = "" m.recomputeNamespaceFilter() } @@ -370,12 +370,11 @@ func (m *GenerateModel) handleNamespaceKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) m.recomputeNamespaceFilter() } return m, nil - case tea.KeyRunes, tea.KeySpace: - m.nsFilter += string(msg.Runes) + default: + m.nsFilter += msg.Text m.recomputeNamespaceFilter() return m, nil } - return m, nil } switch msg.String() { @@ -391,7 +390,7 @@ func (m *GenerateModel) handleNamespaceKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) m.moveCursor(&m.nsCursor, &m.nsScroll, len(m.nsFilteredView), -10) case "pgdown": m.moveCursor(&m.nsCursor, &m.nsScroll, len(m.nsFilteredView), 10) - case " ": + case "space": if len(m.nsFilteredView) > 0 { ns := m.nsFilteredView[m.nsCursor] m.nsSelected[ns] = !m.nsSelected[ns] @@ -475,12 +474,12 @@ func (m *GenerateModel) buildServiceOrder() { } } -func (m *GenerateModel) handleServiceKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *GenerateModel) handleServiceKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { if m.svcFiltering { - switch msg.Type { - case tea.KeyEnter, tea.KeyEsc: + switch msg.Code { + case tea.KeyEnter, tea.KeyEscape: m.svcFiltering = false - if msg.Type == tea.KeyEsc { + if msg.Code == tea.KeyEscape { m.svcFilter = "" m.recomputeServiceFilter() } @@ -491,12 +490,11 @@ func (m *GenerateModel) handleServiceKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.recomputeServiceFilter() } return m, nil - case tea.KeyRunes, tea.KeySpace: - m.svcFilter += string(msg.Runes) + default: + m.svcFilter += msg.Text m.recomputeServiceFilter() return m, nil } - return m, nil } switch msg.String() { @@ -515,7 +513,7 @@ func (m *GenerateModel) handleServiceKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.moveCursor(&m.svcCursor, &m.svcScroll, len(m.svcFilteredView), -10) case "pgdown": m.moveCursor(&m.svcCursor, &m.svcScroll, len(m.svcFilteredView), 10) - case " ": + case "space": if len(m.svcFilteredView) > 0 { c := m.svcFilteredView[m.svcCursor] if !m.svcLocked[c.Key()] && c.Protocol == "TCP" { @@ -593,7 +591,7 @@ func (m *GenerateModel) selectedCandidates() []ServiceCandidate { // ---------- Port assignment step ---------- -func (m *GenerateModel) handlePortKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *GenerateModel) handlePortKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c": m.step = GenerateStepCancelled @@ -627,7 +625,7 @@ func (m *GenerateModel) handlePortKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // Digit-only input - for _, r := range msg.Runes { + for _, r := range msg.Text { if r >= '0' && r <= '9' && len(m.startingPortStr) < 5 { m.startingPortStr += string(r) m.portError = "" @@ -716,7 +714,13 @@ func (m *GenerateModel) countSkippedNonTCP() int { // ---------- View ---------- // View implements tea.Model. -func (m *GenerateModel) View() string { +func (m *GenerateModel) View() tea.View { + v := tea.NewView(m.view()) + v.AltScreen = true + return v +} + +func (m *GenerateModel) view() string { var b strings.Builder b.WriteString(wizardHeaderStyle.Render(fmt.Sprintf("kportal generate · context: %s", m.contextName))) b.WriteString("\n") @@ -975,7 +979,7 @@ func RunGenerate( existingForwards []config.Forward, ) (GenerateResult, error) { m := NewGenerateModel(discovery, mutator, contextName, configPath, dryRun, existingForwards) - prog := tea.NewProgram(m, tea.WithAltScreen()) + prog := tea.NewProgram(m) finalModel, err := prog.Run() if err != nil { return GenerateResult{}, err diff --git a/internal/ui/generate_test.go b/internal/ui/generate_test.go index 92b3d56..ddc5c6c 100644 --- a/internal/ui/generate_test.go +++ b/internal/ui/generate_test.go @@ -8,7 +8,7 @@ import ( "sync" "testing" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/lukaszraczylo/kportal/internal/config" "github.com/lukaszraczylo/kportal/internal/k8s" ) @@ -72,22 +72,22 @@ func (f *fakeDiscovery) ListServices(_ context.Context, _, ns string) ([]k8s.Ser return f.servicesByNS[ns], nil } -// keyOf builds a tea.KeyMsg the same way bubbletea does for typed runes. -func keyOf(s string) tea.KeyMsg { +// keyOf builds a tea.KeyPressMsg the same way bubbletea does for typed runes. +func keyOf(s string) tea.KeyPressMsg { switch s { case "enter": - return tea.KeyMsg{Type: tea.KeyEnter} + return tea.KeyPressMsg{Code: tea.KeyEnter} case "esc": - return tea.KeyMsg{Type: tea.KeyEsc} + return tea.KeyPressMsg{Code: tea.KeyEscape} case "space": - return tea.KeyMsg{Type: tea.KeySpace, Runes: []rune(" ")} + return tea.KeyPressMsg{Code: tea.KeySpace, Text: " "} case "backspace": - return tea.KeyMsg{Type: tea.KeyBackspace} + return tea.KeyPressMsg{Code: tea.KeyBackspace} } if len(s) == 1 { - return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(s)} + return tea.KeyPressMsg{Text: s} } - return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(s)} + return tea.KeyPressMsg{Text: s} } // drainModel applies a sequence of messages and returns the final model. @@ -169,7 +169,7 @@ func TestGenerateModel_NamespaceFilter(t *testing.T) { t.Fatalf("expected [beta], got %v", gm.nsFilteredView) } // Exit filter - gm = drainModel(t, gm, tea.KeyMsg{Type: tea.KeyEnter}).(*GenerateModel) + gm = drainModel(t, gm, tea.KeyPressMsg{Code: tea.KeyEnter}).(*GenerateModel) if gm.nsFiltering { t.Fatal("expected filtering to be off after enter") } @@ -496,7 +496,7 @@ func TestGenerateModel_PortStepView(t *testing.T) { m.step = GenerateStepPortAssign m.svcOrder = []ServiceCandidate{{Namespace: "ns", Service: "svc", Port: 80, Protocol: "TCP"}} m.svcSelected[m.svcOrder[0].Key()] = true - view := m.View() + view := m.View().Content if !contains(view, "Step 3 / 3") { t.Fatalf("expected step header in view, got: %s", view) } diff --git a/internal/ui/handlers_test.go b/internal/ui/handlers_test.go index 60d1732..fb71dca 100644 --- a/internal/ui/handlers_test.go +++ b/internal/ui/handlers_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/lukaszraczylo/kportal/internal/config" "github.com/lukaszraczylo/kportal/internal/k8s" "github.com/stretchr/testify/assert" @@ -44,10 +44,10 @@ func TestHandleMainViewKeys_Quit(t *testing.T) { for _, tt := range tests { t.Run(tt.key, func(t *testing.T) { m := newTestModel() - _, cmd := m.handleMainViewKeys(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(tt.key)}) + _, cmd := m.handleMainViewKeys(tea.KeyPressMsg{Text: tt.key}) if tt.key == "ctrl+c" { - keyMsg := tea.KeyMsg{Type: tea.KeyCtrlC} + keyMsg := tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl} _, cmd = m.handleMainViewKeys(keyMsg) } @@ -76,14 +76,14 @@ func TestHandleMainViewKeys_Navigation(t *testing.T) { tests := []struct { name string key string - keyType tea.KeyType + code rune initialIndex int expectedIndex int }{ {"down arrow", "down", tea.KeyDown, 0, 1}, - {"j key", "j", tea.KeyRunes, 0, 1}, + {"j key", "j", 0, 0, 1}, {"up arrow", "up", tea.KeyUp, 2, 1}, - {"k key", "k", tea.KeyRunes, 2, 1}, + {"k key", "k", 0, 2, 1}, } for _, tt := range tests { @@ -92,11 +92,11 @@ func TestHandleMainViewKeys_Navigation(t *testing.T) { m.ui.selectedIndex = tt.initialIndex m.ui.mu.Unlock() - var keyMsg tea.KeyMsg - if tt.keyType == tea.KeyRunes { - keyMsg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(tt.key)} + var keyMsg tea.KeyPressMsg + if tt.code == 0 { + keyMsg = tea.KeyPressMsg{Text: tt.key} } else { - keyMsg = tea.KeyMsg{Type: tt.keyType} + keyMsg = tea.KeyPressMsg{Code: tt.code} } m.handleMainViewKeys(keyMsg) @@ -123,7 +123,7 @@ func TestHandleMainViewKeys_Toggle(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Toggle with space - keyMsg := tea.KeyMsg{Type: tea.KeySpace} + keyMsg := tea.KeyPressMsg{Code: tea.KeySpace, Text: " "} m.handleMainViewKeys(keyMsg) // Check disabled state changed @@ -151,7 +151,7 @@ func TestHandleMainViewKeys_NewWizard(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Without dependencies, 'n' should do nothing - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("n")} + keyMsg := tea.KeyPressMsg{Text: "n"} m.handleMainViewKeys(keyMsg) m.ui.mu.RLock() @@ -180,7 +180,7 @@ func TestHandleMainViewKeys_DeleteConfirmation(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Press 'd' to show delete confirmation - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("d")} + keyMsg := tea.KeyPressMsg{Text: "d"} m.handleMainViewKeys(keyMsg) m.ui.mu.RLock() @@ -204,7 +204,7 @@ func TestHandleMainViewKeys_DeleteConfirmation_PreventsDuplicate(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Press 'd' for first forward - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("d")} + keyMsg := tea.KeyPressMsg{Text: "d"} m.handleMainViewKeys(keyMsg) // Change selection @@ -234,7 +234,7 @@ func TestHandleDeleteConfirmation_Cancel(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Press Esc - keyMsg := tea.KeyMsg{Type: tea.KeyEsc} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEscape} m.handleDeleteConfirmation(keyMsg) m.ui.mu.RLock() @@ -257,7 +257,7 @@ func TestHandleDeleteConfirmation_NavigateAndConfirm(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Navigate left to "Yes" - keyMsg := tea.KeyMsg{Type: tea.KeyLeft} + keyMsg := tea.KeyPressMsg{Code: tea.KeyLeft} m.handleDeleteConfirmation(keyMsg) m.ui.mu.RLock() @@ -265,7 +265,7 @@ func TestHandleDeleteConfirmation_NavigateAndConfirm(t *testing.T) { m.ui.mu.RUnlock() // Navigate right back to "No" - keyMsg = tea.KeyMsg{Type: tea.KeyRight} + keyMsg = tea.KeyPressMsg{Code: tea.KeyRight} m.handleDeleteConfirmation(keyMsg) m.ui.mu.RLock() @@ -288,7 +288,7 @@ func TestHandleDeleteConfirmation_ConfirmYes(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Press Enter on "Yes" - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleDeleteConfirmation(keyMsg) // Should return a command to remove the forward @@ -314,7 +314,7 @@ func TestHandleDeleteConfirmation_QuickYKey(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Press 'y' - should confirm regardless of cursor position - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("y")} + keyMsg := tea.KeyPressMsg{Text: "y"} _, cmd := m.handleDeleteConfirmation(keyMsg) assert.NotNil(t, cmd) @@ -336,7 +336,7 @@ func TestHandleDeleteConfirmation_QuickNKey(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Press 'n' - should cancel regardless of cursor position - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("n")} + keyMsg := tea.KeyPressMsg{Text: "n"} m.handleDeleteConfirmation(keyMsg) m.ui.mu.RLock() @@ -358,7 +358,7 @@ func TestHandleBenchmarkKeys_Cancel(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Press Esc - keyMsg := tea.KeyMsg{Type: tea.KeyEsc} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEscape} m.handleBenchmarkKeys(keyMsg) assert.True(t, cancelled, "Cancel function should be called") @@ -385,7 +385,7 @@ func TestHandleBenchmarkKeys_Navigation(t *testing.T) { m.ui.mu.RUnlock() // Move down - keyMsg := tea.KeyMsg{Type: tea.KeyDown} + keyMsg := tea.KeyPressMsg{Code: tea.KeyDown} m.handleBenchmarkKeys(keyMsg) m.ui.mu.RLock() @@ -400,7 +400,7 @@ func TestHandleBenchmarkKeys_Navigation(t *testing.T) { m.ui.mu.RUnlock() // Move up - keyMsg = tea.KeyMsg{Type: tea.KeyUp} + keyMsg = tea.KeyPressMsg{Code: tea.KeyUp} m.handleBenchmarkKeys(keyMsg) m.ui.mu.RLock() @@ -422,7 +422,7 @@ func TestHandleHTTPLogKeys_Close(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Press Esc - keyMsg := tea.KeyMsg{Type: tea.KeyEsc} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEscape} m.handleHTTPLogKeys(keyMsg) m.ui.mu.RLock() @@ -451,7 +451,7 @@ func TestHandleHTTPLogKeys_FilterCycle(t *testing.T) { m.ui.mu.RUnlock() // Press 'f' to cycle - should skip Text mode and go to Non200 - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("f")} + keyMsg := tea.KeyPressMsg{Text: "f"} m.handleHTTPLogKeys(keyMsg) m.ui.mu.RLock() @@ -484,7 +484,7 @@ func TestHandleHTTPLogKeys_TextFilter(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Press '/' - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")} + keyMsg := tea.KeyPressMsg{Text: "/"} m.handleHTTPLogKeys(keyMsg) m.ui.mu.RLock() @@ -505,7 +505,7 @@ func TestHandleHTTPLogKeys_ClearFilters(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Press 'c' - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("c")} + keyMsg := tea.KeyPressMsg{Text: "c"} m.handleHTTPLogKeys(keyMsg) m.ui.mu.RLock() @@ -869,7 +869,7 @@ func TestModel_Update_ViewModeRouting(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Send a key message - should not panic - keyMsg := tea.KeyMsg{Type: tea.KeyEsc} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEscape} _, _ = m.Update(keyMsg) }) } @@ -925,7 +925,7 @@ func TestHandleRemoveWizardKeys_EscInConfirmingCancels(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEsc} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEscape} _, cmd := m.handleRemoveWizardKeys(keyMsg) // No removal command must be dispatched. @@ -962,7 +962,7 @@ func TestHandleRemoveWizardKeys_EscNotConfirmingExits(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEsc} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEscape} _, cmd := m.handleRemoveWizardKeys(keyMsg) // Should return tea.ClearScreen command on full exit. @@ -992,7 +992,7 @@ func TestHandleRemoveWizardKeys_EnterOnYesStillConfirms(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleRemoveWizardKeys(keyMsg) assert.NotNil(t, cmd, "Enter on Yes must still dispatch removeForwardsCmd") @@ -1017,7 +1017,7 @@ func TestHandleAddWizardKeys_HToggleHTTPLog(t *testing.T) { require.False(t, m.ui.addWizard.httpLog, "httpLog should default to false") - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("h")} + keyMsg := tea.KeyPressMsg{Text: "h"} m.handleAddWizardKeys(keyMsg) assert.True(t, m.ui.addWizard.httpLog, "first 'h' should enable httpLog") @@ -1042,7 +1042,7 @@ func TestHandleAddWizardKeys_HOnAliasFocusIsTextInput(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("h")} + keyMsg := tea.KeyPressMsg{Text: "h"} m.handleAddWizardKeys(keyMsg) assert.False(t, m.ui.addWizard.httpLog, "httpLog must NOT toggle when alias has focus") @@ -1067,7 +1067,7 @@ func TestEditPrefill_PreservesHTTPLog(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("e")} + keyMsg := tea.KeyPressMsg{Text: "e"} m.handleMainViewKeys(keyMsg) require.NotNil(t, m.ui.addWizard, "wizard should be active after 'e'") diff --git a/internal/ui/wizard_commands.go b/internal/ui/wizard_commands.go index 2ac66a1..4a082b7 100644 --- a/internal/ui/wizard_commands.go +++ b/internal/ui/wizard_commands.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/lukaszraczylo/kportal/internal/benchmark" "github.com/lukaszraczylo/kportal/internal/config" "github.com/lukaszraczylo/kportal/internal/k8s" diff --git a/internal/ui/wizard_handlers.go b/internal/ui/wizard_handlers.go index 0ac99c0..d6f4420 100644 --- a/internal/ui/wizard_handlers.go +++ b/internal/ui/wizard_handlers.go @@ -9,7 +9,7 @@ import ( "strings" "time" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/lukaszraczylo/kportal/internal/config" "github.com/lukaszraczylo/kportal/internal/k8s" ) @@ -31,7 +31,7 @@ func isFilterableStep(step AddWizardStep) bool { } // handleMainViewKeys handles keyboard input in the main view -func (m model) handleMainViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m model) handleMainViewKeys(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { // If delete confirmation is showing, handle it separately if m.ui.deleteConfirming { return m.handleDeleteConfirmation(msg) @@ -53,7 +53,7 @@ func (m model) handleMainViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) { case "pgdown", "ctrl+d": m.ui.moveSelection(10) - case " ", "enter": + case "space", "enter": m.ui.toggleSelected() case "n": // Enter add wizard @@ -294,7 +294,7 @@ func (m model) handleMainViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handleDeleteConfirmation handles keyboard input for delete confirmation dialog -func (m model) handleDeleteConfirmation(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m model) handleDeleteConfirmation(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { m.ui.mu.Lock() switch msg.String() { @@ -336,7 +336,7 @@ func (m model) handleDeleteConfirmation(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handleAddWizardKeys handles keyboard input in the add wizard -func (m model) handleAddWizardKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m model) handleAddWizardKeys(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { m.ui.mu.Lock() defer m.ui.mu.Unlock() @@ -781,7 +781,7 @@ func (m model) handleAddWizardEnter() (tea.Model, tea.Cmd) { } // handleRemoveWizardKeys handles keyboard input in the remove wizard -func (m model) handleRemoveWizardKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m model) handleRemoveWizardKeys(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { m.ui.mu.Lock() defer m.ui.mu.Unlock() @@ -822,7 +822,7 @@ func (m model) handleRemoveWizardKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) { case "pgdown", "ctrl+d": wizard.moveCursor(10) - case " ": + case "space": if !wizard.confirming { wizard.toggleSelection() } @@ -1051,7 +1051,7 @@ func (m model) handleForwardsRemoved(msg ForwardsRemovedMsg) (tea.Model, tea.Cmd } // handleBenchmarkKeys handles keyboard input in the benchmark view -func (m model) handleBenchmarkKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m model) handleBenchmarkKeys(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { m.ui.mu.Lock() defer m.ui.mu.Unlock() @@ -1192,7 +1192,7 @@ func (m model) applyBenchmarkTextInput() { } // handleHTTPLogKeys handles keyboard input in the HTTP log view -func (m model) handleHTTPLogKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m model) handleHTTPLogKeys(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { m.ui.mu.Lock() defer m.ui.mu.Unlock() diff --git a/internal/ui/wizard_handlers_extended_test.go b/internal/ui/wizard_handlers_extended_test.go index 9036926..c5bddc0 100644 --- a/internal/ui/wizard_handlers_extended_test.go +++ b/internal/ui/wizard_handlers_extended_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/lukaszraczylo/kportal/internal/config" "github.com/lukaszraczylo/kportal/internal/k8s" "github.com/stretchr/testify/assert" @@ -44,7 +44,7 @@ func TestHandleMainViewKeys_PageUpDown(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyPgUp} + keyMsg := tea.KeyPressMsg{Code: tea.KeyPgUp} m.handleMainViewKeys(keyMsg) ui.mu.RLock() assert.Equal(t, 0, ui.selectedIndex) @@ -54,7 +54,7 @@ func TestHandleMainViewKeys_PageUpDown(t *testing.T) { ui.mu.Lock() ui.selectedIndex = 0 ui.mu.Unlock() - keyMsg = tea.KeyMsg{Type: tea.KeyPgDown} + keyMsg = tea.KeyPressMsg{Code: tea.KeyPgDown} m.handleMainViewKeys(keyMsg) ui.mu.RLock() assert.Equal(t, 10, ui.selectedIndex) @@ -76,7 +76,7 @@ func TestHandleMainViewKeys_NewWizard_WithDiscovery(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("n")} + keyMsg := tea.KeyPressMsg{Text: "n"} _, cmd := m.handleMainViewKeys(keyMsg) ui.mu.RLock() @@ -99,7 +99,7 @@ func TestHandleMainViewKeys_NewWizard_AlreadyActive(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("n")} + keyMsg := tea.KeyPressMsg{Text: "n"} m.handleMainViewKeys(keyMsg) // Should still be the same wizard (not replaced). @@ -116,7 +116,7 @@ func TestHandleMainViewKeys_HttpLog_NoSubscriber(t *testing.T) { ui.AddForward("id-1", fwd) m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("l")} + keyMsg := tea.KeyPressMsg{Text: "l"} m.handleMainViewKeys(keyMsg) ui.mu.RLock() @@ -133,7 +133,7 @@ func TestHandleMainViewKeys_HttpLog_WithSubscriber(t *testing.T) { ui.AddForward("id-1", fwd) m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("l")} + keyMsg := tea.KeyPressMsg{Text: "l"} m.handleMainViewKeys(keyMsg) // Subscription should be established. @@ -151,7 +151,7 @@ func TestHandleMainViewKeys_Benchmark(t *testing.T) { ui.AddForward("id-1", fwd) m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("b")} + keyMsg := tea.KeyPressMsg{Text: "b"} m.handleMainViewKeys(keyMsg) ui.mu.RLock() @@ -170,7 +170,7 @@ func TestHandleMainViewKeys_Benchmark_BlocksWhenActive(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} prevState := ui.benchmarkState - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("b")} + keyMsg := tea.KeyPressMsg{Text: "b"} m.handleMainViewKeys(keyMsg) // Should not have replaced the benchmark state. @@ -184,7 +184,7 @@ func TestHandleMainViewKeys_Benchmark_BlocksWhenActive(t *testing.T) { func TestHandleAddWizardKeys_CtrlC(t *testing.T) { m := newModelWithWizard(StepSelectContext) - keyMsg := tea.KeyMsg{Type: tea.KeyCtrlC} + keyMsg := tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl} _, cmd := m.handleAddWizardKeys(keyMsg) assert.NotNil(t, cmd) // tea.ClearScreen @@ -197,7 +197,7 @@ func TestHandleAddWizardKeys_CtrlC(t *testing.T) { func TestHandleAddWizardKeys_Esc_FirstStep_Cancels(t *testing.T) { m := newModelWithWizard(StepSelectContext) - keyMsg := tea.KeyMsg{Type: tea.KeyEsc} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEscape} m.handleAddWizardKeys(keyMsg) m.ui.mu.RLock() @@ -209,7 +209,7 @@ func TestHandleAddWizardKeys_Esc_FirstStep_Cancels(t *testing.T) { func TestHandleAddWizardKeys_Esc_MiddleStep_GoesBack(t *testing.T) { m := newModelWithWizard(StepSelectNamespace) - keyMsg := tea.KeyMsg{Type: tea.KeyEsc} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEscape} m.handleAddWizardKeys(keyMsg) m.ui.mu.RLock() @@ -221,7 +221,7 @@ func TestHandleAddWizardKeys_Esc_ClearsSearchFilter_InsteadOfBack(t *testing.T) m := newModelWithWizard(StepSelectContext) m.ui.addWizard.searchFilter = "my" - keyMsg := tea.KeyMsg{Type: tea.KeyEsc} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEscape} m.handleAddWizardKeys(keyMsg) m.ui.mu.RLock() @@ -235,7 +235,7 @@ func TestHandleAddWizardKeys_Esc_EditMode_AlwaysCancels(t *testing.T) { m := newModelWithWizard(StepEnterRemotePort) m.ui.addWizard.isEditing = true - keyMsg := tea.KeyMsg{Type: tea.KeyEsc} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEscape} m.handleAddWizardKeys(keyMsg) m.ui.mu.RLock() @@ -250,11 +250,11 @@ func TestHandleAddWizardKeys_Navigation(t *testing.T) { m := newModelWithWizard(StepSelectContext) m.ui.addWizard.contexts = []string{"ctx-a", "ctx-b", "ctx-c"} - keyMsg := tea.KeyMsg{Type: tea.KeyDown} + keyMsg := tea.KeyPressMsg{Code: tea.KeyDown} m.handleAddWizardKeys(keyMsg) assert.Equal(t, 1, m.ui.addWizard.cursor) - keyMsg = tea.KeyMsg{Type: tea.KeyUp} + keyMsg = tea.KeyPressMsg{Code: tea.KeyUp} m.handleAddWizardKeys(keyMsg) assert.Equal(t, 0, m.ui.addWizard.cursor) } @@ -268,11 +268,11 @@ func TestHandleAddWizardKeys_PageNavigation(t *testing.T) { m.ui.addWizard.contexts = contexts m.ui.addWizard.cursor = 15 - keyMsg := tea.KeyMsg{Type: tea.KeyPgUp} + keyMsg := tea.KeyPressMsg{Code: tea.KeyPgUp} m.handleAddWizardKeys(keyMsg) assert.Equal(t, 5, m.ui.addWizard.cursor) - keyMsg = tea.KeyMsg{Type: tea.KeyPgDown} + keyMsg = tea.KeyPressMsg{Code: tea.KeyPgDown} m.handleAddWizardKeys(keyMsg) assert.Equal(t, 15, m.ui.addWizard.cursor) } @@ -283,7 +283,7 @@ func TestHandleAddWizardKeys_ConfirmationStep_UpFocusAlias(t *testing.T) { m := newModelWithWizard(StepConfirmation) m.ui.addWizard.confirmationFocus = FocusButtons - keyMsg := tea.KeyMsg{Type: tea.KeyUp} + keyMsg := tea.KeyPressMsg{Code: tea.KeyUp} m.handleAddWizardKeys(keyMsg) assert.Equal(t, FocusAlias, m.ui.addWizard.confirmationFocus) } @@ -292,7 +292,7 @@ func TestHandleAddWizardKeys_ConfirmationStep_DownFocusButtons(t *testing.T) { m := newModelWithWizard(StepConfirmation) m.ui.addWizard.confirmationFocus = FocusAlias - keyMsg := tea.KeyMsg{Type: tea.KeyDown} + keyMsg := tea.KeyPressMsg{Code: tea.KeyDown} m.handleAddWizardKeys(keyMsg) assert.Equal(t, FocusButtons, m.ui.addWizard.confirmationFocus) } @@ -301,7 +301,7 @@ func TestHandleAddWizardKeys_Tab_TogglesFocus(t *testing.T) { m := newModelWithWizard(StepConfirmation) m.ui.addWizard.confirmationFocus = FocusAlias - keyMsg := tea.KeyMsg{Type: tea.KeyTab} + keyMsg := tea.KeyPressMsg{Code: tea.KeyTab} m.handleAddWizardKeys(keyMsg) assert.Equal(t, FocusButtons, m.ui.addWizard.confirmationFocus) @@ -316,7 +316,7 @@ func TestHandleAddWizardKeys_Backspace_TextMode(t *testing.T) { m.ui.addWizard.inputMode = InputModeText m.ui.addWizard.textInput = "808" - keyMsg := tea.KeyMsg{Type: tea.KeyBackspace} + keyMsg := tea.KeyPressMsg{Code: tea.KeyBackspace} m.handleAddWizardKeys(keyMsg) assert.Equal(t, "80", m.ui.addWizard.textInput) } @@ -325,7 +325,7 @@ func TestHandleAddWizardKeys_Backspace_SearchFilter(t *testing.T) { m := newModelWithWizard(StepSelectContext) m.ui.addWizard.searchFilter = "my" - keyMsg := tea.KeyMsg{Type: tea.KeyBackspace} + keyMsg := tea.KeyPressMsg{Code: tea.KeyBackspace} m.handleAddWizardKeys(keyMsg) assert.Equal(t, "m", m.ui.addWizard.searchFilter) } @@ -334,7 +334,7 @@ func TestHandleAddWizardKeys_Backspace_EmptyFilter_NoOp(t *testing.T) { m := newModelWithWizard(StepSelectContext) m.ui.addWizard.searchFilter = "" - keyMsg := tea.KeyMsg{Type: tea.KeyBackspace} + keyMsg := tea.KeyPressMsg{Code: tea.KeyBackspace} // Must not panic. m.handleAddWizardKeys(keyMsg) } @@ -345,7 +345,7 @@ func TestHandleAddWizardKeys_TypeCharAddsToSearchFilter(t *testing.T) { m := newModelWithWizard(StepSelectContext) // Context step is filterable in list mode. - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("p")} + keyMsg := tea.KeyPressMsg{Text: "p"} m.handleAddWizardKeys(keyMsg) assert.Equal(t, "p", m.ui.addWizard.searchFilter) } @@ -356,7 +356,7 @@ func TestHandleAddWizardKeys_TypeCharInTextMode(t *testing.T) { m := newModelWithWizard(StepEnterRemotePort) m.ui.addWizard.inputMode = InputModeText - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("8")} + keyMsg := tea.KeyPressMsg{Text: "8"} m.handleAddWizardKeys(keyMsg) assert.Equal(t, "8", m.ui.addWizard.textInput) } @@ -377,7 +377,7 @@ func TestHandleAddWizardEnter_SelectContext(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleAddWizardKeys(keyMsg) ui.mu.RLock() @@ -393,7 +393,7 @@ func TestHandleAddWizardEnter_SelectContext_Loading_NoOp(t *testing.T) { m := newModelWithWizard(StepSelectContext) m.ui.addWizard.loading = true - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleAddWizardKeys(keyMsg) assert.Nil(t, cmd) } @@ -405,7 +405,7 @@ func TestHandleAddWizardEnter_SelectNamespace(t *testing.T) { m.ui.addWizard.namespaces = []string{"default", "kube-system"} m.ui.addWizard.cursor = 1 // kube-system - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleAddWizardKeys(keyMsg) assert.Equal(t, StepSelectResourceType, m.ui.addWizard.step) @@ -428,7 +428,7 @@ func TestHandleAddWizardEnter_SelectResourceType_Service(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleAddWizardKeys(keyMsg) ui.mu.RLock() @@ -454,7 +454,7 @@ func TestHandleAddWizardEnter_SelectResourceType_PodPrefix(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleAddWizardKeys(keyMsg) ui.mu.RLock() @@ -477,7 +477,7 @@ func TestHandleAddWizardEnter_EnterResource_PodPrefix(t *testing.T) { Containers: []k8s.ContainerInfo{{Name: "main", Ports: []k8s.PortInfo{{Port: 8080}}}}, }} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleAddWizardKeys(keyMsg) assert.Equal(t, StepEnterRemotePort, m.ui.addWizard.step) @@ -490,7 +490,7 @@ func TestHandleAddWizardEnter_EnterResource_PodPrefix_EmptyInput_NoOp(t *testing m.ui.addWizard.inputMode = InputModeText m.ui.addWizard.textInput = "" - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleAddWizardKeys(keyMsg) assert.Equal(t, StepEnterResource, m.ui.addWizard.step) // should not advance @@ -505,7 +505,7 @@ func TestHandleAddWizardEnter_EnterResource_PodSelector(t *testing.T) { m.ui.addWizard.textInput = "app=my" m.ui.addWizard.matchingPods = []k8s.PodInfo{{Name: "my-pod"}} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleAddWizardKeys(keyMsg) assert.Equal(t, StepEnterRemotePort, m.ui.addWizard.step) @@ -518,7 +518,7 @@ func TestHandleAddWizardEnter_EnterResource_PodSelector_NoMatchingPods_NoOp(t *t m.ui.addWizard.textInput = "app=my" m.ui.addWizard.matchingPods = nil - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleAddWizardKeys(keyMsg) assert.Equal(t, StepEnterResource, m.ui.addWizard.step) @@ -535,7 +535,7 @@ func TestHandleAddWizardEnter_EnterResource_Service(t *testing.T) { } m.ui.addWizard.cursor = 0 - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleAddWizardKeys(keyMsg) assert.Equal(t, StepEnterRemotePort, m.ui.addWizard.step) @@ -549,7 +549,7 @@ func TestHandleAddWizardEnter_RemotePort_TextMode_ValidPort(t *testing.T) { m.ui.addWizard.inputMode = InputModeText m.ui.addWizard.textInput = "8080" - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleAddWizardKeys(keyMsg) assert.Equal(t, StepEnterLocalPort, m.ui.addWizard.step) @@ -561,7 +561,7 @@ func TestHandleAddWizardEnter_RemotePort_TextMode_InvalidPort(t *testing.T) { m.ui.addWizard.inputMode = InputModeText m.ui.addWizard.textInput = "invalid" - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleAddWizardKeys(keyMsg) assert.Equal(t, StepEnterRemotePort, m.ui.addWizard.step) @@ -577,7 +577,7 @@ func TestHandleAddWizardEnter_RemotePort_ListMode_SelectPort(t *testing.T) { } m.ui.addWizard.cursor = 1 // 8080 → TargetPort 9090 - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleAddWizardKeys(keyMsg) assert.Equal(t, StepEnterLocalPort, m.ui.addWizard.step) @@ -590,7 +590,7 @@ func TestHandleAddWizardEnter_RemotePort_ListMode_ManualEntry(t *testing.T) { m.ui.addWizard.detectedPorts = []k8s.PortInfo{{Port: 80}} m.ui.addWizard.cursor = 1 // "Manual entry" option - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleAddWizardKeys(keyMsg) assert.Equal(t, InputModeText, m.ui.addWizard.inputMode) @@ -612,7 +612,7 @@ func TestHandleAddWizardEnter_LocalPort_ValidPort(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleAddWizardKeys(keyMsg) // loading set, cmd generated. @@ -627,7 +627,7 @@ func TestHandleAddWizardEnter_LocalPort_InvalidPort(t *testing.T) { m := newModelWithWizard(StepEnterLocalPort) m.ui.addWizard.textInput = "0" // invalid (must be > 0) - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleAddWizardKeys(keyMsg) assert.NotNil(t, m.ui.addWizard.error) @@ -639,7 +639,7 @@ func TestHandleAddWizardEnter_Confirmation_FocusAlias_MoveToButtons(t *testing.T m := newModelWithWizard(StepConfirmation) m.ui.addWizard.confirmationFocus = FocusAlias - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleAddWizardKeys(keyMsg) assert.Equal(t, FocusButtons, m.ui.addWizard.confirmationFocus) @@ -650,7 +650,7 @@ func TestHandleAddWizardEnter_Confirmation_CancelButton(t *testing.T) { m.ui.addWizard.confirmationFocus = FocusButtons m.ui.addWizard.cursor = 1 // Cancel - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleAddWizardKeys(keyMsg) assert.NotNil(t, cmd) // tea.ClearScreen @@ -667,7 +667,7 @@ func TestHandleAddWizardEnter_Confirmation_PortNotAvailable_Error(t *testing.T) m.ui.addWizard.portAvailable = false m.ui.addWizard.localPort = 8080 - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleAddWizardKeys(keyMsg) assert.NotNil(t, m.ui.addWizard.error) @@ -697,7 +697,7 @@ func TestHandleAddWizardEnter_Confirmation_Save_NewForward(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleAddWizardKeys(keyMsg) require.NotNil(t, cmd) @@ -731,7 +731,7 @@ func TestHandleAddWizardEnter_Confirmation_Save_NewForward_WithHTTPLog(t *testin ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleAddWizardKeys(keyMsg) require.NotNil(t, cmd) msg := cmd() @@ -767,7 +767,7 @@ func TestHandleAddWizardEnter_Confirmation_Update_EditMode(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleAddWizardKeys(keyMsg) require.NotNil(t, cmd) msg := cmd() @@ -790,7 +790,7 @@ func TestHandleAddWizardEnter_Success_AddAnother(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleAddWizardKeys(keyMsg) // A cmd (loadContextsCmd) must be returned. @@ -801,7 +801,7 @@ func TestHandleAddWizardEnter_Success_ReturnToMain(t *testing.T) { m := newModelWithWizard(StepSuccess) m.ui.addWizard.cursor = 1 // "Return to main" - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleAddWizardKeys(keyMsg) assert.NotNil(t, cmd) @@ -828,7 +828,7 @@ func TestHandleAddWizardKeys_SelectorValidation_OnChar(t *testing.T) { m := model{ui: ui, termWidth: 120, termHeight: 40} // Typing a char in PodSelector step should return a validation cmd. - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("-")} + keyMsg := tea.KeyPressMsg{Text: "-"} _, cmd := m.handleAddWizardKeys(keyMsg) // A cmd is returned (validateSelectorCmd); we don't invoke it as it needs a real cluster. @@ -849,7 +849,7 @@ func TestHandleRemoveWizardKeys_SpaceToggle(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeySpace} + keyMsg := tea.KeyPressMsg{Code: tea.KeySpace, Text: " "} m.handleRemoveWizardKeys(keyMsg) ui.mu.RLock() @@ -874,7 +874,7 @@ func TestHandleRemoveWizardKeys_SelectAll(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("a")} + keyMsg := tea.KeyPressMsg{Text: "a"} m.handleRemoveWizardKeys(keyMsg) ui.mu.RLock() @@ -895,7 +895,7 @@ func TestHandleRemoveWizardKeys_SelectNone(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("n")} + keyMsg := tea.KeyPressMsg{Text: "n"} m.handleRemoveWizardKeys(keyMsg) ui.mu.RLock() @@ -915,7 +915,7 @@ func TestHandleRemoveWizardKeys_CtrlC_ExitsAlways(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyCtrlC} + keyMsg := tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl} m.handleRemoveWizardKeys(keyMsg) ui.mu.RLock() @@ -935,7 +935,7 @@ func TestHandleRemoveWizardKeys_Enter_NothingSelected_NoOp(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleRemoveWizardKeys(keyMsg) assert.Nil(t, cmd) @@ -955,7 +955,7 @@ func TestHandleRemoveWizardKeys_Enter_ShowConfirmation(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleRemoveWizardKeys(keyMsg) ui.mu.RLock() @@ -976,7 +976,7 @@ func TestHandleRemoveWizardKeys_Enter_ConfirmNo(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleRemoveWizardKeys(keyMsg) assert.Nil(t, cmd) @@ -997,13 +997,13 @@ func TestHandleRemoveWizardKeys_Navigation_InSelection(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - keyMsg := tea.KeyMsg{Type: tea.KeyDown} + keyMsg := tea.KeyPressMsg{Code: tea.KeyDown} m.handleRemoveWizardKeys(keyMsg) ui.mu.RLock() assert.Equal(t, 1, ui.removeWizard.cursor) ui.mu.RUnlock() - keyMsg = tea.KeyMsg{Type: tea.KeyUp} + keyMsg = tea.KeyPressMsg{Code: tea.KeyUp} m.handleRemoveWizardKeys(keyMsg) ui.mu.RLock() assert.Equal(t, 0, ui.removeWizard.cursor) @@ -1017,7 +1017,7 @@ func TestHandleBenchmarkKeys_Tab(t *testing.T) { m.ui.benchmarkState.step = BenchmarkStepConfig m.ui.benchmarkState.cursor = 0 - keyMsg := tea.KeyMsg{Type: tea.KeyTab} + keyMsg := tea.KeyPressMsg{Code: tea.KeyTab} m.handleBenchmarkKeys(keyMsg) assert.Equal(t, 1, m.ui.benchmarkState.cursor) @@ -1028,7 +1028,7 @@ func TestHandleBenchmarkKeys_Tab_Wraps(t *testing.T) { m.ui.benchmarkState.step = BenchmarkStepConfig m.ui.benchmarkState.cursor = 3 - keyMsg := tea.KeyMsg{Type: tea.KeyTab} + keyMsg := tea.KeyPressMsg{Code: tea.KeyTab} m.handleBenchmarkKeys(keyMsg) assert.Equal(t, 0, m.ui.benchmarkState.cursor) @@ -1040,7 +1040,7 @@ func TestHandleBenchmarkKeys_Backspace(t *testing.T) { m.ui.benchmarkState.cursor = 0 m.ui.benchmarkState.textInput = "/api" - keyMsg := tea.KeyMsg{Type: tea.KeyBackspace} + keyMsg := tea.KeyPressMsg{Code: tea.KeyBackspace} m.handleBenchmarkKeys(keyMsg) assert.Equal(t, "/ap", m.ui.benchmarkState.textInput) @@ -1053,7 +1053,7 @@ func TestHandleBenchmarkKeys_TypeChar_UpdatesField(t *testing.T) { m.ui.benchmarkState.cursor = 1 // Method field m.ui.benchmarkState.textInput = "GE" - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("T")} + keyMsg := tea.KeyPressMsg{Text: "T"} m.handleBenchmarkKeys(keyMsg) assert.Equal(t, "GET", m.ui.benchmarkState.method) @@ -1063,7 +1063,7 @@ func TestHandleBenchmarkKeys_Enter_ResultsStep_ReturnsToMain(t *testing.T) { m := newModelWithBenchmark() m.ui.benchmarkState.step = BenchmarkStepResults - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleBenchmarkKeys(keyMsg) assert.NotNil(t, cmd) // tea.ClearScreen @@ -1077,7 +1077,7 @@ func TestHandleBenchmarkKeys_Enter_ConfigStep_StartsRunning(t *testing.T) { m := newModelWithBenchmark() m.ui.benchmarkState.step = BenchmarkStepConfig - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := m.handleBenchmarkKeys(keyMsg) require.NotNil(t, cmd) @@ -1157,7 +1157,7 @@ func TestHandleHTTPLogKeys_Detail_Esc(t *testing.T) { m.ui.httpLogState.showingDetail = true m.ui.httpLogState.entries = []HTTPLogEntry{{Method: "GET", Path: "/test"}} - keyMsg := tea.KeyMsg{Type: tea.KeyEsc} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEscape} m.handleHTTPLogKeys(keyMsg) assert.False(t, m.ui.httpLogState.showingDetail) @@ -1169,11 +1169,11 @@ func TestHandleHTTPLogKeys_Detail_UpDown(t *testing.T) { m.ui.httpLogState.detailScroll = 5 m.ui.httpLogState.entries = []HTTPLogEntry{{Method: "GET"}} - keyMsg := tea.KeyMsg{Type: tea.KeyUp} + keyMsg := tea.KeyPressMsg{Code: tea.KeyUp} m.handleHTTPLogKeys(keyMsg) assert.Equal(t, 4, m.ui.httpLogState.detailScroll) - keyMsg = tea.KeyMsg{Type: tea.KeyDown} + keyMsg = tea.KeyPressMsg{Code: tea.KeyDown} m.handleHTTPLogKeys(keyMsg) assert.Equal(t, 5, m.ui.httpLogState.detailScroll) } @@ -1184,11 +1184,11 @@ func TestHandleHTTPLogKeys_Detail_PgUpDown(t *testing.T) { m.ui.httpLogState.detailScroll = 25 m.ui.httpLogState.entries = []HTTPLogEntry{{Method: "GET"}} - keyMsg := tea.KeyMsg{Type: tea.KeyPgUp} + keyMsg := tea.KeyPressMsg{Code: tea.KeyPgUp} m.handleHTTPLogKeys(keyMsg) assert.Equal(t, 5, m.ui.httpLogState.detailScroll) - keyMsg = tea.KeyMsg{Type: tea.KeyPgDown} + keyMsg = tea.KeyPressMsg{Code: tea.KeyPgDown} m.handleHTTPLogKeys(keyMsg) assert.Equal(t, 25, m.ui.httpLogState.detailScroll) } @@ -1199,7 +1199,7 @@ func TestHandleHTTPLogKeys_Detail_GoToTop(t *testing.T) { m.ui.httpLogState.detailScroll = 99 m.ui.httpLogState.entries = []HTTPLogEntry{{Method: "GET"}} - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("g")} + keyMsg := tea.KeyPressMsg{Text: "g"} m.handleHTTPLogKeys(keyMsg) assert.Equal(t, 0, m.ui.httpLogState.detailScroll) @@ -1213,7 +1213,7 @@ func TestHandleHTTPLogKeys_Enter_ShowDetail(t *testing.T) { m.ui.httpLogState.entries = []HTTPLogEntry{{Method: "GET", Path: "/test", StatusCode: 200}} m.ui.httpLogState.cursor = 0 - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleHTTPLogKeys(keyMsg) assert.True(t, m.ui.httpLogState.showingDetail) @@ -1227,11 +1227,11 @@ func TestHandleHTTPLogKeys_Navigate_UpDown(t *testing.T) { } m.ui.httpLogState.cursor = 1 - keyMsg := tea.KeyMsg{Type: tea.KeyUp} + keyMsg := tea.KeyPressMsg{Code: tea.KeyUp} m.handleHTTPLogKeys(keyMsg) assert.Equal(t, 0, m.ui.httpLogState.cursor) - keyMsg = tea.KeyMsg{Type: tea.KeyDown} + keyMsg = tea.KeyPressMsg{Code: tea.KeyDown} m.handleHTTPLogKeys(keyMsg) assert.Equal(t, 1, m.ui.httpLogState.cursor) } @@ -1243,7 +1243,7 @@ func TestHandleHTTPLogKeys_Down_AtBottom_EnablesAutoScroll(t *testing.T) { m.ui.httpLogState.cursor = 0 m.ui.httpLogState.autoScroll = false - keyMsg := tea.KeyMsg{Type: tea.KeyDown} + keyMsg := tea.KeyPressMsg{Code: tea.KeyDown} m.handleHTTPLogKeys(keyMsg) assert.True(t, m.ui.httpLogState.autoScroll) @@ -1257,7 +1257,7 @@ func TestHandleHTTPLogKeys_GoToTop_G(t *testing.T) { } m.ui.httpLogState.cursor = 2 - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("g")} + keyMsg := tea.KeyPressMsg{Text: "g"} m.handleHTTPLogKeys(keyMsg) assert.Equal(t, 0, m.ui.httpLogState.cursor) @@ -1272,7 +1272,7 @@ func TestHandleHTTPLogKeys_GoToBottom_CapitalG(t *testing.T) { } m.ui.httpLogState.cursor = 0 - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("G")} + keyMsg := tea.KeyPressMsg{Text: "G"} m.handleHTTPLogKeys(keyMsg) assert.Equal(t, 2, m.ui.httpLogState.cursor) @@ -1283,7 +1283,7 @@ func TestHandleHTTPLogKeys_ToggleAutoScroll(t *testing.T) { m := newModelWithHTTPLog() m.ui.httpLogState.autoScroll = false - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("a")} + keyMsg := tea.KeyPressMsg{Text: "a"} m.handleHTTPLogKeys(keyMsg) assert.True(t, m.ui.httpLogState.autoScroll) @@ -1301,12 +1301,12 @@ func TestHandleHTTPLogKeys_PgUpDown_List(t *testing.T) { m.ui.httpLogState.entries = entries m.ui.httpLogState.cursor = 25 - keyMsg := tea.KeyMsg{Type: tea.KeyPgUp} + keyMsg := tea.KeyPressMsg{Code: tea.KeyPgUp} m.handleHTTPLogKeys(keyMsg) assert.Equal(t, 5, m.ui.httpLogState.cursor) assert.False(t, m.ui.httpLogState.autoScroll) - keyMsg = tea.KeyMsg{Type: tea.KeyPgDown} + keyMsg = tea.KeyPressMsg{Code: tea.KeyPgDown} m.handleHTTPLogKeys(keyMsg) // PgDown +20 from 5 = 25; index 25 is in range (30 items, indices 0-29). assert.Equal(t, 25, m.ui.httpLogState.cursor) @@ -1319,7 +1319,7 @@ func TestHandleHTTPLogKeys_FilterActive_Typing(t *testing.T) { m.ui.httpLogState.filterActive = true m.ui.httpLogState.filterText = "te" - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("s")} + keyMsg := tea.KeyPressMsg{Text: "s"} m.handleHTTPLogKeys(keyMsg) assert.Equal(t, "tes", m.ui.httpLogState.filterText) @@ -1330,7 +1330,7 @@ func TestHandleHTTPLogKeys_FilterActive_Backspace(t *testing.T) { m.ui.httpLogState.filterActive = true m.ui.httpLogState.filterText = "tes" - keyMsg := tea.KeyMsg{Type: tea.KeyBackspace} + keyMsg := tea.KeyPressMsg{Code: tea.KeyBackspace} m.handleHTTPLogKeys(keyMsg) assert.Equal(t, "te", m.ui.httpLogState.filterText) @@ -1341,7 +1341,7 @@ func TestHandleHTTPLogKeys_FilterActive_EscClearsFilter(t *testing.T) { m.ui.httpLogState.filterActive = true m.ui.httpLogState.filterText = "test" - keyMsg := tea.KeyMsg{Type: tea.KeyEsc} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEscape} m.handleHTTPLogKeys(keyMsg) assert.False(t, m.ui.httpLogState.filterActive) @@ -1353,7 +1353,7 @@ func TestHandleHTTPLogKeys_FilterActive_EnterConfirms(t *testing.T) { m.ui.httpLogState.filterActive = true m.ui.httpLogState.filterText = "api" - keyMsg := tea.KeyMsg{Type: tea.KeyEnter} + keyMsg := tea.KeyPressMsg{Code: tea.KeyEnter} m.handleHTTPLogKeys(keyMsg) assert.False(t, m.ui.httpLogState.filterActive) diff --git a/internal/ui/wizard_manual_namespace_test.go b/internal/ui/wizard_manual_namespace_test.go index 1c6b9b8..c6e5fe3 100644 --- a/internal/ui/wizard_manual_namespace_test.go +++ b/internal/ui/wizard_manual_namespace_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -12,7 +12,7 @@ import ( func typeKeys(t *testing.T, m model, text string) { t.Helper() for _, r := range text { - m.handleAddWizardKeys(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}}) + m.handleAddWizardKeys(tea.KeyPressMsg{Text: string(r)}) } } @@ -87,7 +87,7 @@ func TestManualNamespace_BackFromLaterStepRestoresTextInput(t *testing.T) { m := newModelWithWizard(StepSelectResourceType) m.ui.addWizard.namespaceManual = true - m.handleAddWizardKeys(tea.KeyMsg{Type: tea.KeyEsc}) + m.handleAddWizardKeys(tea.KeyPressMsg{Code: tea.KeyEscape}) w := m.ui.addWizard assert.Equal(t, StepSelectNamespace, w.step) @@ -98,7 +98,7 @@ func TestKeysJK_StillNavigateInListMode(t *testing.T) { m := newModelWithWizard(StepSelectResourceType) m.ui.addWizard.inputMode = InputModeList - m.handleAddWizardKeys(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'j'}}) + m.handleAddWizardKeys(tea.KeyPressMsg{Text: string('j')}) assert.Equal(t, 1, m.ui.addWizard.cursor) } diff --git a/internal/ui/wizard_styles.go b/internal/ui/wizard_styles.go index cb07b4d..a247ce4 100644 --- a/internal/ui/wizard_styles.go +++ b/internal/ui/wizard_styles.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" ) // Color palette for wizards diff --git a/internal/ui/wizard_views_test.go b/internal/ui/wizard_views_test.go index 92f1b7a..835a495 100644 --- a/internal/ui/wizard_views_test.go +++ b/internal/ui/wizard_views_test.go @@ -987,7 +987,7 @@ func TestRenderDeleteConfirmation_NoSelected(t *testing.T) { func TestModelView_MainViewDefault(t *testing.T) { m := newTestModel() - result := m.View() + result := m.View().Content assert.NotEmpty(t, result) } @@ -1001,7 +1001,7 @@ func TestModelView_DeleteConfirmationOverlay(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - result := m.View() + result := m.View().Content assert.Contains(t, result, "Delete Port Forward") } @@ -1013,7 +1013,7 @@ func TestModelView_AddWizardOverlay(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - result := m.View() + result := m.View().Content assert.NotEmpty(t, result) } @@ -1028,7 +1028,7 @@ func TestModelView_RemoveWizardOverlay(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - result := m.View() + result := m.View().Content assert.Contains(t, result, "alpha") } @@ -1040,7 +1040,7 @@ func TestModelView_BenchmarkOverlay(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - result := m.View() + result := m.View().Content assert.NotEmpty(t, result) } @@ -1052,7 +1052,7 @@ func TestModelView_HTTPLogFullScreen(t *testing.T) { ui.mu.Unlock() m := model{ui: ui, termWidth: 120, termHeight: 40} - result := m.View() + result := m.View().Content assert.Contains(t, result, "HTTP Traffic Log") } @@ -1061,7 +1061,7 @@ func TestModelView_ZeroTermSize(t *testing.T) { m.termWidth = 0 m.termHeight = 0 // Must not panic even with zero term dims. - result := m.View() + result := m.View().Content assert.NotEmpty(t, result) } @@ -1406,8 +1406,8 @@ func TestGetStatusIconAndText(t *testing.T) { func TestDefaultMainViewColors(t *testing.T) { colors := defaultMainViewColors() - assert.NotEmpty(t, string(colors.header)) - assert.NotEmpty(t, string(colors.active)) + assert.NotNil(t, colors.header) + assert.NotNil(t, colors.active) } func TestMainViewKeyBindings(t *testing.T) {