Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/entire/cli/activity_tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"strings"

"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
Expand Down Expand Up @@ -113,7 +114,7 @@ func (m activityModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:iretu
return m, nil

case tea.KeyMsg:
if msg.Type == tea.KeyEscape || msg.Type == tea.KeyCtrlC || msg.String() == "q" {
if key.Matches(msg, keys.Quit) || key.Matches(msg, keys.Back) {
return m, tea.Quit
}

Expand Down Expand Up @@ -222,7 +223,7 @@ func (m activityModel) renderFooter() string {
}

return keyStyle.Render("↑↓") + helpStyle.Render(" scroll") +
sep + keyStyle.Render("q") + helpStyle.Render(" quit") +
sep + keyStyle.Render(keys.Quit.Help().Key) + helpStyle.Render(" "+keys.Quit.Help().Desc) +
scrollPct
}

Expand Down
6 changes: 2 additions & 4 deletions cmd/entire/cli/dispatch_tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"strings"

"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
Expand Down Expand Up @@ -60,8 +61,6 @@ var newDispatchProgram = func(model tea.Model, outW io.Writer, altScreen bool) d
return tea.NewProgram(model, options...)
}

const tuiEscKey = "esc"

func defaultRunInteractiveDispatch(ctx context.Context, outW io.Writer, opts dispatchpkg.Options) (string, error) {
runCtx, cancel := context.WithCancel(ctx)
defer cancel()
Expand Down Expand Up @@ -381,8 +380,7 @@ func (m dispatchStatusModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.result = msg
return m, tea.Quit
case tea.KeyMsg:
switch msg.String() {
case tea.KeyCtrlC.String(), tuiEscKey, "q":
if key.Matches(msg, keys.Quit) || key.Matches(msg, keys.Back) {
if m.cancel != nil {
m.cancel()
}
Expand Down
62 changes: 62 additions & 0 deletions cmd/entire/cli/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cli

import "github.com/charmbracelet/bubbles/key"

// keyMap defines the keybindings used across the CLI's TUIs. Single source of
// truth so help text and matching logic stay aligned, and so the strings "esc",
// "ctrl+c", etc. live in exactly one place.
type keyMap struct {
Quit key.Binding
Back key.Binding
Search key.Binding
Confirm key.Binding
Up key.Binding
Down key.Binding
NextPage key.Binding
PrevPage key.Binding
Home key.Binding
End key.Binding
}

var keys = keyMap{
Quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"),
key.WithHelp("q", "quit"),
),
Back: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "back"),
),
Search: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "search"),
),
Confirm: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "select"),
),
Up: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑/k", "up"),
),
Down: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓/j", "down"),
),
NextPage: key.NewBinding(
key.WithKeys("n", "right"),
key.WithHelp("n/→", "next page"),
),
PrevPage: key.NewBinding(
key.WithKeys("p", "left"),
key.WithHelp("p/←", "prev page"),
),
Home: key.NewBinding(
key.WithKeys("home", "g"),
key.WithHelp("g/home", "top"),
),
End: key.NewBinding(
key.WithKeys("end", "G"),
key.WithHelp("G/end", "bottom"),
),
}
67 changes: 37 additions & 30 deletions cmd/entire/cli/search_tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"charm.land/glamour/v2/ansi"
glamourstyles "charm.land/glamour/v2/styles"
"github.com/charmbracelet/bubbles/cursor"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
Expand Down Expand Up @@ -84,6 +85,13 @@ func newSearchStyles(ss statusStyles) searchStyles {
return s
}

// helpItem renders a "<key> <desc>" pair for a TUI help footer using the
// shared helpKey style. keyLabel may come from a key.Binding's Help().Key or
// be a composite literal like "j/k".
func (s searchStyles) helpItem(keyLabel, desc string) string {
return s.render(s.helpKey, keyLabel) + " " + desc
}

const resultsPerPage = 25

// searchModel is the bubbletea model for interactive search results.
Expand Down Expand Up @@ -249,13 +257,13 @@ func (m searchModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:ireturn
}

