package gitctl import ( "fmt" "strings" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" ) type detailKind int const ( detailRepo detailKind = iota detailNS detailKey ) type detailModel struct { kind detailKind repo repoRow ns nsRow key sshKeyRow grants []grantRow list list.Model loaded bool permissions bool err string w, h int } func newNSDetail(ns nsRow, w, h int) detailModel { d := detailModel{kind: detailNS, ns: ns, w: w, h: h} d.list = newListModel(nil, w, listHeight(h)) return d } func newRepoDetail(repo repoRow, w, h int) detailModel { d := detailModel{kind: detailRepo, repo: repo, w: w, h: h} d.list = newListModel(nil, w, listHeight(h)) return d } func newKeyDetail(key sshKeyRow, w, h int) detailModel { d := detailModel{kind: detailKey, key: key, w: w, h: h} d.list = newListModel(nil, w, listHeight(h)) return d } func (d detailModel) resize(w, h int) detailModel { d.w, d.h = w, h d.list.SetSize(w, listHeight(h)) return d } func (d detailModel) withGrants(grants []grantRow, errStr string) detailModel { d.grants = grants d.err = errStr d.loaded = true d.list = newListModel(grantItems(grants), d.w, listHeight(d.h)) return d } func (d detailModel) selectedGrant() (grantRow, bool) { item := d.list.SelectedItem() if item == nil { return grantRow{}, false } g, ok := item.(grantItem) return g.g, ok } func (d detailModel) Update(msg tea.Msg) (detailModel, tea.Cmd) { if d.permissions && d.list.FilterState() == list.Filtering { var cmd tea.Cmd d.list, cmd = d.list.Update(msg) return d, cmd } k, ok := msg.(tea.KeyMsg) if !ok { var cmd tea.Cmd d.list, cmd = d.list.Update(msg) return d, cmd } switch k.String() { case "ctrl+c", "q": return d, tea.Quit case "esc": if d.permissions && d.list.FilterState() == list.FilterApplied { d.list.ResetFilter() return d, nil } if d.permissions && d.kind != detailKey { d.permissions = false return d, nil } return d, func() tea.Msg { return detailBackMsg{} } } if !d.permissions && d.kind != detailKey && (k.String() == "a" || k.String() == "enter") { d.permissions = true return d, nil } if cmd, handled := d.handleQuickKey(k); handled { return d, cmd } if d.permissions || d.kind == detailKey { var cmd tea.Cmd d.list, cmd = d.list.Update(msg) return d, cmd } return d, nil } func (d detailModel) handleQuickKey(k tea.KeyMsg) (tea.Cmd, bool) { path := d.repo.Ns + "/" + d.repo.Name switch d.kind { case detailNS: if !d.permissions { switch k.String() { case "m": return emitOp(opRequest{a: opMoveNS(d.ns.Name), ret: "list"}), true case "d": return emitOp(opRequest{a: opDeleteNS(d.ns.Name), ret: "list"}), true } return nil, false } switch k.String() { case "g": return emitOp(opRequest{a: opGrantSSHKey(), prefill: map[string]string{"target": d.ns.Name}, focus: 1, ret: "detail"}), true case "p": return emitOp(opRequest{a: opGrantPAT(), prefill: map[string]string{"target": d.ns.Name}, focus: 1, ret: "detail"}), true case "c": return emitOp(opRequest{a: opChangePassword(d.ns.Name), ret: "detail"}), true case "u": if !d.ns.HasPassword { return nil, true } return emitOp(opRequest{a: opRemovePassword(d.ns.Name), ret: "detail"}), true case "x": g, ok := d.selectedGrant() if !ok { return nil, true } return emitOp(opRequest{a: opACLDelete(g.AclID), ret: "detail"}), true case "r": g, ok := d.selectedGrant() if !ok { return nil, true } return emitOp(opRequest{a: opRotatePerm(g), ret: "detail"}), true } case detailRepo: if !d.permissions { switch k.String() { case "v": return emitOp(opRequest{a: opToggleRepoVisibility(d.repo), ret: "detail"}), true case "d": return emitOp(opRequest{a: opDeleteRepo(d.repo.Ns, d.repo.Name), ret: "list"}), true case "m": return emitOp(opRequest{a: opMoveRepo(path), focus: 0, ret: "list"}), true } return nil, false } switch k.String() { case "g": return emitOp(opRequest{a: opGrantSSHKey(), prefill: map[string]string{"target": path}, focus: 1, ret: "detail"}), true case "p": return emitOp(opRequest{a: opGrantPAT(), prefill: map[string]string{"target": path}, focus: 1, ret: "detail"}), true case "x": g, ok := d.selectedGrant() if !ok { return nil, true } return emitOp(opRequest{a: opACLDelete(g.AclID), ret: "detail"}), true case "r": g, ok := d.selectedGrant() if !ok { return nil, true } return emitOp(opRequest{a: opRotatePerm(g), ret: "detail"}), true } case detailKey: switch k.String() { case "a": line, err := reconstructAuthorizedLine(d.key.PublicKey) if err != nil { return nil, true } return emitOp(opRequest{a: opGrantSSHKey(), prefill: map[string]string{"key": line}, focus: 0, ret: "detail"}), true case "x": g, ok := d.selectedGrant() if !ok { return nil, true } return emitOp(opRequest{a: opACLDelete(g.AclID), ret: "detail"}), true case "r": g, ok := d.selectedGrant() if !ok { return nil, true } return emitOp(opRequest{a: opRotatePerm(g), ret: "detail"}), true } } return nil, false } func (d detailModel) View() string { if !d.permissions && d.kind != detailKey { return d.infoView() } var b strings.Builder switch d.kind { case detailNS: b.WriteString(detailTitleStyle.Render("gitctl · Namespace Permissions · " + d.ns.Name)) b.WriteString("\n\n " + fieldLabelStyle.Render(fmt.Sprintf("Grants (%d):", len(d.grants))) + "\n") case detailRepo: b.WriteString(detailTitleStyle.Render("gitctl · Repo Permissions · " + d.repo.Ns + "/" + d.repo.Name)) b.WriteString("\n\n " + fieldLabelStyle.Render(fmt.Sprintf("Grants (%d):", len(d.grants))) + "\n") case detailKey: c := d.key.Comment if c == "" { c = "(no comment)" } b.WriteString(detailTitleStyle.Render("gitctl · SSH Key · " + c)) b.WriteString("\n") last := "-" if d.key.LastUsedAt != nil { last = fmtTime(*d.key.LastUsedAt) } b.WriteString(" " + fieldLabelStyle.Render("type:") + " " + d.key.KeyType + " " + fieldLabelStyle.Render("fingerprint:") + " " + shortFP(d.key.Fingerprint) + " " + fieldLabelStyle.Render("last used:") + " " + last) b.WriteString("\n\n " + fieldLabelStyle.Render(fmt.Sprintf("Grants (%d):", len(d.grants))) + "\n") } b.WriteString(d.grantsBody()) b.WriteString("\n\n " + hintStyle.Render(d.hintLine())) return frame.Render(b.String()) } func (d detailModel) infoView() string { var b strings.Builder switch d.kind { case detailNS: password := "not configured" if d.ns.HasPassword { password = "enabled" } b.WriteString(detailTitleStyle.Render("gitctl · Namespace · " + d.ns.Name)) b.WriteString("\n\n " + fieldLabelStyle.Render("repos:") + fmt.Sprintf(" %d", d.ns.RepoCount)) b.WriteString(" " + fieldLabelStyle.Render("password:") + " " + password) b.WriteString("\n " + fieldLabelStyle.Render("created:") + " " + fmtTime(d.ns.Created)) b.WriteString(" " + fieldLabelStyle.Render("updated:") + " " + fmtTime(d.ns.Updated)) b.WriteString("\n\n " + hintStyle.Render("a/enter permissions · m rename · d delete · esc back")) case detailRepo: visibility := "public" if d.repo.IsPrivate { visibility = "private" } b.WriteString(detailTitleStyle.Render("gitctl · Repo · " + d.repo.Ns + "/" + d.repo.Name)) b.WriteString("\n\n " + fieldLabelStyle.Render("visibility:") + " " + visibility) b.WriteString("\n " + fieldLabelStyle.Render("created:") + " " + fmtTime(d.repo.Created)) b.WriteString(" " + fieldLabelStyle.Render("updated:") + " " + fmtTime(d.repo.Updated)) b.WriteString("\n\n " + hintStyle.Render("a/enter permissions · v private/public · m move · d delete · esc back")) } return frame.Render(b.String()) } func (d detailModel) grantsBody() string { if d.err != "" { return " " + errStyle.Render("✗ "+d.err) } if !d.loaded { return " " + hintStyle.Render("loading…") } if len(d.grants) == 0 { return " " + hintStyle.Render("(no access grants)") } return d.list.View() } func (d detailModel) hintLine() string { switch d.kind { case detailNS: return "g add ssh · p add pat · c set password · u remove password\n x revoke · r permission · / search · esc details" case detailRepo: return "g add ssh · p add pat · x revoke · r permission · / search · esc details" case detailKey: return "a grant · x revoke · r permission · / search · esc back" } return "esc back" }