f7fc10ddf5
- Events were hard-capped to the last 7 per session in addEvent, so the e-inspector and EVENTS panel could never show more. Raise to 1000 (effectively all, bounded); the EVENTS panel now shows the latest that fit (tail), and the inspector scrolls a window around the selection with a position indicator. - The TUI had no approval.resolved handling at all — the decision frame was ignored and pending only cleared on session.resumed. Add the constant + case: clear the gate and record an ApprovalResolved event (APPROVED/REJECTED/AUTO_APPROVED + reason) into the stream. - Status bar now shows the current stage and session status alongside the name. Relabel the cryptic "N bg" badge to "N elsewhere".
585 lines
16 KiB
Go
585 lines
16 KiB
Go
package app
|
||
|
||
import (
|
||
"strings"
|
||
|
||
"github.com/charmbracelet/lipgloss"
|
||
"github.com/charmbracelet/x/ansi"
|
||
)
|
||
|
||
const (
|
||
statusH = 1
|
||
footerH = 1
|
||
inputH = 4 // rounded box: 2 borders + 2 content lines
|
||
)
|
||
|
||
// View renders the whole frame purely from the model (immediate-mode).
|
||
func (m Model) View() string {
|
||
if m.quitting {
|
||
return ""
|
||
}
|
||
if m.width < 20 || m.height < 8 {
|
||
return m.theme.Screen.Render("correx — terminal too small")
|
||
}
|
||
|
||
bottom := m.renderInput()
|
||
bottomH := inputH
|
||
if m.displayState() == StateApproval {
|
||
bottomH = m.approvalBandHeight()
|
||
bottom = m.renderApprovalBand(bottomH)
|
||
}
|
||
|
||
mainH := m.height - statusH - footerH - bottomH
|
||
if mainH < 3 {
|
||
mainH = 3
|
||
}
|
||
|
||
base := lipgloss.JoinVertical(lipgloss.Left,
|
||
m.renderStatus(),
|
||
m.renderMain(mainH),
|
||
bottom,
|
||
m.renderFooter(),
|
||
)
|
||
|
||
if m.overlay != OverlayNone {
|
||
return m.renderOverlay(base)
|
||
}
|
||
return base
|
||
}
|
||
|
||
// --- top status bar ---
|
||
|
||
func (m Model) renderStatus() string {
|
||
t := m.theme
|
||
bg := t.P.BgPanel
|
||
span := func(s string, fg lipgloss.Color) string {
|
||
return lipgloss.NewStyle().Foreground(fg).Background(bg).Render(s)
|
||
}
|
||
sep := span(" · ", t.P.Faint)
|
||
|
||
parts := []string{span("correx", t.P.Dim)}
|
||
if m.connected {
|
||
parts = append(parts, span("●", t.P.OK)+span(" connected", t.P.Dim))
|
||
} else if m.reconnecting {
|
||
parts = append(parts, span("◌", t.P.Warn)+span(" reconnecting", t.P.Warn))
|
||
} else {
|
||
parts = append(parts, span("○", t.P.Bad)+span(" disconnected", t.P.Bad))
|
||
}
|
||
|
||
if s := m.session(m.selectedID); s != nil && m.displayState() != StateIdle {
|
||
parts = append(parts, span(s.Name, t.P.FgStrong))
|
||
if s.CurrentStage != "" {
|
||
parts = append(parts, span("⟐ "+s.CurrentStage, t.P.Accent2))
|
||
}
|
||
if s.Status != "" {
|
||
parts = append(parts, lipgloss.NewStyle().Foreground(statusColor(t, s.Status)).Background(bg).Render(statusLabel(s.Status)))
|
||
}
|
||
}
|
||
|
||
model := m.currentModel
|
||
if model == "" {
|
||
model = "no model"
|
||
}
|
||
loc := "local"
|
||
if m.providerType == "REMOTE" {
|
||
loc = "remote"
|
||
}
|
||
parts = append(parts, span(model, t.P.Fg)+span(" ("+loc+")", t.P.Faint))
|
||
left := strings.Join(parts, sep)
|
||
|
||
var right []string
|
||
if g := m.gaugeText(); g != "" {
|
||
right = append(right, span(g, t.P.Faint))
|
||
}
|
||
if s := m.session(m.selectedID); s != nil && s.Active {
|
||
right = append(right, lipgloss.NewStyle().Foreground(t.P.Accent).Background(bg).Render(m.spinner())+span(" active", t.P.Accent2))
|
||
}
|
||
if m.bgUpdates > 0 {
|
||
right = append(right, span(itoa(m.bgUpdates)+" elsewhere", t.P.Warn))
|
||
}
|
||
if len(right) == 0 {
|
||
return m.fill(left, m.width, bg)
|
||
}
|
||
return m.justify(left, strings.Join(right, span(" · ", t.P.Faint)), m.width, bg)
|
||
}
|
||
|
||
// gaugeText renders the live VRAM/GPU/RAM gauge, omitting any unavailable field.
|
||
func (m Model) gaugeText() string {
|
||
var parts []string
|
||
if m.gpuUsedMB != nil && m.gpuTotalMB != nil {
|
||
parts = append(parts, "VRAM "+itoa(int(*m.gpuUsedMB))+"/"+itoa(int(*m.gpuTotalMB))+"M")
|
||
}
|
||
if m.gpuUtil != nil {
|
||
parts = append(parts, "GPU "+itoa(*m.gpuUtil)+"%")
|
||
}
|
||
if m.ramMB != nil {
|
||
parts = append(parts, "RAM "+itoa(int(*m.ramMB))+"M")
|
||
}
|
||
return strings.Join(parts, " ")
|
||
}
|
||
|
||
var spinnerFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||
|
||
func (m Model) spinner() string { return spinnerFrames[m.frame%len(spinnerFrames)] }
|
||
|
||
// caretVisible blinks the insert caret roughly twice a second.
|
||
func (m Model) caretVisible() bool { return (m.frame/4)%2 == 0 }
|
||
|
||
// --- bottom footer hints ---
|
||
|
||
func (m Model) renderFooter() string {
|
||
t := m.theme
|
||
bg := t.P.BgDeep
|
||
hint := func(key, label string) string {
|
||
k := lipgloss.NewStyle().Foreground(t.P.Accent).Background(bg).Bold(true).Render(key)
|
||
l := lipgloss.NewStyle().Foreground(t.P.Faint).Background(bg).Render(" " + label)
|
||
return k + l
|
||
}
|
||
gap := lipgloss.NewStyle().Background(bg).Render(" ")
|
||
|
||
var hints []string
|
||
switch {
|
||
case m.editMode == ModeInsert:
|
||
hints = []string{hint("enter", "send"), hint("esc", "normal"), hint("↑↓", "history")}
|
||
case m.displayState() == StateApproval:
|
||
// Approval actions live in the band itself; the footer offers navigation only.
|
||
hints = []string{hint("l", "back"), hint("e", "events"), hint("q", "quit")}
|
||
case m.displayState() == StateIdle:
|
||
hints = []string{hint("i", "name"), hint("/", "filter"), hint("enter", "open"), hint("jk", "move"), hint("w", "workflows"), hint("p", "cmds"), hint("q", "quit")}
|
||
default: // StateInSession
|
||
hints = []string{hint("i", "message"), hint("e", "events"), hint("t", "tools"), hint("m", "model"), hint("s", "mode"), hint("l", "back"), hint("p", "cmds"), hint("q", "quit")}
|
||
if s := m.session(m.selectedID); s != nil && s.Pending != nil {
|
||
hints = append(hints, hint("a", "approval (pending)"))
|
||
}
|
||
}
|
||
left := strings.Join(hints, gap)
|
||
|
||
right := lipgloss.NewStyle().Foreground(t.P.Dim).Background(bg).Render("Soft") +
|
||
lipgloss.NewStyle().Foreground(t.P.Faint).Background(bg).Render(" · ") +
|
||
lipgloss.NewStyle().Foreground(t.P.Accent).Background(bg).Render("blue")
|
||
|
||
return m.justify(left, right, m.width, bg)
|
||
}
|
||
|
||
// --- main split ---
|
||
|
||
func (m Model) renderMain(h int) string {
|
||
leftW := m.width * 63 / 100
|
||
rightW := m.width - leftW
|
||
|
||
var leftTitle, rightTitle string
|
||
var leftBody, rightBody []string
|
||
leftActive, rightActive := false, false
|
||
|
||
switch m.displayState() {
|
||
case StateIdle:
|
||
leftTitle = "sessions"
|
||
if m.wfVisible {
|
||
leftTitle = "workflows"
|
||
leftBody = m.workflowRows(leftW - 4)
|
||
} else {
|
||
leftBody = m.sessionRows(leftW - 4)
|
||
}
|
||
leftActive = true
|
||
rightTitle = "welcome"
|
||
rightBody = m.welcomeRows(rightW - 4)
|
||
case StateInSession, StateApproval:
|
||
leftTitle = "output"
|
||
leftBody = m.routerRows(leftW-4, h-2)
|
||
leftActive = true
|
||
rightTitle = "events"
|
||
rightBody = m.eventRows(rightW-4, h)
|
||
}
|
||
|
||
left := m.theme.box(leftTitle, leftBody, leftW, h, leftActive)
|
||
right := m.theme.box(rightTitle, rightBody, rightW, h, rightActive)
|
||
return lipgloss.JoinHorizontal(lipgloss.Top, left, right)
|
||
}
|
||
|
||
// --- input bar ---
|
||
|
||
func (m Model) renderInput() string {
|
||
t := m.theme
|
||
|
||
placeholder := "Ask anything…"
|
||
switch {
|
||
case m.inputMode == ModeFilter:
|
||
placeholder = "Filter sessions…"
|
||
case m.displayState() == StateIdle:
|
||
placeholder = "Session name…"
|
||
}
|
||
|
||
insert := m.editMode == ModeInsert
|
||
caret := t.span(" ", t.P.Bg)
|
||
if insert && m.caretVisible() {
|
||
caret = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Render("▏")
|
||
}
|
||
var line string
|
||
switch {
|
||
case m.inputBuffer == "" && !insert:
|
||
line = t.span(placeholder, t.P.Faint)
|
||
case m.inputBuffer == "":
|
||
line = caret + t.span(placeholder, t.P.Faint)
|
||
default:
|
||
line = t.span(m.inputBuffer, t.P.FgStrong) + caret
|
||
}
|
||
|
||
// status sub-line: <session/none> · <uuid> · <mode>
|
||
name := "(no session)"
|
||
if s := m.session(m.selectedID); s != nil {
|
||
name = s.Name
|
||
}
|
||
mode := "chat"
|
||
if m.chatMode == ChatModeSteering {
|
||
mode = "steering"
|
||
}
|
||
if m.inputMode == ModeFilter {
|
||
mode = "filter"
|
||
}
|
||
sub := lipgloss.NewStyle().Foreground(t.P.Accent2).Background(t.P.Bg).Render(name)
|
||
if m.selectedID != "" {
|
||
sub += t.span(" · ", t.P.Faint) +
|
||
t.span(m.selectedID, t.P.Faint)
|
||
}
|
||
sub += t.span(" · ", t.P.Faint) +
|
||
t.span(mode, t.P.Dim)
|
||
|
||
body := []string{line, sub}
|
||
return t.box("", body, m.width, inputH, insert)
|
||
}
|
||
|
||
// --- body row builders ---
|
||
|
||
func (m Model) sessionRows(w int) []string {
|
||
t := m.theme
|
||
list := m.filteredSessions()
|
||
if len(list) == 0 {
|
||
return []string{t.span("no sessions yet", t.P.Faint), "", t.span("type a name below to begin", t.P.Faint)}
|
||
}
|
||
rows := make([]string, 0, len(list))
|
||
for _, s := range list {
|
||
sel := s.ID == m.selectedID && !m.wfVisible
|
||
short := s.ID
|
||
if len(short) > 6 {
|
||
short = short[:6]
|
||
}
|
||
nameFg := t.P.Fg
|
||
bar := t.span(" ", t.P.Bg)
|
||
if sel {
|
||
bar = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Render("▌ ")
|
||
nameFg = t.P.FgStrong
|
||
}
|
||
idCell := t.span("["+short+"] ", t.P.Faint)
|
||
statusCell := lipgloss.NewStyle().Foreground(statusColor(t, s.Status)).Background(t.P.Bg).Bold(true).
|
||
Render(padRaw(statusLabel(s.Status), 9))
|
||
nameCell := lipgloss.NewStyle().Foreground(nameFg).Background(t.P.Bg).Render(" " + s.Name)
|
||
stage := ""
|
||
if s.CurrentStage != "" {
|
||
stage = t.span(" ["+s.CurrentStage+"]", t.P.Dim)
|
||
}
|
||
rows = append(rows, bar+idCell+statusCell+nameCell+stage)
|
||
}
|
||
return rows
|
||
}
|
||
|
||
func (m Model) workflowRows(w int) []string {
|
||
t := m.theme
|
||
if len(m.workflows) == 0 {
|
||
return []string{t.span("no workflows advertised", t.P.Faint)}
|
||
}
|
||
rows := make([]string, 0, len(m.workflows))
|
||
for i, wf := range m.workflows {
|
||
marker := t.span(" ", t.P.Bg)
|
||
fg := t.P.Fg
|
||
if i == m.wfIndex {
|
||
marker = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Render("▸ ")
|
||
fg = t.P.FgStrong
|
||
}
|
||
rows = append(rows, marker+lipgloss.NewStyle().Foreground(fg).Background(t.P.Bg).Render(wf.ID)+t.span(" "+wf.Description, t.P.Faint))
|
||
}
|
||
return rows
|
||
}
|
||
|
||
func (m Model) welcomeRows(w int) []string {
|
||
t := m.theme
|
||
title := lipgloss.NewStyle().Foreground(t.P.FgStrong).Background(t.P.Bg).Bold(true).Render("Corre") +
|
||
lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Bold(true).Render("x") +
|
||
lipgloss.NewStyle().Foreground(t.P.FgStrong).Background(t.P.Bg).Bold(true).Render(" Agent Harness")
|
||
|
||
var status string
|
||
if m.connected {
|
||
status = lipgloss.NewStyle().Foreground(t.P.OK).Background(t.P.Bg).Render("Connected") +
|
||
t.span(" · ", t.P.Faint) + t.span(plural(len(m.sessions), "session"), t.P.Dim)
|
||
} else {
|
||
status = lipgloss.NewStyle().Foreground(t.P.Bad).Background(t.P.Bg).Render("Disconnected")
|
||
}
|
||
|
||
rows := []string{title, status, "",
|
||
t.span("Select a session from the left", t.P.Dim),
|
||
t.span("or type a name to start a new one.", t.P.Dim), ""}
|
||
|
||
if n := len(m.sessions); n > 0 {
|
||
rows = append(rows, t.span("recent:", t.P.Faint))
|
||
start := 0
|
||
if n > 5 {
|
||
start = n - 5
|
||
}
|
||
for _, s := range m.sessions[start:] {
|
||
mark := statusGlyph(t, s.Status)
|
||
rows = append(rows, t.span(" "+s.Name+" ", t.P.Fg)+t.span(formatTime(s.LastEventAt), t.P.Faint)+" "+mark)
|
||
}
|
||
}
|
||
return rows
|
||
}
|
||
|
||
func (m Model) routerRows(w, h int) []string {
|
||
t := m.theme
|
||
msgs := m.routerMessages[m.selectedID]
|
||
if len(msgs) == 0 {
|
||
return []string{t.span("awaiting router response…", t.P.Faint)}
|
||
}
|
||
var rows []string
|
||
for _, e := range msgs {
|
||
switch e.Role {
|
||
case "user":
|
||
tag := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Bold(true).Render("›")
|
||
rows = append(rows, tag+" "+t.span(e.Content, t.P.FgStrong))
|
||
case "router":
|
||
for _, ln := range wrap(e.Content, w) {
|
||
rows = append(rows, t.span(ln, t.P.Fg))
|
||
}
|
||
case "tool":
|
||
rows = append(rows, t.span("· tool output ("+itoaLen(e.Content)+" chars) — ^x to view", t.P.Dim))
|
||
}
|
||
}
|
||
// keep last h rows
|
||
if len(rows) > h {
|
||
rows = rows[len(rows)-h:]
|
||
}
|
||
return rows
|
||
}
|
||
|
||
func (m Model) eventRows(w, h int) []string {
|
||
t := m.theme
|
||
header := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Bold(true).Render("event stream") +
|
||
t.span(" ", t.P.Bg)
|
||
if m.connected {
|
||
header += lipgloss.NewStyle().Foreground(t.P.OK).Background(t.P.Bg).Render("● live")
|
||
} else {
|
||
header += t.span("○ idle", t.P.Faint)
|
||
}
|
||
|
||
s := m.session(m.selectedID)
|
||
if s == nil || len(s.Events) == 0 {
|
||
return []string{header, "", t.span("no events yet", t.P.Faint)}
|
||
}
|
||
evRows := make([]string, 0, len(s.Events))
|
||
for _, e := range s.Events {
|
||
cat := inferCategory(e.Type)
|
||
catCell := lipgloss.NewStyle().Foreground(t.categoryColor(cat)).Background(t.P.BgPanel).
|
||
Render(" " + padRaw(cat, 9) + " ")
|
||
evRows = append(evRows,
|
||
t.span(e.Time+" ", t.P.Faint)+catCell+" "+
|
||
t.span(padRaw(e.Type, 18), t.P.Fg)+t.span(e.Detail, t.P.Dim))
|
||
}
|
||
// Show the latest events that fit under the 2-line header (box inner = h-2).
|
||
if avail := h - 4; avail >= 1 && len(evRows) > avail {
|
||
evRows = evRows[len(evRows)-avail:]
|
||
}
|
||
return append([]string{header, ""}, evRows...)
|
||
}
|
||
|
||
// --- width helpers ---
|
||
|
||
// fill left-justifies a styled line and pads with bg to width w.
|
||
func (m Model) fill(s string, w int, bg lipgloss.Color) string {
|
||
return " " + padTo(s, w-1, bg)
|
||
}
|
||
|
||
// justify places left and right segments on a bg-filled line of width w.
|
||
func (m Model) justify(left, right string, w int, bg lipgloss.Color) string {
|
||
lw := lipgloss.Width(left)
|
||
rw := lipgloss.Width(right)
|
||
mid := w - lw - rw - 2
|
||
if mid < 1 {
|
||
mid = 1
|
||
}
|
||
pad := lipgloss.NewStyle().Background(bg).Render(strings.Repeat(" ", mid))
|
||
return " " + left + pad + right + lipgloss.NewStyle().Background(bg).Render(" ")
|
||
}
|
||
|
||
// padTo pads (or truncates) a possibly-styled string to visible width w, filling
|
||
// with the given background so the panel stays opaque.
|
||
func padTo(s string, w int, bg lipgloss.Color) string {
|
||
vw := lipgloss.Width(s)
|
||
if vw == w {
|
||
return s
|
||
}
|
||
if vw > w {
|
||
return ansi.Truncate(s, w, "")
|
||
}
|
||
return s + lipgloss.NewStyle().Background(bg).Render(strings.Repeat(" ", w-vw))
|
||
}
|
||
|
||
// padRaw pads a plain (unstyled) string to width w with spaces, truncating with
|
||
// an ellipsis only when it genuinely overflows.
|
||
func padRaw(s string, w int) string {
|
||
n := len([]rune(s))
|
||
if n == w {
|
||
return s
|
||
}
|
||
if n > w {
|
||
if w <= 1 {
|
||
return string([]rune(s)[:w])
|
||
}
|
||
return string([]rune(s)[:w-1]) + "…"
|
||
}
|
||
return s + strings.Repeat(" ", w-n)
|
||
}
|
||
|
||
func statusColor(t Theme, status string) lipgloss.Color {
|
||
u := strings.ToUpper(status)
|
||
switch {
|
||
case strings.Contains(u, "FAIL"):
|
||
return t.P.Bad
|
||
case strings.Contains(u, "COMPLETE"):
|
||
return t.P.Dim
|
||
case strings.Contains(u, "PAUSE"), strings.Contains(u, "STARTING"):
|
||
return t.P.Warn
|
||
case strings.Contains(u, "CHAT"), strings.Contains(u, "ACTIVE"):
|
||
return t.P.Accent
|
||
default:
|
||
return t.P.Dim
|
||
}
|
||
}
|
||
|
||
func statusLabel(status string) string {
|
||
u := strings.ToUpper(status)
|
||
if i := strings.Index(u, " "); i > 0 {
|
||
return u[:i]
|
||
}
|
||
return u
|
||
}
|
||
|
||
func statusGlyph(t Theme, status string) string {
|
||
u := strings.ToUpper(status)
|
||
switch {
|
||
case strings.Contains(u, "FAIL"):
|
||
return lipgloss.NewStyle().Foreground(t.P.Bad).Background(t.P.Bg).Render("✗")
|
||
case strings.Contains(u, "COMPLETE"):
|
||
return lipgloss.NewStyle().Foreground(t.P.OK).Background(t.P.Bg).Render("✓")
|
||
default:
|
||
return t.span("·", t.P.Faint)
|
||
}
|
||
}
|
||
|
||
// span renders text with foreground fg over the panel background.
|
||
func (t Theme) span(s string, fg lipgloss.Color) string {
|
||
return lipgloss.NewStyle().Foreground(fg).Background(t.P.Bg).Render(s)
|
||
}
|
||
|
||
// box draws a rounded, opaque panel with the title embedded in the top border.
|
||
func (t Theme) box(title string, body []string, w, h int, active bool) string {
|
||
if w < 4 {
|
||
w = 4
|
||
}
|
||
if h < 2 {
|
||
h = 2
|
||
}
|
||
inner := w - 2
|
||
textW := inner - 2
|
||
bc := t.P.Border
|
||
if active {
|
||
bc = t.P.BorderHi
|
||
}
|
||
bs := lipgloss.NewStyle().Foreground(bc).Background(t.P.Bg)
|
||
cell := lipgloss.NewStyle().Background(t.P.Bg)
|
||
|
||
var top string
|
||
if title == "" {
|
||
top = bs.Render("╭" + strings.Repeat("─", inner) + "╮")
|
||
} else {
|
||
ts := t.PanelTitle
|
||
if active {
|
||
ts = t.TitleHi
|
||
}
|
||
label := strings.ToUpper(title)
|
||
fill := inner - (2 + lipgloss.Width(label) + 1)
|
||
if fill < 0 {
|
||
fill = 0
|
||
}
|
||
top = bs.Render("╭─ ") + ts.Render(label) + bs.Render(" "+strings.Repeat("─", fill)) + bs.Render("╮")
|
||
}
|
||
lines := make([]string, 0, h)
|
||
lines = append(lines, top)
|
||
for i := 0; i < h-2; i++ {
|
||
content := ""
|
||
if i < len(body) {
|
||
content = body[i]
|
||
}
|
||
padded := padTo(content, textW, t.P.Bg)
|
||
lines = append(lines, bs.Render("│")+cell.Render(" ")+padded+cell.Render(" ")+bs.Render("│"))
|
||
}
|
||
lines = append(lines, bs.Render("╰"+strings.Repeat("─", inner)+"╯"))
|
||
return strings.Join(lines, "\n")
|
||
}
|
||
|
||
func plural(n int, word string) string {
|
||
s := itoa(n) + " " + word
|
||
if n != 1 {
|
||
s += "s"
|
||
}
|
||
return s
|
||
}
|
||
|
||
func itoa(n int) string {
|
||
if n == 0 {
|
||
return "0"
|
||
}
|
||
neg := n < 0
|
||
if neg {
|
||
n = -n
|
||
}
|
||
var b [20]byte
|
||
i := len(b)
|
||
for n > 0 {
|
||
i--
|
||
b[i] = byte('0' + n%10)
|
||
n /= 10
|
||
}
|
||
if neg {
|
||
i--
|
||
b[i] = '-'
|
||
}
|
||
return string(b[i:])
|
||
}
|
||
|
||
func itoaLen(s string) string { return itoa(len(s)) }
|
||
|
||
// wrap splits s into lines no wider than w (rune-based, whitespace-greedy).
|
||
func wrap(s string, w int) []string {
|
||
if w < 1 {
|
||
w = 1
|
||
}
|
||
words := strings.Fields(s)
|
||
if len(words) == 0 {
|
||
return []string{""}
|
||
}
|
||
var lines []string
|
||
cur := ""
|
||
for _, word := range words {
|
||
if cur == "" {
|
||
cur = word
|
||
} else if len(cur)+1+len(word) <= w {
|
||
cur += " " + word
|
||
} else {
|
||
lines = append(lines, cur)
|
||
cur = word
|
||
}
|
||
}
|
||
if cur != "" {
|
||
lines = append(lines, cur)
|
||
}
|
||
return lines
|
||
}
|