Modules

What are modules?

Tasks to be performed by Astrality are grouped into so-called modules. These modules are used to define:

Action blocks:
A grouping of actions which is supposed to be performed at a specific time, such as “on Astrality startup”, “on Astrality exit”, or “on event”.
Actions
Tasks to be performed by Astrality, for example compiling templates or running shell commands.
Event listeners
Event listeners can listen to predefined events and trigger the “on event” action block of the module.

You can easily enable and disable modules, making your configuration more modular.

How to define modules

There are two types of places where you can define your modules:

Directly in $ASTRALITY_CONFIG_HOME/modules.yml:
Useful if you don’t have too many modules, and you want to keep everything easily accessible in one file.
In a file named modules.yml within a modules directory:

Useful if you have lots of modules, and want to separate them into separate directories with common responsibilities.

See the documentation for external modules for how to define modules this way.

You can use templating features in modules.yml, since they are compiled at startup with all context values defined in all context.yml files.

Hint

A useful configuration structure is to define modules with “global responsibilities” in $ASTRALITY_CONFIG_HOME/modules.yml, and group the remaining modules in seperate module directories by their categorical responsibilites (for example “terminals”).

Here “global responsibility” means having the responsibility to satisfy the dependecies of several other modules, such as defining context values used in several templates, creating directories, or installing common dependencies.

Module definition syntax

Modules are formated as separate dictionaries placed at the root indentation level of “modules.yml”. The key used will become the module name.

The simplest module, with no specific behaviour, is:

# Source: $ASTRALITY_CONFIG_HOME/modules.yml

my_module:
    enabled: true

Astrality skips parsing any modules which contain the option enabled: false. The default value of enabled is true, so you do not have to specify it.

Module dependencies

You can specify conditionals that must be satisfied in order to consider a module enabled. It can be useful if a module requires certain dependencies in order to work correctly

You can specify module requirements by setting the module option requires equal to a list of dictionaries containing one, or more, of the following keywords:

env:
Environment variable specified as a string. The environment variable must be set in order to consider the module enabled.
installed:
Program name specified as a string. The program name must be invokable through the command line, i.e. available through the $PATH environment variable. You can test this by typing command -v program_name in your shell.
shell:

Shell command specified as a string. The shell command must return a 0 exit code (which defines success), in order to consider the module enabled.

If the shell command uses more than 1 second to return, it will be considered failed. You can change the default timeout by setting the requires_timeout configuration option.

You can also override the default timeout on a case-by-case basis by setting the timeout key to a numeric value (in seconds).

module:

Module dependent on other module(s), specified with the same name syntax as with enabled_modules.

If a module is missing one or more module dependencies, it will be disabled, and an error will be logged.

All specified dependencies must be satisfied in order to enable the module.

For example, if your module depends on the docker shell command, another module named docker-machine, the environment variable $ENABLE_DOCKER being set, and “my_docker_container” existing, you can check this by setting the following requirements:

# Souce: $ASTRALITY_CONFIG_HOME/modules.yml

docker:
    requires:
        - installed: docker
        - module: docker-machine
        - env: ENABLE_DOCKER
        - shell: '[ $(docker ps -a | grep my_docker_container) ]'
          timeout: 10 # seconds

Hint

requires can be useful if you want to use Astrality to manage your dotfiles. You can use module dependencies in order to only compile configuration templates to their respective directories if the dependent application is available on the system. This way, Astrality becomes a “conditional symlinker” for your dotfiles.

Action blocks

When you want to assign tasks for Astrality to perform, you have to define when to perform them. This is done by defining those actions in one of five available action blocks.

on_setup:

Tasks to be performed only once and never again. Can be used for setting up dependencies.

Executed actions are written to $XDG_DATA_HOME/astrality/setup.yml, by default $HOME/.local/share. Execute astrality --reset-setup module_name if you want to re-execute a module’s setup actions during the next run.

on_startup:

Tasks to be performed when Astrality first starts up. Useful for compiling templates that don’t need to change after they have been compiled.

Actions defined outside action blocks are considered to be part of this block.

