// 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/atspi" "git.stormux.org/storm/cthulhu/internal/input" ) // Inline completions are selected by the browser. Echo the inserted portion // outside that selection; it contains the user's edit, not the suggested suffix. func autocompleteInsertion(event atspi.Event) string { text := []rune(event.Payload) start, end := event.Source.SelectionStart, event.Source.SelectionEnd if end <= start || end <= event.Detail1 || start >= event.Detail1+len(text) { return event.Payload } left := max(0, start-event.Detail1) right := min(len(text), end-event.Detail1) return string(text[:left]) + string(text[right:]) } func recentInput(source any) input.Activity { if provider, ok := source.(interface{ RecentInput() input.Activity }); ok { return provider.RecentInput() } return input.Activity{} } type autocompleteEcho struct { sequence uint64 source atspi.ObjectID } // autocompleteAvailability presents the selected address when it changes. // Chromium can send several insertion and popup events for the same suggestion. type autocompleteAvailability struct { entry atspi.ObjectID lastAddress string } func supportsAutocomplete(snapshot atspi.Snapshot) bool { return snapshot.Autocomplete || snapshot.Role == "autocomplete" } func (availability *autocompleteAvailability) focus(event atspi.Event) { if !isFocusGainEvent(event) || event.Source.AutocompletePopup { return } entry := atspi.ObjectID{} if supportsAutocomplete(event.Source) { entry = event.Source.ID() } if entry != availability.entry { availability.entry = entry availability.lastAddress = "" } } func (availability *autocompleteAvailability) offered(event atspi.Event, activity input.Activity) string { if !availability.entry.Valid() || event.Type != atspi.TextInsertedEvent || !supportsAutocomplete(event.Source) || event.Source.ID() != availability.entry || (activity.Intent != input.TypingIntent && activity.Intent != input.DeleteIntent) { return "" } address := strings.TrimSpace(event.Source.Text) if event.Source.SelectionEnd <= event.Source.SelectionStart || event.Source.SelectionEnd > len([]rune(address)) || address == "" || address == availability.lastAddress { return "" } availability.lastAddress = address return address } func (echo *autocompleteEcho) insertion(event atspi.Event, activity input.Activity) string { text := autocompleteInsertion(event) if text == "" && activity.Sequence != 0 && (activity.Sequence != echo.sequence || event.Source.ID() != echo.source) && event.Detail1 == event.Source.SelectionStart { // Chromium can retain an unchanged typed prefix and emit only a replacement // of the selected suffix. Recover that character from accessible text once // for this key press; a normal insertion already covers the other case. text = event.Source.CompletionPrefixCharacter } if text != "" { echo.sequence, echo.source = activity.Sequence, event.Source.ID() } return text }