// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "strings" "git.stormux.org/storm/cthulhu/internal/ocr" "git.stormux.org/storm/cthulhu/internal/review" ) // Retain an unchanged prefix or suffix exactly. A changed middle keeps the // nearest legal offset; identical repeated text is deliberately not guessed. func remapOCROffset(oldText, newText string, offset int) int { oldRunes, newRunes := []rune(oldText), []rune(newText) offset = max(0, min(offset, len(oldRunes))) prefix := 0 for prefix < len(oldRunes) && prefix < len(newRunes) && oldRunes[prefix] == newRunes[prefix] { prefix++ } if offset < prefix { return offset } suffix := 0 for suffix < len(oldRunes)-prefix && suffix < len(newRunes)-prefix && oldRunes[len(oldRunes)-1-suffix] == newRunes[len(newRunes)-1-suffix] { suffix++ } if offset >= len(oldRunes)-suffix { return len(newRunes) - (len(oldRunes) - offset) } return min(offset, len(newRunes)) } func (session *ocrSession) finish_refresh_locked(job ocrJob, interest review.Interest, capture ocr.Capture, result ocr.Result, err error) { session.refreshing = false if err != nil { session.refreshFailed = true session.status_locked("OCR refresh failed; activate after a successful refresh") return } previous := session.cursor.Text() offset := remapOCROffset(previous, result.Text, session.cursor.Offset()) anchor := -1 if session.selectionAnchor >= 0 { anchor = remapOCROffset(previous, result.Text, session.selectionAnchor) } session.source, session.pixels = interest, capture session.cursor = ocr.NewCursor(result) position, _ := session.cursor.Set(offset) session.selectionAnchor = anchor session.refreshFailed = result.Text == "" if result.Text == "" { session.status_locked("No text recognized") } else if result.Text != previous { session.status_locked(ocrChangedAnnouncement(previous, result.Text)) } else if !job.automatic { session.status_locked("OCR text unchanged") } if session.display != nil { session.display.EnqueueFrame(session.frame_locked(position)) } session.schedule_refresh_locked() } func ocrChangedAnnouncement(previous, current string) string { oldRunes, newRunes := []rune(previous), []rune(current) index := 0 for index < len(oldRunes) && index < len(newRunes) && oldRunes[index] == newRunes[index] { index++ } if index == len(newRunes) && index < len(oldRunes) { return "OCR text removed" } start, end := index, index for start > 0 && newRunes[start-1] != '\n' { start-- } for end < len(newRunes) && newRunes[end] != '\n' && end-start < 100 { end++ } line := strings.TrimSpace(string(newRunes[start:end])) if line == "" { return "OCR text changed" } return line }