66 "strconv"
77 "strings"
88
9- tea "github.com/charmbracelet/bubbletea"
109 "github.com/charmbracelet/bubbles/viewport"
10+ tea "github.com/charmbracelet/bubbletea"
1111 "github.com/charmbracelet/lipgloss"
1212)
1313
@@ -17,7 +17,7 @@ import (
1717// When a valid range is submitted, it transitions the model to fetch and display the diff.
1818func (m Model ) handleGitRangeInput (msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
1919 switch msg .String () {
20- case "enter" :
20+ case keyEnter :
2121 val := strings .TrimSpace (m .gitRangeInput .Value ())
2222 m .mode = modeNormal
2323 m .gitRangeInput .Blur ()
@@ -28,7 +28,7 @@ func (m Model) handleGitRangeInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
2828 m , ctx = resetForNewStream (m )
2929 m .diffSrc = diffSourceRange
3030 return m , fetchDiffCmd (ctx , val , m .diffFetchGen )
31- case "esc" :
31+ case keyEsc :
3232 m .mode = modeNormal
3333 m .gitRangeInput .Blur ()
3434 return m , nil
@@ -46,7 +46,7 @@ func (m Model) handleGitRangeInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
4646// If the prompt is empty or no diff content is loaded, an error is returned.
4747func (m Model ) handlePromptInput (msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
4848 switch msg .String () {
49- case "enter" :
49+ case keyEnter :
5050 val := strings .TrimSpace (m .promptInput .Value ())
5151 m .mode = modeNormal
5252 m .promptInput .Blur ()
@@ -61,7 +61,7 @@ func (m Model) handlePromptInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
6161 m .promptNoPersona = false
6262 m = resetForNewReview (m )
6363 return m , startStreamCmd (m .activeHarness , m .buildFullPrompt (), m .diffContent )
64- case "esc" :
64+ case keyEsc :
6565 m .mode = modeNormal
6666 m .promptInput .Blur ()
6767 return m , nil
@@ -84,7 +84,7 @@ func (m Model) handlePromptInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
8484// Returns the updated model and any resulting command.
8585func (m Model ) handleTildeInput (msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
8686 switch msg .String () {
87- case "enter" :
87+ case keyEnter :
8888 val := strings .TrimSpace (m .tildeInput .Value ())
8989 m .mode = modeNormal
9090 m .tildeInput .Blur ()
@@ -101,7 +101,7 @@ func (m Model) handleTildeInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
101101 m , ctx = resetForNewStream (m )
102102 m .diffSrc = diffSourceRange
103103 return m , fetchDiffCmd (ctx , m .gitRange , m .diffFetchGen )
104- case "esc" :
104+ case keyEsc :
105105 m .mode = modeNormal
106106 m .tildeInput .Blur ()
107107 return m , nil
@@ -119,24 +119,24 @@ func (m Model) handleTildeInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
119119// to normal mode.
120120func (m Model ) handleLibraryInput (msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
121121 switch msg .String () {
122- case "enter" :
122+ case keyEnter :
123123 entry := PromptLibrary [m .libraryIndex ]
124124 m .prompt = entry .Prompt
125125 m .promptNoPersona = entry .NoPersona
126126 m .mode = modeNormal
127127 m = resetForNewReview (m )
128128 return m , startStreamCmd (m .activeHarness , m .buildFullPrompt (), m .diffContent )
129- case "esc" :
129+ case keyEsc :
130130 m .mode = modeNormal
131131 m .reviewViewport .SetContent (m .renderMarkdown (m .reviewContent .String ()))
132132 return m , nil
133- case "j" , "down" :
133+ case keyScrollDown , keyDown :
134134 if m .libraryIndex < len (PromptLibrary )- 1 {
135135 m .libraryIndex ++
136136 }
137137 m .reviewViewport .SetContent (m .renderLibraryList ())
138138 return m , nil
139- case "k" , "up" :
139+ case keyScrollUp , keyUp :
140140 if m .libraryIndex > 0 {
141141 m .libraryIndex --
142142 }
@@ -189,7 +189,7 @@ func (m Model) handlePersonaInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
189189 // Index 0 = "(None)", 1 = "(Critical Only)", 2 = "(Terse)", 3..len(Personas)+2 = personas
190190 maxIndex := len (Personas ) + 2
191191 switch msg .String () {
192- case "enter" :
192+ case keyEnter :
193193 m .mode = modeNormal
194194 switch m .personaIndex {
195195 case 0 :
@@ -208,19 +208,19 @@ func (m Model) handlePersonaInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
208208 }
209209 m = resetForNewReview (m )
210210 return m , tea .Batch (saveCmd , startStreamCmd (m .activeHarness , m .buildFullPrompt (), m .diffContent ))
211- case "esc" :
211+ case keyEsc :
212212 m .mode = modeNormal
213213 m .reviewViewport .SetContent (m .renderMarkdown (m .reviewContent .String ()))
214214 return m , nil
215- case "j" , "down" :
215+ case keyScrollDown , keyDown :
216216 if m .personaIndex < maxIndex {
217217 m .personaIndex ++
218218 }
219219 content , sel := m .renderPersonaList ()
220220 m .reviewViewport .SetContent (content )
221221 scrollPersonaViewport (& m .reviewViewport , sel )
222222 return m , nil
223- case "k" , "up" :
223+ case keyScrollUp , keyUp :
224224 if m .personaIndex > 0 {
225225 m .personaIndex --
226226 }
@@ -299,12 +299,12 @@ func scrollPersonaViewport(vp *viewport.Model, selectedLine int) {
299299// state, returning to normal mode.
300300func (m Model ) handleConfirmLargeDiff (msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
301301 switch msg .String () {
302- case "enter" :
302+ case keyEnter :
303303 m .pendingDiff = nil
304304 m .mode = modeNormal
305305 m = resetForNewReview (m )
306306 return m , startStreamCmd (m .activeHarness , m .buildFullPrompt (), m .diffContent )
307- case "esc" :
307+ case keyEsc :
308308 m .pendingDiff = nil
309309 m .mode = modeNormal
310310 m .diffContent = ""
@@ -319,25 +319,47 @@ func (m Model) handleConfirmLargeDiff(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
319319 }
320320}
321321
322+ // handleHelpInput processes keyboard input while the help menu is visible. Esc,
323+ // q, Ctrl-C, Help, Atl-Help F1 or ? closes the overlay and restores the current
324+ // review content. j/k and up/down are passed through so users can scroll long
325+ // help pages.
326+ func (m Model ) handleHelpInput (msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
327+ switch msg .String () {
328+ case keyEsc , keyQuit , keyHelp , keyHelpAlt , keyCtrlC , keyF1 :
329+ m .mode = modeNormal
330+ m .reviewViewport .SetContent (m .renderMarkdown (m .reviewContent .String ()))
331+ if m .autoScroll {
332+ m .reviewViewport .GotoBottom ()
333+ }
334+ return m , nil
335+ case keyScrollDown , keyDown , keyScrollUp , keyUp :
336+ var cmd tea.Cmd
337+ m .reviewViewport , cmd = m .reviewViewport .Update (msg )
338+ return m , cmd
339+ default :
340+ return m , nil
341+ }
342+ }
343+
322344// handleHarnessInput processes keyboard input when the TUI is in harness selection mode.
323345func (m Model ) handleHarnessInput (msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
324346 switch msg .String () {
325- case "enter" :
347+ case keyEnter :
326348 m .activeHarness = m .availableHarnesses [m .harnessIndex ].WithModel (m .modelName )
327349 m .mode = modeNormal
328350 m .reviewViewport .SetContent (m .renderMarkdown (m .reviewContent .String ()))
329351 return m , saveProfileCmd (m )
330- case "esc" :
352+ case keyEsc :
331353 m .mode = modeNormal
332354 m .reviewViewport .SetContent (m .renderMarkdown (m .reviewContent .String ()))
333355 return m , nil
334- case "j" , "down" :
356+ case keyScrollDown , keyDown :
335357 if m .harnessIndex < len (m .availableHarnesses )- 1 {
336358 m .harnessIndex ++
337359 }
338360 m .reviewViewport .SetContent (m .renderHarnessList ())
339361 return m , nil
340- case "k" , "up" :
362+ case keyScrollUp , keyUp :
341363 if m .harnessIndex > 0 {
342364 m .harnessIndex --
343365 }
@@ -365,3 +387,65 @@ func (m Model) renderHarnessList() string {
365387 }
366388 return b .String ()
367389}
390+
391+ // renderHelp builds the full keybinding help text shown in help mode.
392+ func (m Model ) renderHelp () string {
393+ var b strings.Builder
394+ headerStyle := lipgloss .NewStyle ().
395+ Bold (true ).
396+ Foreground (lipgloss .Color ("229" ))
397+ sectionStyle := lipgloss .NewStyle ().
398+ Bold (true ).
399+ Foreground (lipgloss .Color ("63" ))
400+ noteStyle := lipgloss .NewStyle ().
401+ Foreground (lipgloss .Color ("240" ))
402+
403+ b .WriteString (headerStyle .Render ("Keyboard Shortcuts" ))
404+ b .WriteString ("\n " )
405+ b .WriteString (noteStyle .Render (fmt .Sprintf ("Press [%s], [%s], [%s], %s, or [%s] to close" , keyEsc , keyQuit , keyCtrlC , keyHelp , keyF1 )))
406+ b .WriteString ("\n \n " )
407+
408+ b .WriteString (sectionStyle .Render ("Normal Mode" ) + "\n " )
409+ b .WriteString (fmt .Sprintf ("[%s] switch focused pane\n " , keyTab ))
410+ b .WriteString (fmt .Sprintf ("[%s/%s/%s/%s] scroll focused pane\n " , keyScrollDown , keyScrollUp , keyDown , keyUp ))
411+ b .WriteString (fmt .Sprintf ("[%s] zoom in/out the active pane\n " , keyZoom ))
412+ b .WriteString (fmt .Sprintf ("[%s/%s] open this cheat sheet\n " , keyHelp , keyF1 ))
413+ b .WriteString (fmt .Sprintf ("[%s] / [%s] quit app\n " , keyQuit , keyCtrlC ))
414+ b .WriteString ("\n " )
415+
416+ b .WriteString (sectionStyle .Render ("Review Actions" ) + "\n " )
417+ b .WriteString (fmt .Sprintf ("[%s] set a custom prompt\n " , keyPrompt ))
418+ b .WriteString (fmt .Sprintf ("[%s] open prompt library\n " , keyLibrary ))
419+ b .WriteString (fmt .Sprintf ("[%s] choose review persona\n " , keyPersona ))
420+ b .WriteString (fmt .Sprintf ("[%s] cycle review model\n " , keyModel ))
421+ b .WriteString (fmt .Sprintf ("[%s] copy focused pane\n " , keyCopy ))
422+ if len (m .availableHarnesses ) > 1 {
423+ b .WriteString (fmt .Sprintf ("[%s] choose review harness\n " , keyHarness ))
424+ }
425+ b .WriteString ("\n " )
426+
427+ b .WriteString (sectionStyle .Render ("Diff Source" ) + "\n " )
428+ b .WriteString (fmt .Sprintf ("[%s] set an arbitrary git range\n " , keyGitRange ))
429+ b .WriteString (fmt .Sprintf ("[%s] open HEAD~n..HEAD picker\n " , keyTilde ))
430+ b .WriteString (fmt .Sprintf ("[%s] review last commit\n " , keyLastCommit ))
431+ b .WriteString (fmt .Sprintf ("[%s] review staged changes\n " , keyStaged ))
432+ b .WriteString (fmt .Sprintf ("[%s] review uncommitted changes\n " , keyDirty ))
433+ b .WriteString (fmt .Sprintf ("[%s] review upstream diff\n " , keyUpstream ))
434+ b .WriteString (fmt .Sprintf ("[%s] refresh current diff\n " , keyRefresh ))
435+ b .WriteString ("\n " )
436+
437+ b .WriteString (sectionStyle .Render ("Input Modes" ) + "\n " )
438+ b .WriteString (fmt .Sprintf ("git range (%s): [%s] fetch | [%s] cancel\n " , keyGitRange , keyEnter , keyEsc ))
439+ b .WriteString (fmt .Sprintf ("prompt (%s): [%s] run review | [%s] cancel\n " , keyPrompt , keyEnter , keyEsc ))
440+ b .WriteString (fmt .Sprintf ("HEAD~n (%s): [%s] fetch | [%s] cancel\n " , keyTilde , keyEnter , keyEsc ))
441+ b .WriteString (fmt .Sprintf ("library/persona/harness: [%s/%s/%s/%s] navigate | [%s] select | [%s] cancel\n " , keyScrollDown , keyScrollUp , keyDown , keyUp , keyEnter , keyEsc ))
442+ b .WriteString (fmt .Sprintf ("large diff confirm: [%s] continue | [%s] cancel\n " , keyEnter , keyEsc ))
443+ b .WriteString (fmt .Sprintf ("help mode: [%s/%s/%s/%s] scroll | [%s/%s/%s/%s/%s] close\n " , keyScrollDown , keyScrollUp , keyDown , keyUp , keyEsc , keyQuit , keyCtrlC , keyHelp , keyF1 ))
444+ b .WriteString ("\n " )
445+
446+ b .WriteString (sectionStyle .Render ("Notes" ) + "\n " )
447+ b .WriteString (noteStyle .Render (fmt .Sprintf ("If only one harness is configured, [%s] will not be shown." , keyHarness )))
448+ b .WriteString ("\n " )
449+
450+ return b .String ()
451+ }
0 commit comments