{ config, lib, pkgs, ... }: with lib; let cfg = config.programs.vdirsyncer; vdirsyncerCalendarAccounts = filterAttrs (_: v: v.vdirsyncer.enable) (mapAttrs' (n: v: nameValuePair ("calendar_" + n) v) config.accounts.calendar.accounts); vdirsyncerContactAccounts = filterAttrs (_: v: v.vdirsyncer.enable) (mapAttrs' (n: v: nameValuePair ("contacts_" + n) v) config.accounts.contact.accounts); vdirsyncerAccounts = vdirsyncerCalendarAccounts // vdirsyncerContactAccounts; wrap = s: ''"${s}"''; listString = l: "[${concatStringsSep ", " l}]"; boolString = b: if b then "true" else "false"; localStorage = a: filterAttrs (_: v: v != null) ((getAttrs [ "type" "fileExt" "encoding" ] a.local) // { path = a.local.path; postHook = if a.vdirsyncer.postHook != null then (pkgs.writeShellScriptBin "post-hook" a.vdirsyncer.postHook + "/bin/post-hook") else null; }); remoteStorage = a: filterAttrs (_: v: v != null) ((getAttrs [ "type" "url" "userName" "passwordCommand" ] a.remote) // (if a.vdirsyncer == null then { } else getAttrs [ "urlCommand" "userNameCommand" "itemTypes" "verify" "verifyFingerprint" "auth" "authCert" "userAgent" "tokenFile" "clientIdCommand" "clientSecretCommand" "timeRange" ] a.vdirsyncer)); pair = a: with a.vdirsyncer; filterAttrs (k: v: k == "collections" || (v != null && v != [ ])) (getAttrs [ "collections" "conflictResolution" "metadata" "partialSync" ] a.vdirsyncer); pairs = mapAttrs (_: v: pair v) vdirsyncerAccounts; localStorages = mapAttrs (_: v: localStorage v) vdirsyncerAccounts; remoteStorages = mapAttrs (_: v: remoteStorage v) vdirsyncerAccounts; optionString = n: v: if (n == "type") then ''type = "${v}"'' else if (n == "path") then ''path = "${v}"'' else if (n == "fileExt") then ''fileext = "${v}"'' else if (n == "encoding") then ''encoding = "${v}"'' else if (n == "postHook") then ''post_hook = "${v}"'' else if (n == "url") then ''url = "${v}"'' else if (n == "urlCommand") then "url.fetch = ${listString (map wrap ([ "command" ] ++ v))}" else if (n == "timeRange") then '' start_date = "${v.start}" end_date = "${v.end}"'' else if (n == "itemTypes") then "item_types = ${listString (map wrap v)}" else if (n == "userName") then ''username = "${v}"'' else if (n == "userNameCommand") then "username.fetch = ${listString (map wrap ([ "command" ] ++ v))}" else if (n == "password") then ''password = "${v}"'' else if (n == "passwordCommand") then "password.fetch = ${listString (map wrap ([ "command" ] ++ v))}" else if (n == "passwordPrompt") then ''password.fetch = ["prompt", "${v}"]'' else if (n == "verify") then ''verify = "${v}"'' else if (n == "verifyFingerprint") then ''verify_fingerprint = "${v}"'' else if (n == "auth") then ''auth = "${v}"'' else if (n == "authCert" && isString (v)) then ''auth_cert = "${v}"'' else if (n == "authCert") then "auth_cert = ${listString (map wrap v)}" else if (n == "userAgent") then ''useragent = "${v}"'' else if (n == "tokenFile") then ''token_file = "${v}"'' else if (n == "clientId") then ''client_id = "${v}"'' else if (n == "clientIdCommand") then "client_id.fetch = ${listString (map wrap ([ "command" ] ++ v))}" else if (n == "clientSecret") then ''client_secret = "${v}"'' else if (n == "clientSecretCommand") then "client_secret.fetch = ${listString (map wrap ([ "command" ] ++ v))}" else if (n == "metadata") then "metadata = ${listString (map wrap v)}" else if (n == "partialSync") then ''partial_sync = "${v}"'' else if (n == "collections") then let contents = map (c: if (isString c) then ''"${c}"'' else listString (map wrap c)) v; in "collections = ${ if ((isNull v) || v == [ ]) then "null" else listString contents }" else if (n == "conflictResolution") then if v == "remote wins" then ''conflict_resolution = "a wins"'' else if v == "local wins" then ''conflict_resolution = "b wins"'' else "conflict_resolution = ${listString (map wrap ([ "command" ] ++ v))}" else throw "Unrecognized option: ${n}"; attrsString = a: concatStringsSep "\n" (mapAttrsToList optionString a); pairString = n: v: '' [pair ${n}] a = "${n}_remote" b = "${n}_local" ${attrsString v} ''; configFile = pkgs.writeText "config" '' [general] status_path = "${cfg.statusPath}" ### Pairs ${concatStringsSep "\n" (mapAttrsToList pairString pairs)} ### Local storages ${concatStringsSep "\n\n" (mapAttrsToList (n: v: "[storage ${n}_local]" + "\n" + attrsString v) localStorages)} ### Remote storages ${concatStringsSep "\n\n" (mapAttrsToList (n: v: "[storage ${n}_remote]" + "\n" + attrsString v) remoteStorages)} ''; in { options = { programs.vdirsyncer = { enable = mkEnableOption "vdirsyncer"; package = mkOption { type = types.package; default = pkgs.vdirsyncer; defaultText = "pkgs.vdirsyncer"; description = '' vdirsyncer package to use. ''; }; statusPath = mkOption { type = types.str; default = "${config.xdg.dataHome}/vdirsyncer/status"; defaultText = "$XDG_DATA_HOME/vdirsyncer/status"; description = '' A directory where vdirsyncer will store some additional data for the next sync. For more information, see the [vdirsyncer manual](https://vdirsyncer.pimutils.org/en/stable/config.html#general-section). ''; }; }; }; config = mkIf cfg.enable { assertions = let mutuallyExclusiveOptions = [ [ "url" "urlCommand" ] [ "userName" "userNameCommand" ] ]; requiredOptions = t: if (t == "caldav" || t == "carddav" || t == "http") then [ "url" ] else if (t == "filesystem") then [ "path" "fileExt" ] else if (t == "singlefile") then [ "path" ] else if (t == "google_calendar" || t == "google_contacts") then [ "tokenFile" "clientId" "clientSecret" ] else throw "Unrecognized storage type: ${t}"; allowedOptions = let remoteOptions = [ "urlCommand" "userName" "userNameCommand" "password" "passwordCommand" "passwordPrompt" "verify" "verifyFingerprint" "auth" "authCert" "userAgent" ]; in t: if (t == "caldav") then [ "timeRange" "itemTypes" ] ++ remoteOptions else if (t == "carddav" || t == "http") then remoteOptions else if (t == "filesystem") then [ "fileExt" "encoding" "postHook" ] else if (t == "singlefile") then [ "encoding" ] else if (t == "google_calendar") then [ "timeRange" "itemTypes" "clientIdCommand" "clientSecretCommand" ] else if (t == "google_contacts") then [ "clientIdCommand" "clientSecretCommand" ] else throw "Unrecognized storage type: ${t}"; assertStorage = n: v: let allowed = allowedOptions v.type ++ (requiredOptions v.type); in mapAttrsToList (a: v': [{ assertion = (elem a allowed); message = '' Storage ${n} is of type ${v.type}. Option ${a} is not allowed for this type. ''; }] ++ (let required = filter (a: !hasAttr "${a}Command" v) (requiredOptions v.type); in map (a: [{ assertion = hasAttr a v; message = '' Storage ${n} is of type ${v.type}, but required option ${a} is not set. ''; }]) required) ++ map (attrs: let defined = attrNames (filterAttrs (n: v: v != null) (genAttrs attrs (a: v.${a} or null))); in { assertion = length defined <= 1; message = "Storage ${n} has mutually exclusive options: ${ concatStringsSep ", " defined }"; }) mutuallyExclusiveOptions) (removeAttrs v [ "type" "_module" ]); storageAssertions = flatten (mapAttrsToList assertStorage localStorages) ++ flatten (mapAttrsToList assertStorage remoteStorages); in storageAssertions; home.packages = [ cfg.package ]; xdg.configFile."vdirsyncer/config".source = configFile; }; }