Files

95 lines
3.1 KiB
Go

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