8 Commits
Author SHA1 Message Date
Hadi f69373267e v1.3.0
Signed-off-by: Hadi <[email protected]>
2026-08-23 18:33:11 +02:00
Hadi 83fa5622aa Update docs
Signed-off-by: Hadi <[email protected]>
2026-08-23 18:28:32 +02:00
Hadi b3a59c9174 edit gif
Signed-off-by: Hadi <[email protected]>
2026-08-23 18:27:00 +02:00
Hadi 658b052307 Min-width, style & mouse support
Signed-off-by: Hadi <[email protected]>
2026-08-23 18:09:45 +02:00
Hadi 1ba517179e edit
Signed-off-by: Hadi <[email protected]>
2026-08-23 18:09:11 +02:00
Hadi 2e91b6bc2a Fix width in modal
Signed-off-by: Hadi <[email protected]>
2026-08-23 17:14:19 +02:00
Hadi 2d6c337610 move goreleaser
Signed-off-by: Hadi <[email protected]>
2026-08-23 17:12:48 +02:00
Hadi 032397e1fd move main
Signed-off-by: Hadi <[email protected]>
2026-08-23 17:12:34 +02:00
16 changed files with 227 additions and 165 deletions
@@ -6,6 +6,7 @@ before:
builds:
- binary: usbguard-tui
main: ./cmd/usbguard-tui
goos:
- linux
goarch:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 533 KiB

