diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index 58cf591b197f..6792993f7015 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -12543,6 +12543,13 @@ githubId = 39434424; name = "Felix Springer"; }; + junestepp = { + email = "git@junestepp.me"; + github = "junestepp"; + githubId = 26205306; + name = "June Stepp"; + keys = [ { fingerprint = "2561 0243 2233 CFE6 E13E 3C33 348C 6EB3 39AE C582"; } ]; + }; junjihashimoto = { email = "junji.hashimoto@gmail.com"; github = "junjihashimoto"; diff --git a/pkgs/games/anki/addons/adjust-sound-volume/default.nix b/pkgs/games/anki/addons/adjust-sound-volume/default.nix new file mode 100644 index 000000000000..cdeb7d3d727a --- /dev/null +++ b/pkgs/games/anki/addons/adjust-sound-volume/default.nix @@ -0,0 +1,23 @@ +{ + lib, + anki-utils, + fetchFromGitHub, + nix-update-script, +}: +anki-utils.buildAnkiAddon (finalAttrs: { + pname = "adjust-sound-volume"; + version = "0.0.6"; + src = fetchFromGitHub { + owner = "mnogu"; + repo = "adjust-sound-volume"; + rev = "v${finalAttrs.version}"; + hash = "sha256-6reIUz+tHKd4KQpuofLa/tIL5lCloj3yODZ8Cz29jFU="; + }; + passthru.updateScript = nix-update-script { }; + meta = { + description = "Add a new menu item for adjusting the sound volume"; + homepage = "https://github.com/mnogu/adjust-sound-volume"; + license = lib.licenses.agpl3Plus; + maintainers = with lib.maintainers; [ junestepp ]; + }; +}) diff --git a/pkgs/games/anki/addons/anki-connect/default.nix b/pkgs/games/anki/addons/anki-connect/default.nix new file mode 100644 index 000000000000..27a0411e52f7 --- /dev/null +++ b/pkgs/games/anki/addons/anki-connect/default.nix @@ -0,0 +1,27 @@ +{ + lib, + anki-utils, + fetchFromSourcehut, + nix-update-script, +}: +anki-utils.buildAnkiAddon (finalAttrs: { + pname = "anki-connect"; + version = "24.7.25.0"; + src = fetchFromSourcehut { + owner = "~foosoft"; + repo = "anki-connect"; + rev = finalAttrs.version; + hash = "sha256-N98EoCE/Bx+9QUQVeU64FXHXSek7ASBVv1b9ltJ4G1U="; + }; + sourceRoot = "${finalAttrs.src.name}/plugin"; + passthru.updateScript = nix-update-script { }; + meta = { + description = '' + Enable external applications such as Yomichan to communicate + with Anki over a simple HTTP API + ''; + homepage = "https://foosoft.net/projects/anki-connect/"; + license = lib.licenses.gpl3Plus; + maintainers = with lib.maintainers; [ junestepp ]; + }; +}) diff --git a/pkgs/games/anki/addons/anki-utils.nix b/pkgs/games/anki/addons/anki-utils.nix new file mode 100644 index 000000000000..e2247ebac874 --- /dev/null +++ b/pkgs/games/anki/addons/anki-utils.nix @@ -0,0 +1,126 @@ +{ + lib, + stdenv, + symlinkJoin, + lndir, + formats, + runCommand, +}: +{ + buildAnkiAddon = lib.extendMkDerivation { + constructDrv = stdenv.mkDerivation; + extendDrvArgs = + finalAttrs: + { + pname, + version, + src, + sourceRoot ? "", + configurePhase ? '' + runHook preConfigure + runHook postConfigure + '', + buildPhase ? '' + runHook preBuild + runHook postBuild + '', + dontPatchELF ? true, + dontStrip ? true, + nativeBuildInputs ? [ ], + passthru ? { }, + meta ? { }, + # Script run after "user_files" folder is populated. + # Used when an add-on needs to process and change "user_files" based + # on what the user added to it. + processUserFiles ? "", + ... + }: + { + inherit + version + src + sourceRoot + configurePhase + buildPhase + dontPatchELF + dontStrip + nativeBuildInputs + ; + + pname = "anki-addon-${pname}"; + + installPrefix = "share/anki/addons/${pname}"; + + installPhase = '' + runHook preInstall + + mkdir -p "$out/$installPrefix" + find . -mindepth 1 -maxdepth 1 | xargs -d'\n' mv -t "$out/$installPrefix/" + + runHook postInstall + ''; + + passthru = { + withConfig = + { + # JSON add-on config. The available options for an add-on are in its + # config.json file. + # See https://addon-docs.ankiweb.net/addon-config.html#config-json + config ? { }, + # Path to a folder to be merged with the add-on "user_files" folder. + # See https://addon-docs.ankiweb.net/addon-config.html#user-files. + userFiles ? null, + }: + let + metaConfigFormat = formats.json { }; + addonMetaConfig = metaConfigFormat.generate "meta.json" { inherit config; }; + in + symlinkJoin { + pname = "${finalAttrs.pname}-with-config"; + inherit (finalAttrs) version meta; + + paths = [ + finalAttrs.finalPackage + ]; + + postBuild = '' + cd $out/${finalAttrs.installPrefix} + + rm -f meta.json + ln -s ${addonMetaConfig} meta.json + + mkdir -p user_files + ${ + if (userFiles != null) then + '' + ${lndir}/bin/lndir -silent "${userFiles}" user_files + '' + else + "" + } + + ${processUserFiles} + ''; + }; + } // passthru; + + meta = { + platforms = lib.platforms.all; + } // meta; + }; + }; + + buildAnkiAddonsDir = + addonPackages: + let + addonDirs = map (pkg: "${pkg}/share/anki/addons") addonPackages; + addons = lib.concatMapStringsSep " " (p: "${p}/*") addonDirs; + in + runCommand "anki-addons" { } '' + mkdir $out + [[ '${addons}' ]] || exit 0 + for addon in ${addons}; do + ln -s "$addon" $out/ + done + ''; +} diff --git a/pkgs/games/anki/addons/default.nix b/pkgs/games/anki/addons/default.nix new file mode 100644 index 000000000000..d602b1ea877e --- /dev/null +++ b/pkgs/games/anki/addons/default.nix @@ -0,0 +1,18 @@ +{ + callPackage, +}: +{ + adjust-sound-volume = callPackage ./adjust-sound-volume { }; + + anki-connect = callPackage ./anki-connect { }; + + local-audio-yomichan = callPackage ./local-audio-yomichan { }; + + passfail2 = callPackage ./passfail2 { }; + + recolor = callPackage ./recolor { }; + + reviewer-refocus-card = callPackage ./reviewer-refocus-card { }; + + yomichan-forvo-server = callPackage ./yomichan-forvo-server { }; +} diff --git a/pkgs/games/anki/addons/local-audio-yomichan/default.nix b/pkgs/games/anki/addons/local-audio-yomichan/default.nix new file mode 100644 index 000000000000..7ba361c8590c --- /dev/null +++ b/pkgs/games/anki/addons/local-audio-yomichan/default.nix @@ -0,0 +1,73 @@ +{ + lib, + anki-utils, + fetchFromGitHub, + python3, + nix-update-script, +}: +anki-utils.buildAnkiAddon (finalAttrs: { + pname = "local-audio-yomichan"; + version = "0-unstable-2025-04-26"; + src = fetchFromGitHub { + owner = "yomidevs"; + repo = "local-audio-yomichan"; + rev = "34750f1d8ca1cb473128fea7976a4d981e5e78a4"; + sparseCheckout = [ "plugin" ]; + hash = "sha256-2gyggcvxParay+1B7Sg2COKyocoxaRO1WTz+ymdRp4w="; + }; + sourceRoot = "${finalAttrs.src.name}/plugin"; + processUserFiles = '' + # Addon will try to load extra stuff unless Python package name is "plugin". + temp=$(mktemp -d) + ln -s $PWD $temp/plugin + # Addoon expects `user_files` dir at `$XDG_DATA_HOME/local-audio-yomichan` + ln -s $PWD/user_files $temp/local-audio-yomichan + + PYTHONPATH=$temp \ + WO_ANKI=1 \ + XDG_DATA_HOME=$temp \ + ${lib.getExe python3} -c \ + "from plugin import db_utils; \ + db_utils.init_db()" + ''; + passthru.updateScript = nix-update-script { + extraArgs = [ "--version=branch" ]; + }; + meta = { + description = "Run a local audio server for Yomitan"; + longDescription = '' + This add-on must be configured with an audio collection. + + Example: + + ```nix + pkgs.ankiAddons.local-audio-yomichan.withConfig { + userFiles = + let + audio-collection = + pkgs.runCommand "local-yomichan-audio-collection" + { + outputHashMode = "recursive"; + outputHash = "sha256-NxbcXh2SDPfCd+ZHAWT5JdxRecNbT4Xpo0pxX5/DOfo="; + + src = pkgs.requireFile { + name = "local-yomichan-audio-collection-2023-06-11-opus.tar.xz"; + url = "https://github.com/yomidevs/local-audio-yomichan?tab=readme-ov-file#steps"; + sha256 = "1xsxp8iggklv77rj972mqaa1i8f9hvr3ir0r2mwfqcdz4q120hr1"; + }; + } + ''' + mkdir -p $out + cd $out + tar -xf "$src" + '''; + in + "''${audio-collection}/user_files"; + } + ``` + ''; + homepage = "https://github.com/yomidevs/local-audio-yomichan"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ junestepp ]; + }; +}) diff --git a/pkgs/games/anki/addons/passfail2/default.nix b/pkgs/games/anki/addons/passfail2/default.nix new file mode 100644 index 000000000000..73746c1a568a --- /dev/null +++ b/pkgs/games/anki/addons/passfail2/default.nix @@ -0,0 +1,34 @@ +{ + lib, + anki-utils, + fetchFromGitHub, + nix-update-script, +}: +anki-utils.buildAnkiAddon (finalAttrs: { + pname = "passfail2"; + version = "0.3.0-unstable-2024-10-17"; + src = fetchFromGitHub { + owner = "lambdadog"; + repo = "passfail2"; + rev = "d5313e4f1217e968b36edbc0a4fe92386209ffe6"; + hash = "sha256-HMe6/fHpYj/MN0dUFj3W71vK7qqcp9l1xm8SAiKkJLs="; + }; + buildPhase = '' + runHook preBuild + + substitute build_info.py.in build_info.py \ + --replace-fail '$version' '"${finalAttrs.version}"' + + runHook postBuild + ''; + passthru.updateScript = nix-update-script { }; + meta = { + description = '' + Replaces the default Anki review buttons with only two options: + “Fail” and “Pass” + ''; + homepage = "https://github.com/lambdadog/passfail2"; + license = lib.licenses.gpl3Plus; + maintainers = with lib.maintainers; [ junestepp ]; + }; +}) diff --git a/pkgs/games/anki/addons/recolor/default.nix b/pkgs/games/anki/addons/recolor/default.nix new file mode 100644 index 000000000000..3e60b0193904 --- /dev/null +++ b/pkgs/games/anki/addons/recolor/default.nix @@ -0,0 +1,27 @@ +{ + lib, + anki-utils, + fetchFromGitHub, + nix-update-script, +}: +anki-utils.buildAnkiAddon (finalAttrs: { + pname = "recolor"; + version = "3.1"; + src = fetchFromGitHub { + owner = "AnKing-VIP"; + repo = "AnkiRecolor"; + rev = finalAttrs.version; + sparseCheckout = [ "src/addon" ]; + hash = "sha256-28DJq2l9DP8O6OsbNQCZ0pm4S6CQ3Yz0Vfvlj+iQw8Y="; + }; + sourceRoot = "${finalAttrs.src.name}/src/addon"; + passthru.updateScript = nix-update-script { }; + meta = { + description = "ReColor your Anki desktop to whatever aesthetic you like"; + homepage = "https://github.com/AnKing-VIP/AnkiRecolor"; + # No license file, but it can be assumed to be AGPL3 based on + # https://ankiweb.net/account/terms. + license = lib.licenses.agpl3Only; + maintainers = with lib.maintainers; [ junestepp ]; + }; +}) diff --git a/pkgs/games/anki/addons/reviewer-refocus-card/default.nix b/pkgs/games/anki/addons/reviewer-refocus-card/default.nix new file mode 100644 index 000000000000..6fde24afd22e --- /dev/null +++ b/pkgs/games/anki/addons/reviewer-refocus-card/default.nix @@ -0,0 +1,30 @@ +{ + lib, + anki-utils, + fetchFromGitHub, + nix-update-script, +}: +anki-utils.buildAnkiAddon (finalAttrs: { + pname = "reviewer-refocus-card"; + version = "0-unstable-2022-12-24"; + src = fetchFromGitHub { + owner = "glutanimate"; + repo = "anki-addons-misc"; + rev = "7b981836e0a6637a1853f3e8d73d022ab95fed31"; + sparseCheckout = [ "src/reviewer_refocus_card" ]; + hash = "sha256-181hyc4ED+0lBzn1FnrBvNIYIUQF8xEDB3uHK6SkpHw="; + }; + sourceRoot = "${finalAttrs.src.name}/src/reviewer_refocus_card"; + passthru.updateScript = nix-update-script { + extraArgs = [ "--version=branch" ]; + }; + meta = { + description = '' + Set focus to the card area, allowing you to scroll through your cards using + Page Up / Page Down, etc + ''; + homepage = "https://github.com/glutanimate/anki-addons-misc"; + license = lib.licenses.agpl3Only; + maintainers = with lib.maintainers; [ junestepp ]; + }; +}) diff --git a/pkgs/games/anki/addons/yomichan-forvo-server/default.nix b/pkgs/games/anki/addons/yomichan-forvo-server/default.nix new file mode 100644 index 000000000000..751798f6e3a6 --- /dev/null +++ b/pkgs/games/anki/addons/yomichan-forvo-server/default.nix @@ -0,0 +1,25 @@ +{ + lib, + anki-utils, + fetchFromGitHub, + nix-update-script, +}: +anki-utils.buildAnkiAddon { + pname = "yomichan-forvo-server"; + version = "0-unstable-2024-10-21"; + src = fetchFromGitHub { + owner = "jamesnicolas"; + repo = "yomichan-forvo-server"; + rev = "364fc6d5d10969f516e0fa283460dfaf08c98e15"; + hash = "sha256-Jpee9hkXCiBmSW7hzJ1rAg45XVIiLC8WENc09+ySFVI="; + }; + passthru.updateScript = nix-update-script { + extraArgs = [ "--version=branch" ]; + }; + meta = { + description = "Audio server for yomichan that scrapes forvo for audio files"; + homepage = "https://github.com/jamesnicolas/yomichan-forvo-server"; + license = lib.licenses.unlicense; + maintainers = with lib.maintainers; [ junestepp ]; + }; +} diff --git a/pkgs/games/anki/default.nix b/pkgs/games/anki/default.nix index ab520dea5da0..15b88f67a7bc 100644 --- a/pkgs/games/anki/default.nix +++ b/pkgs/games/anki/default.nix @@ -10,6 +10,7 @@ lame, mpv-unwrapped, ninja, + callPackage, nixosTests, nodejs, jq, @@ -91,6 +92,8 @@ python3.pkgs.buildPythonApplication rec { ./patches/disable-auto-update.patch ./patches/remove-the-gl-library-workaround.patch ./patches/skip-formatting-python-code.patch + # Used in with-addons.nix + ./patches/allow-setting-addons-folder.patch ]; inherit cargoDeps; @@ -269,6 +272,7 @@ python3.pkgs.buildPythonApplication rec { ''; passthru = { + withAddons = ankiAddons: callPackage ./with-addons.nix { inherit ankiAddons; }; tests.anki-sync-server = nixosTests.anki-sync-server; }; @@ -292,6 +296,7 @@ python3.pkgs.buildPythonApplication rec { inherit (mesa.meta) platforms; maintainers = with maintainers; [ euank + junestepp oxij ]; # Reported to crash at launch on darwin (as of 2.1.65) diff --git a/pkgs/games/anki/patches/allow-setting-addons-folder.patch b/pkgs/games/anki/patches/allow-setting-addons-folder.patch new file mode 100644 index 000000000000..12f7846267d5 --- /dev/null +++ b/pkgs/games/anki/patches/allow-setting-addons-folder.patch @@ -0,0 +1,15 @@ +diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py +index 469908c1b2..34612d6e08 100644 +--- a/qt/aqt/profiles.py ++++ b/qt/aqt/profiles.py +@@ -310,7 +310,9 @@ def profileFolder(self, create: bool = True) -> str: + return path + + def addonFolder(self) -> str: +- return self._ensureExists(os.path.join(self.base, "addons21")) ++ path = Path(os.environ.get("ANKI_ADDONS") or Path(self.base) / "addons21") ++ path.mkdir(parents=True, exist_ok=True) ++ return str(path.resolve()) + + def backupFolder(self) -> str: + return self._ensureExists(os.path.join(self.profileFolder(), "backups")) diff --git a/pkgs/games/anki/with-addons.nix b/pkgs/games/anki/with-addons.nix new file mode 100644 index 000000000000..d47c2c16f316 --- /dev/null +++ b/pkgs/games/anki/with-addons.nix @@ -0,0 +1,107 @@ +{ + lib, + symlinkJoin, + makeWrapper, + anki, + anki-utils, + writeTextDir, + ankiAddons ? [ ], +}: +/* + `ankiAddons` + : A set of Anki add-ons to be installed. Here's a an example: + + ~~~ + pkgs.anki.withAddons [ + # When the add-on is already available in nixpkgs + pkgs.ankiAddons.anki-connect + + # When the add-on is not available in nixpkgs + (pkgs.anki-utils.buildAnkiAddon (finalAttrs: { + pname = "recolor"; + version = "3.1"; + src = pkgs.fetchFromGitHub { + owner = "AnKing-VIP"; + repo = "AnkiRecolor"; + rev = finalAttrs.version; + sparseCheckout = [ "src/addon" ]; + hash = "sha256-28DJq2l9DP8O6OsbNQCZ0pm4S6CQ3Yz0Vfvlj+iQw8Y="; + }; + sourceRoot = "source/src/addon"; + })) + + # When the add-on needs to be configured + pkgs.ankiAddons.passfail2.withConfig { + config = { + again_button_name = "not quite"; + good_button_name = "excellent"; + }; + + user_files = ./dir-to-be-merged-into-addon-user-files-dir; + }; + ] + ~~~ + + The original `anki` executable will be wrapped so that it uses the addons from + `ankiAddons`. + + This only works with Anki versions patched to support the `ANKI_ADDONS` environment + variable. `pkgs.anki` has this, but `pkgs.anki-bin` does not. +*/ +let + defaultAddons = [ + (anki-utils.buildAnkiAddon { + pname = "nixos"; + version = "1.0"; + src = writeTextDir "__init__.py" '' + import aqt + from aqt.qt import QMessageBox + import json + + def addons_dialog_will_show(dialog: aqt.addons.AddonsDialog) -> None: + dialog.setEnabled(False) + QMessageBox.information( + dialog, + "NixOS Info", + ("These add-ons are managed by NixOS.
" + "See " + "github.com/NixOS/nixpkgs/tree/master/pkgs/games/anki/with-addons.nix") + ) + + def addon_tried_to_write_config(module: str, conf: dict) -> None: + message_box = QMessageBox( + QMessageBox.Icon.Warning, + "NixOS Info", + (f"The add-on module: \"{module}\" tried to update its config.
" + "See " + "github.com/NixOS/nixpkgs/tree/master/pkgs/games/anki/with-addons.nix" + " for how to configure add-ons managed by NixOS.") + ) + message_box.setDetailedText(json.dumps(conf)) + message_box.exec() + + aqt.gui_hooks.addons_dialog_will_show.append(addons_dialog_will_show) + aqt.mw.addonManager.writeConfig = addon_tried_to_write_config + ''; + meta.maintainers = with lib.maintainers; [ junestepp ]; + }) + ]; +in +symlinkJoin { + inherit (anki) version; + pname = "${anki.pname}-with-addons"; + + paths = [ anki ]; + + nativeBuildInputs = [ makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/anki \ + --set ANKI_ADDONS "${anki-utils.buildAnkiAddonsDir (ankiAddons ++ defaultAddons)}" + ''; + + meta = builtins.removeAttrs anki.meta [ + "name" + "outputsToInstall" + "position" + ]; +} diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 31f5b7fe7c9e..160f7c5a0006 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -14838,6 +14838,8 @@ with pkgs; amoeba-data = callPackage ../games/amoeba/data.nix { }; anki = callPackage ../games/anki { }; + anki-utils = callPackage ../games/anki/addons/anki-utils.nix { }; + ankiAddons = recurseIntoAttrs (callPackage ../games/anki/addons { }); anki-bin = callPackage ../games/anki/bin.nix { }; anki-sync-server = callPackage ../games/anki/sync-server.nix { };