Gitfed
bastien-mrq/gitfed / cmd / gitfed-tui / model.go
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 ""
}