// 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) }