From 9e70e36f7f802b9bfe7491e60ee3dbad363e946e Mon Sep 17 00:00:00 2001 From: mirivlad Date: Fri, 5 Jun 2026 07:34:45 +0800 Subject: [PATCH] feat: add native clipboard capture bridge --- cmd/verstak-gui/bindings_clipboard.go | 42 ++++++++++++++++++++++ cmd/verstak-gui/capture_test.go | 18 ++++++++++ frontend/src/wailsjs/go/main/App.js | 52 +++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 cmd/verstak-gui/bindings_clipboard.go diff --git a/cmd/verstak-gui/bindings_clipboard.go b/cmd/verstak-gui/bindings_clipboard.go new file mode 100644 index 0000000..df82d4d --- /dev/null +++ b/cmd/verstak-gui/bindings_clipboard.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "strings" + + wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime" +) + +func (a *App) ReadClipboardText() (string, error) { + text, err := wailsruntime.ClipboardGetText(a.ctx) + if err != nil { + return "", fmt.Errorf("clipboard text is unavailable") + } + return text, nil +} + +func (a *App) CaptureClipboardTextWithContext(contextJSON string) (*InboxNodeDTO, error) { + text, err := a.ReadClipboardText() + if err != nil { + return nil, err + } + kind, value := classifyClipboardText(text) + if value == "" { + return nil, fmt.Errorf("clipboard is empty") + } + if kind == "url" { + return a.CaptureURLWithContext(value, "", "clipboard_button", contextJSON) + } + return a.CaptureTextWithContext(value, "clipboard_button", contextJSON) +} + +func classifyClipboardText(text string) (string, string) { + value := strings.TrimSpace(text) + if value == "" { + return "text", "" + } + if isURLLike(value) { + return "url", value + } + return "text", value +} diff --git a/cmd/verstak-gui/capture_test.go b/cmd/verstak-gui/capture_test.go index f799bad..178f235 100644 --- a/cmd/verstak-gui/capture_test.go +++ b/cmd/verstak-gui/capture_test.go @@ -245,3 +245,21 @@ func TestCapturePathWithNodeContextUsesNodeIDForLocalInbox(t *testing.T) { t.Fatalf("local inbox B = %+v, want empty for same title different node", localB) } } + +func TestClassifyClipboardTextRoutesURLBeforePlainText(t *testing.T) { + kind, value := classifyClipboardText(" https://example.test/page ") + if kind != "url" { + t.Fatalf("kind = %q, want url", kind) + } + if value != "https://example.test/page" { + t.Fatalf("value = %q, want trimmed URL", value) + } + + kind, value = classifyClipboardText("not a url\nwith more text") + if kind != "text" { + t.Fatalf("kind = %q, want text", kind) + } + if value != "not a url\nwith more text" { + t.Fatalf("value = %q, want trimmed text", value) + } +} diff --git a/frontend/src/wailsjs/go/main/App.js b/frontend/src/wailsjs/go/main/App.js index 1c8d8d2..1a6e73a 100644 --- a/frontend/src/wailsjs/go/main/App.js +++ b/frontend/src/wailsjs/go/main/App.js @@ -82,30 +82,82 @@ export function ListInboxNodes() { return window['go']['main']['App']['ListInboxNodes'](); } +export function ListInboxNodesForTarget(arg1) { + return window['go']['main']['App']['ListInboxNodesForTarget'](arg1); +} + export function CaptureText(arg1) { return window['go']['main']['App']['CaptureText'](arg1); } +export function CaptureTextWithContext(arg1, arg2, arg3) { + return window['go']['main']['App']['CaptureTextWithContext'](arg1, arg2, arg3); +} + export function CaptureURL(arg1, arg2) { return window['go']['main']['App']['CaptureURL'](arg1, arg2); } +export function CaptureURLWithContext(arg1, arg2, arg3, arg4) { + return window['go']['main']['App']['CaptureURLWithContext'](arg1, arg2, arg3, arg4); +} + export function CapturePath(arg1) { return window['go']['main']['App']['CapturePath'](arg1); } +export function CapturePathWithContext(arg1, arg2, arg3) { + return window['go']['main']['App']['CapturePathWithContext'](arg1, arg2, arg3); +} + export function CaptureFileData(arg1, arg2) { return window['go']['main']['App']['CaptureFileData'](arg1, arg2); } +export function CaptureFileDataWithContext(arg1, arg2, arg3, arg4) { + return window['go']['main']['App']['CaptureFileDataWithContext'](arg1, arg2, arg3, arg4); +} + +export function ReadClipboardText() { + return window['go']['main']['App']['ReadClipboardText'](); +} + +export function CaptureClipboardTextWithContext(arg1) { + return window['go']['main']['App']['CaptureClipboardTextWithContext'](arg1); +} + export function AssignInboxNode(arg1, arg2) { return window['go']['main']['App']['AssignInboxNode'](arg1, arg2); } +export function ResolveInboxNode(arg1, arg2) { + return window['go']['main']['App']['ResolveInboxNode'](arg1, arg2); +} + +export function ResolveInboxNodeHere(arg1) { + return window['go']['main']['App']['ResolveInboxNodeHere'](arg1); +} + export function DeleteInboxNode(arg1) { return window['go']['main']['App']['DeleteInboxNode'](arg1); } +export function ListLinks(arg1) { + return window['go']['main']['App']['ListLinks'](arg1); +} + +export function UpdateLink(arg1, arg2, arg3, arg4) { + return window['go']['main']['App']['UpdateLink'](arg1, arg2, arg3, arg4); +} + +export function DeleteLink(arg1) { + return window['go']['main']['App']['DeleteLink'](arg1); +} + +export function OpenLink(arg1) { + return window['go']['main']['App']['OpenLink'](arg1); +} + export function ListTrash() { return window['go']['main']['App']['ListTrash'](); }