129 lines
4.0 KiB
Go
129 lines
4.0 KiB
Go
// Cthulhu screen reader.
|
|
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/config"
|
|
"git.stormux.org/storm/cthulhu/internal/notification"
|
|
)
|
|
|
|
func presentNotifications(speech speechEnqueuer, messages []string) {
|
|
speech.Interrupt()
|
|
for _, message := range messages {
|
|
// Notifications remain audible while XTerm owns keyboard handling.
|
|
// The output gate still rejects these requests during screen lock.
|
|
speech.Enqueue(speechRequest{Text: message, Role: "system", Cause: "notifications", BypassFocusGate: true})
|
|
}
|
|
}
|
|
|
|
type notificationAnnouncer struct {
|
|
presenter *notification.Presenter
|
|
speech speechEnqueuer
|
|
braille interface {
|
|
Flash(string, config.BrailleOptions) bool
|
|
}
|
|
live *liveSettings
|
|
}
|
|
|
|
func (announcer *notificationAnnouncer) Announce(entry notification.Entry) {
|
|
if announcer.braille != nil && announcer.live != nil {
|
|
options := announcer.live.Load().Braille
|
|
message := entry.Message
|
|
if !options.FlashDetailed && entry.Summary != "" {
|
|
message = entry.Summary
|
|
}
|
|
announcer.braille.Flash(message, options)
|
|
}
|
|
announcer.presenter.Save(entry.Message, notification.SaveOptions{
|
|
Source: entry.Source,
|
|
SourceGeneration: entry.SourceGeneration,
|
|
NotificationID: entry.NotificationID,
|
|
Live: entry.Live,
|
|
Actions: entry.Actions,
|
|
AppName: entry.AppName,
|
|
Summary: entry.Summary,
|
|
Body: entry.Body,
|
|
Urgency: entry.Urgency,
|
|
DesktopEntry: entry.DesktopEntry,
|
|
})
|
|
announcer.speech.Interrupt()
|
|
message := entry.Message
|
|
if announcer.live != nil && !announcer.live.Load().Reading.MessagesAreDetailed && entry.Summary != "" {
|
|
message = entry.Summary
|
|
}
|
|
announcer.speech.Enqueue(speechRequest{
|
|
Text: message, Role: "system", Cause: "mako-notification",
|
|
})
|
|
}
|
|
|
|
func notificationEvent(event atspi.Event) (string, []string, bool, bool) {
|
|
applicationName := strings.ToLower(event.Source.ApplicationName)
|
|
if event.Type == atspi.WindowCreateEvent {
|
|
switch applicationName {
|
|
case "notification-daemon", "mate-notification-daemon", "xfce4-notifyd":
|
|
message := notification.MessageNotification
|
|
if event.Source.Text != "" {
|
|
message += " " + event.Source.Text
|
|
}
|
|
return message, []string{message}, true, true
|
|
}
|
|
}
|
|
|
|
if applicationName == "notify-osd" {
|
|
if event.Type == atspi.AccessibleNameEvent {
|
|
message := event.Source.Name + " " + event.Source.Description
|
|
if !event.Source.ValueKnown || event.Source.CurrentValue < 0 {
|
|
return message, []string{notification.MessageNotification, message}, true, true
|
|
}
|
|
message = event.Source.Name + " " + strconv.Itoa(int(event.Source.CurrentValue))
|
|
return message, []string{message, message}, true, true
|
|
}
|
|
if event.Type == atspi.ValueChangedEvent || event.Type == atspi.AccessibleValueEvent {
|
|
if !event.Source.ValueKnown || event.Source.CurrentValue < 0 {
|
|
return "", nil, false, false
|
|
}
|
|
value := strconv.Itoa(int(event.Source.CurrentValue))
|
|
return value, []string{value}, true, false
|
|
}
|
|
}
|
|
|
|
if event.Type == atspi.ShowingStateEvent &&
|
|
strings.EqualFold(event.Source.Role, "notification") && event.Detail1 != 0 {
|
|
return notificationContent(event.Source), nil, false, true
|
|
}
|
|
return "", nil, false, false
|
|
}
|
|
|
|
func notificationContent(snapshot atspi.Snapshot) string {
|
|
parts := []string{snapshot.Name}
|
|
if snapshot.Text != "" && snapshot.Text != snapshot.Name {
|
|
parts = append(parts, snapshot.Text)
|
|
}
|
|
if len(parts) == 1 && parts[0] == "" && snapshot.Description != "" {
|
|
parts = []string{snapshot.Description}
|
|
}
|
|
var content []string
|
|
for _, part := range parts {
|
|
if part != "" && !containsString(content, part) {
|
|
content = append(content, part)
|
|
}
|
|
}
|
|
return strings.Join(content, " ")
|
|
}
|
|
|
|
func containsString(values []string, candidate string) bool {
|
|
for _, value := range values {
|
|
if value == candidate {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|