300 lines
7.6 KiB
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 = ©
|
|
}
|
|
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()
|
|
}
|
|
}
|