Files

142 lines
4.2 KiB
Go

// Fenrir console screen reader.
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package speech
import (
"context"
"fmt"
)
// Driver binds a stable file/CLI ID to its implementation. Availability must
// be a cheap dependency/hardware check, never connect, speak or start a service.
// Connection failures are reported by Open and never select another driver.
type Driver struct {
ID, Label string
Serial bool
Hardware bool
Check func() error
Open func(Options) (Transport, error)
Modules func(Options) ([]string, error)
Catalog func(Options) (Catalog, error)
Voices func(Options) ([]Voice, error)
}
func (d Driver) Availability() error {
if d.Check != nil {
return d.Check()
}
return nil
}
var drivers = append([]Driver{{
ID: "speech-dispatcher", Label: "Speech Dispatcher",
Open: func(options Options) (Transport, error) {
return Open(options)
},
Modules: discover_modules,
Catalog: Discover,
Voices: discover_voices,
}}, hardware_drivers()...)
func hardware_drivers() []Driver {
var result []Driver
for _, entry := range []struct{ id, label string }{
{"dectalk", "DECtalk"}, {"dectalk-usb", "DECtalk USB"},
{"litetalk", "LiteTalk"}, {"doubletalk", "DoubleTalk LT"}, {"tripletalk", "TripleTalk"},
} {
driver := Driver{ID: entry.id, Label: entry.label,
Serial: entry.id != "dectalk-usb",
Hardware: true,
Open: func(options Options) (Transport, error) { return open_hardware(entry.id, options) },
}
if entry.id == "dectalk-usb" {
driver.Check = usb_available
}
result = append(result, driver)
}
return result
}
// Drivers returns a copy; registering a completed implementation here supplies
// settings validation, UI choices, discovery and startup from the same entry.
func Drivers() []Driver { return append([]Driver(nil), drivers...) }
func Find_driver(id string) (Driver, error) {
for _, driver := range drivers {
if driver.ID == id {
return driver, nil
}
}
return Driver{}, fmt.Errorf("unknown speech driver %q", id)
}
func Open_driver(id string, options Options) (Transport, error) {
return Open_driver_context(context.Background(), id, options)
}
// Open_driver_context bounds connection setup without binding the live client to ctx.
func Open_driver_context(ctx context.Context, id string, options Options) (Transport, error) {
options.context = ctx
driver, err := Find_driver(id)
if err != nil {
return nil, err
}
if err := Validate_driver_options(id, options); err != nil {
return nil, err
}
if err = driver.Availability(); err != nil {
return nil, fmt.Errorf("speech driver %s unavailable: %w", id, err)
}
client, err := driver.Open(options)
if err != nil {
return nil, err
}
return client, nil
}
// discover_modules uses a separate, bounded connection. It never changes the
// active speaking client or cancels speech. Only Prepare permits daemon startup.
func discover_modules(options Options) ([]string, error) {
wire, err := connect_options(options)
if err != nil {
return nil, err
}
defer wire.close()
modules, err := wire.command("LIST OUTPUT_MODULES")
if err != nil {
return nil, err
}
seen := map[string]bool{}
for _, module := range modules {
if !valid_module(module) || module == "" || seen[module] {
return nil, fmt.Errorf("invalid Speech Dispatcher module list")
}
seen[module] = true
}
return modules, nil
}
func valid_module(module string) bool {
for _, char := range module {
if !(char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' || char == '.' || char == '_' || char == '+' || char == '-') {
return false
}
}
return true
}