101 lines
2.2 KiB
Go
101 lines
2.2 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package clipboard
|
|
|
|
/*
|
|
#include "watch.h"
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"runtime"
|
|
"time"
|
|
)
|
|
|
|
type WatchSettings struct {
|
|
Enabled bool
|
|
Epoch uint64
|
|
}
|
|
|
|
type TextChange struct {
|
|
Text string
|
|
Epoch uint64
|
|
Timestamp uint32
|
|
Ready bool
|
|
}
|
|
|
|
// WatchText keeps a GTK selection connection warm so successive clipboard
|
|
// changes are captured asynchronously and emitted in arrival order. Disabled
|
|
// applications never request clipboard targets or contents.
|
|
func WatchText(ctx context.Context, input io.Reader, output io.Writer) error {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
settings := make(chan WatchSettings, 1)
|
|
inputDone := make(chan error, 1)
|
|
go func() {
|
|
scanner := bufio.NewScanner(input)
|
|
scanner.Buffer(make([]byte, 1024), 4096)
|
|
for scanner.Scan() {
|
|
var request WatchSettings
|
|
if err := json.Unmarshal(scanner.Bytes(), &request); err != nil {
|
|
inputDone <- err
|
|
return
|
|
}
|
|
select {
|
|
case settings <- request:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
inputDone <- scanner.Err()
|
|
}()
|
|
if C.clipboard_watch_init() == 0 {
|
|
return errors.New("cannot watch clipboard on this display")
|
|
}
|
|
encoder := json.NewEncoder(output)
|
|
if err := encoder.Encode(TextChange{Ready: true}); err != nil {
|
|
return err
|
|
}
|
|
ticker := time.NewTicker(time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case err := <-inputDone:
|
|
return err
|
|
case request := <-settings:
|
|
enabled := C.int(0)
|
|
if request.Enabled {
|
|
enabled = 1
|
|
}
|
|
C.clipboard_watch_enable(enabled, C.ulonglong(request.Epoch))
|
|
if err := encoder.Encode(TextChange{Ready: true, Epoch: request.Epoch}); err != nil {
|
|
return err
|
|
}
|
|
case <-ticker.C:
|
|
C.clipboard_watch_iterate()
|
|
for {
|
|
change := C.clipboard_watch_next()
|
|
if change == nil {
|
|
break
|
|
}
|
|
value := TextChange{Text: C.GoString(change.text), Epoch: uint64(change.epoch), Timestamp: uint32(change.timestamp)}
|
|
C.clipboard_watch_free(change)
|
|
if err := encoder.Encode(value); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|