1package tui
2
3import "strings"
4
5const (
6 InputStylePlain = "plain"
7 InputStyleLines = "lines"
8 InputStyleBlock = "block"
9
10 StatusPositionAboveInput = "above_input"
11 StatusPositionBelowInput = "below_input"
12
13 WorkingPositionAboveInput = "above_input"
14 WorkingPositionBelowInput = "below_input"
15
16 SubagentPositionAboveInput = "above_input"
17 SubagentPositionBelowInput = "below_input"
18)
19
20func NormalizeInputStyle(v string) string {
21 switch strings.ToLower(strings.TrimSpace(v)) {
22 case InputStyleLines, "line":
23 return InputStyleLines
24 case InputStyleBlock, "bubble", "boxed", "box":
25 return InputStyleBlock
26 default:
27 return InputStylePlain
28 }
29}
30
31func NormalizeStatusPosition(v string) string {
32 switch strings.ToLower(strings.TrimSpace(v)) {
33 case StatusPositionBelowInput, "below", "bottom":
34 return StatusPositionBelowInput
35 default:
36 return StatusPositionAboveInput
37 }
38}
39
40func NormalizeWorkingPosition(v string) string {
41 switch strings.ToLower(strings.TrimSpace(v)) {
42 case WorkingPositionBelowInput, "below", "bottom":
43 return WorkingPositionBelowInput
44 default:
45 return WorkingPositionAboveInput
46 }
47}
48
49// NormalizeSubagentPosition returns the placement for live subagent activity.
50// A missing value keeps activity immediately below the input by default.
51func NormalizeSubagentPosition(v string) string {
52 switch strings.ToLower(strings.TrimSpace(v)) {
53 case SubagentPositionAboveInput, "above", "top":
54 return SubagentPositionAboveInput
55 default:
56 return SubagentPositionBelowInput
57 }
58}
59
60func InputLines(th Theme, lines []string, width int) []string {
61 if width < 1 {
62 width = 1
63 }
64 rule := th.FGColor(th.Muted, strings.Repeat("─", width))
65 out := make([]string, 0, len(lines)+2)
66 out = append(out, rule)
67 out = append(out, lines...)
68 out = append(out, rule)
69 return out
70}
71
72func InputBlock(th Theme, lines []string, width int) []string {
73 if width < 1 {
74 width = 1
75 }
76 bubbleW := width - 2
77 if bubbleW < 1 {
78 bubbleW = 1
79 }
80 bg := th.sgrBGColor(th.UserBubbleBG)
81 bar := bg + th.FGColor(th.Accent, "▌ ")
82 row := func(content string) string {
83 visible := visibleWidth(content)
84 if visible < bubbleW {
85 content += strings.Repeat(" ", bubbleW-visible)
86 }
87 body := bg + strings.ReplaceAll(content, reset, reset+bg) + reset
88 return bar + body
89 }
90 out := make([]string, 0, len(lines)+2)
91 out = append(out, row(""))
92 for _, line := range lines {
93 out = append(out, row(line))
94 }
95 out = append(out, row(""))
96 return out
97}
98
99// CursorColor returns OSC 12 to set the terminal cursor color. Unlike SGR
100// styling, the cursor is terminal-owned, so modal backdrops must set this
101// separately from their dimmed text rows.
102func CursorColor(color TerminalColor) string {
103 rgb, ok := rgbForTerminalColor(color)
104 if !ok {
105 return ""
106 }
107 return "\x1b]12;rgb:" + hexByte(rgb[0]) + "/" + hexByte(rgb[1]) + "/" + hexByte(rgb[2]) + "\x07"
108}
109
110func CursorColor256(index int) string { return CursorColor(Color256(index)) }
111
112func ResetCursorColor() string { return "\x1b]112\x07" }
113
114func CursorShapeBlock() string { return "\x1b[1 q" }
115
116func ResetCursorShape() string { return "\x1b[0 q" }
117
118func xterm256RGB(index int) (int, int, int) {
119 if index < 0 {
120 index = 0
121 }
122 if index > 255 {
123 index = 255
124 }
125 ansi := [16][3]int{
126 {0, 0, 0}, {128, 0, 0}, {0, 128, 0}, {128, 128, 0},
127 {0, 0, 128}, {128, 0, 128}, {0, 128, 128}, {192, 192, 192},
128 {128, 128, 128}, {255, 0, 0}, {0, 255, 0}, {255, 255, 0},
129 {0, 0, 255}, {255, 0, 255}, {0, 255, 255}, {255, 255, 255},
130 }
131 if index < 16 {
132 c := ansi[index]
133 return c[0], c[1], c[2]
134 }
135 if index < 232 {
136 idx := index - 16
137 levels := [6]int{0, 95, 135, 175, 215, 255}
138 return levels[idx/36], levels[(idx/6)%6], levels[idx%6]
139 }
140 gray := 8 + (index-232)*10
141 return gray, gray, gray
142}
143
144func hexByte(v int) string {
145 const digits = "0123456789abcdef"
146 if v < 0 {
147 v = 0
148 }
149 if v > 255 {
150 v = 255
151 }
152 return string([]byte{digits[v>>4], digits[v&0xf]})
153}