mirror of
https://github.com/anotherhadi/usbguard-tui.git
synced 2026-10-05 10:18:25 +02:00
Min-width, style & mouse support
Signed-off-by: Hadi <[email protected]>
This commit is contained in:
+24
-7
@@ -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
|
||||
|
||||
+22
-6
@@ -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))
|
||||
}
|
||||
|
||||
+55
-5
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user