Files
cthulhu/internal/app/event_notification.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
}