87 lines
2.5 KiB
Go
87 lines
2.5 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type ocrTextCopier interface {
|
|
Copy(context.Context, string) error
|
|
}
|
|
|
|
// xclip becomes the X11 selection owner after its short-lived parent exits.
|
|
// It releases ownership when another application replaces the clipboard.
|
|
type xclipOCRCopier struct{}
|
|
|
|
func (xclipOCRCopier) Copy(ctx context.Context, value string) error {
|
|
if value == "" {
|
|
return errors.New("empty OCR selection")
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
|
defer cancel()
|
|
command := exec.CommandContext(ctx, "xclip", "-selection", "clipboard", "-in")
|
|
command.Stdin = strings.NewReader(value)
|
|
return command.Run()
|
|
}
|
|
|
|
type ocrCopyRequest struct {
|
|
ctx context.Context
|
|
generation uint64
|
|
text string
|
|
automatic bool
|
|
}
|
|
|
|
func (session *ocrSession) copy_selection_locked() {
|
|
if session.cursor == nil || session.selectionAnchor < 0 || session.selectionAnchor == session.cursor.Offset() {
|
|
session.status_locked("No OCR text selected")
|
|
return
|
|
}
|
|
start, end := min(session.selectionAnchor, session.cursor.Offset()), max(session.selectionAnchor, session.cursor.Offset())
|
|
value, ok := session.cursor.Slice(start, end)
|
|
if !ok || value == "" || session.copier == nil || session.copyRequests == nil {
|
|
session.status_locked("OCR copy unavailable")
|
|
return
|
|
}
|
|
request := ocrCopyRequest{ctx: session.job.ctx, generation: session.generation, text: value}
|
|
select {
|
|
case session.copyRequests <- request:
|
|
default:
|
|
// Keep the newest explicit copy request without blocking the input loop.
|
|
select {
|
|
case <-session.copyRequests:
|
|
default:
|
|
}
|
|
session.copyRequests <- request
|
|
}
|
|
}
|
|
|
|
func (session *ocrSession) copy(request ocrCopyRequest) {
|
|
if request.ctx.Err() != nil {
|
|
return
|
|
}
|
|
err := session.copier.Copy(request.ctx, request.text)
|
|
session.mu.Lock()
|
|
defer session.mu.Unlock()
|
|
if request.ctx.Err() != nil || request.generation != session.generation || !session.current_locked(session.job) {
|
|
return
|
|
}
|
|
// A successful optional copy must not cancel initial Say All or replace its
|
|
// routable braille frame. Explicit copies retain their normal confirmation.
|
|
if request.automatic && err == nil && session.job.autoReadAll {
|
|
return
|
|
}
|
|
message := "OCR text copied to clipboard"
|
|
if err != nil {
|
|
message = "OCR copy unavailable"
|
|
session.logger.Debug("OCR copy failed", "error", err)
|
|
}
|
|
session.status_locked(message)
|
|
}
|