125 lines
3.1 KiB
Go
125 lines
3.1 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package preferences
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/speech"
|
|
)
|
|
|
|
const initialMessageLimit = 512 * 1024
|
|
|
|
type catalogPart struct {
|
|
Catalog speech.Catalog
|
|
CatalogPending bool
|
|
}
|
|
|
|
// EncodeInitial keeps each protocol line bounded without removing voices or
|
|
// languages. The helper assembles the catalog before constructing GTK controls.
|
|
func EncodeInitial(encoder *json.Encoder, response Response) error {
|
|
encoded, err := json.Marshal(response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(encoded)+1 <= initialMessageLimit {
|
|
return encoder.Encode(json.RawMessage(encoded))
|
|
}
|
|
catalog := response.Catalog
|
|
response.Catalog = nil
|
|
response.CatalogPending = true
|
|
encoded, err = json.Marshal(response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(encoded)+1 > initialMessageLimit {
|
|
return fmt.Errorf("preferences settings exceed %d-byte protocol limit", initialMessageLimit)
|
|
}
|
|
if err := encoder.Encode(json.RawMessage(encoded)); err != nil {
|
|
return err
|
|
}
|
|
modules := make([]string, 0, len(catalog))
|
|
for module := range catalog {
|
|
modules = append(modules, module)
|
|
}
|
|
sort.Strings(modules)
|
|
for _, module := range modules {
|
|
voices := catalog[module]
|
|
for {
|
|
count := min(len(voices), 128)
|
|
for {
|
|
encoded, err = json.Marshal(catalogPart{
|
|
Catalog: speech.Catalog{module: voices[:count]}, CatalogPending: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(encoded)+1 <= initialMessageLimit {
|
|
break
|
|
}
|
|
if count <= 1 {
|
|
return fmt.Errorf("preferences voice in module %q exceeds %d-byte protocol limit", module, initialMessageLimit)
|
|
}
|
|
count /= 2
|
|
}
|
|
if err := encoder.Encode(json.RawMessage(encoded)); err != nil {
|
|
return err
|
|
}
|
|
voices = voices[count:]
|
|
if len(voices) == 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return encoder.Encode(catalogPart{})
|
|
}
|
|
|
|
// read_responses owns catalog assembly for the lifetime of the helper pipe.
|
|
// Only complete responses reach GTK, including after application selection.
|
|
func read_responses(ctx context.Context, input io.Reader, updates chan<- Response, errorsIn chan<- error) {
|
|
scanner := bufio.NewScanner(input)
|
|
scanner.Buffer(make([]byte, 4096), 1024*1024)
|
|
var initial *Response
|
|
for scanner.Scan() {
|
|
var response Response
|
|
if err := json.Unmarshal(scanner.Bytes(), &response); err != nil {
|
|
errorsIn <- err
|
|
return
|
|
}
|
|
if initial != nil {
|
|
if initial.Catalog == nil {
|
|
initial.Catalog = make(speech.Catalog)
|
|
}
|
|
for module, voices := range response.Catalog {
|
|
initial.Catalog[module] = append(initial.Catalog[module], voices...)
|
|
}
|
|
if response.CatalogPending {
|
|
continue
|
|
}
|
|
response = *initial
|
|
response.CatalogPending = false
|
|
initial = nil
|
|
} else if response.CatalogPending {
|
|
initial = &response
|
|
continue
|
|
}
|
|
select {
|
|
case updates <- response:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
err := scanner.Err()
|
|
if err == nil && initial != nil {
|
|
err = io.ErrUnexpectedEOF
|
|
}
|
|
errorsIn <- err
|
|
}
|