Add infrastructure for contacts and calendars (#4078)
* 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> wip * 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. * khal: added options 'color' and 'priority' * Apply nixfmt --------- Co-authored-by: Sebastian Zivota <sebastian.zivota@mailbox.org> Co-authored-by: Johannes Rosenberger <johannes.rosenberger@jorsn.eu> Co-authored-by: Johannes Rosenberger <johannes@jorsn.eu> Co-authored-by: Robert Helgesson <robert@rycee.net>
This commit is contained in:
parent
9e37a1b6f9
commit
b01eb1eb3b
163
modules/accounts/calendar.nix
Normal file
163
modules/accounts/calendar.nix
Normal 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;
|
||||
}];
|
||||
};
|
||||
}
|
134
modules/accounts/contacts.nix
Normal file
134
modules/accounts/contacts.nix
Normal 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.";
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1070,6 +1070,30 @@ in
|
|||
A new module is available: 'programs.boxxy'.
|
||||
'';
|
||||
}
|
||||
|
||||
{
|
||||
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.
|
||||
'';
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -110,6 +110,7 @@ let
|
|||
./programs/k9s.nix
|
||||
./programs/kakoune.nix
|
||||
./programs/keychain.nix
|
||||
./programs/khal.nix
|
||||
./programs/kitty.nix
|
||||
./programs/kodi.nix
|
||||
./programs/lazygit.nix
|
||||
|
@ -197,6 +198,7 @@ let
|
|||
./programs/topgrade.nix
|
||||
./programs/translate-shell.nix
|
||||
./programs/urxvt.nix
|
||||
./programs/vdirsyncer.nix
|
||||
./programs/vim.nix
|
||||
./programs/vim-vint.nix
|
||||
./programs/vscode.nix
|
||||
|
|
17
modules/programs/khal-accounts.nix
Normal file
17
modules/programs/khal-accounts.nix
Normal 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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
58
modules/programs/khal-calendar-accounts.nix
Normal file
58
modules/programs/khal-calendar-accounts.nix
Normal 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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
170
modules/programs/khal.nix
Normal file
170
modules/programs/khal.nix
Normal file
|
@ -0,0 +1,170 @@
|
|||
# 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}]]"
|
||||
"highlight_event_days = True"
|
||||
"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;
|
||||
};
|
||||
})
|
||||
]);
|
||||
};
|
||||
}
|
187
modules/programs/vdirsyncer-accounts.nix
Normal file
187
modules/programs/vdirsyncer-accounts.nix
Normal 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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
276
modules/programs/vdirsyncer.nix
Normal file
276
modules/programs/vdirsyncer.nix
Normal 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;
|
||||
};
|
||||
}
|
87
modules/services/vdirsyncer.nix
Normal file
87
modules/services/vdirsyncer.nix
Normal 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 = literalExpression "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" ]; };
|
||||
};
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue