{
  description = "Handy - A free, open source, and extensible speech-to-text application that works completely offline";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

    # bun2nix: generates per-package Nix fetchurl expressions from bun.lock,
    # replacing the old FOD approach where a single hash covered the entire
    # node_modules directory (that hash would break on bun version changes).
    # See: https://github.com/nix-community/bun2nix
    bun2nix = {
      url = "github:nix-community/bun2nix/2.0.8";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs =
    {
      self,
      nixpkgs,
      bun2nix,
    }:
    let
      supportedSystems = [
        "x86_64-linux"
        "aarch64-linux"
      ];
      forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
      # Read version from Cargo.toml
      cargoToml = fromTOML (builtins.readFile ./src-tauri/Cargo.toml);
      version = cargoToml.package.version;
    in
    {
      packages = forAllSystems (
        system:
        let
          pkgs = import nixpkgs {
            inherit system;
            overlays = [ bun2nix.overlays.default ];
          };
          lib = pkgs.lib;
        in
        {
          handy = pkgs.rustPlatform.buildRustPackage {
            pname = "handy";
            inherit version;
            src = self;

            cargoRoot = "src-tauri";

            cargoLock = {
              lockFile = ./src-tauri/Cargo.lock;
              # Automatically fetch git dependencies using builtins.fetchGit.
              # This eliminates the need for manual outputHashes that had to be
              # updated every time a git dependency changed in Cargo.lock.
              # Safe for standalone flakes (not allowed in nixpkgs, it is needed something like crate2nix).
              allowBuiltinFetchGit = true;
            };

            postPatch = ''
              ${pkgs.jq}/bin/jq 'del(.build.beforeBuildCommand) | .bundle.createUpdaterArtifacts = false' \
                src-tauri/tauri.conf.json > $TMPDIR/tauri.conf.json
              cp $TMPDIR/tauri.conf.json src-tauri/tauri.conf.json

              # Strip postinstall hook — it runs check-nix-deps.ts which is only
              # needed during local development, not inside the Nix sandbox.
              ${pkgs.jq}/bin/jq 'del(.scripts.postinstall)' \
                package.json > $TMPDIR/package.json
              cp $TMPDIR/package.json package.json

              # Point libappindicator-sys to the Nix store path
              substituteInPlace \
                $cargoDepsCopy/libappindicator-sys-*/src/lib.rs \
                --replace-fail \
                  "libayatana-appindicator3.so.1" \
                  "${pkgs.libayatana-appindicator}/lib/libayatana-appindicator3.so.1"

              # Disable cbindgen in ferrous-opencc (calls cargo metadata which fails in sandbox)
              # Upstream removed this call in v0.3.1+
              substituteInPlace $cargoDepsCopy/ferrous-opencc-0.2.3/build.rs \
                --replace-fail '.expect("Unable to generate bindings")' '.ok();'
              substituteInPlace $cargoDepsCopy/ferrous-opencc-0.2.3/build.rs \
                --replace-fail '.write_to_file("opencc.h");' '// skipped'
            '';

            # Bun dependencies: fetched per-package using hashes from .nix/bun.nix.
            # This file is auto-generated by `bunx bun2nix -o .nix/bun.nix` and
            # kept in sync via the postinstall hook in package.json.
            # To regenerate manually: bun scripts/check-nix-deps.ts
            bunDeps = pkgs.bun2nix.fetchBunDeps {
              bunNix = ./.nix/bun.nix;
            };

            nativeBuildInputs = with pkgs; [
              cargo-tauri.hook
              pkg-config
              wrapGAppsHook4
              bun
              # pkgs.bun2nix (from overlay), not the flake input — `with pkgs;`
              # doesn't shadow function arguments in Nix.
              pkgs.bun2nix.hook # Sets up node_modules from pre-fetched bun cache
              jq
              cmake
              llvmPackages.libclang
              shaderc
            ];

            preBuild = ''
              # bun2nix.hook has already set up node_modules from pre-fetched cache.
              # Build the frontend with bun (tsc + vite).
              export HOME=$TMPDIR
              bun run build
            '';

            # Tests require runtime resources (audio devices, model files, GPU/Vulkan)
            # not available in the Nix build sandbox
            doCheck = false;

            # The tauri hook's installPhase expects target/ in cwd, but our
            # cargoRoot puts it under src-tauri/. Override to extract the DEB.
            installPhase = ''
              runHook preInstall
              mkdir -p $out
              cd src-tauri
              mv target/${pkgs.stdenv.hostPlatform.rust.rustcTarget}/release/bundle/deb/*/data/usr/* $out/
              runHook postInstall
            '';

            buildInputs = with pkgs; [
              webkitgtk_4_1
              gtk3
              glib
              glib-networking
              libsoup_3
              alsa-lib
              onnxruntime
              libayatana-appindicator
              libevdev
              libx11
              libxtst
              gtk-layer-shell
              openssl
              vulkan-loader
              vulkan-headers
              shaderc

              # Required for WebKitGTK audio/video
              gst_all_1.gstreamer
              gst_all_1.gst-plugins-base
              gst_all_1.gst-plugins-good
              gst_all_1.gst-plugins-bad
              gst_all_1.gst-plugins-ugly
            ];

            env = {
              LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
              BINDGEN_EXTRA_CLANG_ARGS = "-isystem ${pkgs.llvmPackages.libclang.lib}/lib/clang/${lib.getVersion pkgs.llvmPackages.libclang}/include -isystem ${pkgs.glibc.dev}/include";
              ORT_LIB_LOCATION = "${pkgs.onnxruntime}/lib";
              OPENSSL_NO_VENDOR = "1";

              # Tell Gstreamer where to find plugins
              GST_PLUGIN_SYSTEM_PATH_1_0 = "${pkgs.lib.makeSearchPathOutput "lib" "lib/gstreamer-1.0" (
                with pkgs.gst_all_1;
                [
                  gstreamer
                  gst-plugins-base
                  gst-plugins-good
                  gst-plugins-bad
                  gst-plugins-ugly
                ]
              )}";
            };

            preFixup = ''
              gappsWrapperArgs+=(
                --set WEBKIT_DISABLE_DMABUF_RENDERER 1
                --prefix LD_LIBRARY_PATH : "${
                  lib.makeLibraryPath [
                    pkgs.vulkan-loader
                    pkgs.onnxruntime
                  ]
                }"
              )
            '';

            meta = {
              description = "A free, open source, and extensible speech-to-text application that works completely offline";
              homepage = "https://github.com/cjpais/Handy";
              license = lib.licenses.mit;
              mainProgram = "handy";
              platforms = supportedSystems;
            };
          };

          default = self.packages.${system}.handy;
        }
      );

      # Development shell for building from source
      devShells = forAllSystems (
        system:
        let
          pkgs = import nixpkgs {
            inherit system;
          };
        in
        {
          default = pkgs.mkShell {
            buildInputs = with pkgs; [
              # Rust
              rustc
              cargo
              rust-analyzer
              clippy
              # Frontend
              nodejs
              bun
              # Tauri CLI
              cargo-tauri
              # Native deps
              pkg-config
              openssl
              alsa-lib
              libsoup_3
              webkitgtk_4_1
              gtk3
              gtk-layer-shell
              glib
              libxtst
              libevdev
              llvmPackages.libclang
              cmake
              vulkan-headers
              vulkan-loader
              shaderc
              libappindicator
            ];

            LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
            LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath [ pkgs.libappindicator ]}";
            GST_PLUGIN_SYSTEM_PATH_1_0 = "${pkgs.lib.makeSearchPathOutput "lib" "lib/gstreamer-1.0" (
              with pkgs.gst_all_1;
              [
                gstreamer
                gst-plugins-base
                gst-plugins-good
                gst-plugins-bad
                gst-plugins-ugly
              ]
            )}";

            # Same as wrapGAppsHook4
            XDG_DATA_DIRS = "${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:${pkgs.hicolor-icon-theme}/share";

            shellHook = ''
              echo "Handy development environment"
              bun install
              echo "Run 'bun run tauri dev' to start"
            '';
          };
        }
      );
    };
}
