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 = © } 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() } }