summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/components/dialog
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-07-02 16:08:06 -0500
committeradamdottv <[email protected]>2025-07-02 16:08:11 -0500
commitc82a060eca41b990b4dd89cecffb874b2133af6f (patch)
treec3bb84aee2e621da9feb18f866c88fc51da0e7c1 /packages/tui/internal/components/dialog
parent63e783ef795d91c745733b945247e917f1683d31 (diff)
downloadopencode-c82a060eca41b990b4dd89cecffb874b2133af6f.tar.gz
opencode-c82a060eca41b990b4dd89cecffb874b2133af6f.zip
feat(tui): file viewer, select messages
Diffstat (limited to 'packages/tui/internal/components/dialog')
-rw-r--r--packages/tui/internal/components/dialog/complete.go5
-rw-r--r--packages/tui/internal/components/dialog/find.go235
2 files changed, 235 insertions, 5 deletions
diff --git a/packages/tui/internal/components/dialog/complete.go b/packages/tui/internal/components/dialog/complete.go
index 68e656145..f204d910c 100644
--- a/packages/tui/internal/components/dialog/complete.go
+++ b/packages/tui/internal/components/dialog/complete.go
@@ -41,7 +41,6 @@ func (ci *CompletionItem) Render(selected bool, width int) string {
title := itemStyle.Render(
ci.DisplayValue(),
)
-
return title
}
@@ -59,7 +58,6 @@ func NewCompletionItem(completionItem CompletionItem) CompletionItemI {
type CompletionProvider interface {
GetId() string
- GetEntry() CompletionItemI
GetChildEntries(query string) ([]CompletionItemI, error)
GetEmptyMessage() string
}
@@ -175,9 +173,6 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, c.pseudoSearchTextArea.Focus())
return c, tea.Batch(cmds...)
}
- case tea.WindowSizeMsg:
- c.width = msg.Width
- c.height = msg.Height
}
return c, tea.Batch(cmds...)
diff --git a/packages/tui/internal/components/dialog/find.go b/packages/tui/internal/components/dialog/find.go
new file mode 100644
index 000000000..3ca0d105b
--- /dev/null
+++ b/packages/tui/internal/components/dialog/find.go
@@ -0,0 +1,235 @@
+package dialog
+
+import (
+ "log/slog"
+
+ "github.com/charmbracelet/bubbles/v2/key"
+ "github.com/charmbracelet/bubbles/v2/textinput"
+ tea "github.com/charmbracelet/bubbletea/v2"
+ "github.com/sst/opencode/internal/components/list"
+ "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"
+ "github.com/sst/opencode/internal/util"
+)
+
+type FindSelectedMsg struct {
+ FilePath string
+}
+
+type FindDialogCloseMsg struct{}
+
+type FindDialog interface {
+ layout.Modal
+ tea.Model
+ tea.ViewModel
+ SetWidth(width int)
+ SetHeight(height int)
+ IsEmpty() bool
+ SetProvider(provider CompletionProvider)
+}
+
+type findDialogComponent struct {
+ query string
+ completionProvider CompletionProvider
+ width, height int
+ modal *modal.Modal
+ textInput textinput.Model
+ list list.List[CompletionItemI]
+}
+
+type findDialogKeyMap struct {
+ Select key.Binding
+ Cancel key.Binding
+}
+
+var findDialogKeys = findDialogKeyMap{
+ Select: key.NewBinding(
+ key.WithKeys("enter"),
+ ),
+ Cancel: key.NewBinding(
+ key.WithKeys("esc"),
+ ),
+}
+
+func (f *findDialogComponent) Init() tea.Cmd {
+ return textinput.Blink
+}
+
+func (f *findDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmd tea.Cmd
+ var cmds []tea.Cmd
+
+ switch msg := msg.(type) {
+ case []CompletionItemI:
+ f.list.SetItems(msg)
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "ctrl+c":
+ if f.textInput.Value() == "" {
+ return f, nil
+ }
+ f.textInput.SetValue("")
+ return f.update(msg)
+ }
+
+ switch {
+ case key.Matches(msg, findDialogKeys.Select):
+ item, i := f.list.GetSelectedItem()
+ if i == -1 {
+ return f, nil
+ }
+ return f, f.selectFile(item)
+ case key.Matches(msg, findDialogKeys.Cancel):
+ return f, f.Close()
+ default:
+ f.textInput, cmd = f.textInput.Update(msg)
+ cmds = append(cmds, cmd)
+
+ f, cmd = f.update(msg)
+ cmds = append(cmds, cmd)
+ }
+ }
+
+ return f, tea.Batch(cmds...)
+}
+
+func (f *findDialogComponent) update(msg tea.Msg) (*findDialogComponent, tea.Cmd) {
+ var cmd tea.Cmd
+ var cmds []tea.Cmd
+
+ query := f.textInput.Value()
+ if query != f.query {
+ f.query = query
+ cmd = func() tea.Msg {
+ items, err := f.completionProvider.GetChildEntries(query)
+ if err != nil {
+ slog.Error("Failed to get completion items", "error", err)
+ }
+ return items
+ }
+ cmds = append(cmds, cmd)
+ }
+
+ u, cmd := f.list.Update(msg)
+ f.list = u.(list.List[CompletionItemI])
+ cmds = append(cmds, cmd)
+
+ return f, tea.Batch(cmds...)
+}
+
+func (f *findDialogComponent) View() string {
+ t := theme.CurrentTheme()
+ f.textInput.SetWidth(f.width - 8)
+ f.list.SetMaxWidth(f.width - 4)
+ inputView := f.textInput.View()
+ inputView = styles.NewStyle().
+ Background(t.BackgroundPanel()).
+ Height(1).
+ Width(f.width-4).
+ Padding(0, 0).
+ Render(inputView)
+
+ listView := f.list.View()
+ return styles.NewStyle().Height(12).Render(inputView + "\n" + listView)
+}
+
+func (f *findDialogComponent) SetWidth(width int) {
+ f.width = width
+ if width > 4 {
+ f.textInput.SetWidth(width - 4)
+ f.list.SetMaxWidth(width - 4)
+ }
+}
+
+func (f *findDialogComponent) SetHeight(height int) {
+ f.height = height
+}
+
+func (f *findDialogComponent) IsEmpty() bool {
+ return f.list.IsEmpty()
+}
+
+func (f *findDialogComponent) SetProvider(provider CompletionProvider) {
+ f.completionProvider = provider
+ f.list.SetEmptyMessage(" " + provider.GetEmptyMessage())
+ f.list.SetItems([]CompletionItemI{})
+}
+
+func (f *findDialogComponent) selectFile(item CompletionItemI) tea.Cmd {
+ return tea.Sequence(
+ f.Close(),
+ util.CmdHandler(FindSelectedMsg{
+ FilePath: item.GetValue(),
+ }),
+ )
+}
+
+func (f *findDialogComponent) Render(background string) string {
+ return f.modal.Render(f.View(), background)
+}
+
+func (f *findDialogComponent) Close() tea.Cmd {
+ f.textInput.Reset()
+ f.textInput.Blur()
+ return util.CmdHandler(modal.CloseModalMsg{})
+}
+
+func createTextInput(existing *textinput.Model) textinput.Model {
+ t := theme.CurrentTheme()
+ bgColor := t.BackgroundPanel()
+ textColor := t.Text()
+ textMutedColor := t.TextMuted()
+
+ ti := textinput.New()
+
+ ti.Styles.Blurred.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
+ ti.Styles.Blurred.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
+ ti.Styles.Focused.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
+ ti.Styles.Focused.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
+ ti.Styles.Cursor.Color = t.Primary()
+ ti.VirtualCursor = true
+
+ ti.Prompt = " "
+ ti.CharLimit = -1
+ ti.Focus()
+
+ if existing != nil {
+ ti.SetValue(existing.Value())
+ ti.SetWidth(existing.Width())
+ }
+
+ return ti
+}
+
+func NewFindDialog(completionProvider CompletionProvider) FindDialog {
+ ti := createTextInput(nil)
+
+ li := list.NewListComponent(
+ []CompletionItemI{},
+ 10, // max visible items
+ completionProvider.GetEmptyMessage(),
+ false,
+ )
+
+ // Load initial items
+ go func() {
+ items, err := completionProvider.GetChildEntries("")
+ if err != nil {
+ slog.Error("Failed to get completion items", "error", err)
+ }
+ li.SetItems(items)
+ }()
+
+ return &findDialogComponent{
+ query: "",
+ completionProvider: completionProvider,
+ textInput: ti,
+ list: li,
+ modal: modal.New(
+ modal.WithTitle("Find Files"),
+ modal.WithMaxWidth(80),
+ ),
+ }
+}