Files
m3ta-chironandClaude Fable 5.1 83a15af9c5 feat(notify): desktop notification per announcement with a Cancel button
Every playing announcement now shows a desktop notification with its text
and, on Linux, a Cancel button (Abbrechen for German) that stops exactly
that announcement; the notification is closed when the announcement ends.
This brings back what the old talk.sh had with notify-send --action, as a
default: a long announcement can be cut short without a terminal.

Implementation: the freedesktop Notifications service on the session bus
via godbus (pure Go): one connection per process, Notify with the action
list, ActionInvoked dispatched to the announcement, CloseNotification at
the end, expire_timeout 0 so the button stays for the whole announcement.
Without a session bus the notify-send fallback shows a plain notification;
macOS and Windows show one without a button for now. queue.CancelItem(id)
cancels only the playing item. Config [notify] on_speak (default true, env
TALK_NOTIFY). `talk test --toast` now shows the real thing and waits for a
click; `talk doctor` names the service and whether it supports the button.

Config hygiene found on the way: a key written below a [table] header
belongs to that table in TOML (lang under [voice] was silently ignored).
config.Load now collects unknown keys (undecoded ones and non-language
keys under [voice.<provider>]) and `talk doctor` warns with the hint.

Tests: process test on a private dbus-daemon with a fake
org.freedesktop.Notifications service (Notify payload, cancel click aborts
the synthesis, CloseNotification), queue.CancelItem, config keys. All test
binaries point DBUS_SESSION_BUS_ADDRESS at a dead socket so no test ever
pops a notification on the developer's desktop. flake vendorHash updated
for the new dependency.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 12:10:49 +02:00

71 lines
2.8 KiB
Nix

{
# talk: voice notifications for scripts and AI agents.
#
# Usage in a NixOS / Home Manager configuration:
# inputs.talk.url = "git+https://code.m3ta.dev/m3tam3re/talk";
# environment.systemPackages = [ inputs.talk.packages.${system}.talk ];
# or as an overlay: nixpkgs.overlays = [ inputs.talk.overlays.default ];
#
# In the long run this flake replaces m3ta-nixpkgs talk/crunch (bash + mpv).
description = "talk: voice notifications for scripts and AI agents (one binary, daemon with queue)";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }:
let
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forAll = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
version = if self ? shortRev then "0.0.0-${self.shortRev}" else "0.0.0-dirty";
mkTalk = pkgs: pkgs.buildGoModule {
pname = "talk";
inherit version;
src = self;
subPackages = [ "cmd/talk" ];
# Update after changes to go.mod/go.sum: `nix build` prints the
# expected hash.
vendorHash = "sha256-Cne8FiMlpsrFfntMJVtV4D3CzEWQ4Jnd+tIEHF9ecEc=";
env.CGO_ENABLED = 0;
ldflags = [ "-s" "-w" "-X" "main.version=${version}" "-X" "main.commit=${self.shortRev or "dirty"}" ];
# The process-boundary tests start daemons and bind ports; not in the sandbox.
doCheck = false;
# Audio: oto talks to PulseAudio/PipeWire directly (pure Go) and falls
# back to ALSA, which it loads at runtime via dlopen; on NixOS that is
# only found if we put it on the path. OS voice: espeak-ng.
nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.makeWrapper ];
postInstall = pkgs.lib.optionalString pkgs.stdenv.isLinux ''
wrapProgram $out/bin/talk \
--prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath [ pkgs.alsa-lib ]} \
--prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.espeak-ng pkgs.libnotify ]}
'';
meta = with pkgs.lib; {
description = "Voice notifications for scripts and AI agents";
homepage = "https://code.m3ta.dev/m3tam3re/talk";
license = licenses.mit;
mainProgram = "talk";
platforms = platforms.unix;
};
};
in
{
packages = forAll (pkgs: rec {
talk = mkTalk pkgs;
default = talk;
});
apps = forAll (pkgs: rec {
talk = { type = "app"; program = "${mkTalk pkgs}/bin/talk"; };
default = talk;
});
overlays.default = final: prev: { talk = mkTalk final; };
devShells = forAll (pkgs: {
default = pkgs.mkShell {
packages = [ pkgs.go pkgs.gopls pkgs.gotools pkgs.espeak-ng pkgs.libnotify ];
};
});
formatter = forAll (pkgs: pkgs.nixfmt-rfc-style);
};
}