1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-08 19:46:05 +01:00
home-manager/modules/programs/anki/helper.nix
2025-10-16 14:03:28 -05:00

269 lines
9.2 KiB
Nix

{
lib,
config,
pkgs,
...
}:
let
cfg = config.programs.anki;
# Convert Nix `nullOr bool` to Python types.
pyOptionalBool =
val:
if val == null then
"None"
else if val then
"True"
else
"False";
# 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
profile_manager.setLang("${cfg.language}")
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: bool | None = ${pyOptionalBool cfg.hideTopBar}
if hide_top_bar is not None:
profile_manager.set_hide_top_bar(hide_top_bar)
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: bool | None = ${pyOptionalBool cfg.hideBottomBar}
if hide_bottom_bar is not None:
profile_manager.set_hide_bottom_bar(hide_bottom_bar)
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: bool | None = ${pyOptionalBool cfg.reduceMotion}
if reduce_motion is not None:
profile_manager.set_reduce_motion(reduce_motion)
minimalist_mode: bool | None = ${pyOptionalBool cfg.minimalistMode}
if minimalist_mode is not None:
profile_manager.set_minimalist_mode(minimalist_mode)
spacebar_rates_card: bool | None = ${pyOptionalBool cfg.spacebarRatesCard}
if spacebar_rates_card is not None:
profile_manager.set_spacebar_rates_card(spacebar_rates_card)
legacy_import_export: bool | None = ${pyOptionalBool cfg.legacyImportExport}
if legacy_import_export is not None:
profile_manager.set_legacy_import_export(legacy_import_export)
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: bool | None = ${pyOptionalBool cfg.sync.autoSync}
if auto_sync is not None:
profile_manager.profile["autoSync"] = auto_sync
sync_media: bool | None = ${pyOptionalBool cfg.sync.syncMedia}
if sync_media is not None:
profile_manager.profile["syncMedia"] = sync_media
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.keyFile == null then "None" else "Path('${cfg.sync.keyFile}')"
}
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().strip())
if key_file and key_file.exists():
aqt.mw.pm.set_sync_key(key_file.read_text().strip())
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)
'';
};
}