admin 8dc496b626
Some checks failed
CI / test (push) Has been cancelled
Release / release (push) Failing after 4m36s
first commit
2026-03-08 15:40:34 +07:00

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
}