diff options
| author | adamdottv <[email protected]> | 2025-06-12 16:00:20 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-06-12 16:00:26 -0500 |
| commit | 653965ef5908a240f6038609e17bc8fa27640203 (patch) | |
| tree | 439910084fdcea0c45856ad0132f916fd9038a4b /packages/tui/internal/components | |
| parent | ca0ea3f94dc1929071efc93bfcf708c8cf054be8 (diff) | |
| download | opencode-653965ef5908a240f6038609e17bc8fa27640203.tar.gz opencode-653965ef5908a240f6038609e17bc8fa27640203.zip | |
wip: refactoring tui
Diffstat (limited to 'packages/tui/internal/components')
| -rw-r--r-- | packages/tui/internal/components/chat/editor.go | 38 | ||||
| -rw-r--r-- | packages/tui/internal/components/chat/message.go | 4 | ||||
| -rw-r--r-- | packages/tui/internal/components/chat/messages.go | 2 | ||||
| -rw-r--r-- | packages/tui/internal/components/dialog/help.go | 239 | ||||
| -rw-r--r-- | packages/tui/internal/components/dialog/models.go | 129 | ||||
| -rw-r--r-- | packages/tui/internal/components/dialog/quit.go | 73 | ||||
| -rw-r--r-- | packages/tui/internal/components/dialog/session.go | 161 | ||||
| -rw-r--r-- | packages/tui/internal/components/dialog/theme.go | 219 | ||||
| -rw-r--r-- | packages/tui/internal/components/modal/modal.go | 137 | ||||
| -rw-r--r-- | packages/tui/internal/components/util/simple-list.go | 7 |
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() |
