Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package webscripts
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const steamNotificationDelay = 500 * time.Millisecond
|
||||
|
||||
var steamRelativeTime = regexp.MustCompile(`(?i)^(?:just now|now|\d+\s+(?:second|minute|hour|day|week|month|year)s?\s+ago)$`)
|
||||
|
||||
// NotificationRoot mirrors Python's Steam root selection: notification/alert
|
||||
// ancestors take precedence over inherited live-region attributes on a leaf.
|
||||
// Candidates are ordered from the event source towards its application root.
|
||||
func NotificationRoot(candidates []NotificationCandidate) int {
|
||||
for index, candidate := range candidates {
|
||||
if candidate.Role == "notification" || candidate.Role == "alert" {
|
||||
return index
|
||||
}
|
||||
}
|
||||
for index, candidate := range candidates {
|
||||
if candidate.Live == "assertive" || candidate.Live == "polite" ||
|
||||
candidate.ContainerLive == "assertive" || candidate.ContainerLive == "polite" {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type notificationKey struct{ busName, rootPath string }
|
||||
type pendingNotification struct {
|
||||
text string
|
||||
windowPath string
|
||||
timestampOnly bool
|
||||
deadline time.Time
|
||||
}
|
||||
type completedNotification struct {
|
||||
text string
|
||||
at time.Time
|
||||
}
|
||||
type steamNotifications struct {
|
||||
pending map[notificationKey]pendingNotification
|
||||
last map[string]completedNotification
|
||||
}
|
||||
|
||||
func normalize(text string) string {
|
||||
return strings.Join(strings.Fields(strings.ReplaceAll(text, "\ufffc", "")), " ")
|
||||
}
|
||||
|
||||
func contains(text, other string) bool {
|
||||
text, other = strings.ToLower(text), strings.ToLower(other)
|
||||
if len(other) < 4 {
|
||||
return text == other
|
||||
}
|
||||
return strings.Contains(text, other)
|
||||
}
|
||||
|
||||
func appendTimestamp(text, timestamp string) string {
|
||||
if strings.ContainsAny(text[len(text)-1:], ".!?") {
|
||||
return text + " " + timestamp
|
||||
}
|
||||
return text + ". " + timestamp
|
||||
}
|
||||
|
||||
func (steam *steamNotifications) queue(message Notification, now time.Time) []string {
|
||||
text := normalize(message.Text)
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
if steam.pending == nil {
|
||||
steam.pending = make(map[notificationKey]pendingNotification)
|
||||
}
|
||||
key := notificationKey{message.BusName, message.RootPath}
|
||||
pending, exists := steam.pending[key]
|
||||
timestamp := steamRelativeTime.MatchString(text)
|
||||
var messages []string
|
||||
if exists {
|
||||
switch {
|
||||
case text == pending.text:
|
||||
return nil
|
||||
case timestamp:
|
||||
if pending.timestampOnly {
|
||||
pending.text = text
|
||||
} else {
|
||||
pending.text = appendTimestamp(pending.text, text)
|
||||
}
|
||||
case pending.timestampOnly:
|
||||
pending.text = appendTimestamp(text, pending.text)
|
||||
pending.timestampOnly = false
|
||||
case contains(text, pending.text):
|
||||
pending.text = text
|
||||
case contains(pending.text, text):
|
||||
return nil
|
||||
default:
|
||||
pending.text = normalize(pending.text + " " + text)
|
||||
}
|
||||
} else {
|
||||
// Bound outstanding roots without merging independent toasts. Finish
|
||||
// the oldest root if a provider emits an excessive burst.
|
||||
if len(steam.pending) >= 32 {
|
||||
oldest := steam.orderedKeys()[0]
|
||||
messages = steam.complete(oldest, now)
|
||||
}
|
||||
pending = pendingNotification{text: text, timestampOnly: timestamp}
|
||||
}
|
||||
// Bound accumulated fragments as well as each native capture.
|
||||
characters := []rune(pending.text)
|
||||
if len(characters) > 4096 {
|
||||
pending.text = string(characters[:4096])
|
||||
}
|
||||
pending.deadline = now.Add(steamNotificationDelay)
|
||||
pending.windowPath = message.WindowPath
|
||||
steam.pending[key] = pending
|
||||
return messages
|
||||
}
|
||||
|
||||
func (steam *steamNotifications) orderedKeys() []notificationKey {
|
||||
keys := make([]notificationKey, 0, len(steam.pending))
|
||||
for key := range steam.pending {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Slice(keys, func(first, second int) bool {
|
||||
left, right := keys[first], keys[second]
|
||||
if steam.pending[left].deadline.Equal(steam.pending[right].deadline) {
|
||||
if left.busName == right.busName {
|
||||
return left.rootPath < right.rootPath
|
||||
}
|
||||
return left.busName < right.busName
|
||||
}
|
||||
return steam.pending[left].deadline.Before(steam.pending[right].deadline)
|
||||
})
|
||||
return keys
|
||||
}
|
||||
|
||||
func (steam *steamNotifications) complete(key notificationKey, now time.Time) []string {
|
||||
pending := steam.pending[key]
|
||||
delete(steam.pending, key)
|
||||
if pending.timestampOnly {
|
||||
return nil
|
||||
}
|
||||
last := steam.last[key.busName]
|
||||
if last.text == pending.text && now.Sub(last.at) < time.Second {
|
||||
return nil
|
||||
}
|
||||
if steam.last == nil {
|
||||
steam.last = make(map[string]completedNotification)
|
||||
}
|
||||
for busName, completed := range steam.last {
|
||||
if now.Sub(completed.at) >= time.Second {
|
||||
delete(steam.last, busName)
|
||||
}
|
||||
}
|
||||
if len(steam.last) >= 64 {
|
||||
clear(steam.last)
|
||||
}
|
||||
steam.last[key.busName] = completedNotification{text: pending.text, at: now}
|
||||
return []string{"Steam: " + pending.text}
|
||||
}
|
||||
|
||||
func (steam *steamNotifications) flush(now time.Time) []string {
|
||||
var messages []string
|
||||
for _, key := range steam.orderedKeys() {
|
||||
if !steam.pending[key].deadline.After(now) {
|
||||
messages = append(messages, steam.complete(key, now)...)
|
||||
}
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
func (steam *steamNotifications) deadline() time.Time {
|
||||
keys := steam.orderedKeys()
|
||||
if len(keys) == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return steam.pending[keys[0]].deadline
|
||||
}
|
||||
Reference in new issue
Block a user