Some echo tests added.
This commit is contained in:
1 parent
8a32895457
commit
eb5b2038d6
38 files changed
+1739
-31
No files matched your search
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/input"
|
||||
"git.stormux.org/storm/cthulhu/internal/messaging"
|
||||
)
|
||||
|
||||
type messageCopyRequest struct {
|
||||
ctx context.Context
|
||||
serial uint64
|
||||
text string
|
||||
}
|
||||
|
||||
type messageCopyResult struct {
|
||||
messageCopyRequest
|
||||
err error
|
||||
}
|
||||
|
||||
// messagingController belongs to the accessibility event loop. Histories survive
|
||||
// focus changes, retire with their native window/transcript, and clear on lock.
|
||||
// Its one clipboard worker has a bounded queue and never performs native queries.
|
||||
type messagingController struct {
|
||||
history messaging.History
|
||||
speech speechEnqueuer
|
||||
braille interface {
|
||||
Flash(string, config.BrailleOptions) bool
|
||||
}
|
||||
live *liveSettings
|
||||
gate *focusGate
|
||||
ctx context.Context
|
||||
cancelWorker context.CancelFunc
|
||||
cancelCopy context.CancelFunc
|
||||
serial uint64
|
||||
requests chan messageCopyRequest
|
||||
results chan messageCopyResult
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func newMessagingController(ctx context.Context, speech speechEnqueuer, gate *focusGate, live *liveSettings, write func(context.Context, string) error) *messagingController {
|
||||
ctx, cancelWorker := context.WithCancel(ctx)
|
||||
controller := &messagingController{ctx: ctx, cancelWorker: cancelWorker, speech: speech, gate: gate, live: live,
|
||||
requests: make(chan messageCopyRequest, 1), results: make(chan messageCopyResult, 1), done: make(chan struct{})}
|
||||
go func() {
|
||||
defer close(controller.done)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case request := <-controller.requests:
|
||||
if request.ctx.Err() != nil || gate.Locked() {
|
||||
continue
|
||||
}
|
||||
err := write(request.ctx, request.text)
|
||||
if errors.Is(request.ctx.Err(), context.Canceled) {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case controller.results <- messageCopyResult{request, err}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return controller
|
||||
}
|
||||
|
||||
func (controller *messagingController) handle(event atspi.Event) bool {
|
||||
if controller == nil {
|
||||
return false
|
||||
}
|
||||
if event.Type == atspi.WindowDestroyedEvent || event.Type == atspi.DefunctStateEvent && event.Detail1 != 0 {
|
||||
id := event.Source.ID()
|
||||
if event.Type == atspi.WindowDestroyedEvent && event.Window.Valid() {
|
||||
id = event.Window
|
||||
}
|
||||
controller.history.Retire(id.BusName, id.Path)
|
||||
}
|
||||
message := event.ChatMessage
|
||||
if message == nil || event.Type != atspi.TextInsertedEvent || event.Source.ApplicationProcess != "pidgin" ||
|
||||
event.Source.Defunct || event.Source.Role != "text" ||
|
||||
message.Provider != event.Source.BusName || message.Conversation != event.Source.Path ||
|
||||
!event.Source.ID().Valid() || event.Window != atspi.NewObjectID(message.Provider, message.Window) {
|
||||
return false
|
||||
}
|
||||
if !controller.gate.Locked() && controller.history.Add(*message) {
|
||||
stored, _ := controller.history.Previous(message.Provider, 0)
|
||||
controller.present(stored.Text, true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (controller *messagingController) present(text string, background bool) {
|
||||
if controller.gate.Locked() {
|
||||
return
|
||||
}
|
||||
if controller.braille != nil {
|
||||
options := config.DefaultSettings().Braille
|
||||
if controller.live != nil {
|
||||
options = controller.live.Load().Braille
|
||||
}
|
||||
controller.braille.Flash(text, options)
|
||||
}
|
||||
// The controller owns the flash; avoid a duplicate system-message flash.
|
||||
controller.speech.Enqueue(speechRequest{Text: text, Role: "notification", Cause: "chat-message", BypassFocusGate: background})
|
||||
}
|
||||
|
||||
func (controller *messagingController) review(command input.Command, current atspi.Snapshot) {
|
||||
index, copyMessage, ok := input.MessageCommandIndex(command)
|
||||
if !ok || controller.gate.Locked() || controller.gate.Suspended() || current.ApplicationProcess != "pidgin" {
|
||||
return
|
||||
}
|
||||
message, exists := controller.history.Previous(current.BusName, index)
|
||||
if !exists {
|
||||
return
|
||||
} // Legacy leaves empty history slots silent.
|
||||
if !copyMessage {
|
||||
controller.present(message.Text, false)
|
||||
return
|
||||
}
|
||||
controller.cancelCopyRequest()
|
||||
copyCtx, cancel := context.WithTimeout(controller.ctx, clipboardReadTimeout)
|
||||
controller.cancelCopy = cancel
|
||||
request := messageCopyRequest{ctx: copyCtx, serial: controller.serial, text: message.Text}
|
||||
// Only the event loop produces requests. Replace an obsolete queued copy
|
||||
// rather than dropping the newest command while the worker cancels a write.
|
||||
select {
|
||||
case <-controller.requests:
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case controller.requests <- request:
|
||||
default:
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (controller *messagingController) copied(result messageCopyResult) {
|
||||
if result.serial != controller.serial || errors.Is(result.ctx.Err(), context.Canceled) || controller.gate.Locked() || controller.gate.Suspended() {
|
||||
return
|
||||
}
|
||||
text := fmt.Sprintf("Copied %s to clipboard.", result.text)
|
||||
if result.err != nil {
|
||||
text = "Error accessing clipboard"
|
||||
}
|
||||
controller.present(text, false)
|
||||
controller.cancelCopyRequest()
|
||||
}
|
||||
|
||||
func (controller *messagingController) cancelCopyRequest() {
|
||||
controller.serial++
|
||||
if controller.cancelCopy != nil {
|
||||
controller.cancelCopy()
|
||||
controller.cancelCopy = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (controller *messagingController) reset() {
|
||||
controller.cancelCopyRequest()
|
||||
controller.history.Reset()
|
||||
}
|
||||
|
||||
func (controller *messagingController) stop() {
|
||||
controller.reset()
|
||||
controller.cancelWorker()
|
||||
<-controller.done
|
||||
}
|
||||
Reference in new issue
Block a user