145 lines
4.2 KiB
Go
145 lines
4.2 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
|
|
)
|
|
|
|
type tentacleOutputTicket struct {
|
|
ctx context.Context
|
|
session context.Context
|
|
speechGeneration uint64
|
|
speechRevision uint64
|
|
brailleSequence uint64
|
|
}
|
|
|
|
// tentacleOutput owns one explicit command's presentation lifetime. Any later
|
|
// command, foreground/mode transition or accessibility event retires it. This
|
|
// deliberately conservative revision gate exposes no accessible text/positions.
|
|
// Speech's interruption generation and braille's frame sequence are independent.
|
|
// Lock order: runtime, focus gate, output owner, then speech/braille owner.
|
|
type tentacleOutput struct {
|
|
mutex sync.Mutex
|
|
speech *speechWorker
|
|
braille *brailleWorker
|
|
gate *focusGate
|
|
live *liveSettings
|
|
current context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
func (owner *tentacleOutput) Capture(parent context.Context) tentacleOutputTicket {
|
|
if owner == nil {
|
|
return tentacleOutputTicket{}
|
|
}
|
|
owner.mutex.Lock()
|
|
defer owner.mutex.Unlock()
|
|
owner.retireLocked()
|
|
ctx, cancel := context.WithCancel(parent)
|
|
owner.current, owner.cancel = ctx, cancel
|
|
ticket := tentacleOutputTicket{ctx: ctx, session: parent}
|
|
if owner.speech != nil {
|
|
owner.speech.mutex.Lock()
|
|
ticket.speechGeneration = owner.speech.generation
|
|
ticket.speechRevision = owner.speech.presentationRevision
|
|
owner.speech.mutex.Unlock()
|
|
}
|
|
if owner.braille != nil {
|
|
owner.braille.mu.Lock()
|
|
ticket.brailleSequence = owner.braille.sequence
|
|
owner.braille.mu.Unlock()
|
|
}
|
|
return ticket
|
|
}
|
|
|
|
func (owner *tentacleOutput) Retire() {
|
|
if owner == nil {
|
|
return
|
|
}
|
|
owner.mutex.Lock()
|
|
defer owner.mutex.Unlock()
|
|
owner.retireLocked()
|
|
}
|
|
|
|
// Stop additionally revokes accepted protected speech during reader shutdown
|
|
// or terminal handoff. Ordinary focus/command retirement only retires replies.
|
|
func (owner *tentacleOutput) Stop() {
|
|
if owner == nil {
|
|
return
|
|
}
|
|
owner.mutex.Lock()
|
|
defer owner.mutex.Unlock()
|
|
owner.retireLocked()
|
|
if owner.speech != nil {
|
|
owner.speech.stop_uninterruptible()
|
|
}
|
|
}
|
|
|
|
func (owner *tentacleOutput) RetireRevoked() {
|
|
if owner == nil {
|
|
return
|
|
}
|
|
owner.mutex.Lock()
|
|
defer owner.mutex.Unlock()
|
|
if owner.current != nil && owner.current.Err() != nil {
|
|
owner.retireLocked()
|
|
}
|
|
}
|
|
|
|
func (owner *tentacleOutput) retireLocked() {
|
|
if owner.cancel == nil {
|
|
return
|
|
}
|
|
owner.cancel()
|
|
if owner.speech != nil {
|
|
owner.speech.revokePresentation(owner.current)
|
|
}
|
|
if owner.braille != nil {
|
|
owner.braille.retirePresentation(owner.current)
|
|
}
|
|
owner.current, owner.cancel = nil, nil
|
|
}
|
|
|
|
func (owner *tentacleOutput) Present(ticket tentacleOutputTicket, value tentaclewire.Presentation, command string) {
|
|
if owner == nil || ticket.ctx == nil {
|
|
return
|
|
}
|
|
if owner.gate != nil {
|
|
owner.gate.mutex.Lock()
|
|
defer owner.gate.mutex.Unlock()
|
|
}
|
|
owner.mutex.Lock()
|
|
defer owner.mutex.Unlock()
|
|
if ticket.ctx != owner.current || ticket.ctx.Err() != nil || owner.gate.Locked() || owner.gate.Suspended() {
|
|
return
|
|
}
|
|
if value.Speech != "" && owner.speech != nil {
|
|
request := speechRequest{Text: value.Speech, Role: "system", Cause: command,
|
|
context: ticket.ctx, presentationGeneration: &ticket.speechGeneration, presentationRevision: &ticket.speechRevision}
|
|
if value.Uninterruptible {
|
|
// Admission still checks the ticket; accepted speech belongs to the
|
|
// session so subsequent focus/command changes cannot cancel it.
|
|
request.Uninterruptible = true
|
|
request.context, request.cancel = context.WithTimeout(ticket.session, uninterruptibleSpeechTimeout)
|
|
}
|
|
owner.speech.Enqueue(request)
|
|
}
|
|
if value.Braille != "" && owner.braille != nil && owner.live != nil {
|
|
settings := owner.live.Load()
|
|
if settings.EnableBraille {
|
|
if owner.braille.flash_message_guarded(value.Braille, settings.Braille, false, ticket.ctx, ticket.brailleSequence) {
|
|
owner.braille.mu.Lock()
|
|
if owner.braille.flashContext == ticket.ctx {
|
|
owner.braille.logger.Debug("queued tentacle braille message", "command", command,
|
|
"frame_id", owner.braille.frame.ID, "braille_text", owner.braille.frame.Text)
|
|
}
|
|
owner.braille.mu.Unlock()
|
|
}
|
|
}
|
|
}
|
|
}
|