// 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 }