mirror of
https://github.com/nix-community/home-manager.git
synced 2025-12-02 07:01:03 +01:00
anki: add module (#7274)
This commit is contained in:
parent
57d1027e1e
commit
e8da7372fd
6 changed files with 679 additions and 0 deletions
9
modules/misc/news/2025/06/2025-06-14_21-49-02.nix
Normal file
9
modules/misc/news/2025/06/2025-06-14_21-49-02.nix
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
time = "2025-06-15T02:49:02+00:00";
|
||||
condition = true;
|
||||
message = ''
|
||||
A new module is available: 'programs.anki'.
|
||||
|
||||
Anki is a spaced repetition flashcard program.
|
||||
'';
|
||||
}
|
||||
327
modules/programs/anki/default.nix
Normal file
327
modules/programs/anki/default.nix
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
helper = import ./helper.nix { inherit lib config pkgs; };
|
||||
|
||||
ankiBaseDir =
|
||||
if pkgs.stdenv.hostPlatform.isDarwin then
|
||||
"Library/Application Support/Anki2"
|
||||
else
|
||||
"${config.xdg.dataHome}/Anki2";
|
||||
cfg = config.programs.anki;
|
||||
in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.junestepp ];
|
||||
|
||||
options.programs.anki = {
|
||||
enable = lib.mkEnableOption "Anki";
|
||||
|
||||
package = lib.mkPackageOption pkgs "anki" { };
|
||||
|
||||
language = lib.mkOption {
|
||||
type = with lib.types; nullOr str;
|
||||
default = null;
|
||||
example = "en_US";
|
||||
description = ''
|
||||
Display language. Should be an underscore separated language tag.
|
||||
See <https://github.com/ankitects/anki/blob/main/pylib/anki/lang.py> for
|
||||
supported tags.
|
||||
'';
|
||||
};
|
||||
|
||||
videoDriver = lib.mkOption {
|
||||
type =
|
||||
with lib.types;
|
||||
nullOr (enum [
|
||||
"opengl"
|
||||
"angle"
|
||||
"software"
|
||||
"metal"
|
||||
"vulkan"
|
||||
"d3d11"
|
||||
]);
|
||||
default = null;
|
||||
example = "opengl";
|
||||
description = "Video driver to use.";
|
||||
};
|
||||
|
||||
theme = lib.mkOption {
|
||||
type =
|
||||
with lib.types;
|
||||
nullOr (enum [
|
||||
"followSystem"
|
||||
"light"
|
||||
"dark"
|
||||
]);
|
||||
default = null;
|
||||
example = "dark";
|
||||
description = "Theme to use.";
|
||||
};
|
||||
|
||||
style = lib.mkOption {
|
||||
type =
|
||||
with lib.types;
|
||||
nullOr (enum [
|
||||
"anki"
|
||||
"native"
|
||||
]);
|
||||
default = null;
|
||||
example = "native";
|
||||
description = "Widgets style.";
|
||||
};
|
||||
|
||||
uiScale = lib.mkOption {
|
||||
type = with lib.types; nullOr (numbers.between 0.0 1.0);
|
||||
default = null;
|
||||
example = 1.0;
|
||||
description = "User interface scale.";
|
||||
};
|
||||
|
||||
hideTopBar = lib.mkOption {
|
||||
type = with lib.types; nullOr bool;
|
||||
default = null;
|
||||
example = true;
|
||||
description = "Hide top bar during review.";
|
||||
};
|
||||
hideTopBarMode = lib.mkOption {
|
||||
type =
|
||||
with lib.types;
|
||||
nullOr (enum [
|
||||
"fullscreen"
|
||||
"always"
|
||||
]);
|
||||
default = null;
|
||||
example = "fullscreen";
|
||||
description = "When to hide the top bar when `hideTopBar` is enabled.";
|
||||
};
|
||||
|
||||
hideBottomBar = lib.mkOption {
|
||||
type = with lib.types; nullOr bool;
|
||||
default = null;
|
||||
example = true;
|
||||
description = "Hide bottom bar during review.";
|
||||
};
|
||||
hideBottomBarMode = lib.mkOption {
|
||||
type =
|
||||
with lib.types;
|
||||
nullOr (enum [
|
||||
"fullscreen"
|
||||
"always"
|
||||
]);
|
||||
default = null;
|
||||
example = "fullscreen";
|
||||
description = "When to hide the bottom bar when `hideBottomBar` is enabled.";
|
||||
};
|
||||
|
||||
reduceMotion = lib.mkOption {
|
||||
type = with lib.types; nullOr bool;
|
||||
default = null;
|
||||
example = true;
|
||||
description = "Disable various animations and transitions of the user interface.";
|
||||
};
|
||||
|
||||
minimalistMode = lib.mkOption {
|
||||
type = with lib.types; nullOr bool;
|
||||
default = null;
|
||||
example = true;
|
||||
description = "Minimalist user interface mode.";
|
||||
};
|
||||
|
||||
spacebarRatesCard = lib.mkOption {
|
||||
type = with lib.types; nullOr bool;
|
||||
default = null;
|
||||
example = true;
|
||||
description = "Spacebar (or enter) also answers card.";
|
||||
};
|
||||
|
||||
legacyImportExport = lib.mkOption {
|
||||
type = with lib.types; nullOr bool;
|
||||
default = null;
|
||||
description = "Use legacy (pre 2.1.55) import/export handling code.";
|
||||
};
|
||||
|
||||
answerKeys = lib.mkOption {
|
||||
type =
|
||||
with lib.types;
|
||||
listOf (submodule {
|
||||
options = {
|
||||
ease = lib.mkOption {
|
||||
type = with lib.types; int;
|
||||
example = 3;
|
||||
description = ''
|
||||
Number associated with an answer button.
|
||||
|
||||
By default, 1 = Again, 2 = Hard, 3 = Good, and 4 = Easy.
|
||||
'';
|
||||
};
|
||||
key = lib.mkOption {
|
||||
type = with lib.types; str;
|
||||
example = "3";
|
||||
description = ''
|
||||
Keyboard shortcut for this answer button. The shortcut should be in
|
||||
the string format used by <https://doc.qt.io/qt-6/qkeysequence.html>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [ ];
|
||||
example = [
|
||||
{
|
||||
ease = 1;
|
||||
key = "left";
|
||||
}
|
||||
{
|
||||
ease = 2;
|
||||
key = "up";
|
||||
}
|
||||
{
|
||||
ease = 3;
|
||||
key = "right";
|
||||
}
|
||||
{
|
||||
ease = 4;
|
||||
key = "down";
|
||||
}
|
||||
];
|
||||
description = ''
|
||||
Overrides for choosing what keyboard shortcut activates each
|
||||
answer button. The Anki default will be used for ones without an
|
||||
override defined.
|
||||
'';
|
||||
};
|
||||
|
||||
sync = {
|
||||
username = lib.mkOption {
|
||||
type = with lib.types; nullOr str;
|
||||
default = null;
|
||||
example = "lovelearning@email.com";
|
||||
description = "Sync account username.";
|
||||
};
|
||||
|
||||
usernameFile = lib.mkOption {
|
||||
type = with lib.types; nullOr path;
|
||||
default = null;
|
||||
description = "Path to a file containing the sync account username.";
|
||||
};
|
||||
|
||||
passwordFile = lib.mkOption {
|
||||
type = with lib.types; nullOr path;
|
||||
default = null;
|
||||
description = "Path to a file containing the sync account password.";
|
||||
};
|
||||
|
||||
url = lib.mkOption {
|
||||
type = with lib.types; nullOr str;
|
||||
default = null;
|
||||
example = "http://example.com/anki-sync/";
|
||||
description = ''
|
||||
Custom sync server URL. See <https://docs.ankiweb.net/sync-server.html>.
|
||||
'';
|
||||
};
|
||||
|
||||
autoSync = lib.mkOption {
|
||||
type = with lib.types; nullOr bool;
|
||||
default = null;
|
||||
example = true;
|
||||
description = "Automatically sync on profile open/close.";
|
||||
};
|
||||
|
||||
syncMedia = lib.mkOption {
|
||||
type = with lib.types; nullOr bool;
|
||||
default = null;
|
||||
example = true;
|
||||
description = "Synchronize audio and images too.";
|
||||
};
|
||||
|
||||
autoSyncMediaMinutes = lib.mkOption {
|
||||
type = with lib.types; nullOr ints.unsigned;
|
||||
default = null;
|
||||
example = 15;
|
||||
description = ''
|
||||
Automatically sync media every X minutes. Set this to 0 to disable
|
||||
periodic media syncing.
|
||||
'';
|
||||
};
|
||||
|
||||
networkTimeout = lib.mkOption {
|
||||
type = with lib.types; nullOr ints.unsigned;
|
||||
default = null;
|
||||
example = 60;
|
||||
description = "Network timeout in seconds.";
|
||||
};
|
||||
};
|
||||
|
||||
addons = lib.mkOption {
|
||||
type = with lib.types; listOf package;
|
||||
default = [ ];
|
||||
example = lib.literalExpression ''
|
||||
[
|
||||
# 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 = "''${finalAttrs.src.name}/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;
|
||||
};
|
||||
]
|
||||
'';
|
||||
description = "List of Anki add-on packages to install.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = !(cfg.sync.username != null && cfg.sync.usernameFile != null);
|
||||
message = ''
|
||||
The `programs.anki.sync` `username` option is mutually exclusive with
|
||||
the `usernameFile` option.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = cfg.package ? withAddons;
|
||||
message = ''
|
||||
The value of `programs.anki.package` doesn't support declaratively managing
|
||||
add-ons. Make sure you are using `pkgs.anki`.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
home.packages = [
|
||||
(cfg.package.withAddons (
|
||||
[
|
||||
helper.homeManagerAnkiAddon
|
||||
helper.syncConfigAnkiAddon
|
||||
]
|
||||
++ cfg.addons
|
||||
))
|
||||
];
|
||||
|
||||
home.file."${ankiBaseDir}/gldriver6" = lib.mkIf (cfg.videoDriver != null) {
|
||||
source = "${helper.ankiConfig}/gldriver6";
|
||||
};
|
||||
home.file."${ankiBaseDir}/prefs21.db".source = "${helper.ankiConfig}/prefs21.db";
|
||||
};
|
||||
}
|
||||
261
modules/programs/anki/helper.nix
Normal file
261
modules/programs/anki/helper.nix
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.programs.anki;
|
||||
# This script generates the Anki SQLite settings DB using the Anki Python API.
|
||||
# The configuration options in the SQLite database take the form of Python
|
||||
# Pickle data.
|
||||
# A simple "gldriver6" file is also generated for the `videoDriver` option.
|
||||
buildAnkiConfig = pkgs.writers.writeText "buildAnkiConfig" ''
|
||||
import sys
|
||||
|
||||
from aqt.profiles import ProfileManager, VideoDriver
|
||||
from aqt.theme import Theme, WidgetStyle, theme_manager
|
||||
from aqt.toolbar import HideMode
|
||||
|
||||
profile_manager = ProfileManager(
|
||||
ProfileManager.get_created_base_folder(sys.argv[1])
|
||||
)
|
||||
_ = profile_manager.setupMeta()
|
||||
profile_manager.meta["firstRun"] = False
|
||||
|
||||
# Video driver. Option is stored in a separate file from other options.
|
||||
video_driver_str: str = "${toString cfg.videoDriver}"
|
||||
if video_driver_str:
|
||||
# The enum value for OpenGL isn't "opengl"
|
||||
if video_driver_str == "opengl":
|
||||
video_driver = VideoDriver.OpenGL
|
||||
else:
|
||||
video_driver = VideoDriver(video_driver_str)
|
||||
profile_manager.set_video_driver(video_driver)
|
||||
|
||||
|
||||
# Shared options
|
||||
|
||||
lang_tag: str = "${toString cfg.language}"
|
||||
if lang_tag:
|
||||
profile_manager.setLang(lang_tag)
|
||||
|
||||
theme_str: str = "${toString cfg.theme}"
|
||||
if theme_str:
|
||||
theme: Theme = {
|
||||
"followSystem": Theme.FOLLOW_SYSTEM,
|
||||
"light": Theme.LIGHT,
|
||||
"dark": Theme.DARK
|
||||
}[theme_str]
|
||||
profile_manager.set_theme(theme)
|
||||
|
||||
style_str: str = "${toString cfg.style}"
|
||||
if style_str:
|
||||
style: WidgetStyle = {
|
||||
"anki": WidgetStyle.ANKI, "native": WidgetStyle.NATIVE
|
||||
}[style_str]
|
||||
# Fix error from there being no main window to update the style of
|
||||
theme_manager.apply_style = lambda: None
|
||||
profile_manager.set_widget_style(style)
|
||||
|
||||
ui_scale_str: str = "${toString cfg.uiScale}"
|
||||
if ui_scale_str:
|
||||
profile_manager.setUiScale(float(ui_scale_str))
|
||||
|
||||
hide_top_bar_str: str = "${toString cfg.hideTopBar}"
|
||||
if hide_top_bar_str:
|
||||
profile_manager.set_hide_top_bar(bool(hide_top_bar_str))
|
||||
|
||||
hide_top_bar_mode_str: str = "${toString cfg.hideTopBarMode}"
|
||||
if hide_top_bar_mode_str:
|
||||
hide_mode: HideMode = {
|
||||
"fullscreen": HideMode.FULLSCREEN,
|
||||
"always": HideMode.ALWAYS,
|
||||
}[hide_top_bar_mode_str]
|
||||
profile_manager.set_top_bar_hide_mode(hide_mode)
|
||||
|
||||
hide_bottom_bar_str: str = "${toString cfg.hideBottomBar}"
|
||||
if hide_bottom_bar_str:
|
||||
profile_manager.set_hide_bottom_bar(bool(hide_bottom_bar_str))
|
||||
|
||||
hide_bottom_bar_mode_str: str = "${toString cfg.hideBottomBarMode}"
|
||||
if hide_bottom_bar_mode_str:
|
||||
hide_mode: HideMode = {
|
||||
"fullscreen": HideMode.FULLSCREEN,
|
||||
"always": HideMode.ALWAYS,
|
||||
}[hide_bottom_bar_mode_str]
|
||||
profile_manager.set_bottom_bar_hide_mode(hide_mode)
|
||||
|
||||
reduce_motion_str: str = "${toString cfg.reduceMotion}"
|
||||
if reduce_motion_str:
|
||||
profile_manager.set_reduce_motion(bool(reduce_motion_str))
|
||||
|
||||
minimalist_mode_str: str = "${toString cfg.minimalistMode}"
|
||||
if minimalist_mode_str:
|
||||
profile_manager.set_minimalist_mode(bool(minimalist_mode_str))
|
||||
|
||||
spacebar_rates_card_str: str = "${toString cfg.spacebarRatesCard}"
|
||||
if spacebar_rates_card_str:
|
||||
profile_manager.set_spacebar_rates_card(bool(spacebar_rates_card_str))
|
||||
|
||||
legacy_import_export_str: str = "${toString cfg.legacyImportExport}"
|
||||
if legacy_import_export_str:
|
||||
profile_manager.set_legacy_import_export(bool(legacy_import_export_str))
|
||||
|
||||
answer_keys: tuple[tuple[int, str], ...] = (${
|
||||
lib.strings.concatMapStringsSep ", " (val: "(${toString val.ease}, '${val.key}')") cfg.answerKeys
|
||||
})
|
||||
for ease, key in answer_keys:
|
||||
profile_manager.set_answer_key(ease, key)
|
||||
|
||||
# Profile specific options
|
||||
|
||||
profile_manager.create("User 1")
|
||||
profile_manager.openProfile("User 1")
|
||||
|
||||
# Without this, the collection DB won't get automatically optimized.
|
||||
profile_manager.profile["lastOptimize"] = None
|
||||
|
||||
auto_sync_str: str = "${toString cfg.sync.autoSync}"
|
||||
if auto_sync_str:
|
||||
profile_manager.profile["autoSync"] = bool(auto_sync_str)
|
||||
|
||||
sync_media_str: str = "${toString cfg.sync.syncMedia}"
|
||||
if sync_media_str:
|
||||
profile_manager.profile["syncMedia"] = bool(sync_media_str)
|
||||
|
||||
media_sync_minutes_str: str = "${toString cfg.sync.autoSyncMediaMinutes}"
|
||||
if media_sync_minutes_str:
|
||||
profile_manager.set_periodic_sync_media_minutes = int(media_sync_minutes_str)
|
||||
|
||||
network_timeout_str: str = "${toString cfg.sync.networkTimeout}"
|
||||
if network_timeout_str:
|
||||
profile_manager.set_network_timeout = int(network_timeout_str)
|
||||
|
||||
profile_manager.save()
|
||||
'';
|
||||
in
|
||||
{
|
||||
ankiConfig =
|
||||
let
|
||||
cfgAnkiPython = (
|
||||
lib.lists.findSingle (x: x.isPy3 or false) null null (cfg.package.nativeBuildInputs or [ ])
|
||||
);
|
||||
ankiPackage = if cfgAnkiPython == null then pkgs.anki else cfg.package;
|
||||
ankiPython = if cfgAnkiPython == null then pkgs.python3 else cfgAnkiPython;
|
||||
in
|
||||
pkgs.runCommand "ankiConfig"
|
||||
{
|
||||
nativeBuildInputs = [ ankiPackage ];
|
||||
}
|
||||
''
|
||||
${ankiPython.interpreter} ${buildAnkiConfig} $out
|
||||
'';
|
||||
|
||||
# An Anki add-on is used for sync settings, so the secrets can be
|
||||
# retrieved at runtime.
|
||||
syncConfigAnkiAddon = pkgs.anki-utils.buildAnkiAddon {
|
||||
pname = "hm-sync-config";
|
||||
version = "1.0";
|
||||
src = pkgs.writeTextDir "__init__.py" ''
|
||||
import aqt
|
||||
from pathlib import Path
|
||||
|
||||
username: str | None = ${if cfg.sync.username == null then "None" else "'${cfg.sync.username}'"}
|
||||
username_file: Path | None = ${
|
||||
if cfg.sync.usernameFile == null then "None" else "Path('${cfg.sync.usernameFile}')"
|
||||
}
|
||||
key_file: Path | None = ${
|
||||
if cfg.sync.passwordFile == null then "None" else "Path('${cfg.sync.passwordFile}')"
|
||||
}
|
||||
custom_sync_url: str | None = ${if cfg.sync.url == null then "None" else "'${cfg.sync.url}'"}
|
||||
|
||||
def set_server() -> None:
|
||||
if custom_sync_url:
|
||||
aqt.mw.pm.set_custom_sync_url(custom_sync_url)
|
||||
if username:
|
||||
aqt.mw.pm.set_sync_username(username)
|
||||
elif username_file and username_file.exists():
|
||||
aqt.mw.pm.set_sync_username(username_file.read_text())
|
||||
if key_file and key_file.exists():
|
||||
aqt.mw.pm.set_sync_key(key_file.read_text())
|
||||
|
||||
aqt.gui_hooks.profile_did_open.append(set_server)
|
||||
'';
|
||||
};
|
||||
|
||||
# Make Anki work better with declarative settings. See script for specific changes.
|
||||
homeManagerAnkiAddon = pkgs.anki-utils.buildAnkiAddon {
|
||||
pname = "home-manager";
|
||||
version = "1.0";
|
||||
src = pkgs.writeTextDir "__init__.py" ''
|
||||
import aqt
|
||||
from aqt.qt import QWidget, QMessageBox
|
||||
from anki.hooks import wrap
|
||||
from typing import Any
|
||||
|
||||
def make_config_differences_str(initial_config: dict[str, Any],
|
||||
new_config: dict[str, Any]) -> str:
|
||||
details = ""
|
||||
for key, val in new_config.items():
|
||||
initial_val = initial_config.get(key)
|
||||
if val != initial_val:
|
||||
details += f"{key} changed from `{initial_val}` to `{val}`\n"
|
||||
return details
|
||||
|
||||
def dialog_did_open(dialog_manager: aqt.DialogManager,
|
||||
dialog_name: str,
|
||||
dialog_instance: QWidget) -> None:
|
||||
if dialog_name != "Preferences":
|
||||
return
|
||||
|
||||
# Make sure defaults are loaded before copying the initial configs
|
||||
dialog_instance.update_global()
|
||||
dialog_instance.update_profile()
|
||||
initial_meta = aqt.mw.pm.meta.copy()
|
||||
initial_profile_conf = aqt.mw.pm.profile.copy()
|
||||
|
||||
def on_preferences_save() -> None:
|
||||
aqt.mw.pm.save = lambda: None
|
||||
|
||||
details = make_config_differences_str(initial_meta, aqt.mw.pm.meta)
|
||||
details += make_config_differences_str(initial_profile_conf,
|
||||
aqt.mw.pm.profile)
|
||||
if not details:
|
||||
return
|
||||
|
||||
message_box = QMessageBox(
|
||||
QMessageBox.Icon.Warning,
|
||||
"NixOS Info",
|
||||
("Anki settings are currently being managed by Home Manager.<br>"
|
||||
"Changes to certain settings won't be saved.")
|
||||
)
|
||||
message_box.setDetailedText(details)
|
||||
message_box.exec()
|
||||
|
||||
aqt.mw.pm.save = on_preferences_save
|
||||
|
||||
def state_will_change(new_state: aqt.main.MainWindowState,
|
||||
old_state: aqt.main.MainWindowState):
|
||||
if new_state != "profileManager":
|
||||
return
|
||||
|
||||
QMessageBox.warning(
|
||||
aqt.mw,
|
||||
"NixOS Info",
|
||||
("Profiles cannot be changed or added while settings are managed with "
|
||||
"Home Manager.")
|
||||
)
|
||||
|
||||
|
||||
# Ensure Anki doesn't try to save to the read-only DB settings file.
|
||||
aqt.mw.pm.save = lambda: None
|
||||
|
||||
# Tell the user when they try to change settings that won't be persisted.
|
||||
aqt.gui_hooks.dialog_manager_did_open_dialog.append(dialog_did_open)
|
||||
|
||||
# Show warning when users try to switch or customize profiles.
|
||||
aqt.gui_hooks.state_will_change.append(state_will_change)
|
||||
'';
|
||||
};
|
||||
}
|
||||
7
tests/modules/programs/anki/default.nix
Normal file
7
tests/modules/programs/anki/default.nix
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{ lib, pkgs, ... }:
|
||||
|
||||
# Anki is currently marked as broken on Darwin (2025/06/23)
|
||||
lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
|
||||
anki-minimal-config = ./minimal-config.nix;
|
||||
anki-full-config = ./full-config.nix;
|
||||
}
|
||||
57
tests/modules/programs/anki/full-config.nix
Normal file
57
tests/modules/programs/anki/full-config.nix
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{ pkgs, ... }:
|
||||
let
|
||||
# This would normally not be a file in the store for security reasons.
|
||||
testPasswordFile = pkgs.writeText "test-password-file" "password";
|
||||
in
|
||||
{
|
||||
programs.anki = {
|
||||
enable = true;
|
||||
addons = [ pkgs.ankiAddons.passfail2 ];
|
||||
answerKeys = [
|
||||
{
|
||||
ease = 1;
|
||||
key = "left";
|
||||
}
|
||||
{
|
||||
ease = 2;
|
||||
key = "up";
|
||||
}
|
||||
];
|
||||
hideBottomBar = true;
|
||||
hideBottomBarMode = "fullscreen";
|
||||
hideTopBar = false;
|
||||
hideTopBarMode = "always";
|
||||
language = "en_US";
|
||||
legacyImportExport = false;
|
||||
minimalistMode = true;
|
||||
reduceMotion = true;
|
||||
spacebarRatesCard = true;
|
||||
style = "native";
|
||||
theme = "dark";
|
||||
uiScale = 1.0;
|
||||
videoDriver = "opengl";
|
||||
sync = {
|
||||
autoSync = true;
|
||||
syncMedia = true;
|
||||
autoSyncMediaMinutes = 15;
|
||||
networkTimeout = 60;
|
||||
url = "http://example.com/anki-sync/";
|
||||
username = "lovelearning@email.com";
|
||||
passwordFile = testPasswordFile;
|
||||
};
|
||||
};
|
||||
|
||||
nmt.script =
|
||||
let
|
||||
ankiBaseDir =
|
||||
if pkgs.stdenv.hostPlatform.isDarwin then
|
||||
"home-files/Library/Application Support/Anki2"
|
||||
else
|
||||
"home-files/.local/share/Anki2";
|
||||
in
|
||||
''
|
||||
assertFileExists "${ankiBaseDir}/prefs21.db"
|
||||
|
||||
assertFileExists "${ankiBaseDir}/gldriver6"
|
||||
'';
|
||||
}
|
||||
18
tests/modules/programs/anki/minimal-config.nix
Normal file
18
tests/modules/programs/anki/minimal-config.nix
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{ pkgs, ... }:
|
||||
{
|
||||
programs.anki = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
nmt.script =
|
||||
let
|
||||
ankiBaseDir =
|
||||
if pkgs.stdenv.hostPlatform.isDarwin then
|
||||
"home-files/Library/Application Support/Anki2"
|
||||
else
|
||||
"home-files/.local/share/Anki2";
|
||||
in
|
||||
''
|
||||
assertFileExists "${ankiBaseDir}/prefs21.db"
|
||||
'';
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue