Compare commits

...

7 commits

Author SHA1 Message Date
Robert Helgesson a4a4774423
Apply nixfmt 2020-04-26 15:40:37 +02:00
Johannes Rosenberger b0df7d9919
vdirsyncer: fix undefined variable 'mkList'
... at .../modules/programs/vdirsyncer.nix:117:38
change to listString
2020-04-26 15:39:33 +02:00
Johannes Rosenberger 3ff5f6073f
khal: make locale configurable via option 'locale' 2020-04-26 15:32:30 +02:00
Johannes Rosenberger faabf67f93
khal: added options 'color' and 'priority' 2020-04-26 15:32:30 +02:00
Johannes Rosenberger 8e49f65f24
vdirsyncer: allow option userName, disallow userNameCommand
1. account option `userName` is now allowed by `programs.vdirsyncer`

2. The commented out account option `userNameCommand` was required to be set
   by `programs.vdirsyncer` (e.g. as `null`).
   It is now disallowed (commented out) by vdirsyncer.
2020-04-26 15:32:29 +02:00
Robert Helgesson f8ec686211
wip 2020-04-26 15:32:28 +02:00
Sebastian Zivota e8abc2ac53
Add infrastructure for contacts and calendars
This also adds the modules

  - programs.vdirsyncer,
  - programs.khal, and
  - services.vdirsyncer

that integrate with the new infrastructure.

Co-authored-by: Andrew Scott <3648487+ayyjayess@users.noreply.github.com>
Co-authored-by: Sebastian Zivota <sebastian.zivota@mailbox.org>
2020-04-26 15:32:27 +02:00
10 changed files with 1119 additions and 0 deletions

View file

@ -0,0 +1,163 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.accounts.calendar;
localModule = name:
types.submodule {
options = {
path = mkOption {
type = types.str;
default = "${cfg.basePath}/${name}";
defaultText = "accounts.contact.basePath/name";
description = "The path of the storage.";
};
type = mkOption {
type = types.enum [ "filesystem" "singlefile" ];
description = "The type of the storage.";
};
fileExt = mkOption {
type = types.nullOr types.str;
default = null;
description = "The file extension to use.";
};
encoding = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
File encoding for items, both content and file name.
Defaults to UTF-8.
'';
};
};
};
remoteModule = types.submodule {
options = {
type = mkOption {
type = types.enum [ "caldav" "http" "google_calendar" ];
description = "The type of the storage.";
};
url = mkOption {
type = types.nullOr types.str;
default = null;
description = "The URL of the storage.";
};
userName = mkOption {
type = types.nullOr types.str;
default = null;
description = "User name for authentication.";
};
# userNameCommand = mkOption {
# type = types.nullOr (types.listOf types.str);
# default = null;
# example = [ "~/get-username.sh" ];
# description = ''
# A command that prints the user name to standard output.
# '';
# };
passwordCommand = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "pass" "caldav" ];
description = ''
A command that prints the password to standard output.
'';
};
};
};
calendarOpts = { name, config, ... }: {
options = {
name = mkOption {
type = types.str;
readOnly = true;
description = ''
Unique identifier of the calendar. This is set to the
attribute name of the calendar configuration.
'';
};
primary = mkOption {
type = types.bool;
default = false;
description = ''
Whether this is the primary account. Only one account may be
set as primary.
'';
};
primaryCollection = mkOption {
type = types.str;
description = ''
The primary collection of the account. Required when an
account has multiple collections.
'';
};
local = mkOption {
type = types.nullOr (localModule name);
default = null;
description = ''
Local configuration for the calendar.
'';
};
remote = mkOption {
type = types.nullOr remoteModule;
default = null;
description = ''
Remote configuration for the calendar.
'';
};
};
config = { name = name; };
};
in {
options.accounts.calendar = {
basePath = mkOption {
type = types.str;
apply = p:
if hasPrefix "/" p then p else "${config.home.homeDirectory}/${p}";
description = ''
The base directory in which to save calendars. May be a
relative path, in which case it is relative the home
directory.
'';
};
accounts = mkOption {
type = types.attrsOf (types.submodule [
calendarOpts
(import ../programs/vdirsyncer-accounts.nix)
(import ../programs/khal-accounts.nix)
(import ../programs/khal-calendar-accounts.nix)
]);
default = { };
description = "List of calendars.";
};
};
config = mkIf (cfg.accounts != { }) {
assertions = let
primaries =
catAttrs "name" (filter (a: a.primary) (attrValues cfg.accounts));
in [{
assertion = length primaries <= 1;
message = "Must have at most one primary calendar account but found "
+ toString (length primaries) + ", namely "
+ concatStringsSep ", " primaries;
}];
};
}

