Reset master to testing content

This commit is contained in:
Storm Dragon committed 2026-10-25 01:57:54 -04:00
1 parent 0f2587b719
commit 42afa2a56c
1483 files changed
+145308 -938069

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package clipboard
import (
"encoding/json"
"io"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"syscall"
)
// Snapshot contains only the clipboard facts needed for Cthulhu's announcement.
type Snapshot struct {
Text string `json:"text"`
HasText bool `json:"hasText"`
Image bool `json:"image"`
URIs []string `json:"uris"`
}
// WriteMessage is called only by the short lived clipboard helper process.
// URI filesystem checks stay here so the parent can enforce a read deadline.
func WriteMessage(output io.Writer) error {
snapshot, err := read_native()
if err != nil {
return err
}
return json.NewEncoder(output).Encode(struct {
Message string `json:"message"`
}{Message: Format(snapshot)})
}
func Format(snapshot Snapshot) string {
if snapshot.Image {
return "The clipboard contains a image"
}
if len(snapshot.URIs) != 0 {
return format_uris(snapshot.URIs)
}
if snapshot.HasText {
return "Text in clipboard " + snapshot.Text
}
return "The clipboard is empty"
}
func format_uris(uris []string) string {
objects := make([]string, 0, len(uris))
for _, raw := range uris {
if raw == "" {
continue
}
parsed, err := url.Parse(raw)
if err != nil {
objects = append(objects, raw)
continue
}
name := path.Base(parsed.Path)
if name == "." || name == "/" {
name = raw
}
parts := []string{name}
if parsed.Scheme == "file" && (parsed.Host == "" || parsed.Host == "localhost") {
localPath := parsed.Path
link := false
if info, err := os.Lstat(localPath); err == nil && info.Mode()&os.ModeSymlink != 0 {
link = true
}
if info, err := os.Stat(localPath); err == nil {
if info.IsDir() {
parts = append(parts, "Folder")
} else if info.Mode().IsRegular() {
parts = append(parts, "File")
}
if !link && is_mount(localPath, info) {
parts = append(parts, "Disk")
}
}
if link {
parts = append(parts, "Link")
}
}
objects = append(objects, strings.Join(parts, " "))
}
if len(objects) == 0 {
return "The clipboard is empty"
}
label := " Object in clipboard "
if len(objects) > 1 {
label = " Objects in clipboard "
}
return strconv.Itoa(len(objects)) + label + strings.Join(objects, " ")
}
func is_mount(localPath string, info os.FileInfo) bool {
parentPath := filepath.Dir(filepath.Clean(localPath))
parent, err := os.Stat(parentPath)
if err != nil {
return false
}
currentStat, currentOK := info.Sys().(*syscall.Stat_t)
parentStat, parentOK := parent.Sys().(*syscall.Stat_t)
return currentOK && parentOK && (currentStat.Dev != parentStat.Dev || currentStat.Ino == parentStat.Ino)
}
+56
View File
@@ -0,0 +1,56 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package clipboard
import (
"net/url"
"os"
"path/filepath"
"testing"
)
func TestFormatClipboardContent(t *testing.T) {
for _, test := range []struct {
name string
snapshot Snapshot
want string
}{
{"empty", Snapshot{}, "The clipboard is empty"},
{"text", Snapshot{HasText: true, Text: "hello"}, "Text in clipboard hello"},
{"empty text", Snapshot{HasText: true}, "Text in clipboard "},
{"image overrides text", Snapshot{HasText: true, Text: "hello", Image: true}, "The clipboard contains a image"},
} {
t.Run(test.name, func(t *testing.T) {
if got := Format(test.snapshot); got != test.want {
t.Fatalf("got %q, want %q", got, test.want)
}
})
}
}
func TestFormatURIObjects(t *testing.T) {
root := t.TempDir()
folder := filepath.Join(root, "a folder")
file := filepath.Join(root, "a file")
link := filepath.Join(root, "link")
if err := os.Mkdir(folder, 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(file, []byte("x"), 0600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(file, link); err != nil {
t.Fatal(err)
}
uri := func(localPath string) string { return (&url.URL{Scheme: "file", Path: localPath}).String() }
snapshot := Snapshot{URIs: []string{uri(folder), uri(file), uri(link)}}
want := "3 Objects in clipboard a folder Folder a file File link File Link"
if got := Format(snapshot); got != want {
t.Fatalf("got %q, want %q", got, want)
}
if got := Format(Snapshot{URIs: []string{"", uri(file)}}); got != "1 Object in clipboard a file File" {
t.Fatalf("single URI = %q", got)
}
}
+68
View File
@@ -0,0 +1,68 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package clipboard
/*
#cgo pkg-config: gtk+-3.0
#include <gtk/gtk.h>
typedef struct {
char *text;
char **uris;
int image;
int initialized;
} ClipboardSnapshot;
static ClipboardSnapshot read_clipboard(void) {
ClipboardSnapshot result = {0};
if (!gtk_init_check(NULL, NULL)) return result;
result.initialized = 1;
GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
if (!clipboard) { result.initialized = 0; return result; }
result.text = gtk_clipboard_wait_for_text(clipboard);
GdkPixbuf *image = gtk_clipboard_wait_for_image(clipboard);
if (image) {
result.image = 1;
g_object_unref(image);
}
if (gtk_clipboard_wait_is_uris_available(clipboard)) {
result.uris = gtk_clipboard_wait_for_uris(clipboard);
}
return result;
}
static void free_clipboard(ClipboardSnapshot *snapshot) {
g_free(snapshot->text);
g_strfreev(snapshot->uris);
}
*/
import "C"
import (
"errors"
"unsafe"
)
func read_native() (Snapshot, error) {
native := C.read_clipboard()
defer C.free_clipboard(&native)
if native.initialized == 0 {
return Snapshot{}, errors.New("cannot access the desktop clipboard")
}
snapshot := Snapshot{Image: native.image != 0, HasText: native.text != nil}
if native.text != nil {
snapshot.Text = C.GoString(native.text)
}
if native.uris != nil {
for offset := uintptr(0); ; offset += unsafe.Sizeof(uintptr(0)) {
uri := *(**C.char)(unsafe.Add(unsafe.Pointer(native.uris), offset))
if uri == nil {
break
}
snapshot.URIs = append(snapshot.URIs, C.GoString(uri))
}
}
return snapshot, nil
}
+70
View File
@@ -0,0 +1,70 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
#include "watch.h"
static GQueue changes = G_QUEUE_INIT;
static gboolean watchEnabled;
static unsigned long long watchEpoch;
void clipboard_watch_free(ClipboardChange *change) {
if (--change->references) return;
g_free(change->text);
g_free(change);
}
static void text_received(GtkClipboard *clipboard, const gchar *text, gpointer data) {
(void)clipboard;
ClipboardChange *change = data;
change->text = g_strdup(text);
change->done = TRUE;
clipboard_watch_free(change);
}
static void targets_received(GtkClipboard *clipboard, GdkAtom *targets, gint count, gpointer data) {
ClipboardChange *change = data;
if (!watchEnabled || watchEpoch != change->epoch || !targets ||
gtk_targets_include_image(targets, count, FALSE) ||
gtk_targets_include_uri(targets, count) ||
!gtk_targets_include_text(targets, count)) {
change->done = TRUE;
clipboard_watch_free(change);
return;
}
// No image or file payload is requested, only a text target.
gtk_clipboard_request_text(clipboard, text_received, change);
}
static void owner_changed(GtkClipboard *clipboard, GdkEventOwnerChange *event, gpointer data) {
(void)data;
if (!watchEnabled) return;
if (g_queue_get_length(&changes) >= 128)
clipboard_watch_free(g_queue_pop_head(&changes));
ClipboardChange *change = g_new0(ClipboardChange, 1);
change->epoch = watchEpoch;
change->timestamp = event->time;
change->deadline = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND;
change->references = 2; // queue and asynchronous callback
g_queue_push_tail(&changes, change);
gtk_clipboard_request_targets(clipboard, targets_received, change);
}
int clipboard_watch_init(void) {
if (!gtk_init_check(NULL, NULL)) return 0;
GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
if (!clipboard) return 0;
g_signal_connect(clipboard, "owner-change", G_CALLBACK(owner_changed), NULL);
return 1;
}
void clipboard_watch_enable(int enabled, unsigned long long epoch) {
watchEnabled = enabled;
watchEpoch = epoch;
}
void clipboard_watch_iterate(void) {
while (gtk_events_pending()) gtk_main_iteration_do(FALSE);
}
ClipboardChange *clipboard_watch_next(void) {
ClipboardChange *change = g_queue_peek_head(&changes);
if (!change || (!change->done && g_get_monotonic_time() < change->deadline)) return NULL;
return g_queue_pop_head(&changes);
}
+100
View File
@@ -0,0 +1,100 @@
// 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
}
}
}
}
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
#include <gtk/gtk.h>
typedef struct {
char *text;
unsigned long long epoch;
gboolean done;
gint64 deadline;
guint timestamp;
int references;
} ClipboardChange;
int clipboard_watch_init(void);
void clipboard_watch_enable(int enabled, unsigned long long epoch);
void clipboard_watch_iterate(void);
ClipboardChange *clipboard_watch_next(void);
void clipboard_watch_free(ClipboardChange *change);