package tui import ( "strings" "github.com/charmbracelet/lipgloss" ) func (m *Model) appendOutput(text string) { if text == "" { return } m.content.WriteString(text) if m.ready { m.viewport.SetContent(m.content.String()) if m.followTail { m.viewport.GotoBottom() } } } func (m *Model) renderPrompt() string { lines := []boxLine{ {text: m.promptHint, style: modalBodyLineStyle()}, {text: m.promptInput.View(), style: modalBodyLineStyle()}, {text: "Enter submit | Esc cancel", style: modalFooterLineStyle()}, } return renderBox(m.promptTitle, lines, 48, m.availableModalWidth()) } func renderModal(title, body string, maxWidth int) string { if title == "" { title = "Info" } parts := strings.Split(strings.ReplaceAll(body, "\r\n", "\n"), "\n") if len(parts) > 12 { parts = append(parts[:12], "... (press Esc/Enter to close)") } lines := make([]boxLine, 0, len(parts)) for _, part := range parts { lines = append(lines, boxLine{text: part, style: modalBodyLineStyle()}) } return renderBox(title, lines, 20, maxWidth) } func renderPanelModal(title string, lines []panelLine, footer string, maxWidth int) string { boxLines := make([]boxLine, 0, len(lines)+1) for _, line := range lines { style := modalBodyLineStyle() prefix := " " if line.selected { style = selectedPanelLineStyle() prefix = "▸ " } boxLines = append(boxLines, boxLine{text: prefix + line.text, style: style}) } boxLines = append(boxLines, boxLine{text: footer, style: modalFooterLineStyle()}) return renderBox(title, boxLines, 40, maxWidth) } func fillScreen(width, height int, content string) string { if width <= 0 || height <= 0 { return content } return lipgloss.Place(width, height, lipgloss.Left, lipgloss.Top, content, lipgloss.WithWhitespaceChars(" "), lipgloss.WithWhitespaceForeground(lipgloss.Color("0")), ) } func renderCenteredModal(width, height int, title, body string) string { maxWidth := width - 8 if maxWidth < 20 { maxWidth = 20 } return renderCenteredModalContent(width, height, renderModal(title, body, maxWidth)) } func renderCenteredModalContent(width, height int, content string) string { if width <= 0 || height <= 0 { return content } return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, content, lipgloss.WithWhitespaceChars(" "), lipgloss.WithWhitespaceForeground(lipgloss.Color("0")), ) } func (m *Model) availableModalWidth() int { if m.width <= 0 { return 100 } maxWidth := m.width - 8 if maxWidth < 20 { maxWidth = 20 } return maxWidth } type boxLine struct { text string style lipgloss.Style } func renderBox(title string, lines []boxLine, minWidth, maxWidth int) string { contentWidth := lipgloss.Width(title) for _, line := range lines { contentWidth = maxInt(contentWidth, lipgloss.Width(line.text)) } contentWidth = maxInt(minWidth, contentWidth) contentWidth = minInt(contentWidth, maxWidth) boxStyle := lipgloss.NewStyle().Background(lipgloss.Color("236")) top := boxStyle.Render("╭" + strings.Repeat("─", contentWidth+2) + "╮") bottom := boxStyle.Render("╰" + strings.Repeat("─", contentWidth+2) + "╯") rows := make([]string, 0, len(lines)+3) rows = append(rows, top) rows = append(rows, renderBoxRow(modalHeaderLineStyle(), title, contentWidth)) for _, line := range lines { rows = append(rows, renderBoxRow(line.style, truncateToWidth(line.text, contentWidth), contentWidth)) } rows = append(rows, bottom) return strings.Join(rows, "\n") } func renderBoxRow(contentStyle lipgloss.Style, text string, width int) string { visible := truncateToWidth(text, width) pad := strings.Repeat(" ", maxInt(0, width-lipgloss.Width(visible))) inner := contentStyle.Render(" " + visible + pad + " ") return contentStyle.Render("│" + inner + "│") } func modalHeaderLineStyle() lipgloss.Style { return lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("230")).Background(lipgloss.Color("25")) } func modalBodyLineStyle() lipgloss.Style { return lipgloss.NewStyle().Foreground(lipgloss.Color("252")).Background(lipgloss.Color("236")) } func modalFooterLineStyle() lipgloss.Style { return lipgloss.NewStyle().Foreground(lipgloss.Color("250")).Background(lipgloss.Color("236")) } func selectedPanelLineStyle() lipgloss.Style { return lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("230")).Background(lipgloss.Color("31")) } func truncateToWidth(s string, width int) string { if width <= 0 || lipgloss.Width(s) <= width { return s } var b strings.Builder for _, r := range s { next := b.String() + string(r) if lipgloss.Width(next) > width { break } b.WriteRune(r) } return b.String() } func clampIndex(idx, n int) int { if n <= 0 || idx < 0 { return 0 } if idx >= n { return n - 1 } return idx } func minInt(a, b int) int { if a < b { return a } return b } func maxInt(a int, rest ...int) int { max := a for _, v := range rest { if v > max { max = v } } return max }