on_exit:
Tasks to be performed when you kill the Astrality process. Useful for cleaning up any unwanted clutter.
on_event:
Tasks to be performed when the specified module event listener detects a new event. Useful for dynamic behaviour, periodic tasks, and templates that should change during runtime. The on_event block will never be triggered when no module event listener is defined. More on event listeners follows in the next section.
on_modified:

Tasks to be performed when specific files are modified on disk. You specify a set of tasks to performed on a per-file-basis. Useful for quick feedback when editing template files.

Caution

Only files within $ASTRALITY_CONFIG_HOME/**/* are observed for modifications.

If this is an issue for you, please open a GitHub issue!

Demonstration of module action blocks:

module_name:
    ...startup actions (option 1)...

    on_setup:
        ...setup actions...

    on_startup:
        ...startup actions (option 2)...

    on_event:
        ...event actions...

    on_exit:
        ...shutdow actions...

    on_modified:
        some/file/path:
            ...some/file/path modified actions...

Note

On Astrality startup, the on_startup event will be triggered, but not on_event. The on_event event will only be triggered when the event listener detects a new event after Astrality startup.

Actions

Actions are tasks for Astrality to perform, and are placed within action blocks in order to specify when to perform them. These are the available action types:

import_context:
Import a context section from a YAML formatted file. context variables are used as replacement values for placeholders in your templates. See context for more information.
compile:
Compile a specific template or template directory to a target path.
copy:
Copy a specific file or directory to a target path.
symlink:
Create symbolic link(s) pointing to a specific file or directory.
stow:
Combination of compile + copy or compile + symlink, bisected based on filename pattern of files within a content directory.
run:
Execute a shell command, possibly referring to any compiled template and/or the last detected event defined by the module event listener.
trigger:
Perform all actions specified within another action block. With other words, this action appends all the actions within another action block to the actions already specified in the action block. Useful for not having to repeat yourself when you want the same actions to be performed during different events.

Context imports

The simplest way to define context values is to just define their values in $ASTRALITY_CONFIG_HOME/context.yml. Those context values are available for insertion into all your templates.

But you can also import context values from arbitrary YAML files. Among other use cases, this allows you to:

  • Split context definitions into separate files in order to clean up your configuration.
  • Combine context imports with on_event blocks in order to dynamically change how templates compile. This allows quite complex behaviour.

Context imports are defined as a dictionary, or a list of dictionaries, if you need several imports. Use the import_context keyword in an action block of a module.

This is best explained with an example. Let us create a color schemes file:

# Source file: $ASTRALITY_CONFIG_HOME/modules/color_schemes/color_schemes.yml

gruvbox_dark:
    background: 282828
    foreground: ebdbb2

gruvbox_light:
    background: fbf1c7
    foreground: 3c3836

Then let us import the gruvbox dark color scheme into the “colors” context section:

# Source file: $ASTRALITY_CONFIG_HOME/modules.yml

color_scheme:
    on_startup:
        import_context:
            from_path: modules/color_schemes/color_schemes.yml
            from_section: gruvbox_dark
            to_section: colors

This is functionally equivalent to writing the following global context file:

# Source file: $ASTRALITY_CONFIG_HOME/context.yml

colors:
    background: 282828
    foreground: ebdbb2

Hint

You may wonder why you would want to use this kind of redirection when definining context variables. The advantages are:

  • You can now use {{ colors.foreground }} in all your templates instead of {{ gruvbox_dark.foreground }}. Since your templates do not know exactly which color scheme you are using, you can easily change it in the future by editing only one line in modules.yml.
  • You can use import_context in a on_event action block in order to change your colorscheme based on the time of day. Perhaps you want to use “gruvbox light” during daylight, but change to “gruvbox dark” after dusk?

The available attributes for import_context are:

from_path:
A YAML formatted file containing context sections.
from_section: [Optional]

Which context section to import from the file specified in from_path.

If none is specified, all sections defined in from_path will be imported.

to_section: [Optional]

What you want to name the imported context section. If this attribute is omitted, Astrality will use the same name as from_section.

