From 79c00278334aa296a06792855f0c33ae26d0a207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Untersch=C3=BCtz?= Date: Tue, 27 Oct 2026 22:29:54 +0100 Subject: [PATCH] fix(deps): migrate Charm to v2 and require Go 1.27.3 --- CHANGELOG.md | 3 + cmd/netssh/main.go | 4 +- go.mod | 23 +++---- go.sum | 39 +++++------- internal/setup/wizard.go | 2 +- internal/tui/model.go | 74 +++++++++++++++++----- internal/tui/model_test.go | 12 ++-- internal/tui/v2_test.go | 123 +++++++++++++++++++++++++++++++++++++ 8 files changed, 219 insertions(+), 61 deletions(-) create mode 100644 internal/tui/v2_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 5150a9e..a2e588d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## v0.0.18 +- Require Go 1.27.3 and migrate Bubbles, Bubble Tea, Huh, and Lip Gloss to their v2 modules under `charm.land`. +- Update TUI rendering and keyboard events for Bubble Tea v2 while preserving alternate-screen behavior. +- Route paste events to the focused editor, size input fields explicitly, and keep host lists within the terminal height. - Reject ambiguous cached shortcuts and skip duplicate hostnames during cache refresh. - Use paginated, unambiguous host lookups for SSH connections. - Accept shortcut domains with or without a leading dot during cold lookups. diff --git a/cmd/netssh/main.go b/cmd/netssh/main.go index 0356f87..5c0d4e4 100644 --- a/cmd/netssh/main.go +++ b/cmd/netssh/main.go @@ -13,7 +13,7 @@ import ( "text/tabwriter" "time" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/spf13/cobra" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache" @@ -209,7 +209,7 @@ func runTUI() { m := tui.New(nbClient, c, cfg.SSH.DefaultUser, cfg.SSH.DefaultPort) m.SetNotes(notes.New(cfg.NetBox.URL, nbClient)) - p := tea.NewProgram(m, tea.WithAltScreen()) + p := tea.NewProgram(m) final, err := p.Run() if err != nil { fatalf("TUI: %v", err) diff --git a/go.mod b/go.mod index 65398e6..f413bb7 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module git.zb-server.de/Sebi/ssh-netbox-wrapper -go 1.26.3 +go 1.27.3 require ( - github.com/charmbracelet/bubbles v1.0.0 - github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/huh v1.0.0 - github.com/charmbracelet/lipgloss v1.1.0 + charm.land/bubbles/v2 v2.2.1 + charm.land/bubbletea/v2 v2.1.0 + charm.land/huh/v2 v2.0.3 + charm.land/lipgloss/v2 v2.0.6 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 golang.org/x/sys v0.49.0 @@ -14,32 +14,28 @@ require ( require ( github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/aymanbagabas/go-udiff v0.4.1 // indirect github.com/catppuccin/go v0.3.0 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4 // indirect github.com/charmbracelet/x/ansi v0.11.9 // indirect - github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/exp/golden v0.1.0 // indirect + github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect github.com/charmbracelet/x/exp/strings v0.1.0 // 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/charmbracelet/x/xpty v0.1.4 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/dustin/go-humanize v1.1.0 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // 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.31 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // 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/pelletier/go-toml/v2 v2.4.3 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.16.0 // indirect @@ -52,5 +48,6 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/xo/terminfo v1.2.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/sync v0.24.0 // indirect golang.org/x/text v0.43.0 // indirect ) diff --git a/go.sum b/go.sum index 110e3a7..428f6db 100644 --- a/go.sum +++ b/go.sum @@ -1,37 +1,39 @@ +charm.land/bubbles/v2 v2.2.1 h1:Fq1+qm5hV6GkvzLQDhCBpXXE5tLgvh1PRriCLwSvIQU= +charm.land/bubbles/v2 v2.2.1/go.mod h1:wdMgn+sje1KNXdwFizIWjbf328fIUBxqEmJ/vYPo8yc= +charm.land/bubbletea/v2 v2.1.0 h1:A9fj9h5LFrvQhQHFNggp8K8rvY6CUfgn9dMbb6p2HIw= +charm.land/bubbletea/v2 v2.1.0/go.mod h1:xWHeyxIX9/wiFU2kgPW04ojOZyTzrWf+8lGjUCaftiU= +charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU= +charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= +charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ= +charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -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.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= -github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= -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/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= -github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= -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-20261008173134-6b8d4baf91b4 h1:hISLzUbwcvJbGoNR6A3L/TpguRRnACBmZOee8LcHLJw= +github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4/go.mod h1:aJDVk+WOVM1K04rdc/71PMPoUGfjxjhvXfP3Pjz1Zio= github.com/charmbracelet/x/ansi v0.11.9 h1:kIrqLBxtTVbLyyHO2Evoi93EjQo1mylWmpNEsfPxXAM= github.com/charmbracelet/x/ansi v0.11.9/go.mod h1:DG/hpOnaI2QJ/KMMqLL2h0KVhluuJLazf8973P+Vy5w= -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/conpty v0.2.0 h1:eKtA2hm34qNfgJCDp/M6Dc0gLy7e07YEK4qAdNGOvVY= github.com/charmbracelet/x/conpty v0.2.0/go.mod h1:fexgUnVrZgw8scD49f6VSi0Ggj9GWYIrpedRthAwW/8= github.com/charmbracelet/x/exp/golden v0.1.0 h1:qgUxGaHX4I8Fg077vD+54+qFmDWrgO19lTCQLblHWko= github.com/charmbracelet/x/exp/golden v0.1.0/go.mod h1:6fMpcW6iwN/kX+xJ52eqVWsDiBTe0UJD24JLoHFe+P0= +github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE= +github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8= github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA= github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= 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/charmbracelet/x/xpty v0.1.4 h1:4jaW7u+8AHQMxesiVc+zUMsspu7GyDwtJO+gy/tFtW4= github.com/charmbracelet/x/xpty v0.1.4/go.mod h1:7t8P7BpPiolHJ1pLzz7/4ujDbD+sUxI9yA3CBOLOIcU= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= @@ -43,8 +45,6 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg= github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI= -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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= @@ -63,20 +63,12 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss= github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -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.31 h1:1oCV4ReTlosDBc6rYRw5cL8S/4y8iv5oRl4SJ5IBgwo= github.com/mattn/go-runewidth v0.0.31/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= -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/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -108,7 +100,8 @@ github.com/xo/terminfo v1.2.0/go.mod h1:lGzkSo8Fe7IRh/w+Gqz7n5mDog4FVXCj3gy/DYTB go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= diff --git a/internal/setup/wizard.go b/internal/setup/wizard.go index 50983c9..826b7bb 100644 --- a/internal/setup/wizard.go +++ b/internal/setup/wizard.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" - "github.com/charmbracelet/huh" + "charm.land/huh/v2" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config" diff --git a/internal/tui/model.go b/internal/tui/model.go index d11e2b1..be9f943 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -8,15 +8,15 @@ import ( "strings" "time" - "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/notes" - "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/textarea" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/list" + "charm.land/bubbles/v2/textarea" + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/netbox" + "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/notes" ) // SelectedHost is returned when the user confirms a host in the TUI. @@ -78,7 +78,7 @@ func (d compactDelegate) Render(w io.Writer, m list.Model, index int, item list. if index == m.Index() { line = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("86")).Render("> " + strings.TrimPrefix(line, " ")) } - fmt.Fprintln(w, line) + fmt.Fprint(w, line) } // --- filter --- @@ -221,6 +221,7 @@ func (m *Model) loadRecent() tea.Cmd { } func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + defer m.resizeList() switch msg := msg.(type) { case noteResultMsg: m.noteBusy = false @@ -240,18 +241,13 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height - extraRows := 4 - if m.filterOpen { - extraRows++ - } - if m.editMode { - extraRows += 2 - } - m.list.SetSize(msg.Width, max(1, msg.Height-extraRows)) + m.input.SetWidth(max(1, msg.Width-lipgloss.Width(m.input.Prompt))) + m.filterInput.SetWidth(max(1, msg.Width-lipgloss.Width(m.filterInput.Prompt))) + m.editInput.SetWidth(max(1, msg.Width-lipgloss.Width("Connect as: "))) m.noteInput.SetWidth(max(10, msg.Width-4)) return m, nil - case tea.KeyMsg: + case tea.KeyPressMsg: if msg.String() == "ctrl+c" { return m, tea.Quit } @@ -427,10 +423,26 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if m.noteMode { + if m.noteBusy { + return m, nil + } var cmd tea.Cmd m.noteInput, cmd = m.noteInput.Update(msg) return m, cmd } + // Paste and cursor events are separate from key presses in Bubble Tea v2. + // Route them to the focused editor as well. + if m.editMode { + var cmd tea.Cmd + m.editInput, cmd = m.editInput.Update(msg) + return m, cmd + } + if m.filterOpen { + var cmd tea.Cmd + m.filterInput, cmd = m.filterInput.Update(msg) + m.updateListItems(m.lastResults) + return m, cmd + } // Forward to search input and restart debounce timer. var cmds []tea.Cmd var inputCmd tea.Cmd @@ -449,7 +461,13 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } -func (m *Model) View() string { +func (m *Model) View() tea.View { + view := tea.NewView(m.viewContent()) + view.AltScreen = true + return view +} + +func (m *Model) viewContent() string { if m.noteMode { target := "local" if m.noteRemote { @@ -562,6 +580,28 @@ func (m *Model) updateListItems(entries []netbox.HostEntry) { items[i] = hostItem{id: e.ID, name: e.Name, ip: ip, kind: e.Kind, tags: e.Tags} } m.list.SetItems(items) + m.resizeList() +} + +// resizeList keeps the footer visible when the recent heading or editors change. +func (m *Model) resizeList() { + if m.width <= 0 || m.height <= 0 { + return + } + extraRows := 5 // title, blank line, search, footer separator, footer + if m.recentMode && m.input.Value() == "" && len(m.list.Items()) > 0 { + extraRows++ + } + if m.filterOpen { + extraRows++ + } + if m.editMode { + extraRows++ + } + height := max(1, m.height-extraRows) + if m.list.Width() != m.width || m.list.Height() != height { + m.list.SetSize(m.width, height) + } } // openEdit switches to edit mode for the given list item. diff --git a/internal/tui/model_test.go b/internal/tui/model_test.go index 03c291f..10514f4 100644 --- a/internal/tui/model_test.go +++ b/internal/tui/model_test.go @@ -1,16 +1,18 @@ package tui import ( + "testing" + + tea "charm.land/bubbletea/v2" + "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/netbox" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/notes" - tea "github.com/charmbracelet/bubbletea" - "testing" ) func TestTypingEDoesNotOpenConnectionEditor(t *testing.T) { m := New(nil, nil, "") m.updateListItems([]netbox.HostEntry{{Name: "server"}}) - m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'e'}}) + m.Update(tea.KeyPressMsg{Code: 'e', Text: "e"}) if m.editMode || m.input.Value() != "e" { t.Fatal("e intercepted") } @@ -52,7 +54,7 @@ func TestFailedNoteLoadCannotBeSaved(t *testing.T) { m := New(nil, nil, "") m.SetNotes(¬es.Service{Dir: t.TempDir()}) m.noteMode = true - _, cmd := m.Update(tea.KeyMsg{Type: tea.KeyCtrlS}) + _, cmd := m.Update(tea.KeyPressMsg{Code: 's', Mod: tea.ModCtrl}) if cmd != nil { t.Fatal("save allowed before successful load") } @@ -63,7 +65,7 @@ func TestConnectionEditorKeepsOriginalHost(t *testing.T) { first := hostItem{name: "first", ip: "10.0.0.1"} m.openEdit(first) m.updateListItems([]netbox.HostEntry{{Name: "second", PrimaryIP4: "10.0.0.2"}}) - m.Update(tea.KeyMsg{Type: tea.KeyEnter}) + m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) if m.selected == nil || m.selected.Name != "first" { t.Fatalf("edited host changed: %+v", m.selected) } diff --git a/internal/tui/v2_test.go b/internal/tui/v2_test.go new file mode 100644 index 0000000..6078d67 --- /dev/null +++ b/internal/tui/v2_test.go @@ -0,0 +1,123 @@ +package tui + +import ( + "strings" + "testing" + + tea "charm.land/bubbletea/v2" + + "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/netbox" +) + +func TestV2PasteFollowsFocusedEditor(t *testing.T) { + t.Run("search", func(t *testing.T) { + m := New(nil, nil, "") + _, cmd := m.Update(tea.PasteMsg{Content: "web-ä"}) + if m.input.Value() != "web-ä" || m.seq != 1 || cmd == nil { + t.Fatalf("paste did not start search: %q, seq %d", m.input.Value(), m.seq) + } + }) + t.Run("connection editor", func(t *testing.T) { + m := New(nil, nil, "") + m.openEdit(hostItem{name: "web", ip: "10.0.0.1"}) + m.editInput.SetValue("") + m.Update(tea.PasteMsg{Content: "ops@web:2222"}) + m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) + if m.selected == nil || m.selected.User != "ops" || m.selected.Port != "2222" { + t.Fatalf("paste lost in editor: %+v", m.selected) + } + if m.input.Value() != "" { + t.Fatal("editor paste changed search") + } + }) + t.Run("filter", func(t *testing.T) { + m := New(nil, nil, "") + m.lastResults = []netbox.HostEntry{{Name: "vm", Kind: "vm"}, {Name: "device", Kind: "device"}} + m.updateListItems(m.lastResults) + m.Update(tea.KeyPressMsg{Code: 'f', Mod: tea.ModCtrl}) + m.Update(tea.PasteMsg{Content: "kind:vm"}) + if m.filterInput.Value() != "kind:vm" || len(m.list.Items()) != 1 { + t.Fatalf("paste did not filter: %q, %d results", m.filterInput.Value(), len(m.list.Items())) + } + if m.input.Value() != "" { + t.Fatal("filter paste changed search") + } + }) + t.Run("notes", func(t *testing.T) { + m := New(nil, nil, "") + m.noteMode = true + m.input.Blur() + m.noteInput.Focus() + m.Update(tea.PasteMsg{Content: "First line\nSecond line"}) + if m.noteInput.Value() != "First line\nSecond line" { + t.Fatalf("multiline paste lost: %q", m.noteInput.Value()) + } + m.noteBusy = true + m.Update(tea.PasteMsg{Content: "must not change busy note"}) + if m.noteInput.Value() != "First line\nSecond line" { + t.Fatal("paste edited note while loading/saving") + } + }) +} + +func TestV2ViewKeepsAlternateScreenInEachMode(t *testing.T) { + for _, mode := range []string{"search", "edit", "notes"} { + t.Run(mode, func(t *testing.T) { + m := New(nil, nil, "") + m.Update(tea.WindowSizeMsg{Width: 80, Height: 24}) + if mode == "edit" { + m.openEdit(hostItem{name: "web"}) + } + if mode == "notes" { + m.noteMode = true + } + view := m.View() + if !view.AltScreen || strings.TrimSpace(view.Content) == "" { + t.Fatalf("invalid %s view: %+v", mode, view) + } + }) + } +} + +func TestV2KeyReleaseDoesNotTypeTwice(t *testing.T) { + m := New(nil, nil, "") + m.Update(tea.KeyPressMsg{Code: 'e', Text: "e"}) + m.Update(tea.KeyReleaseMsg{Code: 'e', Text: "e"}) + if m.input.Value() != "e" { + t.Fatalf("key release changed input: %q", m.input.Value()) + } + m.Update(tea.KeyPressMsg{Code: ' ', Text: " "}) + if m.input.Value() != "e " { + t.Fatalf("space not inserted: %q", m.input.Value()) + } +} + +func TestV2ResizeShowsFullSearchPromptAndFooter(t *testing.T) { + m := New(nil, nil, "") + m.Update(tea.WindowSizeMsg{Width: 100, Height: 24}) + var entries []netbox.HostEntry + for range 30 { + entries = append(entries, netbox.HostEntry{Name: "web", Kind: "device"}) + } + m.updateListItems(entries) + view := m.View() + if m.input.Width() != 98 || m.filterInput.Width() != 92 || m.editInput.Width() != 88 { + t.Fatalf("editors were not sized for terminal: search=%d filter=%d edit=%d", m.input.Width(), m.filterInput.Width(), m.editInput.Width()) + } + if !strings.Contains(view.Content, "earch hostname") || !strings.Contains(view.Content, "ctrl+c quit") { + t.Fatalf("search prompt or footer missing: %s", view.Content) + } + assertFits := func() { + t.Helper() + if rows := strings.Count(m.View().Content, "\n"); rows > 24 { + t.Fatalf("view overflows terminal: %d rows", rows) + } + } + assertFits() + m.lastResults = entries + m.Update(tea.KeyPressMsg{Code: 'f', Mod: tea.ModCtrl}) + assertFits() + m.Update(tea.KeyPressMsg{Code: 'f', Mod: tea.ModCtrl}) + m.Update(tea.KeyPressMsg{Code: 'e', Mod: tea.ModCtrl}) + assertFits() +}