// 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/config" "git.stormux.org/storm/cthulhu/internal/notification" "git.stormux.org/storm/cthulhu/internal/webscripts" ) // webScriptController is owned by processEventsWithInput. Delayed presentation // runs in that loop, preserving event order without timer callback goroutines. type webScriptController struct { registry webscripts.Registry timer *time.Timer due <-chan time.Time speech speechEnqueuer notifications *notification.Presenter braille interface { Flash(string, config.BrailleOptions) bool } live *liveSettings gate *focusGate } func (controller *webScriptController) handle(event atspi.Event) bool { retiring := event.Type == atspi.WindowDestroyedEvent || event.Type == atspi.DefunctStateEvent && event.Detail1 != 0 if retiring { id := event.Source.ID() if event.Type == atspi.WindowDestroyedEvent && event.Window.Valid() { id = event.Window } controller.registry.Retire(id.BusName, id.Path) } messages, handled := controller.registry.Receive(event.Source.ApplicationProcess, event.Type, event.Detail1, event.WebNotification, time.Now()) controller.present(messages) if handled || retiring { controller.schedule() } return handled } func (controller *webScriptController) schedule() { deadline := controller.registry.Deadline() controller.due = nil if controller.timer != nil { controller.timer.Stop() } if deadline.IsZero() { return } if controller.timer == nil { controller.timer = time.NewTimer(time.Until(deadline)) } else { controller.timer.Reset(time.Until(deadline)) } controller.due = controller.timer.C } func (controller *webScriptController) flush() { if controller.gate.Locked() { controller.reset() return } controller.present(controller.registry.Flush(time.Now())) controller.schedule() } func (controller *webScriptController) present(messages []string) { if controller.gate.Locked() { return } for _, message := range messages { controller.notifications.Save(message, notification.SaveOptions{AppName: "Steam", Source: "steam"}) if controller.braille != nil { options := config.DefaultSettings().Braille if controller.live != nil { options = controller.live.Load().Braille } controller.braille.Flash(message, options) } controller.speech.Interrupt() controller.speech.Enqueue(speechRequest{ Text: message, Role: "notification", Cause: "steam-notification", BypassFocusGate: true, }) } } func (controller *webScriptController) reset() { controller.registry.Reset() controller.schedule() }