okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
f51cc1a
main
1package tui 2 3import ( 4"cmp" 5"fmt" 6) 7 8// View draws the list beside the highlighted hook's detail. The form and the 9// secret shown once take the list's place, and a question takes the footer. 10func (m WebhooksPageModel )View ()string { 11lv := m .listView 12lv .Empty = "No webhooks yet. Add one to wake a session with a signed request." 13switch { 14case m .Loading : 15lv .Empty = "loading webhooks…" 16case ! m .Loaded : 17lv .Empty = "webhooks did not load" 18 } 19frame := lv .frame (brand ("albedo" )+ " " + DefaultStyles .Muted .Render ("/webhooks" ),DefaultStyles .Faint .Render ("all sessions" ),m .footer (max (1 ,m .Width ))) 20switch { 21case m .Reveal != nil : 22frame .filter ,frame .pane ,frame .list = "" ,nil ,m .revealRows 23case m .Form != nil : 24frame .filter ,frame .pane ,frame .list = "" ,nil ,m .formRows 25 } 26return frame .view (m .Width ,m .Height ) 27} 28 29// footer is the keys for what is open and one status: a failure, a notice, 30// work in flight, or what the hooks are doing. 31func (m WebhooksPageModel )footer (width int )string { 32switch { 33case m .Reveal != nil : 34status ,urgent := m .status () 35return footerLine (width , []hint {{"ctrl+l" ,"copy" }, {"enter" ,"done" }},status ,urgent ) 36case m .Form != nil : 37return m .formFooter (width ) 38case m .confirm .asking (): 39return m .confirm .footer (width ,m .Error ) 40 } 41status ,urgent := m .status () 42return footerLine (width ,m .browseHints (),status ,urgent ) 43} 44 45// status is the footer's state beside the keys. An urgent status takes a row 46// of its own when the keys do not leave room for it. 47func (m WebhooksPageModel )status () (string ,bool ) { 48switch { 49case m .Error != "" : 50return DefaultStyles .Error .Render (m .Error ),true 51case m .Saving : 52return DefaultStyles .Busy .Render ("saving…" ),true 53case m .Loading : 54return DefaultStyles .Faint .Render ("loading…" ),false 55case m .Notice != "" : 56return DefaultStyles .Warning .Render (m .Notice ),true 57case ! m .Mounted : 58return DefaultStyles .Warning .Render ("Webhooks are not listening · enable them in /extensions" ),true 59 } 60return "" ,false 61} 62 63// browseHints keeps the main hook action and session agent access visible first. 64func (m WebhooksPageModel )browseHints () []hint { 65if ! m .Loaded { 66return []hint {{"ctrl+r" ,"retry" }, {"esc" ,"back" }} 67 } 68_ ,selected := m .current () 69var hints []hint 70if selected { 71hints = append (hints ,hint {"enter" ,"on/off" }) 72 } 73hints = append (hints ,hint {"ctrl+t" ,"agent access " + onOff (m .AgentManagement )},hint {"ctrl+o" ,"add hook" }) 74if selected { 75hints = append (hints ,hint {"ctrl+e" ,"edit" },hint {"ctrl+d" ,"delete" },hint {"ctrl+g" ,"new secret" },hint {"ctrl+l" ,"copy url" }) 76 } 77return append (hints ,hint {"ctrl+r" ,"refresh" },hint {"esc" ,"back" }) 78} 79 80// formFooter is the form's keys, with a failure or the save in flight above them. 81func (m WebhooksPageModel )formFooter (width int )string { 82line := m .Form .footer (width ) 83switch { 84case m .Error != "" : 85return footerLine (width ,nil ,DefaultStyles .Error .Render (m .Error ),true )+ "\n" + line 86case m .Saving : 87return footerLine (width ,nil ,DefaultStyles .Busy .Render ("saving…" ),true )+ "\n" + line 88 } 89return line 90} 91 92// formRows is the form in the list's place, scrolled to keep its focused field in view. 93func (m WebhooksPageModel )formRows (width ,height int ) []string { 94return scrolled (m .Form .view (width ),1 + m .Form .Focus ,height ) 95} 96 97// revealRows is the generated secret, shown once, in the list's place. Its 98// session and warning sit on rows of their own so a narrow terminal keeps them. 99func (m WebhooksPageModel )revealRows (_ ,_ int ) []string { 100r := m .Reveal 101return []string { 102DefaultStyles .Bold .Render ("signing secret for " + r .Hook ), 103DefaultStyles .Faint .Render ("→ " + sessionLabel (m .Sessions ,r .Session ,m .SessionID )), 104DefaultStyles .Warning .Render ("shown only once; copy it now" ), 105"" , 106" " + DefaultStyles .Prompt .Render (r .Secret ), 107 } 108} 109 110// entry is how a hook reads in the list: its state in front, its URL under 111// its name, and the deliveries waiting for its session at the edge. 112func (m WebhooksPageModel )entry (hook webhookEntry )listEntry { 113lead ,tag := DefaultStyles .Faint .Render ("off" ),"" 114if hook .Enabled { 115lead = DefaultStyles .Success .Render ("on" ) 116 } 117if hook .Queued > 0 { 118tag = DefaultStyles .Warning .Render (fmt .Sprintf ("%d queued" ,hook .Queued )) 119 } 120section := sessionLabel (m .Sessions ,hook .Session ,m .SessionID ) 121return listEntry { 122key :hook .ID , 123section :section , 124lead :lead , 125name :hook .Name , 126desc :hook .URL , 127tag :tag , 128search : []string {section }, 129detail :func (width int ) []string {return m .details (hook ,width ) }, 130 } 131} 132 133// details is the pane: the state, where deliveries go, how they are signed, 134// and what is waiting for the session. 135func (m WebhooksPageModel )details (hook webhookEntry ,width int ) []string { 136state := "off · new deliveries answer 404" 137if hook .Enabled { 138state = "on · accepting signed deliveries" 139 } 140lines := paneTitle (hook .Name ,state ,width ) 141lines = append (lines ,factRows ("wakes" ,sessionLabel (m .Sessions ,hook .Session ,m .SessionID ),width )... ) 142lines = append (lines ,factRows ("url" ,"POST " + cmp .Or (hook .Address ,hook .URL ),width )... ) 143lines = append (lines ,factRows ("signature" ,hook .Header + ": " + hook .Prefix + "<hex HMAC-SHA256 of the body>" ,width )... ) 144lines = append (lines ,factRows ("secret" ,"stored · ctrl+g makes a new one" ,width )... ) 145inbox := "empty" 146if hook .Queued > 0 { 147inbox = fmt .Sprintf ("%d waiting for the session" ,hook .Queued ) 148if hook .Deferred != "" { 149inbox += " · last attempt: " + hook .Deferred 150 } 151 } 152return append (lines ,factRows ("inbox" ,inbox ,width )... ) 153} 154 155// scrolled is the window of rows lines that keeps line at in view. 156func scrolled (lines []string ,at ,rows int ) []string { 157start := max (0 ,at - rows + 1 ) 158return lines [start :min (len (lines ),start + rows )] 159}