// Cthulhu screen reader. // Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "time" "git.stormux.org/storm/cthulhu/internal/atspi" "git.stormux.org/storm/cthulhu/internal/input" ) // pendingPresentation owns one coalesced event and its captured input intent. // Only the main event loop accesses it; timers never run presentation callbacks. // Replacing or clearing an event disconnects its old timer from the select loop. type pendingPresentation struct { event atspi.Event intent input.Intent timer *time.Timer ready <-chan time.Time } func (pending *pendingPresentation) schedule(event atspi.Event, intent input.Intent, delay time.Duration) { pending.clear() pending.event, pending.intent = event, intent pending.timer = time.NewTimer(delay) pending.ready = pending.timer.C } func (pending *pendingPresentation) clear() { if pending.timer != nil { pending.timer.Stop() } *pending = pendingPresentation{} } func (pending *pendingPresentation) take() (atspi.Event, input.Intent) { event, intent := pending.event, pending.intent pending.clear() return event, intent } // pendingPresentations bounds delayed presentation to one event per family. // Its lifetime is the event loop's lifetime, including adapter closure. type pendingPresentations struct { caret pendingPresentation terminalFocus pendingPresentation terminalDeletion pendingPresentation } func (pending *pendingPresentations) clear() { pending.caret.clear() pending.terminalFocus.clear() pending.terminalDeletion.clear() }