205 lines
5.7 KiB
Go
205 lines
5.7 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"charm.land/bubbles/v2/viewport"
|
|
"charm.land/lipgloss/v2"
|
|
|
|
"ai-agent/internal/command"
|
|
)
|
|
|
|
// helpContentWidth returns the inner width for the help modal content.
|
|
func (m *Model) helpContentWidth() int {
|
|
maxW := 60
|
|
if m.width < maxW+8 {
|
|
maxW = m.width - 8
|
|
}
|
|
if maxW < 30 {
|
|
maxW = 30
|
|
}
|
|
return maxW
|
|
}
|
|
|
|
// helpViewportHeight returns the viewport height for the help modal.
|
|
func (m *Model) helpViewportHeight() int {
|
|
// Leave room for border (2), padding (2), title (2), footer (1)
|
|
h := m.height - 10
|
|
if h < 5 {
|
|
h = 5
|
|
}
|
|
return h
|
|
}
|
|
|
|
// buildHelpContent builds the raw help text (without border/viewport wrapper).
|
|
func (m *Model) buildHelpContent(innerW int) string {
|
|
var b strings.Builder
|
|
|
|
loc := m.tr()
|
|
b.WriteString(m.styles.OverlayAccent.Render(loc.KeyboardShortcuts))
|
|
b.WriteString("\n")
|
|
|
|
shortcuts := []struct{ key, desc string }{
|
|
{"enter", loc.SendMessage},
|
|
{"shift+enter", loc.NewLineInInput},
|
|
{"shift+tab", loc.CycleMode},
|
|
{"F6", loc.QuickModelSwitch},
|
|
{"esc", loc.CancelStreaming},
|
|
{"ctrl+c / ctrl+q / F10", loc.QuitKeys},
|
|
{"ctrl+l", loc.ClearScreen},
|
|
{"ctrl+n", loc.NewConversation},
|
|
{"?", loc.ToggleHelp},
|
|
{"t", loc.ExpandTools},
|
|
{"space", loc.ToggleToolDetails},
|
|
{"ctrl+y", loc.CopyLastResponse},
|
|
{"ctrl+t", loc.ToggleThinking},
|
|
{"ctrl+k", loc.ToggleCompact},
|
|
{"ctrl+e", loc.OpenInEditor},
|
|
{"↑/↓", loc.BrowseHistory},
|
|
{"pgup/pgdown", loc.ScrollViewport},
|
|
{"ctrl+u/d", loc.HalfPageScroll},
|
|
{"tab", loc.Autocomplete},
|
|
{"F2", loc.LanguageF2},
|
|
}
|
|
|
|
for _, s := range shortcuts {
|
|
fmt.Fprintf(&b, " %s %s\n",
|
|
m.styles.FocusIndicator.Width(16).Render(s.key),
|
|
m.styles.OverlayDim.Render(s.desc),
|
|
)
|
|
}
|
|
|
|
b.WriteString("\n")
|
|
b.WriteString(m.styles.OverlayAccent.Render(loc.InputShortcuts))
|
|
b.WriteString("\n")
|
|
|
|
inputShortcuts := []struct{ key, desc string }{
|
|
{"@file", loc.AttachFile},
|
|
{"#skill", loc.ActivateSkill},
|
|
{"/cmd", loc.RunSlashCommand},
|
|
}
|
|
|
|
for _, s := range inputShortcuts {
|
|
fmt.Fprintf(&b, " %s %s\n",
|
|
m.styles.FocusIndicator.Width(16).Render(s.key),
|
|
m.styles.OverlayDim.Render(s.desc),
|
|
)
|
|
}
|
|
|
|
b.WriteString("\n")
|
|
b.WriteString(m.styles.OverlayAccent.Render(loc.SlashCommands))
|
|
b.WriteString("\n")
|
|
|
|
// Slash commands.
|
|
if m.cmdRegistry != nil {
|
|
for _, cmd := range m.cmdRegistry.All() {
|
|
fmt.Fprintf(&b, " %s %s\n",
|
|
m.styles.FocusIndicator.Width(16).Render("/"+cmd.Name),
|
|
m.styles.OverlayDim.Render(cmd.Description),
|
|
)
|
|
}
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// initHelpViewport creates and populates the help viewport for scrolling.
|
|
func (m *Model) initHelpViewport() {
|
|
innerW := m.helpContentWidth()
|
|
vpH := m.helpViewportHeight()
|
|
|
|
m.helpViewport = viewport.New(
|
|
viewport.WithWidth(innerW),
|
|
viewport.WithHeight(vpH),
|
|
)
|
|
// Disable default arrow key bindings (we handle j/k/up/down ourselves via parent)
|
|
m.helpViewport.KeyMap.Up.SetEnabled(false)
|
|
m.helpViewport.KeyMap.Down.SetEnabled(false)
|
|
m.helpViewport.KeyMap.PageUp.SetEnabled(false)
|
|
m.helpViewport.KeyMap.PageDown.SetEnabled(false)
|
|
m.helpViewport.KeyMap.HalfPageUp.SetEnabled(false)
|
|
m.helpViewport.KeyMap.HalfPageDown.SetEnabled(false)
|
|
|
|
content := m.buildHelpContent(innerW)
|
|
m.helpViewport.SetContent(content)
|
|
}
|
|
|
|
// renderHelpOverlay builds a centered, scrollable help modal.
|
|
func (m *Model) renderHelpOverlay(contentWidth int) string {
|
|
innerW := m.helpContentWidth()
|
|
|
|
var b strings.Builder
|
|
|
|
loc := m.tr()
|
|
b.WriteString(m.styles.OverlayTitle.Render(loc.Help))
|
|
b.WriteString("\n\n")
|
|
|
|
// Viewport content (scrollable).
|
|
b.WriteString(m.helpViewport.View())
|
|
b.WriteString("\n")
|
|
|
|
pct := m.helpViewport.ScrollPercent()
|
|
var hint string
|
|
if pct <= 0 {
|
|
hint = loc.ScrollMore
|
|
} else if pct >= 1.0 {
|
|
hint = loc.ScrollClose
|
|
} else {
|
|
hint = fmt.Sprintf(loc.ScrollPct, pct*100)
|
|
}
|
|
b.WriteString(m.styles.OverlayDim.Render(hint))
|
|
|
|
// Wrap in a box.
|
|
box := lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(m.styles.FocusIndicator.GetForeground()).
|
|
Padding(1, 2).
|
|
Width(innerW + 6) // +6 for padding (2*2) + border (2)
|
|
|
|
return box.Render(b.String())
|
|
}
|
|
|
|
// overlayOnContent renders the overlay centered on the viewport area.
|
|
func (m *Model) overlayOnContent(base, overlay string) string {
|
|
baseLines := strings.Split(base, "\n")
|
|
overlayLines := strings.Split(overlay, "\n")
|
|
|
|
// Center vertically.
|
|
startY := (len(baseLines) - len(overlayLines)) / 2
|
|
if startY < 0 {
|
|
startY = 0
|
|
}
|
|
|
|
for i, ol := range overlayLines {
|
|
row := startY + i
|
|
if row >= len(baseLines) {
|
|
break
|
|
}
|
|
// Center horizontally.
|
|
olW := lipgloss.Width(ol)
|
|
padLeft := (m.width - olW) / 2
|
|
if padLeft < 0 {
|
|
padLeft = 0
|
|
}
|
|
baseLines[row] = strings.Repeat(" ", padLeft) + ol
|
|
}
|
|
|
|
return strings.Join(baseLines, "\n")
|
|
}
|
|
|
|
// commandHelpEntries extracts SkillInfo from commands for display.
|
|
func commandHelpEntries(reg *command.Registry) []struct{ Name, Desc string } {
|
|
var entries []struct{ Name, Desc string }
|
|
if reg == nil {
|
|
return entries
|
|
}
|
|
for _, cmd := range reg.All() {
|
|
entries = append(entries, struct{ Name, Desc string }{
|
|
Name: "/" + cmd.Name,
|
|
Desc: cmd.Description,
|
|
})
|
|
}
|
|
return entries
|
|
}
|