Files
cthulhu/internal/app/braille_frames.go

300 lines
7.6 KiB
Go

package app
import (
"context"
"time"
"unicode/utf8"
"git.stormux.org/storm/cthulhu/internal/braille"
"git.stormux.org/storm/cthulhu/internal/config"
)
func (worker *brailleWorker) Enqueue(text string) {
worker.EnqueueFrame(braille.Frame{Text: text, Cursor: -1})
}
func (worker *brailleWorker) EnqueueMessage(text string) bool {
if text == "" || len(text) > 10000 {
return false
}
worker.mu.Lock()
if !worker.connected {
worker.mu.Unlock()
return false
}
queued := worker.enqueueFrameLocked(braille.Frame{Text: text, Cursor: -1, WordWrap: worker.wordWrap}, true)
worker.mu.Unlock()
if queued {
worker.wakeDisplay()
}
return queued
}
func (worker *brailleWorker) EnqueueFrame(frame braille.Frame) {
worker.enqueueFrame(frame, true)
}
// SetInternalUIFrame temporarily owns presentation while normal focus frames
// continue updating the base. Closing restores the newest application frame.
func (worker *brailleWorker) SetInternalUIFrame(frame *braille.Frame) {
worker.mu.Lock()
if worker.flashTimer != nil {
worker.flashTimer.Stop()
worker.flashTimer = nil
}
worker.flashGeneration++
worker.flashContext = nil
worker.protectedFlash = false
worker.internalUIFrame = nil
next := worker.baseFrame
if frame != nil {
next = *frame
next.WordWrap = worker.wordWrap
if len(next.Text) > 10000 {
next.Text = next.Text[:10000]
for !utf8.ValidString(next.Text) {
next.Text = next.Text[:len(next.Text)-1]
}
if next.Cursor >= utf8.RuneCountInString(next.Text) {
next.Cursor = -1
}
}
}
queued := worker.enqueueFrameLocked(next, false)
if frame != nil {
copy := *frame
worker.internalUIFrame = &copy
}
worker.mu.Unlock()
if queued {
worker.wakeDisplay()
}
}
func (worker *brailleWorker) enqueueFrame(frame braille.Frame, base bool) {
// JSON escaping can expand a frame; keep it below the helper's 64 KiB
// scanner limit even when every byte needs escaping.
if len(frame.Text) > 10000 {
frame.Text = frame.Text[:10000]
for !utf8.ValidString(frame.Text) {
frame.Text = frame.Text[:len(frame.Text)-1]
}
if frame.Cursor > utf8.RuneCountInString(frame.Text) {
frame.Cursor = -1
}
if len(frame.Dots) > utf8.RuneCountInString(frame.Text) {
frame.Dots = frame.Dots[:utf8.RuneCountInString(frame.Text)]
}
}
worker.mu.Lock()
queued := worker.enqueueFrameLocked(frame, base)
worker.mu.Unlock()
if queued {
worker.wakeDisplay()
}
}
// enqueueFrameLocked must run under mu so flash, focus and timer restoration
// cannot reorder their frames.
func (worker *brailleWorker) enqueueFrameLocked(frame braille.Frame, base bool) bool {
if worker.secure {
return false
}
return worker.enqueueSecureFrameLocked(frame, base)
}
func (worker *brailleWorker) enqueueSecureFrameLocked(frame braille.Frame, base bool) bool {
if worker.internalUIFrame != nil {
if base {
worker.baseFrame = frame
}
return false
}
if worker.cancel == nil || worker.stopping {
return false
}
if base {
worker.flashContext = nil
if worker.protectedFlash {
worker.baseFrame = frame
return false
}
worker.flashGeneration++
if worker.flashTimer != nil {
worker.flashTimer.Stop()
worker.flashTimer = nil
}
worker.baseFrame = frame
}
frame.Contract = worker.contracted
frame.Table = worker.table
frame.ExpandAtCursor = frame.Cursor >= 0 && len(frame.Routes) > 0
worker.sequence++
frame.ID = worker.sequence
frame.Routes = append([]braille.TextRoute(nil), frame.Routes...)
frame.Dots = append([]byte(nil), frame.Dots...)
if worker.routeCancel != nil {
worker.routeCancel()
}
worker.frame = frame
return true
}
func (worker *brailleWorker) wakeDisplay() {
select {
case worker.wake <- struct{}{}:
default:
}
}
func (worker *brailleWorker) Flash(text string, options config.BrailleOptions) bool {
return worker.flash_message(text, options, false)
}
// Confirmation flashes survive the automatic focus change caused by Apply/OK.
// Normal flashes keep their existing focus-replacement behavior.
func (worker *brailleWorker) FlashConfirmation(text string, options config.BrailleOptions) bool {
return worker.flash_message(text, options, true)
}
func (worker *brailleWorker) flash_message(text string, options config.BrailleOptions, confirmation bool) bool {
return worker.flash_message_guarded(text, options, confirmation, nil, 0)
}
func (worker *brailleWorker) flash_message_guarded(text string, options config.BrailleOptions, confirmation bool, owner context.Context, sequence uint64) bool {
if text == "" || !options.FlashMessages {
return false
}
if len(text) > 10000 {
text = text[:10000]
for !utf8.ValidString(text) {
text = text[:len(text)-1]
}
}
worker.mu.Lock()
if worker.internalUIFrame != nil {
worker.mu.Unlock()
return false
}
if owner != nil && (owner.Err() != nil || worker.sequence != sequence) {
worker.mu.Unlock()
return false
}
if worker.cancel == nil || worker.stopping || !worker.connected || worker.secure {
worker.mu.Unlock()
return false
}
worker.flashGeneration++
worker.flashContext = owner
worker.protectedFlash = confirmation
generation := worker.flashGeneration
if worker.flashTimer != nil {
worker.flashTimer.Stop()
}
queued := worker.enqueueFrameLocked(braille.Frame{Text: text, Cursor: -1, WordWrap: options.WordWrap}, false)
if !options.FlashPersistent && options.FlashTime > 0 {
worker.flashTimer = time.AfterFunc(time.Duration(options.FlashTime)*time.Millisecond, func() {
worker.mu.Lock()
if worker.flashGeneration != generation {
worker.mu.Unlock()
return
}
worker.flashTimer = nil
worker.flashContext = nil
worker.protectedFlash = false
queued := worker.enqueueFrameLocked(worker.baseFrame, false)
worker.mu.Unlock()
if queued {
worker.wakeDisplay()
}
})
}
worker.mu.Unlock()
if queued {
worker.wakeDisplay()
}
return true
}
func (worker *brailleWorker) DismissConfirmation() {
worker.mu.Lock()
if !worker.protectedFlash {
worker.mu.Unlock()
return
}
worker.protectedFlash = false
worker.flashGeneration++
if worker.flashTimer != nil {
worker.flashTimer.Stop()
worker.flashTimer = nil
}
queued := worker.enqueueFrameLocked(worker.baseFrame, false)
worker.mu.Unlock()
if queued {
worker.wakeDisplay()
}
}
func (worker *brailleWorker) SetContraction(enabled bool, table string) {
worker.mu.Lock()
worker.contracted, worker.table = enabled, table
frame := worker.frame
running := worker.cancel != nil && !worker.stopping
queued := false
if running {
queued = worker.enqueueFrameLocked(frame, false)
}
worker.mu.Unlock()
if queued {
worker.wakeDisplay()
}
}
func (worker *brailleWorker) SetWordWrap(enabled bool) {
worker.mu.Lock()
if worker.wordWrap == enabled {
worker.mu.Unlock()
return
}
worker.wordWrap = enabled
worker.baseFrame.WordWrap = enabled
frame := worker.frame
frame.WordWrap = enabled
queued := worker.enqueueFrameLocked(frame, false)
worker.mu.Unlock()
if queued {
worker.wakeDisplay()
}
}
func (worker *brailleWorker) current() braille.Frame {
worker.mu.Lock()
defer worker.mu.Unlock()
if worker.flashContext != nil && worker.flashContext.Err() != nil {
worker.retirePresentationLocked(worker.flashContext)
}
return worker.frame
}
func (worker *brailleWorker) retirePresentationLocked(owner context.Context) bool {
if worker.flashContext != owner {
return false
}
worker.flashContext = nil
worker.flashGeneration++
if worker.flashTimer != nil {
worker.flashTimer.Stop()
worker.flashTimer = nil
}
return worker.enqueueFrameLocked(worker.baseFrame, false)
}
func (worker *brailleWorker) retirePresentation(owner context.Context) {
worker.mu.Lock()
queued := worker.retirePresentationLocked(owner)
worker.mu.Unlock()
if queued {
worker.wakeDisplay()
}
}