View file

@ -0,0 +1,134 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.accounts.contact;
localModule = name:
types.submodule {
options = {
path = mkOption {
type = types.str;
default = "${cfg.basePath}/${name}";
defaultText = "accounts.contact.basePath/name";
description = "The path of the storage.";
};
type = mkOption {
type = types.enum [ "filesystem" "singlefile" ];
description = "The type of the storage.";
};
fileExt = mkOption {
type = types.nullOr types.str;
default = null;
description = "The file extension to use.";
};
encoding = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
File encoding for items, both content and file name.
Defaults to UTF-8.
'';
};
};
};
remoteModule = types.submodule {
options = {
type = mkOption {
type = types.enum [ "carddav" "http" "google_contacts" ];
description = "The type of the storage.";
};
url = mkOption {
type = types.nullOr types.str;
default = null;
description = "The URL of the storage.";
};
userName = mkOption {
type = types.nullOr types.str;
default = null;
description = "User name for authentication.";
};
# userNameCommand = mkOption {
# type = types.nullOr (types.listOf types.str);
# default = null;
# example = [ "~/get-username.sh" ];
# description = ''
# A command that prints the user name to standard output.
# '';
# };
passwordCommand = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "pass" "caldav" ];
description = ''
A command that prints the password to standard output.
'';
};
};
};
contactOpts = { name, config, ... }: {
options = {
name = mkOption {
type = types.str;
readOnly = true;
description = ''
Unique identifier of the contact account. This is set to the
attribute name of the contact configuration.
'';
};
local = mkOption {
type = types.nullOr (localModule name);
default = null;
description = ''
Local configuration for the contacts.
'';
};
remote = mkOption {
type = types.nullOr remoteModule;
default = null;
description = ''
Remote configuration for the contacts.
'';
};
};
config = { name = name; };
};
in {
options.accounts.contact = {
basePath = mkOption {
type = types.str;
apply = p:
if hasPrefix "/" p then p else "${config.home.homeDirectory}/${p}";
description = ''
The base directory in which to save contacts. May be a
relative path, in which case it is relative the home
directory.
'';
};
accounts = mkOption {
type = types.attrsOf (types.submodule [
contactOpts
(import ../programs/vdirsyncer-accounts.nix)
(import ../programs/khal-accounts.nix)
]);
default = { };
description = "List of contacts.";
};
};
}

View file

@ -1481,6 +1481,30 @@ in
A new module is available: 'programs.lf' A new module is available: 'programs.lf'
''; '';
} }
{
time = "2020-04-26T13:32:17+00:00";
message = ''
A number of new modules are available:
- 'accounts.calendar',
- 'accounts.contact',
- 'programs.khal',
- 'programs.vdirsyncer', and
- 'services.vdirsyncer' (Linux only).
The two first modules offer a number of options for
configuring calendar and contact accounts. This includes,
for example, information about carddav and caldav servers.
The khal and vdirsyncer modules make use of this new account
infrastructure.
Note, these module are still somewhat experimental and their
structure should not be seen as final, some modifications
may be necessary as new modules are added.
'';
}
]; ];
}; };
} }

View file

