Reset master to testing content
This commit is contained in:
1 parent
0f2587b719
commit
42afa2a56c
1483 files changed
+145308
-938069
No files matched your search
@@ -0,0 +1,643 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user