initial public release
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user