Files
2026-07-06 11:05:50 -04:00

186 lines
4.6 KiB
Go

package tui
import (
"database/sql"
"fmt"
"os"
"os/exec"
"github.com/cobr-ai/apex/internal/scan"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type SettingsModel struct {
db *sql.DB
scanCfgPath string
cursor int
items []settingRow
status string
width int
height int
}
type settingRow struct {
Label string
Value string
Kind string
}
func NewSettingsModel(db *sql.DB, scanCfgPath string) *SettingsModel {
m := &SettingsModel{db: db, scanCfgPath: scanCfgPath, status: "[↑/↓] move [enter] edit externally [r] refresh"}
m.load()
return m
}
func (m *SettingsModel) load() {
m.items = nil
claudePath, _ := exec.LookPath("claude")
claudeStatus := "not found"
if claudePath != "" {
claudeStatus = claudePath
}
m.items = append(m.items, settingRow{"Claude CLI", claudeStatus, "info"})
apexDir, _ := os.UserHomeDir()
dbPath := apexDir + "/.apex/apex.db"
dbInfo := dbPath
if st, err := os.Stat(dbPath); err == nil {
dbInfo = fmt.Sprintf("%s (%d KB)", dbPath, st.Size()/1024)
}
m.items = append(m.items, settingRow{"Database", dbInfo, "info"})
m.items = append(m.items, settingRow{"Adapters YAML", m.scanCfgPath, "info"})
if cfg, err := scan.LoadConfig(m.scanCfgPath); err == nil {
enabled := 0
for _, c := range cfg.TrackedCompanies {
if c.Enabled {
enabled++
}
}
m.items = append(m.items, settingRow{"Companies", fmt.Sprintf("%d total · %d enabled", len(cfg.TrackedCompanies), enabled), "info"})
}
if m.db != nil {
var jobs int
_ = m.db.QueryRow("SELECT COUNT(*) FROM jobs WHERE archived=0").Scan(&jobs)
m.items = append(m.items, settingRow{"Jobs in DB", fmt.Sprintf("%d", jobs), "info"})
var apps int
_ = m.db.QueryRow("SELECT COUNT(*) FROM applications").Scan(&apps)
m.items = append(m.items, settingRow{"Applications", fmt.Sprintf("%d", apps), "info"})
var reports int
_ = m.db.QueryRow("SELECT COUNT(*) FROM reports").Scan(&reports)
m.items = append(m.items, settingRow{"Reports", fmt.Sprintf("%d", reports), "info"})
}
infisicalStatus := "not configured"
if os.Getenv("INFISICAL_TOKEN") != "" {
infisicalStatus = "INFISICAL_TOKEN set"
} else if _, err := exec.LookPath("creds"); err == nil {
infisicalStatus = "creds CLI available"
}
m.items = append(m.items, settingRow{"Infisical", infisicalStatus, "info"})
m.items = append(m.items, settingRow{"Editor", editor(), "info"})
minScore := os.Getenv("APEX_MIN_SCORE")
if minScore == "" {
minScore = "0 (default)"
}
m.items = append(m.items, settingRow{"APEX_MIN_SCORE", minScore, "info"})
}
func editor() string {
if v := os.Getenv("VISUAL"); v != "" {
return v
}
if v := os.Getenv("EDITOR"); v != "" {
return v
}
return "vi (default)"
}
func (m *SettingsModel) Init() tea.Cmd { return nil }
func (m *SettingsModel) Resize(w, h int) {
m.width = w
m.height = h
}
func (m *SettingsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.items)-1 {
m.cursor++
}
case "r":
m.load()
if m.cursor >= len(m.items) {
m.cursor = len(m.items) - 1
}
case "enter":
if m.cursor < len(m.items) {
m.openEditor(m.items[m.cursor])
}
}
}
return m, nil
}
func (m *SettingsModel) openEditor(row settingRow) {
var path string
switch row.Label {
case "Adapters YAML":
path = m.scanCfgPath
case "Database":
m.status = "database is binary — open with sqlite3"
return
default:
m.status = fmt.Sprintf("no editable file for %q", row.Label)
return
}
ed := os.Getenv("VISUAL")
if ed == "" {
ed = os.Getenv("EDITOR")
}
if ed == "" {
ed = "vi"
}
cmd := exec.Command(ed, path)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run()
m.load()
}
func (m *SettingsModel) View() string {
header := StyleTitle.Render("Settings — apex config + environment")
sub := StyleSubtitle.Render(fmt.Sprintf("%d items · [↑/↓] move · [enter] edit (adapters) · [r] refresh", len(m.items)))
var lines []string
for i, it := range m.items {
marker := " "
label := it.Label
if i == m.cursor {
marker = "▌ "
label = lipgloss.NewStyle().Bold(true).Foreground(ColorAccentAlt).Render(it.Label)
}
line := fmt.Sprintf("%s%-18s %s", marker, label, StyleKeyDesc.Render(it.Value))
lines = append(lines, line)
}
body := lipgloss.JoinVertical(lipgloss.Left, lines...)
status := StyleStatusMuted.Render(m.status)
return lipgloss.JoinVertical(lipgloss.Left, header, sub, "", body, "", status)
}