@ -22,6 +22,8 @@ let
allModules = [ allModules = [
(loadModule ./accounts/email.nix { }) (loadModule ./accounts/email.nix { })
(loadModule ./accounts/calendar.nix { })
(loadModule ./accounts/contacts.nix { })
(loadModule ./files.nix { }) (loadModule ./files.nix { })
(loadModule ./home-environment.nix { }) (loadModule ./home-environment.nix { })
(loadModule ./manual.nix { }) (loadModule ./manual.nix { })
@ -74,6 +76,7 @@ let
(loadModule ./programs/jq.nix { }) (loadModule ./programs/jq.nix { })
(loadModule ./programs/kakoune.nix { }) (loadModule ./programs/kakoune.nix { })
(loadModule ./programs/keychain.nix { }) (loadModule ./programs/keychain.nix { })
(loadModule ./programs/khal.nix { })
(loadModule ./programs/kitty.nix { }) (loadModule ./programs/kitty.nix { })
(loadModule ./programs/lesspipe.nix { }) (loadModule ./programs/lesspipe.nix { })
(loadModule ./programs/lf.nix { }) (loadModule ./programs/lf.nix { })
@ -107,6 +110,7 @@ let
(loadModule ./programs/texlive.nix { }) (loadModule ./programs/texlive.nix { })
(loadModule ./programs/tmux.nix { }) (loadModule ./programs/tmux.nix { })
(loadModule ./programs/urxvt.nix { }) (loadModule ./programs/urxvt.nix { })
(loadModule ./programs/vdirsyncer.nix { })
(loadModule ./programs/vim.nix { }) (loadModule ./programs/vim.nix { })
(loadModule ./programs/vscode.nix { }) (loadModule ./programs/vscode.nix { })
(loadModule ./programs/vscode/haskell.nix { }) (loadModule ./programs/vscode/haskell.nix { })
@ -161,6 +165,7 @@ let
(loadModule ./services/udiskie.nix { }) (loadModule ./services/udiskie.nix { })
(loadModule ./services/unclutter.nix { }) (loadModule ./services/unclutter.nix { })
(loadModule ./services/unison.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/unison.nix { condition = hostPlatform.isLinux; })
(loadModule ./services/vdirsyncer.nix { condition = hostPlatform.isLinux; })
(loadModule ./services/window-managers/awesome.nix { }) (loadModule ./services/window-managers/awesome.nix { })
(loadModule ./services/window-managers/bspwm/default.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/window-managers/bspwm/default.nix { condition = hostPlatform.isLinux; })
(loadModule ./services/window-managers/i3-sway/i3.nix { }) (loadModule ./services/window-managers/i3-sway/i3.nix { })

View file

@ -0,0 +1,17 @@
{ config, lib, ... }:
with lib;
{
options.khal = {
enable = lib.mkEnableOption "khal access";
readOnly = mkOption {
type = types.bool;
default = false;
description = ''
Keep khal from making any changes to this account.
'';
};
};
}

View file

@ -0,0 +1,58 @@
{ config, lib, ... }:
with lib;
{
options.khal = {
type = mkOption {
type = types.nullOr (types.enum [ "calendar" "discover" ]);
default = null;
description = ''
There is no description of this option.
'';
};
glob = mkOption {
type = types.str;
default = "*";
description = ''
The glob expansion to be searched for events or birthdays when
type is set to discover.
'';
};
color = mkOption {
type = types.nullOr (types.enum [
"black"
"white"
"brown"
"yellow"
"dark gray"
"dark green"
"dark blue"
"light gray"
"light green"
"light blue"
"dark magenta"
"dark cyan"
"dark red"
"light magenta"
"light cyan"
"light red"
]);
default = null;
description = ''
Color in which events in this calendar are displayed.
'';
example = "light green";
};
priority = mkOption {
type = types.int;
default = 10;
description = ''
Priority of a calendar used for coloring.
'';
};
};
}

168
modules/programs/khal.nix Normal file
View file

@ -0,0 +1,168 @@
# khal config loader is sensitive to leading space !
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.programs.khal;
khalCalendarAccounts =
filterAttrs (_: a: a.khal.enable) config.accounts.calendar.accounts;
khalContactAccounts = mapAttrs (_: v: v // { type = "birthdays"; })
(filterAttrs (_: a: a.khal.enable) config.accounts.contact.accounts);
khalAccounts = khalCalendarAccounts // khalContactAccounts;
primaryAccount = findSingle (a: a.primary) null null
(mapAttrsToList (n: v: v // { name = n; }) khalAccounts);
definedAttrs = filterAttrs (_: v: !isNull v);
toKeyValueIfDefined = attrs: generators.toKeyValue { } (definedAttrs attrs);
genCalendarStr = name: value:
concatStringsSep "\n" ([
"[[${name}]]"
"path = ${
value.local.path + "/"
+ (optionalString (value.khal.type == "discover") value.khal.glob)
}"
] ++ optional (value.khal.readOnly) "readonly = True" ++ [
(toKeyValueIfDefined (getAttrs [ "type" "color" "priority" ] value.khal))
] ++ [ "\n" ]);
localeFormatOptions = let T = lib.types;
in mapAttrs (n: v:
v // {
description = v.description + ''
Format strings are for python 'strftime', similarly to man 3 strftime.
'';
}) {
dateformat = {
type = T.str;
default = "%x";
description = ''
khal will display and understand all dates in this format.
'';
};
timeformat = {
type = T.str;
default = "%X";
description = ''
khal will display and understand all times in this format.
'';
};
datetimeformat = {
type = T.str;
default = "%c";
description = ''
khal will display and understand all datetimes in this format.
'';
};
longdateformat = {
type = T.str;
default = "%x";
description = ''
khal will display and understand all dates in this format.
It should contain a year (e.g. %Y).
'';
};
longdatetimeformat = {
type = T.str;
default = "%c";
description = ''
khal will display and understand all datetimes in this format.
It should contain a year (e.g. %Y).
'';
};
};
localeOptions = let T = lib.types;
in localeFormatOptions // {
unicode_symbols = {
type = T.bool;
default = true;
description = ''
By default khal uses some unicode symbols (as in non-ascii) as
indicators for things like repeating events.
If your font, encoding etc. does not support those symbols, set this
to false (this will enable ascii based replacements).
'';
};
default_timezone = {
type = T.nullOr T.str;
default = null;
description = ''
Default for new events or if khal does not understand the timezone
in an ical file.
If 'null', the timezone of your computer will be used.
'';
};
local_timezone = {
type = T.nullOr T.str;
default = null;
description = ''
khal will show all times in this timezone.
If 'null', the timezone of your computer will be used.
'';
};
firstweekday = {
type = T.ints.between 0 6;
default = 0;
description = ''
the first day of the week, where Monday is 0 and Sunday is 6
'';
};
weeknumbers = {
type = T.enum [ "off" "left" "right" ];
default = "off";
description = ''
Enable weeknumbers in calendar and interactive (ikhal) mode.
As those are iso weeknumbers, they only work properly if firstweekday
is set to 0.
'';
};
};
in {
options.programs.khal = {
enable = mkEnableOption "khal, a CLI calendar application";
locale = mkOption {
type = lib.types.submodule {
options = mapAttrs (n: v: mkOption v) localeOptions;
};
description = ''
khal locale settings.
'';
};
};
config = mkIf cfg.enable {
home.packages = [ pkgs.khal ];
xdg.configFile."khal/config".text = concatStringsSep "\n" ([ "[calendars]" ]
++ mapAttrsToList genCalendarStr khalAccounts ++ [
(generators.toINI { } {
locale = definedAttrs (cfg.locale // { _module = null; });
default = optionalAttrs (!isNull primaryAccount) {
default_calendar = if isNull primaryAccount.primaryCollection then
primaryAccount.name
else
primaryAccount.primaryCollection;
};
})
]);
};
}

View file

@ -0,0 +1,187 @@
{ lib, ... }:
with lib;
let
collection = types.either types.str (types.listOf types.str);
in {
options.vdirsyncer = {
enable = mkEnableOption "synchronization using vdirsyncer";
collections = mkOption {
type = types.nullOr (types.listOf collection);
default = null;
description = ''
The collections to synchronize between the storages.
'';
};
conflictResolution = mkOption {
type = types.nullOr
(types.either (types.enum [ "remote wins" "local wins" ])
(types.listOf types.str));
default = null;
description = ''
What to do in case of a conflict between the storages. Either
<literal>remote wins</literal> or
<literal>local wins</literal> or
a list that contains a command to run. By default, an error
message is printed.
'';
};
partialSync = mkOption {
type = types.nullOr (types.enum [ "revert" "error" "ignore" ]);
default = null;
description = ''
What should happen if synchronization in one direction
is impossible due to one storage being read-only.
Defaults to <literal>revert</literal>.
</para><para>
See
<link xlink:href="https://vdirsyncer.pimutils.org/en/stable/config.html#pair-section"/>
for more information.
'';
};
metadata = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "color" "displayname" ];
description = ''
Metadata keys that should be synchronized when vdirsyncer
metasync is executed.
'';
};
timeRange = mkOption {
type = types.nullOr (types.submodule {
options = {
start = mkOption {
type = types.str;
description = "Start of time range to show.";
};
end = mkOption {
type = types.str;
description = "End of time range to show.";
};
};
});
default = null;
description = ''
A time range to synchronize. start and end can be any Python
expression that returns a <literal>datetime.datetime</literal>
object.
'';
example = {
start = "datetime.now() - timedelta(days=365)";
end = "datetime.now() + timedelta(days=365)";
};
};
itemTypes = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = ''
Kinds of items to show. The default is to show everything.
This depends on particular features of the server, the results
are not validated.
'';
};
verify = mkOption {
type = types.nullOr types.bool;
default = null;
description = "Verify SSL certificate.";
};
verifyFingerprint = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Optional. SHA1 or MD5 fingerprint of the expected server certificate.
</para><para>
See
<link xlink:href="https://vdirsyncer.pimutils.org/en/stable/ssl-tutorial.html#ssl-tutorial"/>
for more information.
'';
};
auth = mkOption {
type = types.nullOr (types.enum [ "basic" "digest" "guess" ]);
default = null;
description = ''
Authentication settings. The default is <literal>basic</literal>.
'';
};
authCert = mkOption {
type = types.nullOr (types.either types.str (types.listOf types.str));
default = null;
description = ''
Either a path to a certificate with a client certificate and
the key or a list of paths to the files with them.
'';
};
userAgent = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The user agent to report to the server. Defaults to
<literal>vdirsyncer</literal>.
'';
};
postHook = mkOption {
type = types.lines;
default = "";
description = ''
Command to call for each item creation and modification.
The command will be called with the path of the new/updated
file.
'';
};
## Options for google storages
tokenFile = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
A file path where access tokens are stored.
'';
};
clientIdCommand = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "pass" "client_id" ];
description = ''
A command that prints the OAuth credentials to standard
output.
</para><para>
See
<link xlink:href="https://vdirsyncer.pimutils.org/en/stable/config.html#google"/>
for more information.
'';
};
clientSecretCommand = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "pass" "client_secret" ];
description = ''
A command that prints the OAuth credentials to standard
output.
</para><para>
See
<link xlink:href="https://vdirsyncer.pimutils.org/en/stable/config.html#google"/>
for more information.
'';
};
};
}

