644 lines
21 KiB
Go
644 lines
21 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/config"
|
|
"git.stormux.org/storm/cthulhu/internal/interaction"
|
|
"git.stormux.org/storm/cthulhu/internal/preferences"
|
|
"git.stormux.org/storm/cthulhu/internal/tentacle"
|
|
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
|
|
)
|
|
|
|
type readerExtension struct {
|
|
session *tentacle.Session
|
|
value tentaclewire.Extensions
|
|
revision uint64
|
|
}
|
|
|
|
type readerSettingsCall struct {
|
|
ctx context.Context
|
|
session *tentacle.Session
|
|
request tentaclewire.ReaderRequest
|
|
result chan readerSettingsResult
|
|
}
|
|
type readerSettingsResult struct {
|
|
state *tentaclewire.ReaderState
|
|
err error
|
|
}
|
|
type readerTransaction struct {
|
|
extensions map[string]*readerExtension
|
|
override *tentaclewire.SpeechConfiguration
|
|
owner string
|
|
}
|
|
|
|
func copy_reader_value[T any](value T) T {
|
|
data, _ := json.Marshal(value)
|
|
var result T
|
|
_ = json.Unmarshal(data, &result)
|
|
return result
|
|
}
|
|
|
|
func (runtime *tentacleRuntime) signal_extensions() {
|
|
if runtime == nil {
|
|
return
|
|
}
|
|
select {
|
|
case runtime.extensionChanges <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
func (worker *preferencesWorker) extension_changes() <-chan struct{} {
|
|
if worker.tentacles == nil {
|
|
return nil
|
|
}
|
|
return worker.tentacles.extensionChanges
|
|
}
|
|
func (runtime *tentacleRuntime) reader_sessions() map[string]*tentacle.Session {
|
|
sessions := map[string]*tentacle.Session{}
|
|
if runtime == nil {
|
|
return sessions
|
|
}
|
|
runtime.mutex.Lock()
|
|
defer runtime.mutex.Unlock()
|
|
if runtime.disabled {
|
|
return sessions
|
|
}
|
|
for _, command := range runtime.commands {
|
|
if state, _ := command.session.Status(); command.reader && state == tentacle.Active {
|
|
sessions[command.session.Identity()] = command.session
|
|
}
|
|
}
|
|
return sessions
|
|
}
|
|
|
|
// Called only by the settings owner. Revocation wakes this mailbox even if no
|
|
// preferences window is open. No native input callback waits for restoration.
|
|
func (worker *preferencesWorker) sync_extensions(ctx context.Context) {
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
sessions := worker.tentacles.reader_sessions()
|
|
if worker.extensions == nil {
|
|
worker.extensions = map[string]*readerExtension{}
|
|
}
|
|
for id := range worker.extensions {
|
|
if sessions[id] == nil {
|
|
delete(worker.extensions, id)
|
|
}
|
|
}
|
|
for id, session := range sessions {
|
|
if worker.extensions[id] != nil {
|
|
continue
|
|
}
|
|
value := session.Extensions()
|
|
worker.load_page_store()
|
|
for index := range value.Pages {
|
|
page := &value.Pages[index]
|
|
if saved := worker.savedPages[session.TentacleID()][page.ID]; saved != nil && tentaclewire.ValidatePageValues(*page, saved) == nil {
|
|
page.Values = copy_reader_value(saved)
|
|
}
|
|
}
|
|
worker.extensionRevision++
|
|
worker.extensions[id] = &readerExtension{session: session, value: value, revision: worker.extensionRevision}
|
|
if err := validate_reader_extensions(worker.extensions); err != nil {
|
|
delete(worker.extensions, id)
|
|
session.Disable()
|
|
worker.announceSpeechSetting("A tentacle exceeded the reader settings limits and was disabled.")
|
|
}
|
|
}
|
|
if worker.speechOverride != nil && sessions[worker.speechOverrideOwner] == nil {
|
|
restoreCtx, cancel := context.WithTimeout(ctx, 6*time.Second)
|
|
var err error
|
|
if worker.settings.EnableSpeech {
|
|
err = worker.updateSpeech(restoreCtx, worker.settings)
|
|
}
|
|
cancel()
|
|
if err != nil {
|
|
worker.logger.Error("restore speech after tentacle revocation", "error", err)
|
|
worker.announceSpeechSetting("The tentacle stopped, but normal speech settings could not be restored. The working voice has been retained.")
|
|
return
|
|
}
|
|
worker.speechOverride, worker.speechOverrideOwner = nil, ""
|
|
if worker.live != nil {
|
|
worker.live.Store(worker.settings)
|
|
}
|
|
}
|
|
}
|
|
func (worker *preferencesWorker) load_page_store() {
|
|
if worker.pageStore != nil {
|
|
return
|
|
}
|
|
worker.pageStore = &tentacle.PageStore{Path: filepath.Join(filepath.Dir(worker.settings.Path), "tentacle-preferences.json")}
|
|
worker.savedPages, worker.pageLoadError = worker.pageStore.Load()
|
|
if worker.savedPages == nil {
|
|
worker.savedPages = tentacle.PageValues{}
|
|
}
|
|
if worker.pageLoadError != nil {
|
|
worker.logger.Warn("load tentacle preferences", "error", worker.pageLoadError)
|
|
}
|
|
}
|
|
|
|
func (worker *preferencesWorker) effective_settings(base config.Settings) config.Settings {
|
|
if worker.speechOverride != nil {
|
|
return worker.speechOverride.Apply(base)
|
|
}
|
|
return base
|
|
}
|
|
func (worker *preferencesWorker) extension_pages() []preferences.ExtensionPage {
|
|
if worker.editApp != "" {
|
|
return nil
|
|
}
|
|
var pages []preferences.ExtensionPage
|
|
for _, id := range sorted_extension_ids(worker.extensions) {
|
|
extension := worker.extensions[id]
|
|
for _, page := range extension.value.Pages {
|
|
pages = append(pages, preferences.ExtensionPage{Session: id, Revision: extension.revision, PreferencePage: copy_reader_value(page)})
|
|
}
|
|
}
|
|
return pages
|
|
}
|
|
func sorted_extension_ids(extensions map[string]*readerExtension) []string {
|
|
ids := make([]string, 0, len(extensions))
|
|
for id := range extensions {
|
|
ids = append(ids, id)
|
|
}
|
|
sort.Slice(ids, func(i, j int) bool {
|
|
left, right := extensions[ids[i]].session.TentacleID(), extensions[ids[j]].session.TentacleID()
|
|
if left != right {
|
|
return left < right
|
|
}
|
|
return ids[i] < ids[j]
|
|
})
|
|
return ids
|
|
}
|
|
func (worker *preferencesWorker) reader_state(extension *readerExtension) tentaclewire.ReaderState {
|
|
return copy_reader_value(tentaclewire.ReaderState{Speech: tentaclewire.SpeechFromSettings(worker.effective_settings(worker.settings)), DefaultSpeech: tentaclewire.SpeechFromSettings(worker.settings), Extensions: extension.value})
|
|
}
|
|
func (worker *preferencesWorker) request_reader(ctx context.Context, session *tentacle.Session, request tentaclewire.ReaderRequest) (*tentaclewire.ReaderState, error) {
|
|
call := readerSettingsCall{ctx: ctx, session: session, request: request, result: make(chan readerSettingsResult, 1)}
|
|
select {
|
|
case worker.extensionRequests <- call:
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
return nil, tentacle.ErrBusy
|
|
}
|
|
select {
|
|
case result := <-call.result:
|
|
return result.state, result.err
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-worker.done:
|
|
return nil, tentacle.ErrUnavailable
|
|
}
|
|
}
|
|
func (worker *preferencesWorker) handle_reader_call(ctx context.Context, call readerSettingsCall, windowOpen bool) {
|
|
callCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
stop := context.AfterFunc(call.ctx, cancel)
|
|
defer stop()
|
|
if call.session != nil {
|
|
stopSession := context.AfterFunc(call.session.OutputContext(), cancel)
|
|
defer stopSession()
|
|
}
|
|
worker.sync_extensions(ctx)
|
|
state, err := worker.apply_reader_request(callCtx, call.session, call.request, windowOpen)
|
|
call.result <- readerSettingsResult{state, err}
|
|
}
|
|
func (worker *preferencesWorker) reader_allowed(ctx context.Context, session *tentacle.Session) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if session == nil {
|
|
return tentacle.ErrUnavailable
|
|
}
|
|
if state, _ := session.Status(); state != tentacle.Active || worker.extensions[session.Identity()] == nil {
|
|
return tentacle.ErrUnavailable
|
|
}
|
|
if worker.tentacles != nil && worker.tentacles.output != nil && worker.tentacles.output.gate != nil {
|
|
if worker.tentacles.output.gate.Locked() || worker.tentacles.output.gate.Suspended() {
|
|
return errors.New("reader interaction is unavailable")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
func (worker *preferencesWorker) reader_transaction() readerTransaction {
|
|
extensions := map[string]*readerExtension{}
|
|
for id, extension := range worker.extensions {
|
|
extensions[id] = &readerExtension{session: extension.session, value: copy_reader_value(extension.value), revision: extension.revision}
|
|
}
|
|
return readerTransaction{extensions, copy_reader_value(worker.speechOverride), worker.speechOverrideOwner}
|
|
}
|
|
func (worker *preferencesWorker) stage_reader_request(ctx context.Context, transaction *readerTransaction, session *tentacle.Session, request tentaclewire.ReaderRequest) error {
|
|
if err := worker.reader_allowed(ctx, session); err != nil {
|
|
return err
|
|
}
|
|
if err := tentaclewire.ValidateReaderRequest(request); err != nil {
|
|
return err
|
|
}
|
|
id := session.Identity()
|
|
if request.Extensions != nil {
|
|
allowed := map[string]bool{}
|
|
for _, command := range session.Commands() {
|
|
if command.Reader && !command.Settings {
|
|
allowed[command.ID] = true
|
|
}
|
|
}
|
|
for _, page := range request.Extensions.Pages {
|
|
if !allowed[page.Command] {
|
|
return tentacle.ErrProtocol
|
|
}
|
|
}
|
|
for _, setting := range request.Extensions.QuickSettings {
|
|
if !allowed[setting.Command] {
|
|
return tentacle.ErrProtocol
|
|
}
|
|
}
|
|
transaction.extensions[id].value = copy_reader_value(*request.Extensions)
|
|
transaction.extensions[id].revision++
|
|
}
|
|
if request.Speech != nil || request.RestoreSpeech {
|
|
if transaction.override != nil && transaction.owner != id {
|
|
return errors.New("another tentacle owns the active speech override")
|
|
}
|
|
if request.RestoreSpeech {
|
|
transaction.override, transaction.owner = nil, ""
|
|
} else {
|
|
next := request.Speech.Apply(worker.settings)
|
|
if err := config.Validate(next); err != nil {
|
|
return err
|
|
}
|
|
transaction.override, transaction.owner = copy_reader_value(request.Speech), id
|
|
}
|
|
}
|
|
return validate_reader_extensions(transaction.extensions)
|
|
}
|
|
func (worker *preferencesWorker) commit_reader_transaction(ctx context.Context, session *tentacle.Session, transaction readerTransaction) error {
|
|
if err := worker.reader_allowed(ctx, session); err != nil {
|
|
return err
|
|
}
|
|
next := worker.settings
|
|
if transaction.override != nil {
|
|
next = transaction.override.Apply(next)
|
|
}
|
|
if !reflect.DeepEqual(transaction.override, worker.speechOverride) && next.EnableSpeech {
|
|
operationCtx, cancel := context.WithTimeout(ctx, 6*time.Second)
|
|
stop := context.AfterFunc(session.OutputContext(), cancel)
|
|
err := worker.updateSpeech(operationCtx, next)
|
|
stop()
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := worker.reader_allowed(ctx, session); err != nil {
|
|
restoreCtx, restoreCancel := context.WithTimeout(context.Background(), 6*time.Second)
|
|
defer restoreCancel()
|
|
if restoreErr := worker.updateSpeech(restoreCtx, worker.effective_settings(worker.settings)); restoreErr != nil {
|
|
worker.logger.Error("restore revoked speech transaction", "error", restoreErr)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
worker.extensions = transaction.extensions
|
|
worker.speechOverride, worker.speechOverrideOwner = transaction.override, transaction.owner
|
|
if worker.live != nil {
|
|
worker.live.Store(next)
|
|
}
|
|
return nil
|
|
}
|
|
func (worker *preferencesWorker) apply_reader_request(ctx context.Context, session *tentacle.Session, request tentaclewire.ReaderRequest, windowOpen bool) (*tentaclewire.ReaderState, error) {
|
|
if err := worker.reader_allowed(ctx, session); err != nil {
|
|
return nil, err
|
|
}
|
|
if windowOpen && (request.Speech != nil || request.RestoreSpeech || request.Extensions != nil) {
|
|
return nil, errors.New("close preferences before changing reader settings from a command")
|
|
}
|
|
transaction := worker.reader_transaction()
|
|
if err := worker.stage_reader_request(ctx, &transaction, session, request); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := worker.commit_reader_transaction(ctx, session, transaction); err != nil {
|
|
return nil, err
|
|
}
|
|
state := worker.reader_state(worker.extensions[session.Identity()])
|
|
if request.Snapshot {
|
|
catalogCtx, cancel := context.WithTimeout(ctx, 4*time.Second)
|
|
catalog, err := worker.discover(catalogCtx, worker.socket)
|
|
cancel()
|
|
if err != nil {
|
|
return &state, errors.New("speech voice catalog is unavailable")
|
|
}
|
|
for module := range catalog {
|
|
if module != "" {
|
|
state.Modules = append(state.Modules, module)
|
|
}
|
|
}
|
|
sort.Strings(state.Modules)
|
|
state.Voices = append(state.Voices, catalog[request.Module]...)
|
|
// Keep a continuation below the RPC argument limit, explicitly marking
|
|
// partial catalogs instead of silently dropping voices.
|
|
for {
|
|
data, _ := json.Marshal(tentaclewire.Submission{Reader: &state})
|
|
if len(data) <= tentacle.MaxReaderArgumentBytes {
|
|
break
|
|
}
|
|
state.CatalogTruncated = true
|
|
if len(state.Voices) > 0 {
|
|
state.Voices = state.Voices[:len(state.Voices)/2]
|
|
} else if len(state.Modules) > 0 {
|
|
state.Modules = state.Modules[:len(state.Modules)/2]
|
|
} else {
|
|
return nil, tentacle.ErrLimit
|
|
}
|
|
}
|
|
}
|
|
return &state, nil
|
|
}
|
|
|
|
func (worker *preferencesWorker) extension_effect(ctx context.Context, extension *readerExtension, command string, event tentaclewire.ExtensionEvent) (tentaclewire.ReaderRequest, error) {
|
|
arguments := any(event)
|
|
for transition := 0; transition < 16; transition++ {
|
|
if err := worker.reader_allowed(ctx, extension.session); err != nil {
|
|
return tentaclewire.ReaderRequest{}, err
|
|
}
|
|
data, err := json.Marshal(arguments)
|
|
if err != nil {
|
|
return tentaclewire.ReaderRequest{}, err
|
|
}
|
|
replies, err := extension.session.Invoke(ctx, command, data)
|
|
if err != nil {
|
|
return tentaclewire.ReaderRequest{}, err
|
|
}
|
|
result := <-replies
|
|
if result.Err != nil {
|
|
return tentaclewire.ReaderRequest{}, result.Err
|
|
}
|
|
value, err := tentaclewire.DecodeInteraction(result.Value)
|
|
if err != nil || value.Reader == nil || value.Form != nil || value.Poll != "" || value.Source != "" || value.Clipboard != nil || value.Speech != "" || value.Braille != "" {
|
|
return tentaclewire.ReaderRequest{}, errors.New("extension callback must return only a reader settings proposal")
|
|
}
|
|
if !value.Reader.Snapshot {
|
|
if value.Next != "" {
|
|
return tentaclewire.ReaderRequest{}, tentacle.ErrProtocol
|
|
}
|
|
return *value.Reader, nil
|
|
}
|
|
if !worker.tentacles.reader_continuation(tentacleCommand{session: extension.session}, value.Next) {
|
|
return tentaclewire.ReaderRequest{}, tentacle.ErrProtocol
|
|
}
|
|
state, snapshotErr := worker.apply_reader_request(ctx, extension.session, *value.Reader, true)
|
|
submission := tentaclewire.Submission{Reader: state}
|
|
if snapshotErr != nil {
|
|
submission.Error = snapshotErr.Error()
|
|
}
|
|
arguments, command = submission, value.Next
|
|
}
|
|
return tentaclewire.ReaderRequest{}, errors.New("reader callback continuation limit reached")
|
|
}
|
|
|
|
// Optional page loading happens before GTK constructs controls. Handlers may
|
|
// inspect the reader/catalog and replace declarations, but cannot apply speech.
|
|
func (worker *preferencesWorker) load_extension_pages(ctx context.Context) error {
|
|
loadCtx, cancel := context.WithTimeout(ctx, 6*time.Second)
|
|
defer cancel()
|
|
var failures error
|
|
for _, view := range worker.extension_pages() {
|
|
if !view.Load {
|
|
continue
|
|
}
|
|
extension := worker.extensions[view.Session]
|
|
if extension == nil {
|
|
continue
|
|
}
|
|
event := tentaclewire.ExtensionEvent{Event: "preferences.open", ID: view.ID, Reader: worker.reader_state(extension)}
|
|
request, err := worker.extension_effect(loadCtx, extension, view.Command, event)
|
|
if err == nil && (request.Speech != nil || request.RestoreSpeech || request.Extensions == nil) {
|
|
err = errors.New("opening a preference page may only update its declarations")
|
|
}
|
|
transaction := worker.reader_transaction()
|
|
if err == nil {
|
|
err = worker.stage_reader_request(loadCtx, &transaction, extension.session, request)
|
|
}
|
|
if err == nil {
|
|
worker.extensions = transaction.extensions
|
|
} else {
|
|
failures = errors.Join(failures, err)
|
|
}
|
|
}
|
|
worker.sync_extensions(ctx)
|
|
return failures
|
|
}
|
|
|
|
func (worker *preferencesWorker) prepare_extension_pages(ctx context.Context, drafts []preferences.PageDraft) (readerTransaction, error) {
|
|
worker.sync_extensions(ctx)
|
|
transaction := worker.reader_transaction()
|
|
if len(drafts) == 0 {
|
|
return transaction, nil
|
|
}
|
|
if worker.editApp != "" || worker.choosingApplication {
|
|
return transaction, errors.New("tentacle pages are global preferences")
|
|
}
|
|
seen := map[string]bool{}
|
|
speechProposals := 0
|
|
for _, draft := range drafts {
|
|
key := draft.Session + "/" + draft.ID
|
|
extension := worker.extensions[draft.Session]
|
|
if extension == nil || extension.revision != draft.Revision || seen[key] {
|
|
return transaction, errors.New("tentacle page changed or was removed; reopen preferences")
|
|
}
|
|
seen[key] = true
|
|
var page *tentaclewire.PreferencePage
|
|
for _, candidate := range extension.value.Pages {
|
|
if candidate.ID == draft.ID {
|
|
copy := candidate
|
|
page = ©
|
|
break
|
|
}
|
|
}
|
|
if page == nil {
|
|
return transaction, tentacle.ErrProtocol
|
|
}
|
|
if err := tentaclewire.ValidatePageValues(*page, draft.Values); err != nil {
|
|
return transaction, err
|
|
}
|
|
for _, element := range page.Elements {
|
|
if element.Disabled && !reflect.DeepEqual(page.Values[element.ID], draft.Values[element.ID]) {
|
|
return transaction, tentacle.ErrProtocol
|
|
}
|
|
}
|
|
if reflect.DeepEqual(page.Values, draft.Values) {
|
|
continue
|
|
}
|
|
event := tentaclewire.ExtensionEvent{Event: "preferences.apply", ID: page.ID, Values: draft.Values, Reader: worker.reader_state(extension)}
|
|
request, err := worker.extension_effect(ctx, extension, page.Command, event)
|
|
if err != nil {
|
|
return transaction, err
|
|
}
|
|
if request.Speech != nil || request.RestoreSpeech {
|
|
speechProposals++
|
|
}
|
|
if speechProposals > 1 {
|
|
return transaction, errors.New("multiple pages proposed conflicting speech changes")
|
|
}
|
|
if err := worker.stage_reader_request(ctx, &transaction, extension.session, request); err != nil {
|
|
return transaction, err
|
|
}
|
|
staged := transaction.extensions[draft.Session]
|
|
found := false
|
|
for index := range staged.value.Pages {
|
|
if staged.value.Pages[index].ID == page.ID {
|
|
// A callback may replace choices or normalize input. Otherwise
|
|
// the successfully submitted draft becomes its committed value.
|
|
if request.Extensions == nil {
|
|
staged.value.Pages[index].Values = copy_reader_value(draft.Values)
|
|
}
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return transaction, errors.New("an applied page cannot remove itself")
|
|
}
|
|
staged.revision++
|
|
}
|
|
return transaction, validate_reader_extensions(transaction.extensions)
|
|
}
|
|
|
|
func validate_reader_extensions(extensions map[string]*readerExtension) error {
|
|
total := 0
|
|
for _, extension := range extensions {
|
|
if err := tentaclewire.ValidateExtensions(extension.value); err != nil {
|
|
return err
|
|
}
|
|
data, err := json.Marshal(extension.value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
total += len(data)
|
|
}
|
|
if total > 256*1024 {
|
|
return tentacle.ErrLimit
|
|
}
|
|
return nil
|
|
}
|
|
func (worker *preferencesWorker) save_extension_pages() error {
|
|
if worker.pageStore == nil {
|
|
return nil
|
|
}
|
|
if worker.pageLoadError != nil {
|
|
return worker.pageLoadError
|
|
}
|
|
values := copy_reader_value(worker.savedPages)
|
|
if values == nil {
|
|
values = tentacle.PageValues{}
|
|
}
|
|
for _, extension := range worker.extensions {
|
|
pages := map[string]map[string]interaction.Value{}
|
|
for _, page := range extension.value.Pages {
|
|
pages[page.ID] = copy_reader_value(page.Values)
|
|
}
|
|
if len(pages) > 0 {
|
|
values[extension.session.TentacleID()] = pages
|
|
}
|
|
}
|
|
if reflect.DeepEqual(values, worker.savedPages) {
|
|
return nil
|
|
}
|
|
if err := worker.pageStore.Save(values); err != nil {
|
|
return err
|
|
}
|
|
worker.savedPages = values
|
|
return nil
|
|
}
|
|
|
|
type readerQuickSetting struct {
|
|
key string
|
|
extension *readerExtension
|
|
setting tentaclewire.QuickSetting
|
|
}
|
|
|
|
func (worker *preferencesWorker) quick_settings() []readerQuickSetting {
|
|
var result []readerQuickSetting
|
|
for _, id := range sorted_extension_ids(worker.extensions) {
|
|
extension := worker.extensions[id]
|
|
for _, setting := range extension.value.QuickSettings {
|
|
result = append(result, readerQuickSetting{id + "/" + setting.ID, extension, setting})
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
func (worker *preferencesWorker) handle_extension_quick(ctx context.Context, setting readerQuickSetting, adjust bool, delta int) {
|
|
selected := 0
|
|
for index, option := range setting.setting.Options {
|
|
if option.ID == setting.setting.Selected {
|
|
selected = index
|
|
break
|
|
}
|
|
}
|
|
candidate := selected
|
|
if adjust {
|
|
for index := selected + delta; index >= 0 && index < len(setting.setting.Options); index += delta {
|
|
if !setting.setting.Options[index].Disabled {
|
|
candidate = index
|
|
break
|
|
}
|
|
}
|
|
}
|
|
option := setting.setting.Options[candidate]
|
|
if candidate != selected {
|
|
event := tentaclewire.ExtensionEvent{Event: "quick-setting", ID: setting.setting.ID, Selected: option.ID, Reader: worker.reader_state(setting.extension)}
|
|
request, err := worker.extension_effect(ctx, setting.extension, setting.setting.Command, event)
|
|
transaction := worker.reader_transaction()
|
|
if err == nil {
|
|
err = worker.stage_reader_request(ctx, &transaction, setting.extension.session, request)
|
|
}
|
|
if err == nil {
|
|
found := false
|
|
extension := transaction.extensions[setting.extension.session.Identity()]
|
|
for index := range extension.value.QuickSettings {
|
|
current := &extension.value.QuickSettings[index]
|
|
if current.ID != setting.setting.ID {
|
|
continue
|
|
}
|
|
for _, choice := range current.Options {
|
|
if choice.ID == option.ID && !choice.Disabled {
|
|
current.Selected = option.ID
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
err = errors.New("selected quick setting was removed by its callback")
|
|
}
|
|
}
|
|
if err == nil {
|
|
err = worker.commit_reader_transaction(ctx, setting.extension.session, transaction)
|
|
}
|
|
if err != nil {
|
|
worker.logger.Warn("apply tentacle quick setting", "error", err)
|
|
worker.announceSpeechSetting(setting.setting.Label + " was not changed")
|
|
return
|
|
}
|
|
}
|
|
worker.announceSpeechSetting(setting.setting.Label + " " + option.Label)
|
|
}
|
|
|
|
func (runtime *tentacleRuntime) reader_continuation(command tentacleCommand, id string) bool {
|
|
for _, registered := range command.session.Commands() {
|
|
if registered.ID == id {
|
|
return registered.Reader && !registered.Settings
|
|
}
|
|
}
|
|
return false
|
|
}
|