-
Notifications
You must be signed in to change notification settings - Fork 10
fix(ui): support clipboard paste in filter and command inputs #202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| package view | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
| "time" | ||
|
|
||
| tea "charm.land/bubbletea/v2" | ||
|
|
||
| "github.com/clawscli/claws/internal/dao" | ||
| "github.com/clawscli/claws/internal/registry" | ||
| ) | ||
|
|
||
| func TestResourceBrowserFilterPaste(t *testing.T) { | ||
| ctx := context.Background() | ||
| reg := registry.New() | ||
|
|
||
| browser := NewResourceBrowser(ctx, reg, "ec2") | ||
| browser.SetSize(100, 50) | ||
| browser.renderer = &mockRenderer{detail: "test"} | ||
| browser.resources = []dao.Resource{ | ||
| &mockResource{id: "i-0123456789abcdef0", name: "web-server"}, | ||
| &mockResource{id: "i-0fedcba9876543210", name: "db-server"}, | ||
| } | ||
| browser.applyFilter() | ||
| browser.buildTable() | ||
|
|
||
| // Paste while the filter is inactive should be ignored | ||
| browser.Update(tea.PasteMsg{Content: "i-0123456789abcdef0"}) | ||
| if browser.filterText != "" { | ||
| t.Fatalf("Expected paste to be ignored when filter is inactive, got filterText %q", browser.filterText) | ||
| } | ||
|
|
||
| browser.filterActive = true | ||
| browser.filterInput.Focus() | ||
|
|
||
| // Bracketed paste (Ctrl+Shift+V / tmux paste) arrives as tea.PasteMsg | ||
| browser.Update(tea.PasteMsg{Content: "i-0123456789abcdef0"}) | ||
|
|
||
| if browser.filterText != "i-0123456789abcdef0" { | ||
| t.Errorf("filterText = %q, want %q", browser.filterText, "i-0123456789abcdef0") | ||
| } | ||
| if len(browser.filtered) != 1 || browser.filtered[0].GetID() != "i-0123456789abcdef0" { | ||
| t.Errorf("Expected filter to narrow to the pasted instance ID, got %d resources", len(browser.filtered)) | ||
| } | ||
|
|
||
| // Ctrl+V must produce the textinput's clipboard-read command | ||
| _, cmd := browser.Update(tea.KeyPressMsg{Code: 'v', Mod: tea.ModCtrl}) | ||
| if cmd == nil { | ||
| t.Error("Expected ctrl+v to return the clipboard paste command") | ||
| } | ||
| } | ||
|
|
||
| func TestServiceBrowserFilterPaste(t *testing.T) { | ||
| ctx := context.Background() | ||
| reg := registry.New() | ||
| reg.RegisterCustom("ec2", "instances", registry.Entry{}) | ||
| reg.RegisterCustom("s3", "buckets", registry.Entry{}) | ||
|
|
||
| browser := NewServiceBrowser(ctx, reg) | ||
| browser.Update(browser.Init()()) | ||
|
|
||
| browser.filterActive = true | ||
| browser.filterInput.Focus() | ||
|
|
||
| browser.Update(tea.PasteMsg{Content: "ec2"}) | ||
|
|
||
| if browser.filterText != "ec2" { | ||
| t.Errorf("filterText = %q, want %q", browser.filterText, "ec2") | ||
| } | ||
| if len(browser.flatItems) != 1 { | ||
| t.Errorf("Expected 1 service after paste, got %d", len(browser.flatItems)) | ||
| } | ||
| } | ||
|
|
||
| func TestLogViewFilterPaste(t *testing.T) { | ||
| ctx := context.Background() | ||
| lv := NewLogView(ctx, "/aws/test") | ||
| lv.SetSize(80, 24) | ||
| lv.loading = false | ||
| lv.logs = []logEntry{ | ||
| {timestamp: time.Now(), message: "error in handler"}, | ||
| {timestamp: time.Now(), message: "request completed"}, | ||
| } | ||
|
|
||
| lv.filterActive = true | ||
| lv.filterInput.Focus() | ||
|
|
||
| lv.Update(tea.PasteMsg{Content: "error"}) | ||
|
|
||
| if lv.filterText != "error" { | ||
| t.Errorf("filterText = %q, want %q", lv.filterText, "error") | ||
| } | ||
| } | ||
|
|
||
| func TestTagSearchViewFilterPaste(t *testing.T) { | ||
| ctx := context.Background() | ||
| reg := registry.New() | ||
|
|
||
| v := NewTagSearchView(ctx, reg, "") | ||
| v.SetSize(100, 50) | ||
| v.resources = []taggedARN{ | ||
| {RawARN: "arn:aws:ec2:us-east-1:123456789012:instance/i-0123456789abcdef0"}, | ||
| {RawARN: "arn:aws:s3:::my-bucket"}, | ||
| } | ||
| v.applyFilter() | ||
| v.buildTable() | ||
|
|
||
| v.filterActive = true | ||
| v.filterInput.Focus() | ||
|
|
||
| v.Update(tea.PasteMsg{Content: "i-0123456789abcdef0"}) | ||
|
|
||
| if v.filterText != "i-0123456789abcdef0" { | ||
| t.Errorf("filterText = %q, want %q", v.filterText, "i-0123456789abcdef0") | ||
| } | ||
| if len(v.filtered) != 1 { | ||
| t.Errorf("Expected 1 resource after paste, got %d", len(v.filtered)) | ||
| } | ||
| } | ||
|
|
||
| type selectorMockItem struct { | ||
| id string | ||
| label string | ||
| } | ||
|
|
||
| func (s selectorMockItem) GetID() string { return s.id } | ||
| func (s selectorMockItem) GetLabel() string { return s.label } | ||
|
|
||
| func TestMultiSelectorFilterPaste(t *testing.T) { | ||
| m := NewMultiSelector[selectorMockItem]("Select", nil) | ||
| m.SetItems([]selectorMockItem{ | ||
| {id: "i-1", label: "web-server"}, | ||
| {id: "i-2", label: "db-server"}, | ||
| }) | ||
|
|
||
| m.filterActive = true | ||
| m.filterInput.Focus() | ||
|
|
||
| _, result := m.HandleUpdate(tea.PasteMsg{Content: "db"}) | ||
|
|
||
| if result != KeyHandled { | ||
| t.Errorf("Expected paste to be handled, got %v", result) | ||
| } | ||
| if m.filterText != "db" { | ||
| t.Errorf("filterText = %q, want %q", m.filterText, "db") | ||
| } | ||
| if len(m.filtered) != 1 || m.filtered[0].GetID() != "i-2" { | ||
| t.Errorf("Expected filter to narrow to the pasted text, got %d items", len(m.filtered)) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -382,6 +382,11 @@ func (v *LogView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |
| return v, nil | ||
| } | ||
|
|
||
| // Route paste and other textinput-bound messages to the active filter. | ||
| if v.filterActive { | ||
| return v.updateFilterInput(msg) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mouse-wheel scrolling no longer works while the filter is active.
Could you route mouse-wheel messages to the viewport before this fallback? A regression test for scrolling while the filter is active would also be helpful. |
||
| } | ||
|
|
||
| if v.vp.Ready { | ||
| var cmd tea.Cmd | ||
| v.vp.Model, cmd = v.vp.Model.Update(msg) | ||
|
|
@@ -438,17 +443,25 @@ func (v *LogView) handleFilterInput(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { | |
| } | ||
| return v, nil | ||
| default: | ||
| var cmd tea.Cmd | ||
| v.filterInput, cmd = v.filterInput.Update(msg) | ||
| return v.updateFilterInput(msg) | ||
| } | ||
| } | ||
|
|
||
| // Apply filter in real-time as user types | ||
| v.filterText = v.filterInput.Value() | ||
| // updateFilterInput forwards msg to the filter text input and re-applies the | ||
| // filter in real-time when the input value changed. Besides key presses this | ||
| // must receive tea.PasteMsg (bracketed paste) and the textinput's internal | ||
| // clipboard-read results, or pasting into the filter is silently dropped. | ||
| func (v *LogView) updateFilterInput(msg tea.Msg) (tea.Model, tea.Cmd) { | ||
| var cmd tea.Cmd | ||
| v.filterInput, cmd = v.filterInput.Update(msg) | ||
| if value := v.filterInput.Value(); value != v.filterText { | ||
| v.filterText = value | ||
| if v.vp.Ready { | ||
| v.updateViewportContent() | ||
| } | ||
|
|
||
| return v, tea.Batch(cmd, tea.ClearScreen) | ||
| } | ||
| return v, cmd | ||
| } | ||
|
|
||
| func (v *LogView) ViewString() string { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like Ctrl+V still won't paste into the command input.
In Bubbles v2.1.0, the result of the clipboard read is returned as the unexported
textinput.pasteMsg, rather thantea.PasteMsg.Since this branch only forwards
tea.KeyPressMsgandtea.PasteMsgtocommandInput.Update,textinput.pasteMsgfalls through to the current view and never reaches the command input.One concrete way to address this would be to add a clipboard-read
tea.Cmdunderinternal/clipboardthat returns a publictea.PasteMsgon success, and intercept Ctrl+V inCommandInput.Updatebefore delegating totextInput.Update. This would reuse the existinghandleCommandModeMsgbranch without depending on Bubbles' unexported message type.It would also be helpful to add a test that executes the command returned by Ctrl+V and feeds its result back into
App.Update.