This option will only have an effect if from_section is specified.

Compile templates

Template compilations are defined as a dictionary, or a list of dictionaries, under the compile keyword in an action block of a module.

Each template compilation action has the following available attributes:

content:

Path to either a template file or template directory.

If content is a directory, Astrality will compile all templates recursively to the target directory, preserving the directory hierarchy.

target: [Optional]

Default: Temporary file created by Astrality.

Path which specifies where to put the compiled template.

You can skip this option if you do not care where the compiled template is placed, and what it is named. You can still use the compiled result by writing {template_path} in the rest of your module. This placeholder will be replaced with the absolute path of the compiled template. You can for instance refer to the file in a shell command.

include [Optional]

Default: '(.+)'

Regular expression defining which filenames that are considered to be templates. Useful when content is a directory which contains non-template files. By default Astrality will try to compile all files.

If you specify a capture group, astrality will use the captured string as the target filename. For example, templates: 'template\.(.+)' will match the file “template.kitty.conf” and rename the target to “kitty.conf”.

Hint

You can test your regex here. Astrality uses the capture group with the greatest index.

permissions: [Optional]

Default: Same permissions as the template file.

The file mode (i.e. permission bits) assigned to the compiled template. Given either as a string of octal permissions, such as '755', or as a string of symbolic permissions, such as 'u+x'. This option is passed to the linux shell command chmod. Refer to chmod’s manual for the full details on possible arguments.

Note

The permissions specified in the permissions option are applied on top of the default permissions copied from the template file.

For example, if the template’s permissions are rw-r--r-- (644) and the value of 'ug+x' is supplied for the permissions option, the 644 permissions will first be copied to the resulting compiled file and then chmod ug+x will be applied on top of that to give a resulting permission on the file of rwxr-xr-- (754).

If an invalid value is supplied for the permissions option, only the default permissions are copied to the compiled file.

Here is an example:

# Source file: $ASTRALITY_CONFIG_HOME/modules.yml

desktop:
    compile:
        - content: modules/scripts/executable.sh.template
          target: ${XDG_CONFIG_HOME}/bin/executable.sh
          permissions: 0o555
        - content: modules/desktop/conky_module.template

    run:
        - shell: conky -c {modules/desktop/conky_module.template}
        - shell: polybar bar

Notice that the shell command conky -c {modules/desktop/conky_module.template} is replaced with something like conky -c /tmp/astrality/compiled.conky_module.template.

Note

All relative file paths in modules are interpreted relative to the directory which contains “module.yml” which defines the module.

Copy files

You can copy a file or directory to a target destination. Directories will be recursively copied, leaving non-conflicting files at the target destination intact. The copy action have the following available parameters.

content:

Where to copy from, with other words a path to a file or directory with existing content to be copied.

If content is a directory, Astrality will create an identical directory hierarchy at the target directory path and recursively copy all files.

target:
A path specifying where to copy to. Any non-conflicting files at the target destination will be left alone.
include [Optional]

Default: '(.+)'

Regular expression restricting which filenames that should be copied. By default Astrality will try to copy all files.

If you specify a capture group, astrality will use the captured string as the name for the copied file. For example, include: 'copy\.(.+)' will copy the file “copy.binary.blob” and rename the copy to “binary.blob”.

permissions: [Optional]

Default: Same permissions as the original file(s).

See compilation permissions for more information.

Stow a directory

Often you want to:

  1. Move all content from a directory in your dotfile repository to a specific target directory, while…
  2. Compiling any template according to a consistent naming scheme, and…
  3. Symlink or copy the remaining files which are not templates.

The stow action type allows you to do just that! Stow has the following available parameters:

content:
Path to a directory of mixed content, i.e. both templates and non-templates.
target:
Path to directory where processed content should be placed. Templates will be compiled to target, and the remaining files will be treated according to the non_templates parameter.
templates: [Optional]

Default: 'template\.(.+)'

Regular expression restricting which filenames that should be compiled as templates. By default, Astrality will only compile files named “template.*” and rename the compilation target to “*”.

See the compile action include parameter for more information.

non_templates: [Optional]

