From c82a060eca41b990b4dd89cecffb874b2133af6f Mon Sep 17 00:00:00 2001 From: adamdottv <2363879+adamdottv@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:08:06 -0500 Subject: feat(tui): file viewer, select messages --- .../tui/internal/components/dialog/complete.go | 5 - packages/tui/internal/components/dialog/find.go | 235 +++++++++++++++++++++ 2 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 packages/tui/internal/components/dialog/find.go (limited to 'packages/tui/internal/components/dialog') 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), + ), + } +} -- cgit v1.2.3