func (m searchModel) updateSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { //nolint:ireturn // bubbletea pattern
switch msg.String() {
case tuiEscKey:
switch {
case key.Matches(msg, keys.Back):
m.mode = modeBrowse
m.input.Blur()
m = m.refreshBrowseContent()
return m, nil
case "enter":
case key.Matches(msg, keys.Confirm):
raw := strings.TrimSpace(m.input.Value())
if raw == "" {
return m, nil
Expand Down Expand Up @@ -291,25 +299,25 @@ func (m searchModel) updateSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { //n

func (m searchModel) updateBrowseMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { //nolint:ireturn // bubbletea pattern
pageLen := len(m.pageResults())
switch msg.String() {
case "q", "ctrl+c", tuiEscKey, "h":
switch {
case key.Matches(msg, keys.Quit), key.Matches(msg, keys.Back), msg.String() == "h":
return m, tea.Quit
case "up", "k":
case key.Matches(msg, keys.Up):
if m.cursor > 0 {
m.cursor--
m = m.refreshBrowseContent()
}
case "down", "j":
case key.Matches(msg, keys.Down):
if m.cursor < pageLen-1 {
m.cursor++
m = m.refreshBrowseContent()
}
case "home", "g":
case key.Matches(msg, keys.Home):
m.page = 0
m.cursor = 0
m = m.refreshBrowseContent()
m.browseVP.GotoTop()
case "end", "G":
case key.Matches(msg, keys.End):
if len(m.results) > 0 {
lastLoaded := len(m.results) - 1
m.page = min(lastLoaded/resultsPerPage, m.totalPages()-1)
Expand All @@ -319,7 +327,7 @@ func (m searchModel) updateBrowseMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { //n
m = m.refreshBrowseContent()
m.browseVP.GotoBottom()
}
case "n", "right":
case key.Matches(msg, keys.NextPage):
if m.page < m.totalPages()-1 {
m.page++
m.cursor = 0
Expand All @@ -333,22 +341,22 @@ func (m searchModel) updateBrowseMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { //n
}
m = m.refreshBrowseContent()
}
case "p", "left":
case key.Matches(msg, keys.PrevPage):
if m.page > 0 {
m.page--
m.cursor = 0
m.browseVP.GotoTop()
m = m.refreshBrowseContent()
}
case "enter":
case key.Matches(msg, keys.Confirm):
if r := m.selectedResult(); r != nil {
m.mode = modeDetail
content := m.renderDetailContent(*r, m.width, true)
m.detailVP = viewport.New(m.width, max(m.height-2, 1))
m.detailVP.SetContent(content)
return m, nil
}
case "/":
case key.Matches(msg, keys.Search):
m.mode = modeSearch
m.input.Focus()
return m, m.input.Cursor.SetMode(cursor.CursorBlink)
Expand All @@ -362,13 +370,13 @@ func (m searchModel) updateBrowseMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { //n
}

func (m searchModel) updateDetailMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { //nolint:ireturn // bubbletea pattern
switch msg.String() {
case tuiEscKey, "backspace":
switch {
case key.Matches(msg, keys.Quit):
return m, tea.Quit
case key.Matches(msg, keys.Back), msg.String() == "backspace":
m.mode = modeBrowse
return m, nil
case "q", "ctrl+c":
return m, tea.Quit
case "/":
case key.Matches(msg, keys.Search):
m.mode = modeSearch
m.input.Focus()
return m, m.input.Cursor.SetMode(cursor.CursorBlink)
Expand Down Expand Up @@ -693,11 +701,10 @@ func (m searchModel) viewDetailFull() string {

// Scroll indicator + help
scrollPct := m.styles.render(m.styles.dim, fmt.Sprintf("%3.f%%", m.detailVP.ScrollPercent()*100))
help := m.styles.render(m.styles.helpKey, "j/k") + " scroll" +
m.styles.render(m.styles.helpSep, " · ") +
m.styles.render(m.styles.helpKey, tuiEscKey) + " back" +
m.styles.render(m.styles.helpSep, " · ") +
m.styles.render(m.styles.helpKey, "q") + " quit"
dot := m.styles.render(m.styles.helpSep, " · ")
help := m.styles.helpItem("j/k", "scroll") + dot +
m.styles.helpItem(keys.Back.Help().Key, keys.Back.Help().Desc) + dot +
m.styles.helpItem(keys.Quit.Help().Key, keys.Quit.Help().Desc)

gap := m.width - lipgloss.Width(help) - lipgloss.Width(scrollPct) - 2
if gap < 1 {
Expand All @@ -712,19 +719,19 @@ func (m searchModel) viewHelp() string {
dot := m.styles.render(m.styles.helpSep, " · ")

if m.mode == modeSearch {
return m.styles.render(m.styles.helpKey, "enter") + " search" + dot +
m.styles.render(m.styles.helpKey, tuiEscKey) + " cancel" + "\n"
return m.styles.helpItem(keys.Confirm.Help().Key, "search") + dot +
m.styles.helpItem(keys.Back.Help().Key, "cancel") + "\n"
}

pages := m.totalPages()

left := m.styles.render(m.styles.helpKey, "/") + " search" + dot +
m.styles.render(m.styles.helpKey, "↑/↓, j/k") + " scroll" + dot +
m.styles.render(m.styles.helpKey, "home/end, g/G") + " top/bottom"
left := m.styles.helpItem(keys.Search.Help().Key, keys.Search.Help().Desc) + dot +
m.styles.helpItem("↑/↓, j/k", "scroll") + dot +
m.styles.helpItem("home/end, g/G", "top/bottom")
if pages > 1 {
left += dot + m.styles.render(m.styles.helpKey, "n/p") + " page"
left += dot + m.styles.helpItem("n/p", "page")
}
left += dot + m.styles.render(m.styles.helpKey, "q") + " quit"
left += dot + m.styles.helpItem(keys.Quit.Help().Key, keys.Quit.Help().Desc)

right := fmt.Sprintf("%d results", m.total)
if pages > 1 {
Expand Down
Loading