summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/components
diff options
context:
space:
mode:
Diffstat (limited to 'packages/tui/internal/components')
-rw-r--r--packages/tui/internal/components/chat/editor.go38
-rw-r--r--packages/tui/internal/components/chat/message.go4
-rw-r--r--packages/tui/internal/components/chat/messages.go2
-rw-r--r--packages/tui/internal/components/dialog/help.go239
-rw-r--r--packages/tui/internal/components/dialog/models.go129
-rw-r--r--packages/tui/internal/components/dialog/quit.go73
-rw-r--r--packages/tui/internal/components/dialog/session.go161
-rw-r--r--packages/tui/internal/components/dialog/theme.go219
-rw-r--r--packages/tui/internal/components/modal/modal.go137
-rw-r--r--packages/tui/internal/components/util/simple-list.go7
10 files changed, 352 insertions, 657 deletions
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go
index d1b8bd7fa..ad7d140a6 100644
--- a/packages/tui/internal/components/chat/editor.go
+++ b/packages/tui/internal/components/chat/editor.go
@@ -119,6 +119,13 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.attachments = append(m.attachments, msg.Attachment)
case tea.KeyMsg:
switch msg.String() {
+ case "ctrl+c":
+ if m.textarea.Value() != "" {
+ m.textarea.Reset()
+ return m, func() tea.Msg {
+ return nil
+ }
+ }
case "shift+enter":
value := m.textarea.Value()
m.textarea.SetValue(value + "\n")
@@ -264,8 +271,12 @@ func (m *editorComponent) View() string {
)
textarea = styles.BaseStyle().
Width(m.width).
- Border(lipgloss.NormalBorder(), true, true).
- BorderForeground(t.Border()).
+ PaddingTop(1).
+ PaddingBottom(1).
+ Background(t.BackgroundElement()).
+ Border(lipgloss.ThickBorder(), false, true).
+ BorderForeground(t.BorderActive()).
+ BorderBackground(t.Background()).
Render(textarea)
hint := base("enter") + muted(" send ") + base("shift") + muted("+") + base("enter") + muted(" newline")
@@ -287,6 +298,7 @@ func (m *editorComponent) View() string {
content := lipgloss.JoinVertical(
lipgloss.Top,
// m.attachmentsContent(),
+ "",
textarea,
info,
)
@@ -409,21 +421,21 @@ func (m *editorComponent) attachmentsContent() string {
func createTextArea(existing *textarea.Model) textarea.Model {
t := theme.CurrentTheme()
- bgColor := t.Background()
+ bgColor := t.BackgroundElement()
textColor := t.Text()
textMutedColor := t.TextMuted()
ta := textarea.New()
- ta.Placeholder = "It's prompting time..."
-
- ta.Styles.Blurred.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
- ta.Styles.Blurred.CursorLine = styles.BaseStyle().Background(bgColor)
- ta.Styles.Blurred.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
- ta.Styles.Blurred.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
- ta.Styles.Focused.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
- ta.Styles.Focused.CursorLine = styles.BaseStyle().Background(bgColor)
- ta.Styles.Focused.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
- ta.Styles.Focused.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
+
+ ta.Styles.Blurred.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
+ ta.Styles.Blurred.CursorLine = lipgloss.NewStyle().Background(bgColor)
+ ta.Styles.Blurred.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
+ ta.Styles.Blurred.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
+ ta.Styles.Focused.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
+ ta.Styles.Focused.CursorLine = lipgloss.NewStyle().Background(bgColor)
+ ta.Styles.Focused.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
+ ta.Styles.Focused.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
+ ta.Styles.Cursor.Color = t.Primary()
ta.Prompt = " "
ta.ShowLineNumbers = false
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index f3fbca475..2c2cd03f0 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -292,8 +292,8 @@ func renderToolInvocation(
toolArgs = renderArgs(&toolArgsMap, "filePath")
title = fmt.Sprintf("Read: %s %s", toolArgs, elapsed)
body = ""
- filename := toolArgsMap["filePath"].(string)
- if metadata["preview"] != nil {
+ if metadata["preview"] != nil && toolArgsMap["filePath"] != nil {
+ filename := toolArgsMap["filePath"].(string)
body = metadata["preview"].(string)
body = renderFile(filename, body, WithTruncate(6))
}
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index 47a9b8362..3985ee0d7 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -237,7 +237,7 @@ func (m *messagesComponent) renderView() {
}
m.viewport.SetHeight(m.height - lipgloss.Height(m.header()))
- m.viewport.SetContent(strings.Join(centered, "\n"))
+ m.viewport.SetContent("\n" + strings.Join(centered, "\n") + "\n")
}
func (m *messagesComponent) header() string {
diff --git a/packages/tui/internal/components/dialog/help.go b/packages/tui/internal/components/dialog/help.go
index 6ceeb40b2..a1508a01a 100644
--- a/packages/tui/internal/components/dialog/help.go
+++ b/packages/tui/internal/components/dialog/help.go
@@ -6,196 +6,115 @@ import (
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
+ "github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
- "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
-type helpComponent struct {
- width int
- height int
- keys []key.Binding
+type helpDialog struct {
+ width int
+ height int
+ modal *modal.Modal
+ bindings []key.Binding
}
-func (h *helpComponent) Init() tea.Cmd {
+// func (i bindingItem) Render(selected bool, width int) string {
+// t := theme.CurrentTheme()
+// baseStyle := styles.BaseStyle().
+// Width(width - 2).
+// Background(t.BackgroundElement())
+//
+// if selected {
+// baseStyle = baseStyle.
+// Background(t.Primary()).
+// Foreground(t.BackgroundElement()).
+// Bold(true)
+// } else {
+// baseStyle = baseStyle.
+// Foreground(t.Text())
+// }
+//
+// return baseStyle.Padding(0, 1).Render(i.binding.Help().Desc)
+// }
+
+func (h *helpDialog) Init() tea.Cmd {
return nil
}
-func (h *helpComponent) SetBindings(k []key.Binding) {
- h.keys = k
-}
-
-func (h *helpComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
- h.width = 90
+ h.width = msg.Width
h.height = msg.Height
}
return h, nil
}
-func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
- seen := make(map[string]struct{})
- result := make([]key.Binding, 0, len(bindings))
-
- // Process bindings in reverse order
- for i := len(bindings) - 1; i >= 0; i-- {
- b := bindings[i]
- k := strings.Join(b.Keys(), " ")
- if _, ok := seen[k]; ok {
- // duplicate, skip
- continue
- }
- seen[k] = struct{}{}
- // Add to the beginning of result to maintain original order
- result = append([]key.Binding{b}, result...)
- }
-
- return result
-}
-
-func (h *helpComponent) render() string {
+// func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
+// seen := make(map[string]struct{})
+// result := make([]key.Binding, 0, len(bindings))
+//
+// // Process bindings in reverse order
+// for i := len(bindings) - 1; i >= 0; i-- {
+// b := bindings[i]
+// k := strings.Join(b.Keys(), " ")
+// if _, ok := seen[k]; ok {
+// // duplicate, skip
+// continue
+// }
+// seen[k] = struct{}{}
+// // Add to the beginning of result to maintain original order
+// result = append([]key.Binding{b}, result...)
+// }
+//
+// return result
+// }
+
+func (h *helpDialog) View() string {
t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- helpKeyStyle := styles.Bold().
- Background(t.Background()).
+ keyStyle := lipgloss.NewStyle().
+ Background(t.BackgroundElement()).
Foreground(t.Text()).
- Padding(0, 1, 0, 0)
-
- helpDescStyle := styles.Regular().
- Background(t.Background()).
+ Bold(true)
+ descStyle := lipgloss.NewStyle().
+ Background(t.BackgroundElement()).
Foreground(t.TextMuted())
- // Compile list of bindings to render
- bindings := removeDuplicateBindings(h.keys)
-
- // Enumerate through each group of bindings, populating a series of
- // pairs of columns, one for keys, one for descriptions
- var (
- pairs []string
- width int
- rows = 12 - 2
- )
-
- for i := 0; i < len(bindings); i += rows {
- var (
- keys []string
- descs []string
- )
- for j := i; j < min(i+rows, len(bindings)); j++ {
- keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key))
- descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc))
- }
-
- // Render pair of columns; beyond the first pair, render a three space
- // left margin, in order to visually separate the pairs.
- var cols []string
- if len(pairs) > 0 {
- cols = []string{baseStyle.Render(" ")}
- }
-
- maxDescWidth := 0
- for _, desc := range descs {
- if maxDescWidth < lipgloss.Width(desc) {
- maxDescWidth = lipgloss.Width(desc)
- }
- }
- for i := range descs {
- remainingWidth := maxDescWidth - lipgloss.Width(descs[i])
- if remainingWidth > 0 {
- descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
- }
- }
- maxKeyWidth := 0
- for _, key := range keys {
- if maxKeyWidth < lipgloss.Width(key) {
- maxKeyWidth = lipgloss.Width(key)
- }
- }
- for i := range keys {
- remainingWidth := maxKeyWidth - lipgloss.Width(keys[i])
- if remainingWidth > 0 {
- keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
+ lines := []string{}
+ for _, b := range h.bindings {
+ content := keyStyle.Render(b.Help().Key)
+ content += descStyle.Render(" " + b.Help().Desc)
+ for i, key := range b.Keys() {
+ if i == 0 {
+ keyString := " (" + strings.ToUpper(key) + ")"
+ // space := max(h.width-lipgloss.Width(content)-lipgloss.Width(keyString), 0)
+ // spacer := strings.Repeat(" ", space)
+ // content += descStyle.Render(spacer)
+ content += descStyle.Render(keyString)
}
}
- cols = append(cols,
- strings.Join(keys, "\n"),
- strings.Join(descs, "\n"),
- )
-
- pair := baseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
- // check whether it exceeds the maximum width avail (the width of the
- // terminal, subtracting 2 for the borders).
- width += lipgloss.Width(pair)
- if width > h.width-2 {
- break
- }
- pairs = append(pairs, pair)
+ lines = append(lines, content)
}
- // https://github.com/charmbracelet/lipgloss/v2/issues/209
- if len(pairs) > 1 {
- prefix := pairs[:len(pairs)-1]
- lastPair := pairs[len(pairs)-1]
- prefix = append(prefix, lipgloss.Place(
- lipgloss.Width(lastPair), // width
- lipgloss.Height(prefix[0]), // height
- lipgloss.Left, // x
- lipgloss.Top, // y
- lastPair, // content
- // lipgloss.WithWhitespaceBackground(t.Background()),
- ))
- content := baseStyle.Width(h.width).Render(
- lipgloss.JoinHorizontal(
- lipgloss.Top,
- prefix...,
- ),
- )
- return content
- }
-
- // Join pairs of columns and enclose in a border
- content := baseStyle.Width(h.width).Render(
- lipgloss.JoinHorizontal(
- lipgloss.Top,
- pairs...,
- ),
- )
- return content
+ return strings.Join(lines, "\n")
}
-func (h *helpComponent) View() string {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- content := h.render()
- header := baseStyle.
- Bold(true).
- Width(lipgloss.Width(content)).
- Foreground(t.Primary()).
- Render("Keyboard Shortcuts")
+func (h *helpDialog) Render(background string) string {
+ return h.modal.Render(h.View(), background)
+}
- return baseStyle.Padding(1).
- Border(lipgloss.RoundedBorder()).
- BorderForeground(t.TextMuted()).
- Width(h.width).
- BorderBackground(t.Background()).
- Render(
- lipgloss.JoinVertical(lipgloss.Center,
- header,
- baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
- content,
- ),
- )
+func (h *helpDialog) Close() tea.Cmd {
+ return nil
}
-type HelpComponent interface {
- layout.ModelWithView
- SetBindings([]key.Binding)
+type HelpDialog interface {
+ layout.Modal
}
-func NewHelpCmp() HelpComponent {
- return &helpComponent{}
+func NewHelpDialog(bindings ...key.Binding) HelpDialog {
+ return &helpDialog{
+ bindings: bindings,
+ modal: modal.New(),
+ }
}
diff --git a/packages/tui/internal/components/dialog/models.go b/packages/tui/internal/components/dialog/models.go
index 50d1791cd..ca6561502 100644
--- a/packages/tui/internal/components/dialog/models.go
+++ b/packages/tui/internal/components/dialog/models.go
@@ -11,7 +11,9 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app"
+ "github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
+ "github.com/sst/opencode/internal/state"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
@@ -19,25 +21,16 @@ import (
)
const (
- numVisibleModels = 10
+ numVisibleModels = 6
maxDialogWidth = 40
)
-// CloseModelDialogMsg is sent when a model is selected
-type CloseModelDialogMsg struct {
- Provider *client.ProviderInfo
- Model *client.ProviderModel
-}
-
// ModelDialog interface for the model selection dialog
type ModelDialog interface {
- layout.ModelWithView
- layout.Bindings
-
- SetProviders(providers []client.ProviderInfo)
+ layout.Modal
}
-type modelDialogComponent struct {
+type modelDialog struct {
app *app.App
availableProviders []client.ProviderInfo
provider client.ProviderInfo
@@ -48,6 +41,8 @@ type modelDialogComponent struct {
scrollOffset int
hScrollOffset int
hScrollPossible bool
+
+ modal *modal.Modal
}
type modelKeyMap struct {
@@ -57,27 +52,23 @@ type modelKeyMap struct {
Right key.Binding
Enter key.Binding
Escape key.Binding
- J key.Binding
- K key.Binding
- H key.Binding
- L key.Binding
}
var modelKeys = modelKeyMap{
Up: key.NewBinding(
- key.WithKeys("up"),
+ key.WithKeys("up", "k"),
key.WithHelp("↑", "previous model"),
),
Down: key.NewBinding(
- key.WithKeys("down"),
+ key.WithKeys("down", "j"),
key.WithHelp("↓", "next model"),
),
Left: key.NewBinding(
- key.WithKeys("left"),
+ key.WithKeys("left", "h"),
key.WithHelp("←", "scroll left"),
),
Right: key.NewBinding(
- key.WithKeys("right"),
+ key.WithKeys("right", "l"),
key.WithHelp("→", "scroll right"),
),
Enter: key.NewBinding(
@@ -88,25 +79,9 @@ var modelKeys = modelKeyMap{
key.WithKeys("esc"),
key.WithHelp("esc", "close"),
),
- J: key.NewBinding(
- key.WithKeys("j"),
- key.WithHelp("j", "next model"),
- ),
- K: key.NewBinding(
- key.WithKeys("k"),
- key.WithHelp("k", "previous model"),
- ),
- H: key.NewBinding(
- key.WithKeys("h"),
- key.WithHelp("h", "scroll left"),
- ),
- L: key.NewBinding(
- key.WithKeys("l"),
- key.WithHelp("l", "scroll right"),
- ),
}
-func (m *modelDialogComponent) Init() tea.Cmd {
+func (m *modelDialog) Init() tea.Cmd {
// cfg := config.Get()
// modelInfo := GetSelectedModel(cfg)
// m.availableProviders = getEnabledProviders(cfg)
@@ -116,40 +91,31 @@ func (m *modelDialogComponent) Init() tea.Cmd {
// m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
// m.setupModelsForProvider(m.provider)
-
- m.availableProviders, _ = m.app.ListProviders(context.Background())
- m.hScrollOffset = 0
- m.hScrollPossible = len(m.availableProviders) > 1
- m.provider = m.availableProviders[m.hScrollOffset]
-
return nil
}
-func (m *modelDialogComponent) SetProviders(providers []client.ProviderInfo) {
- m.availableProviders = providers
-}
-
-func (m *modelDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
- case key.Matches(msg, modelKeys.Up) || key.Matches(msg, modelKeys.K):
+ case key.Matches(msg, modelKeys.Up):
m.moveSelectionUp()
- case key.Matches(msg, modelKeys.Down) || key.Matches(msg, modelKeys.J):
+ case key.Matches(msg, modelKeys.Down):
m.moveSelectionDown()
- case key.Matches(msg, modelKeys.Left) || key.Matches(msg, modelKeys.H):
+ case key.Matches(msg, modelKeys.Left):
if m.hScrollPossible {
m.switchProvider(-1)
}
- case key.Matches(msg, modelKeys.Right) || key.Matches(msg, modelKeys.L):
+ case key.Matches(msg, modelKeys.Right):
if m.hScrollPossible {
m.switchProvider(1)
}
case key.Matches(msg, modelKeys.Enter):
models := m.models()
- return m, util.CmdHandler(CloseModelDialogMsg{Provider: &m.provider, Model: &models[m.selectedIdx]})
+ cmd := util.CmdHandler(state.ModelSelectedMsg{Provider: m.provider, Model: models[m.selectedIdx]})
+ return m, tea.Batch(cmd, util.CmdHandler(modal.CloseModalMsg{}))
case key.Matches(msg, modelKeys.Escape):
- return m, util.CmdHandler(CloseModelDialogMsg{})
+ return m, util.CmdHandler(modal.CloseModalMsg{})
}
case tea.WindowSizeMsg:
m.width = msg.Width
@@ -159,7 +125,7 @@ func (m *modelDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
-func (m *modelDialogComponent) models() []client.ProviderModel {
+func (m *modelDialog) models() []client.ProviderModel {
models := slices.SortedFunc(maps.Values(m.provider.Models), func(a, b client.ProviderModel) int {
return strings.Compare(*a.Name, *b.Name)
})
@@ -167,7 +133,7 @@ func (m *modelDialogComponent) models() []client.ProviderModel {
}
// moveSelectionUp moves the selection up or wraps to bottom
-func (m *modelDialogComponent) moveSelectionUp() {
+func (m *modelDialog) moveSelectionUp() {
if m.selectedIdx > 0 {
m.selectedIdx--
} else {
@@ -182,7 +148,7 @@ func (m *modelDialogComponent) moveSelectionUp() {
}
// moveSelectionDown moves the selection down or wraps to top
-func (m *modelDialogComponent) moveSelectionDown() {
+func (m *modelDialog) moveSelectionDown() {
if m.selectedIdx < len(m.provider.Models)-1 {
m.selectedIdx++
} else {
@@ -196,7 +162,7 @@ func (m *modelDialogComponent) moveSelectionDown() {
}
}
-func (m *modelDialogComponent) switchProvider(offset int) {
+func (m *modelDialog) switchProvider(offset int) {
newOffset := m.hScrollOffset + offset
// Ensure we stay within bounds
@@ -212,9 +178,11 @@ func (m *modelDialogComponent) switchProvider(offset int) {
m.setupModelsForProvider(m.provider.Id)
}
-func (m *modelDialogComponent) View() string {
+func (m *modelDialog) View() string {
t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
+ baseStyle := lipgloss.NewStyle().
+ Background(t.BackgroundElement()).
+ Foreground(t.Text())
// Capitalize first letter of provider name
title := baseStyle.
@@ -232,8 +200,10 @@ func (m *modelDialogComponent) View() string {
for i := m.scrollOffset; i < endIdx; i++ {
itemStyle := baseStyle.Width(maxDialogWidth)
if i == m.selectedIdx {
- itemStyle = itemStyle.Background(t.Primary()).
- Foreground(t.Background()).Bold(true)
+ itemStyle = itemStyle.
+ Background(t.Primary()).
+ Foreground(t.BackgroundElement()).
+ Bold(true)
}
modelItems = append(modelItems, itemStyle.Render(*models[i].Name))
}
@@ -247,15 +217,10 @@ func (m *modelDialogComponent) View() string {
scrollIndicator,
)
- return baseStyle.Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderBackground(t.Background()).
- BorderForeground(t.TextMuted()).
- Width(lipgloss.Width(content) + 4).
- Render(content)
+ return content
}
-func (m *modelDialogComponent) getScrollIndicators(maxWidth int) string {
+func (m *modelDialog) getScrollIndicators(maxWidth int) string {
var indicator string
if len(m.provider.Models) > numVisibleModels {
@@ -291,10 +256,6 @@ func (m *modelDialogComponent) getScrollIndicators(maxWidth int) string {
Render(indicator)
}
-func (m *modelDialogComponent) BindingKeys() []key.Binding {
- return layout.KeyMapToSlice(modelKeys)
-}
-
// findProviderIndex returns the index of the provider in the list, or -1 if not found
// func findProviderIndex(providers []string, provider string) int {
// for i, p := range providers {
@@ -305,7 +266,7 @@ func (m *modelDialogComponent) BindingKeys() []key.Binding {
// return -1
// }
-func (m *modelDialogComponent) setupModelsForProvider(_ string) {
+func (m *modelDialog) setupModelsForProvider(_ string) {
m.selectedIdx = 0
m.scrollOffset = 0
@@ -331,8 +292,22 @@ func (m *modelDialogComponent) setupModelsForProvider(_ string) {
// }
}
-func NewModelDialogCmp(app *app.App) ModelDialog {
- return &modelDialogComponent{
- app: app,
+func (m *modelDialog) Render(background string) string {
+ return m.modal.Render(m.View(), background)
+}
+
+func (s *modelDialog) Close() tea.Cmd {
+ return nil
+}
+
+func NewModelDialog(app *app.App) ModelDialog {
+ availableProviders, _ := app.ListProviders(context.Background())
+
+ return &modelDialog{
+ availableProviders: availableProviders,
+ hScrollOffset: 0,
+ hScrollPossible: len(availableProviders) > 1,
+ provider: availableProviders[0],
+ modal: modal.New(),
}
}
diff --git a/packages/tui/internal/components/dialog/quit.go b/packages/tui/internal/components/dialog/quit.go
index 8362e7672..be5400652 100644
--- a/packages/tui/internal/components/dialog/quit.go
+++ b/packages/tui/internal/components/dialog/quit.go
@@ -6,6 +6,7 @@ import (
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
+ "github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
@@ -14,14 +15,17 @@ import (
const question = "Are you sure you want to quit?"
-type CloseQuitMsg struct{}
-
+// QuitDialog interface for the quit confirmation dialog
type QuitDialog interface {
- layout.ModelWithView
- layout.Bindings
+ layout.Modal
+ IsQuitDialog() bool
}
-type quitDialogComponent struct {
+type quitDialog struct {
+ width int
+ height int
+
+ modal *modal.Modal
selectedNo bool
}
@@ -30,12 +34,11 @@ type helpMapping struct {
EnterSpace key.Binding
Yes key.Binding
No key.Binding
- Tab key.Binding
}
var helpKeys = helpMapping{
LeftRight: key.NewBinding(
- key.WithKeys("left", "right"),
+ key.WithKeys("left", "right", "h", "l", "tab"),
key.WithHelp("←/→", "switch options"),
),
EnterSpace: key.NewBinding(
@@ -43,58 +46,61 @@ var helpKeys = helpMapping{
key.WithHelp("enter/space", "confirm"),
),
Yes: key.NewBinding(
- key.WithKeys("y", "Y"),
+ key.WithKeys("y", "Y", "ctrl+c"),
key.WithHelp("y/Y", "yes"),
),
No: key.NewBinding(
key.WithKeys("n", "N"),
key.WithHelp("n/N", "no"),
),
- Tab: key.NewBinding(
- key.WithKeys("tab"),
- key.WithHelp("tab", "switch options"),
- ),
}
-func (q *quitDialogComponent) Init() tea.Cmd {
+func (q *quitDialog) Init() tea.Cmd {
return nil
}
-func (q *quitDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (q *quitDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ q.width = msg.Width
+ q.height = msg.Height
case tea.KeyMsg:
switch {
- case key.Matches(msg, helpKeys.LeftRight) || key.Matches(msg, helpKeys.Tab):
+ case key.Matches(msg, helpKeys.LeftRight):
q.selectedNo = !q.selectedNo
return q, nil
case key.Matches(msg, helpKeys.EnterSpace):
if !q.selectedNo {
return q, tea.Quit
}
- return q, util.CmdHandler(CloseQuitMsg{})
+ return q, tea.Batch(
+ util.CmdHandler(modal.CloseModalMsg{}),
+ )
case key.Matches(msg, helpKeys.Yes):
return q, tea.Quit
case key.Matches(msg, helpKeys.No):
- return q, util.CmdHandler(CloseQuitMsg{})
+ return q, tea.Batch(
+ util.CmdHandler(modal.CloseModalMsg{}),
+ )
}
}
return q, nil
}
-func (q *quitDialogComponent) View() string {
+func (q *quitDialog) Render(background string) string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
yesStyle := baseStyle
noStyle := baseStyle
- spacerStyle := baseStyle.Background(t.Background())
+ spacerStyle := baseStyle.Background(t.BackgroundElement())
if q.selectedNo {
- noStyle = noStyle.Background(t.Primary()).Foreground(t.Background())
- yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary())
+ noStyle = noStyle.Background(t.Primary()).Foreground(t.BackgroundElement())
+ yesStyle = yesStyle.Background(t.BackgroundElement()).Foreground(t.Primary())
} else {
- yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background())
- noStyle = noStyle.Background(t.Background()).Foreground(t.Primary())
+ yesStyle = yesStyle.Background(t.Primary()).Foreground(t.BackgroundElement())
+ noStyle = noStyle.Background(t.BackgroundElement()).Foreground(t.Primary())
}
yesButton := yesStyle.Padding(0, 1).Render("Yes")
@@ -117,20 +123,21 @@ func (q *quitDialogComponent) View() string {
),
)
- return baseStyle.Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderBackground(t.Background()).
- BorderForeground(t.TextMuted()).
- Width(lipgloss.Width(content) + 4).
- Render(content)
+ return q.modal.Render(content, background)
+}
+
+func (q *quitDialog) Close() tea.Cmd {
+ return nil
}
-func (q *quitDialogComponent) BindingKeys() []key.Binding {
- return layout.KeyMapToSlice(helpKeys)
+func (q *quitDialog) IsQuitDialog() bool {
+ return true
}
-func NewQuitCmp() QuitDialog {
- return &quitDialogComponent{
+// NewQuitDialog creates a new quit confirmation dialog
+func NewQuitDialog() QuitDialog {
+ return &quitDialog{
selectedNo: true,
+ modal: modal.New(),
}
}
diff --git a/packages/tui/internal/components/dialog/session.go b/packages/tui/internal/components/dialog/session.go
index 6c4db997f..9bcf0690c 100644
--- a/packages/tui/internal/components/dialog/session.go
+++ b/packages/tui/internal/components/dialog/session.go
@@ -1,29 +1,23 @@
package dialog
import (
- "github.com/charmbracelet/bubbles/v2/key"
+ "context"
+
tea "github.com/charmbracelet/bubbletea/v2"
+ "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/modal"
- utilComponents "github.com/sst/opencode/internal/components/util"
+ components "github.com/sst/opencode/internal/components/util"
"github.com/sst/opencode/internal/layout"
+ "github.com/sst/opencode/internal/state"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
"github.com/sst/opencode/pkg/client"
)
-// CloseSessionDialogMsg is sent when the session dialog is closed
-type CloseSessionDialogMsg struct {
- Session *client.SessionInfo
-}
-
// SessionDialog interface for the session switching dialog
type SessionDialog interface {
- tea.Model
- layout.Bindings
- SetSessions(sessions []client.SessionInfo)
- SetSelectedSession(sessionID string)
- Render(background string) string
+ layout.Modal
}
type sessionItem struct {
@@ -49,163 +43,70 @@ func (s sessionItem) Render(selected bool, width int) string {
return baseStyle.Padding(0, 1).Render(s.session.Title)
}
-// sessionDialogContent is the inner content of the session dialog
-type sessionDialogContent struct {
- sessions []client.SessionInfo
+type sessionDialog struct {
width int
height int
+ modal *modal.Modal
selectedSessionID string
- list utilComponents.SimpleList[sessionItem]
-}
-
-type sessionKeyMap struct {
- Enter key.Binding
- Escape key.Binding
+ list components.SimpleList[sessionItem]
}
-var sessionKeys = sessionKeyMap{
- Enter: key.NewBinding(
- key.WithKeys("enter"),
- key.WithHelp("enter", "select session"),
- ),
- Escape: key.NewBinding(
- key.WithKeys("esc"),
- key.WithHelp("esc", "close"),
- ),
-}
-
-func (s *sessionDialogContent) Init() tea.Cmd {
+func (s *sessionDialog) Init() tea.Cmd {
return nil
}
-func (s *sessionDialogContent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
s.width = msg.Width
s.height = msg.Height
+ s.list.SetMaxWidth(layout.Current.Container.Width - 12)
case tea.KeyMsg:
- switch {
- case key.Matches(msg, sessionKeys.Enter):
+ switch msg.String() {
+ case "enter":
if item, idx := s.list.GetSelectedItem(); idx >= 0 {
selectedSession := item.session
s.selectedSessionID = selectedSession.Id
-
- return s, util.CmdHandler(CloseSessionDialogMsg{
- Session: &selectedSession,
- })
+ return s, tea.Batch(
+ util.CmdHandler(state.SessionSelectedMsg(&selectedSession)),
+ util.CmdHandler(modal.CloseModalMsg{}),
+ )
}
- case key.Matches(msg, sessionKeys.Escape):
- return s, util.CmdHandler(CloseSessionDialogMsg{})
- default:
- // Pass other key messages to the list component
- var cmd tea.Cmd
- listModel, cmd := s.list.Update(msg)
- s.list = listModel.(utilComponents.SimpleList[sessionItem])
- return s, cmd
}
}
- // For non-key messages
var cmd tea.Cmd
listModel, cmd := s.list.Update(msg)
- s.list = listModel.(utilComponents.SimpleList[sessionItem])
- return s, cmd
-}
-
-func (s *sessionDialogContent) View() string {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle().Background(t.BackgroundElement())
- width := layout.Current.Container.Width - 12
-
- if len(s.sessions) == 0 {
- return baseStyle.Padding(1, 2).
- Foreground(t.TextMuted()).
- Width(width).
- Render("No sessions available")
- }
-
- // Set the max width for the list
- s.list.SetMaxWidth(width)
-
- return s.list.View()
-}
-
-func (s *sessionDialogContent) BindingKeys() []key.Binding {
- // Combine session dialog keys with list keys
- dialogKeys := layout.KeyMapToSlice(sessionKeys)
- listKeys := s.list.BindingKeys()
- return append(dialogKeys, listKeys...)
-}
-
-// sessionDialogComponent wraps the content with a modal
-type sessionDialogComponent struct {
- content *sessionDialogContent
- modal *modal.Modal
-}
-
-func (s *sessionDialogComponent) Init() tea.Cmd {
- return s.modal.Init()
-}
-
-func (s *sessionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- m, cmd := s.modal.Update(msg)
- s.modal = m.(*modal.Modal)
+ s.list = listModel.(components.SimpleList[sessionItem])
return s, cmd
}
-func (s *sessionDialogComponent) View() string {
- return s.modal.View()
+func (s *sessionDialog) Render(background string) string {
+ return s.modal.Render(s.list.View(), background)
}
-func (s *sessionDialogComponent) Render(background string) string {
- return s.modal.Render(background)
-}
-
-func (s *sessionDialogComponent) BindingKeys() []key.Binding {
- return s.modal.BindingKeys()
+func (s *sessionDialog) Close() tea.Cmd {
+ return nil
}
-func (s *sessionDialogComponent) SetSessions(sessions []client.SessionInfo) {
- s.content.sessions = sessions
+// NewSessionDialog creates a new session switching dialog
+func NewSessionDialog(app *app.App) SessionDialog {
+ sessions, _ := app.ListSessions(context.Background())
- // Convert sessions to sessionItems
var sessionItems []sessionItem
-
for _, sess := range sessions {
sessionItems = append(sessionItems, sessionItem{session: sess})
}
- s.content.list.SetItems(sessionItems)
-}
-
-func (s *sessionDialogComponent) SetSelectedSession(sessionID string) {
- s.content.selectedSessionID = sessionID
-
- // Update the selected index if sessions are already loaded
- if len(s.content.sessions) > 0 {
- // Re-set the sessions to update the selection
- s.SetSessions(s.content.sessions)
- }
-}
-
-// NewSessionDialogCmp creates a new session switching dialog
-func NewSessionDialogCmp() SessionDialog {
- list := utilComponents.NewSimpleList[sessionItem](
- []sessionItem{},
+ list := components.NewSimpleList(
+ sessionItems,
10, // maxVisibleSessions
"No sessions available",
true, // useAlphaNumericKeys
)
- content := &sessionDialogContent{
- sessions: []client.SessionInfo{},
- selectedSessionID: "",
- list: list,
- }
-
- return &sessionDialogComponent{
- content: content,
- modal: modal.New(content, modal.WithTitle("Switch Session")),
+ return &sessionDialog{
+ list: list,
+ modal: modal.New(modal.WithTitle("Switch Session"), modal.WithMaxWidth(80)),
}
}
-
diff --git a/packages/tui/internal/components/dialog/theme.go b/packages/tui/internal/components/dialog/theme.go
index 4704cb42e..66fd5feec 100644
--- a/packages/tui/internal/components/dialog/theme.go
+++ b/packages/tui/internal/components/dialog/theme.go
@@ -1,9 +1,9 @@
package dialog
import (
- "github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
+ "github.com/sst/opencode/internal/components/modal"
+ components "github.com/sst/opencode/internal/components/util"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/styles"
@@ -16,184 +16,113 @@ type ThemeChangedMsg struct {
ThemeName string
}
-// CloseThemeDialogMsg is sent when the theme dialog is closed
-type CloseThemeDialogMsg struct{}
-
// ThemeDialog interface for the theme switching dialog
type ThemeDialog interface {
- layout.ModelWithView
- layout.Bindings
+ layout.Modal
}
-type themeDialogComponent struct {
- themes []string
- selectedIdx int
- width int
- height int
- currentTheme string
+type themeItem struct {
+ name string
}
-type themeKeyMap struct {
- Up key.Binding
- Down key.Binding
- Enter key.Binding
- Escape key.Binding
- J key.Binding
- K key.Binding
-}
+func (t themeItem) Render(selected bool, width int) string {
+ th := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle().
+ Width(width - 2).
+ Background(th.BackgroundElement())
+
+ if selected {
+ baseStyle = baseStyle.
+ Background(th.Primary()).
+ Foreground(th.BackgroundElement()).
+ Bold(true)
+ } else {
+ baseStyle = baseStyle.
+ Foreground(th.Text())
+ }
-var themeKeys = themeKeyMap{
- Up: key.NewBinding(
- key.WithKeys("up"),
- key.WithHelp("↑", "previous theme"),
- ),
- Down: key.NewBinding(
- key.WithKeys("down"),
- key.WithHelp("↓", "next theme"),
- ),
- Enter: key.NewBinding(
- key.WithKeys("enter"),
- key.WithHelp("enter", "select theme"),
- ),
- Escape: key.NewBinding(
- key.WithKeys("esc"),
- key.WithHelp("esc", "close"),
- ),
- J: key.NewBinding(
- key.WithKeys("j"),
- key.WithHelp("j", "next theme"),
- ),
- K: key.NewBinding(
- key.WithKeys("k"),
- key.WithHelp("k", "previous theme"),
- ),
+ return baseStyle.Padding(0, 1).Render(t.name)
}
-func (t *themeDialogComponent) Init() tea.Cmd {
- // Load available themes and update selectedIdx based on current theme
- t.themes = theme.AvailableThemes()
- t.currentTheme = theme.CurrentThemeName()
+type themeDialog struct {
+ width int
+ height int
- // Find the current theme in the list
- for i, name := range t.themes {
- if name == t.currentTheme {
- t.selectedIdx = i
- break
- }
- }
+ modal *modal.Modal
+ list components.SimpleList[themeItem]
+}
+func (t *themeDialog) Init() tea.Cmd {
return nil
}
-func (t *themeDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ t.width = msg.Width
+ t.height = msg.Height
case tea.KeyMsg:
- switch {
- case key.Matches(msg, themeKeys.Up) || key.Matches(msg, themeKeys.K):
- if t.selectedIdx > 0 {
- t.selectedIdx--
- }
- return t, nil
- case key.Matches(msg, themeKeys.Down) || key.Matches(msg, themeKeys.J):
- if t.selectedIdx < len(t.themes)-1 {
- t.selectedIdx++
- }
- return t, nil
- case key.Matches(msg, themeKeys.Enter):
- if len(t.themes) > 0 {
+ switch msg.String() {
+ case "enter":
+ if item, idx := t.list.GetSelectedItem(); idx >= 0 {
previousTheme := theme.CurrentThemeName()
- selectedTheme := t.themes[t.selectedIdx]
+ selectedTheme := item.name
if previousTheme == selectedTheme {
- return t, util.CmdHandler(CloseThemeDialogMsg{})
+ return t, util.CmdHandler(modal.CloseModalMsg{})
}
if err := theme.SetTheme(selectedTheme); err != nil {
status.Error(err.Error())
return t, nil
}
- return t, util.CmdHandler(ThemeChangedMsg{
- ThemeName: selectedTheme,
- })
+ return t, tea.Batch(
+ util.CmdHandler(ThemeChangedMsg{ThemeName: selectedTheme}),
+ util.CmdHandler(modal.CloseModalMsg{}),
+ )
}
- case key.Matches(msg, themeKeys.Escape):
- return t, util.CmdHandler(CloseThemeDialogMsg{})
}
- case tea.WindowSizeMsg:
- t.width = msg.Width
- t.height = msg.Height
- }
- return t, nil
-}
-
-func (t *themeDialogComponent) View() string {
- currentTheme := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- if len(t.themes) == 0 {
- return baseStyle.Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderBackground(currentTheme.Background()).
- BorderForeground(currentTheme.TextMuted()).
- Width(40).
- Render("No themes available")
}
- // Calculate max width needed for theme names
- maxWidth := 40 // Minimum width
- for _, themeName := range t.themes {
- if len(themeName) > maxWidth-4 { // Account for padding
- maxWidth = len(themeName) + 4
- }
- }
+ var cmd tea.Cmd
+ listModel, cmd := t.list.Update(msg)
+ t.list = listModel.(components.SimpleList[themeItem])
+ return t, cmd
+}
- maxWidth = max(30, min(maxWidth, t.width-15)) // Limit width to avoid overflow
+func (t *themeDialog) Render(background string) string {
+ return t.modal.Render(t.list.View(), background)
+}
- // Build the theme list
- themeItems := make([]string, 0, len(t.themes))
- for i, themeName := range t.themes {
- itemStyle := baseStyle.Width(maxWidth)
+func (t *themeDialog) Close() tea.Cmd {
+ return nil
+}
- if i == t.selectedIdx {
- itemStyle = itemStyle.
- Background(currentTheme.Primary()).
- Foreground(currentTheme.Background()).
- Bold(true)
+// NewThemeDialog creates a new theme switching dialog
+func NewThemeDialog() ThemeDialog {
+ themes := theme.AvailableThemes()
+ currentTheme := theme.CurrentThemeName()
+
+ var themeItems []themeItem
+ var selectedIdx int
+ for i, name := range themes {
+ themeItems = append(themeItems, themeItem{name: name})
+ if name == currentTheme {
+ selectedIdx = i
}
-
- themeItems = append(themeItems, itemStyle.Padding(0, 1).Render(themeName))
}
- title := baseStyle.
- Foreground(currentTheme.Primary()).
- Bold(true).
- Width(maxWidth).
- Padding(0, 1).
- Render("Select Theme")
-
- content := lipgloss.JoinVertical(
- lipgloss.Left,
- title,
- baseStyle.Width(maxWidth).Render(""),
- baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, themeItems...)),
- baseStyle.Width(maxWidth).Render(""),
+ list := components.NewSimpleList(
+ themeItems,
+ 10, // maxVisibleThemes
+ "No themes available",
+ true,
)
- return baseStyle.Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderBackground(currentTheme.Background()).
- BorderForeground(currentTheme.TextMuted()).
- Width(lipgloss.Width(content) + 4).
- Render(content)
-}
+ // Set the initial selection to the current theme
+ list.SetSelectedIndex(selectedIdx)
-func (t *themeDialogComponent) BindingKeys() []key.Binding {
- return layout.KeyMapToSlice(themeKeys)
-}
-
-// NewThemeDialogCmp creates a new theme switching dialog
-func NewThemeDialogCmp() ThemeDialog {
- return &themeDialogComponent{
- themes: []string{},
- selectedIdx: 0,
- currentTheme: "",
+ return &themeDialog{
+ list: list,
+ modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)),
}
}
+
diff --git a/packages/tui/internal/components/modal/modal.go b/packages/tui/internal/components/modal/modal.go
index 9477aee29..88e37c50b 100644
--- a/packages/tui/internal/components/modal/modal.go
+++ b/packages/tui/internal/components/modal/modal.go
@@ -1,25 +1,23 @@
package modal
import (
- "github.com/charmbracelet/bubbles/v2/key"
- tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
+// CloseModalMsg is a message to signal that the active modal should be closed.
+type CloseModalMsg struct{}
+
// Modal is a reusable modal component that handles frame rendering and overlay placement
type Modal struct {
- content tea.Model
- width int
- height int
- title string
- showBorder bool
- borderStyle lipgloss.Border
- maxWidth int
- maxHeight int
- centerContent bool
+ width int
+ height int
+ title string
+ maxWidth int
+ maxHeight int
+ fitContent bool
}
// ModalOption is a function that configures a Modal
@@ -32,24 +30,11 @@ func WithTitle(title string) ModalOption {
}
}
-// WithBorder enables/disables the border
-func WithBorder(show bool) ModalOption {
- return func(m *Modal) {
- m.showBorder = show
- }
-}
-
-// WithBorderStyle sets the border style
-func WithBorderStyle(style lipgloss.Border) ModalOption {
- return func(m *Modal) {
- m.borderStyle = style
- }
-}
-
// WithMaxWidth sets the maximum width
func WithMaxWidth(width int) ModalOption {
return func(m *Modal) {
m.maxWidth = width
+ m.fitContent = false
}
}
@@ -60,22 +45,18 @@ func WithMaxHeight(height int) ModalOption {
}
}
-// WithCenterContent centers the content within the modal
-func WithCenterContent(center bool) ModalOption {
+func WithFitContent(fit bool) ModalOption {
return func(m *Modal) {
- m.centerContent = center
+ m.fitContent = fit
}
}
-// New creates a new Modal with the given content and options
-func New(content tea.Model, opts ...ModalOption) *Modal {
+// New creates a new Modal with the given options
+func New(opts ...ModalOption) *Modal {
m := &Modal{
- content: content,
- showBorder: true,
- borderStyle: lipgloss.ThickBorder(),
- maxWidth: 0,
- maxHeight: 0,
- centerContent: false,
+ maxWidth: 0,
+ maxHeight: 0,
+ fitContent: true,
}
for _, opt := range opts {
@@ -85,40 +66,24 @@ func New(content tea.Model, opts ...ModalOption) *Modal {
return m
}
-func (m *Modal) Init() tea.Cmd {
- return m.content.Init()
-}
-
-func (m *Modal) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- m.width = msg.Width
- m.height = msg.Height
- }
-
- // Pass all messages to the content
- var cmd tea.Cmd
- m.content, cmd = m.content.Update(msg)
- return m, cmd
-}
-
-func (m *Modal) View() string {
+// Render renders the modal centered on the screen
+func (m *Modal) Render(contentView string, background string) string {
t := theme.CurrentTheme()
-
- // Get the content view
- contentView := ""
- if v, ok := m.content.(layout.ModelWithView); ok {
- contentView = v.View()
- }
- // Calculate dimensions
outerWidth := layout.Current.Container.Width - 8
if m.maxWidth > 0 && outerWidth > m.maxWidth {
outerWidth = m.maxWidth
}
-
+
+ if m.fitContent {
+ titleWidth := lipgloss.Width(m.title)
+ contentWidth := lipgloss.Width(contentView)
+ largestWidth := max(titleWidth+2, contentWidth)
+ outerWidth = largestWidth + 6
+ }
+
innerWidth := outerWidth - 4
-
+
// Base style for the modal
baseStyle := styles.BaseStyle().
Background(t.BackgroundElement()).
@@ -132,7 +97,7 @@ func (m *Modal) View() string {
Bold(true).
Width(innerWidth).
Padding(0, 1)
-
+
titleView := titleStyle.Render(m.title)
finalContent = lipgloss.JoinVertical(
lipgloss.Left,
@@ -143,56 +108,36 @@ func (m *Modal) View() string {
finalContent = contentView
}
- // Apply modal styling
modalStyle := baseStyle.
PaddingTop(1).
PaddingBottom(1).
PaddingLeft(2).
- PaddingRight(2)
-
- if m.showBorder {
- modalStyle = modalStyle.
- BorderStyle(m.borderStyle).
- BorderLeft(true).
- BorderRight(true).
- BorderLeftForeground(t.BackgroundSubtle()).
- BorderLeftBackground(t.Background()).
- BorderRightForeground(t.BackgroundSubtle()).
- BorderRightBackground(t.Background())
- }
-
- return modalStyle.
+ PaddingRight(2).
+ BorderStyle(lipgloss.ThickBorder()).
+ BorderLeft(true).
+ BorderRight(true).
+ BorderLeftForeground(t.BackgroundSubtle()).
+ BorderLeftBackground(t.Background()).
+ BorderRightForeground(t.BackgroundSubtle()).
+ BorderRightBackground(t.Background())
+
+ modalView := modalStyle.
Width(outerWidth).
Render(finalContent)
-}
-// Render renders the modal centered on the screen
-func (m *Modal) Render(background string) string {
- modalView := m.View()
-
// Calculate position for centering
bgHeight := lipgloss.Height(background)
bgWidth := lipgloss.Width(background)
modalHeight := lipgloss.Height(modalView)
modalWidth := lipgloss.Width(modalView)
-
+
row := (bgHeight - modalHeight) / 2
col := (bgWidth - modalWidth) / 2
-
- // Use PlaceOverlay to render the modal on top of the background
+
return layout.PlaceOverlay(
col,
row,
modalView,
background,
- true, // shadow
)
}
-
-// BindingKeys returns the key bindings from the content if it implements layout.Bindings
-func (m *Modal) BindingKeys() []key.Binding {
- if b, ok := m.content.(layout.Bindings); ok {
- return b.BindingKeys()
- }
- return []key.Binding{}
-} \ No newline at end of file
diff --git a/packages/tui/internal/components/util/simple-list.go b/packages/tui/internal/components/util/simple-list.go
index 341495613..fcffe5b96 100644
--- a/packages/tui/internal/components/util/simple-list.go
+++ b/packages/tui/internal/components/util/simple-list.go
@@ -20,6 +20,7 @@ type SimpleList[T SimpleListItem] interface {
GetSelectedItem() (item T, idx int)
SetItems(items []T)
GetItems() []T
+ SetSelectedIndex(idx int)
}
type simpleListComponent[T SimpleListItem] struct {
@@ -109,6 +110,12 @@ func (c *simpleListComponent[T]) SetMaxWidth(width int) {
c.maxWidth = width
}
+func (c *simpleListComponent[T]) SetSelectedIndex(idx int) {
+ if idx >= 0 && idx < len(c.items) {
+ c.selectedIdx = idx
+ }
+}
+
func (c *simpleListComponent[T]) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()