Notifications could only be tried through an active quiet window with quiet_mode = toast and a daemon restart. `talk test --toast` sends a test notification directly through the same notify path, reports the tool (notify-send / Notification Center / Windows toast) and the result, and exits 1 with a hint when no tool exists or the call fails. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
111 lines
4.3 KiB
Go
111 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"code.m3ta.dev/m3tam3re/talk/internal/config"
|
|
"code.m3ta.dev/m3tam3re/talk/internal/notify"
|
|
"code.m3ta.dev/m3tam3re/talk/internal/queue"
|
|
"code.m3ta.dev/m3tam3re/talk/internal/speak"
|
|
)
|
|
|
|
// testPhrase is the fixed test sentence per language.
|
|
var testPhrase = map[string]string{
|
|
"de": "Dies ist eine Testansage von talk. Wenn du das hörst, funktioniert alles.",
|
|
"en": "This is a test announcement from talk. If you can hear this, everything works.",
|
|
}
|
|
|
|
// toastPhrase is the text of the test notification per language.
|
|
var toastPhrase = map[string]string{
|
|
"de": "Dies ist eine Testbenachrichtigung von talk. So sehen Ansagen in der Ruhezeit aus.",
|
|
"en": "This is a test notification from talk. This is what announcements look like during quiet hours.",
|
|
}
|
|
|
|
// runTest implements `talk test`: speaks the test sentence DIRECTLY in this
|
|
// process (not via the daemon), so the command sees which tier really spoke
|
|
// and how long synthesis and playback took. Mute and quiet hours
|
|
// deliberately do not apply (the user is testing right now); an active mute
|
|
// is mentioned. With --toast it sends a test desktop notification instead,
|
|
// through the same path quiet_mode = toast uses, so the notification setup
|
|
// can be checked without a quiet window and a daemon restart.
|
|
func runTest(args []string, opts *sayOptions, _ io.Reader, stdout, stderr io.Writer) int {
|
|
if len(args) > 0 {
|
|
fmt.Fprintln(stderr, "talk test: no arguments expected (language: --lang de|en)")
|
|
return exitUsage
|
|
}
|
|
if code, bad := validateSayOptions(opts, stderr); bad {
|
|
return code
|
|
}
|
|
cfg, err := config.Load(config.Options{Getenv: getenv})
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "talk: %v\n", err)
|
|
return exitFailure
|
|
}
|
|
lang := opts.lang
|
|
if lang == "" {
|
|
lang = cfg.Lang
|
|
}
|
|
if opts.toast {
|
|
return runToastTest(lang, stdout, stderr)
|
|
}
|
|
if dir, err := config.Dir(getenv); err == nil {
|
|
if (queue.MuteStore{Path: filepath.Join(dir, queue.MuteFileName)}).Muted() {
|
|
fmt.Fprintln(stderr, "talk test: mute is on; the test plays anyway, normal announcements stay silent (talk unmute)")
|
|
}
|
|
}
|
|
if cfg.Audio.Sink == config.SinkNull {
|
|
fmt.Fprintln(stderr, "talk test: audio.sink = null; no sound will be produced")
|
|
}
|
|
|
|
engine, err := speak.New(cfg, getenv, stderr)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "talk: %v\n", err)
|
|
return exitFailure
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
out, err := engine.Speak(ctx, speak.Request{Text: testPhrase[lang], Lang: lang, Provider: opts.provider, Voice: opts.voice})
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "talk test: %v\n", err)
|
|
fmt.Fprintln(stderr, "hint: `talk doctor` shows which tier is missing and why.")
|
|
return exitFailure
|
|
}
|
|
fmt.Fprintf(stdout, "provider: %s\n", out.Provider)
|
|
fmt.Fprintf(stdout, "language: %s\n", lang)
|
|
switch {
|
|
case out.Discarded:
|
|
fmt.Fprintln(stdout, "playback: discarded (audio.sink = null)")
|
|
case out.Direct:
|
|
fmt.Fprintf(stdout, "duration: %s (direct speech via the OS)\n", out.Synthesis.Round(time.Millisecond))
|
|
default:
|
|
fmt.Fprintf(stdout, "synthesis: %s (%d bytes)\n", out.Synthesis.Round(time.Millisecond), out.Bytes)
|
|
fmt.Fprintf(stdout, "playback: %s\n", out.Playback.Round(time.Millisecond))
|
|
}
|
|
return exitOK
|
|
}
|
|
|
|
// runToastTest sends the test notification and reports the tool and the
|
|
// result. Exit 1 when no tool exists or the call fails.
|
|
func runToastTest(lang string, stdout, stderr io.Writer) int {
|
|
fmt.Fprintf(stdout, "toast: %s\n", notify.Describe())
|
|
if !notify.Available() {
|
|
fmt.Fprintln(stderr, "talk test: no notification tool found; install libnotify (notify-send) on Linux, macOS and Windows have it built in")
|
|
return exitFailure
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
start := time.Now()
|
|
if err := notify.Show(ctx, toastPhrase[lang]); err != nil {
|
|
fmt.Fprintf(stderr, "talk test: %v\n", err)
|
|
fmt.Fprintln(stderr, "hint: on Linux a notification daemon (mako, dunst, GNOME Shell, KDE) must be running for notify-send to show anything.")
|
|
return exitFailure
|
|
}
|
|
fmt.Fprintf(stdout, "sent: %s\n", time.Since(start).Round(time.Millisecond))
|
|
fmt.Fprintln(stdout, "If no notification appeared, your notification daemon did not show it (talk only hands it over).")
|
|
return exitOK
|
|
}
|