Default: 'symlink'

Accepts: symlink, copy, ignore

What to do with files that do not match the templates regex.

permissions: [Optional]

Default: Same permissions as the original file(s).

See compilation permissions for more information.

Here is an example module which compiles all files matching the glob $XDG_CONFIG_HOME/**/*.t, and places the compiled template besides the template, but without the file extension “.t”. It leaves all other files alone:

# Source file: $ASTRALITY_CONFIG_HOME/modules.yml

dotfiles:
    stow:
        content: $XDG_CONFIG_HOME
        target: $XDG_CONFIG_HOME
        templates: '(.+)\.t'
        non_templates: ignore

Run shell commands

You can instruct Astrality to run an arbitrary number of shell commands when different action blocks are triggered. Each shell command is specified as a dictionary. The shell command is specified as a string keyed to shell. Place the commands within a list under the run option of an action block. See the example below.

You can place the following placeholders within your shell commands

{event}:
The last event detected by the module event listener.
{template_path}:
Replaced with the absolute path of the compiled version of the template placed at the path template_path.

Example:

weekday_module:
    event_listener:
        type: weekday

    on_startup:
        run:
            - shell: 'notify-send "You just started Astrality, and the day is {event}"'

    on_event:
        run:
            - shell: 'notify-send "It is now midnight, have a great {event}! I'm creating a notes document for this day."'
            - shell: 'touch ~/notes/notes_for_{event}.txt'

    on_exit:
        run:
            - shell: 'echo "Deleting today's notes!"'
            - shell: 'rm ~/notes/notes_for_{event}.txt'

You can actually place these placeholders in any action type’s string values. Placeholders are replaced at runtime every time an action is triggered.

Warning

template/path must be compiled when an action type with a {template/path} placeholder is executed. Otherwise, Astrality does not know what to replace the placeholder with, so it will leave it alone and log an error instead.

Trigger action blocks

From one action block you can trigger another action block by specifying a trigger action.

Each trigger option is a dictionary with a mandatory block key, on of on_startup, on_event, on_exit, or on_modified. In the case of setting block: on_modified, you have to specify an additional path key indicating which file modification block you want to trigger.

An example of a module using trigger actions:

module_using_triggers:
     event_listener:
         type: weekday

     on_startup:
         run:
             - shell: startup_command

         trigger:
             - block: on_event

     on_event:
         import_context:
             - from_path: contexts/A.yml
               from_section: '{event}'
               to_section: a_stuff

         trigger:
             - block: on_modified
               path: templates/templateA

     on_modified:
         templates/A.template:
             compile:
                 content: templates/A.template

             run: shell_command_dependent_on_templateA

This is equivalent to writing the following module:

module_using_triggers:
    event_listener:
        type: weekday

    on_startup:
        import_context:
            - from_path: contexts/A.yml
              from_section: '{event}'
              to_section: a_stuff

        compile:
            content: templates/templateA

        run:
            - shell: startup_command
            - shell: shell_command_dependent_on_templateA

    on_event:
        import_context:
            from_path: contexts/A.yml
            from_section: '{event}'
            to_section: a_stuff

        compile:
            content: templateA

        run:
            - shell: shell_command_dependent_on_templateA

    on_modified:
        templates/templateA:
            compile:
                content: templates/templateA

            run:
                - shell: shell_command_dependent_on_templateA

Hint

You can use trigger: on_event in the on_startup block in order to consider the event detected on Astrality startup as a new event.

The trigger action can also help you reduce the degree of repetition in your configuration.

The execution order of module actions

The order of action execution is as follows:

  1. context_import for each module.
  2. symlink for each module.
  3. copy for each module.
  4. compile for each module.
  5. stow for each module.
  6. run for each module.

Modules are iterated over from top to bottom such that they appear in modules.yml. This ensures the following invariants:

  • When you compile templates, all context imports have been performed, and are available for placeholder substitution.
  • When you run shell commands, all (non-)templates have been compiled/copied/symlinked, and are available for reference.

Global configuration options for modules

Global configuration options for all your modules are specified in $ASTRALITY_CONFIG_HOME/astrality.yml within a dictionary named modules at root indentation, i.e.:

# Source file: $ASTRALITY_CONFIG_HOME/astrality.yml

modules:
    option1: value1
    option2: value2
    ...

Available modules configuration options:

requires_timeout:

Default: 1

Determines how long Astrality waits for module requirements to exit successfully, given in seconds. If the requirement times out, it will be considered failed.

Useful when requirements are costly to determine, but you still do not want them to time out.

run_timeout:

Default: 0

Determines how long Astrality waits for module run actions to exit, given in seconds.

Useful when you are dependent on shell commands running sequantially.

reprocess_modified_files:

Default: false

If enabled, Astrality will watch for file modifications in $ASTRALITY_CONFIG_HOME. All files that have been compiled or copied to a destination will be recompiled or recopied if they are modified.

Hint

You can have more fine-grained control over exactly what happens when a file is modified by using the on_modified module event. This way you can run shell commands, import context values, and compile arbitrary templates when specific files are modified on disk.

Caution

At the moment, Astrality only watches for file changes recursively within $ASTRALITY_CONFIG_HOME.

modules_directory:

default: modules

Where Astrality looks for externally defined configurations directories.

enabled_modules:

default:

enabled_modules:
    - name: '*'
    - name: '*::*'

A list of modules which you want Astrality to use. By default, Astrality enables all defined modules.

Specifying enabled_modules allows you to define a module without necessarily using it, making configuration switching easy.

Module defined in “$ASTRALITY_CONFIG_HOME/modules.yml”:
name: name_of_module
Module defined in “<modules_directory>/dir_name/modules.yml”:
name: dir_name::name_of_module
Module defined at “github.com/<user>/<repo>/blob/master/modules.yml”:
name: github::<user>/<repo>::name_of_module

You can also use wildcards when specifying enabled modules:

  • name: '*' enables all modules defined in: $ASTRALITY_CONFIG_HOME/modules.yml.
  • name: 'text_editors::* enables all modules defined in: $ASTRALITY_CONFIG_HOME/<modules_directory>/text_editors/modules.yml.
  • name: '*::* enables all modules defined in: $ASTRALITY_CONFIG_HOME/<modules_directory>/*/modules.yml.

Module subdirectories

You can define “external modules” in files named modules.yml placed within separate subdirectories of your modules directory. You can also place context.yml within these directories, and the context values will become available for compilation in all templates.

Astrality compiles enabled modules.yml files with context from all enabled context.yml files before parsing it. This allows you to modify the behaviour of modules based on context, useful if you want to offer configuration options for modules.

  1. Define your modules in $ASTRALITY_CONFIG_HOME/<modules_directory>/directory/modules.yml.
  2. Enable modules from this config file by appending name: directory::module_name to enabled_modules. Alternatively, you can enable all modules defined in a module directory by appending name: directory::* instead.

By default, all module subdirectories are enabled.

Context values defined in context.yml have preference above context values defined in module subdirectories, allowing you to define default context values, while still allowing others to override these values.

Caution

All relative paths and shell commands in external modules are interpreted relative to the external module directory, not $ASTRALITY_CONFIG_HOME. This way it is more portable between different configurations.

GitHub modules

You can share a module directory with others by publishing the module subdirectory to GitHub. Just define modules.yml at the repository root, i.e. where .git exists, and include any dependent files within the repository.

Others can fetch your module by appending name: github::<your_github_username>/<repository> to enabled_modules.

For example enabling the module named module_name defined in modules.yml in the repository at https://github.com/username/repository:

modules:
    enabled_modules:
        - name: github::username/repository::module_name

Astrality will automatically fetch the module on startup and place it within $ASTRALITY_CONFIG_HOME/<modules_directory>/username/repository. If you want to automatically update the GitHub module, you can specify autoupdate: true:

modules:
    enabled_modules:
        - name: github::username/repository::module_name
          autoupdate: true

If module_name is not specified, all modules will be enabled:

modules:
    enabled_modules:
        - name: github::username/repository
          autoupdate: true