tui: migrate Bubble Tea v1 → v2 (charm.land), enable Shift+Enter
Move the Go TUI from github.com/charmbracelet/{bubbletea,lipgloss} to the v2
ecosystem at charm.land/{bubbletea,lipgloss}/v2 (Go 1.25). v2's kitty keyboard
disambiguation is on by default, so Shift+Enter now reliably inserts a newline
in the composer (Ctrl+J / Alt+Enter kept as fallbacks for non-kitty terminals).
Approach: rather than rewrite ~190 key-match sites to v2's (Code, Mod) idiom, a
small shim (internal/app/key.go) converts a v2 KeyPressMsg into the v1-shaped
keyMsg the handlers already expect, at the single Update boundary. The rest is
mechanical:
- key constants tea.Key* → shim consts; tea.KeyMsg → keyMsg.
- lipgloss.Color is now a func returning color.Color, not a type → fields/params
retyped to image/color.Color (theme/diff/overlays/view).
- v2 View() returns tea.View: render() builds the string, View() wraps it and
carries AltScreen (alt-screen is a per-frame View field now, not a program opt).
- WithWhitespaceBackground/Foreground → WithWhitespaceStyle(Style).
- preview: drop lipgloss.SetColorProfile (v2 renders truecolor by default).
Build, vet, gofmt, and the full test suite are green; preview renders truecolor
across kinds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+104
-103
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
osc52 "github.com/aymanbagabas/go-osc52/v2"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/correx/tui-go/internal/protocol"
|
||||
"github.com/correx/tui-go/internal/ws"
|
||||
)
|
||||
@@ -113,8 +113,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
next, cmd := m.handleKey(msg)
|
||||
case tea.KeyPressMsg:
|
||||
next, cmd := m.handleKey(toKeyMsg(msg))
|
||||
// A keypress may have entered a typing/active state (insert mode, steering, …);
|
||||
// kick the tick so the caret/spinner animates again.
|
||||
if nm, ok := next.(Model); ok {
|
||||
@@ -175,12 +175,12 @@ func (m *Model) applyServerPhased(msg protocol.ServerMessage) {
|
||||
|
||||
// --- key handling ---
|
||||
|
||||
func (m Model) handleKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
func (m Model) handleKey(k keyMsg) (tea.Model, tea.Cmd) {
|
||||
debugLog("KEY type=%v runes=%q alt=%v | ds=%s input=%s edit=%s overlay=%d steering=%v steerBuf=%q dismissed=%v entered=%v sel=%s",
|
||||
k.Type, string(k.Runes), k.Alt, m.displayState(), m.inputMode, m.editMode, m.overlay,
|
||||
m.steering, m.steerBuffer, m.approvalDismissed, m.sessionEntered, m.selectedID)
|
||||
// Ctrl+C is a universal hard-quit safety; everything else is bare-key modal.
|
||||
if k.Type == tea.KeyCtrlC {
|
||||
if k.Type == keyCtrlC {
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
@@ -198,17 +198,17 @@ func (m Model) handleKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
// back through your sent messages without leaving the input. ctrl+↑ older, ctrl+↓ newer.
|
||||
if m.displayState() == StateInSession {
|
||||
switch k.Type {
|
||||
case tea.KeyCtrlUp:
|
||||
case keyCtrlUp:
|
||||
m.transcriptNavUser(-1)
|
||||
return m, nil
|
||||
case tea.KeyCtrlDown:
|
||||
case keyCtrlDown:
|
||||
m.transcriptNavUser(1)
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
// On the idle launcher, Tab cycles the launch target (chat → workflows → chat) in any
|
||||
// edit mode, so it works whether or not the input is focused.
|
||||
if m.displayState() == StateIdle && k.Type == tea.KeyTab {
|
||||
if m.displayState() == StateIdle && k.Type == keyTab {
|
||||
m.cycleLauncherWf()
|
||||
return m, nil
|
||||
}
|
||||
@@ -219,7 +219,7 @@ func (m Model) handleKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
// handleNormalKey processes vim-style bare-key commands.
|
||||
func (m Model) handleNormalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
func (m Model) handleNormalKey(k keyMsg) (tea.Model, tea.Cmd) {
|
||||
ds := m.displayState()
|
||||
// The approval band owns its keys: single-key y/n/e for low tiers, an arm→confirm
|
||||
// gate for T3+, and ↑/↓ to walk the pending queue. Handling it up front keeps the
|
||||
@@ -228,15 +228,15 @@ func (m Model) handleNormalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m.handleApprovalKey(k)
|
||||
}
|
||||
switch k.Type {
|
||||
case tea.KeyUp:
|
||||
case keyUp:
|
||||
m.navUp()
|
||||
return m, nil
|
||||
case tea.KeyDown:
|
||||
case keyDown:
|
||||
m.navDown()
|
||||
return m, nil
|
||||
case tea.KeyEnter:
|
||||
case keyEnter:
|
||||
return m.normalEnter()
|
||||
case tea.KeyEsc:
|
||||
case keyEsc:
|
||||
// In-session, esc first drops a transcript selection or output scroll (back to
|
||||
// tail-follow); otherwise it clears the session-list filter.
|
||||
if m.transcriptSel >= 0 {
|
||||
@@ -249,26 +249,26 @@ func (m Model) handleNormalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
m.filter = ""
|
||||
return m, nil
|
||||
case tea.KeyPgUp:
|
||||
case keyPgUp:
|
||||
m.scrollOutput(m.outputViewportPage())
|
||||
return m, nil
|
||||
case tea.KeyPgDown:
|
||||
case keyPgDown:
|
||||
m.scrollOutput(-m.outputViewportPage())
|
||||
return m, nil
|
||||
case tea.KeyCtrlU:
|
||||
case keyCtrlU:
|
||||
m.scrollOutput(m.outputViewportPage() / 2)
|
||||
return m, nil
|
||||
case tea.KeyCtrlD:
|
||||
case keyCtrlD:
|
||||
m.scrollOutput(-m.outputViewportPage() / 2)
|
||||
return m, nil
|
||||
case tea.KeyCtrlX:
|
||||
case keyCtrlX:
|
||||
if m.currentDiff() != "" {
|
||||
m.overlay = OverlayDiff
|
||||
m.diffScrollOffset = 0
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
if k.Type != tea.KeyRunes {
|
||||
if k.Type != keyRunes {
|
||||
return m, nil
|
||||
}
|
||||
switch string(k.Runes) {
|
||||
@@ -350,18 +350,18 @@ func (m Model) handleNormalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
// handleApprovalKey owns the approval band's bare keys. Low tiers (T0–T2) act on a
|
||||
// single keypress; a T3+ approve arms first and only a second confirming key sends
|
||||
// it (any other key disarms). ↑/↓ walk the pending queue without resolving anything.
|
||||
func (m Model) handleApprovalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
func (m Model) handleApprovalKey(k keyMsg) (tea.Model, tea.Cmd) {
|
||||
s := m.session(m.selectedID)
|
||||
if s == nil || s.Pending == nil {
|
||||
return m, nil
|
||||
}
|
||||
// Queue navigation never resolves a gate; it just changes which one is shown.
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
case k.Type == keyUp || runeIs(k, "k"):
|
||||
s.navApproval(-1)
|
||||
m.approvalArmed = false
|
||||
return m, nil
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
case k.Type == keyDown || runeIs(k, "j"):
|
||||
s.navApproval(1)
|
||||
m.approvalArmed = false
|
||||
return m, nil
|
||||
@@ -369,7 +369,7 @@ func (m Model) handleApprovalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
|
||||
// Approve: y / a / enter / ^a. For a high tier this arms the confirm instead of
|
||||
// sending; the same key pressed again (while armed) confirms.
|
||||
approveKey := k.Type == tea.KeyEnter || k.Type == tea.KeyCtrlA ||
|
||||
approveKey := k.Type == keyEnter || k.Type == keyCtrlA ||
|
||||
runeIs(k, "y") || runeIs(k, "a")
|
||||
if approveKey {
|
||||
if s.Pending.HighTier() && !m.approvalArmed {
|
||||
@@ -383,7 +383,7 @@ func (m Model) handleApprovalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.approvalArmed = false
|
||||
|
||||
switch {
|
||||
case k.Type == tea.KeyCtrlR || runeIs(k, "n") || runeIs(k, "r"):
|
||||
case k.Type == keyCtrlR || runeIs(k, "n") || runeIs(k, "r"):
|
||||
return m.decide("REJECT")
|
||||
case runeIs(k, "e") || runeIs(k, "s"):
|
||||
// Steer/edit flows into the existing steering-note input path.
|
||||
@@ -396,10 +396,10 @@ func (m Model) handleApprovalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.sessionEntered = false
|
||||
m.approvalDismissed = false
|
||||
return m, nil
|
||||
case k.Type == tea.KeyEsc:
|
||||
case k.Type == keyEsc:
|
||||
m.approvalDismissed = true
|
||||
return m, nil
|
||||
case k.Type == tea.KeyCtrlX:
|
||||
case k.Type == keyCtrlX:
|
||||
if m.currentDiff() != "" {
|
||||
m.overlay = OverlayDiff
|
||||
m.diffScrollOffset = 0
|
||||
@@ -414,26 +414,26 @@ func (m Model) handleApprovalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
// handleInsertKey processes typing (chat/filter/steer note).
|
||||
func (m Model) handleInsertKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
func (m Model) handleInsertKey(k keyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.steering {
|
||||
switch k.Type {
|
||||
case tea.KeyEsc:
|
||||
case keyEsc:
|
||||
m.steering = false
|
||||
m.editMode = ModeNormal
|
||||
case tea.KeyEnter:
|
||||
case keyEnter:
|
||||
return m.submitApproval()
|
||||
case tea.KeyBackspace:
|
||||
case keyBackspace:
|
||||
if n := len(m.steerBuffer); n > 0 {
|
||||
m.steerBuffer = m.steerBuffer[:n-1]
|
||||
}
|
||||
case tea.KeyRunes, tea.KeySpace:
|
||||
case keyRunes, keySpace:
|
||||
m.steerBuffer += string(k.Runes)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
switch k.Type {
|
||||
case tea.KeyEsc:
|
||||
case keyEsc:
|
||||
m.editMode = ModeNormal
|
||||
if m.inputMode == ModeFilter {
|
||||
m.inputMode = ModeRouter
|
||||
@@ -444,45 +444,46 @@ func (m Model) handleInsertKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.clearInput()
|
||||
m.inputMode = ModeRouter
|
||||
}
|
||||
case tea.KeyEnter:
|
||||
// Alt+Enter inserts a newline; a bare Enter submits. (Shift+Enter is not
|
||||
// distinguishable from Enter in this terminal stack — see Ctrl+J below.)
|
||||
if k.Alt && m.inputMode != ModeFilter {
|
||||
case keyEnter:
|
||||
// Shift+Enter (and Alt+Enter) insert a newline; a bare Enter submits. Shift+Enter
|
||||
// is now distinguishable thanks to bubbletea v2's kitty keyboard disambiguation;
|
||||
// Ctrl+J below stays as a fallback for terminals without kitty support.
|
||||
if (k.Shift || k.Alt) && m.inputMode != ModeFilter {
|
||||
m.appendRunes("\n")
|
||||
return m, nil
|
||||
}
|
||||
return m.submit()
|
||||
case tea.KeyCtrlJ:
|
||||
case keyCtrlJ:
|
||||
// Reliable "newline without sending" (Ctrl+J is the literal LF key) for the
|
||||
// single-line filter excluded.
|
||||
if m.inputMode != ModeFilter {
|
||||
m.appendRunes("\n")
|
||||
}
|
||||
return m, nil
|
||||
case tea.KeyBackspace:
|
||||
case keyBackspace:
|
||||
m.backspace()
|
||||
m.syncFilter()
|
||||
case tea.KeyLeft:
|
||||
case keyLeft:
|
||||
if m.inputCursor > 0 {
|
||||
m.inputCursor--
|
||||
}
|
||||
case tea.KeyRight:
|
||||
case keyRight:
|
||||
if m.inputCursor < len(m.inputBuffer) {
|
||||
m.inputCursor++
|
||||
}
|
||||
case tea.KeyUp:
|
||||
case keyUp:
|
||||
if m.inputMode == ModeFilter {
|
||||
m.listNav(-1)
|
||||
} else {
|
||||
m.historyPrev()
|
||||
}
|
||||
case tea.KeyDown:
|
||||
case keyDown:
|
||||
if m.inputMode == ModeFilter {
|
||||
m.listNav(1)
|
||||
} else {
|
||||
m.historyNext()
|
||||
}
|
||||
case tea.KeyRunes, tea.KeySpace:
|
||||
case keyRunes, keySpace:
|
||||
// opencode-style slash commands: `/` as the first char of a chat line opens the
|
||||
// command palette inline instead of typing a literal slash. Mid-line `/` is literal.
|
||||
if m.inputMode == ModeRouter && m.inputBuffer == "" && string(k.Runes) == "/" {
|
||||
@@ -627,7 +628,7 @@ func (m Model) autoApprove() (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
func (m Model) handleOverlayKey(k keyMsg) (tea.Model, tea.Cmd) {
|
||||
// Config overlay owns all its keys (esc cancels an in-progress edit rather than closing).
|
||||
if m.overlay == OverlayConfig {
|
||||
return m.handleConfigKey(k)
|
||||
@@ -637,7 +638,7 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.overlay == OverlaySessions {
|
||||
return m.handleSessionsKey(k)
|
||||
}
|
||||
if k.Type == tea.KeyEsc {
|
||||
if k.Type == keyEsc {
|
||||
// In the event inspector, esc first stops an in-progress filter edit (keeping the
|
||||
// query) rather than closing the overlay.
|
||||
if m.overlay == OverlayEventInspector && m.eventFilterTyping {
|
||||
@@ -656,37 +657,37 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
// full body, ^u/^d by half, g/G to the ends. All clamp to the diff bounds.
|
||||
page := m.diffBodyHeight()
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
case k.Type == keyUp || runeIs(k, "k"):
|
||||
m.scrollDiff(-1)
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
case k.Type == keyDown || runeIs(k, "j"):
|
||||
m.scrollDiff(1)
|
||||
case k.Type == tea.KeyPgUp:
|
||||
case k.Type == keyPgUp:
|
||||
m.scrollDiff(-page)
|
||||
case k.Type == tea.KeyPgDown:
|
||||
case k.Type == keyPgDown:
|
||||
m.scrollDiff(page)
|
||||
case k.Type == tea.KeyCtrlU:
|
||||
case k.Type == keyCtrlU:
|
||||
m.scrollDiff(-page / 2)
|
||||
case k.Type == tea.KeyCtrlD:
|
||||
case k.Type == keyCtrlD:
|
||||
m.scrollDiff(page / 2)
|
||||
case runeIs(k, "g"):
|
||||
m.diffScrollOffset = 0
|
||||
case runeIs(k, "G"):
|
||||
m.diffScrollOffset = m.diffMaxScroll()
|
||||
case k.Type == tea.KeyCtrlX:
|
||||
case k.Type == keyCtrlX:
|
||||
m.overlay = OverlayNone
|
||||
}
|
||||
case OverlayEventInspector:
|
||||
evs := m.currentEvents()
|
||||
if m.eventFilterTyping {
|
||||
switch {
|
||||
case k.Type == tea.KeyEnter:
|
||||
case k.Type == keyEnter:
|
||||
m.eventFilterTyping = false
|
||||
case k.Type == tea.KeyBackspace:
|
||||
case k.Type == keyBackspace:
|
||||
if n := len(m.eventFilter); n > 0 {
|
||||
m.eventFilter = m.eventFilter[:n-1]
|
||||
m.overlayEventIdx = 0
|
||||
}
|
||||
case k.Type == tea.KeyRunes || k.Type == tea.KeySpace:
|
||||
case k.Type == keyRunes || k.Type == keySpace:
|
||||
m.eventFilter += string(k.Runes)
|
||||
m.overlayEventIdx = 0
|
||||
}
|
||||
@@ -698,11 +699,11 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
case runeIs(k, "c"):
|
||||
m.eventFilter = ""
|
||||
m.overlayEventIdx = 0
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
case k.Type == keyUp || runeIs(k, "k"):
|
||||
if m.overlayEventIdx > 0 {
|
||||
m.overlayEventIdx--
|
||||
}
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
case k.Type == keyDown || runeIs(k, "j"):
|
||||
if m.overlayEventIdx < len(evs)-1 {
|
||||
m.overlayEventIdx++
|
||||
}
|
||||
@@ -712,17 +713,17 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
// dismisses it (esc handled above), keeping the quick-glance feel.
|
||||
page := m.modalBodyRows()
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
case k.Type == keyUp || runeIs(k, "k"):
|
||||
m.scrollModal(-1)
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
case k.Type == keyDown || runeIs(k, "j"):
|
||||
m.scrollModal(1)
|
||||
case k.Type == tea.KeyPgUp:
|
||||
case k.Type == keyPgUp:
|
||||
m.scrollModal(-page)
|
||||
case k.Type == tea.KeyPgDown:
|
||||
case k.Type == keyPgDown:
|
||||
m.scrollModal(page)
|
||||
case k.Type == tea.KeyCtrlU:
|
||||
case k.Type == keyCtrlU:
|
||||
m.scrollModal(-page / 2)
|
||||
case k.Type == tea.KeyCtrlD:
|
||||
case k.Type == keyCtrlD:
|
||||
m.scrollModal(page / 2)
|
||||
case runeIs(k, "g"):
|
||||
m.modalScroll = 0
|
||||
@@ -737,23 +738,23 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
case OverlayArtifacts:
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
case k.Type == keyUp || runeIs(k, "k"):
|
||||
if m.artifactsIndex > 0 {
|
||||
m.artifactsIndex--
|
||||
m.artifactScroll = 0
|
||||
}
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
case k.Type == keyDown || runeIs(k, "j"):
|
||||
if m.artifactsIndex < len(m.artifacts)-1 {
|
||||
m.artifactsIndex++
|
||||
m.artifactScroll = 0
|
||||
}
|
||||
case k.Type == tea.KeyPgUp:
|
||||
case k.Type == keyPgUp:
|
||||
m.scrollArtifact(-m.artifactContentBodyH())
|
||||
case k.Type == tea.KeyPgDown:
|
||||
case k.Type == keyPgDown:
|
||||
m.scrollArtifact(m.artifactContentBodyH())
|
||||
case k.Type == tea.KeyCtrlU:
|
||||
case k.Type == keyCtrlU:
|
||||
m.scrollArtifact(-m.artifactContentBodyH() / 2)
|
||||
case k.Type == tea.KeyCtrlD:
|
||||
case k.Type == keyCtrlD:
|
||||
m.scrollArtifact(m.artifactContentBodyH() / 2)
|
||||
case runeIs(k, "g"):
|
||||
m.artifactScroll = 0
|
||||
@@ -764,15 +765,15 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
case OverlayModels:
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
case k.Type == keyUp || runeIs(k, "k"):
|
||||
if m.modelsIndex > 0 {
|
||||
m.modelsIndex--
|
||||
}
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
case k.Type == keyDown || runeIs(k, "j"):
|
||||
if m.modelsIndex < len(m.availableModels)-1 {
|
||||
m.modelsIndex++
|
||||
}
|
||||
case k.Type == tea.KeyEnter:
|
||||
case k.Type == keyEnter:
|
||||
if m.modelsIndex >= 0 && m.modelsIndex < len(m.availableModels) {
|
||||
m.client.Send(protocol.SwapModel(m.availableModels[m.modelsIndex]))
|
||||
m.overlay = OverlayNone
|
||||
@@ -786,17 +787,17 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
case OverlayStats:
|
||||
page := m.modalBodyRows()
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
case k.Type == keyUp || runeIs(k, "k"):
|
||||
m.scrollModal(-1)
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
case k.Type == keyDown || runeIs(k, "j"):
|
||||
m.scrollModal(1)
|
||||
case k.Type == tea.KeyPgUp:
|
||||
case k.Type == keyPgUp:
|
||||
m.scrollModal(-page)
|
||||
case k.Type == tea.KeyPgDown:
|
||||
case k.Type == keyPgDown:
|
||||
m.scrollModal(page)
|
||||
case k.Type == tea.KeyCtrlU:
|
||||
case k.Type == keyCtrlU:
|
||||
m.scrollModal(-page / 2)
|
||||
case k.Type == tea.KeyCtrlD:
|
||||
case k.Type == keyCtrlD:
|
||||
m.scrollModal(page / 2)
|
||||
case runeIs(k, "g"):
|
||||
m.modalScroll = 0
|
||||
@@ -807,11 +808,11 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
case OverlayIdeas:
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
case k.Type == keyUp || runeIs(k, "k"):
|
||||
if m.ideasIndex > 0 {
|
||||
m.ideasIndex--
|
||||
}
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
case k.Type == keyDown || runeIs(k, "j"):
|
||||
if m.ideasIndex < len(m.ideas)-1 {
|
||||
m.ideasIndex++
|
||||
}
|
||||
@@ -830,39 +831,39 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
case OverlayFiles:
|
||||
files := m.filteredFiles()
|
||||
switch {
|
||||
case k.Type == tea.KeyUp:
|
||||
case k.Type == keyUp:
|
||||
if m.fileIndex > 0 {
|
||||
m.fileIndex--
|
||||
}
|
||||
case k.Type == tea.KeyDown:
|
||||
case k.Type == keyDown:
|
||||
if m.fileIndex < len(files)-1 {
|
||||
m.fileIndex++
|
||||
}
|
||||
case k.Type == tea.KeyEnter:
|
||||
case k.Type == keyEnter:
|
||||
if m.fileIndex >= 0 && m.fileIndex < len(files) {
|
||||
m.insertFileRef(files[m.fileIndex])
|
||||
}
|
||||
m.overlay = OverlayNone // back to typing (still in insert mode)
|
||||
case k.Type == tea.KeyBackspace:
|
||||
case k.Type == keyBackspace:
|
||||
if n := len(m.fileFilter); n > 0 {
|
||||
m.fileFilter = m.fileFilter[:n-1]
|
||||
m.fileIndex = 0
|
||||
}
|
||||
case k.Type == tea.KeyRunes || k.Type == tea.KeySpace:
|
||||
case k.Type == keyRunes || k.Type == keySpace:
|
||||
m.fileFilter += string(k.Runes)
|
||||
m.fileIndex = 0
|
||||
}
|
||||
case OverlayStatusbar:
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
case k.Type == keyUp || runeIs(k, "k"):
|
||||
if m.sbIndex > 0 {
|
||||
m.sbIndex--
|
||||
}
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
case k.Type == keyDown || runeIs(k, "j"):
|
||||
if m.sbIndex < len(statusSegments)-1 {
|
||||
m.sbIndex++
|
||||
}
|
||||
case k.Type == tea.KeySpace || k.Type == tea.KeyEnter || runeIs(k, "x"):
|
||||
case k.Type == keySpace || k.Type == keyEnter || runeIs(k, "x"):
|
||||
if m.sbIndex >= 0 && m.sbIndex < len(statusSegments) {
|
||||
m.toggleStatusSegment(statusSegments[m.sbIndex].id)
|
||||
}
|
||||
@@ -871,15 +872,15 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
case OverlayGrants:
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
case k.Type == keyUp || runeIs(k, "k"):
|
||||
if m.grantIndex > 0 {
|
||||
m.grantIndex--
|
||||
}
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
case k.Type == keyDown || runeIs(k, "j"):
|
||||
if m.grantIndex < len(m.grants)-1 {
|
||||
m.grantIndex++
|
||||
}
|
||||
case k.Type == tea.KeyEnter || runeIs(k, "x"):
|
||||
case k.Type == keyEnter || runeIs(k, "x"):
|
||||
if m.grantIndex >= 0 && m.grantIndex < len(m.grants) {
|
||||
g := m.grants[m.grantIndex]
|
||||
m.client.Send(protocol.RevokeGrant(g.GrantID))
|
||||
@@ -910,14 +911,14 @@ func grantScopeOptions() []grantScopeOption {
|
||||
|
||||
// handleGrantScopeKey drives the scope picker: ↑/↓ move, s/p/g jump-and-confirm, enter
|
||||
// confirms the highlighted scope. esc (handled upstream) cancels without granting.
|
||||
func (m Model) handleGrantScopeKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
func (m Model) handleGrantScopeKey(k keyMsg) (tea.Model, tea.Cmd) {
|
||||
opts := grantScopeOptions()
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
case k.Type == keyUp || runeIs(k, "k"):
|
||||
if m.grantScopeIndex > 0 {
|
||||
m.grantScopeIndex--
|
||||
}
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
case k.Type == keyDown || runeIs(k, "j"):
|
||||
if m.grantScopeIndex < len(opts)-1 {
|
||||
m.grantScopeIndex++
|
||||
}
|
||||
@@ -930,7 +931,7 @@ func (m Model) handleGrantScopeKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
case runeIs(k, "g"):
|
||||
m.grantScopeIndex = 2
|
||||
return m.applyGrantScope()
|
||||
case k.Type == tea.KeyEnter:
|
||||
case k.Type == keyEnter:
|
||||
return m.applyGrantScope()
|
||||
}
|
||||
return m, nil
|
||||
@@ -1046,31 +1047,31 @@ func (m *Model) openModelsOverlay() {
|
||||
}
|
||||
}
|
||||
|
||||
func runeIs(k tea.KeyMsg, s string) bool {
|
||||
return k.Type == tea.KeyRunes && string(k.Runes) == s
|
||||
func runeIs(k keyMsg, s string) bool {
|
||||
return k.Type == keyRunes && string(k.Runes) == s
|
||||
}
|
||||
|
||||
func (m Model) handlePaletteKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
func (m Model) handlePaletteKey(k keyMsg) (tea.Model, tea.Cmd) {
|
||||
cmds := m.filteredPalette()
|
||||
switch k.Type {
|
||||
case tea.KeyUp:
|
||||
case keyUp:
|
||||
if m.paletteIndex > 0 {
|
||||
m.paletteIndex--
|
||||
}
|
||||
case tea.KeyDown:
|
||||
case keyDown:
|
||||
if m.paletteIndex < len(cmds)-1 {
|
||||
m.paletteIndex++
|
||||
}
|
||||
case tea.KeyEnter:
|
||||
case keyEnter:
|
||||
if m.paletteIndex >= 0 && m.paletteIndex < len(cmds) {
|
||||
return m.execPalette(cmds[m.paletteIndex].id)
|
||||
}
|
||||
case tea.KeyBackspace:
|
||||
case keyBackspace:
|
||||
if n := len(m.paletteFilter); n > 0 {
|
||||
m.paletteFilter = m.paletteFilter[:n-1]
|
||||
m.paletteIndex = 0
|
||||
}
|
||||
case tea.KeyRunes, tea.KeySpace:
|
||||
case keyRunes, keySpace:
|
||||
m.paletteFilter += string(k.Runes)
|
||||
m.paletteIndex = 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user