Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.

Commit d9f7a41

Browse files
committed
wip focus and changes
1 parent 67529e5 commit d9f7a41

17 files changed

Lines changed: 463 additions & 158 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ require (
1313
github.com/bmatcuk/doublestar/v4 v4.8.1
1414
github.com/catppuccin/go v0.3.0
1515
github.com/charlievieth/fastwalk v1.0.11
16-
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250526131538-b3f0c9e42318
16+
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603125125-87aee03b3d4f
1717
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174
1818
github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
1919
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c

go.sum

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr
7272
github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
7373
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250526131538-b3f0c9e42318 h1:f8Q0ybZGxT+St1JfPM7yoz/XFpbmtodcIehaom/9XT8=
7474
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250526131538-b3f0c9e42318/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
75-
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250526132317-434f93986a5c h1:EoW1x1K2EDKYw1D7raqZqWKnwk21IZVpYqLHQVhz1ZU=
76-
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250526132317-434f93986a5c/go.mod h1:sXuGtrlVJo43r1fVGBM06E7PPb16oBl8rDRr6YgQOck=
77-
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154534-5681225ad367 h1:X+w3YtXyLG3oguOKXvcDT8jQP856YLQsq6SwTE+gqTk=
78-
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154534-5681225ad367/go.mod h1:oOn1YZGZyJHxJfh4sFAna9vDzxJRNuErLETr/lnlB/I=
75+
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603122936-f1a3fad2b64e h1:+3I/1v7vbN0Vf8Tjm3Q0zdLQqjOM/TjQBvoRDQtoAss=
76+
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603122936-f1a3fad2b64e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
77+
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603125125-87aee03b3d4f h1:vvNB+i59Wp3L6gYcpuhfAdNjr4/e6qM/st3ySWfmZnU=
78+
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603125125-87aee03b3d4f/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
7979
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174 h1:TlVW+df0rdU/osP0O8DIVS9WFOAzXe3nuiMwJR4n+CA=
8080
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174/go.mod h1:oOn1YZGZyJHxJfh4sFAna9vDzxJRNuErLETr/lnlB/I=
8181
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
@@ -84,8 +84,6 @@ github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe h1:i6ce4C
8484
github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe/go.mod h1:p3Q+aN4eQKeM5jhrmXPMgPrlKbmc59rWSnMsSA3udhk=
8585
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c h1:177KMz8zHRlEZJsWzafbKYh6OdjgvTspoH+UjaxgIXY=
8686
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ=
87-
github.com/charmbracelet/x/ansi v0.9.3-0.20250516160309-24eee56f89fa h1:JU05TLAB6nOEL46bxHDV/+e8umBX32ODsGbVkc7o7bk=
88-
github.com/charmbracelet/x/ansi v0.9.3-0.20250516160309-24eee56f89fa/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
8987
github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413 h1:L07QkDqRF274IZ2UJ/mCTL8DR95efU9BNWLYCDXEjvQ=
9088
github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
9189
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=

internal/tui/components/chat/editor/editor.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,10 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
263263
func (m *editorCmp) View() tea.View {
264264
t := styles.CurrentTheme()
265265
cursor := m.textarea.Cursor()
266-
cursor.X = cursor.X + m.x + 1
267-
cursor.Y = cursor.Y + m.y + 1 // adjust for padding
266+
if cursor != nil {
267+
cursor.X = cursor.X + m.x + 1
268+
cursor.Y = cursor.Y + m.y + 1 // adjust for padding
269+
}
268270
if len(m.attachments) == 0 {
269271
content := t.S().Base.Padding(1).Render(
270272
m.textarea.View(),
@@ -358,11 +360,15 @@ func CreateTextArea(existing *textarea.Model) textarea.Model {
358360
t := styles.CurrentTheme()
359361
ta := textarea.New()
360362
ta.SetStyles(t.S().TextArea)
361-
ta.SetPromptFunc(4, func(lineIndex int) string {
363+
ta.SetPromptFunc(4, func(lineIndex int, focused bool) string {
362364
if lineIndex == 0 {
363365
return " > "
364366
}
365-
return t.S().Base.Foreground(t.Blue).Render("::: ")
367+
if focused {
368+
return t.S().Base.Foreground(t.Blue).Render("::: ")
369+
} else {
370+
return t.S().Muted.Render("::: ")
371+
}
366372
})
367373
ta.ShowLineNumbers = false
368374
ta.CharLimit = -1
@@ -379,6 +385,23 @@ func CreateTextArea(existing *textarea.Model) textarea.Model {
379385
return ta
380386
}
381387

388+
// Blur implements Container.
389+
func (c *editorCmp) Blur() tea.Cmd {
390+
c.textarea.Blur()
391+
return nil
392+
}
393+
394+
// Focus implements Container.
395+
func (c *editorCmp) Focus() tea.Cmd {
396+
logging.Info("Focusing editor textarea")
397+
return c.textarea.Focus()
398+
}
399+
400+
// IsFocused implements Container.
401+
func (c *editorCmp) IsFocused() bool {
402+
return c.textarea.Focused()
403+
}
404+
382405
func NewEditorCmp(app *app.App) util.Model {
383406
ta := CreateTextArea(nil)
384407
return &editorCmp{

internal/tui/components/core/helpers.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ func Title(title string, width int) string {
2626
char := "╱"
2727
length := lipgloss.Width(title) + 1
2828
remainingWidth := width - length
29-
lineStyle := t.S().Base.Foreground(t.Primary)
3029
titleStyle := t.S().Base.Foreground(t.Primary)
3130
if remainingWidth > 0 {
32-
title = titleStyle.Render(title) + " " + lineStyle.Render(strings.Repeat(char, remainingWidth))
31+
lines := strings.Repeat(char, remainingWidth)
32+
lines = styles.ApplyForegroundGrad(lines, t.Primary, t.Secondary)
33+
title = titleStyle.Render(title) + " " + lines
3334
}
3435
return title
3536
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package status
2+
3+
import (
4+
"github.com/charmbracelet/bubbles/v2/key"
5+
"github.com/opencode-ai/opencode/internal/tui/layout"
6+
)
7+
8+
type KeyMap struct {
9+
Tab,
10+
Commands,
11+
Help key.Binding
12+
}
13+
14+
func DefaultKeyMap(tabHelp string) KeyMap {
15+
return KeyMap{
16+
Tab: key.NewBinding(
17+
key.WithKeys("tab"),
18+
key.WithHelp("tab", tabHelp),
19+
),
20+
Commands: key.NewBinding(
21+
key.WithKeys("ctrl+p"),
22+
key.WithHelp("ctrl+p", "commands"),
23+
),
24+
Help: key.NewBinding(
25+
key.WithKeys("ctrl+?", "ctrl+_", "ctrl+/"),
26+
key.WithHelp("ctrl+?", "more"),
27+
),
28+
}
29+
}
30+
31+
// FullHelp implements help.KeyMap.
32+
func (k KeyMap) FullHelp() [][]key.Binding {
33+
m := [][]key.Binding{}
34+
slice := layout.KeyMapToSlice(k)
35+
for i := 0; i < len(slice); i += 4 {
36+
end := min(i+4, len(slice))
37+
m = append(m, slice[i:end])
38+
}
39+
return m
40+
}
41+
42+
// ShortHelp implements help.KeyMap.
43+
func (k KeyMap) ShortHelp() []key.Binding {
44+
return []key.Binding{
45+
k.Tab,
46+
k.Commands,
47+
k.Help,
48+
}
49+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package status
2+
3+
import (
4+
"time"
5+
6+
"github.com/charmbracelet/bubbles/v2/help"
7+
tea "github.com/charmbracelet/bubbletea/v2"
8+
"github.com/opencode-ai/opencode/internal/logging"
9+
"github.com/opencode-ai/opencode/internal/pubsub"
10+
"github.com/opencode-ai/opencode/internal/session"
11+
"github.com/opencode-ai/opencode/internal/tui/styles"
12+
"github.com/opencode-ai/opencode/internal/tui/util"
13+
)
14+
15+
type StatusCmp interface {
16+
util.Model
17+
}
18+
19+
type statusCmp struct {
20+
info util.InfoMsg
21+
width int
22+
messageTTL time.Duration
23+
session session.Session
24+
help help.Model
25+
}
26+
27+
// clearMessageCmd is a command that clears status messages after a timeout
28+
func (m statusCmp) clearMessageCmd(ttl time.Duration) tea.Cmd {
29+
return tea.Tick(ttl, func(time.Time) tea.Msg {
30+
return util.ClearStatusMsg{}
31+
})
32+
}
33+
34+
func (m statusCmp) Init() tea.Cmd {
35+
return nil
36+
}
37+
38+
func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
39+
switch msg := msg.(type) {
40+
case tea.WindowSizeMsg:
41+
m.width = msg.Width
42+
return m, nil
43+
44+
// Handle status info
45+
case util.InfoMsg:
46+
m.info = msg
47+
ttl := msg.TTL
48+
if ttl == 0 {
49+
ttl = m.messageTTL
50+
}
51+
return m, m.clearMessageCmd(ttl)
52+
case util.ClearStatusMsg:
53+
m.info = util.InfoMsg{}
54+
55+
// Handle persistent logs
56+
case pubsub.Event[logging.LogMessage]:
57+
if msg.Payload.Persist {
58+
switch msg.Payload.Level {
59+
case "error":
60+
m.info = util.InfoMsg{
61+
Type: util.InfoTypeError,
62+
Msg: msg.Payload.Message,
63+
TTL: msg.Payload.PersistTime,
64+
}
65+
case "info":
66+
m.info = util.InfoMsg{
67+
Type: util.InfoTypeInfo,
68+
Msg: msg.Payload.Message,
69+
TTL: msg.Payload.PersistTime,
70+
}
71+
case "warn":
72+
m.info = util.InfoMsg{
73+
Type: util.InfoTypeWarn,
74+
Msg: msg.Payload.Message,
75+
TTL: msg.Payload.PersistTime,
76+
}
77+
default:
78+
m.info = util.InfoMsg{
79+
Type: util.InfoTypeInfo,
80+
Msg: msg.Payload.Message,
81+
TTL: msg.Payload.PersistTime,
82+
}
83+
}
84+
}
85+
}
86+
return m, nil
87+
}
88+
89+
func (m statusCmp) View() tea.View {
90+
t := styles.CurrentTheme()
91+
status := t.S().Base.Padding(0, 1).Render(m.help.View(DefaultKeyMap("focus chat")))
92+
if m.info.Msg != "" {
93+
switch m.info.Type {
94+
case util.InfoTypeError:
95+
status = t.S().Base.Background(t.Error).Padding(0, 1).Width(m.width).Render(m.info.Msg)
96+
case util.InfoTypeWarn:
97+
status = t.S().Base.Background(t.Warning).Padding(0, 1).Width(m.width).Render(m.info.Msg)
98+
default:
99+
status = t.S().Base.Background(t.Info).Padding(0, 1).Width(m.width).Render(m.info.Msg)
100+
}
101+
}
102+
return tea.NewView(status)
103+
}
104+
105+
func NewStatusCmp() StatusCmp {
106+
t := styles.CurrentTheme()
107+
help := help.New()
108+
help.Styles = t.S().Help
109+
return &statusCmp{
110+
messageTTL: 10 * time.Second,
111+
help: help,
112+
}
113+
}

internal/tui/components/dialogs/commands/commands.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,9 @@ func (c *commandDialogCmp) commandTypeRadio() string {
160160
iconSelected := "◉"
161161
iconUnselected := "○"
162162
if c.commandType == SystemCommands {
163-
return t.S().Text.Render(iconSelected + " " + choices[0] + " " + iconUnselected + " " + choices[1])
163+
return t.S().Base.Foreground(t.FgHalfMuted).Render(iconSelected + " " + choices[0] + " " + iconUnselected + " " + choices[1])
164164
}
165-
return t.S().Text.Render(iconUnselected + " " + choices[0] + " " + iconSelected + " " + choices[1])
165+
return t.S().Base.Foreground(t.FgHalfMuted).Render(iconUnselected + " " + choices[0] + " " + iconSelected + " " + choices[1])
166166
}
167167

168168
func (c *commandDialogCmp) listWidth() int {

internal/tui/components/logo/logo.go

Lines changed: 2 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import (
1010
"github.com/charmbracelet/lipgloss/v2"
1111
"github.com/charmbracelet/x/ansi"
1212
"github.com/charmbracelet/x/exp/slice"
13-
"github.com/lucasb-eyer/go-colorful"
14-
"github.com/rivo/uniseg"
13+
"github.com/opencode-ai/opencode/internal/tui/styles"
1514
)
1615

1716
// letterform represents a letterform. It can be stretched horizontally by
@@ -46,7 +45,7 @@ func Render(version string, compact bool, o Opts) string {
4645
crushWidth := lipgloss.Width(crush)
4746
b := new(strings.Builder)
4847
for r := range strings.SplitSeq(crush, "\n") {
49-
fmt.Fprintln(b, applyForegroundGrad(r, o.TitleColorA, o.TitleColorB))
48+
fmt.Fprintln(b, styles.ApplyForegroundGrad(r, o.TitleColorA, o.TitleColorB))
5049
}
5150
crush = b.String()
5251

@@ -312,76 +311,3 @@ func stretchLetterformPart(s string, p letterformProps) string {
312311
}
313312
return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
314313
}
315-
316-
// applyForegroundGrad renders a given string with a horizontal gradient
317-
// foreground.
318-
func applyForegroundGrad(input string, color1, color2 color.Color) string {
319-
if input == "" {
320-
return ""
321-
}
322-
323-
var o strings.Builder
324-
if len(input) == 1 {
325-
return lipgloss.NewStyle().Foreground(color1).Render(input)
326-
}
327-
328-
var clusters []string
329-
gr := uniseg.NewGraphemes(input)
330-
for gr.Next() {
331-
clusters = append(clusters, string(gr.Runes()))
332-
}
333-
334-
ramp := blendColors(len(clusters), color1, color2)
335-
for i, c := range ramp {
336-
fmt.Fprint(&o, lipgloss.NewStyle().Foreground(c).Render(clusters[i]))
337-
}
338-
339-
return o.String()
340-
}
341-
342-
// blendColors returns a slice of colors blended between the given keys.
343-
// Blending is done in Hcl to stay in gamut.
344-
func blendColors(size int, stops ...color.Color) []color.Color {
345-
if len(stops) < 2 {
346-
return nil
347-
}
348-
349-
stopsPrime := make([]colorful.Color, len(stops))
350-
for i, k := range stops {
351-
stopsPrime[i], _ = colorful.MakeColor(k)
352-
}
353-
354-
numSegments := len(stopsPrime) - 1
355-
blended := make([]color.Color, 0, size)
356-
357-
// Calculate how many colors each segment should have.
358-
segmentSizes := make([]int, numSegments)
359-
baseSize := size / numSegments
360-
remainder := size % numSegments
361-
362-
// Distribute the remainder across segments.
363-
for i := range numSegments {
364-
segmentSizes[i] = baseSize
365-
if i < remainder {
366-
segmentSizes[i]++
367-
}
368-
}
369-
370-
// Generate colors for each segment.
371-
for i := range numSegments {
372-
c1 := stopsPrime[i]
373-
c2 := stopsPrime[i+1]
374-
segmentSize := segmentSizes[i]
375-
376-
for j := range segmentSize {
377-
var t float64
378-
if segmentSize > 1 {
379-
t = float64(j) / float64(segmentSize-1)
380-
}
381-
c := c1.BlendHcl(c2, t)
382-
blended = append(blended, c)
383-
}
384-
}
385-
386-
return blended
387-
}

0 commit comments

Comments
 (0)