Move UI & make a root model.

Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-08-20 20:28:24 +02:00
parent e99f32ded8
commit e77eaf4e2c
12 changed files with 76 additions and 47 deletions
+55
View File
@@ -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,
}
}
+1 -1
View File
@@ -25,7 +25,7 @@
src = ./.;
outputs = ["out"];
vendorHash = "sha256-tXMeJy9IpXTRhikYedcL+76H9X3In9mb1/KnN1XFPu4=";
vendorHash = "sha256-8QP2FbNmvqrJhFe3Ia7tPhkxMeFWBhvr4Zr9kpV9bR4=";
meta = with pkgs.lib; {
description = "A terminal UI for managing USB devices via usbguard.";
+1 -1
View File
@@ -6,7 +6,7 @@ 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.1
github.com/anotherhadi/ilovetui v0.2.2
github.com/charmbracelet/x/ansi v0.11.7
)
+2 -2
View File
@@ -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/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.1 h1:ndU52r0nzlT6LM53HNBvwVyvn3dRPKrbGVvzux9YXbk=
github.com/anotherhadi/ilovetui v0.2.1/go.mod h1:gAn69amLM3z0x8PV4obyf5bJntL/PTZusEuhu/xdFIY=
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/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=
+1 -2
View File
@@ -6,7 +6,6 @@ import (
tea "charm.land/bubbletea/v2"
"github.com/anotherhadi/usbguard-tui/internal/guard"
"github.com/anotherhadi/usbguard-tui/internal/ui"
)
var version = "dev"
@@ -22,7 +21,7 @@ func main() {
os.Exit(1)
}
p := tea.NewProgram(ui.New())
p := tea.NewProgram(newApp())
if _, err := p.Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
View File
+13 -38
View File
@@ -12,6 +12,7 @@ import (
"charm.land/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
"github.com/anotherhadi/ilovetui/app"
"github.com/anotherhadi/ilovetui/bubbles"
"github.com/anotherhadi/ilovetui/helpbar"
"github.com/anotherhadi/ilovetui/modal"
@@ -41,8 +42,6 @@ type deviceSummary struct {
type Model struct {
list list.Model
help helpbar.Model
modals modal.Model
notif notification.Model
daemonStatus string
defaultPolicy guard.Status
deviceCounts deviceSummary
@@ -52,6 +51,8 @@ type Model struct {
rulesWritable bool
pendingRules []pendingRule
fatalShown bool
ModalOpen bool
}
func New() Model {
@@ -76,8 +77,6 @@ func New() Model {
return Model{
list: l,
help: h,
modals: modal.New(),
notif: notification.New(),
rulesManaged: rulesManaged,
rulesWritable: rulesWritable,
}
@@ -117,21 +116,10 @@ func makeActionList(rulesManaged bool) list.Model {
}
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) {
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) {
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
@@ -200,13 +188,13 @@ func (m Model) updateMain(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, fetchDevices
case tea.KeyPressMsg:
if m.modals.Open() {
if m.ModalOpen {
return m, nil
}
return m.updateList(msg)
case tea.MouseWheelMsg:
if m.modals.Open() {
if m.ModalOpen {
return m, nil
}
switch msg.Button {
@@ -218,7 +206,7 @@ func (m Model) updateMain(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
if m.modals.Open() {
if m.ModalOpen {
return m, nil
}
var cmd tea.Cmd
@@ -226,15 +214,15 @@ func (m Model) updateMain(msg tea.Msg) (tea.Model, tea.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" {
return m, tea.Quit
return m, app.Quit()
}
if !m.list.SettingFilter() {
dev, hasSelection := m.selectedDevice()
switch {
case key.Matches(msg, listKeys.Quit):
return m, tea.Quit
return m, app.Quit()
case key.Matches(msg, listKeys.Refresh):
return m, tea.Batch(fetchDevices, fetchDaemonStatus, fetchDefaultPolicy)
case key.Matches(msg, listKeys.Help):
@@ -274,24 +262,11 @@ func (m Model) updateList(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
return m, cmd
}
func (m Model) View() tea.View {
return tea.View{
Content: m.renderContent(),
AltScreen: true,
WindowTitle: "USBGuard TUI",
MouseMode: tea.MouseModeCellMotion,
}
}
func (m Model) renderContent() string {
func (m Model) View() string {
header := m.renderHeader()
listView := strings.TrimRight(m.list.View(), "\n")
helpView := strings.TrimRight(m.help.View(), "\n")
bg := strings.Join([]string{header, listView, helpView}, "\n")
bg = m.modals.Render(bg)
bg = m.notif.Render(bg)
return bg
return strings.Join([]string{header, listView, helpView}, "\n")
}
func (m Model) renderHeader() string {
@@ -5,6 +5,7 @@ import (
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/app"
"github.com/anotherhadi/ilovetui/modal"
"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) {
switch msg.(type) {
case tea.KeyPressMsg, tea.MouseClickMsg:
return m, tea.Quit
return m, app.Quit()
}
return m, nil
}
@@ -35,8 +36,7 @@ func (permissionModal) View() tea.View {
hintStyle := lipgloss.NewStyle().Foreground(style.S.Muted)
parts := []string{
textStyle.Render("Can't reach the usbguard daemon"),
textStyle.Render("(permission denied)."),
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"),
}