View file

@ -0,0 +1,276 @@
{ 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 = pkgs.writeShellScriptBin "post-hook" a.vdirsyncer.postHook
+ "/bin/post-hook";
});
remoteStorage = a:
filterAttrs (_: v: v != null) ((getAttrs [
"type"
"url"
"userName"
#"userNameCommand"
"passwordCommand"
] a.remote) // (if a.vdirsyncer == null then
{ }
else
getAttrs [
"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 == "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 = ${if v then "true" else "false"}"
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.
</para>
<para>For more information, see
<link xlink:href="https://vdirsyncer.pimutils.org/en/stable/config.html#general-section"/>
'';
};
};
};
config = mkIf cfg.enable {
assertions = let
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 = [
"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)) (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;
};
}

View file

@ -0,0 +1,87 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.vdirsyncer;
vdirsyncerOptions = [ ]
++ optional (cfg.verbosity != null) "--verbosity ${cfg.verbosity}"
++ optional (cfg.configFile != null) "--config ${cfg.configFile}";
in {
meta.maintainers = [ maintainers.pjones ];
options.services.vdirsyncer = {
enable = mkEnableOption "vdirsyncer";
package = mkOption {
type = types.package;
default = pkgs.vdirsyncer;
defaultText = "pkgs.vdirsyncer";
example = literalExample "pkgs.vdirsyncer";
description = "The package to use for the vdirsyncer binary.";
};
frequency = mkOption {
type = types.str;
default = "*:0/5";
description = ''
How often to run vdirsyncer. This value is passed to the systemd
timer configuration as the onCalendar option. See
<citerefentry>
<refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum>
</citerefentry>
for more information about the format.
'';
};
verbosity = mkOption {
type = types.nullOr
(types.enum [ "CRITICAL" "ERROR" "WARNING" "INFO" "DEBUG" ]);
default = null;
description = ''
Whether vdirsyncer should produce verbose output.
'';
};
configFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Optional configuration file to link to use instead of
the default file (<filename>$XDG_CONFIG_HOME/vdirsyncer/config</filename>).
'';
};
};
config = mkIf cfg.enable {
systemd.user.services.vdirsyncer = {
Unit = {
Description = "vdirsyncer calendar&contacts synchronization";
PartOf = [ "network-online.target" ];
};
Service = {
Type = "oneshot";
# TODO `vdirsyncer discover`
ExecStart = "${cfg.package}/bin/vdirsyncer sync ${
concatStringsSep " " vdirsyncerOptions
}";
};
};
systemd.user.timers.vdirsyncer = {
Unit = { Description = "vdirsyncer calendar&contacts synchronization"; };
Timer = {
OnCalendar = cfg.frequency;
Unit = "vdirsyncer.service";
};
Install = { WantedBy = [ "timers.target" ]; };
};
};
}