Files

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
}