// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package speech import ( "fmt" "net" "os" "os/user" "path/filepath" "strconv" "strings" ) type serverAddress struct { network string address string } // default_socket_path follows Python speechd, retaining Fenrir's sudo-user // runtime directory selection for console sessions. func default_socket_path() string { if uid := os.Getenv("SUDO_UID"); uid != "" { if parsed, err := strconv.ParseUint(uid, 10, 32); err == nil { runtimeDirectory := os.Getenv("XDG_RUNTIME_DIR") if !owned_directory(runtimeDirectory, uint32(parsed)) { runtimeDirectory = filepath.Join("/run/user", uid) } if !owned_directory(runtimeDirectory, uint32(parsed)) { if account, err := user.LookupId(uid); err == nil { runtimeDirectory = filepath.Join(account.HomeDir, ".cache") } } return filepath.Join(runtimeDirectory, "speech-dispatcher/speechd.sock") } } runtimeDirectory := os.Getenv("XDG_RUNTIME_DIR") if runtimeDirectory == "" { runtimeDirectory = os.Getenv("XDG_CACHE_HOME") } if runtimeDirectory == "" { cacheDirectory, err := os.UserCacheDir() if err == nil { runtimeDirectory = cacheDirectory } } return filepath.Join(runtimeDirectory, "speech-dispatcher", "speechd.sock") } // resolve_server_address gives an internal socket override priority over speechd's // standard address variables. SPEECHD_SOCKET is the older Unix-only setting. func resolve_server_address(socket string) (serverAddress, error) { if socket != "" { return serverAddress{network: "unix", address: socket}, nil } if address := os.Getenv("SPEECHD_ADDRESS"); address != "" { return parse_server_address(address) } if socket := os.Getenv("SPEECHD_SOCKET"); socket != "" { return serverAddress{network: "unix", address: socket}, nil } return serverAddress{network: "unix", address: default_socket_path()}, nil } func parse_server_address(address string) (serverAddress, error) { method, value, _ := strings.Cut(address, ":") switch method { case "unix_socket": if value == "" { value = default_socket_path() } if !filepath.IsAbs(value) { return serverAddress{}, fmt.Errorf("Speech Dispatcher Unix socket must be an absolute path: %q", value) } return serverAddress{network: "unix", address: value}, nil case "inet_socket": host, port := "127.0.0.1", 6560 if value != "" { parts := strings.Split(value, ":") if len(parts) > 2 { return serverAddress{}, fmt.Errorf("invalid Speech Dispatcher address %q", address) } if parts[0] != "" { host = parts[0] } if len(parts) == 2 { parsed, err := strconv.Atoi(parts[1]) if err != nil || parsed < 1 || parsed > 65535 { return serverAddress{}, fmt.Errorf("invalid Speech Dispatcher port in %q", address) } port = parsed } } return serverAddress{network: "tcp4", address: net.JoinHostPort(host, strconv.Itoa(port))}, nil default: return serverAddress{}, fmt.Errorf("unsupported Speech Dispatcher address %q", address) } } // Explicit settings select exactly one server; only defaults use fallback. func automatic_server_address(socket string) bool { return socket == "" && os.Getenv("SPEECHD_ADDRESS") == "" && os.Getenv("SPEECHD_SOCKET") == "" }