diff --git a/doc/generate_configpanel_doc.py b/doc/generate_configpanel_doc.py
index e29a80dbc..1eb7b5ebb 100644
--- a/doc/generate_configpanel_doc.py
+++ b/doc/generate_configpanel_doc.py
@@ -88,41 +88,41 @@ print(
"""
## Full example
-We supposed we have an upstream app with this simple config.yml file:
+Let's imagine that the upstream app is configured using this simple `config.yml` file stored in the app's install directory (typically `/var/www/$app/config.yml`):
```yaml
-title: 'My dummy apps'
+title: 'My dummy app'
theme: 'white'
max_rate: 10
max_age: 365
```
-We could for example create a simple configuration panel for it like this one, by following the syntax `\[PANEL.SECTION.QUESTION\]`:
+We could for example create a simple configuration panel for it like this one, by following the syntax `[PANEL.SECTION.QUESTION]`:
```toml
version = "1.0"
[main]
-[main.main]
-[main.main.title]
-ask.en = "Title"
-type = "string"
-bind = ":__INSTALL_DIR__/config.yml"
+ [main.main]
+ [main.main.title]
+ ask.en = "Title"
+ type = "string"
+ bind = ":__INSTALL_DIR__/config.yml"
-[main.main.theme]
-ask.en = "Theme"
-type = "select"
-choices = ["white", "dark"]
-bind = ":__INSTALL_DIR__/config.yml"
+ [main.main.theme]
+ ask.en = "Theme"
+ type = "select"
+ choices = ["white", "dark"]
+ bind = ":__INSTALL_DIR__/config.yml"
-[main.limits]
-[main.limits.max_rate]
-ask.en = "Maximum display rate"
-type = "number"
-bind = ":__INSTALL_DIR__/config.yml"
+ [main.limits]
+ [main.limits.max_rate]
+ ask.en = "Maximum display rate"
+ type = "number"
+ bind = ":__INSTALL_DIR__/config.yml"
-[main.limits.max_age]
-ask.en = "Duration of a dummy"
-type = "number"
-bind = ":__INSTALL_DIR__/config.yml"
+ [main.limits.max_age]
+ ask.en = "Duration of a dummy"
+ type = "number"
+ bind = ":__INSTALL_DIR__/config.yml"
```
Here we have created one `main` panel, containing the `main` and `limits` sections, containing questions according to params name of our `config.yml` file. Thanks to the `bind` properties, all those questions are bind to their values in the `config.yml` file.
@@ -147,13 +147,13 @@ ynh_app_config_apply() {
}
```
-List of main configuration helpers
-* ynh_app_config_get
-* ynh_app_config_show
-* ynh_app_config_validate
-* ynh_app_config_apply
-* ynh_app_config_run
+List of main configuration helpers:
+ * `ynh_app_config_get`
+ * `ynh_app_config_show`
+ * `ynh_app_config_validate`
+ * `ynh_app_config_apply`
+ * `ynh_app_config_run`
-More info on this could be found by reading [vpnclient_ynh config script](https://github.com/YunoHost-Apps/vpnclient_ynh/blob/master/scripts/config)
+More info on this can be found by reading [vpnclient_ynh config script](https://github.com/YunoHost-Apps/vpnclient_ynh/blob/master/scripts/config)
"""
)
diff --git a/doc/generate_options_doc.py b/doc/generate_options_doc.py
index 88f6deb20..ea7febe6d 100644
--- a/doc/generate_options_doc.py
+++ b/doc/generate_options_doc.py
@@ -132,62 +132,70 @@ print(
"""
----------------
-## Read and write values: the `bind` property
+## Reading and writing values
! Config panels only
You can read and write values with 2 mechanisms: the `bind` property in the `config_panel.toml` and for complex use cases the getter/setter in a `config` script.
-`bind` allows us to alter the default behavior of applying option's values, which is: get from and set in the app `settings.yml`.
+If you did not define a specific getter/setter (see below), and no `bind` argument was defined, YunoHost will read/write the value from/to the app's `/etc/yunohost/$app/settings.yml` file.
-We can:
+With `bind`, we can:
- alter the source the value comes from with binds to file or custom getters.
-- alter the destination with binds to file or settings.
+- alter the destination with binds to file or custom setters.
- parse/validate the value before destination with validators
-! IMPORTANT: with the exception of `bind = "null"` options, options ids should almost **always** correspond to an app setting initialized / reused during install/upgrade.
+! IMPORTANT: with the exception of `bind = "null"` options, options ids should almost **always** correspond to an app setting initialized/reused during install/upgrade.
Not doing so may result in inconsistencies between the config panel mechanism and the use of ynh_add_config
-### Read / write into a var of a configuration file
+### Read / write into a var of an actual configuration file
Settings usually correspond to key/values in actual app configurations. Hence, a more useful mode is to have `bind = ":FILENAME"` with a colon `:` before. In that case, YunoHost will automagically find a line with `KEY=VALUE` in `FILENAME` (with the adequate separator between `KEY` and `VALUE`).
YunoHost will then use this value for the read/get operation. During write/set operations, YunoHost will overwrite the value in **both** FILENAME and in the app's settings.yml
-Configuration file format supported: `yaml`, `toml`, `json`, `ini`, `env`, `php`, `python`.
+Configuration file format supported: `YAML`, `TOML`, `JSON`, `INI`, `PHP`, `.env`-like, `.py`.
The feature probably works with others formats, but should be tested carefully.
-Note that this feature only works with relatively simple cases such as `KEY: VALUE`, but won't properly work with complex data structures like multiline array/lists or dictionnaries.
-It also doesn't work with XML format, custom config function call, php define(), …
-If you need to save complex/multiline content in a configuration variable, you should do it via a specific getter/setter.
-
```toml
-[panel.section.config_value]
+[main.main.theme]
# Do not use `file` for this since we only want to insert/save a value
type = "string"
-bind = ":__FINALPATH__/config.ini"
-default = ""
+bind = ":__INSTALL_DIR__/config.yml"
+```
+In which case, YunoHost will look for something like a key/value, with the key being `theme`.
+
+If the question id in the config panel (here, `theme`) differs from the key in the actual conf file (let's say it's not `theme` but `css_theme`), then you can write:
+```toml
+[main.main.theme]
+type = "string"
+bind = "css_theme:__FINALPATH__/config.yml"
```
-By default, `bind = ":FILENAME"` will use the option id as `KEY` but the option id may sometime not be the exact same `KEY` name in the configuration file.
-For example, [In pepettes app](https://github.com/YunoHost-Apps/pepettes_ynh/blob/5cc2d3ffd6529cc7356ff93af92dbb6785c3ab9a/conf/settings.py##L11), the python variable is `name` and not `project_name`. In that case, the key name can be specified before the colon `:`.
-
-```toml
-[panel.section.project_name]
-bind = "name:__FINALPATH__/config.ini"
+!!!! Note: This mechanism is quasi language agnostic and will use regexes to find something that looks like a key=value or common variants. However, it does assume that the key and value are stored on the same line. It doesn't support multiline text or file in a variable with this method. If you need to save multiline content in a configuration variable, you should create a custom getter/setter (see below).
+Nested syntax is also supported, which may be useful for example to remove ambiguities about stuff looking like:
+```json
+{
+ "foo": {
+ "max": 123
+ },
+ "bar": {
+ "max": 456
+ }
+}
```
-Sometimes, you want to read and save a value in a variable name that appears several time in the configuration file (for example variables called `max`). The `bind` property allows you to change the value on the variable following a regex in a the file:
+which we can `bind` to using:
```toml
-bind = "importExportRateLimiting>max:__INSTALL_DIR__/conf.json"
+bind = "foo>max:__INSTALL_DIR__/conf.json"
```
### Read / write an entire file
-You can bind a `text` or directly a `file` to a specific file by using `bind = "FILEPATH`.
+Useful when using a question `file` or `text` for which you want to save the raw content directly as a file on the system.
```toml
[panel.section.config_file]
@@ -207,17 +215,14 @@ default = "key: 'value'"
Sometimes the `bind` mechanism is not enough:
* the config file format is not supported (e.g. xml, csv)
* the data is not contained in a config file (e.g. database, directory, web resources...)
- * the data should be writen but not read (e.g. password)
- * the data should be read but not writen (e.g. status information)
+ * the data should be written but not read (e.g. password)
+ * the data should be read but not written (e.g. fetching status information)
* we want to change other things than the value (e.g. the choices list of a select)
* the question answer contains several values to dispatch in several places
* and so on
-For all of those use cases, there are the specific getter or setter mechanism for an option!
+You can create specific getter/setters functions inside the `scripts/config` of your app to customize how the information is read/written.
-To create specific getter/setter, you first need to create a `config` script inside the `scripts` directory
-
-`scripts/config`
```bash
#!/bin/bash
source /usr/share/yunohost/helpers
@@ -232,54 +237,65 @@ ynh_app_config_run $1
### Getters
-Define an option's custom getter in a bash script `script/config`.
-It has to be named after an option's `id` prepended by `get__`.
+A question's getter is the function used to read the current value/state. Custom getters are defined using bash functions called `getter__QUESTION_SHORT_KEY()` which returns data through stdout.
-The function should returns one of these two formats:
- * a raw format, in this case the return is binded directly to the value of the question
- * a yaml format, in this case you can rewrite several properties of your option (like the `style` of an `alert`, the list of `choices` of a `select`, etc.)
+Stdout can generated using one of those formats:
+ 1) either a raw format, in which case the return is binded directly to the value of the question
+ 2) or a yaml format, in this case you dynamically provide properties for your question (for example the `style` of an `alert`, the list of available `choices` of a `select`, etc.)
-[details summary="Basic example : Get the login inside the first line of a file " class="helper-card-subtitle text-muted"]
-scripts/config
-```bash
-get__login_user() {
- if [ -s /etc/openvpn/keys/credentials ]
- then
- echo "$(sed -n 1p /etc/openvpn/keys/credentials)"
- else
- echo ""
- fi
-}
+[details summary="Basic example with raw stdout: get the timezone on the system" class="helper-card-subtitle text-muted"]
+
+`config_panel.toml`
+
+```toml
+[main.main.timezone]
+ask = "Timezone"
+type = "string"
```
-config_panel.toml
-```toml
-[main.auth.login_user]
-ask = "Username"
-type = "string"
+`scripts/config`
+
+```bash
+get__timezone() {
+ echo "$(cat /etc/timezone)"
+}
```
[/details]
-[details summary="Advanced example 1 : Display a list of available plugins" class="helper-card-subtitle text-muted"]
-scripts/config
-```bash
-get__plugins() {
- echo "choices: [$(ls $install_dir/plugins/ | tr '\n' ',')]"
-}
-```
+[details summary="Basic example with yaml-formated stdout : Display a list of available plugins" class="helper-card-subtitle text-muted"]
-config_panel.toml
+`config_panel.toml`
```toml
[main.plugins.plugins]
ask = "Plugin to activate"
type = "tags"
choices = []
```
+
+`scripts/config`
+
+```bash
+get__plugins() {
+ echo "choices: [$(ls $install_dir/plugins/ | tr '\n' ',')]"
+}
+```
+
[/details]
-[details summary="Example 2 : Display the status of a VPN" class="helper-card-subtitle text-muted"]
-scripts/config
+[details summary="Advanced example with yaml-formated stdout : Display the status of a VPN" class="helper-card-subtitle text-muted"]
+
+`config_panel.toml`
+
+```toml
+[main.cube.status]
+ask = "Custom getter alert"
+type = "alert"
+style = "info"
+bind = "null" # no behaviour on
+```
+
+`scripts/config`
```bash
get__status() {
if [ -f "/sys/class/net/tun0/operstate" ] && [ "$(cat /sys/class/net/tun0/operstate)" == "up" ]
@@ -298,50 +314,38 @@ EOF
fi
}
```
-
-config_panel.toml
-```toml
-[main.cube.status]
-ask = "Custom getter alert"
-type = "alert"
-style = "info"
-bind = "null" # no behaviour on
-```
[/details]
### Setters
-Define an option's custom setter in a bash script `script/config`.
-It has to be named after an option's id prepended by `set__`.
+A question's setter is the function used to set new value/state. Custom setters are defined using bash functions called `setter__QUESTION_SHORT_KEY()`. In the context of the setter function, variables named with the various quetion's short keys are avaible ... for example the user-specified date for question `[main.main.theme]` is available as `$theme`.
-This function could access new values defined by the users by using bash variable with the same name as the short key of a question.
-
-You probably should use `ynh_print_info` in order to display info for user about change that has been made to help them to understand a bit what's going.
+When doing non-trivial operations to set a value, you may want to use `ynh_print_info` to inform the admin about what's going on.
-[details summary="Basic example : Set the login into the first line of a file " class="helper-card-subtitle text-muted"]
-scripts/config
-```bash
-set__login_user() {
- if [ -z "${login_user}" ]
- then
- echo "${login_user}" > /etc/openvpn/keys/credentials
- ynh_print_info "The user login has been registered in /etc/openvpn/keys/credentials"
- fi
-}
+[details summary="Basic example : Set the system timezone" class="helper-card-subtitle text-muted"]
+
+`config_panel.toml`
+
+```toml
+[main.main.timezone]
+ask = "Timezone"
+type = "string"
```
-config_panel.toml
-```toml
-[main.auth.login_user]
-ask = "Username"
-type = "string"
+`scripts/config`
+
+```bash
+set__timezone() {
+ echo "$timezone" > /etc/timezone
+ ynh_print_info "The timezone has been changed to $timezone"
+}
```
[/details]
-#### Validation
+### Validation
You will often need to validate data answered by the user before to save it somewhere.
@@ -353,9 +357,9 @@ pattern.error = 'An email is required for this field'
You can also restrict several types with a choices list.
```toml
-choices.option1 = "Plop1"
-choices.option2 = "Plop2"
-choices.option3 = "Plop3"
+choices.foo = "Foo (some explanation)"
+choices.bar = "Bar (moar explanation)"
+choices.loremipsum = "Lorem Ipsum Dolor Sit Amet"
```
Some other type specific argument exist like
@@ -366,15 +370,12 @@ Some other type specific argument exist like
| `boolean` | `yes` `no` |
-If you need more control over validation, you can use custom validators.
-Define an option's custom validator in a bash script `script/config`.
-It has to be named after an option's id prepended by `validate__`.
-
+Finally, if you need specific or multi variable validation, you can use custom validators function.
Validators allows us to return custom error messages depending on the value.
```bash
validate__login_user() {
- if [[ "${#login_user}" -lt 4 ]]; then echo 'Too short user login'; fi
+ if [[ "${#login_user}" -lt 4 ]]; then echo 'User login is too short, should be at least 4 chars'; fi
}
```
diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py
index 4f333cc5a..47c97a808 100644
--- a/src/utils/configpanel.py
+++ b/src/utils/configpanel.py
@@ -217,30 +217,30 @@ class PanelModel(ContainerModel):
class ConfigPanelModel(BaseModel):
"""
- Configuration panels allows instances adminitrators to manage some parameters or runs some actions for which the app's upstream doesn't provide any configuration panels itself. It's a good way to reduce manual change on config files and avoid conflicts on it.
+ Configuration panels allows admins to manage parameters or runs actions for which the upstream's app doesn't provide any appropriate UI itself. It's a good way to reduce manual change on config files and avoid conflicts on it.
- Those panels could also be used as interface generator to extend quickly capabilities of YunoHost (e.g. VPN Client, Hotspost, Borg, etc.).
+ Those panels can also be used to quickly create interfaces that extend the capabilities of YunoHost (e.g. VPN Client, Hotspost, Borg, etc.).
From a packager perspective, this `config_panel.toml` is coupled to the `scripts/config` script, which may be used to define custom getters/setters/validations/actions. However, most use cases should be covered automagically by the core, thus it may not be necessary to define a scripts/config at all!
- ! IMPORTANT: Please: Keep in mind the YunoHost spirit, and try to build your panels in such a way as to expose only really useful parameters, and if there are many of them, to relegate those corresponding to rarer use cases to "Advanced" sub-sections.
+ ! Please: Keep in mind the YunoHost spirit, and try to build your panels in such a way as to expose only really useful, "high-level" parameters, and if there are many of them, to relegate those corresponding to rarer use cases to "Advanced" sub-sections. Keep it simple, focus on common needs, don't expect the admins to have 3 PhDs in computer science.
- ### How does `config_panel.toml` work
- Basically, configuration panels for apps uses at least a `config_panel.toml` at the root of your package. For advanced usecases, this TOML file could also be paired with a `scripts/config` to define custom getters/setters/validators/actions. However, most use cases should be covered automagically by the core, thus it may not be necessary to define a `scripts/config` at all!
+ ### `config_panel.toml`'s principle and general format
+ To create configuration panels for apps, you should at least create a `config_panel.toml` at the root of the package. For more complex cases, this TOML file can be paired with a `config` script inside the scripts directory of your package, which will handle specific controller logic.
- The `config_panel.toml` file describes one or several panels, containing some sections, containing some options generally binded to a params in a configuration file.
+ The `config_panel.toml` describes one or several panels, containing sections, each containing questions generally binded to a params in the app's actual configuration files.
### Options short keys have to be unique
- For performance reasons, questions short keys should be unique in all the `config_panel.toml` file, not just inside its panel or its section.
-
- So you can't have
+ For performance reasons, questions short keys have to be unique in all the `config_panel.toml` file, not just inside its panel or its section. Hence it's not possible to have:
```toml
[manual.vpn.server_ip]
[advanced.dns.server_ip]
```
- Indeed the real variable name is server_ip and here you have a conflict.
+ In which two questions have "real variable name" `is server_ip` and therefore conflict with each other.
- ### Options
+ ! Some short keys are forbidden cause it can interfer with config scripts (`old`, `file_hash`, `types`, `binds`, `formats`, `changed`) and you probably should avoid to use common settings name to avoid to bind your question to this settings (e.g. `id`, `install_time`, `mysql_pwd`, `path`, `domain`, `port`, `db_name`, `current_revision`, `admin`)
+
+ ### Supported questions types and properties
[Learn more about Options](/dev/forms) in their dedicated doc page as those are also used in app install forms and core config panels.