mirror of
https://github.com/nix-community/home-manager.git
synced 2025-11-08 11:36:05 +01:00
269 lines
9.2 KiB
Nix
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)
|
|
'';
|
|
};
|
|
}
|