// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "strings" "unicode/utf8" "git.stormux.org/storm/cthulhu/internal/atspi" ) // textSelection tracks only the focused text object. Its cached content is // bounded by the native snapshot and is retired when focus changes. type textSelection struct { id atspi.ObjectID start, end int text string } const selectionFragmentLimit = 120 const lineSelectionFragmentLimit = 4096 func (selection *textSelection) reset(snapshot atspi.Snapshot) { selection.id = snapshot.ID() selection.start, selection.end = snapshot.SelectionStart, snapshot.SelectionEnd selection.text = snapshot.SelectionText } func (selection *textSelection) changed(snapshot atspi.Snapshot) string { return selection.changedWithLimit(snapshot, selectionFragmentLimit) } func (selection *textSelection) changedWithLimit(snapshot atspi.Snapshot, fragmentLimit int) string { start, end := snapshot.SelectionStart, snapshot.SelectionEnd if selection.id != snapshot.ID() { selection.reset(snapshot) return "" } oldStart, oldEnd, oldText := selection.start, selection.end, selection.text selection.reset(snapshot) if start == oldStart && end == oldEnd && oldText == snapshot.SelectionText { return "" } if end <= start { if oldEnd > oldStart { return "selection removed" } return "" } if oldEnd <= oldStart { return selectionFragment(snapshot.SelectionText, "selected", fragmentLimit) } var changes []string if start > oldStart { changes = append(changes, selectionFragment(selectionSlice(oldText, 0, min(oldEnd, start)-oldStart), "unselected", fragmentLimit)) } if end < oldEnd { changes = append(changes, selectionFragment(selectionSlice(oldText, max(oldStart, end)-oldStart, oldEnd-oldStart), "unselected", fragmentLimit)) } if start < oldStart { changes = append(changes, selectionFragment(selectionSlice(snapshot.SelectionText, 0, min(end, oldStart)-start), "selected", fragmentLimit)) } if end > oldEnd { changes = append(changes, selectionFragment(selectionSlice(snapshot.SelectionText, max(start, oldEnd)-start, end-start), "selected", fragmentLimit)) } if len(changes) == 0 { return "selection changed" } return strings.Join(changes, ", ") } func selectionSlice(value string, start, end int) string { runes := []rune(value) if start < 0 || end > len(runes) || start >= end { return "" } return string(runes[start:end]) } func selectionFragment(value, change string, limit int) string { if utf8.RuneCountInString(value) > limit { return change } value = strings.Join(strings.Fields(value), " ") if value == "" { return change } return value + " " + change }