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) }