Configuration sections
This page shows how a plugin reads its own table of config.toml, and how a protocol plugin reads the settings of an account.
1. Write the section class
Section titled “1. Write the section class”A section is a class with a public parameterless constructor and a property for each key, with its default value. The keys are the names of the properties in snake_case: MaxNames is max_names. I built this one against IConfigSection:
public sealed class HelloConfig : IConfigSection{ public string Greeting { get; set; } = "Hello";
public int MaxNames { get; set; } = 3;
public void Validate() { if (MaxNames is < 1 or > 10) { throw new InvalidOperationException("max_names must be between 1 and 10."); } }}IConfigSection is optional. With it, Validate runs after the section is read and throws InvalidOperationException for a value that is missing or wrong. The message names the key, never the value, because the value may come from a command.
2. Read it in RegisterAsync
Section titled “2. Read it in RegisterAsync”Ask IConfigService for the section by the name of its table, and put it in the container so the services of the plugin can ask for it. From PgpPlugin.cs:
var pgp = await config.AddSectionAsync<PgpConfig>("pgp", cancellationToken);container.RegisterInstance(pgp);The user then writes the table in config.toml:
[hello]greeting = "Hi"max_names = 5What AddSectionAsync does
Section titled “What AddSectionAsync does”- It expands every string of that section first:
${NAME}from the environment and#{command}from what a command prints. A command in another section does not run. - A missing section, or a missing file, gives the defaults of the class. The file is never written.
- Each name can be asked for once. A second plugin that asks for the same table gets
The configuration section [<name>] is already in use. - Tilde reads
accounts,logging,jobs,keysanduiitself, before any plugin. The plugins of the program readcache,pgpandscripting.
The OpenPGP plugin and the cache are off until enabled = true: RegisterAsync reads the section, and registers nothing more when it is off. A plugin that runs a program or writes files can do the same.
When the file is wrong
Section titled “When the file is wrong”tcli and tt stop at the first error, with the section, the key and the line. tcli check goes on instead: each error is one line of its report, the section that is wrong keeps its defaults, and the other sections are still read. There, a key that the class does not have is an error too (is not a key of this section.), and so is a table that no plugin asked for.
When you add a section class, add it to the list Sections() of ConfigKeyScannerTests.cs. The test fails when a key that the serializer writes is called unknown by the check.
The settings of an account
Section titled “The settings of an account”A protocol plugin does not have a section of its own. Its factory gets the part of the account it handles as a ProtocolSettings:
| Property | From config.toml |
|---|---|
Protocol |
protocol, which picked the factory |
Credentials |
username and password, or null when there is no username |
Values |
the settings table, every value as a string |
The settings table may hold strings, numbers and booleans. Tilde turns each one into its text with the invariant culture, so port = 993 is "993". Tilde does not check the keys of settings: the plugin reads the ones it knows. The mbox plugin reads one key, path. From MboxSettings.cs:
public static MboxSettings From(ProtocolSettings protocol){ ArgumentNullException.ThrowIfNull(protocol);
if (!protocol.Values.TryGetValue("path", out var path) || string.IsNullOrWhiteSpace(path)) { throw new InvalidOperationException("The mbox account needs a 'path' setting."); }
return new MboxSettings(System.IO.Path.TrimEndingDirectorySeparator(ExpandHome(path)));}And the account that uses it:
[[accounts.mail]]id = "archive"name = "Archive"address = "ada@example.org"
[accounts.mail.store]protocol = "mbox"
[accounts.mail.store.settings]path = "~/Mail/archive"
[accounts.mail.transport]protocol = "smtp"username = "ada@example.org"password = "#{pass show mail/work}"
[accounts.mail.transport.settings]host = "smtp.example.org"The strings of settings are expanded like the rest of accounts, so password = "#{pass show mail/work}" reaches the plugin as the password itself. Never write a value of Credentials or Values to the log.