Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162package tui
import ( "albedo/cli/internal/daemon" "errors" "regexp" "strings"
tea "charm.land/bubbletea/v2")
// webhookForm adds a hook or edits one on a single screen: the session it// wakes, its name, signing secret, and the header and prefix its sender signs// with. The session and name are fixed once the hook exists.type webhookForm struct { form // Editing is the hook being edited; nil for a new hook. Editing *webhookEntry sessionChoice}
var hookFields = []string{hookFieldSession, hookFieldName, hookFieldSecret, hookFieldHeader, hookFieldPrefix}
func newWebhookForm(editing *webhookEntry, sessions []daemon.Session, current string) *webhookForm { f := &webhookForm{form: newForm(hookFields, hookFieldSecret), sessionChoice: sessionChoice{Sessions: sessions, Current: current, Chosen: current}} f.Inputs[hookFieldSession].Placeholder = "type to filter" f.Inputs[hookFieldName].Placeholder = "github-deploys" f.Inputs[hookFieldHeader].SetValue(defaultSignatureHeader) f.Inputs[hookFieldPrefix].SetValue(defaultSignaturePrefix) f.Inputs[hookFieldSecret].Placeholder = "leave blank to generate one" f.Focus = 1 // the session already defaults to this one if editing != nil { hook := *editing f.Editing, f.Chosen, f.Focus = &hook, hook.Session, 2 f.Inputs[hookFieldName].SetValue(hook.Name) f.Inputs[hookFieldHeader].SetValue(hook.Header) f.Inputs[hookFieldPrefix].SetValue(hook.Prefix) f.Inputs[hookFieldSecret].Placeholder = "stored · leave blank to keep it" } f.focus(f.current()) return f}
func (f *webhookForm) current() string { return hookFields[f.Focus] }
// update handles one key or paste; submit reports that the form should be saved.func (f *webhookForm) update(msg tea.Msg) (submit bool, cmd tea.Cmd) { firstField := 0 if f.Editing != nil { firstField = 2 } if submit, handled := f.key(msg, hookFields, firstField); handled { return submit, nil } query := f.Inputs[hookFieldSession].Value() if key, ok := msg.(tea.KeyPressMsg); ok && f.current() == hookFieldSession && f.step(query, key.String()) { return false, nil } input := f.Inputs[f.current()] next, cmd := input.Update(msg) *input = next if f.current() == hookFieldSession { f.follow(input.Value()) } return false, cmd}
// steps validates the form before preparing its webhook mutations.func (f *webhookForm) steps() ([]daemon.WebhookRequest, string, error) { name, secret, header, prefix := f.value(hookFieldName), f.value(hookFieldSecret), f.value(hookFieldHeader), f.value(hookFieldPrefix) switch { case f.Editing == nil && f.Chosen == "": return nil, "", errors.New("choose the session this webhook will wake") case f.Editing == nil && !hookName.MatchString(name): return nil, "", errors.New("name: 1–64 letters, digits, _ or -") case secret != "" && (len(secret) < 16 || len(secret) > 4096): if f.Editing != nil { return nil, "", errors.New("secret: 16–4096 bytes, or blank to keep it") } return nil, "", errors.New("secret: 16–4096 bytes, or blank to generate") case !hookHeader.MatchString(header): return nil, "", errors.New("header: 1–64 letters, digits or -") case len(prefix) > 32 || strings.ContainsAny(prefix, "\r\n"): return nil, "", errors.New("prefix: at most 32 bytes, no newlines") } if f.Editing == nil { steps := []daemon.WebhookRequest{{Action: daemon.WebhookCreate, SessionID: f.Chosen, Name: name, Secret: secret, Header: header, Prefix: prefix}} return steps, "added " + name + " · wakes " + f.label(), nil } var steps []daemon.WebhookRequest if !strings.EqualFold(header, f.Editing.Header) || prefix != f.Editing.Prefix { steps = append(steps, daemon.WebhookRequest{Action: daemon.WebhookSignature, HookID: f.Editing.ID, Header: header, Prefix: prefix}) } if secret != "" { steps = append(steps, daemon.WebhookRequest{Action: daemon.WebhookRotate, HookID: f.Editing.ID, Secret: secret}) } return steps, "saved " + f.Editing.Name, nil}
// view is the form's rows; its keys are the footer.func (f *webhookForm) view(width int) []string { title := "add webhook" if f.Editing != nil { title = "edit " + f.Editing.Name } f.fit(width - 14) rows := []string{DefaultStyles.Bold.Render(title)} indent := strings.Repeat(" ", 13) for i, key := range hookFields { value := f.Inputs[key].View() switch { case key == hookFieldSession && f.Editing != nil: value = DefaultStyles.Faint.Render(f.label()) case key == hookFieldSession && i != f.Focus: value = f.label() case key == hookFieldName && f.Editing != nil: value = DefaultStyles.Faint.Render(f.Editing.Name) } rows = append(rows, formRow(i == f.Focus, key, 11, value, width)) if key == hookFieldSession && i == f.Focus { rows = append(rows, f.rows(f.Inputs[hookFieldSession].Value(), indent, width)...) } } return rows}
// footer explains the focused field and offers the keys every form shares.func (f *webhookForm) footer(width int) string { var own []hint var note string switch f.current() { case hookFieldSession: note, own = "which session deliveries wake", []hint{{"←→", "choose"}} case hookFieldName: note = "letters, digits, _ or -" case hookFieldSecret: note = "16+ bytes from the sender, or blank to generate one" if f.Editing != nil { note = "enter a new secret to replace the stored one" } case hookFieldHeader: note = "the header the sender signs with · GitHub uses x-hub-signature-256" case hookFieldPrefix: note = "text before the hex digest · blank for none" } keys := append(own, hint{"esc", "cancel"}, hint{"ctrl+s", "save"}, hint{"enter", "next"}, hint{"tab/↑↓", "move"}) return footerLine(width, keys, DefaultStyles.Faint.Render(note), false)}
const ( hookFieldSession = "session" hookFieldName = "name" hookFieldSecret = "secret" hookFieldHeader = "header" hookFieldPrefix = "prefix")
var ( hookName = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`) hookHeader = regexp.MustCompile(`^[A-Za-z0-9-]{1,64}$`))