@@ -4960,15 +4960,15 @@ type authCheckResultMsg struct {
49604960 organization string
49614961}
49624962
4963- func newMainMenuModel (homeDir string ) mainMenuModel {
4963+ func newMainMenuModel (homeDir string , cursor int ) mainMenuModel {
49644964 return mainMenuModel {
49654965 homeDir : homeDir ,
49664966 choices : []menuChoice {
49674967 {icon : "🔄" , name : "Pull" , description : "Sync GitHub data to local database" },
49684968 {icon : "🔧" , name : "Setup" , description : "Configure GitHub username and organization" },
49694969 {icon : "🚪" , name : "Exit" , description : "Ctrl+C" },
49704970 },
4971- cursor : 0 ,
4971+ cursor : cursor ,
49724972 status : "Checking authentication..." ,
49734973 width : 80 ,
49744974 height : 24 ,
@@ -5116,8 +5116,9 @@ func RunMainTUI(homeDir string) error {
51165116 return fmt .Errorf ("failed to create home directory: %w" , err )
51175117 }
51185118
5119+ cursor := 0 // Remember cursor position across menu returns
51195120 for {
5120- m := newMainMenuModel (homeDir )
5121+ m := newMainMenuModel (homeDir , cursor )
51215122 p := tea .NewProgram (m , tea .WithAltScreen ())
51225123
51235124 finalModel , err := p .Run ()
@@ -5130,12 +5131,17 @@ func RunMainTUI(homeDir string) error {
51305131 return fmt .Errorf ("unexpected model type" )
51315132 }
51325133
5134+ cursor = mm .cursor // Remember cursor position
5135+
51335136 if mm .quitting {
51345137 return nil
51355138 }
51365139
51375140 if mm .runSetup {
51385141 if err := RunSetupMenu (homeDir , mm .username , mm .organization ); err != nil {
5142+ if err .Error () == "quit" {
5143+ return nil // Exit app cleanly
5144+ }
51395145 // Log error but continue to menu
51405146 slog .Error ("Setup failed" , "error" , err )
51415147 }
@@ -5554,19 +5560,21 @@ type AccessTokenResponse struct {
55545560
55555561// loginModel is the Bubble Tea model for the login UI
55565562type loginModel struct {
5557- spinner spinner.Model
5558- textInput textinput.Model
5559- userCode string
5560- verificationURI string
5561- status string // "waiting", "org_input", "success", "error"
5562- errorMsg string
5563- username string
5564- token string
5565- organization string
5566- homeDir string
5567- width int
5568- height int
5569- done bool
5563+ spinner spinner.Model
5564+ textInput textinput.Model
5565+ userCode string
5566+ verificationURI string
5567+ status string // "waiting", "org_input", "success", "error"
5568+ errorMsg string
5569+ username string
5570+ token string
5571+ organization string
5572+ homeDir string
5573+ width int
5574+ height int
5575+ done bool
5576+ currentUsername string // current logged-in username for title bar
5577+ currentOrg string // current organization for title bar
55705578}
55715579
55725580// Login message types
@@ -5584,7 +5592,7 @@ type (
55845592 loginOrgSubmittedMsg struct {}
55855593)
55865594
5587- func newLoginModel (homeDir string ) loginModel {
5595+ func newLoginModel (homeDir , currentUsername , currentOrg string ) loginModel {
55885596 s := spinner .New ()
55895597 s .Spinner = spinner .Dot
55905598 s .Style = lipgloss .NewStyle ().Foreground (lipgloss .Color ("12" ))
@@ -5597,12 +5605,14 @@ func newLoginModel(homeDir string) loginModel {
55975605 ti .PromptStyle = lipgloss .NewStyle ().Foreground (lipgloss .Color ("12" ))
55985606
55995607 return loginModel {
5600- spinner : s ,
5601- textInput : ti ,
5602- status : "waiting" ,
5603- homeDir : homeDir ,
5604- width : 80 ,
5605- height : 24 ,
5608+ spinner : s ,
5609+ textInput : ti ,
5610+ status : "waiting" ,
5611+ homeDir : homeDir ,
5612+ width : 80 ,
5613+ height : 24 ,
5614+ currentUsername : currentUsername ,
5615+ currentOrg : currentOrg ,
56065616 }
56075617}
56085618
@@ -5617,9 +5627,19 @@ func (m loginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
56175627 case tea.KeyMsg :
56185628 switch msg .String () {
56195629 case "ctrl+c" :
5630+ m .status = "quit"
5631+ m .done = true
5632+ return m , tea .Quit
5633+ case "esc" :
5634+ m .status = "cancelled"
56205635 m .done = true
56215636 return m , tea .Quit
56225637 case "enter" :
5638+ if m .status == "waiting" {
5639+ m .status = "cancelled"
5640+ m .done = true
5641+ return m , tea .Quit
5642+ }
56235643 if m .status == "org_input" {
56245644 m .organization = strings .TrimSpace (m .textInput .Value ())
56255645 return m , func () tea.Msg { return loginOrgSubmittedMsg {} }
@@ -5728,35 +5748,40 @@ func (m loginModel) renderWaitingView() string {
57285748 }
57295749 innerWidth := maxContentWidth - 2
57305750
5731- b .WriteString (renderTitleBar ("🔧 Setup" , "" , "" , innerWidth ) + "\n " )
5732- b .WriteString ("\n " )
5733- b .WriteString ("🔐 GitHub Authentication (OAuth)\n " )
5751+ b .WriteString (renderTitleBar ("🔧 Setup / ✨ Login with device" , m .currentUsername , m .currentOrg , innerWidth ) + "\n " )
57345752 b .WriteString ("\n " )
57355753
57365754 if m .userCode == "" {
57375755 b .WriteString (m .spinner .View () + " Requesting device code...\n " )
57385756 } else {
5739- b .WriteString ("1. Opening browser to: github.com/login/device\n " )
5757+ b .WriteString ("1. Opening browser to https:// github.com/login/device\n " )
57405758 b .WriteString ("\n " )
57415759 b .WriteString ("2. Enter this code:\n " )
57425760 b .WriteString ("\n " )
57435761
5744- // Code box with margin for alignment
5762+ // Code box with double border - gold/yellow stands out against purple
57455763 codeStyle := lipgloss .NewStyle ().
5746- Border (lipgloss .RoundedBorder ()).
5747- BorderForeground (lipgloss .Color ("12" )).
5748- Padding (0 , 3 ).
5764+ Border (lipgloss .DoubleBorder ()).
5765+ BorderForeground (lipgloss .Color ("220" )).
5766+ Foreground (lipgloss .Color ("220" )).
5767+ Padding (0 , 4 ).
57495768 Bold (true ).
57505769 MarginLeft (3 )
57515770
57525771 b .WriteString (codeStyle .Render (m .userCode ) + "\n " )
57535772 b .WriteString ("\n " )
5773+ b .WriteString ("3. Grant access to the organizations you are planning to use with GitHub Brain\n " )
5774+ b .WriteString ("\n " )
57545775 b .WriteString (m .spinner .View () + " Waiting for authorization...\n " )
57555776 }
57565777
57575778 b .WriteString ("\n " )
5758- b .WriteString ("Press Ctrl+C to cancel\n " )
5759- b .WriteString ("\n " )
5779+
5780+ // Back menu item - always selected, same format as Setup screen
5781+ selectorStyle := lipgloss .NewStyle ().Foreground (lipgloss .Color ("12" ))
5782+ selectedStyle := lipgloss .NewStyle ().Foreground (lipgloss .Color ("12" )).Bold (true )
5783+ paddedName := fmt .Sprintf ("%-4s" , "Back" )
5784+ b .WriteString (selectorStyle .Render ("▶" ) + " ← " + titleStyle .Render (paddedName ) + " " + selectedStyle .Render ("Esc" ))
57605785
57615786 return b .String ()
57625787}
@@ -5833,14 +5858,14 @@ func (m loginModel) renderErrorView() string {
58335858}
58345859
58355860// RunLogin runs the OAuth device flow login
5836- func RunLogin (homeDir string ) error {
5861+ func RunLogin (homeDir , currentUsername , currentOrg string ) error {
58375862 // Ensure home directory exists
58385863 if err := os .MkdirAll (homeDir , 0755 ); err != nil {
58395864 return fmt .Errorf ("failed to create home directory: %w" , err )
58405865 }
58415866
58425867 // Create the Bubble Tea model
5843- m := newLoginModel (homeDir )
5868+ m := newLoginModel (homeDir , currentUsername , currentOrg )
58445869 p := tea .NewProgram (m , tea .WithAltScreen ())
58455870
58465871 // Run the device flow in a goroutine
@@ -5854,9 +5879,15 @@ func RunLogin(homeDir string) error {
58545879
58555880 // Check if login was successful
58565881 if lm , ok := finalModel .(loginModel ); ok {
5882+ if lm .status == "quit" {
5883+ return fmt .Errorf ("quit" )
5884+ }
58575885 if lm .status == "error" {
58585886 return fmt .Errorf ("%s" , lm .errorMsg )
58595887 }
5888+ if lm .status == "cancelled" {
5889+ return nil // Go back without error
5890+ }
58605891 if lm .status != "success" {
58615892 return fmt .Errorf ("login cancelled" )
58625893 }
@@ -5885,18 +5916,18 @@ type setupMenuModel struct {
58855916 goBack bool
58865917}
58875918
5888- func newSetupMenuModel (homeDir , username , organization string ) setupMenuModel {
5919+ func newSetupMenuModel (homeDir , username , organization string , cursor int ) setupMenuModel {
58895920 return setupMenuModel {
58905921 homeDir : homeDir ,
58915922 username : username ,
58925923 organization : organization ,
58935924 choices : []menuChoice {
5894- {icon : "✨" , name : "Login with code " , description : "Recommended for organization owners" },
5925+ {icon : "✨" , name : "Login with device " , description : "Recommended for organization owners" },
58955926 {icon : "🔑" , name : "Login with PAT" , description : "Works without organization ownership" },
58965927 {icon : "📝" , name : "Advanced" , description : "Edit configuration file" },
5897- {icon : "🔙 " , name : "Back" , description : "Esc" },
5928+ {icon : "← " , name : "Back" , description : "Esc" },
58985929 },
5899- cursor : 0 ,
5930+ cursor : cursor ,
59005931 width : 80 ,
59015932 height : 24 ,
59025933 }
@@ -5968,17 +5999,28 @@ func (m setupMenuModel) View() string {
59685999
59696000 // Menu items - same format as Home screen
59706001 selectorStyle := lipgloss .NewStyle ().Foreground (lipgloss .Color ("12" )) // Blue selector
6002+
6003+ // Find the longest name for alignment
6004+ maxNameWidth := 0
6005+ for _ , choice := range m .choices {
6006+ if len (choice .name ) > maxNameWidth {
6007+ maxNameWidth = len (choice .name )
6008+ }
6009+ }
6010+
59716011 for i , choice := range m .choices {
59726012 cursor := " "
59736013 descStyle := dimStyle
59746014 if m .cursor == i {
59756015 cursor = selectorStyle .Render ("▶" ) + " "
59766016 descStyle = selectedStyle
59776017 }
5978- // Pad name to 15 characters for alignment
5979- paddedName := fmt .Sprintf ("%-15s" , choice .name )
6018+ // Pad icon to 2 characters (emoji width) and name for alignment
6019+ iconWidth := lipgloss .Width (choice .icon )
6020+ iconPadding := strings .Repeat (" " , 2 - iconWidth )
6021+ paddedName := fmt .Sprintf ("%-*s" , maxNameWidth , choice .name )
59806022 // Name is always bold (titleStyle), description uses current selection style
5981- b .WriteString (fmt .Sprintf ("%s%s %s %s" , cursor , choice .icon , titleStyle .Render (paddedName ), descStyle .Render (choice .description )))
6023+ b .WriteString (fmt .Sprintf ("%s%s%s %s %s" , cursor , choice .icon , iconPadding , titleStyle .Render (paddedName ), descStyle .Render (choice .description )))
59826024 if i < len (m .choices )- 1 {
59836025 b .WriteString ("\n \n " )
59846026 }
@@ -5996,8 +6038,9 @@ func (m setupMenuModel) View() string {
59966038
59976039// RunSetupMenu runs the setup submenu
59986040func RunSetupMenu (homeDir , username , organization string ) error {
6041+ cursor := 0 // Remember cursor position across menu returns
59996042 for {
6000- m := newSetupMenuModel (homeDir , username , organization )
6043+ m := newSetupMenuModel (homeDir , username , organization , cursor )
60016044 p := tea .NewProgram (m , tea .WithAltScreen ())
60026045
60036046 finalModel , err := p .Run ()
@@ -6010,12 +6053,21 @@ func RunSetupMenu(homeDir, username, organization string) error {
60106053 return fmt .Errorf ("unexpected model type" )
60116054 }
60126055
6013- if sm .quitting || sm .goBack {
6056+ cursor = sm .cursor // Remember cursor position
6057+
6058+ if sm .quitting {
6059+ return fmt .Errorf ("quit" )
6060+ }
6061+
6062+ if sm .goBack {
60146063 return nil
60156064 }
60166065
60176066 if sm .runOAuth {
6018- if err := RunLogin (homeDir ); err != nil {
6067+ if err := RunLogin (homeDir , username , organization ); err != nil {
6068+ if err .Error () == "quit" {
6069+ return err // Propagate quit to exit app
6070+ }
60196071 slog .Error ("OAuth login failed" , "error" , err )
60206072 }
60216073 // Reload .env after login
@@ -6026,6 +6078,9 @@ func RunSetupMenu(homeDir, username, organization string) error {
60266078
60276079 if sm .runPAT {
60286080 if err := RunPATLogin (homeDir ); err != nil {
6081+ if err .Error () == "quit" {
6082+ return err // Propagate quit to exit app
6083+ }
60296084 slog .Error ("PAT login failed" , "error" , err )
60306085 }
60316086 // Reload .env after login
@@ -6138,9 +6193,11 @@ func (m patLoginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
61386193 case tea.KeyMsg :
61396194 switch msg .String () {
61406195 case "ctrl+c" :
6196+ m .status = "quit"
61416197 m .done = true
61426198 return m , tea .Quit
61436199 case "esc" :
6200+ m .status = "cancelled"
61446201 m .done = true
61456202 return m , tea .Quit
61466203 case "enter" :
@@ -6359,9 +6416,15 @@ func RunPATLogin(homeDir string) error {
63596416
63606417 // Check if login was successful
63616418 if pm , ok := finalModel .(patLoginModel ); ok {
6419+ if pm .status == "quit" {
6420+ return fmt .Errorf ("quit" )
6421+ }
63626422 if pm .status == "error" {
63636423 return fmt .Errorf ("%s" , pm .errorMsg )
63646424 }
6425+ if pm .status == "cancelled" {
6426+ return nil // Go back without error
6427+ }
63656428 if pm .status != "success" {
63666429 return fmt .Errorf ("login cancelled" )
63676430 }
0 commit comments