make keybinds configurable and consolidate

This commit is contained in:
2026-07-28 14:55:38 +02:00
parent bb8e91039f
commit 948f3e1a79
12 changed files with 1469 additions and 158 deletions

358
tui.go
View File

@@ -135,6 +135,7 @@ type App struct {
dashboardReturn screen
compactReviews bool
editorMode string
keybindings KeyBindings
readState *readStateStore
knownThreads map[string]bool
knownComments map[string]bool
@@ -153,6 +154,7 @@ type AppSettings struct {
DashboardMode string
CompactReviews bool
EditorMode string
KeyBindings KeyBindings
ReadState *readStateStore
}
@@ -166,6 +168,7 @@ func defaultAppSettings() AppSettings {
DashboardMode: "hotkey",
CompactReviews: true,
EditorMode: "vim",
KeyBindings: defaultKeyBindings(),
}
}
@@ -195,6 +198,7 @@ func NewAppWithSettings(
dashboardMode: settings.DashboardMode, dashboardReturn: prScreen,
compactReviews: settings.CompactReviews,
editorMode: settings.EditorMode,
keybindings: settings.KeyBindings,
readState: state,
knownThreads: make(map[string]bool), knownComments: make(map[string]bool),
initializedPRs: make(map[string]bool), unreadThreads: make(map[string]bool),
@@ -328,10 +332,11 @@ func (m App) writeActionUnavailable(action string, thread *ReviewThread) string
}
func (m App) updateWriteInput(key tea.KeyMsg) (tea.Model, tea.Cmd) {
k := key.String()
if k == "ctrl+c" {
if key.Type != tea.KeyRunes && key.Type != tea.KeySpace &&
keyMatches(key.String(), m.keybindings.General.Quit) {
return m, tea.Quit
}
k := m.keybindings.canonicalWriteKey(key.String())
switch m.writeMode {
case writeReply:
switch k {
@@ -611,10 +616,8 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
k := key.String()
if m.writeMode != writeNone {
return m.updateWriteInput(key)
}
if m.helpVisible {
k = m.keybindings.canonicalHelpKey(k)
switch k {
case "ctrl+c":
return m, tea.Quit
@@ -635,7 +638,25 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, nil
}
if m.writeMode == writePREdit || m.writeMode == writePREditConfirm {
editor := m.prEditEditors[m.prEditField]
helpOutsideTextInput := key.Type != tea.KeyRunes && key.Type != tea.KeySpace
helpInVimNormalMode := editor.Modal && editor.Mode == textEditorNormal
if keyMatches(k, m.keybindings.General.Help) &&
(helpOutsideTextInput || helpInVimNormalMode) {
m.helpVisible, m.helpScroll = true, 0
return m, nil
}
}
if m.writeMode != writeNone {
return m.updateWriteInput(key)
}
if m.searching {
if key.Type != tea.KeyRunes && key.Type != tea.KeySpace &&
keyMatches(k, m.keybindings.General.Quit) {
return m, tea.Quit
}
k = m.keybindings.canonicalSearchKey(k)
switch k {
case "ctrl+c":
return m, tea.Quit
@@ -667,6 +688,8 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, nil
}
rawKey := k
k = m.keybindings.canonicalMainKey(k, m.screen)
if k == "ctrl+c" || k == "q" {
return m, tea.Quit
}
@@ -676,7 +699,8 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
if m.pendingZ {
m.pendingZ = false
if k == "a" && m.screen == threadScreen && len(m.details.Threads) > 0 {
if keyMatches(rawKey, m.keybindings.Threads.FoldToggle) &&
m.screen == threadScreen && len(m.details.Threads) > 0 {
thread := m.details.Threads[m.threadIndex]
m.folded[thread.ID] = !m.folded[thread.ID]
m.scroll = 0
@@ -1227,15 +1251,15 @@ func (m App) View() string {
if m.width == 0 {
return "Loading…"
}
if m.helpVisible {
return m.viewHelp()
}
if m.writeMode != writeNone && m.writeMode != writeReply {
if m.writeMode == writePREdit {
return m.viewDashboard()
}
return m.viewWritePopup()
}
if m.helpVisible {
return m.viewHelp()
}
if m.screen == prScreen {
return m.viewPRs()
}
@@ -1267,11 +1291,20 @@ func (m App) viewWritePopup() string {
if m.err != nil {
lines = append(lines, "", badStyle.Render(m.err.Error()))
}
lines = append(lines, "", dimStyle.Render("enter newline • ctrl-s review • esc cancel"))
lines = append(lines, "", dimStyle.Render(fmt.Sprintf(
"%s newline • %s review • %s cancel",
primaryKeyLabel(m.keybindings.Input.Newline),
primaryKeyLabel(m.keybindings.Input.Submit),
primaryKeyLabel(m.keybindings.Input.Cancel),
)))
case writeReplyConfirm:
lines = []string{titleStyle.Render("Submit this reply to " + location + "?"), ""}
lines = append(lines, renderCommentMarkdown(m.replyDraft, width-2)...)
lines = append(lines, "", warnStyle.Render("y submit • n/esc continue editing"))
lines = append(lines, "", warnStyle.Render(fmt.Sprintf(
"%s submit • %s continue editing",
primaryKeyLabel(m.keybindings.General.Confirm),
primaryCombinedKeyLabel(m.keybindings.General.Reject, m.keybindings.Input.Cancel),
)))
case writeReplyBusy:
lines = []string{titleStyle.Render("Submitting reply…"), "", dimStyle.Render(location)}
case writeResolveConfirm:
@@ -1282,7 +1315,11 @@ func (m App) viewWritePopup() string {
lines = []string{
titleStyle.Render(strings.ToUpper(action[:1]) + action[1:] + " " + location + "?"),
"",
warnStyle.Render("y confirm • n/esc cancel"),
warnStyle.Render(fmt.Sprintf(
"%s confirm • %s cancel",
primaryKeyLabel(m.keybindings.General.Confirm),
primaryCombinedKeyLabel(m.keybindings.General.Reject, m.keybindings.Input.Cancel),
)),
}
case writeResolveBusy:
action := "Resolving"
@@ -1322,21 +1359,94 @@ type helpBinding struct {
}
func (m App) helpBindings() []helpBinding {
if m.writeMode == writePREdit || m.writeMode == writePREditConfirm {
bindings := []helpBinding{
{combinedKeyLabel(m.keybindings.Input.NextField, m.keybindings.Input.PreviousField), "Move to the next / previous field"},
{keyLabel(m.keybindings.Input.Submit), "Review pull request metadata changes"},
{keyLabel(m.keybindings.Input.Cancel), "Return to Normal mode or cancel the editor"},
{combinedKeyLabel(m.keybindings.Navigation.PageDown, m.keybindings.Navigation.PageUp), "Move through the description by half a page"},
{combinedKeyLabel(m.keybindings.Input.PreviousCompletion, m.keybindings.Input.NextCompletion), "Select the previous / next target-branch completion"},
{combinedKeyLabel(m.keybindings.Input.NextField, m.keybindings.Input.Newline), "Complete the selected target branch"},
}
if m.prEditEditors[prEditBodyField].Modal {
bindings = append(bindings,
helpBinding{combinedKeyLabel(
m.keybindings.Navigation.Left, m.keybindings.Navigation.Down,
m.keybindings.Navigation.Up, m.keybindings.Navigation.Right,
), "Move left / down / up / right in Normal or Visual mode"},
helpBinding{combinedKeyLabel(
m.keybindings.Vim.WordForward, m.keybindings.Vim.WORDForward,
m.keybindings.Vim.WordBackward, m.keybindings.Vim.WORDBackward,
m.keybindings.Vim.WordEnd, m.keybindings.Vim.WORDEnd,
), "Move by words or WORDs"},
helpBinding{combinedKeyLabel(
m.keybindings.Vim.LineStart, m.keybindings.Vim.FirstNonBlank,
m.keybindings.Vim.LineEnd,
), "Move to the line start, first non-blank, or line end"},
helpBinding{combinedKeyLabel(
m.keybindings.Vim.GoPrefix, m.keybindings.Navigation.Last,
), "Move to the start / end of the description"},
helpBinding{combinedKeyLabel(
m.keybindings.Vim.Insert, m.keybindings.Vim.Append,
m.keybindings.Vim.InsertLineStart, m.keybindings.Vim.AppendLineEnd,
m.keybindings.Vim.ReplaceCharacter,
), "Enter Insert mode"},
helpBinding{combinedKeyLabel(
m.keybindings.Vim.OpenBelow, m.keybindings.Vim.OpenAbove,
), "Open a line below / above"},
helpBinding{combinedKeyLabel(
m.keybindings.Vim.Visual, m.keybindings.Vim.VisualLine,
), "Start character-wise / line-wise Visual mode"},
helpBinding{keyLabel(m.keybindings.Vim.SelectionOtherEnd), "Move to the other end of a Visual selection"},
helpBinding{keyLabel(m.keybindings.Vim.Yank), "Copy the Visual selection to the system clipboard"},
helpBinding{keyLabel(m.keybindings.Vim.Paste), "Paste from the system clipboard"},
helpBinding{combinedKeyLabel(
m.keybindings.Vim.Delete, m.keybindings.Vim.DeleteBefore,
), "Delete the selection or text at / before the cursor"},
helpBinding{combinedKeyLabel(
m.keybindings.Vim.FindForward, m.keybindings.Vim.FindBackward,
m.keybindings.Vim.TillForward, m.keybindings.Vim.TillBackward,
), "Find or move until a character on the current visual line"},
helpBinding{combinedKeyLabel(
m.keybindings.Vim.RepeatFind, m.keybindings.Vim.RepeatFindReverse,
), "Repeat the last character find forward / backward"},
)
} else {
bindings = append(bindings,
helpBinding{combinedKeyLabel(
m.keybindings.Navigation.Left, m.keybindings.Navigation.Down,
m.keybindings.Navigation.Up, m.keybindings.Navigation.Right,
), "Move the description cursor"},
helpBinding{combinedKeyLabel(
m.keybindings.Input.LineStart, m.keybindings.Input.LineEnd,
), "Move to the start / end of the current visual line"},
helpBinding{keyLabel(m.keybindings.Input.Newline), "Insert a newline"},
helpBinding{combinedKeyLabel(
m.keybindings.Input.DeleteBackward, m.keybindings.Input.DeleteForward,
), "Delete text before / at the cursor"},
)
}
bindings = append(bindings,
helpBinding{keyLabel(m.keybindings.General.Help), "Close this help"},
helpBinding{keyLabel(m.keybindings.General.Quit), "Quit"},
)
return bindings
}
if m.screen == prScreen {
openAction := "Open pull request dashboard"
if m.dashboardMode == "hotkey" {
openAction = "Open review threads"
}
return []helpBinding{
{"j / ↓", "Next pull request"},
{"k / ↑", "Previous pull request"},
{"g / G", "First / last pull request"},
{"ctrl-d / ctrl-u", "Page down / up"},
{"enter / l", openAction},
{"d", "Open pull request dashboard"},
{"r", "Refresh now"},
{"?", "Close this help"},
{"q / ctrl-c", "Quit"},
{keyLabel(m.keybindings.Navigation.Down), "Next pull request"},
{keyLabel(m.keybindings.Navigation.Up), "Previous pull request"},
{combinedKeyLabel(m.keybindings.Navigation.First, m.keybindings.Navigation.Last), "First / last pull request"},
{combinedKeyLabel(m.keybindings.Navigation.PageDown, m.keybindings.Navigation.PageUp), "Page down / up"},
{keyLabel(m.keybindings.Views.Open), openAction},
{keyLabel(m.keybindings.Views.Dashboard), "Open pull request dashboard"},
{keyLabel(m.keybindings.General.Refresh), "Refresh now"},
{keyLabel(m.keybindings.General.Help), "Close this help"},
{keyLabel(m.keybindings.General.Quit), "Quit"},
}
}
if m.screen == dashboardScreen {
@@ -1345,16 +1455,16 @@ func (m App) helpBindings() []helpBinding {
backAction = "Return to review threads"
}
return []helpBinding{
{"j / ↓", "Scroll description down"},
{"k / ↑", "Scroll description up"},
{"g / G", "Top / bottom"},
{"ctrl-d / ctrl-u", "Page down / up"},
{"e", "Edit title, target branch, and description"},
{"enter / l", "Open review threads"},
{"b / esc", backAction},
{"r", "Refresh now"},
{"?", "Close this help"},
{"q / ctrl-c", "Quit"},
{keyLabel(m.keybindings.Navigation.Down), "Scroll description down"},
{keyLabel(m.keybindings.Navigation.Up), "Scroll description up"},
{combinedKeyLabel(m.keybindings.Navigation.First, m.keybindings.Navigation.Last), "Top / bottom"},
{combinedKeyLabel(m.keybindings.Navigation.PageDown, m.keybindings.Navigation.PageUp), "Page down / up"},
{keyLabel(m.keybindings.Views.Edit), "Edit title, target branch, and description"},
{keyLabel(m.keybindings.Views.Open), "Open review threads"},
{keyLabel(m.keybindings.General.Back), backAction},
{keyLabel(m.keybindings.General.Refresh), "Refresh now"},
{keyLabel(m.keybindings.General.Help), "Close this help"},
{keyLabel(m.keybindings.General.Quit), "Quit"},
}
}
backAction := "Return to pull requests"
@@ -1362,31 +1472,34 @@ func (m App) helpBindings() []helpBinding {
backAction = "Return to PR dashboard"
}
bindings := []helpBinding{
{"h / l", "Focus thread list / detail"},
{"j / k", "Move or scroll focused pane"},
{"↓ / ↑", "Move or scroll focused pane"},
{"g / G", "First / last item"},
{"ctrl-d / ctrl-u", "Page down / up"},
{"tab", "Hide / reveal thread list"},
{"/", "Fuzzy-search file paths and combine status:, author:, and updated:true filters"},
{"F", "Clear the active thread filter and show every thread"},
{"n / N", "Next / previous new update"},
{"c", "Compose a reply to the selected thread"},
{"R", "Resolve or unresolve the selected thread"},
{"d", "Open pull request dashboard"},
{"enter / za", "Fold / expand thread"},
{"b / esc", backAction},
{"r", "Refresh now"},
{combinedKeyLabel(m.keybindings.Navigation.Left, m.keybindings.Navigation.Right), "Focus thread list / detail"},
{combinedKeyLabel(m.keybindings.Navigation.Down, m.keybindings.Navigation.Up), "Move or scroll focused pane"},
{combinedKeyLabel(m.keybindings.Navigation.First, m.keybindings.Navigation.Last), "First / last item"},
{combinedKeyLabel(m.keybindings.Navigation.PageDown, m.keybindings.Navigation.PageUp), "Page down / up"},
{keyLabel(m.keybindings.Views.ToggleList), "Hide / reveal thread list"},
{keyLabel(m.keybindings.Threads.Search), "Fuzzy-search file paths and combine status:, author:, and updated:true filters"},
{keyLabel(m.keybindings.Threads.ClearFilter), "Clear the active thread filter and show every thread"},
{combinedKeyLabel(m.keybindings.Threads.NextUnread, m.keybindings.Threads.PreviousUnread), "Next / previous new update"},
{keyLabel(m.keybindings.Threads.Reply), "Compose a reply to the selected thread"},
{keyLabel(m.keybindings.Threads.Resolve), "Resolve or unresolve the selected thread"},
{keyLabel(m.keybindings.Views.Dashboard), "Open pull request dashboard"},
{combinedKeyLabel(
m.keybindings.Threads.Toggle,
[]string{sequenceKeyLabel(m.keybindings.Threads.FoldPrefix, m.keybindings.Threads.FoldToggle)},
), "Fold / expand thread"},
{keyLabel(m.keybindings.General.Back), backAction},
{keyLabel(m.keybindings.General.Refresh), "Refresh now"},
}
bindings = append(bindings,
helpBinding{"?", "Close this help"},
helpBinding{"q / ctrl-c", "Quit"},
helpBinding{keyLabel(m.keybindings.General.Help), "Close this help"},
helpBinding{keyLabel(m.keybindings.General.Quit), "Quit"},
)
return bindings
}
func (m App) helpVisibleRows() int {
return max(1, m.height-5)
// Reserve rows for the popup border, title, title divider, and footer.
return max(1, m.height-6)
}
func (m App) helpMaxScroll() int {
@@ -1401,25 +1514,45 @@ func (m App) viewHelp() string {
end := min(len(rows), start+visibleRows)
title := "Pull request picker keys"
if m.screen == dashboardScreen {
if m.writeMode == writePREdit || m.writeMode == writePREditConfirm {
title = "Pull request editor keys"
} else if m.screen == dashboardScreen {
title = "Pull request dashboard keys"
} else if m.screen == threadScreen {
title = "Review thread keys"
}
lines := []string{titleStyle.Render(title)}
lines = append(lines, rows[start:end]...)
contentLines := append([]string(nil), rows[start:end]...)
if len(rows) > visibleRows {
lines = append(lines, dimStyle.Render(fmt.Sprintf(
"%d%d of %d • j/k scroll • ?/esc/q close",
contentLines = append(contentLines, dimStyle.Render(fmt.Sprintf(
"%d%d of %d • %s/%s scroll • %s close",
start+1, end, len(rows),
primaryKeyLabel(m.keybindings.Navigation.Down),
primaryKeyLabel(m.keybindings.Navigation.Up),
primaryKeyLabel(m.keybindings.General.Help)+"/"+
primaryKeyLabel(m.keybindings.General.Back),
)))
} else {
lines = append(lines, dimStyle.Render("?/esc/q close"))
contentLines = append(contentLines, dimStyle.Render(
primaryKeyLabel(m.keybindings.General.Help)+"/"+
primaryKeyLabel(m.keybindings.General.Back)+" close",
))
}
for i := range lines {
lines[i] = ansi.Truncate(lines[i], contentWidth, "")
for i := range contentLines {
contentLines[i] = ansi.Truncate(contentLines[i], contentWidth, "")
}
popup := paneStyle(true).Width(contentWidth).Render(strings.Join(lines, "\n"))
border := lipgloss.NewStyle().Foreground(paneActiveColor)
boxLines := []string{
border.Render("╭" + strings.Repeat("─", contentWidth) + "╮"),
border.Render("│") + pad(titleStyle.Render(title), contentWidth) + border.Render("│"),
border.Render("├" + strings.Repeat("─", contentWidth) + "┤"),
}
for _, line := range contentLines {
boxLines = append(boxLines,
border.Render("│")+pad(line, contentWidth)+border.Render("│"),
)
}
boxLines = append(boxLines, border.Render("╰"+strings.Repeat("─", contentWidth)+"╯"))
popup := strings.Join(boxLines, "\n")
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, popup)
}
@@ -1431,16 +1564,34 @@ func (m App) helpRows(contentWidth int) []string {
keyWidth := min(17, max(8, contentWidth/3))
actionWidth := max(1, contentWidth-keyWidth-1)
var rows []string
for _, binding := range m.helpBindings() {
wrapped := ansi.Hardwrap(ansi.Wordwrap(binding.action, actionWidth, ""), actionWidth, false)
actionLines := strings.Split(wrapped, "\n")
for index, action := range actionLines {
key := ""
if index == 0 {
key = binding.key
bindings := m.helpBindings()
for bindingIndex, binding := range bindings {
wrappedKeys := ansi.Hardwrap(
ansi.Wordwrap(binding.key, keyWidth, ""),
keyWidth,
false,
)
keyLines := strings.Split(wrappedKeys, "\n")
wrappedAction := ansi.Hardwrap(
ansi.Wordwrap(binding.action, actionWidth, ""),
actionWidth,
false,
)
actionLines := strings.Split(wrappedAction, "\n")
lineCount := max(len(keyLines), len(actionLines))
for lineIndex := range lineCount {
key, action := "", ""
if lineIndex < len(keyLines) {
key = keyLines[lineIndex]
}
if lineIndex < len(actionLines) {
action = actionLines[lineIndex]
}
rows = append(rows, titleStyle.Render(pad(key, keyWidth))+" "+action)
}
if bindingIndex < len(bindings)-1 {
rows = append(rows, dimStyle.Render(strings.Repeat("─", contentWidth)))
}
}
return rows
}
@@ -1512,9 +1663,22 @@ func (m App) viewPRs() string {
}
lines = append(lines, line)
}
footer := "? keys • j/k move • enter dashboard • q quit"
footer := fmt.Sprintf(
"%s keys • %s move • %s dashboard • %s quit",
primaryKeyLabel(m.keybindings.General.Help),
primaryCombinedKeyLabel(m.keybindings.Navigation.Down, m.keybindings.Navigation.Up),
primaryKeyLabel(m.keybindings.Views.Open),
primaryKeyLabel(m.keybindings.General.Quit),
)
if m.dashboardMode == "hotkey" {
footer = "? keys • j/k move • enter threads • d dashboard • q quit"
footer = fmt.Sprintf(
"%s keys • %s move • %s threads • %s dashboard • %s quit",
primaryKeyLabel(m.keybindings.General.Help),
primaryCombinedKeyLabel(m.keybindings.Navigation.Down, m.keybindings.Navigation.Up),
primaryKeyLabel(m.keybindings.Views.Open),
primaryKeyLabel(m.keybindings.Views.Dashboard),
primaryKeyLabel(m.keybindings.General.Quit),
)
}
return m.frame(lines, footer)
}
@@ -1552,9 +1716,22 @@ func (m App) viewDashboard() string {
maxScroll := max(0, len(lines)-viewportHeight)
scroll := min(m.scroll, maxScroll)
visible := lines[scroll:min(len(lines), scroll+viewportHeight)]
footer := "? keys • j/k scroll • enter threads • b back • q quit"
footer := fmt.Sprintf(
"%s keys • %s scroll • %s threads • %s back • %s quit",
primaryKeyLabel(m.keybindings.General.Help),
primaryCombinedKeyLabel(m.keybindings.Navigation.Down, m.keybindings.Navigation.Up),
primaryKeyLabel(m.keybindings.Views.Open),
primaryKeyLabel(m.keybindings.General.Back),
primaryKeyLabel(m.keybindings.General.Quit),
)
if m.writeMode == writePREdit {
footer = "tab fields • ctrl-d/u page • v/V select • y copy • p paste • ctrl-s review • esc normal/cancel"
footer = fmt.Sprintf(
"%s keys • %s fields • %s review • %s normal/cancel",
primaryKeyLabel(m.keybindings.General.Help),
primaryCombinedKeyLabel(m.keybindings.Input.NextField, m.keybindings.Input.PreviousField),
primaryKeyLabel(m.keybindings.Input.Submit),
primaryKeyLabel(m.keybindings.Input.Cancel),
)
m.positionPREditHardwareCursor(scroll, viewportHeight)
}
view := m.frame(visible, footer)
@@ -2010,11 +2187,30 @@ func (m App) viewThreads() string {
right := m.threadDetail(rightWidth, contentHeight)
body = lipgloss.JoinHorizontal(lipgloss.Top, left, " ", right)
}
help := "? keys • h/l focus • j/k move/scroll • c reply • R resolve • d dashboard • b back • q quit"
help := fmt.Sprintf(
"%s keys • %s focus • %s move/scroll • %s reply • %s resolve • %s dashboard • %s back • %s quit",
primaryKeyLabel(m.keybindings.General.Help),
primaryCombinedKeyLabel(m.keybindings.Navigation.Left, m.keybindings.Navigation.Right),
primaryCombinedKeyLabel(m.keybindings.Navigation.Down, m.keybindings.Navigation.Up),
primaryKeyLabel(m.keybindings.Threads.Reply),
primaryKeyLabel(m.keybindings.Threads.Resolve),
primaryKeyLabel(m.keybindings.Views.Dashboard),
primaryKeyLabel(m.keybindings.General.Back),
primaryKeyLabel(m.keybindings.General.Quit),
)
if m.searching {
help = "path words • status:open • author:name • updated:true • enter apply • esc cancel"
help = fmt.Sprintf(
"path words • status:open • author:name • updated:true • %s apply • %s cancel",
primaryKeyLabel(m.keybindings.Input.Newline),
primaryKeyLabel(m.keybindings.Input.Cancel),
)
} else if m.writeMode == writeReply {
help = "reply inline • enter newline • ctrl-s review • esc cancel"
help = fmt.Sprintf(
"reply inline • %s newline • %s review • %s cancel",
primaryKeyLabel(m.keybindings.Input.Newline),
primaryKeyLabel(m.keybindings.Input.Submit),
primaryKeyLabel(m.keybindings.Input.Cancel),
)
}
return m.frame(append(top, body), help)
}
@@ -2138,7 +2334,16 @@ func (m App) detailLines(width int) []detailLine {
{anchor: "header", text: titleStyle.Render(fmt.Sprintf("[%d/%d] %s:%d", m.threadIndex+1, len(m.details.Threads), truncatePath(thread.Path, max(8, width-24)), thread.Line)) + " " + dimStyle.Render(status)},
}
if m.folded[thread.ID] {
lines = append(lines, detailLine{}, detailLine{text: dimStyle.Render("Thread folded. Press za or enter to expand.")})
lines = append(lines, detailLine{}, detailLine{text: dimStyle.Render(fmt.Sprintf(
"Thread folded. Press %s to expand.",
primaryCombinedKeyLabel(
m.keybindings.Threads.Toggle,
[]string{primarySequenceKeyLabel(
m.keybindings.Threads.FoldPrefix,
m.keybindings.Threads.FoldToggle,
)},
),
))})
} else {
if len(thread.Comments) > 0 {
lines = append(lines, detailLine{})
@@ -2294,7 +2499,12 @@ func (m App) inlineReplyLines(width int) []detailLine {
}
lines = append(lines, detailLine{
rail: rail,
text: dimStyle.Render("enter newline • ctrl-s review • esc cancel"),
text: dimStyle.Render(fmt.Sprintf(
"%s newline • %s review • %s cancel",
primaryKeyLabel(m.keybindings.Input.Newline),
primaryKeyLabel(m.keybindings.Input.Submit),
primaryKeyLabel(m.keybindings.Input.Cancel),
)),
})
return lines
}