+2 -2
View File
@@ -1,8 +1,8 @@
Output ./assets/demo.gif
Output .github/assets/demo.gif
Require usbguard-tui
Set Shell "zsh"
Set FontSize 32
Set FontSize 26
Set Width 1500
Set Height 1000
+1 -1
View File
@@ -23,6 +23,6 @@ jobs:
- uses: goreleaser/goreleaser-action@v6
with:
version: "~> v2"
args: release --clean
args: release --clean --config .github/.goreleaser.yaml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-2
View File
@@ -1,2 +0,0 @@
/go.work
/go.work.sum
+15 -11
View File
@@ -1,17 +1,19 @@
```
▖▖▄▖▄ ▄▖ ▌ ▄▖▖▖▄▖
▌▌▚ ▙▘▌ ▌▌▀▌▛▘▛▌ ▐ ▌▌▐
▙▌▄▌▙▘▙▌▙▌█▌▌ ▙▌ ▐ ▙▌▟▖
_ _ ____ ____ ____ _ _____ _ _ ___
| | | / ___|| __ ) / ___|_ _ __ _ _ __ __| | |_ _| | | |_ _|
| | | \___ \| _ \| | _| | | |/ _` | '__/ _` | | | | | | || |
| |_| |___) | |_) | |_| | |_| | (_| | | | (_| | | | | |_| || |
\___/|____/|____/ \____|\__,_|\__,_|_| \__,_| |_| \___/|___|
```
[![Go Version](https://img.shields.io/github/go-mod/go-version/anotherhadi/usbguard-tui)](go.mod)
[![Release](https://img.shields.io/github/v/release/anotherhadi/usbguard-tui)](https://github.com/anotherhadi/usbguard-tui/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Go Report Card](https://goreportcard.com/badge/github.com/anotherhadi/usbguard-tui)](https://goreportcard.com/report/github.com/anotherhadi/usbguard-tui)
![GitHub Stars](https://www.shieldcn.dev/github/stars/anotherhadi/usbguard-tui.svg?variant=outline&theme=violet)
![Release](https://www.shieldcn.dev/github/release/anotherhadi/usbguard-tui.svg?variant=outline&theme=violet)
![CI](https://www.shieldcn.dev/github/ci/anotherhadi/usbguard-tui.svg?variant=outline&theme=violet)
[![Ko-fi](https://www.shieldcn.dev/badge/Ko--fi-sponsor-FF5E5B.svg?logo=kofi&variant=secondary&theme=violet)](https://ko-fi.com/anotherhadi)
# USBGuard TUI
A terminal UI for managing USB devices via [usbguard](https://usbguard.github.io/).
> A terminal UI for managing USB devices via [usbguard](https://usbguard.github.io/).
USBGuard is a software framework for implementing a USB device authorization policy (allowlisting/blocklisting). It protects your system against rogue USB devices by scanning them and checking their parameters against a set of rules.
@@ -21,6 +23,8 @@ Built with [bubbletea](https://github.com/charmbracelet/bubbletea) & Golang!
Colors and styles can be customized using [ilovetui](https://github.com/anotherhadi/ilovetui), which applies theme changes across all compatible TUI applications at once.
> This project is NOT affiliated with, endorsed by, or connected to "Red Hat, Inc" in any way. It is an unofficial, community-driven tool.
## Features
- List all connected USB devices with their current status
@@ -28,7 +32,7 @@ Colors and styles can be customized using [ilovetui](https://github.com/anotherh
- Action popup for quick device management
- Filter devices by name with `/`
- Auto-refresh
- Keyboard shortcuts for all actions (`a`/`A`, `b`/`B`, `e`/`E`, ...)
- Keyboard shortcuts for all actions (`a`/`A`, `b`/`B`, `e`/`E`, ...) & Mouse support
## Requirements
@@ -52,7 +56,7 @@ go install github.com/anotherhadi/usbguard-tui@latest
```sh
git clone https://github.com/anotherhadi/usbguard-tui
cd usbguard-tui
go build -o usbguard-tui .
go build -o usbguard-tui ./cmd/usbguard-tui
```
</details>
@@ -95,4 +99,4 @@ The device list refreshes automatically every 2 seconds.
<a href="https://github.com/anotherhadi/usbguard-tui">github</a> |
<a href="https://gitlab.com/anotherhadi_mirror/usbguard-tui">gitlab (mirror)</a> |
<a href="https://git.hadi.icu/anotherhadi/usbguard-tui">gitea (mirror)</a>
</div
</div>
+25 -1
View File
@@ -2,23 +2,35 @@ package main
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(),
minsize: minsize.New(minWidth, minHeight),
}
}
@@ -30,6 +42,9 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if _, ok := msg.(app.QuitMsg); ok {
return a, tea.Quit
}
if sizeMsg, ok := msg.(tea.WindowSizeMsg); ok {
a.width, a.height = sizeMsg.Width, sizeMsg.Height
}
a.core.ModalOpen = a.modals.Open()
var coreCmd tea.Cmd
@@ -43,9 +58,18 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (a appModel) View() tea.View {
bg := a.core.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,
+5
View File
@@ -21,6 +21,11 @@ func main() {
os.Exit(1)
}
if _, err := guard.ListDevices(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
p := tea.NewProgram(newApp())
if _, err := p.Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
+3 -3
View File
@@ -14,7 +14,7 @@
(system: f system (import nixpkgs {inherit system;}));
pname = "usbguard-tui";
version = "1.2.0";
version = "1.3.0";
ldflags = ["-s" "-w" "-X main.version=${version}"];
in {
@@ -25,12 +25,12 @@
src = ./.;
outputs = ["out"];
vendorHash = "sha256-8QP2FbNmvqrJhFe3Ia7tPhkxMeFWBhvr4Zr9kpV9bR4=";
vendorHash = "sha256-O9DG0pxRKt8VwpZdyvoQ4wsfEbsh5npmwocmtcm2IfA=";
meta = with pkgs.lib; {
description = "A terminal UI for managing USB devices via usbguard.";
homepage = "https://github.com/anotherhadi/usbguard-tui";
platforms = platforms.unix;
platforms = platforms.linux;
};
};
in {
+21 -20
View File
@@ -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
)
+46 -45
View File
@@ -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=
+25 -8
View File
@@ -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
@@ -76,7 +93,7 @@ func (a actionModal) View() tea.View {
hintStyle := lipgloss.NewStyle().Foreground(style.S.Muted)
parts := []string{a.list.View(), ""}
if a.rulesManaged {
parts = append(parts, hintStyle.Render(fmt.Sprintf("[NixOS: perm rules queued — press %s to copy]", listKeys.CopyRules.Help().Key)))
parts = append(parts, hintStyle.Render(fmt.Sprintf("[NixOS: perm rules queued, press %s to copy]", listKeys.CopyRules.Help().Key)))
}
parts = append(parts, hintStyle.Render("↑↓ navigate enter confirm esc cancel"))
return tea.NewView(strings.Join(parts, "\n"))
+18 -6
View File
@@ -3,14 +3,21 @@ 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"
)
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 +72,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 +95,24 @@ 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 }
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))
}
+55 -16
View File
@@ -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
@@ -50,7 +52,6 @@ type Model struct {
rulesManaged bool
rulesWritable bool
pendingRules []pendingRule
fatalShown bool
ModalOpen bool
}
@@ -65,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),
@@ -132,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++
@@ -146,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
@@ -172,16 +183,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
case actionMsg:
if msg.err != nil {
if msg.err == guard.ErrPermission {
if m.fatalShown {
return m, nil
}
m.fatalShown = true
return m, modal.Show("Permission Error", newPermissionModal(),
modal.WithModalStyle(permissionModalStyle()))
}
return m, errorToast(msg.err)
}
return m, fetchDevices
@@ -203,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 {
@@ -256,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
}
@@ -265,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 {
@@ -378,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() {
-44
View File
@@ -1,44 +0,0 @@
package ui
import (
"strings"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/app"
"github.com/anotherhadi/ilovetui/modal"
"github.com/anotherhadi/ilovetui/style"
)
type permissionModal struct{}
func newPermissionModal() tea.Model { return permissionModal{} }
func permissionModalStyle() modal.Styles {
s := modal.DefaultStyles()
s.Title = s.Title.Foreground(style.S.Error)
s.Border = s.Border.BorderForeground(style.S.Error)
return s
}
func (permissionModal) Init() tea.Cmd { return nil }
func (m permissionModal) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.(type) {
case tea.KeyPressMsg, tea.MouseClickMsg:
return m, app.Quit()
}
return m, nil
}
func (permissionModal) View() tea.View {
textStyle := lipgloss.NewStyle().Foreground(style.S.Text)
hintStyle := lipgloss.NewStyle().Foreground(style.S.Muted)
parts := []string{
textStyle.Render("Can't reach the usbguard daemon (permission denied)."),
textStyle.Render("Join the usbguard group, or use sudo."),
hintStyle.Render("press any key to quit"),
}
return tea.NewView(strings.Join(parts, "\n"))
}
+4
View File
@@ -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)
)