Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func readClipboardTarget(ctx context.Context, target string) (string, error) {
|
||||
command := exec.CommandContext(ctx, "xclip", "-selection", "clipboard", "-out", "-target", target)
|
||||
command.WaitDelay = time.Second
|
||||
pipe, err := command.StdoutPipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = command.Start(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, readErr := io.ReadAll(io.LimitReader(pipe, 8193))
|
||||
if readErr != nil || len(data) > 8192 {
|
||||
command.Process.Kill()
|
||||
command.Wait()
|
||||
return "", errors.New("clipboard too large")
|
||||
}
|
||||
err = command.Wait()
|
||||
return string(data), err
|
||||
}
|
||||
func readTentacleClipboard(ctx context.Context) (tentaclewire.Item, error) {
|
||||
targets, err := readClipboardTarget(ctx, "TARGETS")
|
||||
if err != nil {
|
||||
return tentaclewire.Item{}, err
|
||||
}
|
||||
for _, target := range strings.Fields(targets) {
|
||||
if target == "text/uri-list" || target == "x-special/gnome-copied-files" {
|
||||
data, err := readClipboardTarget(ctx, target)
|
||||
if err != nil {
|
||||
return tentaclewire.Item{}, err
|
||||
}
|
||||
return singleClipboardURI(data)
|
||||
}
|
||||
}
|
||||
for _, target := range []string{"UTF8_STRING", "text/plain;charset=utf-8", "text/plain", "STRING"} {
|
||||
for _, available := range strings.Fields(targets) {
|
||||
if available == target {
|
||||
text, err := readClipboardTarget(ctx, target)
|
||||
return tentaclewire.Item{Text: text}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, target := range strings.Fields(targets) {
|
||||
if target == "image/png" {
|
||||
return clipboardPNG(ctx)
|
||||
}
|
||||
}
|
||||
return tentaclewire.Item{}, errors.New("clipboard has no transferable text or file")
|
||||
}
|
||||
func singleClipboardURI(data string) (tentaclewire.Item, error) {
|
||||
// GTK file managers can include a C-string terminator in the X selection.
|
||||
// Remove only that trailing byte; embedded NULs remain invalid URI data.
|
||||
data = strings.TrimSuffix(data, "\x00")
|
||||
var uris []string
|
||||
for _, line := range strings.Split(data, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") || line == "copy" || line == "cut" {
|
||||
continue
|
||||
}
|
||||
uris = append(uris, line)
|
||||
}
|
||||
if len(uris) != 1 {
|
||||
return tentaclewire.Item{}, errors.New("copy exactly one file or folder")
|
||||
}
|
||||
parsed, err := url.Parse(uris[0])
|
||||
if err != nil || parsed.Scheme != "file" || parsed.Host != "" && parsed.Host != "localhost" || parsed.RawQuery != "" || parsed.Fragment != "" || !filepath.IsAbs(parsed.Path) || strings.ContainsRune(parsed.Path, '\x00') {
|
||||
return tentaclewire.Item{}, errors.New("local file path unavailable")
|
||||
}
|
||||
return tentaclewire.Item{Path: parsed.Path}, nil
|
||||
}
|
||||
|
||||
func clipboardPNG(ctx context.Context) (tentaclewire.Item, error) {
|
||||
directory, err := os.MkdirTemp("", "cthulhu-clipboard-*")
|
||||
if err != nil {
|
||||
return tentaclewire.Item{}, err
|
||||
}
|
||||
keep := false
|
||||
defer func() {
|
||||
if !keep {
|
||||
os.RemoveAll(directory)
|
||||
}
|
||||
}()
|
||||
path := filepath.Join(directory, "clipboard.png")
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
return tentaclewire.Item{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
command := exec.CommandContext(ctx, "xclip", "-selection", "clipboard", "-out", "-target", "image/png")
|
||||
command.WaitDelay = time.Second
|
||||
pipe, err := command.StdoutPipe()
|
||||
if err != nil {
|
||||
return tentaclewire.Item{}, err
|
||||
}
|
||||
if err = command.Start(); err != nil {
|
||||
return tentaclewire.Item{}, err
|
||||
}
|
||||
count, copyErr := io.Copy(file, io.LimitReader(pipe, 32*1024*1024+1))
|
||||
if copyErr != nil || count > 32*1024*1024 {
|
||||
command.Process.Kill()
|
||||
command.Wait()
|
||||
return tentaclewire.Item{}, errors.New("clipboard image too large")
|
||||
}
|
||||
if err = command.Wait(); err != nil {
|
||||
return tentaclewire.Item{}, err
|
||||
}
|
||||
if count == 0 {
|
||||
return tentaclewire.Item{}, errors.New("empty clipboard image")
|
||||
}
|
||||
if err = file.Close(); err != nil {
|
||||
return tentaclewire.Item{}, err
|
||||
}
|
||||
keep = true
|
||||
return tentaclewire.Item{Path: path, Temporary: true}, nil
|
||||
}
|
||||
func copyTentacleText(ctx context.Context, value string) error {
|
||||
command := exec.CommandContext(ctx, "xclip", "-selection", "clipboard", "-in")
|
||||
command.Stdin = strings.NewReader(value)
|
||||
return command.Run()
|
||||
}
|
||||
Reference in new issue
Block a user