only show what is available

Signed-off-by: Hadi <[email protected]>
This commit is contained in:
Hadi
2026-08-13 18:12:44 +02:00
parent 6e4f8b889c
commit fc35732dbf
8 changed files with 86 additions and 28 deletions
+8
View File
@@ -31,6 +31,14 @@ func CheckAvailable() error {
return nil return nil
} }
// Installed reports whether wpctl is present in PATH, without probing
// PipeWire. Cheap enough to call when deciding whether to show the Audio
// page in the sidebar.
func Installed() bool {
_, err := exec.LookPath("wpctl")
return err == nil
}
func run(args ...string) (string, error) { func run(args ...string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel() defer cancel()
+8
View File
@@ -39,6 +39,14 @@ func CheckAvailable() error {
return nil return nil
} }
// Installed reports whether bluetoothctl is present in PATH, without
// probing for a controller. Cheap enough to call when deciding whether to
// show the Bluetooth page in the sidebar.
func Installed() bool {
_, err := exec.LookPath("bluetoothctl")
return err == nil
}
// run executes bluetoothctl in single-command (non-interactive) mode. // run executes bluetoothctl in single-command (non-interactive) mode.
// bluetoothctl writes both normal output and error messages to stdout // bluetoothctl writes both normal output and error messages to stdout
// (colored with ANSI escapes), so errors are extracted from there rather // (colored with ANSI escapes), so errors are extracted from there rather
+8
View File
@@ -22,6 +22,14 @@ func CheckAvailable() error {
return err return err
} }
// Installed reports whether hyprctl is present in PATH, without probing
// Hyprland. Cheap enough to call when deciding whether to show the Inputs
// page in the sidebar.
func Installed() bool {
_, err := exec.LookPath("hyprctl")
return err == nil
}
func Reset() error { func Reset() error {
_, err := run("reload") _, err := run("reload")
return err return err
+8
View File
@@ -36,6 +36,14 @@ func CheckAvailable() error {
return nil return nil
} }
// Installed reports whether nmcli is present in PATH, without probing
// NetworkManager. Cheap enough to call when deciding whether to show the
// Network page in the sidebar.
func Installed() bool {
_, err := exec.LookPath("nmcli")
return err == nil
}
func run(args ...string) (string, error) { func run(args ...string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel() defer cancel()
+8
View File
@@ -36,6 +36,14 @@ func CheckAvailable() error {
return err return err
} }
// Installed reports whether powerprofilesctl is present in PATH, without
// probing power-profiles-daemon. Cheap enough to call when deciding whether
// to show the Power page in the sidebar.
func Installed() bool {
_, err := exec.LookPath("powerprofilesctl")
return err == nil
}
// ListProfiles returns the available profiles in the order reported by // ListProfiles returns the available profiles in the order reported by
// powerprofilesctl, each flagged with whether it's currently active. // powerprofilesctl, each flagged with whether it's currently active.
func ListProfiles() ([]Profile, error) { func ListProfiles() ([]Profile, error) {
+7 -5
View File
@@ -20,10 +20,9 @@ import (
) )
// buildPageShortcuts numbers the visible pages 1..N in sidebar order, so // buildPageShortcuts numbers the visible pages 1..N in sidebar order, so
// hiding a page never leaves a gap in the shortcuts. It must run after // hiding a page (or a missing CLI tool) never leaves a gap in the
// config.Load, hence a function called from New rather than a package var. // shortcuts.
func buildPageShortcuts() map[string]page { func buildPageShortcuts(visible []pageEntry) map[string]page {
visible := visiblePages()
m := make(map[string]page, len(visible)) m := make(map[string]page, len(visible))
for i, e := range visible { for i, e := range visible {
m[strconv.Itoa(i+1)] = e.id m[strconv.Itoa(i+1)] = e.id
@@ -34,6 +33,7 @@ func buildPageShortcuts() map[string]page {
type Model struct { type Model struct {
page page page page
pageShortcuts map[string]page pageShortcuts map[string]page
visiblePages []pageEntry
logPath string logPath string
fatalErr error fatalErr error
logFileErr error logFileErr error
@@ -67,10 +67,12 @@ func New(initialPage string) (Model, error) {
} }
cfg := config.Global cfg := config.Global
visible := visiblePages()
m := Model{ m := Model{
page: startPage, page: startPage,
pageShortcuts: buildPageShortcuts(), pageShortcuts: buildPageShortcuts(visible),
visiblePages: visible,
about: aboutUI.New(), about: aboutUI.New(),
network: networkUI.New(), network: networkUI.New(),
bluetooth: bluetoothUI.New(), bluetooth: bluetoothUI.New(),
+28 -12
View File
@@ -4,8 +4,13 @@ import (
"strings" "strings"
tea "charm.land/bubbletea/v2" tea "charm.land/bubbletea/v2"
"github.com/anotherhadi/settuings/internal/audio"
"github.com/anotherhadi/settuings/internal/bluetooth"
"github.com/anotherhadi/settuings/internal/config" "github.com/anotherhadi/settuings/internal/config"
"github.com/anotherhadi/settuings/internal/icons" "github.com/anotherhadi/settuings/internal/icons"
"github.com/anotherhadi/settuings/internal/input"
"github.com/anotherhadi/settuings/internal/network"
"github.com/anotherhadi/settuings/internal/power"
aboutUI "github.com/anotherhadi/settuings/internal/ui/about" aboutUI "github.com/anotherhadi/settuings/internal/ui/about"
audioUI "github.com/anotherhadi/settuings/internal/ui/audio" audioUI "github.com/anotherhadi/settuings/internal/ui/audio"
bluetoothUI "github.com/anotherhadi/settuings/internal/ui/bluetooth" bluetoothUI "github.com/anotherhadi/settuings/internal/ui/bluetooth"
@@ -30,6 +35,12 @@ type pageEntry struct {
id page id page
icon func() string icon func() string
// available reports whether this page's underlying CLI tool is
// installed. nil = always available (e.g. About has no dependency).
// A page that isn't available is left out of the sidebar and its
// numbered shortcuts, but stays reachable via --page.
available func() bool
// render returns the page's view content. nil = show "empty". // render returns the page's view content. nil = show "empty".
render func(m *Model) string render func(m *Model) string
// update is called when this page is active. nil = no-op. // update is called when this page is active. nil = no-op.
@@ -68,6 +79,7 @@ var pageRegistry = []pageEntry{
{ {
id: pageNetwork, id: pageNetwork,
icon: func() string { return icons.I.Network }, icon: func() string { return icons.I.Network },
available: network.Installed,
render: func(m *Model) string { return m.network.View().Content }, render: func(m *Model) string { return m.network.View().Content },
update: func(m *Model, msg tea.Msg) tea.Cmd { update: func(m *Model, msg tea.Msg) tea.Cmd {
@@ -82,6 +94,7 @@ var pageRegistry = []pageEntry{
{ {
id: pageBluetooth, id: pageBluetooth,
icon: func() string { return icons.I.Bluetooth }, icon: func() string { return icons.I.Bluetooth },
available: bluetooth.Installed,
render: func(m *Model) string { return m.bluetooth.View().Content }, render: func(m *Model) string { return m.bluetooth.View().Content },
update: func(m *Model, msg tea.Msg) tea.Cmd { update: func(m *Model, msg tea.Msg) tea.Cmd {
@@ -96,6 +109,7 @@ var pageRegistry = []pageEntry{
{ {
id: pageAudio, id: pageAudio,
icon: func() string { return icons.I.Audio }, icon: func() string { return icons.I.Audio },
available: audio.Installed,
render: func(m *Model) string { return m.audio.View().Content }, render: func(m *Model) string { return m.audio.View().Content },
update: func(m *Model, msg tea.Msg) tea.Cmd { update: func(m *Model, msg tea.Msg) tea.Cmd {
@@ -111,6 +125,7 @@ var pageRegistry = []pageEntry{
{ {
id: pagePower, id: pagePower,
icon: func() string { return icons.I.Power }, icon: func() string { return icons.I.Power },
available: power.Installed,
render: func(m *Model) string { return m.power.View().Content }, render: func(m *Model) string { return m.power.View().Content },
update: func(m *Model, msg tea.Msg) tea.Cmd { update: func(m *Model, msg tea.Msg) tea.Cmd {
@@ -125,6 +140,7 @@ var pageRegistry = []pageEntry{
{ {
id: pageInputs, id: pageInputs,
icon: func() string { return icons.I.Inputs }, icon: func() string { return icons.I.Inputs },
available: input.Installed,
render: func(m *Model) string { return m.input.View().Content }, render: func(m *Model) string { return m.input.View().Content },
update: func(m *Model, msg tea.Msg) tea.Cmd { update: func(m *Model, msg tea.Msg) tea.Cmd {
@@ -160,30 +176,30 @@ func lookupPage(name string) (page, bool) {
} }
// visiblePages returns pageRegistry entries not listed in the user's // visiblePages returns pageRegistry entries not listed in the user's
// tui.hidden_pages config, preserving registry order. A page hidden this // tui.hidden_pages config and whose underlying CLI tool (if any) is
// way is still reachable via --page: hiding only affects the sidebar and // installed, preserving registry order. A page left out this way is still
// its numbered shortcuts. // reachable via --page: this only affects the sidebar and its numbered
// shortcuts. It shells out to `exec.LookPath` per page, so callers should
// compute it once (e.g. in New) and cache the result rather than calling it
// on every render.
func visiblePages() []pageEntry { func visiblePages() []pageEntry {
hiddenNames := config.Global.TUI.HiddenPages hiddenNames := config.Global.TUI.HiddenPages
if len(hiddenNames) == 0 {
return pageRegistry
}
hidden := make(map[page]bool, len(hiddenNames)) hidden := make(map[page]bool, len(hiddenNames))
for _, name := range hiddenNames { for _, name := range hiddenNames {
if p, ok := lookupPage(name); ok { if p, ok := lookupPage(name); ok {
hidden[p] = true hidden[p] = true
} }
} }
if len(hidden) == 0 {
return pageRegistry
}
visible := make([]pageEntry, 0, len(pageRegistry)) visible := make([]pageEntry, 0, len(pageRegistry))
for _, e := range pageRegistry { for _, e := range pageRegistry {
if !hidden[e.id] { if hidden[e.id] {
visible = append(visible, e) continue
} }
if e.available != nil && !e.available() {
continue
}
visible = append(visible, e)
} }
return visible return visible
} }
+1 -1
View File
@@ -62,7 +62,7 @@ func (m *Model) renderSidebar() string {
var items strings.Builder var items strings.Builder
badgeUnread := lipgloss.NewStyle().Foreground(ilovetui.S.Warning).Bold(true) badgeUnread := lipgloss.NewStyle().Foreground(ilovetui.S.Warning).Bold(true)
for i, entry := range visiblePages() { for i, entry := range m.visiblePages {
selected := entry.id == m.page selected := entry.id == m.page
badgeStyle, textStyle := badgeNormal, textNormal badgeStyle, textStyle := badgeNormal, textNormal
if selected { if selected {