diff --git a/flake.lock b/flake.lock index a750861..380c825 100644 --- a/flake.lock +++ b/flake.lock @@ -91,6 +91,32 @@ } }, "bun2nix": { + "inputs": { + "flake-parts": "flake-parts_2", + "import-tree": "import-tree", + "nixpkgs": [ + "handy", + "nixpkgs" + ], + "systems": "systems_4", + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1770895533, + "narHash": "sha256-v3QaK9ugy9bN9RXDnjw0i2OifKmz2NnKM82agtqm/UY=", + "owner": "nix-community", + "repo": "bun2nix", + "rev": "c843f477b15f51151f8c6bcc886954699440a6e1", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "2.0.8", + "repo": "bun2nix", + "type": "github" + } + }, + "bun2nix_2": { "inputs": { "flake-parts": [ "llm-agents", @@ -292,6 +318,24 @@ } }, "flake-parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib_2" + }, + "locked": { + "lastModified": 1769996383, + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_3": { "inputs": { "nixpkgs-lib": [ "llm-agents", @@ -312,7 +356,7 @@ "type": "github" } }, - "flake-parts_3": { + "flake-parts_4": { "inputs": { "nixpkgs-lib": [ "nixvim", @@ -333,7 +377,7 @@ "type": "github" } }, - "flake-parts_4": { + "flake-parts_5": { "inputs": { "nixpkgs-lib": [ "stylix", @@ -470,6 +514,27 @@ "type": "gitlab" } }, + "handy": { + "inputs": { + "bun2nix": "bun2nix", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1785763469, + "narHash": "sha256-YHfOchlPBKbBbeoaT1amrjKBHJTYMXmcR1Gm31E7TLE=", + "owner": "cjpais", + "repo": "Handy", + "rev": "b1b2d9f9072e55902a46ca6cc655e5893ed7a91d", + "type": "github" + }, + "original": { + "owner": "cjpais", + "repo": "Handy", + "type": "github" + } + }, "home-manager": { "inputs": { "nixpkgs": [ @@ -533,6 +598,21 @@ "type": "github" } }, + "import-tree": { + "locked": { + "lastModified": 1763762820, + "narHash": "sha256-ZvYKbFib3AEwiNMLsejb/CWs/OL/srFQ8AogkebEPF0=", + "owner": "vic", + "repo": "import-tree", + "rev": "3c23749d8013ec6daa1d7255057590e9ca726646", + "type": "github" + }, + "original": { + "owner": "vic", + "repo": "import-tree", + "type": "github" + } + }, "lanzaboote": { "inputs": { "crane": "crane", @@ -558,11 +638,11 @@ }, "llm-agents": { "inputs": { - "bun2nix": "bun2nix", - "flake-parts": "flake-parts_2", + "bun2nix": "bun2nix_2", + "flake-parts": "flake-parts_3", "nixpkgs": "nixpkgs_4", - "systems": "systems_4", - "treefmt-nix": "treefmt-nix" + "systems": "systems_5", + "treefmt-nix": "treefmt-nix_2" }, "locked": { "lastModified": 1785753922, @@ -765,6 +845,21 @@ "type": "github" } }, + "nixpkgs-lib_2": { + "locked": { + "lastModified": 1769909678, + "narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "72716169fe93074c333e8d0173151350670b824c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, "nixpkgs-libxml2": { "locked": { "lastModified": 1751274312, @@ -982,11 +1077,11 @@ }, "nixvim": { "inputs": { - "flake-parts": "flake-parts_3", + "flake-parts": "flake-parts_4", "nixpkgs": [ "nixpkgs" ], - "systems": "systems_5" + "systems": "systems_6" }, "locked": { "lastModified": 1782919967, @@ -1077,6 +1172,7 @@ "flake-parts": "flake-parts", "ghostty": "ghostty", "git-hooks-nix": "git-hooks-nix", + "handy": "handy", "home-manager": "home-manager_2", "lanzaboote": "lanzaboote", "llm-agents": "llm-agents", @@ -1093,8 +1189,8 @@ "services-flake": "services-flake", "sops-nix": "sops-nix", "stylix": "stylix", - "systems": "systems_7", - "treefmt-nix": "treefmt-nix_2", + "systems": "systems_8", + "treefmt-nix": "treefmt-nix_3", "vicinae": "vicinae", "vicinae-extensions": "vicinae-extensions" } @@ -1181,13 +1277,13 @@ "base16-helix": "base16-helix", "base16-vim": "base16-vim", "firefox-gnome-theme": "firefox-gnome-theme", - "flake-parts": "flake-parts_4", + "flake-parts": "flake-parts_5", "gnome-shell": "gnome-shell", "nixpkgs": [ "nixpkgs" ], "nur": "nur", - "systems": "systems_6", + "systems": "systems_7", "tinted-kitty": "tinted-kitty", "tinted-schemes": "tinted-schemes", "tinted-tmux": "tinted-tmux", @@ -1222,6 +1318,21 @@ "type": "github" } }, + "systems_10": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "systems_2": { "locked": { "lastModified": 1681028828, @@ -1284,6 +1395,21 @@ } }, "systems_6": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_7": { "locked": { "lastModified": 1774449309, "narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=", @@ -1299,21 +1425,6 @@ "type": "github" } }, - "systems_7": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, "systems_8": { "locked": { "lastModified": 1681028828, @@ -1409,6 +1520,28 @@ } }, "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "handy", + "bun2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1770228511, + "narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "337a4fe074be1042a35086f15481d763b8ddc0e7", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + }, + "treefmt-nix_2": { "inputs": { "nixpkgs": [ "llm-agents", @@ -1429,7 +1562,7 @@ "type": "github" } }, - "treefmt-nix_2": { + "treefmt-nix_3": { "inputs": { "nixpkgs": "nixpkgs_10" }, @@ -1451,7 +1584,7 @@ "inputs": { "nixpkgs": "nixpkgs_11", "soulver-cpp": "soulver-cpp", - "systems": "systems_8" + "systems": "systems_9" }, "locked": { "lastModified": 1785733880, @@ -1473,7 +1606,7 @@ "nixpkgs": [ "nixpkgs" ], - "systems": "systems_9", + "systems": "systems_10", "vicinae": "vicinae_2" }, "locked": { diff --git a/flake.nix b/flake.nix index 83759f3..57160dc 100644 --- a/flake.nix +++ b/flake.nix @@ -38,6 +38,12 @@ url = "github:ghostty-org/ghostty"; }; + # Speech to text + handy = { + url = "github:cjpais/Handy"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + # Shell / Launcher vicinae.url = "github:vicinaehq/vicinae"; diff --git a/modules/applications/handy/darwin.nix b/modules/applications/handy/darwin.nix new file mode 100644 index 0000000..84ac6a8 --- /dev/null +++ b/modules/applications/handy/darwin.nix @@ -0,0 +1,8 @@ +{ + homebrew.casks = [ "handy" ]; + + launchd.agents.handy = { + command = "/usr/bin/open -gja Handy --args --start-hidden"; + serviceConfig.RunAtLoad = true; + }; +} diff --git a/modules/applications/handy/home/nixos.nix b/modules/applications/handy/home/nixos.nix new file mode 100644 index 0000000..f8d8ef4 --- /dev/null +++ b/modules/applications/handy/home/nixos.nix @@ -0,0 +1,74 @@ +{ + config, + inputs, + lib, + pkgs, + ... +}: +let + handy = inputs.handy.packages.${pkgs.stdenv.hostPlatform.system}.handy; + settingsFile = "${config.xdg.configHome}/com.pais.handy/settings_store.json"; +in +{ + home.packages = [ + handy + pkgs.wtype + ]; + + # Handy has no disabled-shortcut setting. Empty bindings are rejected before + # registration, leaving niri and labwc as the sole owners of Ctrl+Space. + home.activation.disableHandyGlobalShortcuts = { + after = [ "writeBoundary" ]; + before = [ ]; + data = '' + settingsFile=${lib.escapeShellArg settingsFile} + mkdir -p "$(dirname "$settingsFile")" + + if [ -f "$settingsFile" ]; then + ${lib.getExe pkgs.jq} \ + '.settings.bindings.transcribe.current_binding = "" + | .settings.bindings.transcribe_with_post_process.current_binding = ""' \ + "$settingsFile" > "$settingsFile.tmp" + else + ${lib.getExe pkgs.jq} -n ' + { + settings: { + bindings: { + transcribe: { + id: "transcribe", + name: "Transcribe", + description: "Converts your speech into text.", + default_binding: "ctrl+space", + current_binding: "" + }, + transcribe_with_post_process: { + id: "transcribe_with_post_process", + name: "Transcribe with Post-Processing", + description: "Converts your speech into text and applies AI post-processing.", + default_binding: "ctrl+shift+space", + current_binding: "" + } + } + } + } + ' > "$settingsFile.tmp" + fi + + mv "$settingsFile.tmp" "$settingsFile" + ''; + }; + + systemd.user.services.handy = { + Unit = { + Description = "Handy speech-to-text"; + After = [ "graphical-session.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + Service = { + ExecStart = "${lib.getExe handy} --start-hidden"; + Restart = "on-failure"; + RestartSec = 5; + }; + Install.WantedBy = [ "graphical-session.target" ]; + }; +} diff --git a/modules/applications/handy/meta.nix b/modules/applications/handy/meta.nix new file mode 100644 index 0000000..4e78283 --- /dev/null +++ b/modules/applications/handy/meta.nix @@ -0,0 +1,3 @@ +{ + description = "Handy offline speech-to-text"; +} diff --git a/modules/applications/labwc/home.nix b/modules/applications/labwc/home.nix index 59d3ee8..95349a0 100644 --- a/modules/applications/labwc/home.nix +++ b/modules/applications/labwc/home.nix @@ -127,6 +127,7 @@ in (execute "W-space" "ghostty +toggle-quick-terminal") (execute "W-p" "wdisplays") (execute "W-z" "wl-find-cursor -c 0xCCFF453A -s 160 -d 1200") + (execute "C-space" "handy --toggle-transcription") # Function keys (sync with niri modules/applications/niri/home.nix) (execute "XF86AudioRaiseVolume" "noctalia msg volume-up") diff --git a/modules/applications/niri/home/nixos.nix b/modules/applications/niri/home/nixos.nix index 2495296..aeaf62a 100644 --- a/modules/applications/niri/home/nixos.nix +++ b/modules/applications/niri/home/nixos.nix @@ -119,6 +119,13 @@ in ]; hotkey-overlay.title = "Find Cursor"; }; + "Ctrl+Space" = { + action.spawn = [ + "handy" + "--toggle-transcription" + ]; + hotkey-overlay.title = "Toggle Handy Transcription"; + }; # Function keys (sync with labwc modules/applications/labwc/home.nix) "XF86AudioRaiseVolume".action.spawn = [ diff --git a/modules/profiles/README.md b/modules/profiles/README.md index 1733398..0306699 100644 --- a/modules/profiles/README.md +++ b/modules/profiles/README.md @@ -63,6 +63,13 @@ enables performance improvements, synced lyrics, tracker blocking, the album color theme, and custom output-device selection while preserving user-owned settings such as the selected device. +`workload.personal` also provides Handy for offline speech-to-text. It starts +hidden when the graphical session begins. On niri and labwc, `Ctrl+Space` is +owned by the compositor and invokes Handy's `--toggle-transcription` command; +Handy's own shortcut bindings are disabled on NixOS so it does not monitor +keyboard events under Wayland. Handy uses the Homebrew cask and starts at login +on macOS. + On NixOS, `workload.game` provides Steam with Valve Proton and Proton-GE, Protontricks, Wayland-compatible Steam Input, GameMode, Gamescope, and MangoHud. Enabling Steam also activates the 32-bit graphics and PipeWire support required diff --git a/modules/profiles/workload/personal/meta.nix b/modules/profiles/workload/personal/meta.nix index e6de577..069e071 100644 --- a/modules/profiles/workload/personal/meta.nix +++ b/modules/profiles/workload/personal/meta.nix @@ -6,6 +6,7 @@ "applications.chrome" "applications.discord" "applications.gnome-text-editor" + "applications.handy" "applications.kde" "applications.moonlight" "applications.slack"