mirror of
https://github.com/anotherhadi/settuings.git
synced 2026-10-05 16:18:24 +02:00
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
+38
-22
@@ -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.
|
||||||
@@ -66,8 +77,9 @@ var pageRegistry = []pageEntry{
|
|||||||
resize: func(m *Model, w, h int) { m.about.SetSize(w, h) },
|
resize: func(m *Model, w, h int) { m.about.SetSize(w, h) },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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 {
|
||||||
@@ -80,8 +92,9 @@ var pageRegistry = []pageEntry{
|
|||||||
activate: func(m *Model) tea.Cmd { return m.network.Activate() },
|
activate: func(m *Model) tea.Cmd { return m.network.Activate() },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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 {
|
||||||
@@ -94,8 +107,9 @@ var pageRegistry = []pageEntry{
|
|||||||
activate: func(m *Model) tea.Cmd { return m.bluetooth.Activate() },
|
activate: func(m *Model) tea.Cmd { return m.bluetooth.Activate() },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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 {
|
||||||
@@ -109,8 +123,9 @@ var pageRegistry = []pageEntry{
|
|||||||
deactivate: func(m *Model) { m.audio.Deactivate() },
|
deactivate: func(m *Model) { m.audio.Deactivate() },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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 {
|
||||||
@@ -123,8 +138,9 @@ var pageRegistry = []pageEntry{
|
|||||||
activate: func(m *Model) tea.Cmd { return m.power.Activate() },
|
activate: func(m *Model) tea.Cmd { return m.power.Activate() },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user