diff --git a/README.md b/README.md index d32f6e8..c050c01 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ USBGuard is a software framework for implementing a USB device authorization pol 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. + ## Features - List all connected USB devices with their current status (allowed, blocked, rejected) diff --git a/flake.nix b/flake.nix index a88e1d8..353d150 100644 --- a/flake.nix +++ b/flake.nix @@ -25,7 +25,7 @@ src = ./.; outputs = ["out"]; - vendorHash = "sha256-NfmNdQaISob3ZguQnwgfXHUDUFION988ucp18q996pM="; + vendorHash = "sha256-tXMeJy9IpXTRhikYedcL+76H9X3In9mb1/KnN1XFPu4="; meta = with pkgs.lib; { description = "A terminal UI for managing USB devices via usbguard."; diff --git a/go.mod b/go.mod index 7f0b639..c44fe8c 100644 --- a/go.mod +++ b/go.mod @@ -10,20 +10,33 @@ require ( ) require ( + charm.land/glamour/v2 v2.0.0 // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/anotherhadi/ilovetui v0.1.6 // 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/ultraviolet v0.0.0-20260416155717-489999b90468 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // 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/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/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 + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 115baa7..a9ed389 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,20 @@ 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= +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/anotherhadi/ilovetui v0.1.6 h1:NKg+T1DpV08Q4r+iowFrXF+0bTd6Y2f4OFpFwhsfsyY= +github.com/anotherhadi/ilovetui v0.1.6/go.mod h1:HVai6u5NGKSMOpmioYpwrN0lSxQjc7HtISUc5hTwvOw= 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= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 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/ultraviolet v0.0.0-20260416155717-489999b90468 h1:Q9fO0y1Zo5KB/5Vu8JZoLGm1N3RzF9bNj3Ao3xoR+Ac= @@ -16,6 +24,8 @@ github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dA github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= 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/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= @@ -26,12 +36,18 @@ 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/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 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/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= @@ -40,9 +56,21 @@ 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= 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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/ui/delegate.go b/internal/ui/delegate.go index 8a87952..f4a4aa8 100644 --- a/internal/ui/delegate.go +++ b/internal/ui/delegate.go @@ -7,6 +7,7 @@ import ( "charm.land/bubbles/v2/list" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" + "github.com/anotherhadi/ilovetui" "github.com/anotherhadi/usbguard-tui/internal/guard" ) @@ -30,25 +31,25 @@ func (d deviceDelegate) Render(w io.Writer, m list.Model, index int, item list.I } clr, ok := colorMap[dev.Status] if !ok { - clr = colorMuted + clr = ilovetui.S.Muted } var nameStyle, descStyle lipgloss.Style if selected { nameStyle = lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), false, false, false, true). - BorderForeground(colorAccent). + BorderForeground(ilovetui.S.Primary). Foreground(clr). Bold(true). PaddingLeft(1) descStyle = lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), false, false, false, true). - BorderForeground(colorAccent). - Foreground(colorMuted). + BorderForeground(ilovetui.S.Primary). + Foreground(ilovetui.S.Muted). PaddingLeft(1) } else { nameStyle = lipgloss.NewStyle().Foreground(clr).PaddingLeft(2) - descStyle = lipgloss.NewStyle().Foreground(colorMuted).PaddingLeft(2) + descStyle = lipgloss.NewStyle().Foreground(ilovetui.S.Muted).PaddingLeft(2) } permIndicator := "○ tmp" @@ -87,7 +88,7 @@ func (d actionDelegate) Render(w io.Writer, m list.Model, index int, item list.I if index == m.Index() { clr, ok := statusColorsSelected[a.status] if !ok { - clr = colorAccent + clr = ilovetui.S.Primary } fmt.Fprintf(w, " %s", lipgloss.NewStyle().Bold(true).Foreground(clr).Render("> "+a.label)) } else { diff --git a/internal/ui/model.go b/internal/ui/model.go index b8851c8..1b74db9 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -11,6 +11,7 @@ import ( "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" + "github.com/anotherhadi/ilovetui" "github.com/anotherhadi/usbguard-tui/internal/guard" ) @@ -57,12 +58,11 @@ func New() Model { l.Styles = list.DefaultStyles(true) filterStyles := textinput.DefaultStyles(true) - filterStyles.Focused.Prompt = filterStyles.Focused.Prompt.Foreground(colorAccent) - filterStyles.Blurred.Prompt = filterStyles.Blurred.Prompt.Foreground(colorAccent) + filterStyles.Focused.Prompt = filterStyles.Focused.Prompt.Foreground(ilovetui.S.Primary) + filterStyles.Blurred.Prompt = filterStyles.Blurred.Prompt.Foreground(ilovetui.S.Primary) l.Styles.Filter = filterStyles - h := help.New() - h.Styles = help.DefaultStyles(true) + h := ilovetui.NewHelp() rulesManaged := guard.IsRulesManaged() notice := "" @@ -303,11 +303,11 @@ func (m Model) renderActionSelect() string { innerW := m.actionListInnerWidth() title := popupTitleStyle.Foreground(color).Width(innerW).Render(dev.Name) - hint := lipgloss.NewStyle().Foreground(colorMuted).Width(innerW).Render("↑↓ navigate enter confirm esc cancel") + hint := lipgloss.NewStyle().Foreground(ilovetui.S.Muted).Width(innerW).Render("↑↓ navigate enter confirm esc cancel") parts := []string{title, m.actionList.View(), ""} if m.rulesManaged { - nixosHint := lipgloss.NewStyle().Foreground(colorMuted).Width(innerW).Render("[NixOS: perm rules printed on exit]") + nixosHint := lipgloss.NewStyle().Foreground(ilovetui.S.Muted).Width(innerW).Render("[NixOS: perm rules printed on exit]") parts = append(parts, nixosHint) } parts = append(parts, hint) diff --git a/internal/ui/styles.go b/internal/ui/styles.go index a201951..1893e6e 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -4,49 +4,39 @@ import ( "image/color" "charm.land/lipgloss/v2" + "github.com/anotherhadi/ilovetui" "github.com/anotherhadi/usbguard-tui/internal/guard" ) -var ( - colorAllowed color.Color = lipgloss.Color("28") - colorAllowedSelected color.Color = lipgloss.Color("42") - colorBlocked color.Color = lipgloss.Color("124") - colorBlockedSelected color.Color = lipgloss.Color("196") - colorRejected color.Color = lipgloss.Color("130") - colorRejectedSelected color.Color = lipgloss.Color("214") - colorMuted color.Color = lipgloss.Color("240") - colorAccent color.Color = lipgloss.Color("99") -) - var statusColors = map[guard.Status]color.Color{ - guard.Allowed: colorAllowed, - guard.Blocked: colorBlocked, - guard.Rejected: colorRejected, + guard.Allowed: ilovetui.S.Success, + guard.Blocked: ilovetui.S.Error, + guard.Rejected: ilovetui.S.Warning, } var statusColorsSelected = map[guard.Status]color.Color{ - guard.Allowed: colorAllowedSelected, - guard.Blocked: colorBlockedSelected, - guard.Rejected: colorRejectedSelected, + guard.Allowed: ilovetui.S.Success, + guard.Blocked: ilovetui.S.Error, + guard.Rejected: ilovetui.S.Warning, } var ( headerStyle = lipgloss.NewStyle(). Bold(true). - Foreground(colorAccent). + Foreground(ilovetui.S.Primary). PaddingLeft(1) - daemonActiveStyle = lipgloss.NewStyle().Foreground(colorAllowedSelected) - daemonOtherStyle = lipgloss.NewStyle().Foreground(colorMuted) + daemonActiveStyle = lipgloss.NewStyle().Foreground(ilovetui.S.Success) + daemonOtherStyle = lipgloss.NewStyle().Foreground(ilovetui.S.Muted) - mutedStyle = lipgloss.NewStyle().Foreground(colorMuted) + mutedStyle = lipgloss.NewStyle().Foreground(ilovetui.S.Muted) popupStyle = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). - BorderForeground(colorAccent). + BorderForeground(ilovetui.S.Primary). Padding(1, 3) popupTitleStyle = lipgloss.NewStyle().Bold(true).MarginBottom(1) - warnStyle = lipgloss.NewStyle().Foreground(colorRejected) + warnStyle = lipgloss.NewStyle().Foreground(ilovetui.S.Warning) )