From 65ef75f2223ea329c7fda5cfe46457334961c983 Mon Sep 17 00:00:00 2001 From: chendx <55473491+chendx-github@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:42:38 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"=E6=96=B0=E5=A2=9E=E5=8F=AF=E9=80=89?= =?UTF-8?q?=20GTK=20=E6=96=87=E4=BB=B6=E5=89=AA=E8=B4=B4=E6=9D=BF=E5=90=8E?= =?UTF-8?q?=E7=AB=AF"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 34 -------------- clipboard/gtk.go | 95 --------------------------------------- clipboard/gtk_helper.py | 54 ---------------------- clipboard/linux.go | 77 +------------------------------ clipboard/windows.go | 2 +- cmd/agent/main.go | 2 +- internal/config/config.go | 26 +++++------ 7 files changed, 14 insertions(+), 276 deletions(-) delete mode 100644 clipboard/gtk.go delete mode 100644 clipboard/gtk_helper.py diff --git a/README.md b/README.md index 953509f..4c1c384 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,6 @@ clipboard-sync/ - X11:使用 `xclip` - Wayland:使用 `wl-copy` / `wl-paste` -部分旧版 GNOME / Nautilus 环境(例如 Rocky Linux 8 / Nautilus 3.28)可能无法识别 `xclip` 写入的文件剪贴板。此时可以启用可选的 GTK 文件剪贴板后端,见 `clipboard_file_writer` 配置项。 - ### Windows 要求 - Windows 10 / Windows 11 @@ -159,20 +157,6 @@ which wl-paste which fusermount3 ``` -如果需要启用 GTK 文件剪贴板后端,还需要安装 GTK Python 绑定: - -Debian / Ubuntu: - -```bash -sudo apt-get install -y python3-gi gir1.2-gtk-3.0 -``` - -Rocky / RHEL / CentOS: - -```bash -sudo dnf install -y python3-gobject gtk3 -``` - ### 4. Windows 环境准备 PowerShell 中确认 Go: @@ -211,7 +195,6 @@ cache_dir: "" download_dir: "" mount_dir: "" log_level: "error" -clipboard_file_writer: "native" ``` ### 配置项说明 @@ -253,18 +236,6 @@ clipboard_file_writer: "native" 日志级别,可选 `debug`、`info`、`warn`、`error` 默认建议 `error`,仅保留错误和异常日志 -- `clipboard_file_writer` - Linux 文件剪贴板写入后端,可选 `native`、`gtk`、`auto` - 留空或 `native` 时保持原有 `xclip` / `wl-clipboard` 行为 - `gtk` 用于兼容旧版 GNOME / Nautilus 文件粘贴 - `auto` 会优先尝试 GTK 后端,不可用时回退原生后端 - -GTK 后端配置示例: - -```yaml -clipboard_file_writer: "gtk" -``` - ### 推荐配置示例 Linux 设备: @@ -280,7 +251,6 @@ cache_dir: "/tmp/clipboard-sync/cache" download_dir: "/tmp/clipboard-sync/downloads" mount_dir: "/tmp/clipboard-sync/mount" log_level: "error" -clipboard_file_writer: "native" ``` Windows 设备: @@ -343,8 +313,6 @@ netstat -ano | findstr 4222 ### Linux 运行 -Linux agent 应以当前桌面用户身份运行,不建议用 root 运行。否则可能无法访问当前用户的 X11 / Wayland 剪贴板,或导致 FUSE 挂载权限不正确。 - 启动 agent: ```bash @@ -527,8 +495,6 @@ Linux 检查: - 是否是在文件管理器中粘贴,而不是纯文本输入框 - FUSE 挂载是否成功 - 剪切板里是否已变成虚拟文件路径 -- 如果是 Rocky Linux 8 / Nautilus 3.28 等旧版 GNOME 环境,可在配置中启用 `clipboard_file_writer: "gtk"` -- 使用 GTK 后端时,确认已安装 `python3-gobject` / GTK3,并且 agent 是以桌面用户身份运行 Windows 检查: diff --git a/clipboard/gtk.go b/clipboard/gtk.go deleted file mode 100644 index 84e1dff..0000000 --- a/clipboard/gtk.go +++ /dev/null @@ -1,95 +0,0 @@ -//go:build linux - -package clipboard - -import ( - _ "embed" - "fmt" - "os" - "os/exec" - "path/filepath" - "syscall" -) - -//go:embed gtk_helper.py -var gtkHelperScript string - -// gtkFileWriter writes a Nautilus-compatible file-copy payload to the clipboard -// via a transient GTK helper process that holds CLIPBOARD ownership. This is -// required for file paste to work in older file managers (Nautilus 3.x) that -// only honour clipboards owned by GTK programs. -type gtkFileWriter struct { - python string - helper string -} - -// detectGTKFileWriter probes for a python3 interpreter that can import pygobject -// (gi + Gtk 3) and materialises the embedded helper script on disk. -func detectGTKFileWriter() (*gtkFileWriter, error) { - python, err := findGTKPython() - if err != nil { - return nil, err - } - dir, err := os.MkdirTemp("", "clipboard-sync-gtk-") - if err != nil { - return nil, err - } - helper := filepath.Join(dir, "gtk_helper.py") - if err := os.WriteFile(helper, []byte(gtkHelperScript), 0o644); err != nil { - os.RemoveAll(dir) - return nil, err - } - return >kFileWriter{python: python, helper: helper}, nil -} - -func findGTKPython() (string, error) { - candidates := []string{"/usr/bin/python3", "python3"} - for _, candidate := range candidates { - path, err := exec.LookPath(candidate) - if err != nil { - continue - } - probe := exec.Command(path, "-c", - "import gi; gi.require_version('Gtk','3.0'); gi.require_version('Gdk','3.0'); from gi.repository import Gtk, Gdk") - if probe.Run() == nil { - return path, nil - } - } - return "", fmt.Errorf("no python3 with pygobject (gi/Gtk 3) available") -} - -// writeFiles spawns a detached GTK helper that takes clipboard ownership with -// the given payload. The previous helper (if any) self-terminates because -// taking ownership fires its owner-change handler. -func (w *gtkFileWriter) writeFiles(payload string) error { - tmp, err := os.CreateTemp("", "clipboard-sync-payload-*.txt") - if err != nil { - return fmt.Errorf("create payload file: %w", err) - } - if _, err := tmp.WriteString(payload); err != nil { - tmp.Close() - os.Remove(tmp.Name()) - return fmt.Errorf("write payload file: %w", err) - } - if err := tmp.Close(); err != nil { - os.Remove(tmp.Name()) - return fmt.Errorf("close payload file: %w", err) - } - - cmd := exec.Command(w.python, w.helper, tmp.Name()) - // Fully detach so the helper survives as an independent session and keeps - // holding clipboard ownership regardless of the agent's lifetime. - cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} - cmd.Stdin = nil - cmd.Stdout = nil - cmd.Stderr = nil - if err := cmd.Start(); err != nil { - return fmt.Errorf("start gtk clipboard helper: %w", err) - } - // Reap adoption: we intentionally do not Wait; the helper self-exits on - // ownership change. Release the handle so it does not become a zombie. - if cmd.Process != nil { - _ = cmd.Process.Release() - } - return nil -} diff --git a/clipboard/gtk_helper.py b/clipboard/gtk_helper.py deleted file mode 100644 index 3c38cb8..0000000 --- a/clipboard/gtk_helper.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -"""GTK clipboard owner for clipboard-sync (Linux). - -Older file managers (Nautilus 3.x on RHEL/Rocky 8) only enable Paste when a -GTK program owns the CLIPBOARD selection. This helper takes ownership with a -Nautilus-compatible file-copy text payload and stays alive until another app -takes ownership (or a safety timeout elapses), so the file manager can paste. - -Payload is read from the file path given as argv[1]; it must be text in the -"x-special/nautilus-clipboard" format produced by the Go agent. -""" -import os -import sys -import time -import gi - -gi.require_version("Gtk", "3.0") -gi.require_version("Gdk", "3.0") -gi.require_version("GLib", "2.0") -from gi.repository import Gtk, Gdk, GLib - - -def main(): - if len(sys.argv) < 2: - sys.exit(1) - payload_path = sys.argv[1] - with open(payload_path, "r", encoding="utf-8") as handle: - payload = handle.read() - try: - os.unlink(payload_path) - except OSError: - pass - - clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - clipboard.set_text(payload, -1) - clipboard.store() - - started = time.time() - - def on_owner_change(*_): - # Ignore the ownership change triggered by our own set_text (delivered - # right after we connect) and quit only when someone else takes over. - if time.time() - started > 1.0: - Gtk.main_quit() - - clipboard.connect("owner-change", on_owner_change) - # Safety net: never stay alive longer than one hour. - GLib.timeout_add_seconds(3600, Gtk.main_quit) - - Gtk.main() - - -if __name__ == "__main__": - main() diff --git a/clipboard/linux.go b/clipboard/linux.go index 605cd08..50b2e47 100644 --- a/clipboard/linux.go +++ b/clipboard/linux.go @@ -20,7 +20,6 @@ import ( type linuxClipboard struct { pollInterval time.Duration backend linuxBackend - gtkWriter *gtkFileWriter last string } @@ -31,27 +30,12 @@ const ( backendWayland ) -func New(pollInterval time.Duration, fileWriter string) (Clipboard, error) { +func New(pollInterval time.Duration) (Clipboard, error) { backend, err := detectLinuxBackend() if err != nil { return nil, err } - clip := &linuxClipboard{pollInterval: pollInterval, backend: backend} - // Older file managers (Nautilus 3.x) only honour file paste from a - // GTK-owned clipboard. Optionally route file writes through a GTK helper. - switch fileWriter { - case "gtk": - writer, gerr := detectGTKFileWriter() - if gerr != nil { - return nil, fmt.Errorf("gtk file writer requested but unavailable: %w", gerr) - } - clip.gtkWriter = writer - case "auto": - if writer, gerr := detectGTKFileWriter(); gerr == nil { - clip.gtkWriter = writer - } - } - return clip, nil + return &linuxClipboard{pollInterval: pollInterval, backend: backend}, nil } func (c *linuxClipboard) Read() (Data, error) { @@ -174,9 +158,6 @@ func (c *linuxClipboard) readFileListPayload() (string, error) { return string(out), nil } } - if payload, ok := c.readNautilusClipboardText(); ok { - return payload, nil - } return "", errors.New("no file mime type in clipboard") } @@ -187,52 +168,9 @@ func (c *linuxClipboard) readFileListPayload() (string, error) { return string(out), nil } } - if payload, ok := c.readNautilusClipboardText(); ok { - return payload, nil - } return "", errors.New("no file payload in clipboard") } -// readNautilusClipboardText reads a text target and returns it when it carries -// an "x-special/" file list (the nautilus-clipboard / gnome-copied-files text -// form). This is a fallback so that clipboards owned by GTK programs — which -// only expose text targets — are still recognised as file lists, letting our -// own GTK file writes round-trip back as files and avoid an echo loop. -func (c *linuxClipboard) readNautilusClipboardText() (string, bool) { - var candidates []string - if c.backend == backendWayland { - mimeTypes, err := exec.Command("wl-paste", "--list-types").Output() - if err != nil { - return "", false - } - for _, target := range []string{"text/plain;charset=utf-8", "text/plain", "UTF8_STRING"} { - if bytes.Contains(mimeTypes, []byte(target)) { - candidates = append(candidates, target) - } - } - } else { - candidates = []string{"text/plain;charset=utf-8", "text/plain", "UTF8_STRING"} - } - for _, target := range candidates { - var ( - out []byte - err error - ) - if c.backend == backendWayland { - out, err = exec.Command("wl-paste", "--type", target, "--no-newline").Output() - } else { - out, err = exec.Command("xclip", "-selection", "clipboard", "-t", target, "-o").Output() - } - if err != nil || len(out) == 0 { - continue - } - if s := string(out); strings.Contains(s, "x-special/") { - return s, true - } - } - return "", false -} - func parseClipboardFileList(payload string) ([]string, error) { var files []string scanner := bufio.NewScanner(strings.NewReader(payload)) @@ -287,17 +225,6 @@ func (c *linuxClipboard) writeFiles(files []string) error { return fmt.Errorf("stat %s: %w", path, err) } } - if c.gtkWriter != nil { - uris := make([]string, 0, len(files)) - for _, path := range files { - uris = append(uris, pathToFileURI(path)) - } - payload := "x-special/nautilus-clipboard\ncopy\n" + strings.Join(uris, "\n") + "\n" - if err := c.gtkWriter.writeFiles(payload); err != nil { - return fmt.Errorf("write gtk files: %w", err) - } - return nil - } lines := make([]string, 0, len(files)+1) lines = append(lines, "copy") for _, path := range files { diff --git a/clipboard/windows.go b/clipboard/windows.go index 02ccd51..8b6ebb1 100644 --- a/clipboard/windows.go +++ b/clipboard/windows.go @@ -141,7 +141,7 @@ type bitmapInfoHeader struct { ClrImportant uint32 } -func New(pollInterval time.Duration, fileWriter string) (Clipboard, error) { +func New(pollInterval time.Duration) (Clipboard, error) { remoteFormatOnce.Do(func() { name, _ := windows.UTF16PtrFromString("ClipboardSyncRemoteMarker") ret, _, _ := procAddClipboardFormat.Call(uintptr(unsafe.Pointer(name))) diff --git a/cmd/agent/main.go b/cmd/agent/main.go index 9d7c80e..11d4f5b 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -87,7 +87,7 @@ func bootstrap(configPath string) (*app.Agent, func(), error) { return nil, nil, err } logger := logx.New(cfg.LogLevel) - clip, err := clipboard.New(cfg.PollInterval(), cfg.ClipboardFileWriter) + clip, err := clipboard.New(cfg.PollInterval()) if err != nil { return nil, nil, err } diff --git a/internal/config/config.go b/internal/config/config.go index d537f62..34b9681 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,17 +10,16 @@ import ( ) type Config struct { - DeviceID string `yaml:"device_id"` - GroupID string `yaml:"group_id"` - NATSURL string `yaml:"nats_url"` - ChunkSize int `yaml:"chunk_size"` - TokenTTL int `yaml:"token_ttl"` - PollIntervalMS int `yaml:"poll_interval_ms"` - CacheDir string `yaml:"cache_dir"` - DownloadDir string `yaml:"download_dir"` - MountDir string `yaml:"mount_dir"` - LogLevel string `yaml:"log_level"` - ClipboardFileWriter string `yaml:"clipboard_file_writer"` + DeviceID string `yaml:"device_id"` + GroupID string `yaml:"group_id"` + NATSURL string `yaml:"nats_url"` + ChunkSize int `yaml:"chunk_size"` + TokenTTL int `yaml:"token_ttl"` + PollIntervalMS int `yaml:"poll_interval_ms"` + CacheDir string `yaml:"cache_dir"` + DownloadDir string `yaml:"download_dir"` + MountDir string `yaml:"mount_dir"` + LogLevel string `yaml:"log_level"` } func Load(path string) (Config, error) { @@ -82,11 +81,6 @@ func validate(cfg Config) error { if cfg.PollIntervalMS < 100 { return fmt.Errorf("poll_interval_ms must be >= 100") } - switch cfg.ClipboardFileWriter { - case "", "native", "gtk", "auto": - default: - return fmt.Errorf("clipboard_file_writer must be one of: native, gtk, auto") - } return nil }