package main
import (
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/admin"
)
type view int
const (
viewMain view = iota
viewUsers
viewRepos
viewACL
viewTrust
viewAudit
)
type statusMsg struct {
text string
err error
}
type formCancelMsg struct{}
type model struct {
ops admin.Ops
domain string
mode string
view view
mainList list.Model
usersList list.Model
reposList list.Model
aclList list.Model
trustList list.Model
auditList list.Model
selectedRepo string
activeForm *form
status string
err error
width, height int
}
func newModel(ops admin.Ops, domain, mode string) model {
mkList := func(title string, items []list.Item) list.Model {
l := list.New(items, list.NewDefaultDelegate(), 0, 0)
l.Title = title
l.SetShowHelp(false)
return l
}
m := model{
ops: ops,
domain: domain,
mode: mode,
view: viewMain,
mainList: mkList("gitfed admin — "+domain, []list.Item{
menuItem{"Users", "manage local users & keys"},
menuItem{"Repos", "manage repositories & ACLs"},
menuItem{"Trust store", "approve/inspect remote CAs"},
menuItem{"Audit log", "recent auth & access events"},
}),
usersList: mkList("Users", nil),
reposList: mkList("Repos", nil),
aclList: mkList("Collaborators", nil),
trustList: mkList("Trust store", nil),
auditList: mkList("Audit log", nil),
}
m.refreshUsers()
m.refreshRepos()
m.refreshTrust()
m.refreshAudit()
return m
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
h := msg.Height - 4
if h < 3 {
h = 3
}
m.mainList.SetSize(msg.Width, h)
m.usersList.SetSize(msg.Width, h)
m.reposList.SetSize(msg.Width, h)
m.aclList.SetSize(msg.Width, h)
m.trustList.SetSize(msg.Width, h)
m.auditList.SetSize(msg.Width, h)
return m, nil
case statusMsg:
m.status, m.err = msg.text, msg.err
m.activeForm = nil
switch m.view {
case viewUsers:
m.refreshUsers()
case viewRepos:
m.refreshRepos()
case viewACL:
m.refreshACL()
case viewTrust:
m.refreshTrust()
case viewAudit:
m.refreshAudit()
}
return m, nil
case formCancelMsg:
m.activeForm = nil
return m, nil
case tea.KeyMsg:
if m.activeForm != nil {
f, cmd := m.activeForm.Update(msg)
m.activeForm = f
return m, cmd
}
return m.handleKey(msg)
}
return m, nil
}
func (m model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
switch m.view {
case viewMain:
switch msg.String() {
case "q":
return m, tea.Quit
case "enter":
switch m.mainList.Index() {
case 0:
m.view = viewUsers
case 1:
m.view = viewRepos
case 2:
m.view = viewTrust
case 3:
m.refreshAudit()
m.view = viewAudit
}
m.status, m.err = "", nil
return m, nil
}
var cmd tea.Cmd
m.mainList, cmd = m.mainList.Update(msg)
return m, cmd
case viewUsers:
switch msg.String() {
case "esc", "q":
m.view, m.status, m.err = viewMain, "", nil
return m, nil
case "a":
return m.openAddUserForm()
case "d":
return m.deleteSelectedUser()
}
var cmd tea.Cmd
m.usersList, cmd = m.usersList.Update(msg)
return m, cmd
case viewRepos:
switch msg.String() {
case "esc", "q":
m.view, m.status, m.err = viewMain, "", nil
return m, nil
case "a":
return m.openAddRepoForm()
case "d":
return m.deleteSelectedRepo()
case "enter":
return m.openACLForSelectedRepo()
}
var cmd tea.Cmd
m.reposList, cmd = m.reposList.Update(msg)
return m, cmd
case viewACL:
switch msg.String() {
case "esc", "q":
m.view, m.status, m.err = viewRepos, "", nil
return m, nil
case "a":
return m.openAddCollaboratorForm()
case "d":
return m.deleteSelectedCollaborator()
}
var cmd tea.Cmd
m.aclList, cmd = m.aclList.Update(msg)
return m, cmd
case viewTrust:
switch msg.String() {
case "esc", "q":
m.view, m.status, m.err = viewMain, "", nil
return m, nil
case "a":
return m.approveSelectedDomain()
}
var cmd tea.Cmd
m.trustList, cmd = m.trustList.Update(msg)
return m, cmd
case viewAudit:
switch msg.String() {
case "esc", "q":
m.view, m.status, m.err = viewMain, "", nil
return m, nil
case "r":
m.refreshAudit()
return m, nil
}
var cmd tea.Cmd
m.auditList, cmd = m.auditList.Update(msg)
return m, cmd
}
return m, nil
}
var (
errStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9"))
statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10"))
helpStyle = lipgloss.NewStyle().Faint(true)
)
func (m model) View() string {
var body string
switch m.view {
case viewMain:
body = m.mainList.View()
case viewUsers:
body = m.usersList.View()
case viewRepos:
body = m.reposList.View()
case viewACL:
body = m.aclList.View()
case viewTrust:
body = m.trustList.View()
case viewAudit:
body = m.auditList.View()
}
if m.activeForm != nil {
body = m.activeForm.View()
}
footer := ""
switch {
case m.err != nil:
footer = errStyle.Render("error: " + m.err.Error())
case m.status != "":
footer = statusStyle.Render(m.status)
}
return lipgloss.JoinVertical(lipgloss.Left, body, footer, helpStyle.Render(m.helpText()), helpStyle.Render("mode: "+m.mode))
}
func (m model) helpText() string {
if m.activeForm != nil {
return ""
}
switch m.view {
case viewMain:
return "↑/↓: navigate · enter: open · q: quit"
case viewUsers:
return "a: add user · d: delete user · esc: back"
case viewRepos:
return "a: add repo · d: delete repo · enter: manage ACL · esc: back"
case viewACL:
return "a: add collaborator · d: remove collaborator · esc: back"
case viewTrust:
return "a: approve pending domain · esc: back"
case viewAudit:
return "r: refresh · esc: back"
}
return ""
}