okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
47735ec
main
1package tui 2 3import ( 4"albedo/cli/internal/config" 5"cmp" 6 7 tea"charm.land/bubbletea/v2" 8"github.com/charmbracelet/x/ansi" 9) 10 11func (m LoginModel )openBrowserCmd (urlStr string ) tea.Cmd { 12return func () tea.Msg { 13if m .openBrowser != nil { 14m .openBrowser (urlStr ) 15 } 16return nil 17 } 18} 19 20// inputWidth leaves the frame's margin and the field's label their room. 21func (m LoginModel )inputWidth ()int { 22return max (8 ,m .Width - 10 - ansi .StringWidth (m .fieldLabel ())) 23} 24 25// fieldLabel is what the text input is asked for. 26func (m LoginModel )fieldLabel ()string { 27if m .Step == StepOAuth { 28return "callback URL or code" 29 } 30label ,_ := m .question () 31return label 32} 33 34// promptInput re-arms the text input for the next question and reports the 35// blink command that shows its cursor. The placeholder is left as the 36// previous step left it. 37func (m * LoginModel )promptInput (value string ,secret bool ) tea.Cmd { 38m .TextInput .SetWidth (m .inputWidth ()) 39return ask (& m .TextInput ,value ,secret ) 40} 41 42// fail shows an error on the current step and keeps waiting for input. 43func (m LoginModel )fail (err string ) (LoginModel , tea.Cmd ) { 44m .Error = err 45return m ,nil 46} 47 48func (m * LoginModel )SetSize (width ,height int ) { 49m .Width ,m .Height = width ,height 50m .TextInput .SetWidth (m .inputWidth ()) 51m .TextInput .SetValue (m .TextInput .Value ()) 52for _ ,p := range []* loginPick {& m .ChoosePicker ,& m .ProtocolPicker ,& m .ModelPicker } { 53p .setSize (width ,height ) 54 } 55} 56 57// View is the login step in the list frame: a list for a choice, the 58// question's input in the search line for a text step, and the step's text 59// above it. Errors and progress sit in the footer. 60func (m LoginModel )View ()string { 61width := cmp .Or (m .Width ,80 ) 62title := brand ("albedo" )+ " " + DefaultStyles .Muted .Render ("/login" ) 63right := DefaultStyles .Faint .Render (m .Name ) 64if m .Step == StepChoose { 65right = DefaultStyles .Faint .Render (config .HomeDir ()) 66 } 67footer := m .footer (width ) 68if m .listing () { 69lv := m .pickerFor ().listView 70lv .Empty = "nothing to choose" 71return lv .frame (title ,right ,footer ).view (m .Width ,m .Height ) 72 } 73f := listFrame { 74title :title , 75right :right , 76filter :promptLead ()+ DefaultStyles .Muted .Render (m .fieldLabel ()+ " " )+ m .TextInput .View (), 77list :func (w ,_ int ) []string {return m .body (w ) }, 78footer :footer , 79 } 80return f .view (m .Width ,m .Height ) 81} 82 83// body is the text above a question: what it asks, what it takes, and what 84// a sign-in or save is doing. 85func (m LoginModel )body (width int ) []string { 86switch m .Step { 87case StepRemove : 88kind := m .Removing .Kind 89return []string {DefaultStyles .Bold .Render (m .Removing .Label ),DefaultStyles .Faint .Render (kind )} 90case StepSaving : 91state := "saving provider…" 92if m .Removing .Kind != "" { 93state = "removing " + m .Removing .Kind + "…" 94 } 95return []string {DefaultStyles .Faint .Render (state )} 96case StepOAuth : 97lines := []string {DefaultStyles .Muted .Render (cmp .Or (m .Status ,"starting sign-in…" ))} 98if m .LoginInstructions != "" { 99lines = append (lines ,paneNote (m .LoginInstructions ,width )... ) 100 } 101if m .SignInURL != "" { 102lines = append (lines ,"" ,DefaultStyles .Faint .Render (ansi .Hardwrap (m .SignInURL ,width ,true ))) 103 } 104lines = append (lines ,"" ) 105return append (lines ,paneNote ("browser sign-in finishes on its own; paste the callback URL or code if it stalls" ,width )... ) 106case StepModels ,StepOAuthModels : 107return []string {DefaultStyles .Faint .Render ("loading models…" )} 108 } 109_ ,notes := m .question () 110var lines []string 111for _ ,note := range notes { 112lines = append (lines ,paneNote (note ,width )... ) 113 } 114return lines 115} 116 117// question is the label a text step asks for, and what it says beside it. 118func (m LoginModel )question () (string , []string ) { 119switch m .Step { 120case StepName : 121var notes []string 122for _ ,login := range m .SignIns { 123notes = append (notes ,"use " + login .Provider + " to sign in" ) 124 } 125return "provider name" ,notes 126case StepBaseURL : 127if m .optionalKey () { 128return "API base URL" , []string {"blank uses your daemon's environment" } 129 } 130return "API base URL" ,nil 131case StepAPIKey : 132return "API key" ,nil 133case StepProject : 134return "Google Cloud project ID" ,nil 135case StepLocation : 136return "Vertex location" ,nil 137case StepModel : 138return "model ID" ,nil 139case StepOAuthFields : 140if m .LoginFieldIndex < len (m .LoginFields ) { 141field := m .LoginFields [m .LoginFieldIndex ] 142return field .Label , []string {field .Description } 143 } 144 } 145return "" ,nil 146} 147 148// hints are the keys for the step, most important first. 149func (m LoginModel )hints () []hint { 150switch { 151case m .Step == StepSaving : 152return nil 153case (m .Step == StepModels || m .Step == StepOAuthModels )&& ! m .listing (): 154return []hint {{"esc" ,"back" }} 155case m .listing (): 156hints := []hint {{"↑↓" ,"move" }, {"enter" ,m .enterVerb ()}} 157if item ,ok := m .ChoosePicker .Highlighted ();ok && m .Step == StepChoose { 158if _ ,removable := m .removalFor (item .ID );removable { 159hints = append (hints ,hint {"ctrl+d" ,"remove" }) 160 } 161 } 162return append (hints ,hint {"esc" ,"back" }) 163case m .Step == StepOAuth : 164return []hint {{"enter" ,"submit code" }, {"esc" ,"cancel" }} 165case m .Step == StepAPIKey && m .Draft .APIKey != "" : 166return []hint {{"enter" ,"keeps the saved key" }, {"esc" ,"cancel" }} 167 } 168return []hint {{"enter" ,"continue" }, {"esc" ,"cancel" }} 169} 170 171// enterVerb says what enter does on the list the step shows. 172func (m LoginModel )enterVerb ()string { 173switch m .Step { 174case StepAccountProfile : 175return "use" 176case StepOAuthFlow : 177return "sign in" 178case StepModels ,StepOAuthModels : 179return "use model" 180 } 181return "choose" 182} 183 184// footer is the keys, with an error on a row of its own above them so the 185// keys keep their labels. 186func (m LoginModel )footer (width int )string { 187if m .Step == StepRemove { 188return m .confirm .footer (width ,m .Error ) 189 } 190if m .Error != "" { 191return " " + DefaultStyles .Error .Render (ansi .Truncate (m .Error ,max (1 ,width - 2 ),"…" ))+ "\n" + footerLine (width ,m .hints (),"" ,false ) 192 } 193status := "" 194if m .CatalogNote != "" && (m .Step == StepModels || m .Step == StepOAuthModels ) { 195status = DefaultStyles .Faint .Render (m .CatalogNote ) 196 }else if m .Status != "" && m .Step == StepChoose { 197status = DefaultStyles .Faint .Render (m .Status ) 198 } 199return footerLine (width ,m .hints (),status ,false ) 200}