diff --git a/doc/generate_configpanel_doc.py b/doc/generate_configpanel_doc.py
index d51ee38a1..e29a80dbc 100644
--- a/doc/generate_configpanel_doc.py
+++ b/doc/generate_configpanel_doc.py
@@ -82,3 +82,78 @@ for c in OptionClasses:
print("")
print(doc)
print("")
+
+
+print(
+ """
+## Full example
+
+We supposed we have an upstream app with this simple config.yml file:
+```yaml
+title: 'My dummy apps'
+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\]`:
+```toml
+version = "1.0"
+[main]
+
+[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.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"
+```
+
+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.
+
+## Overwrite config panel mechanism
+
+All main configuration helpers are overwritable, example:
+
+```bash
+ynh_app_config_apply() {
+
+ # Stop vpn client
+ touch /tmp/.ynh-vpnclient-stopped
+ systemctl stop ynh-vpnclient
+
+ _ynh_app_config_apply
+
+ # Start vpn client
+ systemctl start ynh-vpnclient
+ rm -f /tmp/.ynh-vpnclient-stopped
+
+}
+```
+
+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)
+"""
+)
diff --git a/doc/generate_options_doc.py b/doc/generate_options_doc.py
index 87ba3c295..cebaa2d8d 100644
--- a/doc/generate_options_doc.py
+++ b/doc/generate_options_doc.py
@@ -132,83 +132,60 @@ print(
"""
----------------
-## Advanced use cases
+## Read and write values: the `bind` property
-### `visible` & `enabled` expression evaluation
+! Config panels only
-Sometimes we may want to conditionaly display a message or prompt for a value, for this we have the `visible` prop.
-And we may want to allow a user to trigger an action only if some condition are met, for this we have the `enabled` prop.
+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.
-Expressions are evaluated against a context containing previous values of the current section's options. This quite limited current design exists because on the web-admin or on the CLI we cannot guarantee that a value will be present in the form if the user queried only a single panel/section/option.
-In the case of an action, the user will be shown or asked for each of the options of the section in which the button is present.
-
-The expression has to be written in javascript (this has been designed for the web-admin first and is converted to python on the fly on the cli).
-
-Available operators are: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()`.
-
-##### Examples
-
-```toml
-# simple "my_option_id" is thruthy/falsy
-visible = "my_option_id"
-visible = "!my_option_id"
-# misc
-visible = "my_value >= 10"
-visible = "-(my_value + 1) < 0"
-visible = "!!my_value || my_other_value"
-```
-For a more complete set of examples, [check the tests at the end of the file](https://github.com/YunoHost/yunohost/blob/dev/src/tests/test_questions.py).
-
-##### match()
-
-For more complex evaluation we can use regex matching.
-
-```toml
-[my_string]
-default = "Lorem ipsum dolor et si qua met!"
-
-[my_boolean]
-type = "boolean"
-visible = "my_string && match(my_string, '^Lorem [ia]psumE?')"
-```
-
-Match the content of a file.
-
-```toml
-[my_file]
-type = "file"
-accept = ".txt"
-bind = "/etc/random/lorem.txt"
-
-[my_boolean]
-type = "boolean"
-visible = "my_file && match(my_file, '^Lorem [ia]psumE?')"
-```
-
-with a file with content like:
-```txt
-Lorem ipsum dolor et si qua met!
-```
-
-
-### `bind`
-
-Config panels only
-
-`bind` allows us to alter the generic behavior of option's values which is: get from and set in the app `settings.yml`.
+`bind` allows us to alter the default behavior of applying option's values, which is: get from and set in the app `settings.yml`.
We can:
-- alter the source the value comes from with getters or binds to file.
-- alter the destination with setters or binds to file.
+- alter the source the value comes from wit binds to file or custom getters.
+- alter the destination with binds to file or settings.
- 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
-----------------
-##### bind to file
+### Read / write into a var of a 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`.
+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]
+# Do not use `file` for this since we only want to insert/save a value
+type = "string"
+bind = ":__FINALPATH__/config.ini"
+default = ""
+```
+
+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"
+
+```
+
+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:
+
+```toml
+bind = "importExportRateLimiting>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`.
@@ -225,142 +202,183 @@ bind = "__FINALPATH__/config.ini"
default = "key: 'value'"
```
-##### bind a value inside a file
+## Advanced use cases
-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`).
+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)
+ * 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
-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
+For all of those use cases, there are the specific getter or setter mechanism for an option!
-Configuration file format supported: `yaml`, `toml`, `json`, `ini`, `env`, `php`, `python`.
-The feature probably works with others formats, but should be tested carefully.
+To create specific getter/setter, you first need to create a `config` script inside the `scripts` directory
-Note that this feature only works with relatively simple cases such as `KEY: VALUE`, but won't properly work with complex data structures like multilin array/lists or dictionnaries.
-It also doesn't work with XML format, custom config function call, php define(), …
+`scripts/config`
+```bash
+#!/bin/bash
+source /usr/share/yunohost/helpers
+ynh_abort_if_errors
-```toml
-[panel.section.config_value]
-# Do not use `file` for this since we only want to insert/save a value
-type = "string"
-bind = ":__FINALPATH__/config.ini"
-default = ""
+# Put your getter, setter, validator or action here
+
+# Keep this last line
+ynh_app_config_run $1
```
-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"
-```
-
-##### Getters
+### 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__`.
+It has to be named after an option's `id` prepended by `get__`.
-To display a custom alert message for example. We setup a base option in `config_panel.toml`.
+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.)
-```toml
-[panel.section.alert]
-type = "alert"
-# bind to "null" to inform there's something in `scripts/config`
-bind = "null"
-# `ask` & `style` will be injected by a custom getter
+
+[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
+}
```
-Then add a custom getter that output yaml, every properties defined here will override any property previously declared.
+config_panel.toml
+```toml
+[main.auth.login_user]
+ask = "Username"
+type = "string"
+```
+[/details]
+[details summary="Advanced example 1 : Display a list of available plugins" class="helper-card-subtitle text-muted"]
+scripts/config
```bash
-get__alert() {
- if [ "$whatever" ]; then
- cat << EOF
+get__plugins() {
+ echo "choices: [$(ls $install_dir/plugins/ | tr '\n' ',')]"
+}
+```
+
+config_panel.toml
+```toml
+[main.plugins.plugins]
+ask = "Plugin to activate"
+type = "tags"
+choices = []
+```
+[/details]
+
+[details summary="Example 2 : Display the status of a VPN" class="helper-card-subtitle text-muted"]
+scripts/config
+```bash
+get__status() {
+ if [ -f "/sys/class/net/tun0/operstate" ] && [ "$(cat /sys/class/net/tun0/operstate)" == "up" ]
+ then
+ cat << EOF
style: success
-ask: Your VPN is running :)
+ask:
+ en: Your VPN is running :)
EOF
else
- cat << EOF
+ cat << EOF
style: danger
-ask: Your VPN is down
+ask:
+ en: Your VPN is down
EOF
fi
}
```
-Or to inject a custom value:
-
+config_panel.toml
```toml
-[panel.section.my_hidden_value]
-type = "number"
-bind = "null"
-# option will act as an hidden variable that can be used in context evaluation
-# (ie: `visible` or `enabled`)
-readonly = true
-visible = false
-# `default` injected by a custom getter
+[main.cube.status]
+ask = "Custom getter alert"
+type = "alert"
+style = "info"
+bind = "null" # no behaviour on
```
+[/details]
-```bash
-get__my_hidden_value() {
- if [ "$whatever" ]; then
- # if only a value is needed
- echo "10"
- else
- # or if we need to override some other props
- # (use `default` or `value` to inject the value)
- cat << EOF
-ask: Here's a number
-visible: true
-default: 0
-EOF
- fi
-}
-```
-##### Setters
+### 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__`.
-```toml
-[panel.section.my_value]
-type = "string"
-bind = "null"
-ask = "gimme complex string"
-```
+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.
+
+
+[details summary="Basic example : Set the login into the first line of a file " class="helper-card-subtitle text-muted"]
+scripts/config
```bash
-set__my_value() {
- if [ -n "$my_value" ]; then
- # split the string into multiple elements or idk
+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
- # To save the value or modified value as a setting:
- ynh_app_setting_set --app=$app --key=my_value --value="$my_value"
}
```
-##### Validators
+config_panel.toml
+```toml
+[main.auth.login_user]
+ask = "Username"
+type = "string"
+```
+[/details]
+
+#### Validation
+
+You will often need to validate data answered by the user before to save it somewhere.
+
+Validation can be made with regex through `pattern` argument
+```toml
+pattern.regexp = '^.+@.+$'
+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"
+```
+
+Some other type specific argument exist like
+| type | validation arguments |
+| ----- | --------------------------- |
+| `number`, `range` | `min`, `max`, `step` |
+| `file` | `accept` |
+| `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__`.
Validators allows us to return custom error messages depending on the value.
-```toml
-[panel.section.my_value]
-type = "string"
-bind = "null"
-ask = "Gimme a long string"
-default = "too short"
-```
-
```bash
-validate__my_value() {
- if [[ "${#my_value}" -lt 12 ]]; then echo 'Too short!'; fi
+validate__login_user() {
+ if [[ "${#login_user}" -lt 4 ]]; then echo 'Too short user login'; fi
}
```
-##### Actions
+### Actions
Define an option's action in a bash script `script/config`.
It has to be named after a `button`'s id prepended by `run__`.
@@ -407,5 +425,61 @@ run__my_action() {
git clone "$my_repo" "$my_repo_name"
}
```
+
+### `visible` & `enabled` expression evaluation
+
+Sometimes we may want to conditionaly display a message or prompt for a value, for this we have the `visible` prop.
+And we may want to allow a user to trigger an action only if some condition are met, for this we have the `enabled` prop.
+
+Expressions are evaluated against a context containing previous values of the current section's options. This quite limited current design exists because on the web-admin or on the CLI we cannot guarantee that a value will be present in the form if the user queried only a single panel/section/option.
+In the case of an action, the user will be shown or asked for each of the options of the section in which the button is present.
+
+The expression has to be written in javascript (this has been designed for the web-admin first and is converted to python on the fly on the cli).
+
+Available operators are: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()`.
+
+#### Examples
+
+```toml
+# simple "my_option_id" is thruthy/falsy
+visible = "my_option_id"
+visible = "!my_option_id"
+# misc
+visible = "my_value >= 10"
+visible = "-(my_value + 1) < 0"
+visible = "!!my_value || my_other_value"
+```
+For a more complete set of examples, [check the tests at the end of the file](https://github.com/YunoHost/yunohost/blob/dev/src/tests/test_questions.py).
+
+#### match()
+
+For more complex evaluation we can use regex matching.
+
+```toml
+[my_string]
+default = "Lorem ipsum dolor et si qua met!"
+
+[my_boolean]
+type = "boolean"
+visible = "my_string && match(my_string, '^Lorem [ia]psumE?')"
+```
+
+Match the content of a file.
+
+```toml
+[my_file]
+type = "file"
+accept = ".txt"
+bind = "/etc/random/lorem.txt"
+
+[my_boolean]
+type = "boolean"
+visible = "my_file && match(my_file, '^Lorem [ia]psumE?')"
+```
+
+with a file with content like:
+```txt
+Lorem ipsum dolor et si qua met!
+```
"""
)
diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py
index 5d3972275..e05d6fc6c 100644
--- a/src/utils/configpanel.py
+++ b/src/utils/configpanel.py
@@ -215,17 +215,36 @@ class PanelModel(ContainerModel):
class ConfigPanelModel(BaseModel):
"""
- Configuration panels are descriptive format in TOML to bind settings to form items so that a user can alter some of the app's configuration without manually editing files from the command line.
+ 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.
+
+ Those panels could also be used as interface generator to extend quickly 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. 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.
- Config panels are structured as a series of panels (that renders as tabs in the web-admin) that contains a series of sections that contains options.
- Options can be directly binded to settings, or captured by custom bash setters/getter, `button` Options can trigger "actions" (e.g. custom bash functions).
+ ### 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!
- - [Learn more about Options](/dev/forms) in their dedicated doc page as those are also used in app install forms.
- - [Check the basic toml example](https://github.com/YunoHost/example_ynh/blob/master/config_panel.toml.example) and the [basic `scripts/config` example](https://github.com/YunoHost/example_ynh/blob/master/scripts/config)
+ 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.
+
+ ### 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
+ ```toml
+ [manual.vpn.server_ip]
+ [advanced.dns.server_ip]
+ ```
+ Indeed the real variable name is server_ip and here you have a conflict.
+
+ ### Options
+
+ [Learn more about Options](/dev/forms) in their dedicated doc page as those are also used in app install forms and core config panels.
+
+ ### YunoHost community examples
+ - [Check the basic example at the end of this doc](#basic-example)
+ - [Check the example_ynh app toml](https://github.com/YunoHost/example_ynh/blob/master/config_panel.toml.example) and the [basic `scripts/config` example](https://github.com/YunoHost/example_ynh/blob/master/scripts/config)
- [Check config panels of other apps](https://grep.app/search?q=version&filter[repo.pattern][0]=YunoHost-Apps&filter[lang][0]=TOML)
- [Check `scripts/config` of other apps](https://grep.app/search?q=ynh_app_config_apply&filter[repo.pattern][0]=YunoHost-Apps&filter[lang][0]=Shell)
diff --git a/src/utils/form.py b/src/utils/form.py
index cd1138931..5e4d7e67c 100644
--- a/src/utils/form.py
+++ b/src/utils/form.py
@@ -374,8 +374,9 @@ class BaseOption(BaseModel):
- if the path starts with `:`, the value be saved as its id's variable/property counterpart
- this only works for first level variables/properties and simple types (no array)
- else the value will be stored as the whole content of the file
- - you can use `__FINALPATH__` in your path to point to dynamic install paths
+ - you can use `__FINALPATH__` or `__INSTALL_DIR__` in your path to point to dynamic install paths
- FIXME are other global variables accessible?
+ - [refer to `bind` doc for explaination and examples](#read-and-write-values-the)
"""
type: OptionType