mirror of
https://github.com/anotherhadi/usbguard-tui.git
synced 2026-08-21 12:05:50 +02:00
Compare commits
5 Commits
bbab824a1d
..
v1.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 4199b6aa7f | |||
| ccc353261b | |||
| e77eaf4e2c | |||
| e99f32ded8 | |||
| 1b19643db4 |
@@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"github.com/anotherhadi/ilovetui/app"
|
||||||
|
"github.com/anotherhadi/ilovetui/modal"
|
||||||
|
"github.com/anotherhadi/ilovetui/notification"
|
||||||
|
"github.com/anotherhadi/usbguard-tui/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type appModel struct {
|
||||||
|
core ui.Model
|
||||||
|
modals modal.Model
|
||||||
|
notif notification.Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func newApp() appModel {
|
||||||
|
return appModel{
|
||||||
|
core: ui.New(),
|
||||||
|
modals: modal.New(),
|
||||||
|
notif: notification.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a appModel) Init() tea.Cmd {
|
||||||
|
return tea.Batch(a.core.Init(), a.modals.Init(), a.notif.Init())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
if _, ok := msg.(app.QuitMsg); ok {
|
||||||
|
return a, tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
a.core.ModalOpen = a.modals.Open()
|
||||||
|
var coreCmd tea.Cmd
|
||||||
|
a.core, coreCmd = a.core.Update(msg)
|
||||||
|
|
||||||
|
var modalCmd, notifCmd tea.Cmd
|
||||||
|
a.modals, modalCmd = a.modals.Update(msg)
|
||||||
|
a.notif, notifCmd = a.notif.Update(msg)
|
||||||
|
|
||||||
|
return a, tea.Batch(coreCmd, modalCmd, notifCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a appModel) View() tea.View {
|
||||||
|
bg := a.core.View()
|
||||||
|
bg = a.modals.Render(bg)
|
||||||
|
bg = a.notif.Render(bg)
|
||||||
|
return tea.View{
|
||||||
|
Content: bg,
|
||||||
|
AltScreen: true,
|
||||||
|
WindowTitle: "USBGuard TUI",
|
||||||
|
MouseMode: tea.MouseModeCellMotion,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
(system: f system (import nixpkgs {inherit system;}));
|
(system: f system (import nixpkgs {inherit system;}));
|
||||||
|
|
||||||
pname = "usbguard-tui";
|
pname = "usbguard-tui";
|
||||||
version = "1.1.0";
|
version = "1.2.0";
|
||||||
|
|
||||||
ldflags = ["-s" "-w" "-X main.version=${version}"];
|
ldflags = ["-s" "-w" "-X main.version=${version}"];
|
||||||
in {
|
in {
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
src = ./.;
|
src = ./.;
|
||||||
outputs = ["out"];
|
outputs = ["out"];
|
||||||
|
|
||||||
vendorHash = "sha256-tXMeJy9IpXTRhikYedcL+76H9X3In9mb1/KnN1XFPu4=";
|
vendorHash = "sha256-8QP2FbNmvqrJhFe3Ia7tPhkxMeFWBhvr4Zr9kpV9bR4=";
|
||||||
|
|
||||||
meta = with pkgs.lib; {
|
meta = with pkgs.lib; {
|
||||||
description = "A terminal UI for managing USB devices via usbguard.";
|
description = "A terminal UI for managing USB devices via usbguard.";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ require (
|
|||||||
charm.land/bubbles/v2 v2.1.0
|
charm.land/bubbles/v2 v2.1.0
|
||||||
charm.land/bubbletea/v2 v2.0.6
|
charm.land/bubbletea/v2 v2.0.6
|
||||||
charm.land/lipgloss/v2 v2.0.3
|
charm.land/lipgloss/v2 v2.0.3
|
||||||
github.com/anotherhadi/ilovetui v0.2.1
|
github.com/anotherhadi/ilovetui v0.2.2
|
||||||
github.com/charmbracelet/x/ansi v0.11.7
|
github.com/charmbracelet/x/ansi v0.11.7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46
|
|||||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
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 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/anotherhadi/ilovetui v0.2.1 h1:ndU52r0nzlT6LM53HNBvwVyvn3dRPKrbGVvzux9YXbk=
|
github.com/anotherhadi/ilovetui v0.2.2 h1:ZOwyZRxFBZKVJeEAR+lg0xeQfsMZuymtKLwBV0MnBYY=
|
||||||
github.com/anotherhadi/ilovetui v0.2.1/go.mod h1:gAn69amLM3z0x8PV4obyf5bJntL/PTZusEuhu/xdFIY=
|
github.com/anotherhadi/ilovetui v0.2.2/go.mod h1:gAn69amLM3z0x8PV4obyf5bJntL/PTZusEuhu/xdFIY=
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
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 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"github.com/anotherhadi/usbguard-tui/internal/guard"
|
"github.com/anotherhadi/usbguard-tui/internal/guard"
|
||||||
"github.com/anotherhadi/usbguard-tui/internal/ui"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "dev"
|
var version = "dev"
|
||||||
@@ -22,20 +21,9 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
p := tea.NewProgram(ui.New())
|
p := tea.NewProgram(newApp())
|
||||||
m, err := p.Run()
|
if _, err := p.Run(); err != nil {
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if fm, ok := m.(ui.Model); ok {
|
|
||||||
if rules := fm.PendingRules(); len(rules) > 0 {
|
|
||||||
fmt.Println("# Add to your NixOS configuration:")
|
|
||||||
fmt.Println("services.usbguard.rules = lib.mkAfter ''")
|
|
||||||
for _, rule := range rules {
|
|
||||||
fmt.Println(" ", rule)
|
|
||||||
}
|
|
||||||
fmt.Println("'';")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"charm.land/bubbles/v2/key"
|
"charm.land/bubbles/v2/key"
|
||||||
@@ -50,7 +51,8 @@ func (a actionModal) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
it := item.(actionItem)
|
it := item.(actionItem)
|
||||||
if it.nixos {
|
if it.nixos {
|
||||||
rule := guard.NixOSRule(a.dev, it.status)
|
rule := guard.NixOSRule(a.dev, it.status)
|
||||||
return a, tea.Batch(modal.Close(), func() tea.Msg { return nixRuleMsg{rule: rule} })
|
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, tea.Batch(modal.Close(), doAction(a.dev.ID, it.fn, it.permanent))
|
||||||
}
|
}
|
||||||
@@ -74,7 +76,7 @@ func (a actionModal) View() tea.View {
|
|||||||
hintStyle := lipgloss.NewStyle().Foreground(style.S.Muted)
|
hintStyle := lipgloss.NewStyle().Foreground(style.S.Muted)
|
||||||
parts := []string{a.list.View(), ""}
|
parts := []string{a.list.View(), ""}
|
||||||
if a.rulesManaged {
|
if a.rulesManaged {
|
||||||
parts = append(parts, hintStyle.Render("[NixOS: perm rules printed on exit]"))
|
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"))
|
parts = append(parts, hintStyle.Render("↑↓ navigate enter confirm esc cancel"))
|
||||||
return tea.NewView(strings.Join(parts, "\n"))
|
return tea.NewView(strings.Join(parts, "\n"))
|
||||||
@@ -18,7 +18,7 @@ type listKeyMap struct {
|
|||||||
|
|
||||||
AllowAll key.Binding
|
AllowAll key.Binding
|
||||||
AllowAllPerm key.Binding
|
AllowAllPerm key.Binding
|
||||||
PrintAll key.Binding
|
CopyRules key.Binding
|
||||||
|
|
||||||
Up key.Binding
|
Up key.Binding
|
||||||
Down key.Binding
|
Down key.Binding
|
||||||
@@ -32,11 +32,17 @@ func (k listKeyMap) globalBindings() []key.Binding {
|
|||||||
return []key.Binding{
|
return []key.Binding{
|
||||||
k.Open, k.Filter, k.Refresh, k.Quit,
|
k.Open, k.Filter, k.Refresh, k.Quit,
|
||||||
k.Allow, k.AllowPerm, k.Block, k.BlockPerm, k.Reject, k.RejectPerm,
|
k.Allow, k.AllowPerm, k.Block, k.BlockPerm, k.Reject, k.RejectPerm,
|
||||||
k.AllowAll, k.AllowAllPerm, k.PrintAll,
|
k.AllowAll, k.AllowAllPerm, k.CopyRules,
|
||||||
k.Up, k.Down, k.GoToStart, k.GoToEnd, k.PrevPage, k.NextPage,
|
k.Up, k.Down, k.GoToStart, k.GoToEnd, k.PrevPage, k.NextPage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (k listKeyMap) shortHelpBindings() []key.Binding {
|
||||||
|
return []key.Binding{
|
||||||
|
k.Open, k.Allow, k.Block, k.Reject, k.Filter, k.Quit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var listKeys = listKeyMap{
|
var listKeys = listKeyMap{
|
||||||
Open: key.NewBinding(key.WithKeys("enter", "tab"), key.WithHelp("enter/tab", "select action")),
|
Open: key.NewBinding(key.WithKeys("enter", "tab"), key.WithHelp("enter/tab", "select action")),
|
||||||
Filter: key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")),
|
Filter: key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")),
|
||||||
@@ -52,7 +58,7 @@ var listKeys = listKeyMap{
|
|||||||
RejectPerm: key.NewBinding(key.WithKeys("E"), key.WithHelp("E", "reject (perm)")),
|
RejectPerm: key.NewBinding(key.WithKeys("E"), key.WithHelp("E", "reject (perm)")),
|
||||||
AllowAll: key.NewBinding(key.WithKeys("w"), key.WithHelp("w", "allow all")),
|
AllowAll: key.NewBinding(key.WithKeys("w"), key.WithHelp("w", "allow all")),
|
||||||
AllowAllPerm: key.NewBinding(key.WithKeys("W"), key.WithHelp("W", "allow all (perm)")),
|
AllowAllPerm: key.NewBinding(key.WithKeys("W"), key.WithHelp("W", "allow all (perm)")),
|
||||||
PrintAll: key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "print all rules")),
|
CopyRules: key.NewBinding(key.WithKeys("y"), key.WithHelp("y", "copy rules")),
|
||||||
|
|
||||||
Up: key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")),
|
Up: key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")),
|
||||||
Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")),
|
Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")),
|
||||||
@@ -2,6 +2,7 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/charmbracelet/x/ansi"
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/ilovetui/app"
|
||||||
"github.com/anotherhadi/ilovetui/bubbles"
|
"github.com/anotherhadi/ilovetui/bubbles"
|
||||||
"github.com/anotherhadi/ilovetui/helpbar"
|
"github.com/anotherhadi/ilovetui/helpbar"
|
||||||
"github.com/anotherhadi/ilovetui/modal"
|
"github.com/anotherhadi/ilovetui/modal"
|
||||||
@@ -25,9 +27,14 @@ type (
|
|||||||
daemonStatusMsg string
|
daemonStatusMsg string
|
||||||
defaultPolicyMsg guard.Status
|
defaultPolicyMsg guard.Status
|
||||||
actionMsg struct{ err error }
|
actionMsg struct{ err error }
|
||||||
nixRuleMsg struct{ rule string }
|
nixRuleMsg struct{ key, rule string }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type pendingRule struct {
|
||||||
|
key string
|
||||||
|
rule string
|
||||||
|
}
|
||||||
|
|
||||||
type deviceSummary struct {
|
type deviceSummary struct {
|
||||||
total, allowed, blocked, rejected int
|
total, allowed, blocked, rejected int
|
||||||
}
|
}
|
||||||
@@ -35,8 +42,6 @@ type deviceSummary struct {
|
|||||||
type Model struct {
|
type Model struct {
|
||||||
list list.Model
|
list list.Model
|
||||||
help helpbar.Model
|
help helpbar.Model
|
||||||
modals modal.Model
|
|
||||||
notif notification.Model
|
|
||||||
daemonStatus string
|
daemonStatus string
|
||||||
defaultPolicy guard.Status
|
defaultPolicy guard.Status
|
||||||
deviceCounts deviceSummary
|
deviceCounts deviceSummary
|
||||||
@@ -44,11 +49,11 @@ type Model struct {
|
|||||||
height int
|
height int
|
||||||
rulesManaged bool
|
rulesManaged bool
|
||||||
rulesWritable bool
|
rulesWritable bool
|
||||||
pendingRules []string
|
pendingRules []pendingRule
|
||||||
fatalShown bool
|
fatalShown bool
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) PendingRules() []string { return m.pendingRules }
|
ModalOpen bool
|
||||||
|
}
|
||||||
|
|
||||||
func New() Model {
|
func New() Model {
|
||||||
l := bubbles.NewList(nil, 0, 0)
|
l := bubbles.NewList(nil, 0, 0)
|
||||||
@@ -63,7 +68,6 @@ func New() Model {
|
|||||||
|
|
||||||
h := helpbar.New(
|
h := helpbar.New(
|
||||||
helpbar.WithToggle(listKeys.Help),
|
helpbar.WithToggle(listKeys.Help),
|
||||||
helpbar.WithGlobal(listKeys.globalBindings()...),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
rulesManaged := guard.IsRulesManaged()
|
rulesManaged := guard.IsRulesManaged()
|
||||||
@@ -72,8 +76,6 @@ func New() Model {
|
|||||||
return Model{
|
return Model{
|
||||||
list: l,
|
list: l,
|
||||||
help: h,
|
help: h,
|
||||||
modals: modal.New(),
|
|
||||||
notif: notification.New(),
|
|
||||||
rulesManaged: rulesManaged,
|
rulesManaged: rulesManaged,
|
||||||
rulesWritable: rulesWritable,
|
rulesWritable: rulesWritable,
|
||||||
}
|
}
|
||||||
@@ -113,28 +115,17 @@ func makeActionList(rulesManaged bool) list.Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
return tea.Batch(fetchDevices, fetchDaemonStatus, fetchDefaultPolicy, tickCmd(), m.modals.Init(), m.notif.Init())
|
return tea.Batch(fetchDevices, fetchDaemonStatus, fetchDefaultPolicy, tickCmd(), app.SetTitle("USBGuard TUI"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||||
next, cmd := m.updateMain(msg)
|
|
||||||
nm := next.(Model)
|
|
||||||
|
|
||||||
var modalCmd, notifCmd tea.Cmd
|
|
||||||
nm.modals, modalCmd = nm.modals.Update(msg)
|
|
||||||
nm.notif, notifCmd = nm.notif.Update(msg)
|
|
||||||
|
|
||||||
return nm, tea.Batch(cmd, modalCmd, notifCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updateMain(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
m.help.SetWidth(msg.Width)
|
m.help.SetWidth(msg.Width)
|
||||||
m.list.SetSize(msg.Width, m.listHeight())
|
m.resizeList()
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case tickMsg:
|
case tickMsg:
|
||||||
@@ -156,6 +147,7 @@ func (m Model) updateMain(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
m.deviceCounts = summary
|
m.deviceCounts = summary
|
||||||
cmd := m.list.SetItems(items)
|
cmd := m.list.SetItems(items)
|
||||||
|
m.resizeList()
|
||||||
return m, cmd
|
return m, cmd
|
||||||
|
|
||||||
case daemonStatusMsg:
|
case daemonStatusMsg:
|
||||||
@@ -167,7 +159,15 @@ func (m Model) updateMain(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case nixRuleMsg:
|
case nixRuleMsg:
|
||||||
m.pendingRules = append(m.pendingRules, msg.rule)
|
hadPending := len(m.pendingRules) > 0
|
||||||
|
if i := slices.IndexFunc(m.pendingRules, func(r pendingRule) bool { return r.key == msg.key }); i >= 0 {
|
||||||
|
m.pendingRules[i].rule = msg.rule
|
||||||
|
} else {
|
||||||
|
m.pendingRules = append(m.pendingRules, pendingRule{key: msg.key, rule: msg.rule})
|
||||||
|
}
|
||||||
|
if !hadPending {
|
||||||
|
m.resizeList()
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case actionMsg:
|
case actionMsg:
|
||||||
@@ -187,13 +187,13 @@ func (m Model) updateMain(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, fetchDevices
|
return m, fetchDevices
|
||||||
|
|
||||||
case tea.KeyPressMsg:
|
case tea.KeyPressMsg:
|
||||||
if m.modals.Open() {
|
if m.ModalOpen {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
return m.updateList(msg)
|
return m.updateList(msg)
|
||||||
|
|
||||||
case tea.MouseWheelMsg:
|
case tea.MouseWheelMsg:
|
||||||
if m.modals.Open() {
|
if m.ModalOpen {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
switch msg.Button {
|
switch msg.Button {
|
||||||
@@ -205,7 +205,7 @@ func (m Model) updateMain(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.modals.Open() {
|
if m.ModalOpen {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
@@ -213,20 +213,20 @@ func (m Model) updateMain(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) updateList(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
func (m Model) updateList(msg tea.KeyPressMsg) (Model, tea.Cmd) {
|
||||||
if msg.String() == "ctrl+c" {
|
if msg.String() == "ctrl+c" {
|
||||||
return m, tea.Quit
|
return m, app.Quit()
|
||||||
}
|
}
|
||||||
if !m.list.SettingFilter() {
|
if !m.list.SettingFilter() {
|
||||||
dev, hasSelection := m.selectedDevice()
|
dev, hasSelection := m.selectedDevice()
|
||||||
switch {
|
switch {
|
||||||
case key.Matches(msg, listKeys.Quit):
|
case key.Matches(msg, listKeys.Quit):
|
||||||
return m, tea.Quit
|
return m, app.Quit()
|
||||||
case key.Matches(msg, listKeys.Refresh):
|
case key.Matches(msg, listKeys.Refresh):
|
||||||
return m, tea.Batch(fetchDevices, fetchDaemonStatus, fetchDefaultPolicy)
|
return m, tea.Batch(fetchDevices, fetchDaemonStatus, fetchDefaultPolicy)
|
||||||
case key.Matches(msg, listKeys.Help):
|
case key.Matches(msg, listKeys.Help):
|
||||||
m.help.ShowAll = !m.help.ShowAll
|
m.help.ShowAll = !m.help.ShowAll
|
||||||
m.list.SetSize(m.width, m.listHeight())
|
m.resizeList()
|
||||||
return m, nil
|
return m, nil
|
||||||
case key.Matches(msg, listKeys.Open):
|
case key.Matches(msg, listKeys.Open):
|
||||||
if hasSelection {
|
if hasSelection {
|
||||||
@@ -240,8 +240,15 @@ func (m Model) updateList(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, queueNixOSRules(m.visibleDevices(), guard.Allowed)
|
return m, queueNixOSRules(m.visibleDevices(), guard.Allowed)
|
||||||
}
|
}
|
||||||
return m, doBulkAction(m.visibleDeviceIDs(), guard.AllowDevice, true)
|
return m, doBulkAction(m.visibleDeviceIDs(), guard.AllowDevice, true)
|
||||||
case key.Matches(msg, listKeys.PrintAll):
|
case key.Matches(msg, listKeys.CopyRules):
|
||||||
return m, queueCurrentStateRules(m.visibleDevices())
|
toCopy := m.pendingRules
|
||||||
|
if len(toCopy) == 0 {
|
||||||
|
toCopy = mergeCurrentStateRules(nil, m.visibleDevices())
|
||||||
|
}
|
||||||
|
cmd := copyRulesCmd(toCopy, m.rulesManaged)
|
||||||
|
m.pendingRules = nil
|
||||||
|
m.resizeList()
|
||||||
|
return m, cmd
|
||||||
}
|
}
|
||||||
if hasSelection {
|
if hasSelection {
|
||||||
if cmd := m.deviceActionCmd(msg, dev); cmd != nil {
|
if cmd := m.deviceActionCmd(msg, dev); cmd != nil {
|
||||||
@@ -254,30 +261,28 @@ func (m Model) updateList(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) View() tea.View {
|
func (m Model) View() string {
|
||||||
return tea.View{
|
|
||||||
Content: m.renderContent(),
|
|
||||||
AltScreen: true,
|
|
||||||
WindowTitle: "USBGuard TUI",
|
|
||||||
MouseMode: tea.MouseModeCellMotion,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderContent() string {
|
|
||||||
header := m.renderHeader()
|
header := m.renderHeader()
|
||||||
listView := strings.TrimRight(m.list.View(), "\n")
|
listView := strings.TrimRight(m.list.View(), "\n")
|
||||||
helpView := strings.TrimRight(m.help.View(), "\n")
|
helpView := strings.TrimRight(m.help.View(m.helpBindings()...), "\n")
|
||||||
bg := strings.Join([]string{header, listView, helpView}, "\n")
|
return strings.Join([]string{header, listView, helpView}, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
bg = m.modals.Render(bg)
|
func (m Model) helpBindings() []key.Binding {
|
||||||
bg = m.notif.Render(bg)
|
if m.help.ShowAll {
|
||||||
return bg
|
return listKeys.globalBindings()
|
||||||
|
}
|
||||||
|
return listKeys.shortHelpBindings()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderHeader() string {
|
func (m Model) renderHeader() string {
|
||||||
title := headerStyle.Render("USBGuard-tui")
|
title := headerStyle.Render("USBGuard TUI")
|
||||||
|
if style.S.NerdFonts {
|
||||||
|
title = " " + title
|
||||||
|
}
|
||||||
lines := []string{
|
lines := []string{
|
||||||
title,
|
title,
|
||||||
|
"",
|
||||||
m.renderServiceLine(),
|
m.renderServiceLine(),
|
||||||
m.renderPolicyLine(),
|
m.renderPolicyLine(),
|
||||||
m.renderDevicesLine(),
|
m.renderDevicesLine(),
|
||||||
@@ -367,15 +372,20 @@ func (m Model) renderPendingRulesLine() string {
|
|||||||
noun = "rules"
|
noun = "rules"
|
||||||
}
|
}
|
||||||
label := infoLabelStyle.Render("Pending rules")
|
label := infoLabelStyle.Render("Pending rules")
|
||||||
return label + warnStyle.Render(fmt.Sprintf("%d %s queued (printed on exit)", count, noun))
|
return label + warnStyle.Render(fmt.Sprintf("%d %s queued (press %s to copy)", count, noun, listKeys.CopyRules.Help().Key))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) listHeight() int {
|
func (m Model) listHeight() int {
|
||||||
headerH := lipgloss.Height(m.renderHeader())
|
headerH := lipgloss.Height(m.renderHeader())
|
||||||
helpH := m.help.Height()
|
helpH := m.help.Height(m.helpBindings()...)
|
||||||
return m.height - headerH - helpH
|
return m.height - headerH - helpH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) resizeList() {
|
||||||
|
m.list.SetSize(m.width, m.listHeight())
|
||||||
|
m.list.SetSize(m.width, m.listHeight())
|
||||||
|
}
|
||||||
|
|
||||||
func (m Model) selectedDevice() (guard.Device, bool) {
|
func (m Model) selectedDevice() (guard.Device, bool) {
|
||||||
if item := m.list.SelectedItem(); item != nil {
|
if item := m.list.SelectedItem(); item != nil {
|
||||||
return item.(guard.Device), true
|
return item.(guard.Device), true
|
||||||
@@ -451,19 +461,58 @@ func doBulkAction(ids []int, fn func(int, bool) error, permanent bool) tea.Cmd {
|
|||||||
func queueNixOSRules(devices []guard.Device, status guard.Status) tea.Cmd {
|
func queueNixOSRules(devices []guard.Device, status guard.Status) tea.Cmd {
|
||||||
cmds := make([]tea.Cmd, len(devices))
|
cmds := make([]tea.Cmd, len(devices))
|
||||||
for i, d := range devices {
|
for i, d := range devices {
|
||||||
|
key := d.VidPid
|
||||||
rule := guard.NixOSRule(d, status)
|
rule := guard.NixOSRule(d, status)
|
||||||
cmds[i] = func() tea.Msg { return nixRuleMsg{rule: rule} }
|
cmds[i] = func() tea.Msg { return nixRuleMsg{key: key, rule: rule} }
|
||||||
}
|
}
|
||||||
return tea.Batch(cmds...)
|
return tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func queueCurrentStateRules(devices []guard.Device) tea.Cmd {
|
func mergeCurrentStateRules(pending []pendingRule, devices []guard.Device) []pendingRule {
|
||||||
cmds := make([]tea.Cmd, len(devices))
|
for _, d := range devices {
|
||||||
for i, d := range devices {
|
key := d.VidPid
|
||||||
rule := guard.NixOSRule(d, d.Status)
|
rule := guard.NixOSRule(d, d.Status)
|
||||||
cmds[i] = func() tea.Msg { return nixRuleMsg{rule: rule} }
|
if i := slices.IndexFunc(pending, func(r pendingRule) bool { return r.key == key }); i >= 0 {
|
||||||
|
pending[i].rule = rule
|
||||||
|
} else {
|
||||||
|
pending = append(pending, pendingRule{key: key, rule: rule})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return tea.Batch(cmds...)
|
return pending
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyRulesCmd(pending []pendingRule, nixos bool) tea.Cmd {
|
||||||
|
if len(pending) == 0 {
|
||||||
|
return notification.Show("Copy rules", "No rules to copy.", notification.Warning)
|
||||||
|
}
|
||||||
|
rules := make([]string, len(pending))
|
||||||
|
for i, r := range pending {
|
||||||
|
rules[i] = r.rule
|
||||||
|
}
|
||||||
|
text := formatRulesForClipboard(rules, nixos)
|
||||||
|
noun := "rule"
|
||||||
|
if len(rules) > 1 {
|
||||||
|
noun = "rules"
|
||||||
|
}
|
||||||
|
msg := fmt.Sprintf("%d %s copied to clipboard.", len(rules), noun)
|
||||||
|
return tea.Batch(
|
||||||
|
tea.SetClipboard(text),
|
||||||
|
notification.Show("Copy rules", msg, notification.Success),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatRulesForClipboard(rules []string, nixos bool) string {
|
||||||
|
if !nixos {
|
||||||
|
return strings.Join(rules, "\n")
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("# Add to your NixOS configuration:\n")
|
||||||
|
b.WriteString("services.usbguard.rules = lib.mkAfter ''\n")
|
||||||
|
for _, r := range rules {
|
||||||
|
b.WriteString(" " + r + "\n")
|
||||||
|
}
|
||||||
|
b.WriteString("'';")
|
||||||
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
type actionBinding struct {
|
type actionBinding struct {
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/anotherhadi/ilovetui/app"
|
||||||
"github.com/anotherhadi/ilovetui/modal"
|
"github.com/anotherhadi/ilovetui/modal"
|
||||||
"github.com/anotherhadi/ilovetui/style"
|
"github.com/anotherhadi/ilovetui/style"
|
||||||
)
|
)
|
||||||
@@ -25,7 +26,7 @@ func (permissionModal) Init() tea.Cmd { return nil }
|
|||||||
func (m permissionModal) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m permissionModal) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg.(type) {
|
switch msg.(type) {
|
||||||
case tea.KeyPressMsg, tea.MouseClickMsg:
|
case tea.KeyPressMsg, tea.MouseClickMsg:
|
||||||
return m, tea.Quit
|
return m, app.Quit()
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -35,8 +36,7 @@ func (permissionModal) View() tea.View {
|
|||||||
hintStyle := lipgloss.NewStyle().Foreground(style.S.Muted)
|
hintStyle := lipgloss.NewStyle().Foreground(style.S.Muted)
|
||||||
|
|
||||||
parts := []string{
|
parts := []string{
|
||||||
textStyle.Render("Can't reach the usbguard daemon"),
|
textStyle.Render("Can't reach the usbguard daemon (permission denied)."),
|
||||||
textStyle.Render("(permission denied)."),
|
|
||||||
textStyle.Render("Join the usbguard group, or use sudo."),
|
textStyle.Render("Join the usbguard group, or use sudo."),
|
||||||
hintStyle.Render("press any key to quit"),
|
hintStyle.Render("press any key to quit"),
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user