diff --git a/cmd/usbguard-tui/app.go b/cmd/usbguard-tui/app.go index a16a8fe..e532af6 100644 --- a/cmd/usbguard-tui/app.go +++ b/cmd/usbguard-tui/app.go @@ -4,23 +4,33 @@ import ( tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/anotherhadi/ilovetui/app" + "github.com/anotherhadi/ilovetui/minsize" "github.com/anotherhadi/ilovetui/modal" "github.com/anotherhadi/ilovetui/notification" "github.com/anotherhadi/usbguard-tui/ui" + zone "github.com/lrstanley/bubblezone/v2" +) + +const ( + minWidth = 40 + minHeight = 15 ) type appModel struct { core ui.Model modals modal.Model notif notification.Model + minsize minsize.Model width, height int } func newApp() appModel { + zone.NewGlobal() return appModel{ - core: ui.New(), - modals: modal.New(), - notif: notification.New(), + core: ui.New(), + modals: modal.New(), + notif: notification.New(), + minsize: minsize.New(minWidth, minHeight), } } @@ -48,9 +58,18 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (a appModel) View() tea.View { + if !a.minsize.Fits(a.width, a.height) { + return tea.View{ + Content: a.minsize.View(a.width, a.height), + AltScreen: true, + WindowTitle: "USBGuard TUI", + } + } + bg := lipgloss.Place(a.width, a.height, lipgloss.Left, lipgloss.Top, a.core.View()) bg = a.modals.Render(bg) bg = a.notif.Render(bg) + bg = zone.Scan(bg) return tea.View{ Content: bg, AltScreen: true, diff --git a/go.mod b/go.mod index b650be4..d1a082e 100644 --- a/go.mod +++ b/go.mod @@ -3,42 +3,43 @@ module github.com/anotherhadi/usbguard-tui go 1.26.2 require ( - charm.land/bubbles/v2 v2.1.0 - charm.land/bubbletea/v2 v2.0.6 - charm.land/lipgloss/v2 v2.0.3 - github.com/anotherhadi/ilovetui v0.2.2 - github.com/charmbracelet/x/ansi v0.11.7 + charm.land/bubbles/v2 v2.2.0 + charm.land/bubbletea/v2 v2.0.9 + charm.land/lipgloss/v2 v2.0.6 + github.com/anotherhadi/ilovetui v0.2.4 + github.com/charmbracelet/x/ansi v0.11.8 + github.com/lrstanley/bubblezone/v2 v2.0.0 ) require ( - charm.land/glamour/v2 v2.0.0 // indirect - github.com/alecthomas/chroma/v2 v2.14.0 // indirect + charm.land/glamour/v2 v2.0.1 // indirect + github.com/alecthomas/chroma/v2 v2.27.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 // indirect - github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260812204455-68fa937c71be // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20260823001701-96af6d2cb5f6 // 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/dlclark/regexp2 v1.11.0 // indirect + github.com/dlclark/regexp2/v2 v2.7.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect - github.com/lucasb-eyer/go-colorful v1.4.0 // indirect - github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/lucasb-eyer/go-colorful v1.4.1 // indirect + github.com/mattn/go-runewidth v0.0.28 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/sahilm/fuzzy v0.1.1 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark v1.7.8 // indirect - github.com/yuin/goldmark-emoji v1.0.5 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.24.0 // indirect + github.com/sahilm/fuzzy v0.1.3 // indirect + github.com/xo/terminfo v1.0.0 // indirect + github.com/yuin/goldmark v1.8.5 // indirect + github.com/yuin/goldmark-emoji v1.0.6 // indirect + golang.org/x/net v0.58.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b7c3183..0a65266 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,21 @@ -charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g= -charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY= -charm.land/bubbletea/v2 v2.0.6 h1:UHN/91OyuhaOFGSrBXQ/hMZD8IO1Uc4BvHlgHXL2WJo= -charm.land/bubbletea/v2 v2.0.6/go.mod h1:MH/D8ZLlN3op37vQvijKuU29g3rqTp+aQapURFonF9g= -charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U= -charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w= -charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU= -charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA= +charm.land/bubbles/v2 v2.2.0 h1:9GEMewcejrNtVIxZ9Y2wSsWJamsWNsXa8MpMLnH4yI4= +charm.land/bubbles/v2 v2.2.0/go.mod h1:wdMgn+sje1KNXdwFizIWjbf328fIUBxqEmJ/vYPo8yc= +charm.land/bubbletea/v2 v2.0.9 h1:DpJCMWKgzQK8SJv4zbKKFHAI10ymWy/evClPFk0k0f8= +charm.land/bubbletea/v2 v2.0.9/go.mod h1:2SkdgoTXluXJHOUwAoRlRXF/28vklb1rFl6GcgV1/ss= +charm.land/glamour/v2 v2.0.1 h1:xl+r00A4aJWU0z8fgwKd9fQQ4rsphqGUzuEiXZP5n+c= +charm.land/glamour/v2 v2.0.1/go.mod h1:jo9z8XqVKPeEFMVdvCRLGk++RyJ3CdUwgNr7EvXLw3k= +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/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= -github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= -github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/anotherhadi/ilovetui v0.2.2 h1:ZOwyZRxFBZKVJeEAR+lg0xeQfsMZuymtKLwBV0MnBYY= -github.com/anotherhadi/ilovetui v0.2.2/go.mod h1:gAn69amLM3z0x8PV4obyf5bJntL/PTZusEuhu/xdFIY= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs= +github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8= +github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/anotherhadi/ilovetui v0.2.4 h1:8D2jp/37vOJBRI3DKIHHt9PFsnIyfRxs3R58HuCh0tA= +github.com/anotherhadi/ilovetui v0.2.4/go.mod h1:gAn69amLM3z0x8PV4obyf5bJntL/PTZusEuhu/xdFIY= 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-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= @@ -26,14 +26,14 @@ github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 h1:Q9fO0y1Zo5KB/5Vu8JZoLGm1N3RzF9bNj3Ao3xoR+Ac= -github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468/go.mod h1:bAAz7dh/FTYfC+oiHavL4mX1tOIBZ0ZwYjSi3qE6ivM= -github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= -github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= +github.com/charmbracelet/ultraviolet v0.0.0-20260812204455-68fa937c71be h1:qEvkJy1sjJXP+yf8IH2o13NV+Rh4nIUHjaLNJ+pWxpI= +github.com/charmbracelet/ultraviolet v0.0.0-20260812204455-68fa937c71be/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/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/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= -github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= +github.com/charmbracelet/x/exp/slice v0.0.0-20260823001701-96af6d2cb5f6 h1:je2x9scjVLOqRwO5JbFyRzyG5aCLg64DSOvd463cm04= +github.com/charmbracelet/x/exp/slice v0.0.0-20260823001701-96af6d2cb5f6/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= 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= @@ -44,8 +44,8 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2/v2 v2.7.1 h1:yqDtwI1ptXXvEUNpYTk2lad4jLtAcKqkzepn4savSk4= +github.com/dlclark/regexp2/v2 v2.7.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -54,35 +54,36 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= -github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= -github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/lrstanley/bubblezone/v2 v2.0.0 h1:pMb9fHKs0slJF6OrzQ2hEgWusqyl9VU/S0UZ5hyh7ZA= +github.com/lrstanley/bubblezone/v2 v2.0.0/go.mod h1:yV/QTjcm4Zu5cqvGvdHi7xVUfnB36w/SafOuDp57dgY= +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-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU= +github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= -github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= -github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= -github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +github.com/sahilm/fuzzy v0.1.3 h1:juByESSS32nVD81vr6tHmKmA/8zde7gE+x5CLxrzXPU= +github.com/sahilm/fuzzy v0.1.3/go.mod h1:au6//VbVSqu6DFrkL2CfjlJ5iURpNCPeE+1GwY3XsT8= +github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY= +github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= +github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= +github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/ui/actionmodal.go b/ui/actionmodal.go index 740e93e..3336910 100644 --- a/ui/actionmodal.go +++ b/ui/actionmodal.go @@ -8,6 +8,8 @@ import ( "charm.land/bubbles/v2/list" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" + zone "github.com/lrstanley/bubblezone/v2" + "github.com/anotherhadi/ilovetui/modal" "github.com/anotherhadi/ilovetui/style" "github.com/anotherhadi/usbguard-tui/internal/guard" @@ -37,6 +39,15 @@ func actionModalStyle(status guard.Status) modal.Styles { func (a actionModal) Init() tea.Cmd { return nil } +func (a actionModal) confirmCmd(it actionItem) tea.Cmd { + if it.nixos { + rule := guard.NixOSRule(a.dev, it.status) + key := a.dev.VidPid + return tea.Batch(modal.Close(), func() tea.Msg { return nixRuleMsg{key: key, rule: rule} }) + } + return tea.Batch(modal.Close(), doAction(a.dev.ID, it.fn, it.permanent)) +} + func (a actionModal) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: @@ -48,13 +59,7 @@ func (a actionModal) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if item == nil { return a, nil } - it := item.(actionItem) - if it.nixos { - rule := guard.NixOSRule(a.dev, it.status) - key := a.dev.VidPid - return a, tea.Batch(modal.Close(), func() tea.Msg { return nixRuleMsg{key: key, rule: rule} }) - } - return a, tea.Batch(modal.Close(), doAction(a.dev.ID, it.fn, it.permanent)) + return a, a.confirmCmd(item.(actionItem)) } case tea.MouseWheelMsg: @@ -65,6 +70,18 @@ func (a actionModal) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.list.CursorDown() } return a, nil + + case tea.MouseClickMsg: + if msg.Mouse().Button != tea.MouseLeft { + return a, nil + } + for i, item := range a.list.VisibleItems() { + if zone.Get(actionZoneID(i)).InBounds(msg) { + a.list.Select(i) + return a, a.confirmCmd(item.(actionItem)) + } + } + return a, nil } var cmd tea.Cmd diff --git a/ui/delegate.go b/ui/delegate.go index 3974afd..e0bc694 100644 --- a/ui/delegate.go +++ b/ui/delegate.go @@ -3,14 +3,23 @@ package ui import ( "fmt" "io" + "strconv" "charm.land/bubbles/v2/list" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" + zone "github.com/lrstanley/bubblezone/v2" + "github.com/anotherhadi/ilovetui/style" "github.com/anotherhadi/usbguard-tui/internal/guard" ) +// deviceZoneID returns the bubblezone marker ID used to detect mouse clicks +// on a given device's row in the list. +func deviceZoneID(dev guard.Device) string { + return "device-" + strconv.Itoa(dev.ID) +} + var ruleStateIndicators = map[guard.RuleState]string{ guard.RulePermanent: "● perm", guard.RuleTemporary: "○ tmp", @@ -65,10 +74,9 @@ func (d deviceDelegate) Render(w io.Writer, m list.Model, index int, item list.I permIndicator := ruleStateIndicators[dev.RuleState] width := m.Width() - fmt.Fprintf(w, "%s\n%s", - clampToWidth(nameStyle.Render(name), width), - clampToWidth(descStyle.Render(fmt.Sprintf("id:%-3d %s %s %s", dev.ID, dev.VidPid, string(dev.Status), permIndicator)), width), - ) + line1 := clampToWidth(nameStyle.Render(name), width) + line2 := clampToWidth(descStyle.Render(fmt.Sprintf("id:%-3d %s %s %s", dev.ID, dev.VidPid, string(dev.Status), permIndicator)), width) + fmt.Fprint(w, zone.Mark(deviceZoneID(dev), line1+"\n"+line2)) } type actionItem struct { @@ -89,18 +97,26 @@ func (d actionDelegate) Height() int { return 1 } func (d actionDelegate) Spacing() int { return 0 } func (d actionDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } +// actionZoneID returns the bubblezone marker ID used to detect mouse clicks +// on a given row of the action modal's list. +func actionZoneID(index int) string { + return "action-" + strconv.Itoa(index) +} + func (d actionDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) { a, ok := item.(actionItem) if !ok { return } + var content string if index == m.Index() { clr, ok := statusColorsSelected[a.status] if !ok { clr = style.S.Primary } - fmt.Fprintf(w, " %s", lipgloss.NewStyle().Bold(true).Foreground(clr).Render("> "+a.label)) + content = fmt.Sprintf(" %s", lipgloss.NewStyle().Bold(true).Foreground(clr).Render("> "+a.label)) } else { - fmt.Fprintf(w, " %s", a.label) + content = fmt.Sprintf(" %s", a.label) } + fmt.Fprint(w, zone.Mark(actionZoneID(index), content)) } diff --git a/ui/model.go b/ui/model.go index f9810ea..ccd82c3 100644 --- a/ui/model.go +++ b/ui/model.go @@ -11,6 +11,7 @@ import ( tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/charmbracelet/x/ansi" + zone "github.com/lrstanley/bubblezone/v2" "github.com/anotherhadi/ilovetui/app" "github.com/anotherhadi/ilovetui/bubbles" @@ -41,6 +42,7 @@ type deviceSummary struct { type Model struct { list list.Model + devices []guard.Device help helpbar.Model daemonStatus string defaultPolicy guard.Status @@ -64,6 +66,10 @@ func New() Model { l.DisableQuitKeybindings() l.KeyMap.CursorUp = key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")) l.KeyMap.CursorDown = key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")) + l.Styles.TitleBar = l.Styles.TitleBar.PaddingLeft(1) + l.Styles.Filter.Focused.Prompt = l.Styles.Filter.Focused.Prompt.Foreground(style.S.Muted) + l.Styles.Filter.Blurred.Prompt = l.Styles.Filter.Blurred.Prompt.Foreground(style.S.Muted) + l.FilterInput.SetStyles(l.Styles.Filter) h := helpbar.New( helpbar.WithToggle(listKeys.Help), @@ -131,10 +137,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { return m, tea.Batch(fetchDevices, fetchDaemonStatus, fetchDefaultPolicy, tickCmd()) case devicesMsg: - items := make([]list.Item, len(msg)) summary := deviceSummary{total: len(msg)} - for i, d := range msg { - items[i] = d + for _, d := range msg { switch d.Status { case guard.Allowed: summary.allowed++ @@ -145,6 +149,14 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } } m.deviceCounts = summary + if slices.Equal(m.devices, msg) { + return m, nil + } + m.devices = msg + items := make([]list.Item, len(msg)) + for i, d := range msg { + items[i] = d + } cmd := m.list.SetItems(items) m.resizeList() return m, cmd @@ -192,6 +204,24 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { m.list.CursorDown() } return m, nil + + case tea.MouseClickMsg: + if m.ModalOpen || msg.Mouse().Button != tea.MouseLeft { + return m, nil + } + for i, item := range m.list.VisibleItems() { + dev := item.(guard.Device) + if zone.Get(deviceZoneID(dev)).InBounds(msg) { + alreadyFocused := i == m.list.Index() + m.list.Select(i) + if alreadyFocused { + return m, modal.Show(dev.Name, newActionModal(dev, m.rulesManaged), + modal.WithModalStyle(actionModalStyle(dev.Status))) + } + return m, nil + } + } + return m, nil } if m.ModalOpen { @@ -245,8 +275,12 @@ func (m Model) updateList(msg tea.KeyPressMsg) (Model, tea.Cmd) { } } } + prevFilterState := m.list.FilterState() var cmd tea.Cmd m.list, cmd = m.list.Update(msg) + if m.list.FilterState() != prevFilterState { + m.resizeList() + } return m, cmd } @@ -254,7 +288,19 @@ func (m Model) View() string { header := m.renderHeader() listView := strings.TrimRight(m.list.View(), "\n") helpView := strings.TrimRight(m.help.View(m.helpBindings()...), "\n") - return strings.Join([]string{header, listView, helpView}, "\n") + lines := []string{header} + if filterLine := m.renderFilterLine(); filterLine != "" { + lines = append(lines, filterLine) + } + lines = append(lines, listView, helpView) + return strings.Join(lines, "\n") +} + +func (m Model) renderFilterLine() string { + if m.list.FilterState() != list.FilterApplied { + return "" + } + return filterLabelStyle.Render("Filter: ") + m.list.FilterValue() } func (m Model) helpBindings() []key.Binding { @@ -367,7 +413,11 @@ func (m Model) renderPendingRulesLine() string { func (m Model) listHeight() int { headerH := lipgloss.Height(m.renderHeader()) helpH := m.help.Height(m.helpBindings()...) - return m.height - headerH - helpH + h := m.height - headerH - helpH + if m.list.FilterState() == list.FilterApplied { + h-- + } + return h } func (m *Model) resizeList() { diff --git a/ui/styles.go b/ui/styles.go index 47129f8..b8e3894 100644 --- a/ui/styles.go +++ b/ui/styles.go @@ -36,5 +36,9 @@ var ( PaddingLeft(1). Width(16) + filterLabelStyle = lipgloss.NewStyle(). + Foreground(style.S.Muted). + PaddingLeft(1) + warnStyle = lipgloss.NewStyle().Foreground(style.S.Warning) )