diff --git a/clipboard_gio.go b/clipboard_gio.go new file mode 100644 index 0000000..08a1c37 --- /dev/null +++ b/clipboard_gio.go @@ -0,0 +1,63 @@ +package main + +import ( + "io" + "strings" + "sync" + + gioclipboard "gioui.org/io/clipboard" + "gioui.org/layout" + + appclipboard "git.julianfamily.org/keepassgo/clipboard" +) + +type clipboardCommandWriter struct { + mu sync.Mutex + pending []string + invalidate func() +} + +func newPlatformClipboardWriter(goos string, invalidate func()) appclipboard.Writer { + if strings.EqualFold(goos, "android") { + return &clipboardCommandWriter{invalidate: invalidate} + } + return nil +} + +func processClipboardWrites(gtx layout.Context, writer appclipboard.Writer) { + commandWriter, ok := writer.(*clipboardCommandWriter) + if !ok { + return + } + commandWriter.Process(gtx) +} + +func (w *clipboardCommandWriter) WriteText(text string) error { + w.mu.Lock() + w.pending = append(w.pending, text) + w.mu.Unlock() + if w.invalidate != nil { + w.invalidate() + } + return nil +} + +func (w *clipboardCommandWriter) Process(gtx layout.Context) { + for _, text := range w.drain() { + gtx.Execute(gioclipboard.WriteCmd{ + Type: "application/text", + Data: io.NopCloser(strings.NewReader(text)), + }) + } +} + +func (w *clipboardCommandWriter) drain() []string { + w.mu.Lock() + defer w.mu.Unlock() + if len(w.pending) == 0 { + return nil + } + pending := append([]string(nil), w.pending...) + w.pending = nil + return pending +} diff --git a/clipboard_gio_test.go b/clipboard_gio_test.go new file mode 100644 index 0000000..aa4dcff --- /dev/null +++ b/clipboard_gio_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "slices" + "testing" +) + +func TestNewPlatformClipboardWriterUsesCommandWriterOnAndroid(t *testing.T) { + t.Parallel() + + writer := newPlatformClipboardWriter("android", nil) + if _, ok := writer.(*clipboardCommandWriter); !ok { + t.Fatalf("newPlatformClipboardWriter(android) = %T, want *clipboardCommandWriter", writer) + } +} + +func TestNewPlatformClipboardWriterUsesSystemClipboardOffAndroid(t *testing.T) { + t.Parallel() + + if writer := newPlatformClipboardWriter("linux", nil); writer != nil { + t.Fatalf("newPlatformClipboardWriter(linux) = %T, want nil", writer) + } +} + +func TestClipboardCommandWriterDrainsQueuedWrites(t *testing.T) { + t.Parallel() + + writer := &clipboardCommandWriter{} + if err := writer.WriteText("username"); err != nil { + t.Fatalf("WriteText(username) error = %v", err) + } + if err := writer.WriteText("password"); err != nil { + t.Fatalf("WriteText(password) error = %v", err) + } + + if got := writer.drain(); !slices.Equal(got, []string{"username", "password"}) { + t.Fatalf("drain() = %v, want [username password]", got) + } + if got := writer.drain(); got != nil { + t.Fatalf("drain() after flush = %v, want nil", got) + } +} diff --git a/main.go b/main.go index d9edee7..7448e19 100644 --- a/main.go +++ b/main.go @@ -5864,6 +5864,7 @@ func run(w *app.Window, mode string, paths statePaths, grpcAddr string) error { manager := &session.Manager{} ui := newUIWithSession(mode, manager, paths) ui.invalidate = w.Invalidate + ui.clipboardWriter = newPlatformClipboardWriter(runtime.GOOS, w.Invalidate) host, err := api.StartHost(grpcAddr, manager, passwords.DefaultProfiles(), ui.clipboardWriter, func() bool { return ui.state.Dirty }) if err != nil { ui.state.ErrorMessage = fmt.Sprintf("start gRPC API: %v", err) @@ -5883,6 +5884,7 @@ func run(w *app.Window, mode string, paths statePaths, grpcAddr string) error { gtx := app.NewContext(&ops, e) ui.processBackgroundActions() ui.layout(gtx) + processClipboardWrites(gtx, ui.clipboardWriter) e.Frame(gtx.Ops) } }