theniceboy/agent-tracker/cmd/agent/status_right_panel.go
2026-04-02 14:20:00 -07:00

259 lines
7.9 KiB
Go

package main
import (
"fmt"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type statusRightPanelEntry struct {
Module string
Title string
Subtitle string
Enabled bool
}
type statusRightPanelModel struct {
entries []statusRightPanelEntry
selected int
width int
height int
status string
statusUntil time.Time
showAltHints bool
requestBack bool
}
func newStatusRightPanelModel() *statusRightPanelModel {
model := &statusRightPanelModel{}
model.reload()
return model
}
func (m *statusRightPanelModel) reload() {
entries := make([]statusRightPanelEntry, 0, len(statusRightModules()))
for _, module := range statusRightModules() {
entries = append(entries, statusRightPanelEntry{
Module: module,
Title: statusRightModuleLabel(module),
Subtitle: capitalizeStatusRightDescription(statusRightModuleDescription(module)),
Enabled: statusRightModuleEnabled(module),
})
}
m.entries = entries
m.selected = clampInt(m.selected, 0, maxInt(0, len(m.entries)-1))
}
func capitalizeStatusRightDescription(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
return strings.ToUpper(value[:1]) + value[1:]
}
func (m *statusRightPanelModel) Init() tea.Cmd {
return nil
}
func (m *statusRightPanelModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
return m, nil
case tea.KeyMsg:
if isAltFooterToggleKey(msg) {
m.showAltHints = !m.showAltHints
return m, nil
}
m.showAltHints = false
switch msg.String() {
case "esc":
m.requestBack = true
case "ctrl+u", "alt+u", "up", "u":
m.selected = clampInt(m.selected-1, 0, maxInt(0, len(m.entries)-1))
case "ctrl+e", "alt+e", "down", "e":
m.selected = clampInt(m.selected+1, 0, maxInt(0, len(m.entries)-1))
case "enter", " ":
m.toggleSelected()
}
}
return m, nil
}
func (m *statusRightPanelModel) toggleSelected() {
entry, ok := m.currentEntry()
if !ok {
return
}
if err := togglePaletteStatusRightModule(entry.Module); err != nil {
m.setStatus(err.Error(), 1500*time.Millisecond)
return
}
m.reload()
updated, ok := m.currentEntry()
if !ok {
return
}
verb := "disabled"
if updated.Enabled {
verb = "enabled"
}
m.setStatus(fmt.Sprintf("%s %s", updated.Title, verb), 1500*time.Millisecond)
}
func (m *statusRightPanelModel) currentEntry() (statusRightPanelEntry, bool) {
if len(m.entries) == 0 || m.selected < 0 || m.selected >= len(m.entries) {
return statusRightPanelEntry{}, false
}
return m.entries[m.selected], true
}
func (m *statusRightPanelModel) View() string {
return m.render(newPaletteStyles(), m.width, m.height)
}
func (m *statusRightPanelModel) render(styles paletteStyles, width, height int) string {
if width <= 0 {
width = 96
}
if height <= 0 {
height = 28
}
header := lipgloss.JoinVertical(lipgloss.Left,
styles.title.Render("Bottom-right Status"),
styles.meta.Render("Interactive control center for tmux status-right modules"),
styles.meta.Render("Current layout: "+truncate(m.layoutSummary(), maxInt(28, width-2))),
)
lines := []string{styles.meta.Render(fmt.Sprintf("%d modules", len(m.entries))), ""}
for idx, entry := range m.entries {
rowStyle := styles.item.Width(maxInt(24, width-2))
titleStyle := styles.itemTitle
metaStyle := styles.itemSubtitle
detailStyle := styles.meta
fillStyle := lipgloss.NewStyle()
badgeStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("235")).Background(lipgloss.Color("241")).Padding(0, 1).Bold(true)
badgeLabel := "OFF"
if entry.Enabled {
badgeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("235")).Background(lipgloss.Color("150")).Padding(0, 1).Bold(true)
badgeLabel = "ON"
}
if idx == m.selected {
selectedBG := lipgloss.Color("238")
rowStyle = styles.selectedItem.Width(maxInt(24, width-2))
titleStyle = titleStyle.Background(selectedBG).Foreground(lipgloss.Color("230"))
metaStyle = styles.selectedSubtle.Background(selectedBG)
detailStyle = styles.selectedSubtle.Background(selectedBG)
fillStyle = fillStyle.Background(selectedBG)
}
badge := badgeStyle.Render(badgeLabel)
innerWidth := maxInt(22, width-2)
titleText := truncate(entry.Title, maxInt(8, innerWidth-lipgloss.Width(badge)-1))
gapWidth := maxInt(1, innerWidth-lipgloss.Width(titleText)-lipgloss.Width(badge))
titleRow := lipgloss.JoinHorizontal(lipgloss.Left,
titleStyle.Render(titleText),
fillStyle.Render(strings.Repeat(" ", gapWidth)),
badge,
)
detail := entry.Subtitle + ". " + statusRightVisibilityText(entry.Enabled)
detailText := truncate(detail, innerWidth)
detailGap := maxInt(0, innerWidth-lipgloss.Width(detailText))
detailRow := lipgloss.JoinHorizontal(lipgloss.Left,
metaStyle.Render(detailText),
fillStyle.Render(strings.Repeat(" ", detailGap)),
)
orderText := truncate(statusRightOrderHint(entry.Module, entry.Enabled), innerWidth)
orderGap := maxInt(0, innerWidth-lipgloss.Width(orderText))
orderRow := lipgloss.JoinHorizontal(lipgloss.Left,
detailStyle.Render(orderText),
fillStyle.Render(strings.Repeat(" ", orderGap)),
)
lines = append(lines, rowStyle.Render(lipgloss.JoinVertical(lipgloss.Left, titleRow, detailRow, orderRow)))
}
bodyHeight := maxInt(8, height-7)
body := lipgloss.NewStyle().Height(bodyHeight).Render(strings.Join(lines, "\n"))
footer := m.renderFooter(styles, width)
view := lipgloss.JoinVertical(lipgloss.Left, header, "", body, "", footer)
return lipgloss.NewStyle().Width(width).Height(height).Padding(0, 1).Render(view)
}
func statusRightVisibilityText(enabled bool) string {
if enabled {
return "Visible in the tmux bottom-right status"
}
return "Hidden from the tmux bottom-right status"
}
func statusRightOrderHint(module string, enabled bool) string {
prefix := "Order slot: " + statusRightModuleLabel(module)
if enabled {
return prefix + " follows the fixed module order"
}
return prefix + " stays reserved until re-enabled"
}
func (m *statusRightPanelModel) layoutSummary() string {
labels := make([]string, 0, len(m.entries))
for _, entry := range m.entries {
if entry.Enabled {
labels = append(labels, entry.Title)
}
}
if len(labels) == 0 {
return "nothing enabled"
}
return strings.Join(labels, " -> ")
}
func (m *statusRightPanelModel) renderFooter(styles paletteStyles, width int) string {
status := strings.TrimSpace(m.currentStatus())
renderSegments := func(pairs [][2]string) string {
return renderShortcutPairs(func(v string) string { return styles.shortcutKey.Render(v) }, func(v string) string { return styles.shortcutText.Render(v) }, " ", pairs)
}
footer := ""
if m.showAltHints {
footer = pickRenderedShortcutFooter(width, renderSegments,
[][2]string{{"Space", "toggle"}, {"Alt-S", "close"}, {footerHintToggleKey, "hide"}},
[][2]string{{"Space", "toggle"}, {"Alt-S", "close"}},
)
} else {
footer = pickRenderedShortcutFooter(width, renderSegments,
[][2]string{{"u/e", "move"}, {"Enter", "toggle"}, {"Esc", "back"}, {footerHintToggleKey, "more"}},
[][2]string{{"u/e", "move"}, {"Enter", "toggle"}, {footerHintToggleKey, "more"}},
[][2]string{{"Esc", "back"}, {footerHintToggleKey, "more"}},
)
}
if status != "" {
statusText := styles.statusBad.Render(truncate(status, maxInt(12, minInt(24, width/3))))
if lipgloss.Width(footer)+2+lipgloss.Width(statusText) <= width {
gap := width - lipgloss.Width(footer) - lipgloss.Width(statusText)
if gap < 2 {
gap = 2
}
return footer + strings.Repeat(" ", gap) + statusText
}
return statusText
}
return lipgloss.NewStyle().Width(width).Render(footer)
}
func (m *statusRightPanelModel) setStatus(text string, duration time.Duration) {
m.status = text
m.statusUntil = time.Now().Add(duration)
}
func (m *statusRightPanelModel) currentStatus() string {
if m.status == "" {
return ""
}
if !m.statusUntil.IsZero() && time.Now().After(m.statusUntil) {
m.status = ""
return ""
}
return m.status
}