2.home-manager/modules/services/voxtype.nix
Cody Shearer ed8263e7d7 voxtype: fix output mode paste
Voxtype requires the command `cat` from
`coreutils` when using the output mode `paste`,
but fails with the current configuration because
it cannot find `cat`.

```
WARN paste (clipboard + keystroke) failed: Text
injection failed: wl-copy exited with error,
trying next
```

This commit adds `coreutils` to the runtime path
to resolve the path to `cat`.
2026-06-14 17:58:52 -05:00

203 lines
5.2 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
getExe
makeBinPath
mapAttrsToList
mkEnableOption
mkIf
mkOption
mkPackageOption
optional
optionals
optionalAttrs
recursiveUpdate
escapeShellArg
escapeShellArgs
concatMapStringsSep
types
;
cfg = config.services.voxtype;
toml = pkgs.formats.toml { };
# Voxtype requires these whenever a config file exists.
settings = recursiveUpdate {
hotkey = { };
audio = {
device = "default";
sample_rate = 16000;
max_duration_secs = 60;
};
output = {
mode = "type";
fallback_to_clipboard = true;
};
} cfg.settings;
in
{
meta.maintainers = [ lib.maintainers.marijanp ];
options.services.voxtype = {
enable = mkEnableOption "Voxtype speech-to-text daemon";
package = mkPackageOption pkgs "voxtype" {
example = "pkgs.voxtype-vulkan";
};
settings = mkOption {
inherit (toml) type;
default = { };
example = {
output = {
mode = "type";
fallback_to_clipboard = true;
};
whisper = {
model = "base.en";
language = "en";
};
};
description = ''
Voxtype configuration written to `$XDG_CONFIG_HOME/voxtype/config.toml`.
'';
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--verbose" ];
description = "Extra command-line arguments passed to `voxtype daemon`.";
};
environment = mkOption {
type = types.attrsOf types.str;
default = { };
description = "Environment variables for the Voxtype user service.";
};
x11.display = mkOption {
type = types.nullOr types.str;
default = null;
example = ":0";
description = ''
X11 display name to expose to the Voxtype user service.
'';
};
wayland.display = mkOption {
type = types.nullOr types.str;
default = null;
example = "wayland-1";
description = ''
Wayland display socket name to expose to the Voxtype user service.
'';
};
loadModels = mkOption {
type = types.listOf types.str;
apply = builtins.filter (model: model != "");
default = [ ];
example = [ "base.en" ];
description = ''
Downloads the listed models with `voxtype setup --download` before starting
the daemon.
'';
};
};
config = mkIf cfg.enable {
home.packages = [
cfg.package
]
++ optionals (cfg.x11.display != null) [ pkgs.xclip ]
++ optionals (cfg.wayland.display != null) [
pkgs.wl-clipboard
pkgs.wtype
];
xdg.configFile."voxtype/config.toml" = mkIf (cfg.settings != { }) {
source = toml.generate "voxtype-config.toml" settings;
};
systemd.user.services.voxtype = {
Unit = {
Description = "Voxtype speech-to-text daemon";
PartOf = [ "default.target" ];
X-Restart-Triggers = mkIf (cfg.settings != { }) [
"${config.xdg.configFile."voxtype/config.toml".source}"
];
}
// optionalAttrs (cfg.loadModels != [ ]) {
Wants = [ "voxtype-model-loader.service" ];
After = [ "voxtype-model-loader.service" ];
};
Service =
let
runtimePath = makeBinPath (
[
pkgs.coreutils
pkgs.which
]
++ optionals (cfg.x11.display != null) [ pkgs.xclip ]
++ optionals (cfg.wayland.display != null) [
pkgs.wl-clipboard
pkgs.wtype
]
);
in
{
Type = "exec";
ExecStart = "${getExe cfg.package} daemon ${escapeShellArgs cfg.extraArgs}";
Restart = "on-failure";
RestartSec = "5s";
Environment = [
"PATH=${runtimePath}"
"XDG_RUNTIME_DIR=%t"
]
++ optional (cfg.x11.display != null) "DISPLAY=${cfg.x11.display}"
++ optional (cfg.wayland.display != null) "WAYLAND_DISPLAY=${cfg.wayland.display}"
++ mapAttrsToList (name: value: "${name}=${value}") cfg.environment;
};
Install.WantedBy = [ "default.target" ];
};
systemd.user.services.voxtype-model-loader = mkIf (cfg.loadModels != [ ]) {
Unit = {
Description = "Download Voxtype models";
Before = [ "voxtype.service" ];
Wants = [ "network-online.target" ];
After = [ "network-online.target" ];
};
Service =
let
modelLoaderScript = pkgs.writeShellScript "voxtype-model-loader" ''
set -euo pipefail
tmp="$(${pkgs.coreutils}/bin/mktemp -d /tmp/voxtype-model-loader.XXXXXX)"
trap '${pkgs.coreutils}/bin/rm -rf "$tmp"' EXIT
${concatMapStringsSep "\n" (
model:
"XDG_CONFIG_HOME=\"$tmp\" ${getExe cfg.package} setup --download --model ${escapeShellArg model} --no-post-install"
) cfg.loadModels}
'';
in
{
Type = "oneshot";
ExecStart = modelLoaderScript;
Restart = "on-failure";
RestartSec = "30s";
};
Install.WantedBy = [ "default.target" ];
};
};
}