mirror of
https://github.com/nix-community/home-manager
synced 2024-11-24 03:59:46 +01:00
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>
This commit is contained in:
parent
23220d43f3
commit
e8abc2ac53
10 changed files with 977 additions and 0 deletions
177
modules/accounts/calendar.nix
Normal file
177
modules/accounts/calendar.nix
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
|
||||||
|
cfg = config.accounts.calendar;
|
||||||
|
|
||||||
|
localModule = name: types.submodule {
|
||||||
|
options = {
|
||||||
|
path = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "${cfg.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 = mkMerge [
|
||||||
|
{
|
||||||
|
name = name;
|
||||||
|
khal.type = mkOptionDefault null;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
options.accounts.calendar = {
|
||||||
|
basePath = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "${config.home.homeDirectory}/.calendars/";
|
||||||
|
defaultText = "$HOME/.calendars";
|
||||||
|
description = ''
|
||||||
|
The base directory in which to save calendars.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
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 accounts but found "
|
||||||
|
+ toString (length primaries)
|
||||||
|
+ ", namely "
|
||||||
|
+ concatStringsSep ", " primaries;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
}
|
140
modules/accounts/contacts.nix
Normal file
140
modules/accounts/contacts.nix
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
|
||||||
|
cfg = config.accounts.contact;
|
||||||
|
|
||||||
|
localModule = name: types.submodule {
|
||||||
|
options = {
|
||||||
|
path = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "${cfg.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 = mkMerge [
|
||||||
|
{
|
||||||
|
name = name;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
options.accounts.contact = {
|
||||||
|
basePath = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "${config.home.homeDirectory}/.contacts/";
|
||||||
|
defaultText = "$HOME/.contacts";
|
||||||
|
description = ''
|
||||||
|
The base directory in which to save contacts.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
accounts = mkOption {
|
||||||
|
type = types.attrsOf (types.submodule [
|
||||||
|
contactOpts
|
||||||
|
(import ../programs/vdirsyncer-accounts.nix)
|
||||||
|
(import ../programs/khal-accounts.nix)
|
||||||
|
]);
|
||||||
|
default = {};
|
||||||
|
description = "List of contacts.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
config = mkIf (cfg.accounts != {}) {
|
||||||
|
};
|
||||||
|
}
|
|
@ -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.
|
||||||
|
'';
|
||||||
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { })
|
||||||
|
|
26
modules/programs/khal-accounts.nix
Normal file
26
modules/programs/khal-accounts.nix
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{ config, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
{
|
||||||
|
options.khal = {
|
||||||
|
enable = lib.mkEnableOption "khal access";
|
||||||
|
|
||||||
|
readOnly = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
description = ''
|
||||||
|
Keep khal from making any changes to this account.
|
||||||
|
'';
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
glob = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "*";
|
||||||
|
description = ''
|
||||||
|
The glob expansion to be searched for events or birthdays when type
|
||||||
|
is set to discover.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
13
modules/programs/khal-calendar-accounts.nix
Normal file
13
modules/programs/khal-calendar-accounts.nix
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{ config, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
{
|
||||||
|
options.khal = {
|
||||||
|
type = mkOption {
|
||||||
|
type = types.nullOr (types.enum [ "calendar" "discover"]);
|
||||||
|
description = ''
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
64
modules/programs/khal.nix
Normal file
64
modules/programs/khal.nix
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
# 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);
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
options.programs.khal = {
|
||||||
|
enable = mkEnableOption "khal, a CLI calendar application";
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
home.packages = [ pkgs.khal ];
|
||||||
|
|
||||||
|
xdg.configFile."khal/config".text = concatStringsSep "\n" (
|
||||||
|
[
|
||||||
|
"[calendars]"
|
||||||
|
]
|
||||||
|
++ (mapAttrsToList (name: value: concatStringsSep "\n"
|
||||||
|
([
|
||||||
|
''[[${name}]]''
|
||||||
|
''path = ${value.local.path + "/" + (optionalString (value.khal.type == "discover") value.khal.glob)}''
|
||||||
|
]
|
||||||
|
++ optional (value.khal.readOnly) "readonly = True"
|
||||||
|
++ optional (!isNull value.khal.type) "type = ${value.khal.type}"
|
||||||
|
++ ["\n"]
|
||||||
|
)
|
||||||
|
) khalAccounts)
|
||||||
|
++
|
||||||
|
[
|
||||||
|
(generators.toINI {} {
|
||||||
|
default = optionalAttrs (!isNull primaryAccount) {
|
||||||
|
default_calendar = if isNull primaryAccount.primaryCollection then primaryAccount.name else primaryAccount.primaryCollection;
|
||||||
|
};
|
||||||
|
|
||||||
|
locale = {
|
||||||
|
timeformat = "%H:%M";
|
||||||
|
dateformat = "%Y-%m-%d";
|
||||||
|
longdateformat = "%Y-%m-%d";
|
||||||
|
datetimeformat = "%Y-%m-%d %H:%M";
|
||||||
|
longdatetimeformat = "%Y-%m-%d %H:%M";
|
||||||
|
weeknumbers = "right";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
193
modules/programs/vdirsyncer-accounts.nix
Normal file
193
modules/programs/vdirsyncer-accounts.nix
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
{ 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.
|
||||||
|
|
||||||
|
OAuth credentials, obtained from the Google API Manager.</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.
|
||||||
|
|
||||||
|
OAuth credentials, obtained from the Google API Manager.</para>
|
||||||
|
|
||||||
|
<para> See
|
||||||
|
<link xlink:href="https://vdirsyncer.pimutils.org/en/stable/config.html#google"/>
|
||||||
|
for more information.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
245
modules/programs/vdirsyncer.nix
Normal file
245
modules/programs/vdirsyncer.nix
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
{ 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"
|
||||||
|
"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 mkList (map wrapString 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 = ${mkList (map wrapString (["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;
|
||||||
|
};
|
||||||
|
}
|
90
modules/services/vdirsyncer.nix
Normal file
90
modules/services/vdirsyncer.nix
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
{ 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" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue