Skip to content

Commit

Permalink
feat(ui): ♻️ ui management for both agent and apps
Browse files Browse the repository at this point in the history
- ui to configure preferences now supports configuring agent and any apps with preferences
- refactor and simplify ui code
  • Loading branch information
joshuar committed Jun 22, 2024
1 parent 9b5c1b4 commit 9126f38
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 458 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"vscode",
"mqtt",
"github",
"mage"
"mage",
"preferences"
],
}
48 changes: 41 additions & 7 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (

"github.com/rs/zerolog/log"

ui "github.com/joshuar/go-hass-anything/v9/internal/agent/ui/bubbletea"
"github.com/joshuar/go-hass-anything/v9/pkg/mqtt"
"github.com/joshuar/go-hass-anything/v9/pkg/preferences"
)
Expand All @@ -23,8 +22,8 @@ var (
)

type agent struct {
ui AgentUI
done chan struct{}
prefs *preferences.Preferences
id string
name string
version string
Expand Down Expand Up @@ -78,7 +77,13 @@ func NewAgent(id, name string) *agent {
id: id,
name: name,
}
a.ui = ui.NewBubbleTeaUI()

prefs, err := preferences.Load()
if err != nil {
log.Warn().Err(err).Msg("Could not fetch agent preferences.")
}
a.prefs = prefs

return a
}

Expand All @@ -98,9 +103,38 @@ func (a *agent) Stop() {
close(a.done)
}

func (a *agent) Name() string {
return a.name
}

// GetPreferences returns the agent preferences.
func (a *agent) GetPreferences() *preferences.Preferences {
return a.prefs
}

// SetPreferences sets the agent preferences to the given preferences. If the
// preferences cannot be saved, a non-nil error is returned.
func (a *agent) SetPreferences(prefs *preferences.Preferences) error {
a.prefs = prefs
return a.prefs.Save()
}

// Configure will start a terminal UI to adjust agent preferences and likewise for any
// apps that have user-configurable preferences.
func (a *agent) Configure() {
a.ui.ShowConfiguration()
a.ui.Run()
// Show a terminal UI to configure the agent preferences.
if err := ShowPreferences(a); err != nil {
log.Warn().Err(err).Msg("Problem occurred configuring agent.")
}
// For any apps that satisfy the Preferences interface, meaning they have
// configurable preferences, show a terminal UI to configure them.
for _, app := range AppList {
if prefs, ok := app.(Preferences); ok {
if err := ShowPreferences(prefs); err != nil {
log.Warn().Err(err).Str("app", prefs.Name()).Msg("Problem occurred configuring app.")
}
}
}
}

func Run(ctx context.Context) {
Expand All @@ -111,7 +145,7 @@ func Run(ctx context.Context) {
subscriptions = append(subscriptions, app.Subscriptions()...)
}

prefs, err := preferences.LoadPreferences()
prefs, err := preferences.Load()
if err != nil {
log.Fatal().Err(err).Msg("Could not load preferences.")
}
Expand All @@ -125,7 +159,7 @@ func Run(ctx context.Context) {
}

func ClearApps(ctx context.Context) {
prefs, err := preferences.LoadPreferences()
prefs, err := preferences.Load()
if err != nil {
log.Fatal().Err(err).Msg("Could not load preferences.")
}
Expand Down
196 changes: 192 additions & 4 deletions internal/agent/ui.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,199 @@
// Copyright (c) 2023 Joshua Rich <[email protected]>
// Copyright (c) 2024 Joshua Rich <[email protected]>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

package agent

type AgentUI interface {
Run()
ShowConfiguration()
import (
"fmt"
"strings"

"github.com/charmbracelet/bubbles/cursor"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/rs/zerolog/log"

"github.com/joshuar/go-hass-anything/v9/pkg/preferences"
)

var (
focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
cursorStyle = focusedStyle
noStyle = lipgloss.NewStyle()
helpStyle = blurredStyle
cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))

focusedButton = focusedStyle.Render("[ Submit ]")
blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Submit"))
)

// Preferences represents preferences the user will need to set.
type Preferences interface {
// Name is a name for this group of preferences. It could be an app name.
Name() string
// Preferences returns the current preferences of the app as a map[string]any
GetPreferences() *preferences.Preferences
// SetPreferences will set the given preferences for the app. It returns a
// non-nil error if there was a problem setting any preferences.
SetPreferences(prefs *preferences.Preferences) error
}

type model struct {
title string
inputs []textinput.Model
keys []string
focusIndex int
cursorMode cursor.Mode
}

func (m model) Init() tea.Cmd {
return textinput.Blink
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc":
return m, tea.Quit

// Change cursor mode
case "ctrl+r":
m.cursorMode++
if m.cursorMode > cursor.CursorHide {
m.cursorMode = cursor.CursorBlink
}
cmds := make([]tea.Cmd, len(m.inputs))
for i := range m.inputs {
cmds[i] = m.inputs[i].Cursor.SetMode(m.cursorMode)
}
return m, tea.Batch(cmds...)

// Set focus to next input
case "tab", "shift+tab", "enter", "up", "down":
s := msg.String()

// Did the user press enter while the submit button was focused?
// If so, exit.
if s == "enter" && m.focusIndex == len(m.inputs) {
return m, tea.Quit
}

// Cycle indexes
if s == "up" || s == "shift+tab" {
m.focusIndex--
} else {
m.focusIndex++
}

if m.focusIndex > len(m.inputs) {
m.focusIndex = 0
} else if m.focusIndex < 0 {
m.focusIndex = len(m.inputs)
}

cmds := make([]tea.Cmd, len(m.inputs))
for i := 0; i <= len(m.inputs)-1; i++ {
if i == m.focusIndex {
cmds[i] = m.inputs[i].Focus()
m.inputs[i].PromptStyle = focusedStyle
m.inputs[i].TextStyle = focusedStyle
continue
}
m.inputs[i].Blur()
m.inputs[i].PromptStyle = noStyle
m.inputs[i].TextStyle = noStyle
}

return m, tea.Batch(cmds...)
}
}

// Handle character input and blinking
cmd := m.updateInputs(msg)

return m, cmd
}

func (m *model) updateInputs(msg tea.Msg) tea.Cmd {
cmds := make([]tea.Cmd, len(m.inputs))

// Only text inputs with Focus() set will respond, so it's safe to simply
// update all of them here without any further logic.
for i := range m.inputs {
m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
}

return tea.Batch(cmds...)
}

func (m model) View() string {
var b strings.Builder

b.WriteRune('\n')
b.WriteString(fmt.Sprintf("Set preferences for %s:\n", m.title))
b.WriteRune('\n')
b.WriteRune('\n')

for i := range m.inputs {
b.WriteString(m.inputs[i].View())
if i < len(m.inputs)-1 {
b.WriteRune('\n')
}
}

button := &blurredButton
if m.focusIndex == len(m.inputs) {
button = &focusedButton
}
fmt.Fprintf(&b, "\n\n%s\n\n", *button)

b.WriteString(helpStyle.Render("cursor mode is "))
b.WriteString(cursorModeHelpStyle.Render(m.cursorMode.String()))
b.WriteString(helpStyle.Render(" (ctrl+r to change style)"))

return b.String()
}

func newPreferencesForm(title string, prefs *preferences.Preferences) *model {
model := &model{title: title, keys: prefs.Keys()}

model.inputs = make([]textinput.Model, len(model.keys))

for i := range model.inputs {
t := textinput.New()
t.Cursor.Style = cursorStyle
t.CharLimit = 32
t.Placeholder = prefs.GetString(model.keys[i])
t.PromptStyle = focusedStyle
t.Prompt = model.keys[i] + " > "
t.TextStyle = focusedStyle
model.inputs[i] = t
}

return model
}

func ShowPreferences(app Preferences) error {
appPrefs := app.GetPreferences()
appModel := newPreferencesForm(app.Name(), appPrefs)

if _, err := tea.NewProgram(appModel).Run(); err != nil {
return fmt.Errorf("could not load preferences: %w", err)
}

for i := 0; i <= len(appModel.inputs)-1; i++ {
if err := appPrefs.Set(appModel.keys[i], appModel.inputs[i].Value()); err != nil {
log.Warn().Err(err).Str("app", app.Name()).Str("preference", appModel.keys[i]).Msg("Could not save app preference.")
}

if err := app.SetPreferences(appPrefs); err != nil {
return fmt.Errorf("could not save preferences: %w", err)
}
}

return nil
}
57 changes: 0 additions & 57 deletions internal/agent/ui/bubbletea/bubbletea.go

This file was deleted.

Loading

0 comments on commit 9126f38

Please sign in to comment.