feat(tui-go): scroll tall modals instead of clipping them off-screen
The ? help and S stats modals rendered at fixed height; compositeOverlay drops any rows past the screen, so on a 24-30 row terminal their bottom sections — including the close hint — vanished with no way to reach them (and the backdrop border corrupted). Recent help additions worsened it. - Add a shared scrollable-modal renderer: helpBody()/statsBody() produce discrete lines, renderScrollModal() windows them by m.modalScroll (clamped to the viewport via modalBodyRows/scrollableModalMax) and pins the hint, so the box never exceeds the screen. A "(off-end/total)" indicator shows in the title when scrolled. Keys: ↑↓/jk line, PgUp/PgDn page, ^u/^d half, g/G ends; any other key still dismisses help. modal_scroll_test.go (2): clamp + no-op. - Page the artifacts viewer (v) too: PgUp/PgDn were one line at a time — now a full body, with ^u/^d half and g/G ends via scrollArtifact(). Verified via cmd/preview: help/stats close hints now survive at h=20-28 (were clipped below ~32/40) and show the scroll indicator; full at tall heights. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -270,6 +270,7 @@ func (m Model) handleNormalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.enterInsert(ModeRouter)
|
||||
case "?":
|
||||
m.overlay = OverlayHelp
|
||||
m.modalScroll = 0
|
||||
case "/":
|
||||
m.enterInsert(ModeFilter)
|
||||
case "j":
|
||||
@@ -687,8 +688,29 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
case OverlayHelp:
|
||||
// Any key dismisses the cheat-sheet (esc handled above).
|
||||
m.overlay = OverlayNone
|
||||
// The cheat-sheet scrolls when it's taller than the screen; any non-scroll key
|
||||
// dismisses it (esc handled above), keeping the quick-glance feel.
|
||||
page := m.modalBodyRows()
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
m.scrollModal(-1)
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
m.scrollModal(1)
|
||||
case k.Type == tea.KeyPgUp:
|
||||
m.scrollModal(-page)
|
||||
case k.Type == tea.KeyPgDown:
|
||||
m.scrollModal(page)
|
||||
case k.Type == tea.KeyCtrlU:
|
||||
m.scrollModal(-page / 2)
|
||||
case k.Type == tea.KeyCtrlD:
|
||||
m.scrollModal(page / 2)
|
||||
case runeIs(k, "g"):
|
||||
m.modalScroll = 0
|
||||
case runeIs(k, "G"):
|
||||
m.modalScroll = m.scrollableModalMax(len(m.activeModalBody()))
|
||||
default:
|
||||
m.overlay = OverlayNone
|
||||
}
|
||||
case OverlayToolPalette:
|
||||
if runeIs(k, "t") {
|
||||
m.overlay = OverlayNone
|
||||
@@ -706,14 +728,17 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.artifactScroll = 0
|
||||
}
|
||||
case k.Type == tea.KeyPgUp:
|
||||
if m.artifactScroll > 0 {
|
||||
m.artifactScroll--
|
||||
}
|
||||
m.scrollArtifact(-m.artifactContentBodyH())
|
||||
case k.Type == tea.KeyPgDown:
|
||||
contentW := m.modalWidth() - 6
|
||||
if m.artifactScroll < m.artifactContentMaxScroll(contentW) {
|
||||
m.artifactScroll++
|
||||
}
|
||||
m.scrollArtifact(m.artifactContentBodyH())
|
||||
case k.Type == tea.KeyCtrlU:
|
||||
m.scrollArtifact(-m.artifactContentBodyH() / 2)
|
||||
case k.Type == tea.KeyCtrlD:
|
||||
m.scrollArtifact(m.artifactContentBodyH() / 2)
|
||||
case runeIs(k, "g"):
|
||||
m.artifactScroll = 0
|
||||
case runeIs(k, "G"):
|
||||
m.artifactScroll = m.artifactContentMaxScroll(m.modalWidth() - 6)
|
||||
case runeIs(k, "v"):
|
||||
m.overlay = OverlayNone
|
||||
}
|
||||
@@ -739,7 +764,25 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.overlay = OverlayNone
|
||||
}
|
||||
case OverlayStats:
|
||||
if runeIs(k, "S") {
|
||||
page := m.modalBodyRows()
|
||||
switch {
|
||||
case k.Type == tea.KeyUp || runeIs(k, "k"):
|
||||
m.scrollModal(-1)
|
||||
case k.Type == tea.KeyDown || runeIs(k, "j"):
|
||||
m.scrollModal(1)
|
||||
case k.Type == tea.KeyPgUp:
|
||||
m.scrollModal(-page)
|
||||
case k.Type == tea.KeyPgDown:
|
||||
m.scrollModal(page)
|
||||
case k.Type == tea.KeyCtrlU:
|
||||
m.scrollModal(-page / 2)
|
||||
case k.Type == tea.KeyCtrlD:
|
||||
m.scrollModal(page / 2)
|
||||
case runeIs(k, "g"):
|
||||
m.modalScroll = 0
|
||||
case runeIs(k, "G"):
|
||||
m.modalScroll = m.scrollableModalMax(len(m.activeModalBody()))
|
||||
case runeIs(k, "S"):
|
||||
m.overlay = OverlayNone
|
||||
}
|
||||
case OverlayIdeas:
|
||||
@@ -962,6 +1005,7 @@ func (m *Model) openStats() {
|
||||
return
|
||||
}
|
||||
m.overlay = OverlayStats
|
||||
m.modalScroll = 0
|
||||
// Reuse a cached report only if it's for this session; otherwise show a loading state.
|
||||
if m.statsFor != m.selectedID {
|
||||
m.stats = nil
|
||||
@@ -1082,6 +1126,7 @@ func (m Model) execPalette(id string) (tea.Model, tea.Cmd) {
|
||||
m.toggleInlineActions()
|
||||
case "help":
|
||||
m.overlay = OverlayHelp
|
||||
m.modalScroll = 0
|
||||
case "mode":
|
||||
m.cycleChatMode()
|
||||
case "cancel":
|
||||
|
||||
Reference in New Issue
Block a user