├── .coveragerc ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── _static │ ├── README.md │ ├── extra.css │ └── logo.png ├── build-process.md ├── concept.md ├── getting-started.md ├── index.md ├── installation.md └── resources.md ├── examples ├── 0-default │ ├── config │ └── config.d │ │ ├── .gitignore │ │ ├── config.conf │ │ └── i3configger.json ├── 1-partials │ ├── config │ ├── config.d │ │ ├── basic-settings.conf │ │ ├── i3bar.default.conf │ │ ├── i3bar.tpl.conf │ │ ├── i3configger.json │ │ ├── key-bindings.conf │ │ └── mode-resize.conf │ └── i3bar.default.conf ├── 2-bars │ ├── config │ ├── config.d │ │ ├── filler.conf │ │ ├── i3bar.clock.conf │ │ ├── i3bar.full.conf │ │ ├── i3bar.tpl.conf │ │ └── i3configger.json │ ├── i3bar.clock.conf │ └── i3bar.full.conf ├── 3-variables │ ├── config │ ├── config.d │ │ ├── i3bar.default.conf │ │ ├── i3bar.tpl.conf │ │ ├── i3configger.json │ │ └── some-variables.conf │ └── i3bar.default.conf ├── 4-schemes │ ├── config │ └── config.d │ │ ├── .gitignore │ │ ├── .messages.json │ │ ├── i3configger.json │ │ ├── some-key.value1.conf │ │ ├── some-key.value2.conf │ │ └── some-settings.conf └── README.md ├── i3configger ├── __init__.py ├── __main__.py ├── base.py ├── bindings.py ├── build.py ├── cli.py ├── config.py ├── context.py ├── exc.py ├── inotify_simple.py ├── ipc.py ├── message.py ├── partials.py └── watch.py ├── mkdocs.yml ├── release.py ├── setup.py ├── tests ├── examples │ ├── 0-default │ │ └── config │ ├── 1-partials │ │ ├── config │ │ └── i3bar.default.conf │ ├── 2-bars │ │ ├── config │ │ ├── i3bar.clock.conf │ │ └── i3bar.full.conf │ ├── 3-variables │ │ ├── config │ │ └── i3bar.default.conf │ └── 4-schemes │ │ └── config ├── test_build.py ├── test_cli.py ├── test_config.py ├── test_context.py ├── test_message.py └── test_partials.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | ; needs coverage>=5 (and still causes touble in this setup) 3 | ;dynamic_context = test_function 4 | branch = True 5 | source = i3configger 6 | omit = 7 | *__main__.py 8 | */bindings.py 9 | */inotify_simple.py 10 | 11 | [report] 12 | exclude_lines = 13 | pragma: no cover 14 | raise NotImplementedError 15 | 16 | [html] 17 | directory = .coverage-html 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__* 2 | *.py[cod] 3 | build/* 4 | dist/* 5 | archive/* 6 | 7 | site/* 8 | .messages.json 9 | .coverage-html 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | language_version: python3.7 7 | - repo: https://github.com/pre-commit/mirrors-mypy 8 | rev: 'v0.650' 9 | hooks: 10 | - id: mypy 11 | args: [--no-strict-optional, --ignore-missing-imports] 12 | - repo: https://github.com/pre-commit/pre-commit-hooks 13 | rev: v1.2.3 14 | hooks: 15 | - id: end-of-file-fixer 16 | - id: flake8 17 | - id: trailing-whitespace 18 | additional_dependencies: ["flake8-bugbear == 18.2.0"] 19 | language_version: python3.7 20 | - repo: https://github.com/asottile/pyupgrade 21 | rev: v1.2.0 22 | hooks: 23 | - id: pyupgrade 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | sudo: required 3 | language: python 4 | python: "3.7" 5 | cache: pip 6 | git: 7 | depth: 1 8 | branches: 9 | except: 10 | - experimental 11 | install: 12 | - pip install --pre -U tox 13 | jobs: 14 | include: 15 | - stage: lint 16 | script: tox -e lint 17 | - stage: test 18 | script: tox -e test 19 | after_success: 20 | - tox -e coveralls 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.9.1.post1 (Never too late to botch up the changelog) - 2018-12-28 4 | ## Fixed 5 | 6 | * broken changelog 7 | 8 | ## 0.9.1 (Never too late to make stupid mistakes) - 2018-12-28 9 | ## Fixed 10 | 11 | * crash when executing command due to forgotten explicit self 12 | 13 | ## 0.9.0 (A new beginning) - 2018-12-19 14 | ## Added 15 | 16 | * where it makes sense, defaults for command line settings can be changed in i3configger.json (command line overrides settings in config) 17 | * make status command configurable (for refresh) 18 | * off-switch for ipc configuration (for easier testing) 19 | * functional tests for main cli functionality 20 | 21 | ## Changed 22 | 23 | * keep Python version in sync with Arch system Python: test with Python 3.7 24 | * breaking changes in i3configger.json - check examples to see what is different. Easiest way to upgrade is to move your old config to the side, run i3configger to generate a new default config and add your settings from the old config back in. 25 | * terminology: i3status -> i3bar 26 | * terminology: value -> select 27 | * internal modernization and refactoring 28 | 29 | ## Fixed 30 | 31 | * wrong use of reversed in select-next/previous 32 | 33 | ## Removed 34 | 35 | * option for different config has no real use and unnecessarily complicates things 36 | 37 | ## 0.8.0 (Naming things is hard) - 2017-06-20 38 | ## Changed 39 | 40 | * Name for i3bar key is fixed to `i3bar` - no need to be configurable 41 | 42 | ## Fixed 43 | * don't crash if no i3bar config file was yet generated 44 | * terminology i3status -> i3bar 45 | 46 | ## 0.7.7 (It's just getting better and better) - 2017-06-16 47 | ### Added 48 | 49 | - resolve variables with as many levels of indirections as you want 50 | - if resolving fails proper feedback about the failing path is given 51 | - better error handling/notification, when config is broken 52 | - tests for resolving contexts 53 | 54 | ### Fixed 55 | 56 | - watch process does not crash anymore but gives proper feedback 57 | - don't crash if switching without a default in .messages.json 58 | 59 | ## 0.7.6 (The devil is in the detail) - 2017-06-11 60 | ### Changed 61 | 62 | - do not add partial into config if it purely contains set statements 63 | - strip empty lines from beginning and end of partials 64 | 65 | ## 0.7.5 (Time to make an -git AUR?) - 2017-06-11 66 | ### Changed 67 | 68 | - make checks more forgiving if no i3 is installed - for testing a complete run after a PKGBUILD 69 | 70 | ## 0.7.4 (Packaging is fun and good for testing) - 2017-06-11 71 | ### Changed 72 | 73 | - improve ipc handling - fix setting methods too late 74 | 75 | ## 0.7.3 (Do the right thing) - 2017-06-11 76 | ### Fixed 77 | 78 | - use actual partials path for initialization instead of assuming that parent of config path == partials path 79 | 80 | ## 0.7.2 (Need for speed) - 2017-06-11 81 | ### Changed 82 | 83 | - shave off a few hundred precious milliseconds startup time, by moving the very expensive version fetching into a function that is only called, when the version is really needed. 84 | - help the user, when they use a non existing command 85 | - remove unwanted side effects from message 86 | - when config.d already exists, but no i3configger.json exists yet, it is automatically created now 87 | - better examples/tests 88 | 89 | ## 0.7.1 (The great packaging adventure begins) - 2017-06-10 90 | ### Changed 91 | 92 | - (internal) vendor in a different inotify library that makes it easier to package for Archlinux 93 | 94 | ## 0.7.0 (Better safe than sorry) - 2017-06-10 95 | ### Changed 96 | 97 | - always create a backup of the users files if it does not exist already. Do **not** clobber it on subsequent builds to make sure you can always go back to your old files if needed, even if they have no external backups or SCM in place. 98 | 99 | ## 0.6.0 (Command & Conquer) - 2017-06-10 100 | ### Fixed 101 | 102 | - wrong ordering of context merges (set was not working in all cases) 103 | 104 | ### Added 105 | 106 | - new command: shadow - shadow arbitrary entries in `i3configger.json` 107 | - new command: merge - merge a `.json` file into `.messages.json` 108 | - new command: prune - opposite of merge: remove all keys from a given `.json` file in `.messages.json` 109 | 110 | ### Changed 111 | 112 | - renamed file containing frozen messages from `.state.json` to `.messages.json` 113 | 114 | ## 0.5.3 (KISS) - 2017-06-09 115 | ### Changed 116 | 117 | - de-rocket-science release process 118 | - change description of tool 119 | 120 | ## 0.5.2 (Releasing correctly is hard) - 2017-06-09 121 | ### Fixed 122 | 123 | - wrong CHANGELOG :) 124 | 125 | ## 0.5.1 (Maybe I should test more) - 2017-06-09 126 | ### Fixed 127 | 128 | - #4 repair watch and daemon mode 129 | 130 | ## 0.5.0 (Half way there) - 2017-06-08 131 | ### Added 132 | 133 | - proper documentation at http://oliver.bestwalter.de/i3configger/ 134 | - copy user or default config into `config.d` on initialization 135 | 136 | ### Removed 137 | 138 | - end of line comments are not supported anymore (too much bug potential - would need some form of parsing already to make it work -> not worth the fuzz) 139 | 140 | ### Changed 141 | 142 | - comments are not stripped from the build anymore 143 | - notification is off by default: cli arg changed from `--no-notify-` to `--notify` 144 | 145 | ### Fixed 146 | 147 | - checking the config with `i3 -C` did not work because `-c` (small c) was not passed and the passed path to the new config was silently ignored and the active config was checked instead 148 | 149 | ## 0.4.4 (I am not alone) - 2017-06-05 150 | ### Fixed 151 | 152 | - #2 - fails if not using i3status. Fixed by making the refresh call ignore any errors - not nice, just a quick fix. 153 | 154 | ## 0.4.3 - (The Curious Incident of the Dog in the Night-Time) - 2017-06-04 155 | ### Added 156 | 157 | * examples that are used as test cases 158 | 159 | ### Fixed 160 | 161 | * some small fixes regarding selection 162 | 163 | ## 0.4.2 (The answer) - 2017-06-03 164 | ### Basic implementation 165 | 166 | * build main config and one or several i3status configs from the same sources 167 | * variables are handled slightly more intelligently than i3 does it (variables assigned to other variables are resolved) 168 | * end of line comments are possible (removed at build time) 169 | * variables in i3status configs are also resolved (set anywhere in the sources) 170 | * reload or restart i3 when a change has been done (using `i3-msg`) 171 | * notify when new config has been created and activated (using `notify-send`) 172 | * simple way to render partials based on key value pairs in file name 173 | * simple way to change the configuration by sending messages 174 | * build config as one shot script or watch for changes 175 | * send messages to watching i3configger process 176 | * if `i3 -C fails` with the newly rendered config, the old config will be kept, no harm done 177 | 178 | --- 179 | 180 | **Note:** format based on: [Keep a Changelog](http://keepachangelog.com/) project adheres to [Semantic Versioning](http://semver.org/). 181 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Oliver Bestwalter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 20 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include setup.py 4 | include tox.ini 5 | graft docs 6 | graft i3configger 7 | graft tests 8 | 9 | global-exclude __pycache__ 10 | global-exclude *.pyc 11 | global-exclude *.pyo 12 | global-exclude *.pyd 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](http://www.repostatus.org/badges/latest/active.svg)](http://www.repostatus.org/#active) 2 | [![PyPI version](https://badge.fury.io/py/i3configger.svg)](https://pypi.org/project/i3configger/) 3 | [![Build Status](https://travis-ci.org/obestwalter/i3configger.svg?branch=master)](https://travis-ci.org/obestwalter/i3configger) 4 | [![Coverage Status](https://coveralls.io/repos/github/obestwalter/i3configger/badge.svg?branch=master)](https://coveralls.io/github/obestwalter/i3configger?branch=master) 5 | [![Documentation](https://img.shields.io/badge/docs-sure!-brightgreen.svg)](http://oliver.bestwalter.de/i3configger) 6 | [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 7 | 8 | # i3configger 9 | 10 | **Disclaimer:** this is a tool aimed at users who already know how the configuration of [i3](https://i3wm.org) works (as described in the [excellent docs](https://i3wm.org/docs/userguide.html)). `i3configger` is an independent add-on, not directly affiliated with the project and in no way necessary to use i3 productively. 11 | 12 | **NOTE** using `i3configger` will replace your existing config files (`config` and optional status bar configs), but it will move them to `.bak` if no backup exists yet, so that you can easily revert the damage if you want to go back to your old files. 13 | 14 | ## Why? 15 | 16 | I wanted to be able to switch between different color themes and do things like hide the i3bar with a keyboard shortcut. `i3configger` makes this and other dynamic changes possible without changing i3wm itself. 17 | 18 | ## Main characteristics 19 | 20 | * [same config language as i3](https://i3wm.org/docs/userguide.html#configuring) with these enhancements: 21 | * possibility to spread config over several files 22 | * possibility to assign variables to variables 23 | * variables in i3status configs are also resolved (set them anywhere in the sources) 24 | * additional configuration of `i3configger` itself and persistence of changes to the i3 configuration is achieved by sprinkling a bit of json on top of the config files. 25 | * command line driven - activities can be bound to keyboard shortcuts directly or as part of a [binding mode](https://i3wm.org/docs/userguide.html#binding_modes) 26 | 27 | ## How? 28 | 29 | In the end i3wm needs a config file it can cope with and it needs to reload or restart, when something changes. 30 | 31 | This is realized by adding a build step that can be triggered by calling `i3configger` directly or by running it as a \[daemonized\] watcher process that automatically rebuilds and reloads when source files change or messages are sent. 32 | 33 | ## What can I do with it? 34 | 35 | ### Switch between arbitrary "schemes" 36 | 37 | You can switch sub configurations (e.g. different color schemes) that conform with a simple naming convention (`config.d/..conf`, `config.d/..conf`, etc.) by invoking e.g. `i3configger select-next ` or `i3configger select `. 38 | 39 | To get an idea what can be done, have a look at the [examples](https://github.com/obestwalter/i3configger/tree/master/examples) and [read the docs](http://oliver.bestwalter.de/i3configger). 40 | 41 | ### Override any variable 42 | 43 | You can change any variable you have defined in the configuration by invoking `i3configger set `. These changes are persisted not in the config itself but in an additional file. 44 | 45 | See [i3configger docs](http://oliver.bestwalter.de/i3configger/concept/) for a detailed explanation of the concept and other possible commands. 46 | 47 | ### Usage example 48 | 49 | Here is a snippet from an i3 config that uses a mode to alter itself by sending messages to `i3configger`: 50 | 51 | ```text 52 | set $i3cBin ~/.virtualenvs/i3/bin/i3configger 53 | 54 | bindsym $win+w mode "i3configger" 55 | mode "i3configger" { 56 | bindsym Right exec "$i3cBin select-next colors --i3-refresh-msg restart" 57 | bindsym Left exec "$i3cBin select-previous colors --i3-refresh-msg restart" 58 | bindsym Up exec "$i3cBin shadow bars:targets:laptop:mode dock" 59 | bindsym Down exec "$i3cBin shadow bars:targets:laptop:mode hide" 60 | bindsym Return mode "default" 61 | bindsym Escape mode "default" 62 | } 63 | ``` 64 | 65 | **Explanation of the messages used:** 66 | 67 | * `select[...]` integrates different config partials and can therefore make broad changes. In this case for example there are different `colors..conf` partials that activate different color schemes 68 | * `shadow` adds an overlay that in this case changes the mode of the laptop bar between `hide` and `dock` 69 | 70 | ## Installation 71 | 72 | $ pip install i3configger 73 | 74 | See [docs](http://oliver.bestwalter.de/i3configger/installation) For more details and different ways of installation. 75 | -------------------------------------------------------------------------------- /docs/_static/README.md: -------------------------------------------------------------------------------- 1 | * logo downloaded from https://www.iconfinder.com/icons/43650/curtain_window_icon (free for personal use, which this is I guess? If you don't like me using this icon in this context, let me know and I will remove it). 2 | -------------------------------------------------------------------------------- /docs/_static/extra.css: -------------------------------------------------------------------------------- 1 | blockquote { 2 | font-family: "Courier New", Courier, monospace; 3 | background-color: #FFEEFF; 4 | } 5 | 6 | .copyright { 7 | text-align: right; 8 | font-size: 0.8em; 9 | color: #999999; 10 | } 11 | 12 | .copyright > a { 13 | color: #999999; 14 | } 15 | -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obestwalter/i3configger/c981a01f5b89fd37ab5a98af1229819cea305f6a/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/build-process.md: -------------------------------------------------------------------------------- 1 | # Build process 2 | 3 | 1. merge all files that fit the conditions and configuration 4 | 2. read in all lines that fit the pattern ``set $.*`` 5 | 3. parse them into a map key -> value 6 | 4. Resolve all indirect assignments (e.g. ``set $bla $blub``) 7 | 5. merge additional context from `i3configger.json` and `.message.json` 8 | 5. Replace all variables in configs with their values 9 | 6. Write results 10 | 7. If config is valid: replace and reload. If not: leave the old configuration in place 11 | -------------------------------------------------------------------------------- /docs/concept.md: -------------------------------------------------------------------------------- 1 | # Concept 2 | 3 | The configuration is built from so called `partials` in `/config.d`. For very simple usages (just changing some variable for example) it is not even necessary to spread the configuration over several files though. Have a look at the [examples](https://github.com/obestwalter/i3configger/tree/master/examples) to get an idea about how it can be used. 4 | 5 | Changes that are made to the configuration via i3configger messages are not written back to the `partials` but are persisted in a `.messages.json` in the source folder. This file is used to override variables and choose alternative files during build. Deleting that file puts everything back to normal. 6 | 7 | ## Terms 8 | 9 | * **`partials`**: the i3 configuration is built from files in `config.d` that make up the parts of the configuration to be built. They contain exactly what a normal configuration for i3 would contain only spread over several files (including - if used - status bars and their configuration files). 10 | 11 | * **`message`**: when invoking i3configger with positional arguments they constitute a simple message to the configuration, effectively triggering a build with the changes requested in the message. The messages are persisted in `.messages.json`. 12 | 13 | ## Message "mini language" 14 | 15 | A message consists of a command, a key and - depending on the command - a value. 16 | 17 | Make sure you keep your filenames simple and avoid special characters. To be on the safe side always **single quote** values with special characters like color values or variable markers (`$`) - e.g. `i3configger set bar_bg_color '$orange'`. 18 | 19 | ### Commands 20 | 21 | * **`set `** assigns a new value to any variable that is set anywhere in the configuration (note: `` is the variable name **without** leading `$` sign). You can also assign variables to variables with set, just like in the configuration - e.g. `i3configger set someVar '$someOtherVar'` (note the **single quotes** to make sure the shell does not try to resolve the variable before passing it to i3configger). 22 | 23 | * **`select `** chooses a `partial` from a group of alternatives following the naming scheme `..conf`. 24 | 25 | * **`select-next `**, **`select-previous `** chooses the next/previous `partial` from a group of alternatives (in lexical order). 26 | 27 | * **`shadow `** shadows any entry in `i3configger.json` - to address nested dictionaries chain the keys with `:` as separator - e.g. `i3configger shadow bars:targets:laptop:mode dock` will translate into: 28 | ```python 29 | {"bars": {"targets": {"laptop": {"mode": "dock"}}}} 30 | ``` 31 | 32 | * **`merge `** merges the data from a given path into `.messages.json` to make larger changes in one step. If a relative path is given it is relative to `config.d`. An absolute path is used unchanged. 33 | 34 | * **`prune `** the opposite of merge: remove all keys in given file from `.messages.json`. If a relative path is given it is relative to `config.d`. An absolute path is used unchanged. 35 | 36 | ### Special values 37 | 38 | * `del` if passed as value to `set` or `shadow` the key is deleted. E.g. `i3configger set someVar del`, will remove the key `someVar` in `.messages.json->set` 39 | 40 | ## Example: change variables values with `set` 41 | 42 | One example would be to switch aspects of the status bar(s) - for example mode and position: 43 | 44 | A configuration containing: 45 | 46 | ```text 47 | set $bar_mode hidden 48 | set $bar_position top 49 | ... 50 | bar { 51 | ... 52 | mode $bar_mode 53 | position $bar_position 54 | } 55 | ``` 56 | 57 | can be manipulated by invoking: 58 | 59 | ```text 60 | $ i3configger set bar_mode dock 61 | $ i3configger set bar_position bottom 62 | ``` 63 | 64 | ... and the bar is docked after the first command and jumps to the bottom after the second message has been sent. 65 | 66 | This is completely generic. All variables you set anywhere in the configuration can be changed by this. 67 | 68 | ### Warning about variables 69 | 70 | The implementation of this is very simple (it just parses all variables and an optional message into a single dictionary and uses [Python Template String](https://docs.python.org/2/library/string.html#template-strings) to do the substitutions. To i3configger all `$varName` are the same and will be replaced with their value wherever they are. 71 | 72 | This could bite you if you are not aware of that and use regular expressions containing `$` e.g. in [for_window](https://i3wm.org/docs/userguide.html#for_window). So make sure that you do not use an expression where a part containing `$whatever` also matches an existing variable that was assigned with `set $whatever`. 73 | 74 | ## Example: switch between alternatives with `select` 75 | 76 | Bigger changes can be done by switching between `partials`. To realize this there is a simple naming convention for `partials` to mark them as alternatives of which only ever one is integrated into the final configuration. 77 | 78 | The following `partials` form a group of alternatives: 79 | 80 | ~/.i3/config.d/scheme.blue.conf 81 | ~/.i3/config.d/scheme.red.conf 82 | ~/.i3/config.d/scheme.black.conf 83 | 84 | The first part of the file (`scheme`) serves as the key of that group of alternatives and the second part (`blue`, `red`, `black`) is the value that can be chosen. 85 | 86 | To choose a concrete alternative: 87 | 88 | ```text 89 | $ i3configger select scheme red 90 | ``` 91 | 92 | To cycle through these different alternatives: 93 | 94 | ```text 95 | $ i3configger select-next scheme 96 | $ i3configger select-previous scheme 97 | ``` 98 | 99 | How you call your groups and their values is completely up to you, as long as you stick with the naming convention. 100 | 101 | ## Special conventions 102 | 103 | #### Automatic selection for hostname 104 | 105 | At the moment there is one special name that I deem useful to be populated differently, which is `hostname`. If you have `partials` in your `config.d` that follow the scheme `hostname..conf` the one will automatically be chosen that matches your current hostname the value (or none if none matches). 106 | 107 | #### deactivate partials in `config.d` 108 | 109 | If you want to deactivate a partial, you can do that by prepending a `.` to the file name, e.g. `.whatever.conf` or `.whatever.else.conf` are not included in the build even if they reside in your `config.d`. 110 | 111 | ## Bonus track: keep it DRY 112 | 113 | Using i3configger you can also: 114 | 115 | * assign variables to variables (`set $someVar $someOtherVar`) 116 | * Use variables set anywhere in config `partials` in i3bar configuration files 117 | * generate `bar {...}` settings from templates with some extra config. 118 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | Having that out of the way: simply running i3configger once you have it installed should get you set up nicely. 4 | 5 | $ i3configger -vv 6 | 7 | yields something like: 8 | 9 | ```text 10 | 2017-06-08 19:25:49,131 i3configger.main:main:28 INFO: set i3 refresh method to > 11 | 2017-06-08 19:25:49,131 i3configger.base:set_notify_command:54 DEBUG: do not send notifications 12 | 2017-06-08 19:25:49,131 i3configger.config:fetch:159 INFO: read config from /home/oliver/.i3/config.d/i3configger.json 13 | 2017-06-08 19:25:49,132 i3configger.config:fetch:161 DEBUG: use: 14 | {'bars': {'defaults': {'key': 'i3bar', 15 | 'target': '..', 16 | 'template': 'tpl', 17 | 'value': 'default'}, 18 | 'targets': {}}, 19 | 'main': {'target': '../config'}} 20 | 2017-06-08 19:25:49,132 i3configger.config:fetch:159 INFO: read config from /home/oliver/.i3/config.d/.messages.json 21 | 2017-06-08 19:25:49,132 i3configger.config:fetch:161 DEBUG: use: 22 | {'select': {}, 'set': {}} 23 | 2017-06-08 19:25:49,133 i3configger.config:__init__:43 DEBUG: initialized config I3configgerConfig: 24 | {'barTargets': {}, 25 | 'configPath': PosixPath('/home/oliver/.i3/config.d/i3configger.json'), 26 | 'mainTargetPath': PosixPath('/home/oliver/.i3/config'), 27 | 'message': None, 28 | 'partialsPath': PosixPath('/home/oliver/.i3/config.d'), 29 | 'payload': {'bars': {'defaults': {'key': 'i3bar', 30 | 'target': '..', 31 | 'template': 'tpl', 32 | 'value': 'default'}, 33 | 'targets': {}}, 34 | 'main': {'target': '../config'}}, 35 | 'state': {'select': {}, 'set': {}}, 36 | 'statePath': PosixPath('/home/oliver/.i3/config.d/.messages.json')} 37 | 2017-06-08 19:25:49,134 i3configger.partials:select:92 DEBUG: selected: 38 | [Partial(config.conf)] 39 | ``` 40 | 41 | ## What happened? 42 | 43 | * a structure like this has been created in your `i3` folder: 44 | 45 | ```text 46 | 47 | ├── config 48 | ├── config.bak 49 | └── config.d 50 |    ├── .messages.json 51 |    ├── config.conf 52 |    └── i3configger.json 53 | ``` 54 | 55 | * your config has been copied verbatim to `config.d/config.conf` so that you can now turn it into a malleable, chunky i3configger config as you see fit. 56 | 57 | * a new config file has been generated instead of your old config (which should still be basically the same as your old one). 58 | 59 | * a backup of the last config was created with `.bak` 60 | 61 | * `i3configger.json` can be used to do configuration of the status bars. 62 | * `.messages.json` remembers all the messages you have already sent to the configuration 63 | 64 | ## What now? 65 | 66 | Have a look at the [examples](https://github.com/obestwalter/i3configger/tree/master/examples) to get an idea about how you can move towards a more dynamic configuration. 67 | 68 | For a real world example look at [my own i3 config](https://github.com/obestwalter/i3config). Here are the config partials and settings: [.i3/config.d](https://github.com/obestwalter/i3config/tree/master/config.d), from which [config](https://github.com/obestwalter/i3config/tree/master/config) and all `i3bar.*conf` files are built. 69 | 70 | ## Dev mode - watch config folder 71 | 72 | If you are experimenting with the config and want it automatically updated on change: 73 | 74 | run it in the foreground: 75 | 76 | $ i3configger --watch 77 | 78 | run it as a daemon: 79 | 80 | $ i3configger --daemon 81 | 82 | stop the daemon: 83 | 84 | $ i3configger --kill 85 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # i3configger 2 | 3 | **Disclaimer:** this is a tool aimed at users who already know how the configuration of [i3](https://i3wm.org) works (as described in the [excellent docs](https://i3wm.org/docs/userguide.html)). i3configger is an independent add-on, not directly affiliated with the project and in no way necessary to use i3 productively. It is strictly command line oriented and file based using a very slight enhancement of the existing i3 configuration format with some json sprinkled on top. If you are looking for a graphical tool to help you create a configuration, check out the [resources](resources.md). 4 | 5 | **NOTE** using i3configger will replace your existing config files (configs and optional status bar configs), but it will move them to `.bak` if no backup exists yet, so that you can easily revert the damage if you want to go back to your old files. 6 | 7 | ## Why? 8 | 9 | I3 already has a very nice and simple configuration system. i3configger makes it a bit more malleable by making it possible to send "messages" to your configuration to change variables or to switch between alternative sub configurations (e.g. different color schemes). This is done by adding a build step that can be triggered by calling i3configger directly or by running it as a watcher process that automatically rebuilds and reloads when source files change or sending a message. 10 | 11 | ## Detailed Features 12 | 13 | * build main config and one or several i3bar configs from the same sources 14 | * variables are handled slightly more intelligently than i3 does it (variables assigned to other variables are resolved) 15 | * variables in i3bar configs are also resolved (set anywhere in the sources) 16 | * reload or restart i3 when a change has been done (using `i3-msg`) 17 | * notify when new config has been created and activated (using `notify-send`) 18 | * simple way to render partials based on key value pairs in file name 19 | * simple way to change the configuration by sending messages 20 | * build config as one shot script or watch for changes 21 | * send messages to watching i3configger process 22 | * if `i3 -C -c ` fails with the newly rendered config, the old config will be kept, no harm done 23 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | **Note** the code needs at least Python 3.6. I want to play with the new toys :) 4 | 5 | `i3configger` is released on [the Python Package Index](https://pypi.org/project/i3configger/). 6 | 7 | ## Standard way 8 | 9 | The standard installation method is: 10 | 11 | $ pip install i3configger 12 | 13 | or install in the system Python: 14 | 15 | $ sudo pip install i3configger 16 | 17 | ## Install into a virtualenv 18 | 19 | $ python -m venv /path/to/new/venv 20 | $ source /path/to/new/venv/bin/activate 21 | $ pip install i3configger 22 | 23 | see more about venvs in the [Python documentation]( https://docs.python.org/3/library/venv.html). 24 | 25 | ## Install and run from a clone with tox 26 | 27 | [tox](https://tox.readthedocs.io/en/latest/) is a versatile tool to automate testing and development activities. As it also takes care of the management of virtualenvs it can be used to run i3configger in a tox generated virtualenv. tox is usually packaged in a reasonably recent version for most distributions as installable through your package manager as `python-tox`. 28 | 29 | To install i3configger in a tox managed virtualenv and start it in foreground watch mode directly from source, do: 30 | 31 | $ git clone https://github.com/obestwalter/i3configger.git 32 | $ cd i3configger 33 | $ tox -e i3configger -- --watch 34 | -------------------------------------------------------------------------------- /docs/resources.md: -------------------------------------------------------------------------------- 1 | # Resources 2 | 3 | ## I3 official 4 | 5 | * [i3wm](https://i3wm.org/) 6 | * [i3wm reddit group (FAQs)](https://www.reddit.com/r/i3wm/) 7 | * [Archlinux Wiki](https://wiki.archlinux.org/index.php/I3) 8 | 9 | ## Other Tools 10 | 11 | ... from the i3wm ecosystem 12 | 13 | * [online color configurator](https://thomashunter.name/i3-configurator/) 14 | * [j4-make-config (i3-theme)](https://github.com/okraits/j4-make-config) 15 | * [i3-style](https://github.com/acrisci/i3-style) 16 | * [i3ColourChanger](https://github.com/PMunch/i3ColourChanger) 17 | * [i3-manager](https://github.com/erayaydin/i3-manager) 18 | * [i3 session](https://github.com/joepestro/i3session) 19 | -------------------------------------------------------------------------------- /examples/0-default/config: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Built by i3configger /from/some/directory/who/cares (some time after 1972) # 3 | ############################################################################### 4 | 5 | ### config.conf ### 6 | # i3 config file (v4) 7 | # 8 | # Please see http://i3wm.org/docs/userguide.html for a complete reference! 9 | # 10 | # This config file uses keycodes (bindsym) and was written for the QWERTY 11 | # layout. 12 | # 13 | # To get a config file with the same key positions, but for your current 14 | # layout, use the i3-config-wizard 15 | # 16 | 17 | # Font for window titles. Will also be used by the bar unless a different font 18 | # is used in the bar {} block below. 19 | font pango:monospace 8 20 | 21 | # This font is widely installed, provides lots of unicode glyphs, right-to-left 22 | # text rendering and scalability on retina/hidpi displays (thanks to pango). 23 | #font pango:DejaVu Sans Mono 8 24 | 25 | # Before i3 v4.8, we used to recommend this one as the default: 26 | # font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 27 | # The font above is very space-efficient, that is, it looks good, sharp and 28 | # clear in small sizes. However, its unicode glyph coverage is limited, the old 29 | # X core fonts rendering does not support right-to-left and this being a bitmap 30 | # font, it doesn’t scale on retina/hidpi displays. 31 | 32 | # use these keys for focus, movement, and resize directions when reaching for 33 | # the arrows is not convenient 34 | 35 | # use Mouse+Mod1 to drag floating windows to their wanted position 36 | floating_modifier Mod1 37 | 38 | # start a terminal 39 | bindsym Mod1+Return exec i3-sensible-terminal 40 | 41 | # kill focused window 42 | bindsym Mod1+Shift+q kill 43 | 44 | # start dmenu (a program launcher) 45 | bindsym Mod1+d exec dmenu_run 46 | # There also is the (new) i3-dmenu-desktop which only displays applications 47 | # shipping a .desktop file. It is a wrapper around dmenu, so you need that 48 | # installed. 49 | # bindsym Mod1+d exec --no-startup-id i3-dmenu-desktop 50 | 51 | # change focus 52 | bindsym Mod1+j focus left 53 | bindsym Mod1+k focus down 54 | bindsym Mod1+l focus up 55 | bindsym Mod1+semicolon focus right 56 | 57 | # alternatively, you can use the cursor keys: 58 | bindsym Mod1+Left focus left 59 | bindsym Mod1+Down focus down 60 | bindsym Mod1+Up focus up 61 | bindsym Mod1+Right focus right 62 | 63 | # move focused window 64 | bindsym Mod1+Shift+j move left 65 | bindsym Mod1+Shift+k move down 66 | bindsym Mod1+Shift+l move up 67 | bindsym Mod1+Shift+semicolon move right 68 | 69 | # alternatively, you can use the cursor keys: 70 | bindsym Mod1+Shift+Left move left 71 | bindsym Mod1+Shift+Down move down 72 | bindsym Mod1+Shift+Up move up 73 | bindsym Mod1+Shift+Right move right 74 | 75 | # split in horizontal orientation 76 | bindsym Mod1+h split h 77 | 78 | # split in vertical orientation 79 | bindsym Mod1+v split v 80 | 81 | # enter fullscreen mode for the focused container 82 | bindsym Mod1+f fullscreen toggle 83 | 84 | # change container layout (stacked, tabbed, toggle split) 85 | bindsym Mod1+s layout stacking 86 | bindsym Mod1+w layout tabbed 87 | bindsym Mod1+e layout toggle split 88 | 89 | # toggle tiling / floating 90 | bindsym Mod1+Shift+space floating toggle 91 | 92 | # change focus between tiling / floating windows 93 | bindsym Mod1+space focus mode_toggle 94 | 95 | # focus the parent container 96 | bindsym Mod1+a focus parent 97 | 98 | # focus the child container 99 | #bindsym Mod1+d focus child 100 | 101 | # move the currently focused window to the scratchpad 102 | bindsym Mod1+Shift+minus move scratchpad 103 | 104 | # Show the next scratchpad window or hide the focused scratchpad window. 105 | # If there are multiple scratchpad windows, this command cycles through them. 106 | bindsym Mod1+minus scratchpad show 107 | 108 | # switch to workspace 109 | bindsym Mod1+1 workspace 1 110 | bindsym Mod1+2 workspace 2 111 | bindsym Mod1+3 workspace 3 112 | bindsym Mod1+4 workspace 4 113 | bindsym Mod1+5 workspace 5 114 | bindsym Mod1+6 workspace 6 115 | bindsym Mod1+7 workspace 7 116 | bindsym Mod1+8 workspace 8 117 | bindsym Mod1+9 workspace 9 118 | bindsym Mod1+0 workspace 10 119 | 120 | # move focused container to workspace 121 | bindsym Mod1+Shift+1 move container to workspace 1 122 | bindsym Mod1+Shift+2 move container to workspace 2 123 | bindsym Mod1+Shift+3 move container to workspace 3 124 | bindsym Mod1+Shift+4 move container to workspace 4 125 | bindsym Mod1+Shift+5 move container to workspace 5 126 | bindsym Mod1+Shift+6 move container to workspace 6 127 | bindsym Mod1+Shift+7 move container to workspace 7 128 | bindsym Mod1+Shift+8 move container to workspace 8 129 | bindsym Mod1+Shift+9 move container to workspace 9 130 | bindsym Mod1+Shift+0 move container to workspace 10 131 | 132 | # reload the configuration file 133 | bindsym Mod1+Shift+c reload 134 | # restart i3 inplace (preserves your layout/session, can be used to upgrade i3) 135 | bindsym Mod1+Shift+r restart 136 | # exit i3 (logs you out of your X session) 137 | bindsym Mod1+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'" 138 | 139 | # resize window (you can also use the mouse for that) 140 | mode "resize" { 141 | # These bindings trigger as soon as you enter the resize mode 142 | 143 | # Pressing left will shrink the window’s width. 144 | # Pressing right will grow the window’s width. 145 | # Pressing up will shrink the window’s height. 146 | # Pressing down will grow the window’s height. 147 | bindsym j resize shrink width 10 px or 10 ppt 148 | bindsym k resize grow height 10 px or 10 ppt 149 | bindsym l resize shrink height 10 px or 10 ppt 150 | bindsym semicolon resize grow width 10 px or 10 ppt 151 | 152 | # same bindings, but for the arrow keys 153 | bindsym Left resize shrink width 10 px or 10 ppt 154 | bindsym Down resize grow height 10 px or 10 ppt 155 | bindsym Up resize shrink height 10 px or 10 ppt 156 | bindsym Right resize grow width 10 px or 10 ppt 157 | 158 | # back to normal: Enter or Escape 159 | bindsym Return mode "default" 160 | bindsym Escape mode "default" 161 | } 162 | 163 | bindsym Mod1+r mode "resize" 164 | 165 | # Start i3bar to display a workspace bar (plus the system information i3status 166 | # finds out, if available) 167 | bar { 168 | status_command i3status 169 | } 170 | -------------------------------------------------------------------------------- /examples/0-default/config.d/.gitignore: -------------------------------------------------------------------------------- 1 | # i3configger provides defaults for everything, 2 | # so for simple usages a config file is not necessary. 3 | # On first use a config file contatinin the defaults will always be created 4 | i3configger.json 5 | -------------------------------------------------------------------------------- /examples/0-default/config.d/config.conf: -------------------------------------------------------------------------------- 1 | # i3 config file (v4) 2 | # 3 | # Please see http://i3wm.org/docs/userguide.html for a complete reference! 4 | # 5 | # This config file uses keycodes (bindsym) and was written for the QWERTY 6 | # layout. 7 | # 8 | # To get a config file with the same key positions, but for your current 9 | # layout, use the i3-config-wizard 10 | # 11 | 12 | # Font for window titles. Will also be used by the bar unless a different font 13 | # is used in the bar {} block below. 14 | font pango:monospace 8 15 | 16 | # This font is widely installed, provides lots of unicode glyphs, right-to-left 17 | # text rendering and scalability on retina/hidpi displays (thanks to pango). 18 | #font pango:DejaVu Sans Mono 8 19 | 20 | # Before i3 v4.8, we used to recommend this one as the default: 21 | # font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 22 | # The font above is very space-efficient, that is, it looks good, sharp and 23 | # clear in small sizes. However, its unicode glyph coverage is limited, the old 24 | # X core fonts rendering does not support right-to-left and this being a bitmap 25 | # font, it doesn’t scale on retina/hidpi displays. 26 | 27 | # use these keys for focus, movement, and resize directions when reaching for 28 | # the arrows is not convenient 29 | set $up l 30 | set $down k 31 | set $left j 32 | set $right semicolon 33 | 34 | # use Mouse+Mod1 to drag floating windows to their wanted position 35 | floating_modifier Mod1 36 | 37 | # start a terminal 38 | bindsym Mod1+Return exec i3-sensible-terminal 39 | 40 | # kill focused window 41 | bindsym Mod1+Shift+q kill 42 | 43 | # start dmenu (a program launcher) 44 | bindsym Mod1+d exec dmenu_run 45 | # There also is the (new) i3-dmenu-desktop which only displays applications 46 | # shipping a .desktop file. It is a wrapper around dmenu, so you need that 47 | # installed. 48 | # bindsym Mod1+d exec --no-startup-id i3-dmenu-desktop 49 | 50 | # change focus 51 | bindsym Mod1+$left focus left 52 | bindsym Mod1+$down focus down 53 | bindsym Mod1+$up focus up 54 | bindsym Mod1+$right focus right 55 | 56 | # alternatively, you can use the cursor keys: 57 | bindsym Mod1+Left focus left 58 | bindsym Mod1+Down focus down 59 | bindsym Mod1+Up focus up 60 | bindsym Mod1+Right focus right 61 | 62 | # move focused window 63 | bindsym Mod1+Shift+$left move left 64 | bindsym Mod1+Shift+$down move down 65 | bindsym Mod1+Shift+$up move up 66 | bindsym Mod1+Shift+$right move right 67 | 68 | # alternatively, you can use the cursor keys: 69 | bindsym Mod1+Shift+Left move left 70 | bindsym Mod1+Shift+Down move down 71 | bindsym Mod1+Shift+Up move up 72 | bindsym Mod1+Shift+Right move right 73 | 74 | # split in horizontal orientation 75 | bindsym Mod1+h split h 76 | 77 | # split in vertical orientation 78 | bindsym Mod1+v split v 79 | 80 | # enter fullscreen mode for the focused container 81 | bindsym Mod1+f fullscreen toggle 82 | 83 | # change container layout (stacked, tabbed, toggle split) 84 | bindsym Mod1+s layout stacking 85 | bindsym Mod1+w layout tabbed 86 | bindsym Mod1+e layout toggle split 87 | 88 | # toggle tiling / floating 89 | bindsym Mod1+Shift+space floating toggle 90 | 91 | # change focus between tiling / floating windows 92 | bindsym Mod1+space focus mode_toggle 93 | 94 | # focus the parent container 95 | bindsym Mod1+a focus parent 96 | 97 | # focus the child container 98 | #bindsym Mod1+d focus child 99 | 100 | # move the currently focused window to the scratchpad 101 | bindsym Mod1+Shift+minus move scratchpad 102 | 103 | # Show the next scratchpad window or hide the focused scratchpad window. 104 | # If there are multiple scratchpad windows, this command cycles through them. 105 | bindsym Mod1+minus scratchpad show 106 | 107 | # switch to workspace 108 | bindsym Mod1+1 workspace 1 109 | bindsym Mod1+2 workspace 2 110 | bindsym Mod1+3 workspace 3 111 | bindsym Mod1+4 workspace 4 112 | bindsym Mod1+5 workspace 5 113 | bindsym Mod1+6 workspace 6 114 | bindsym Mod1+7 workspace 7 115 | bindsym Mod1+8 workspace 8 116 | bindsym Mod1+9 workspace 9 117 | bindsym Mod1+0 workspace 10 118 | 119 | # move focused container to workspace 120 | bindsym Mod1+Shift+1 move container to workspace 1 121 | bindsym Mod1+Shift+2 move container to workspace 2 122 | bindsym Mod1+Shift+3 move container to workspace 3 123 | bindsym Mod1+Shift+4 move container to workspace 4 124 | bindsym Mod1+Shift+5 move container to workspace 5 125 | bindsym Mod1+Shift+6 move container to workspace 6 126 | bindsym Mod1+Shift+7 move container to workspace 7 127 | bindsym Mod1+Shift+8 move container to workspace 8 128 | bindsym Mod1+Shift+9 move container to workspace 9 129 | bindsym Mod1+Shift+0 move container to workspace 10 130 | 131 | # reload the configuration file 132 | bindsym Mod1+Shift+c reload 133 | # restart i3 inplace (preserves your layout/session, can be used to upgrade i3) 134 | bindsym Mod1+Shift+r restart 135 | # exit i3 (logs you out of your X session) 136 | bindsym Mod1+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'" 137 | 138 | # resize window (you can also use the mouse for that) 139 | mode "resize" { 140 | # These bindings trigger as soon as you enter the resize mode 141 | 142 | # Pressing left will shrink the window’s width. 143 | # Pressing right will grow the window’s width. 144 | # Pressing up will shrink the window’s height. 145 | # Pressing down will grow the window’s height. 146 | bindsym $left resize shrink width 10 px or 10 ppt 147 | bindsym $down resize grow height 10 px or 10 ppt 148 | bindsym $up resize shrink height 10 px or 10 ppt 149 | bindsym $right resize grow width 10 px or 10 ppt 150 | 151 | # same bindings, but for the arrow keys 152 | bindsym Left resize shrink width 10 px or 10 ppt 153 | bindsym Down resize grow height 10 px or 10 ppt 154 | bindsym Up resize shrink height 10 px or 10 ppt 155 | bindsym Right resize grow width 10 px or 10 ppt 156 | 157 | # back to normal: Enter or Escape 158 | bindsym Return mode "default" 159 | bindsym Escape mode "default" 160 | } 161 | 162 | bindsym Mod1+r mode "resize" 163 | 164 | # Start i3bar to display a workspace bar (plus the system information i3status 165 | # finds out, if available) 166 | bar { 167 | status_command i3status 168 | } 169 | -------------------------------------------------------------------------------- /examples/0-default/config.d/i3configger.json: -------------------------------------------------------------------------------- 1 | { 2 | "bars": { 3 | "defaults": { 4 | "target": "..", 5 | "template": "tpl", 6 | "select": "default" 7 | }, 8 | "targets": {} 9 | }, 10 | "main": { 11 | "i3_refresh_msg": "reload", 12 | "status_command": "i3status", 13 | "log": null, 14 | "notify": false, 15 | "target": "../config" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/1-partials/config: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Built by i3configger /from/some/directory/who/cares (some time after 1972) # 3 | ############################################################################### 4 | 5 | ### basic-settings.conf ### 6 | # Font for window titles. Will also be used by the bar unless a different font 7 | # is used in the bar {} block below. 8 | font pango:monospace 8 9 | 10 | # This font is widely installed, provides lots of unicode glyphs, right-to-left 11 | # text rendering and scalability on retina/hidpi displays (thanks to pango). 12 | #font pango:DejaVu Sans Mono 8 13 | 14 | # Before i3 v4.8, we used to recommend this one as the default: 15 | # font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 16 | # The font above is very space-efficient, that is, it looks good, sharp and 17 | # clear in small sizes. However, its unicode glyph coverage is limited, the old 18 | # X core fonts rendering does not support right-to-left and this being a bitmap 19 | # font, it doesn’t scale on retina/hidpi displays. 20 | 21 | # use these keys for focus, movement, and resize directions when reaching for 22 | # the arrows is not convenient 23 | 24 | # use Mouse+Mod1 to drag floating windows to their wanted position 25 | floating_modifier Mod1 26 | 27 | 28 | ### key-bindings.conf ### 29 | # start a terminal 30 | bindsym Mod1+Return exec i3-sensible-terminal 31 | 32 | # kill focused window 33 | bindsym Mod1+Shift+q kill 34 | 35 | # start dmenu (a program launcher) 36 | bindsym Mod1+d exec dmenu_run 37 | # There also is the (new) i3-dmenu-desktop which only displays applications 38 | # shipping a .desktop file. It is a wrapper around dmenu, so you need that 39 | # installed. 40 | # bindsym Mod1+d exec --no-startup-id i3-dmenu-desktop 41 | 42 | # change focus 43 | bindsym Mod1+j focus left 44 | bindsym Mod1+k focus down 45 | bindsym Mod1+l focus up 46 | bindsym Mod1+semicolon focus right 47 | 48 | # alternatively, you can use the cursor keys: 49 | bindsym Mod1+Left focus left 50 | bindsym Mod1+Down focus down 51 | bindsym Mod1+Up focus up 52 | bindsym Mod1+Right focus right 53 | 54 | # move focused window 55 | bindsym Mod1+Shift+j move left 56 | bindsym Mod1+Shift+k move down 57 | bindsym Mod1+Shift+l move up 58 | bindsym Mod1+Shift+semicolon move right 59 | 60 | # alternatively, you can use the cursor keys: 61 | bindsym Mod1+Shift+Left move left 62 | bindsym Mod1+Shift+Down move down 63 | bindsym Mod1+Shift+Up move up 64 | bindsym Mod1+Shift+Right move right 65 | 66 | # split in horizontal orientation 67 | bindsym Mod1+h split h 68 | 69 | # split in vertical orientation 70 | bindsym Mod1+v split v 71 | 72 | # enter fullscreen mode for the focused container 73 | bindsym Mod1+f fullscreen toggle 74 | 75 | # change container layout (stacked, tabbed, toggle split) 76 | bindsym Mod1+s layout stacking 77 | bindsym Mod1+w layout tabbed 78 | bindsym Mod1+e layout toggle split 79 | 80 | # toggle tiling / floating 81 | bindsym Mod1+Shift+space floating toggle 82 | 83 | # change focus between tiling / floating windows 84 | bindsym Mod1+space focus mode_toggle 85 | 86 | # focus the parent container 87 | bindsym Mod1+a focus parent 88 | 89 | # focus the child container 90 | #bindsym Mod1+d focus child 91 | 92 | # move the currently focused window to the scratchpad 93 | bindsym Mod1+Shift+minus move scratchpad 94 | 95 | # Show the next scratchpad window or hide the focused scratchpad window. 96 | # If there are multiple scratchpad windows, this command cycles through them. 97 | bindsym Mod1+minus scratchpad show 98 | 99 | # switch to workspace 100 | bindsym Mod1+1 workspace 1 101 | bindsym Mod1+2 workspace 2 102 | bindsym Mod1+3 workspace 3 103 | bindsym Mod1+4 workspace 4 104 | bindsym Mod1+5 workspace 5 105 | bindsym Mod1+6 workspace 6 106 | bindsym Mod1+7 workspace 7 107 | bindsym Mod1+8 workspace 8 108 | bindsym Mod1+9 workspace 9 109 | bindsym Mod1+0 workspace 10 110 | 111 | # move focused container to workspace 112 | bindsym Mod1+Shift+1 move container to workspace 1 113 | bindsym Mod1+Shift+2 move container to workspace 2 114 | bindsym Mod1+Shift+3 move container to workspace 3 115 | bindsym Mod1+Shift+4 move container to workspace 4 116 | bindsym Mod1+Shift+5 move container to workspace 5 117 | bindsym Mod1+Shift+6 move container to workspace 6 118 | bindsym Mod1+Shift+7 move container to workspace 7 119 | bindsym Mod1+Shift+8 move container to workspace 8 120 | bindsym Mod1+Shift+9 move container to workspace 9 121 | bindsym Mod1+Shift+0 move container to workspace 10 122 | 123 | # reload the configuration file 124 | bindsym Mod1+Shift+c reload 125 | # restart i3 inplace (preserves your layout/session, can be used to upgrade i3) 126 | bindsym Mod1+Shift+r restart 127 | # exit i3 (logs you out of your X session) 128 | bindsym Mod1+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'" 129 | 130 | 131 | ### mode-resize.conf ### 132 | # resize window (you can also use the mouse for that) 133 | mode "resize" { 134 | # These bindings trigger as soon as you enter the resize mode 135 | 136 | # Pressing left will shrink the window’s width. 137 | # Pressing right will grow the window’s width. 138 | # Pressing up will shrink the window’s height. 139 | # Pressing down will grow the window’s height. 140 | bindsym j resize shrink width 10 px or 10 ppt 141 | bindsym k resize grow height 10 px or 10 ppt 142 | bindsym l resize shrink height 10 px or 10 ppt 143 | bindsym semicolon resize grow width 10 px or 10 ppt 144 | 145 | # same bindings, but for the arrow keys 146 | bindsym Left resize shrink width 10 px or 10 ppt 147 | bindsym Down resize grow height 10 px or 10 ppt 148 | bindsym Up resize shrink height 10 px or 10 ppt 149 | bindsym Right resize grow width 10 px or 10 ppt 150 | 151 | # back to normal: Enter or Escape 152 | bindsym Return mode "default" 153 | bindsym Escape mode "default" 154 | } 155 | 156 | bindsym Mod1+r mode "resize" 157 | 158 | 159 | ### i3bar.tpl.conf ### 160 | # Start i3bar to display a workspace bar (plus the system information i3status 161 | # finds out, if available) 162 | bar { 163 | # i3configger note: 164 | # additionally to all variables set in the configuration files 165 | # you can access the settings from i3configger.json here 166 | # They will be added to the context from bars->targets vars 167 | # they are also pre populated by the bars->defaults 168 | status_command i3status -c ~/.i3/i3bar.default.conf 169 | 170 | # NOTE instead of ~/.i3 you could use the target variable from the json 171 | # It is only not used in the example because this doubles as a test 172 | # in CI. 173 | # So this needs some extra work to make it pass also on CI. 174 | } 175 | -------------------------------------------------------------------------------- /examples/1-partials/config.d/basic-settings.conf: -------------------------------------------------------------------------------- 1 | # Font for window titles. Will also be used by the bar unless a different font 2 | # is used in the bar {} block below. 3 | font pango:monospace 8 4 | 5 | # This font is widely installed, provides lots of unicode glyphs, right-to-left 6 | # text rendering and scalability on retina/hidpi displays (thanks to pango). 7 | #font pango:DejaVu Sans Mono 8 8 | 9 | # Before i3 v4.8, we used to recommend this one as the default: 10 | # font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 11 | # The font above is very space-efficient, that is, it looks good, sharp and 12 | # clear in small sizes. However, its unicode glyph coverage is limited, the old 13 | # X core fonts rendering does not support right-to-left and this being a bitmap 14 | # font, it doesn’t scale on retina/hidpi displays. 15 | 16 | # use these keys for focus, movement, and resize directions when reaching for 17 | # the arrows is not convenient 18 | set $up l 19 | set $down k 20 | set $left j 21 | set $right semicolon 22 | 23 | # use Mouse+Mod1 to drag floating windows to their wanted position 24 | floating_modifier Mod1 25 | -------------------------------------------------------------------------------- /examples/1-partials/config.d/i3bar.default.conf: -------------------------------------------------------------------------------- 1 | general { 2 | output_format = "dzen2" 3 | colors = true 4 | interval = 5 5 | } 6 | 7 | order += "ipv6" 8 | order += "disk /" 9 | order += "run_watch DHCP" 10 | order += "run_watch VPNC" 11 | order += "path_exists VPN" 12 | order += "wireless wlan0" 13 | order += "ethernet eth0" 14 | order += "battery 0" 15 | order += "cpu_temperature 0" 16 | order += "load" 17 | order += "tztime local" 18 | order += "tztime berlin" 19 | 20 | wireless wlan0 { 21 | format_up = "W: (%quality at %essid, %bitrate) %ip" 22 | format_down = "W: down" 23 | } 24 | 25 | ethernet eth0 { 26 | # if you use %speed, i3status requires the cap_net_admin capability 27 | format_up = "E: %ip (%speed)" 28 | format_down = "E: down" 29 | } 30 | 31 | battery 0 { 32 | format = "%status %percentage %remaining %emptytime" 33 | format_down = "No battery" 34 | status_chr = "⚡ CHR" 35 | status_bat = "🔋 BAT" 36 | status_unk = "? UNK" 37 | status_full = "☻ FULL" 38 | path = "/sys/class/power_supply/BAT%d/uevent" 39 | low_threshold = 10 40 | } 41 | 42 | run_watch DHCP { 43 | pidfile = "/var/run/dhclient*.pid" 44 | } 45 | 46 | run_watch VPNC { 47 | # file containing the PID of a vpnc process 48 | pidfile = "/var/run/vpnc/pid" 49 | } 50 | 51 | path_exists VPN { 52 | # path exists when a VPN tunnel launched by nmcli/nm-applet is active 53 | path = "/proc/sys/net/ipv4/conf/tun0" 54 | } 55 | 56 | tztime local { 57 | format = "%Y-%m-%d %H:%M:%S" 58 | } 59 | 60 | tztime berlin { 61 | format = "%Y-%m-%d %H:%M:%S %Z" 62 | timezone = "Europe/Berlin" 63 | } 64 | 65 | load { 66 | format = "%5min" 67 | } 68 | 69 | cpu_temperature 0 { 70 | format = "T: %degrees °C" 71 | path = "/sys/devices/platform/coretemp.0/temp1_input" 72 | } 73 | 74 | disk "/" { 75 | format = "%free" 76 | } 77 | -------------------------------------------------------------------------------- /examples/1-partials/config.d/i3bar.tpl.conf: -------------------------------------------------------------------------------- 1 | # Start i3bar to display a workspace bar (plus the system information i3status 2 | # finds out, if available) 3 | bar { 4 | # i3configger note: 5 | # additionally to all variables set in the configuration files 6 | # you can access the settings from i3configger.json here 7 | # They will be added to the context from bars->targets vars 8 | # they are also pre populated by the bars->defaults 9 | status_command i3status -c ~/.i3/i3bar.$select.conf 10 | 11 | # NOTE instead of ~/.i3 you could use the target variable from the json 12 | # It is only not used in the example because this doubles as a test 13 | # in CI. 14 | # So this needs some extra work to make it pass also on CI. 15 | } 16 | -------------------------------------------------------------------------------- /examples/1-partials/config.d/i3configger.json: -------------------------------------------------------------------------------- 1 | { 2 | "bars": { 3 | "defaults": { 4 | "target": "..", 5 | "template": "tpl" 6 | }, 7 | "targets": { 8 | "single-bar": { 9 | "select": "default" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/1-partials/config.d/key-bindings.conf: -------------------------------------------------------------------------------- 1 | # start a terminal 2 | bindsym Mod1+Return exec i3-sensible-terminal 3 | 4 | # kill focused window 5 | bindsym Mod1+Shift+q kill 6 | 7 | # start dmenu (a program launcher) 8 | bindsym Mod1+d exec dmenu_run 9 | # There also is the (new) i3-dmenu-desktop which only displays applications 10 | # shipping a .desktop file. It is a wrapper around dmenu, so you need that 11 | # installed. 12 | # bindsym Mod1+d exec --no-startup-id i3-dmenu-desktop 13 | 14 | # change focus 15 | bindsym Mod1+$left focus left 16 | bindsym Mod1+$down focus down 17 | bindsym Mod1+$up focus up 18 | bindsym Mod1+$right focus right 19 | 20 | # alternatively, you can use the cursor keys: 21 | bindsym Mod1+Left focus left 22 | bindsym Mod1+Down focus down 23 | bindsym Mod1+Up focus up 24 | bindsym Mod1+Right focus right 25 | 26 | # move focused window 27 | bindsym Mod1+Shift+$left move left 28 | bindsym Mod1+Shift+$down move down 29 | bindsym Mod1+Shift+$up move up 30 | bindsym Mod1+Shift+$right move right 31 | 32 | # alternatively, you can use the cursor keys: 33 | bindsym Mod1+Shift+Left move left 34 | bindsym Mod1+Shift+Down move down 35 | bindsym Mod1+Shift+Up move up 36 | bindsym Mod1+Shift+Right move right 37 | 38 | # split in horizontal orientation 39 | bindsym Mod1+h split h 40 | 41 | # split in vertical orientation 42 | bindsym Mod1+v split v 43 | 44 | # enter fullscreen mode for the focused container 45 | bindsym Mod1+f fullscreen toggle 46 | 47 | # change container layout (stacked, tabbed, toggle split) 48 | bindsym Mod1+s layout stacking 49 | bindsym Mod1+w layout tabbed 50 | bindsym Mod1+e layout toggle split 51 | 52 | # toggle tiling / floating 53 | bindsym Mod1+Shift+space floating toggle 54 | 55 | # change focus between tiling / floating windows 56 | bindsym Mod1+space focus mode_toggle 57 | 58 | # focus the parent container 59 | bindsym Mod1+a focus parent 60 | 61 | # focus the child container 62 | #bindsym Mod1+d focus child 63 | 64 | # move the currently focused window to the scratchpad 65 | bindsym Mod1+Shift+minus move scratchpad 66 | 67 | # Show the next scratchpad window or hide the focused scratchpad window. 68 | # If there are multiple scratchpad windows, this command cycles through them. 69 | bindsym Mod1+minus scratchpad show 70 | 71 | # switch to workspace 72 | bindsym Mod1+1 workspace 1 73 | bindsym Mod1+2 workspace 2 74 | bindsym Mod1+3 workspace 3 75 | bindsym Mod1+4 workspace 4 76 | bindsym Mod1+5 workspace 5 77 | bindsym Mod1+6 workspace 6 78 | bindsym Mod1+7 workspace 7 79 | bindsym Mod1+8 workspace 8 80 | bindsym Mod1+9 workspace 9 81 | bindsym Mod1+0 workspace 10 82 | 83 | # move focused container to workspace 84 | bindsym Mod1+Shift+1 move container to workspace 1 85 | bindsym Mod1+Shift+2 move container to workspace 2 86 | bindsym Mod1+Shift+3 move container to workspace 3 87 | bindsym Mod1+Shift+4 move container to workspace 4 88 | bindsym Mod1+Shift+5 move container to workspace 5 89 | bindsym Mod1+Shift+6 move container to workspace 6 90 | bindsym Mod1+Shift+7 move container to workspace 7 91 | bindsym Mod1+Shift+8 move container to workspace 8 92 | bindsym Mod1+Shift+9 move container to workspace 9 93 | bindsym Mod1+Shift+0 move container to workspace 10 94 | 95 | # reload the configuration file 96 | bindsym Mod1+Shift+c reload 97 | # restart i3 inplace (preserves your layout/session, can be used to upgrade i3) 98 | bindsym Mod1+Shift+r restart 99 | # exit i3 (logs you out of your X session) 100 | bindsym Mod1+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'" 101 | -------------------------------------------------------------------------------- /examples/1-partials/config.d/mode-resize.conf: -------------------------------------------------------------------------------- 1 | # resize window (you can also use the mouse for that) 2 | mode "resize" { 3 | # These bindings trigger as soon as you enter the resize mode 4 | 5 | # Pressing left will shrink the window’s width. 6 | # Pressing right will grow the window’s width. 7 | # Pressing up will shrink the window’s height. 8 | # Pressing down will grow the window’s height. 9 | bindsym $left resize shrink width 10 px or 10 ppt 10 | bindsym $down resize grow height 10 px or 10 ppt 11 | bindsym $up resize shrink height 10 px or 10 ppt 12 | bindsym $right resize grow width 10 px or 10 ppt 13 | 14 | # same bindings, but for the arrow keys 15 | bindsym Left resize shrink width 10 px or 10 ppt 16 | bindsym Down resize grow height 10 px or 10 ppt 17 | bindsym Up resize shrink height 10 px or 10 ppt 18 | bindsym Right resize grow width 10 px or 10 ppt 19 | 20 | # back to normal: Enter or Escape 21 | bindsym Return mode "default" 22 | bindsym Escape mode "default" 23 | } 24 | 25 | bindsym Mod1+r mode "resize" 26 | -------------------------------------------------------------------------------- /examples/1-partials/i3bar.default.conf: -------------------------------------------------------------------------------- 1 | ### i3bar.default.conf ### 2 | general { 3 | output_format = "dzen2" 4 | colors = true 5 | interval = 5 6 | } 7 | 8 | order += "ipv6" 9 | order += "disk /" 10 | order += "run_watch DHCP" 11 | order += "run_watch VPNC" 12 | order += "path_exists VPN" 13 | order += "wireless wlan0" 14 | order += "ethernet eth0" 15 | order += "battery 0" 16 | order += "cpu_temperature 0" 17 | order += "load" 18 | order += "tztime local" 19 | order += "tztime berlin" 20 | 21 | wireless wlan0 { 22 | format_up = "W: (%quality at %essid, %bitrate) %ip" 23 | format_down = "W: down" 24 | } 25 | 26 | ethernet eth0 { 27 | # if you use %speed, i3status requires the cap_net_admin capability 28 | format_up = "E: %ip (%speed)" 29 | format_down = "E: down" 30 | } 31 | 32 | battery 0 { 33 | format = "%status %percentage %remaining %emptytime" 34 | format_down = "No battery" 35 | status_chr = "⚡ CHR" 36 | status_bat = "🔋 BAT" 37 | status_unk = "? UNK" 38 | status_full = "☻ FULL" 39 | path = "/sys/class/power_supply/BAT%d/uevent" 40 | low_threshold = 10 41 | } 42 | 43 | run_watch DHCP { 44 | pidfile = "/var/run/dhclient*.pid" 45 | } 46 | 47 | run_watch VPNC { 48 | # file containing the PID of a vpnc process 49 | pidfile = "/var/run/vpnc/pid" 50 | } 51 | 52 | path_exists VPN { 53 | # path exists when a VPN tunnel launched by nmcli/nm-applet is active 54 | path = "/proc/sys/net/ipv4/conf/tun0" 55 | } 56 | 57 | tztime local { 58 | format = "%Y-%m-%d %H:%M:%S" 59 | } 60 | 61 | tztime berlin { 62 | format = "%Y-%m-%d %H:%M:%S %Z" 63 | timezone = "Europe/Berlin" 64 | } 65 | 66 | load { 67 | format = "%5min" 68 | } 69 | 70 | cpu_temperature 0 { 71 | format = "T: %degrees °C" 72 | path = "/sys/devices/platform/coretemp.0/temp1_input" 73 | } 74 | 75 | disk "/" { 76 | format = "%free" 77 | } 78 | -------------------------------------------------------------------------------- /examples/2-bars/config: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Built by i3configger /from/some/directory/who/cares (some time after 1972) # 3 | ############################################################################### 4 | 5 | ### filler.conf ### 6 | # Theoretically the config could be empty, but i3 -C crashes if it is only 7 | # passed bar settings and not at least one other config value 8 | workspace_layout tabbed 9 | 10 | 11 | ### i3bar.tpl.conf ### 12 | # Start i3bar to display a workspace bar (plus the system information i3status 13 | # finds out, if available) 14 | bar { 15 | # i3configger note: 16 | # additionally to all variables set in the configuration files 17 | # you can access the settings from i3configger.json here 18 | # They will be added to the context from bars->targets 19 | # they are pre populated by the bars->defaults 20 | status_command i3status -c ~/.i3/i3bar.full.conf 21 | output DP-3 22 | mode dock 23 | position bottom 24 | } 25 | ### i3bar.tpl.conf ### 26 | # Start i3bar to display a workspace bar (plus the system information i3status 27 | # finds out, if available) 28 | bar { 29 | # i3configger note: 30 | # additionally to all variables set in the configuration files 31 | # you can access the settings from i3configger.json here 32 | # They will be added to the context from bars->targets 33 | # they are pre populated by the bars->defaults 34 | status_command i3status -c ~/.i3/i3bar.clock.conf 35 | output DP-4 36 | mode dock 37 | position top 38 | } 39 | -------------------------------------------------------------------------------- /examples/2-bars/config.d/filler.conf: -------------------------------------------------------------------------------- 1 | # Theoretically the config could be empty, but i3 -C crashes if it is only 2 | # passed bar settings and not at least one other config value 3 | workspace_layout tabbed 4 | -------------------------------------------------------------------------------- /examples/2-bars/config.d/i3bar.clock.conf: -------------------------------------------------------------------------------- 1 | general { 2 | output_format = "i3bar" 3 | colors = true 4 | interval = 1 5 | } 6 | 7 | order = "time" 8 | 9 | time { 10 | format = "%Y-%m-%d %H:%M:%S" 11 | } 12 | -------------------------------------------------------------------------------- /examples/2-bars/config.d/i3bar.full.conf: -------------------------------------------------------------------------------- 1 | general { 2 | output_format = "dzen2" 3 | colors = true 4 | interval = 5 5 | } 6 | 7 | order += "ipv6" 8 | order += "disk /" 9 | order += "run_watch DHCP" 10 | order += "run_watch VPNC" 11 | order += "path_exists VPN" 12 | order += "wireless wlan0" 13 | order += "ethernet eth0" 14 | order += "battery 0" 15 | order += "cpu_temperature 0" 16 | order += "load" 17 | order += "tztime local" 18 | order += "tztime berlin" 19 | 20 | wireless wlan0 { 21 | format_up = "W: (%quality at %essid, %bitrate) %ip" 22 | format_down = "W: down" 23 | } 24 | 25 | ethernet eth0 { 26 | # if you use %speed, i3status requires the cap_net_admin capability 27 | format_up = "E: %ip (%speed)" 28 | format_down = "E: down" 29 | } 30 | 31 | battery 0 { 32 | format = "%status %percentage %remaining %emptytime" 33 | format_down = "No battery" 34 | status_chr = "⚡ CHR" 35 | status_bat = "🔋 BAT" 36 | status_unk = "? UNK" 37 | status_full = "☻ FULL" 38 | path = "/sys/class/power_supply/BAT%d/uevent" 39 | low_threshold = 10 40 | } 41 | 42 | run_watch DHCP { 43 | pidfile = "/var/run/dhclient*.pid" 44 | } 45 | 46 | run_watch VPNC { 47 | # file containing the PID of a vpnc process 48 | pidfile = "/var/run/vpnc/pid" 49 | } 50 | 51 | path_exists VPN { 52 | # path exists when a VPN tunnel launched by nmcli/nm-applet is active 53 | path = "/proc/sys/net/ipv4/conf/tun0" 54 | } 55 | 56 | tztime local { 57 | format = "%Y-%m-%d %H:%M:%S" 58 | } 59 | 60 | tztime berlin { 61 | format = "%Y-%m-%d %H:%M:%S %Z" 62 | timezone = "Europe/Berlin" 63 | } 64 | 65 | load { 66 | format = "%5min" 67 | } 68 | 69 | cpu_temperature 0 { 70 | format = "T: %degrees °C" 71 | path = "/sys/devices/platform/coretemp.0/temp1_input" 72 | } 73 | 74 | disk "/" { 75 | format = "%free" 76 | } 77 | -------------------------------------------------------------------------------- /examples/2-bars/config.d/i3bar.tpl.conf: -------------------------------------------------------------------------------- 1 | # Start i3bar to display a workspace bar (plus the system information i3status 2 | # finds out, if available) 3 | bar { 4 | # i3configger note: 5 | # additionally to all variables set in the configuration files 6 | # you can access the settings from i3configger.json here 7 | # They will be added to the context from bars->targets 8 | # they are pre populated by the bars->defaults 9 | status_command i3status -c ~/.i3/i3bar.$select.conf 10 | output $output 11 | mode $mode 12 | position $position 13 | } 14 | -------------------------------------------------------------------------------- /examples/2-bars/config.d/i3configger.json: -------------------------------------------------------------------------------- 1 | { 2 | "bars": { 3 | "defaults": { 4 | "target": "..", 5 | "template": "tpl", 6 | "mode": "dock" 7 | }, 8 | "targets": { 9 | "laptop": { 10 | "output": "DP-3", 11 | "position": "bottom", 12 | "select": "full" 13 | }, 14 | "external": { 15 | "output": "DP-4", 16 | "position": "top", 17 | "select": "clock" 18 | } 19 | } 20 | }, 21 | "main": { 22 | "target": "../config" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/2-bars/i3bar.clock.conf: -------------------------------------------------------------------------------- 1 | ### i3bar.clock.conf ### 2 | general { 3 | output_format = "i3bar" 4 | colors = true 5 | interval = 1 6 | } 7 | 8 | order = "time" 9 | 10 | time { 11 | format = "%Y-%m-%d %H:%M:%S" 12 | } 13 | -------------------------------------------------------------------------------- /examples/2-bars/i3bar.full.conf: -------------------------------------------------------------------------------- 1 | ### i3bar.full.conf ### 2 | general { 3 | output_format = "dzen2" 4 | colors = true 5 | interval = 5 6 | } 7 | 8 | order += "ipv6" 9 | order += "disk /" 10 | order += "run_watch DHCP" 11 | order += "run_watch VPNC" 12 | order += "path_exists VPN" 13 | order += "wireless wlan0" 14 | order += "ethernet eth0" 15 | order += "battery 0" 16 | order += "cpu_temperature 0" 17 | order += "load" 18 | order += "tztime local" 19 | order += "tztime berlin" 20 | 21 | wireless wlan0 { 22 | format_up = "W: (%quality at %essid, %bitrate) %ip" 23 | format_down = "W: down" 24 | } 25 | 26 | ethernet eth0 { 27 | # if you use %speed, i3status requires the cap_net_admin capability 28 | format_up = "E: %ip (%speed)" 29 | format_down = "E: down" 30 | } 31 | 32 | battery 0 { 33 | format = "%status %percentage %remaining %emptytime" 34 | format_down = "No battery" 35 | status_chr = "⚡ CHR" 36 | status_bat = "🔋 BAT" 37 | status_unk = "? UNK" 38 | status_full = "☻ FULL" 39 | path = "/sys/class/power_supply/BAT%d/uevent" 40 | low_threshold = 10 41 | } 42 | 43 | run_watch DHCP { 44 | pidfile = "/var/run/dhclient*.pid" 45 | } 46 | 47 | run_watch VPNC { 48 | # file containing the PID of a vpnc process 49 | pidfile = "/var/run/vpnc/pid" 50 | } 51 | 52 | path_exists VPN { 53 | # path exists when a VPN tunnel launched by nmcli/nm-applet is active 54 | path = "/proc/sys/net/ipv4/conf/tun0" 55 | } 56 | 57 | tztime local { 58 | format = "%Y-%m-%d %H:%M:%S" 59 | } 60 | 61 | tztime berlin { 62 | format = "%Y-%m-%d %H:%M:%S %Z" 63 | timezone = "Europe/Berlin" 64 | } 65 | 66 | load { 67 | format = "%5min" 68 | } 69 | 70 | cpu_temperature 0 { 71 | format = "T: %degrees °C" 72 | path = "/sys/devices/platform/coretemp.0/temp1_input" 73 | } 74 | 75 | disk "/" { 76 | format = "%free" 77 | } 78 | -------------------------------------------------------------------------------- /examples/3-variables/config: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Built by i3configger /from/some/directory/who/cares (some time after 1972) # 3 | ############################################################################### 4 | 5 | ### some-variables.conf ### 6 | # some actual config - only needed fo i3 -C not to crash 7 | workspace_layout tabbed 8 | 9 | 10 | ### i3bar.tpl.conf ### 11 | # you can set variables in the bar template also 12 | # They will only be valid in the context of bar generation 13 | # They will also override variables of the same name set in other files 14 | 15 | bar { 16 | # additionally to all variables set in the configuration files 17 | # you can access the settings from i3configger.json here 18 | # They will be added to the context from bars->targets 19 | # they are pre populated by the bars->defaults 20 | status_command i3status -c ~/.i3/i3bar.default.conf 21 | output DP-3 22 | mode dock 23 | position bottom 24 | } 25 | -------------------------------------------------------------------------------- /examples/3-variables/config.d/i3bar.default.conf: -------------------------------------------------------------------------------- 1 | general { 2 | output_format = "dzen2" 3 | colors = true 4 | color = "$color1" 5 | color_good = "$color2" 6 | color_degraded = "$color3" 7 | color_bad = "$color4" 8 | interval = 5 9 | } 10 | 11 | order += "cpu_temperature 0" 12 | order += "load" 13 | 14 | load { 15 | format = "%5min" 16 | } 17 | 18 | cpu_temperature 0 { 19 | format = "T: %degrees °C" 20 | path = "/sys/devices/platform/coretemp.0/temp1_input" 21 | } 22 | -------------------------------------------------------------------------------- /examples/3-variables/config.d/i3bar.tpl.conf: -------------------------------------------------------------------------------- 1 | # you can set variables in the bar template also 2 | # They will only be valid in the context of bar generation 3 | # They will also override variables of the same name set in other files 4 | set $ mode dock 5 | 6 | bar { 7 | # additionally to all variables set in the configuration files 8 | # you can access the settings from i3configger.json here 9 | # They will be added to the context from bars->targets 10 | # they are pre populated by the bars->defaults 11 | status_command i3status -c ~/.i3/i3bar.$select.conf 12 | output $output 13 | mode $mode 14 | position $position 15 | } 16 | -------------------------------------------------------------------------------- /examples/3-variables/config.d/i3configger.json: -------------------------------------------------------------------------------- 1 | { 2 | "bars": { 3 | "defaults": { 4 | "mode": "dock" 5 | }, 6 | "targets": { 7 | "laptop": { 8 | "output": "DP-3", 9 | "position": "bottom" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/3-variables/config.d/some-variables.conf: -------------------------------------------------------------------------------- 1 | # some actual config - only needed fo i3 -C not to crash 2 | workspace_layout tabbed 3 | 4 | set $color1 #000000 5 | set $color2 #000000 6 | set $color3 #AAAAAA 7 | set $color4 #555555 8 | -------------------------------------------------------------------------------- /examples/3-variables/i3bar.default.conf: -------------------------------------------------------------------------------- 1 | ### i3bar.default.conf ### 2 | general { 3 | output_format = "dzen2" 4 | colors = true 5 | color = "#000000" 6 | color_good = "#000000" 7 | color_degraded = "#AAAAAA" 8 | color_bad = "#555555" 9 | interval = 5 10 | } 11 | 12 | order += "cpu_temperature 0" 13 | order += "load" 14 | 15 | load { 16 | format = "%5min" 17 | } 18 | 19 | cpu_temperature 0 { 20 | format = "T: %degrees °C" 21 | path = "/sys/devices/platform/coretemp.0/temp1_input" 22 | } 23 | -------------------------------------------------------------------------------- /examples/4-schemes/config: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Built by i3configger /from/some/directory/who/cares (some time after 1972) # 3 | ############################################################################### 4 | 5 | ### some-key.value2.conf ### 6 | # This is integrated, because it had been selected by a remembered message. 7 | 8 | # var will also be replaced here - although this is just a comment: default 9 | default_orientation horizontal 10 | workspace_layout default 11 | -------------------------------------------------------------------------------- /examples/4-schemes/config.d/.gitignore: -------------------------------------------------------------------------------- 1 | # force to integrate .message.json here for the example 2 | # it should usually not be committed as it contains volatile state 3 | !.messages.json 4 | -------------------------------------------------------------------------------- /examples/4-schemes/config.d/.messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": { 3 | "some-key": "value2" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/4-schemes/config.d/i3configger.json: -------------------------------------------------------------------------------- 1 | { 2 | "bars": { 3 | "defaults": {}, 4 | "targets": {} 5 | }, 6 | "main": { 7 | "target": "../config" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/4-schemes/config.d/some-key.value1.conf: -------------------------------------------------------------------------------- 1 | # If nothing is set in i3configger.json the first file 2 | # that is found is integrated by default (in alphabetical order) 3 | # So normally this file would be integrated 4 | # as `config.d` also contains a `.message.json` which remembers what was sent 5 | # and the message `i3configger select some-category value2` or ` 6 | # i3configger select-next some-category` had been sent 7 | # that file will be integrated 8 | 9 | default_orientation vertical 10 | workspace_layout $layoutVar 11 | -------------------------------------------------------------------------------- /examples/4-schemes/config.d/some-key.value2.conf: -------------------------------------------------------------------------------- 1 | # This is integrated, because it had been selected by a remembered message. 2 | 3 | # var will also be replaced here - although this is just a comment: $layoutVar 4 | default_orientation horizontal 5 | workspace_layout $layoutVar 6 | -------------------------------------------------------------------------------- /examples/4-schemes/config.d/some-settings.conf: -------------------------------------------------------------------------------- 1 | set $layoutVar default 2 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This folder contains some `config.d` folders and what is generated from them (in the folder above, just like it would be in the i3 config directory). 4 | 5 | ## [default](0-default) 6 | 7 | The i3configger version of the i3 default configuration 8 | 9 | Taken from `/etc/i3/config` 10 | 11 | Simplest possible way of using it and a bit pointless, but you have to start somewhere. 12 | 13 | ## [partials](1-partials) 14 | 15 | Different kinds of settings have their own files now and the bar is also build from `config.d`. The name fo the status bar config file is set dynamically with variables populated from `i3configger.json->bars->targets` section for every bar. There is only one bar here though, so this doesn't really show yet. 16 | 17 | ## [bars](2-bars) 18 | 19 | Let's generate more bars now. If you have an external monitor where you want to have a different bar, you can add another section to i3configger.json->bars->targets, add another status bar configuration file. Two bar settings will be generated in the config and the two configuration files are rendered and referenced from the variables in the bar template. 20 | 21 | ## [variables](3-variables) 22 | 23 | Variables can be set and used anywhere (independent of order). 24 | 25 | Variables can also be assigned the value of other variables. 26 | 27 | ## [schemes](4-schemes) 28 | 29 | Files following `..conf` signal that only one of them should be integrated and you can switch between them. One of them will always be included (alphabetically first one on initialization). 30 | 31 | In this example there is also a `.message.json` which usually should not be committed as it contains volatile state. This is to demonstrate how messages play into the build behaviour. 32 | -------------------------------------------------------------------------------- /i3configger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obestwalter/i3configger/c981a01f5b89fd37ab5a98af1229819cea305f6a/i3configger/__init__.py -------------------------------------------------------------------------------- /i3configger/__main__.py: -------------------------------------------------------------------------------- 1 | """Make python -m i3configger possible.""" 2 | import i3configger.cli 3 | 4 | 5 | if __name__ == "__main__": 6 | i3configger.cli.main() 7 | -------------------------------------------------------------------------------- /i3configger/base.py: -------------------------------------------------------------------------------- 1 | """Basic names and functionality.""" 2 | import logging 3 | import os 4 | import sys 5 | import tempfile 6 | from pathlib import Path 7 | 8 | from i3configger import exc 9 | 10 | log = logging.getLogger(__name__) 11 | DEBUG = os.getenv("DEBUG", False) 12 | 13 | 14 | def configure_logging(verbosity: int, logPath, isDaemon=False): 15 | rootLogger = logging.getLogger() 16 | verbosity = 3 if DEBUG else verbosity 17 | level = {0: "ERROR", 1: "WARNING", 2: "INFO"}.get(verbosity, "DEBUG") 18 | if logPath: 19 | logPath = Path(logPath).expanduser() 20 | else: 21 | name = "i3configger-daemon.log" if isDaemon else "i3configger.log" 22 | logPath = Path(tempfile.gettempdir()) / name 23 | if verbosity > 1: 24 | print(f"logging to {logPath} with level {level}", file=sys.stderr) 25 | fmt = "%(asctime)s %(name)s:%(funcName)s:%(lineno)s %(levelname)s: %(message)s" 26 | if not rootLogger.handlers: 27 | logging.basicConfig(format=fmt, level=level) 28 | fileHandler = logging.FileHandler(logPath) 29 | fileHandler.setFormatter(logging.Formatter(fmt)) 30 | fileHandler.setLevel(level) 31 | rootLogger.addHandler(fileHandler) 32 | 33 | 34 | def get_version(): 35 | """hide behind a wrapped function (slow and not a catastrophe if fails)""" 36 | try: 37 | from pkg_resources import get_distribution 38 | 39 | return get_distribution("i3configger").version 40 | except Exception: 41 | log.exception("fetching version failed") 42 | return "unknown" 43 | 44 | 45 | def i3configger_excepthook(type_, value, traceback): 46 | """Make own exceptions look like a friendly error message :)""" 47 | if DEBUG or not isinstance(value, exc.I3configgerException): 48 | _REAL_EXCEPTHOOK(type_, value, traceback) 49 | else: 50 | sys.exit(f"[FATAL] {type(value).__name__}: {value}") 51 | 52 | 53 | _REAL_EXCEPTHOOK = sys.excepthook 54 | sys.excepthook = i3configger_excepthook 55 | -------------------------------------------------------------------------------- /i3configger/bindings.py: -------------------------------------------------------------------------------- 1 | """WARNING Just an experiment - please ignore this.""" 2 | from i3configger import config 3 | 4 | BINDCODE = "bindcode" 5 | BINDSYM = "bindsym" 6 | 7 | 8 | class Bindings: 9 | """ 10 | bindsym | bindcode 11 | [--release] [+][+] command 12 | 13 | [--release] [--border] [--whole-window] [+]button command 14 | """ 15 | 16 | def __init__(self, content): 17 | self.content = content 18 | 19 | def get_all_bindings(self): 20 | lines = [l.strip() for l in self.content.splitlines()] 21 | lines = [l for l in lines if any(m in l for m in [BINDCODE, BINDSYM])] 22 | lines = [l for l in lines if not l.startswith(config.MARK.COMMENT)] 23 | return sorted(set(lines)) 24 | 25 | def translate_bindings(self): 26 | """translate bindcode to bindsym assignments 27 | 28 | this need to be done the moment the information is asked because it 29 | depends on the currently active layout. 30 | """ 31 | raise NotImplementedError() 32 | 33 | def write_bindings_info(self): 34 | """Write info in some format that can be nicely displayed""" 35 | raise NotImplementedError() 36 | 37 | 38 | if __name__ == "__main__": 39 | # use partials and account for modes 40 | # a naming convention would make this quite easy 41 | # mode-.conf -> bindings active in 42 | p = config.I3configgerConfig().targetPath 43 | b = Bindings(p.read_text()) 44 | print("\n".join(b.get_all_bindings())) 45 | -------------------------------------------------------------------------------- /i3configger/build.py: -------------------------------------------------------------------------------- 1 | """High level build functionality bringing it all together.""" 2 | import logging 3 | import os 4 | import tempfile 5 | import time 6 | from pathlib import Path 7 | from pprint import pformat 8 | 9 | from i3configger import config, context, exc, ipc, message, partials 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | def build_all(): 15 | cnf = config.I3configgerConfig() 16 | log.info(f"start building from {cnf.partialsPath}") 17 | prts = partials.create(cnf.partialsPath) 18 | msg = message.Messenger(cnf.messagesPath, prts).fetch_messages( 19 | exludes=[config.I3BAR] 20 | ) 21 | cnf.payload = context.merge(cnf.payload, msg[message.CMD.SHADOW]) 22 | pathContentsMap = generate_contents(cnf, prts, msg) 23 | check_config(pathContentsMap[cnf.targetPath]) 24 | persist_results(pathContentsMap) 25 | ipc.communicate(refresh=True) 26 | log.info("build done") 27 | 28 | 29 | def generate_contents(cnf: config.I3configgerConfig, prts, msg): 30 | selectorMap = msg[message.CMD.SELECT] 31 | setMap = msg[message.CMD.SET] 32 | pathContentsMap = {} 33 | barTargets = cnf.get_bar_targets() 34 | selected = partials.select(prts, selectorMap, excludes=[config.I3BAR]) 35 | ctx = context.process(selected + [setMap]) 36 | log.debug(f"main context:\n{pformat(ctx)}") 37 | mainContent = generate_main_content(cnf.partialsPath, selected, ctx) 38 | for barName, barCnf in barTargets.items(): 39 | barCnf["id"] = barName 40 | log.debug(f"bar {barName} config:\n{pformat(barCnf)}") 41 | extendedContext = context.process([ctx, barCnf]) 42 | mainContent += f"\n{get_bar_setting(barCnf, prts, extendedContext)}" 43 | i3barFileContent = generate_i3bar_content( 44 | prts, barCnf["select"], extendedContext 45 | ) 46 | if i3barFileContent: 47 | filename = f"{config.I3BAR}.{barCnf['select']}{config.SUFFIX}" 48 | dst = Path(barCnf["target"]) / filename 49 | pathContentsMap[dst] = i3barFileContent 50 | pathContentsMap[cnf.targetPath] = mainContent.rstrip("\n") + "\n" 51 | return pathContentsMap 52 | 53 | 54 | def make_header(partialsPath): 55 | strPath = str(partialsPath) 56 | parts = strPath.split(os.getenv("HOME")) 57 | if len(parts) > 1: 58 | strPath = "~" + parts[-1] 59 | msg = f"# Built from {strPath} by i3configger ({time.asctime()}) #" 60 | sep = "#" * len(msg) 61 | return f"{sep}\n{msg}\n{sep}\n" 62 | 63 | 64 | def generate_main_content(partialsPath, selected, ctx): 65 | out = [make_header(partialsPath)] 66 | for prt in selected: 67 | content = prt.get_pruned_content() 68 | if content: 69 | out.append(content) 70 | return context.substitute("\n".join(out), ctx).rstrip("\n") + "\n\n" 71 | 72 | 73 | def get_bar_setting(barCnf, prts, ctx): 74 | tpl = partials.find(prts, config.I3BAR, barCnf["template"]) 75 | assert isinstance(tpl, partials.Partial), tpl 76 | tpl.name = f"{tpl.name} [id: {barCnf['id']}]" 77 | return context.substitute(tpl.get_pruned_content(), ctx).rstrip("\n") 78 | 79 | 80 | def generate_i3bar_content(prts, selectValue, ctx): 81 | prt = partials.find(prts, config.I3BAR, selectValue) 82 | if not prt: 83 | raise exc.ConfigError( 84 | f"[IGNORE] no status config named " 85 | f"{config.I3BAR}.{selectValue}{config.SUFFIX}" 86 | ) 87 | assert isinstance(prt, partials.Partial), prt 88 | content = context.substitute(prt.get_pruned_content(), ctx) 89 | return content.rstrip("\n") + "\n" 90 | 91 | 92 | def check_config(content): 93 | tmpPath = Path(tempfile.gettempdir()) / "i3config_check" 94 | tmpPath.write_text(content) 95 | errorReport = ipc.I3.get_config_error_report(tmpPath) 96 | if errorReport: 97 | msg = ( 98 | f"FATAL: config not changed due to errors. " 99 | f"Broken config is at {tmpPath}" 100 | ) 101 | report = f"config:\n{content}\n\nerrors:\n{errorReport}" 102 | ipc.communicate(msg, urgency="normal") 103 | raise exc.ConfigError(f"{msg}\n{report}") 104 | 105 | 106 | def persist_results(pathContentsMap): 107 | for path, content in pathContentsMap.items(): 108 | backupPath = Path(str(path) + ".bak") 109 | if path.exists() and not backupPath.exists(): 110 | path.rename(backupPath) 111 | path.write_text(content) 112 | -------------------------------------------------------------------------------- /i3configger/cli.py: -------------------------------------------------------------------------------- 1 | """Command Line Interface and main entry point.""" 2 | import argparse 3 | import logging 4 | import sys 5 | 6 | from i3configger import base, build, config, exc, ipc, message, watch 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | def main(): 12 | """Wrap main to show own exceptions wo traceback in normal use.""" 13 | args = process_command_line() 14 | try: 15 | _main(args) 16 | except exc.I3configgerException as e: 17 | if args.v > 2: 18 | raise 19 | sys.exit(f"[FATAL] {e}") 20 | 21 | 22 | def _main(args): 23 | config.ensure_i3_configger_sanity() 24 | cnf = config.I3configgerConfig() 25 | ipc.configure(cnf) 26 | base.configure_logging(verbosity=args.v, logPath=cnf.payload["main"]["log"]) 27 | if args.version: 28 | print(f"i3configger {base.get_version()}") 29 | return 0 30 | 31 | if args.kill: 32 | watch.exorcise() 33 | return 0 34 | 35 | if args.message: 36 | message.save(args.message) 37 | if watch.get_i3configger_process(): 38 | if not args.message: 39 | raise exc.UserError("already running - did you mean to send a message?") 40 | log.info(f"{args.message} is saved - now let the running process do the work") 41 | return 0 42 | 43 | if args.daemon: 44 | watch.daemonized(args.v, args.log) 45 | elif args.watch: 46 | try: 47 | watch.watch_guarded() 48 | except KeyboardInterrupt: 49 | sys.exit("interrupted by user") 50 | else: 51 | build.build_all() 52 | ipc.communicate(refresh=True) 53 | 54 | 55 | def process_command_line(): 56 | parser = argparse.ArgumentParser( 57 | "i3configger", formatter_class=argparse.ArgumentDefaultsHelpFormatter 58 | ) 59 | args = _parse_args(parser) 60 | config.cliMainOverrideMap = vars(args) 61 | if args.message and any([args.daemon, args.kill, args.watch]): 62 | parser.error( 63 | "message and daemon/watch can't be used together. " 64 | "Start the watcher process first and then you can send " 65 | "messages in following calls." 66 | ) 67 | return args 68 | 69 | 70 | def _parse_args(p): 71 | """Command line arguments - all optional with [reasonable] defaults""" 72 | p.add_argument( 73 | "--version", action="store_true", help="show version information and exit" 74 | ) 75 | p.add_argument("-v", action="count", help="raise verbosity", default=0) 76 | g = p.add_mutually_exclusive_group() 77 | g.add_argument( 78 | "--watch", 79 | action="store_true", 80 | help="watch and build in foreground", 81 | default=False, 82 | ) 83 | g.add_argument( 84 | "--daemon", action="store_true", help="watch and build as daemon", default=False 85 | ) 86 | g.add_argument( 87 | "--kill", action="store_true", default=False, help="exorcise daemon if running" 88 | ) 89 | p.add_argument( 90 | "--i3-refresh-msg", 91 | action="store", 92 | default="reload", 93 | choices=["restart", "reload", "nop"], 94 | help="i3-msg to send after build", 95 | ) 96 | p.add_argument( 97 | "--notify", 98 | action="store_true", 99 | default=False, 100 | help="show build notification via notify-send", 101 | ) 102 | p.add_argument( 103 | "--log", 104 | action="store", 105 | default=None, 106 | help="i3configgerPath to where log should be stored", 107 | ) 108 | p.add_argument("message", help="message to send to i3configger", nargs="*") 109 | return p.parse_args() 110 | -------------------------------------------------------------------------------- /i3configger/config.py: -------------------------------------------------------------------------------- 1 | """i3configger configuration functionality.""" 2 | import copy 3 | import json 4 | import logging 5 | import os 6 | import pprint 7 | import shutil 8 | from pathlib import Path 9 | 10 | from i3configger import context, exc 11 | 12 | log = logging.getLogger(__name__) 13 | cliMainOverrideMap: dict = {} 14 | """holds parsed args if started from cli and was initialized""" 15 | 16 | 17 | class MARK: 18 | COMMENT = "#" 19 | SET = "set" 20 | VAR = "$" 21 | 22 | 23 | SUFFIX = ".conf" 24 | I3BAR = "i3bar" 25 | """reserved key for status bar template files""" 26 | 27 | 28 | DEFAULTS = { 29 | "main": { 30 | "target": "../config", 31 | "i3_refresh_msg": "reload", 32 | "status_command": "i3status", 33 | "log": None, 34 | "notify": False, 35 | }, 36 | "bars": { 37 | "defaults": {"template": "tpl", "target": "..", "select": "default"}, 38 | "targets": {}, 39 | }, 40 | } 41 | CONFIG_CANDIDATES = [ 42 | Path("~/.i3").expanduser(), 43 | Path(os.getenv("XDG_CONFIG_HOME", "~/.config/")).expanduser() / "i3", 44 | ] 45 | 46 | 47 | class I3configgerConfig: 48 | PARTIALS_NAME = "config.d" 49 | CONFIG_NAME = "i3configger.json" 50 | MESSAGES_NAME = ".messages.json" 51 | 52 | def __init__(self, load=True): 53 | i3configBasePath = get_i3wm_config_path() 54 | self.partialsPath = i3configBasePath / self.PARTIALS_NAME 55 | self.configPath = self.partialsPath / self.CONFIG_NAME 56 | self.messagesPath = self.partialsPath / self.MESSAGES_NAME 57 | if load: 58 | self.load() 59 | 60 | def __str__(self): 61 | return f"{self.__class__.__name__}:\n{pprint.pformat(vars(self))}" 62 | 63 | def load(self): 64 | """Layered overrides `DEFAULTS` -> `i3configger.json`-> `args`""" 65 | cnfFromDefaults = copy.deepcopy(DEFAULTS) 66 | cnfFromFile = fetch(self.configPath) 67 | self.payload = context.merge(cnfFromDefaults, cnfFromFile) 68 | mainOverrides = dict(main=cliMainOverrideMap) 69 | self.payload = context.merge(self.payload, mainOverrides) 70 | targetPath = Path(self.payload["main"]["target"]).expanduser() 71 | if targetPath.is_absolute(): 72 | self.targetPath = targetPath.resolve() 73 | else: 74 | self.targetPath = (self.partialsPath / targetPath).resolve() 75 | 76 | def get_bar_targets(self): 77 | """Create a resolved copy of the bar settings.""" 78 | barTargets = {} 79 | barSettings = self.payload.get("bars", {}) 80 | if not barSettings: 81 | return barTargets 82 | defaults = barSettings.get("defaults", {}) 83 | for name, bar in barSettings["targets"].items(): 84 | newBar = dict(bar) 85 | barTargets[name] = newBar 86 | for defaultKey, defaultValue in defaults.items(): 87 | if defaultKey not in newBar: 88 | newBar[defaultKey] = defaultValue 89 | container = Path(newBar["target"]) 90 | if not container.is_absolute(): 91 | container = (self.partialsPath / container).resolve() 92 | newBar["target"] = str(container) 93 | return barTargets 94 | 95 | 96 | def fetch(path: Path) -> dict: 97 | if not path.exists(): 98 | raise exc.ConfigError(f"config not found at {path}") 99 | payload = json.loads(path.read_text()) 100 | log.debug(f"{path}:\n{pprint.pformat(payload)}") 101 | return payload 102 | 103 | 104 | def freeze(path, obj): 105 | path.write_text(json.dumps(obj, sort_keys=True, indent=2)) 106 | log.debug(f"froze {pprint.pformat(obj)} to {path}") 107 | 108 | 109 | def ensure_i3_configger_sanity(): 110 | i3wmConfigPath = get_i3wm_config_path() 111 | partialsPath = i3wmConfigPath / I3configgerConfig.PARTIALS_NAME 112 | if not partialsPath.exists(): 113 | log.info(f"create new config folder at {partialsPath}") 114 | partialsPath.mkdir() 115 | for candidate in [i3wmConfigPath / "config", Path("etc/i3/config")]: 116 | if candidate.exists(): 117 | log.info(f"populate config with {candidate}") 118 | shutil.copy2(candidate, partialsPath / "config.conf") 119 | configPath = partialsPath / I3configgerConfig.CONFIG_NAME 120 | if not configPath.exists(): 121 | log.info(f"create default configuration at {configPath}") 122 | freeze(configPath, DEFAULTS) 123 | 124 | 125 | def get_i3wm_config_path(): 126 | """Use same search order like i3 (no system stuff though). 127 | 128 | see: https://github.com/i3/i3/blob/4.13/libi3/get_config_path.c#L31 129 | """ 130 | for candidate in CONFIG_CANDIDATES: 131 | if candidate.exists(): 132 | return candidate 133 | raise exc.ConfigError( 134 | f"can't find i3 config at the standard locations: {CONFIG_CANDIDATES}" 135 | ) 136 | -------------------------------------------------------------------------------- /i3configger/context.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from functools import reduce 3 | from string import Template 4 | from typing import Iterable, Union 5 | 6 | from i3configger import config, exc, partials 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | def process(item: Iterable[Union[dict, partials.Partial]]) -> dict: 12 | ctx = merge_all(item) 13 | ctx = resolve_variables(ctx) 14 | ctx = remove_variable_markers(ctx) 15 | return ctx 16 | 17 | 18 | def merge_all(item: Iterable[Union[dict, partials.Partial]]) -> dict: 19 | """Merge contexts of an arbitrary number of items.""" 20 | dicts = [] 21 | for elem in item: 22 | dicts.append(elem if isinstance(elem, dict) else elem.context) 23 | return reduce(merge, dicts) 24 | 25 | 26 | def merge(dst: dict, src: dict) -> dict: 27 | """merge source into destination mapping (overwrite existing keys).""" 28 | for key in src: 29 | if key in dst: 30 | if isinstance(dst[key], dict) and isinstance(src[key], dict): 31 | merge(dst[key], src[key]) 32 | else: 33 | dst[key] = src[key] 34 | else: 35 | dst[key] = src[key] 36 | return dst 37 | 38 | 39 | def prune(dst: dict, src: dict) -> dict: 40 | for key, value in src.items(): 41 | if isinstance(value, dict): 42 | prune(dst.get(key, {}), value) 43 | elif key in dst: 44 | del dst[key] 45 | return dst 46 | 47 | 48 | def resolve_variables(ctx: dict) -> dict: 49 | """If variables are set by a variable, replace them by their value.""" 50 | while any(v.startswith(config.MARK.VAR) for v in ctx.values()): 51 | for key, value in ctx.items(): 52 | if value.startswith(config.MARK.VAR): 53 | ctx[key] = _resolve_variable(value, ctx, path=key) 54 | return ctx 55 | 56 | 57 | def _resolve_variable(key, ctx, path): 58 | path += "->" + key 59 | resolved = ctx.get(key) 60 | if not resolved: 61 | raise exc.ContextError(f"not resolvable: {path}") 62 | if resolved.startswith(config.MARK.VAR): 63 | return _resolve_variable(resolved, ctx, path) 64 | return resolved 65 | 66 | 67 | def remove_variable_markers(ctx: dict) -> dict: 68 | cleaned = {} 69 | lvm = len(config.MARK.VAR) 70 | for key, value in ctx.items(): 71 | key = key[lvm:] if key.startswith(config.MARK.VAR) else key 72 | cleaned[key] = value 73 | return cleaned 74 | 75 | 76 | def substitute(content: str, ctx: dict): 77 | """Substitute all variables with their values. 78 | 79 | Works out of the box, because '$' is the standard substitution 80 | marker for string.Template 81 | 82 | As there also might be other occurrences of "$" (e.g. in regexes) 83 | `safe_substitute()` is used, 84 | """ 85 | return Template(content).safe_substitute(ctx) 86 | -------------------------------------------------------------------------------- /i3configger/exc.py: -------------------------------------------------------------------------------- 1 | """Custom errors.""" 2 | 3 | 4 | class I3configgerException(Exception): 5 | """Main exception with log style string formatting enhancement""" 6 | 7 | 8 | class BuildError(I3configgerException): 9 | pass 10 | 11 | 12 | class ConfigError(I3configgerException): 13 | pass 14 | 15 | 16 | class MessageError(I3configgerException): 17 | pass 18 | 19 | 20 | class PartialsError(I3configgerException): 21 | pass 22 | 23 | 24 | class ParseError(I3configgerException): 25 | pass 26 | 27 | 28 | class ContextError(I3configgerException): 29 | pass 30 | 31 | 32 | class UserError(I3configgerException): 33 | """User did something wrong and I can't go on.""" 34 | -------------------------------------------------------------------------------- /i3configger/inotify_simple.py: -------------------------------------------------------------------------------- 1 | """Vendored version of inotify_simple as suggested by author 2 | https://github.com/chrisjbillington/inotify_simple 3 | 4 | Copyright (c) 2016, Chris Billington 5 | Copyright (c) 2018, Oliver Bestwalter (simplifications/Py3 only) 6 | 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | 12 | 1. Redistributions of source code must retain the above copyright notice, this 13 | list of conditions and the following disclaimer. 14 | 2. Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided wi6h the distribution. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | """ 29 | import enum 30 | import collections 31 | import struct 32 | import select 33 | import time 34 | import ctypes 35 | from os import strerror, fsencode, read, close, fsdecode 36 | from errno import EINTR 37 | from termios import FIONREAD 38 | from fcntl import ioctl 39 | 40 | 41 | __all__ = ["flags", "masks", "parse_events", "INotify", "Event"] 42 | _libc = ctypes.cdll.LoadLibrary("libc.so.6") 43 | _libc.__errno_location.restype = ctypes.POINTER(ctypes.c_int) 44 | 45 | Event = collections.namedtuple("Event", ["wd", "mask", "cookie", "name"]) 46 | """A ``namedtuple`` (wd, mask, cookie, name) for an inotify event. 47 | 48 | ``namedtuple`` objects are very lightweight to instantiate and access, 49 | whilst being human readable when printed, which is useful for debugging and logging. 50 | For best performance, note that element access by index is about four times faster than by name. 51 | """ 52 | _EVENT_STRUCT_FORMAT = "iIII" 53 | _EVENT_STRUCT_SIZE = struct.calcsize(_EVENT_STRUCT_FORMAT) 54 | 55 | 56 | def _libc_call(func, *args): 57 | """Wrapper which raises errors and retries on EINTR.""" 58 | while True: 59 | rc = func(*args) 60 | if rc == -1: 61 | errno = _libc.__errno_location().contents.value 62 | if errno == EINTR: 63 | # retry 64 | continue 65 | else: 66 | raise OSError(errno, strerror(errno)) 67 | return rc 68 | 69 | 70 | class INotify(object): 71 | def __init__(self): 72 | """Object wrapper around ``inotify_init()`` which stores the inotify file 73 | descriptor. Raises an OSError on failure. :func:`~inotify_simple.INotify.close` 74 | should be called when no longer needed. Can be used as a context manager 75 | to ensure it is closed.""" 76 | #: The inotify file descriptor returned by ``inotify_init()``. You are 77 | #: free to use it directly with ``os.read`` if you'd prefer not to call 78 | #: :func:`~inotify_simple.INotify.read` for some reason. 79 | self.fd = _libc_call(_libc.inotify_init) 80 | self._poller = select.poll() 81 | self._poller.register(self.fd) 82 | 83 | def add_watch(self, path, mask): 84 | """Wrapper around ``inotify_add_watch()``. Returns the watch 85 | descriptor or raises an OSError on failure. 86 | 87 | Args: 88 | path (py3 str or bytes, py2 unicode or str): The path to watch. 89 | If ``str`` in python3 or ``unicode`` in python2, will be encoded with 90 | the filesystem encoding before being passed to 91 | ``inotify_add_watch()``. Note that ``pathlib.Path`` objects are 92 | sufficiently string-like to be passed to this method as-is. 93 | 94 | mask (int): The mask of events to watch for. Can be constructed by 95 | bitwise-ORing :class:`~inotify_simple.flags` together. 96 | 97 | Returns: 98 | int: watch descriptor""" 99 | if not isinstance(path, bytes): 100 | path = fsencode(path) 101 | return _libc_call(_libc.inotify_add_watch, self.fd, path, mask) 102 | 103 | def rm_watch(self, wd): 104 | """Wrapper around ``inotify_rm_watch()``. Raises OSError on failure. 105 | 106 | Args: 107 | wd (int): The watch descriptor to remove""" 108 | _libc_call(_libc.inotify_rm_watch, self.fd, wd) 109 | 110 | def read(self, timeout=None, read_delay=None): 111 | """Read the inotify file descriptor and return the resulting list of 112 | :attr:`~inotify_simple.Event` namedtuples (wd, mask, cookie, name). 113 | 114 | Args: 115 | timeout (int): The time in milliseconds to wait for events if 116 | there are none. If `negative or `None``, block until there are 117 | events. 118 | 119 | read_delay (int): The time in milliseconds to wait after the first 120 | event arrives before reading the buffer. This allows further 121 | events to accumulate before reading, which allows the kernel 122 | to consolidate like events and can enhance performance when 123 | there are many similar events. 124 | 125 | Returns: 126 | list: list of :attr:`~inotify_simple.Event` namedtuples""" 127 | # Wait for the first event: 128 | pending = self._poller.poll(timeout) 129 | if not pending: 130 | # Timed out, no events 131 | return [] 132 | if read_delay is not None: 133 | # Wait for more events to accumulate: 134 | time.sleep(read_delay / 1000.0) 135 | # How much data is available to read? 136 | bytes_avail = ctypes.c_int() 137 | ioctl(self.fd, FIONREAD, bytes_avail) 138 | buffer_size = bytes_avail.value 139 | # Read and parse it: 140 | data = read(self.fd, buffer_size) 141 | events = parse_events(data) 142 | return events 143 | 144 | def close(self): 145 | """Close the inotify file descriptor""" 146 | close(self.fd) 147 | 148 | def __enter__(self): 149 | return self 150 | 151 | def __exit__(self, exc_type, exc_value, traceback): 152 | self.close() 153 | 154 | 155 | def parse_events(data): 156 | """Parse data read from an inotify file descriptor into list of 157 | :attr:`~inotify_simple.Event` namedtuples (wd, mask, cookie, name). This 158 | function can be used if you have decided to call ``os.read()`` on the 159 | inotify file descriptor yourself, instead of calling 160 | :func:`~inotify_simple.INotify.read`. 161 | 162 | Args: 163 | data (bytes): A byte string as read from an inotify file descriptor 164 | Returns: 165 | list: list of :attr:`~inotify_simple.Event` namedtuples""" 166 | events = [] 167 | offset = 0 168 | buffer_size = len(data) 169 | while offset < buffer_size: 170 | wd, mask, cookie, namesize = struct.unpack_from( 171 | _EVENT_STRUCT_FORMAT, data, offset 172 | ) 173 | offset += _EVENT_STRUCT_SIZE 174 | name = fsdecode( 175 | ctypes.c_buffer(data[offset : offset + namesize], namesize).value 176 | ) 177 | offset += namesize 178 | events.append(Event(wd, mask, cookie, name)) 179 | return events 180 | 181 | 182 | class flags(enum.IntEnum): 183 | """Inotify flags as defined in ``inotify.h`` but with ``IN_`` prefix 184 | omitted. Includes a convenience method for extracting flags from a mask. 185 | """ 186 | 187 | ACCESS = 0x00000001 #: File was accessed 188 | MODIFY = 0x00000002 #: File was modified 189 | ATTRIB = 0x00000004 #: Metadata changed 190 | CLOSE_WRITE = 0x00000008 #: Writable file was closed 191 | CLOSE_NOWRITE = 0x00000010 #: Unwritable file closed 192 | OPEN = 0x00000020 #: File was opened 193 | MOVED_FROM = 0x00000040 #: File was moved from X 194 | MOVED_TO = 0x00000080 #: File was moved to Y 195 | CREATE = 0x00000100 #: Subfile was created 196 | DELETE = 0x00000200 #: Subfile was deleted 197 | DELETE_SELF = 0x00000400 #: Self was deleted 198 | MOVE_SELF = 0x00000800 #: Self was moved 199 | 200 | UNMOUNT = 0x00002000 #: Backing fs was unmounted 201 | Q_OVERFLOW = 0x00004000 #: Event queue overflowed 202 | IGNORED = 0x00008000 #: File was ignored 203 | 204 | ONLYDIR = 0x01000000 #: only watch the path if it is a directory 205 | DONT_FOLLOW = 0x02000000 #: don't follow a sym link 206 | EXCL_UNLINK = 0x04000000 #: exclude events on unlinked objects 207 | MASK_ADD = 0x20000000 #: add to the mask of an already existing watch 208 | ISDIR = 0x40000000 #: event occurred against dir 209 | ONESHOT = 0x80000000 #: only send event once 210 | 211 | @classmethod 212 | def from_mask(cls, mask): 213 | """Convenience method. Return a list of every flag in a mask.""" 214 | return [flag for flag in cls.__members__.values() if flag & mask] 215 | 216 | 217 | class masks(enum.IntEnum): 218 | """Convenience masks as defined in ``inotify.h`` but with ``IN_`` prefix 219 | omitted.""" 220 | 221 | #: helper event mask equal to ``flags.CLOSE_WRITE | flags.CLOSE_NOWRITE`` 222 | CLOSE = flags.CLOSE_WRITE | flags.CLOSE_NOWRITE 223 | #: helper event mask equal to ``flags.MOVED_FROM | flags.MOVED_TO`` 224 | MOVE = flags.MOVED_FROM | flags.MOVED_TO 225 | 226 | #: bitwise-OR of all the events that can be passed to 227 | #: :func:`~inotify_simple.INotify.add_watch` 228 | ALL_EVENTS = ( 229 | flags.ACCESS 230 | | flags.MODIFY 231 | | flags.ATTRIB 232 | | flags.CLOSE_WRITE 233 | | flags.CLOSE_NOWRITE 234 | | flags.OPEN 235 | | flags.MOVED_FROM 236 | | flags.MOVED_TO 237 | | flags.DELETE 238 | | flags.CREATE 239 | | flags.DELETE_SELF 240 | | flags.MOVE_SELF 241 | ) 242 | -------------------------------------------------------------------------------- /i3configger/ipc.py: -------------------------------------------------------------------------------- 1 | """Inter Process Communication functionality.""" 2 | import logging 3 | import subprocess 4 | from pathlib import Path 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | 9 | def configure(cnf=None, deactivate=False): 10 | if deactivate: 11 | I3.configure(None) 12 | I3bar.configure(None) 13 | Notify.configure(None) 14 | log.info("ipc deactivated") 15 | else: 16 | I3.configure(cnf.payload["main"]["i3_refresh_msg"]) 17 | log.info(f"set i3 refresh method to {I3.refresh.__name__}") 18 | I3bar.configure(cnf.payload["main"]["status_command"]) 19 | log.info(f"set i3bar refresh method to {I3bar.refresh.__name__}") 20 | Notify.configure(cnf.payload["main"]["notify"]) 21 | log.info(f"set notify method to {Notify.send.__name__}") 22 | 23 | 24 | def communicate(msg="new config active", refresh=False, urgency="low"): 25 | if refresh: 26 | I3.refresh() 27 | I3bar.refresh() 28 | Notify.send(msg, urgency=urgency) 29 | 30 | 31 | class I3: 32 | @classmethod 33 | def configure(cls, which): 34 | cls.refresh = cls.METHOD_MAP.get(which, nop) 35 | 36 | @classmethod 37 | def reload_i3(cls): 38 | cls._send_i3_msg("reload") 39 | 40 | @classmethod 41 | def restart_i3(cls): 42 | subprocess.call(["i3-msg", "restart"]) 43 | 44 | refresh = restart_i3 45 | METHOD_MAP = {"restart": restart_i3, "reload": reload_i3} 46 | 47 | @classmethod 48 | def _send_i3_msg(cls, msg): 49 | try: 50 | output = subprocess.check_output(["i3-msg", msg]).decode() 51 | if '"success":true' in output: 52 | return True 53 | return False 54 | except subprocess.CalledProcessError as e: 55 | if msg == "restart" and e.returncode == 1: 56 | log.debug("[IGNORE] exit 1 is ok for restart") 57 | return True 58 | 59 | @classmethod 60 | def get_config_error_report(cls, path): 61 | cmd = ["i3", "-C", "-c", str(path)] 62 | try: 63 | return subprocess.check_output(cmd).decode() 64 | except subprocess.CalledProcessError as e: 65 | return e.output.decode() 66 | except FileNotFoundError as e: 67 | assert Path(path).exists(), path 68 | assert "No such file or directory: 'i3'" in e.strerror 69 | log.warning("[IGNORE] crashed - no i3 -> assuming test system") 70 | return "" 71 | 72 | 73 | class Notify: 74 | @classmethod 75 | def configure(cls, notify): 76 | cls.send = cls.notify_send if notify else nop 77 | 78 | @classmethod 79 | def notify_send(cls, msg, urgency="low"): 80 | """urgency levels: low, normal, critical""" 81 | subprocess.check_call( 82 | ["notify-send", "-a", "i3configger", "-t", "1", "-u", urgency, msg] 83 | ) 84 | 85 | send = notify_send 86 | 87 | 88 | class I3bar: 89 | command = "i3status" 90 | 91 | @classmethod 92 | def configure(cls, command): 93 | cls.command = command 94 | cls.refresh = cls.refresh if command else nop 95 | 96 | @classmethod 97 | def send_sigusr1(cls): 98 | try: 99 | subprocess.check_output(["killall", "-SIGUSR1", cls.command]) 100 | except subprocess.CalledProcessError as e: 101 | log.debug("[IGNORE] failed status refresh: %s", e) 102 | 103 | refresh = send_sigusr1 104 | 105 | 106 | def nop(*args, **kwargs): 107 | log.debug(f"just ignored everything to do with {args} and {kwargs}") 108 | -------------------------------------------------------------------------------- /i3configger/message.py: -------------------------------------------------------------------------------- 1 | """Functionality implementing the messaging mechanism.""" 2 | import logging 3 | from pathlib import Path 4 | 5 | from i3configger import config, context, exc, partials 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | class CMD: 11 | MERGE = "merge" 12 | PRUNE = "prune" 13 | SELECT = "select" 14 | SELECT_NEXT = "select-next" 15 | SELECT_PREVIOUS = "select-previous" 16 | SET = "set" 17 | SHADOW = "shadow" 18 | 19 | @classmethod 20 | def get_all_commands(cls): 21 | return [v for k, v in cls.__dict__.items() if k[0].isupper() and k[0] != "_"] 22 | 23 | 24 | def save(message): 25 | cnf = config.I3configgerConfig(load=False) 26 | prts = partials.create(cnf.partialsPath) 27 | messenger = Messenger(cnf.messagesPath, prts, message) 28 | messenger.digest_message() 29 | config.freeze(cnf.messagesPath, messenger.payload) 30 | 31 | 32 | class Messenger: 33 | DEL = "del" 34 | """signal to delete a key in shadow or set""" 35 | 36 | def __init__(self, messagesPath, prts, message=None): 37 | self.messagesPath = messagesPath 38 | self.prts = prts 39 | self.message = message 40 | if message: 41 | if len(message) < 2: 42 | raise exc.UserError(f"message needs at least key and value ({message})") 43 | self.command, self.key, *rest = message 44 | self.value = rest[0] if rest else "" 45 | if self.command != CMD.SHADOW and ":" in self.key: 46 | raise exc.UserError(f"nesting of keys only sensible with {CMD.SHADOW}") 47 | self.payload = self.fetch_messages(exludes=[config.I3BAR]) 48 | log.debug(f"send message '{message}' to {messagesPath}") 49 | 50 | def digest_message(self): 51 | try: 52 | self.COMMAND_METHOD_MAP[self.command](self) 53 | except KeyError: 54 | raise exc.UserError( 55 | f"Unknown command: {self.command}. " 56 | f"Use one of {', '.join(CMD.get_all_commands())}" 57 | ) 58 | 59 | def _process_merge(self): 60 | self._transform(context.merge) 61 | 62 | def _process_prune(self): 63 | self._transform(context.prune) 64 | 65 | def _transform(self, func): 66 | path = Path(self.key).expanduser() 67 | if not path.is_absolute(): 68 | path = self.messagesPath.parent / path 69 | self.payload = func(self.payload, config.fetch(path)) 70 | 71 | def _process_set(self): 72 | if self.value.lower() == self.DEL: 73 | del self.payload[CMD.SET][config.MARK.VAR + self.key] 74 | else: 75 | self.payload[CMD.SET][config.MARK.VAR + self.key] = self.value 76 | 77 | def _process_select(self): 78 | candidates = partials.find(self.prts, self.key) 79 | if not candidates: 80 | raise exc.MessageError(f"No candidates for {self.message} in {self.prts}") 81 | candidate = partials.find(self.prts, self.key, self.value) 82 | if not candidate: 83 | raise exc.MessageError(f"No candidates for {self.message} in {candidates}") 84 | if self.value and self.value.lower() == self.DEL: 85 | del self.payload[CMD.SELECT][self.key] 86 | else: 87 | self.payload[CMD.SELECT][self.key] = candidate.value 88 | 89 | def _process_shadow(self): 90 | """Shadow arbitrary settings made in i3configger.json. 91 | 92 | key:deeper:deepest[...] -> [key][deeper][deepest][...] 93 | """ 94 | parts = self.key.split(":") 95 | current = self.payload[CMD.SHADOW] 96 | while True: 97 | part = parts.pop(0) 98 | if parts: 99 | current[part] = {} 100 | current = current[part] 101 | else: 102 | if self.value is not None and self.value.lower() == self.DEL: 103 | del current[part] 104 | else: 105 | current[part] = self.value 106 | break 107 | 108 | def _process_select_shift(self): 109 | candidates = partials.find(self.prts, self.key) 110 | if not candidates: 111 | raise exc.MessageError(f"No candidates for {self.message} in {self.prts}") 112 | if self.command == CMD.SELECT_PREVIOUS: 113 | candidates = list(reversed(candidates)) 114 | current = self.payload["select"].get(self.key) or candidates[0].key 115 | for idx, candidate in enumerate(candidates): 116 | if candidate.value == current: 117 | try: 118 | new = candidates[idx + 1] 119 | except IndexError: 120 | new = candidates[0] 121 | break 122 | else: 123 | new = candidates[0] 124 | log.info(f"select {self.key}.{new}") 125 | self.payload[CMD.SELECT][self.key] = new.value 126 | 127 | def fetch_messages(self, exludes=None): 128 | if self.messagesPath.exists(): 129 | messages = config.fetch(self.messagesPath) 130 | else: 131 | messages = {} 132 | self.ensure_message_keys(messages, self.prts, exludes) 133 | return messages 134 | 135 | def ensure_message_keys(self, state, prts, exludes): 136 | if CMD.SELECT not in state: 137 | initialSelects = {} 138 | for prt in prts: 139 | if not prt.needsSelection: 140 | continue 141 | if prt.key not in initialSelects and prt.key not in exludes: 142 | initialSelects[prt.key] = prt.value 143 | state[CMD.SELECT] = initialSelects 144 | if CMD.SET not in state: 145 | state[CMD.SET] = {} 146 | if CMD.SHADOW not in state: 147 | state[CMD.SHADOW] = {} 148 | 149 | COMMAND_METHOD_MAP = { 150 | CMD.MERGE: _process_merge, 151 | CMD.PRUNE: _process_prune, 152 | CMD.SELECT: _process_select, 153 | CMD.SELECT_NEXT: _process_select_shift, 154 | CMD.SELECT_PREVIOUS: _process_select_shift, 155 | CMD.SET: _process_set, 156 | CMD.SHADOW: _process_shadow, 157 | } 158 | -------------------------------------------------------------------------------- /i3configger/partials.py: -------------------------------------------------------------------------------- 1 | """Functionality to create, find and select partial configurations.""" 2 | import logging 3 | import pprint 4 | import socket 5 | from functools import total_ordering 6 | from pathlib import Path 7 | from typing import Union, List 8 | 9 | from i3configger import config, exc 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | SPECIAL_SELECTORS = {"hostname": socket.gethostname()} 14 | EXCLUDE_MARKER = "." 15 | """config files starting with a dot are always excluded""" 16 | 17 | 18 | @total_ordering 19 | class Partial: 20 | def __init__(self, path: Path): 21 | self.path = path 22 | self.name = self.path.stem 23 | self.selectors = self.name.split(".") 24 | self.needsSelection = len(self.selectors) > 1 25 | self.key = self.selectors[0] if self.needsSelection else None 26 | self.value = self.selectors[1] if self.needsSelection else None 27 | self.lines = self.path.read_text().splitlines() 28 | 29 | def __repr__(self): 30 | return f"{self.__class__.__name__}({self.path.name})" 31 | 32 | def __lt__(self, other): 33 | return self.name < other.name 34 | 35 | def get_pruned_content(self) -> str: 36 | """pruned content or '' if file only contains vars and comments""" 37 | lines = [l for l in self.lines if not l.strip().startswith(config.MARK.SET)] 38 | if not self.contain_something(lines): 39 | return "" 40 | while lines and not lines[0].strip(): 41 | lines.pop(0) 42 | while lines and not lines[-1].strip(): 43 | lines.pop() 44 | joinedLines = "\n".join(lines) 45 | return f"### {self.path.name} ###\n{joinedLines}\n\n" 46 | 47 | @staticmethod 48 | def contain_something(lines): 49 | for line in lines: 50 | line = line.strip() 51 | if not line: 52 | continue 53 | if line.startswith(config.MARK.COMMENT): 54 | continue 55 | if line.startswith(config.MARK.SET): 56 | continue 57 | return True 58 | 59 | @property 60 | def context(self): 61 | ctx = {} 62 | for line in [ 63 | l.strip() for l in self.lines if l.strip().startswith(config.MARK.SET) 64 | ]: 65 | payload = line.split(maxsplit=1)[1] 66 | key, value = payload.split(maxsplit=1) 67 | ctx[key] = value 68 | return ctx 69 | 70 | 71 | def find( 72 | prts: List[Partial], key: str, value: str = None 73 | ) -> Union[Partial, List[Partial]]: 74 | findings = [] 75 | for prt in prts: 76 | if prt.key != key: 77 | continue 78 | if prt.value == value: 79 | return prt 80 | elif not value: 81 | findings.append(prt) 82 | return findings 83 | 84 | 85 | def select(partials, selection, excludes=None) -> List[Partial]: 86 | def _select(): 87 | selected.append(partial) 88 | if partial.needsSelection: 89 | del selection[partial.key] 90 | 91 | for key, value in SPECIAL_SELECTORS.items(): 92 | if key not in selection: 93 | selection[key] = value 94 | selected: List[Partial] = [] 95 | for partial in partials: 96 | if partial.needsSelection: 97 | if excludes and partial.key in excludes: 98 | log.debug(f"[IGNORE] {partial} (in {excludes})") 99 | continue 100 | if ( 101 | selection 102 | and partial.key in selection 103 | and partial.value == selection.get(partial.key) 104 | ): 105 | _select() 106 | else: 107 | _select() 108 | log.debug(f"selected:\n{pprint.pformat(selected)}") 109 | if selection and not all(k in SPECIAL_SELECTORS for k in selection): 110 | raise exc.ConfigError(f"selection processed incompletely: {selection}") 111 | return selected 112 | 113 | 114 | def create(partialsPath) -> List[Partial]: 115 | partialsPath = Path(partialsPath) 116 | assert partialsPath.is_dir(), partialsPath 117 | prts = [] 118 | for path in partialsPath.glob(f"*{config.SUFFIX}"): 119 | if path.name.startswith(EXCLUDE_MARKER): 120 | log.info(f"excluding {path} because it starts with {EXCLUDE_MARKER}") 121 | continue 122 | prts.append(Partial(path)) 123 | if not prts: 124 | raise exc.PartialsError(f"no '*{config.SUFFIX}' at {partialsPath}") 125 | return sorted(prts) 126 | -------------------------------------------------------------------------------- /i3configger/watch.py: -------------------------------------------------------------------------------- 1 | """Functionality to watch the configuration dir to trigger rebuilds on changes.""" 2 | import time 3 | 4 | import logging 5 | import os 6 | import sys 7 | from pathlib import Path 8 | 9 | import daemon # type: ignore 10 | import psutil # type: ignore 11 | 12 | from i3configger import base, build, exc, ipc, config 13 | from i3configger.inotify_simple import INotify, flags 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | _MASK = ( 19 | flags.CREATE 20 | | flags.ATTRIB 21 | | flags.DELETE 22 | | flags.MODIFY 23 | | flags.CLOSE_WRITE 24 | | flags.MOVED_FROM 25 | | flags.MOVED_TO 26 | | flags.DELETE_SELF 27 | | flags.MOVE_SELF 28 | ) 29 | """Tell inotify to trigger on changes""" 30 | 31 | 32 | def watch_guarded(): 33 | while True: 34 | try: 35 | watch_unguarded() 36 | except exc.I3configgerException as e: 37 | ipc.communicate("WARNING", urgency="normal") 38 | log.warning(str(e)) 39 | except Exception as e: 40 | ipc.communicate("ERROR", urgency="critical") 41 | log.error(str(e)) 42 | 43 | 44 | def watch_unguarded(): 45 | cnf = config.I3configgerConfig(load=False) 46 | watcher = INotify() 47 | watcher.add_watch(str(cnf.partialsPath).encode(), mask=_MASK) 48 | log.debug(f"start watching {cnf.partialsPath}") 49 | while True: 50 | events = watcher.read(read_delay=50) 51 | log.debug(f"events: {[f'{e[3]}:m={e[1]}' for e in events]}") 52 | if not events: 53 | continue 54 | paths = [cnf.partialsPath / e[-1] for e in events] 55 | if any(p.suffix in [config.SUFFIX, ".json"] for p in paths): 56 | build.build_all() 57 | ipc.communicate(refresh=True) 58 | 59 | 60 | def get_i3configger_process(): 61 | """should always be max one, but you never know ...""" 62 | all_ = [p for p in psutil.process_iter() if p.name() == "i3configger"] 63 | others = [p for p in all_ if p.pid != os.getpid()] 64 | if len(others) == 1: 65 | return others[0] 66 | elif len(others) > 1: 67 | raise exc.I3configgerException( 68 | f"More than one i3configger running: {others}" 69 | f"If this happens again, please file an issue " 70 | f"and tell me how you did it." 71 | ) 72 | 73 | 74 | def daemonized(verbosity, logPath): 75 | process = get_i3configger_process() 76 | if process: 77 | raise exc.UserError(f"i3configger already running ({process})") 78 | context = daemon.DaemonContext(working_directory=Path(__file__).parent) 79 | if verbosity > 2: 80 | # spew output to terminal from where daemon was started 81 | context.stdout = sys.stdout 82 | context.stderr = sys.stderr 83 | with context: 84 | base.configure_logging(verbosity, logPath, isDaemon=True) 85 | watch_guarded() 86 | 87 | 88 | def exorcise(): 89 | process = get_i3configger_process() 90 | if not process: 91 | raise exc.UserError("no daemon running - nothing to kill") 92 | process.kill() 93 | # sometimes the process needs a bit longer to die 94 | for _ in range(20): 95 | if not process.is_running(): 96 | break 97 | time.sleep(0.1) 98 | else: 99 | raise exc.I3configgerException(f"process {process} does not want to die") 100 | log.info(f"killed {process}") 101 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: i3configger 2 | pages: 3 | - Home: index.md 4 | - Installation: installation.md 5 | - Getting started: getting-started.md 6 | - Concept: concept.md 7 | - Build process: build-process.md 8 | - Resources: resources.md 9 | 10 | extra: 11 | logo: _static/logo.png 12 | author: 13 | github: obestwalter 14 | twitter: obestwalter 15 | extra_css: 16 | - _static/extra.css 17 | #include_404: True 18 | markdown_extensions: 19 | - admonition 20 | - toc: 21 | permalink: True #¶ 22 | repo_url: https://github.com/obestwalter/i3configger 23 | site_name: i3configger 24 | strict: True 25 | theme: material 26 | -------------------------------------------------------------------------------- /release.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import logging 4 | import shutil 5 | from pathlib import Path 6 | from plumbum import local, ProcessExecutionError 7 | from twine.commands.check import check as twine_check 8 | from twine.commands.upload import upload as twine_upload 9 | from twine.settings import Settings as twine_Settings 10 | 11 | log = logging.getLogger(__name__) 12 | PROJECT_ROOT_PATH = Path(__file__).parent 13 | DIST_PATH = PROJECT_ROOT_PATH / "dist" 14 | git = local["git"] 15 | 16 | 17 | def main(): 18 | logging.basicConfig(level=logging.DEBUG) 19 | if repo_is_dirty(): 20 | raise EnvironmentError("repo is dirty!") 21 | if len(sys.argv) < 2: 22 | raise ValueError("need a version!") 23 | version = sys.argv[1] 24 | dryRun = len(sys.argv) > 2 25 | with local.cwd(PROJECT_ROOT_PATH): 26 | release(version, dryRun) 27 | 28 | 29 | def release(version, dryRun): 30 | tidy_up() 31 | tag_repo(version) 32 | build_dists() 33 | dists = get_dists() 34 | if not long_description_is_ok(dists): 35 | sys.exit("Long description not ok.") 36 | if dryRun: 37 | sys.exit(f"This was a dry run for version {version}.") 38 | upload_dists(dists) 39 | push_released_tag(version) 40 | 41 | 42 | def repo_is_dirty(): 43 | try: 44 | git("diff", "--quiet") 45 | return False 46 | except ProcessExecutionError as e: 47 | if e.retcode != 1: 48 | raise 49 | return True 50 | 51 | 52 | def tidy_up(): 53 | for path in [DIST_PATH, PROJECT_ROOT_PATH / "build"]: 54 | if path.exists(): 55 | shutil.rmtree(path) 56 | 57 | 58 | def tag_repo(version): 59 | git("tag", version) 60 | 61 | 62 | def build_dists(): 63 | python = local["python"] 64 | python("setup.py", "sdist", "bdist_wheel") 65 | 66 | 67 | def get_dists(): 68 | distPath = PROJECT_ROOT_PATH / "dist" 69 | return list(str(dist) for dist in distPath.glob("*")) 70 | 71 | 72 | def long_description_is_ok(dists): 73 | log.info(f"check {dists}") 74 | return not twine_check(dists) 75 | 76 | 77 | def upload_dists(dists): 78 | settings = twine_Settings() 79 | twine_upload(settings, dists) 80 | 81 | 82 | def push_released_tag(version): 83 | git("push", "origin", version) 84 | 85 | 86 | if __name__ == "__main__": 87 | sys.exit(main()) 88 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from setuptools import find_packages, setup 4 | 5 | 6 | def make_long_description(): 7 | here = Path(__file__).parent 8 | readme = (here / "README.md").read_text() 9 | changelog = (here / "CHANGELOG.md").read_text() 10 | return f"{readme}\n\n{changelog}" 11 | 12 | 13 | kwargs = dict( 14 | name="i3configger", 15 | author="Oliver Bestwalter", 16 | url="https://github.com/obestwalter/i3configger", 17 | description="i3 config manipulation tool", 18 | long_description=make_long_description(), 19 | long_description_content_type="text/markdown", 20 | use_scm_version=True, 21 | python_requires=">=3.6", 22 | setup_requires=["setuptools_scm"], 23 | entry_points={"console_scripts": ["i3configger = i3configger.cli:main"]}, 24 | install_requires=["psutil", "python-daemon"], 25 | extras_require={ 26 | "lint": ["pre-commit"], 27 | "test": ["pytest", "coverage"], 28 | "docs": ["mkdocs", "mkdocs-material"], 29 | "release": ["plumbum", "twine", "readme_renderer[md]"], 30 | }, 31 | packages=find_packages(), 32 | classifiers=[ 33 | "Development Status :: 4 - Beta", 34 | "Operating System :: POSIX :: Linux", 35 | "License :: OSI Approved :: MIT License", 36 | "Environment :: Console", 37 | "Topic :: Utilities", 38 | "Programming Language :: Python", 39 | "Programming Language :: Python :: 3.6", 40 | "Programming Language :: Python :: 3.7", 41 | ], 42 | ) 43 | 44 | 45 | if __name__ == "__main__": 46 | setup(**kwargs) 47 | -------------------------------------------------------------------------------- /tests/examples/0-default/config: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Built by i3configger /from/some/directory/who/cares (some time after 1972) # 3 | ############################################################################### 4 | 5 | ### config.conf ### 6 | # i3 config file (v4) 7 | # 8 | # Please see http://i3wm.org/docs/userguide.html for a complete reference! 9 | # 10 | # This config file uses keycodes (bindsym) and was written for the QWERTY 11 | # layout. 12 | # 13 | # To get a config file with the same key positions, but for your current 14 | # layout, use the i3-config-wizard 15 | # 16 | 17 | # Font for window titles. Will also be used by the bar unless a different font 18 | # is used in the bar {} block below. 19 | font pango:monospace 8 20 | 21 | # This font is widely installed, provides lots of unicode glyphs, right-to-left 22 | # text rendering and scalability on retina/hidpi displays (thanks to pango). 23 | #font pango:DejaVu Sans Mono 8 24 | 25 | # Before i3 v4.8, we used to recommend this one as the default: 26 | # font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 27 | # The font above is very space-efficient, that is, it looks good, sharp and 28 | # clear in small sizes. However, its unicode glyph coverage is limited, the old 29 | # X core fonts rendering does not support right-to-left and this being a bitmap 30 | # font, it doesn’t scale on retina/hidpi displays. 31 | 32 | # use these keys for focus, movement, and resize directions when reaching for 33 | # the arrows is not convenient 34 | 35 | # use Mouse+Mod1 to drag floating windows to their wanted position 36 | floating_modifier Mod1 37 | 38 | # start a terminal 39 | bindsym Mod1+Return exec i3-sensible-terminal 40 | 41 | # kill focused window 42 | bindsym Mod1+Shift+q kill 43 | 44 | # start dmenu (a program launcher) 45 | bindsym Mod1+d exec dmenu_run 46 | # There also is the (new) i3-dmenu-desktop which only displays applications 47 | # shipping a .desktop file. It is a wrapper around dmenu, so you need that 48 | # installed. 49 | # bindsym Mod1+d exec --no-startup-id i3-dmenu-desktop 50 | 51 | # change focus 52 | bindsym Mod1+j focus left 53 | bindsym Mod1+k focus down 54 | bindsym Mod1+l focus up 55 | bindsym Mod1+semicolon focus right 56 | 57 | # alternatively, you can use the cursor keys: 58 | bindsym Mod1+Left focus left 59 | bindsym Mod1+Down focus down 60 | bindsym Mod1+Up focus up 61 | bindsym Mod1+Right focus right 62 | 63 | # move focused window 64 | bindsym Mod1+Shift+j move left 65 | bindsym Mod1+Shift+k move down 66 | bindsym Mod1+Shift+l move up 67 | bindsym Mod1+Shift+semicolon move right 68 | 69 | # alternatively, you can use the cursor keys: 70 | bindsym Mod1+Shift+Left move left 71 | bindsym Mod1+Shift+Down move down 72 | bindsym Mod1+Shift+Up move up 73 | bindsym Mod1+Shift+Right move right 74 | 75 | # split in horizontal orientation 76 | bindsym Mod1+h split h 77 | 78 | # split in vertical orientation 79 | bindsym Mod1+v split v 80 | 81 | # enter fullscreen mode for the focused container 82 | bindsym Mod1+f fullscreen toggle 83 | 84 | # change container layout (stacked, tabbed, toggle split) 85 | bindsym Mod1+s layout stacking 86 | bindsym Mod1+w layout tabbed 87 | bindsym Mod1+e layout toggle split 88 | 89 | # toggle tiling / floating 90 | bindsym Mod1+Shift+space floating toggle 91 | 92 | # change focus between tiling / floating windows 93 | bindsym Mod1+space focus mode_toggle 94 | 95 | # focus the parent container 96 | bindsym Mod1+a focus parent 97 | 98 | # focus the child container 99 | #bindsym Mod1+d focus child 100 | 101 | # move the currently focused window to the scratchpad 102 | bindsym Mod1+Shift+minus move scratchpad 103 | 104 | # Show the next scratchpad window or hide the focused scratchpad window. 105 | # If there are multiple scratchpad windows, this command cycles through them. 106 | bindsym Mod1+minus scratchpad show 107 | 108 | # switch to workspace 109 | bindsym Mod1+1 workspace 1 110 | bindsym Mod1+2 workspace 2 111 | bindsym Mod1+3 workspace 3 112 | bindsym Mod1+4 workspace 4 113 | bindsym Mod1+5 workspace 5 114 | bindsym Mod1+6 workspace 6 115 | bindsym Mod1+7 workspace 7 116 | bindsym Mod1+8 workspace 8 117 | bindsym Mod1+9 workspace 9 118 | bindsym Mod1+0 workspace 10 119 | 120 | # move focused container to workspace 121 | bindsym Mod1+Shift+1 move container to workspace 1 122 | bindsym Mod1+Shift+2 move container to workspace 2 123 | bindsym Mod1+Shift+3 move container to workspace 3 124 | bindsym Mod1+Shift+4 move container to workspace 4 125 | bindsym Mod1+Shift+5 move container to workspace 5 126 | bindsym Mod1+Shift+6 move container to workspace 6 127 | bindsym Mod1+Shift+7 move container to workspace 7 128 | bindsym Mod1+Shift+8 move container to workspace 8 129 | bindsym Mod1+Shift+9 move container to workspace 9 130 | bindsym Mod1+Shift+0 move container to workspace 10 131 | 132 | # reload the configuration file 133 | bindsym Mod1+Shift+c reload 134 | # restart i3 inplace (preserves your layout/session, can be used to upgrade i3) 135 | bindsym Mod1+Shift+r restart 136 | # exit i3 (logs you out of your X session) 137 | bindsym Mod1+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'" 138 | 139 | # resize window (you can also use the mouse for that) 140 | mode "resize" { 141 | # These bindings trigger as soon as you enter the resize mode 142 | 143 | # Pressing left will shrink the window’s width. 144 | # Pressing right will grow the window’s width. 145 | # Pressing up will shrink the window’s height. 146 | # Pressing down will grow the window’s height. 147 | bindsym j resize shrink width 10 px or 10 ppt 148 | bindsym k resize grow height 10 px or 10 ppt 149 | bindsym l resize shrink height 10 px or 10 ppt 150 | bindsym semicolon resize grow width 10 px or 10 ppt 151 | 152 | # same bindings, but for the arrow keys 153 | bindsym Left resize shrink width 10 px or 10 ppt 154 | bindsym Down resize grow height 10 px or 10 ppt 155 | bindsym Up resize shrink height 10 px or 10 ppt 156 | bindsym Right resize grow width 10 px or 10 ppt 157 | 158 | # back to normal: Enter or Escape 159 | bindsym Return mode "default" 160 | bindsym Escape mode "default" 161 | } 162 | 163 | bindsym Mod1+r mode "resize" 164 | 165 | # Start i3bar to display a workspace bar (plus the system information i3status 166 | # finds out, if available) 167 | bar { 168 | status_command i3status 169 | } 170 | -------------------------------------------------------------------------------- /tests/examples/1-partials/config: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Built by i3configger /from/some/directory/who/cares (some time after 1972) # 3 | ############################################################################### 4 | 5 | ### basic-settings.conf ### 6 | # Font for window titles. Will also be used by the bar unless a different font 7 | # is used in the bar {} block below. 8 | font pango:monospace 8 9 | 10 | # This font is widely installed, provides lots of unicode glyphs, right-to-left 11 | # text rendering and scalability on retina/hidpi displays (thanks to pango). 12 | #font pango:DejaVu Sans Mono 8 13 | 14 | # Before i3 v4.8, we used to recommend this one as the default: 15 | # font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 16 | # The font above is very space-efficient, that is, it looks good, sharp and 17 | # clear in small sizes. However, its unicode glyph coverage is limited, the old 18 | # X core fonts rendering does not support right-to-left and this being a bitmap 19 | # font, it doesn’t scale on retina/hidpi displays. 20 | 21 | # use these keys for focus, movement, and resize directions when reaching for 22 | # the arrows is not convenient 23 | 24 | # use Mouse+Mod1 to drag floating windows to their wanted position 25 | floating_modifier Mod1 26 | 27 | 28 | ### key-bindings.conf ### 29 | # start a terminal 30 | bindsym Mod1+Return exec i3-sensible-terminal 31 | 32 | # kill focused window 33 | bindsym Mod1+Shift+q kill 34 | 35 | # start dmenu (a program launcher) 36 | bindsym Mod1+d exec dmenu_run 37 | # There also is the (new) i3-dmenu-desktop which only displays applications 38 | # shipping a .desktop file. It is a wrapper around dmenu, so you need that 39 | # installed. 40 | # bindsym Mod1+d exec --no-startup-id i3-dmenu-desktop 41 | 42 | # change focus 43 | bindsym Mod1+j focus left 44 | bindsym Mod1+k focus down 45 | bindsym Mod1+l focus up 46 | bindsym Mod1+semicolon focus right 47 | 48 | # alternatively, you can use the cursor keys: 49 | bindsym Mod1+Left focus left 50 | bindsym Mod1+Down focus down 51 | bindsym Mod1+Up focus up 52 | bindsym Mod1+Right focus right 53 | 54 | # move focused window 55 | bindsym Mod1+Shift+j move left 56 | bindsym Mod1+Shift+k move down 57 | bindsym Mod1+Shift+l move up 58 | bindsym Mod1+Shift+semicolon move right 59 | 60 | # alternatively, you can use the cursor keys: 61 | bindsym Mod1+Shift+Left move left 62 | bindsym Mod1+Shift+Down move down 63 | bindsym Mod1+Shift+Up move up 64 | bindsym Mod1+Shift+Right move right 65 | 66 | # split in horizontal orientation 67 | bindsym Mod1+h split h 68 | 69 | # split in vertical orientation 70 | bindsym Mod1+v split v 71 | 72 | # enter fullscreen mode for the focused container 73 | bindsym Mod1+f fullscreen toggle 74 | 75 | # change container layout (stacked, tabbed, toggle split) 76 | bindsym Mod1+s layout stacking 77 | bindsym Mod1+w layout tabbed 78 | bindsym Mod1+e layout toggle split 79 | 80 | # toggle tiling / floating 81 | bindsym Mod1+Shift+space floating toggle 82 | 83 | # change focus between tiling / floating windows 84 | bindsym Mod1+space focus mode_toggle 85 | 86 | # focus the parent container 87 | bindsym Mod1+a focus parent 88 | 89 | # focus the child container 90 | #bindsym Mod1+d focus child 91 | 92 | # move the currently focused window to the scratchpad 93 | bindsym Mod1+Shift+minus move scratchpad 94 | 95 | # Show the next scratchpad window or hide the focused scratchpad window. 96 | # If there are multiple scratchpad windows, this command cycles through them. 97 | bindsym Mod1+minus scratchpad show 98 | 99 | # switch to workspace 100 | bindsym Mod1+1 workspace 1 101 | bindsym Mod1+2 workspace 2 102 | bindsym Mod1+3 workspace 3 103 | bindsym Mod1+4 workspace 4 104 | bindsym Mod1+5 workspace 5 105 | bindsym Mod1+6 workspace 6 106 | bindsym Mod1+7 workspace 7 107 | bindsym Mod1+8 workspace 8 108 | bindsym Mod1+9 workspace 9 109 | bindsym Mod1+0 workspace 10 110 | 111 | # move focused container to workspace 112 | bindsym Mod1+Shift+1 move container to workspace 1 113 | bindsym Mod1+Shift+2 move container to workspace 2 114 | bindsym Mod1+Shift+3 move container to workspace 3 115 | bindsym Mod1+Shift+4 move container to workspace 4 116 | bindsym Mod1+Shift+5 move container to workspace 5 117 | bindsym Mod1+Shift+6 move container to workspace 6 118 | bindsym Mod1+Shift+7 move container to workspace 7 119 | bindsym Mod1+Shift+8 move container to workspace 8 120 | bindsym Mod1+Shift+9 move container to workspace 9 121 | bindsym Mod1+Shift+0 move container to workspace 10 122 | 123 | # reload the configuration file 124 | bindsym Mod1+Shift+c reload 125 | # restart i3 inplace (preserves your layout/session, can be used to upgrade i3) 126 | bindsym Mod1+Shift+r restart 127 | # exit i3 (logs you out of your X session) 128 | bindsym Mod1+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'" 129 | 130 | 131 | ### mode-resize.conf ### 132 | # resize window (you can also use the mouse for that) 133 | mode "resize" { 134 | # These bindings trigger as soon as you enter the resize mode 135 | 136 | # Pressing left will shrink the window’s width. 137 | # Pressing right will grow the window’s width. 138 | # Pressing up will shrink the window’s height. 139 | # Pressing down will grow the window’s height. 140 | bindsym j resize shrink width 10 px or 10 ppt 141 | bindsym k resize grow height 10 px or 10 ppt 142 | bindsym l resize shrink height 10 px or 10 ppt 143 | bindsym semicolon resize grow width 10 px or 10 ppt 144 | 145 | # same bindings, but for the arrow keys 146 | bindsym Left resize shrink width 10 px or 10 ppt 147 | bindsym Down resize grow height 10 px or 10 ppt 148 | bindsym Up resize shrink height 10 px or 10 ppt 149 | bindsym Right resize grow width 10 px or 10 ppt 150 | 151 | # back to normal: Enter or Escape 152 | bindsym Return mode "default" 153 | bindsym Escape mode "default" 154 | } 155 | 156 | bindsym Mod1+r mode "resize" 157 | 158 | 159 | ### i3bar.tpl.conf ### 160 | # Start i3bar to display a workspace bar (plus the system information i3status 161 | # finds out, if available) 162 | bar { 163 | # i3configger note: 164 | # additionally to all variables set in the configuration files 165 | # you can access the settings from i3configger.json here 166 | # They will be added to the context from bars->targets vars 167 | # they are also pre populated by the bars->defaults 168 | status_command i3status -c ~/.i3/i3bar.default.conf 169 | 170 | # NOTE instead of ~/.i3 you could use the target variable from the json 171 | # It is only not used in the example because this doubles as a test 172 | # in CI. 173 | # So this needs some extra work to make it pass also on CI. 174 | } 175 | -------------------------------------------------------------------------------- /tests/examples/1-partials/i3bar.default.conf: -------------------------------------------------------------------------------- 1 | ### i3bar.default.conf ### 2 | general { 3 | output_format = "dzen2" 4 | colors = true 5 | interval = 5 6 | } 7 | 8 | order += "ipv6" 9 | order += "disk /" 10 | order += "run_watch DHCP" 11 | order += "run_watch VPNC" 12 | order += "path_exists VPN" 13 | order += "wireless wlan0" 14 | order += "ethernet eth0" 15 | order += "battery 0" 16 | order += "cpu_temperature 0" 17 | order += "load" 18 | order += "tztime local" 19 | order += "tztime berlin" 20 | 21 | wireless wlan0 { 22 | format_up = "W: (%quality at %essid, %bitrate) %ip" 23 | format_down = "W: down" 24 | } 25 | 26 | ethernet eth0 { 27 | # if you use %speed, i3status requires the cap_net_admin capability 28 | format_up = "E: %ip (%speed)" 29 | format_down = "E: down" 30 | } 31 | 32 | battery 0 { 33 | format = "%status %percentage %remaining %emptytime" 34 | format_down = "No battery" 35 | status_chr = "⚡ CHR" 36 | status_bat = "🔋 BAT" 37 | status_unk = "? UNK" 38 | status_full = "☻ FULL" 39 | path = "/sys/class/power_supply/BAT%d/uevent" 40 | low_threshold = 10 41 | } 42 | 43 | run_watch DHCP { 44 | pidfile = "/var/run/dhclient*.pid" 45 | } 46 | 47 | run_watch VPNC { 48 | # file containing the PID of a vpnc process 49 | pidfile = "/var/run/vpnc/pid" 50 | } 51 | 52 | path_exists VPN { 53 | # path exists when a VPN tunnel launched by nmcli/nm-applet is active 54 | path = "/proc/sys/net/ipv4/conf/tun0" 55 | } 56 | 57 | tztime local { 58 | format = "%Y-%m-%d %H:%M:%S" 59 | } 60 | 61 | tztime berlin { 62 | format = "%Y-%m-%d %H:%M:%S %Z" 63 | timezone = "Europe/Berlin" 64 | } 65 | 66 | load { 67 | format = "%5min" 68 | } 69 | 70 | cpu_temperature 0 { 71 | format = "T: %degrees °C" 72 | path = "/sys/devices/platform/coretemp.0/temp1_input" 73 | } 74 | 75 | disk "/" { 76 | format = "%free" 77 | } 78 | -------------------------------------------------------------------------------- /tests/examples/2-bars/config: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Built by i3configger /from/some/directory/who/cares (some time after 1972) # 3 | ############################################################################### 4 | 5 | ### filler.conf ### 6 | # Theoretically the config could be empty, but i3 -C crashes if it is only 7 | # passed bar settings and not at least one other config value 8 | workspace_layout tabbed 9 | 10 | 11 | ### i3bar.tpl.conf ### 12 | # Start i3bar to display a workspace bar (plus the system information i3status 13 | # finds out, if available) 14 | bar { 15 | # i3configger note: 16 | # additionally to all variables set in the configuration files 17 | # you can access the settings from i3configger.json here 18 | # They will be added to the context from bars->targets 19 | # they are pre populated by the bars->defaults 20 | status_command i3status -c ~/.i3/i3bar.full.conf 21 | output DP-3 22 | mode dock 23 | position bottom 24 | } 25 | ### i3bar.tpl.conf ### 26 | # Start i3bar to display a workspace bar (plus the system information i3status 27 | # finds out, if available) 28 | bar { 29 | # i3configger note: 30 | # additionally to all variables set in the configuration files 31 | # you can access the settings from i3configger.json here 32 | # They will be added to the context from bars->targets 33 | # they are pre populated by the bars->defaults 34 | status_command i3status -c ~/.i3/i3bar.clock.conf 35 | output DP-4 36 | mode dock 37 | position top 38 | } 39 | -------------------------------------------------------------------------------- /tests/examples/2-bars/i3bar.clock.conf: -------------------------------------------------------------------------------- 1 | ### i3bar.clock.conf ### 2 | general { 3 | output_format = "i3bar" 4 | colors = true 5 | interval = 1 6 | } 7 | 8 | order = "time" 9 | 10 | time { 11 | format = "%Y-%m-%d %H:%M:%S" 12 | } 13 | -------------------------------------------------------------------------------- /tests/examples/2-bars/i3bar.full.conf: -------------------------------------------------------------------------------- 1 | ### i3bar.full.conf ### 2 | general { 3 | output_format = "dzen2" 4 | colors = true 5 | interval = 5 6 | } 7 | 8 | order += "ipv6" 9 | order += "disk /" 10 | order += "run_watch DHCP" 11 | order += "run_watch VPNC" 12 | order += "path_exists VPN" 13 | order += "wireless wlan0" 14 | order += "ethernet eth0" 15 | order += "battery 0" 16 | order += "cpu_temperature 0" 17 | order += "load" 18 | order += "tztime local" 19 | order += "tztime berlin" 20 | 21 | wireless wlan0 { 22 | format_up = "W: (%quality at %essid, %bitrate) %ip" 23 | format_down = "W: down" 24 | } 25 | 26 | ethernet eth0 { 27 | # if you use %speed, i3status requires the cap_net_admin capability 28 | format_up = "E: %ip (%speed)" 29 | format_down = "E: down" 30 | } 31 | 32 | battery 0 { 33 | format = "%status %percentage %remaining %emptytime" 34 | format_down = "No battery" 35 | status_chr = "⚡ CHR" 36 | status_bat = "🔋 BAT" 37 | status_unk = "? UNK" 38 | status_full = "☻ FULL" 39 | path = "/sys/class/power_supply/BAT%d/uevent" 40 | low_threshold = 10 41 | } 42 | 43 | run_watch DHCP { 44 | pidfile = "/var/run/dhclient*.pid" 45 | } 46 | 47 | run_watch VPNC { 48 | # file containing the PID of a vpnc process 49 | pidfile = "/var/run/vpnc/pid" 50 | } 51 | 52 | path_exists VPN { 53 | # path exists when a VPN tunnel launched by nmcli/nm-applet is active 54 | path = "/proc/sys/net/ipv4/conf/tun0" 55 | } 56 | 57 | tztime local { 58 | format = "%Y-%m-%d %H:%M:%S" 59 | } 60 | 61 | tztime berlin { 62 | format = "%Y-%m-%d %H:%M:%S %Z" 63 | timezone = "Europe/Berlin" 64 | } 65 | 66 | load { 67 | format = "%5min" 68 | } 69 | 70 | cpu_temperature 0 { 71 | format = "T: %degrees °C" 72 | path = "/sys/devices/platform/coretemp.0/temp1_input" 73 | } 74 | 75 | disk "/" { 76 | format = "%free" 77 | } 78 | -------------------------------------------------------------------------------- /tests/examples/3-variables/config: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Built by i3configger /from/some/directory/who/cares (some time after 1972) # 3 | ############################################################################### 4 | 5 | ### some-variables.conf ### 6 | # some actual config - only needed fo i3 -C not to crash 7 | workspace_layout tabbed 8 | 9 | 10 | ### i3bar.tpl.conf ### 11 | # you can set variables in the bar template also 12 | # They will only be valid in the context of bar generation 13 | # They will also override variables of the same name set in other files 14 | 15 | bar { 16 | # additionally to all variables set in the configuration files 17 | # you can access the settings from i3configger.json here 18 | # They will be added to the context from bars->targets 19 | # they are pre populated by the bars->defaults 20 | status_command i3status -c ~/.i3/i3bar.default.conf 21 | output DP-3 22 | mode dock 23 | position bottom 24 | } 25 | -------------------------------------------------------------------------------- /tests/examples/3-variables/i3bar.default.conf: -------------------------------------------------------------------------------- 1 | ### i3bar.default.conf ### 2 | general { 3 | output_format = "dzen2" 4 | colors = true 5 | color = "#000000" 6 | color_good = "#000000" 7 | color_degraded = "#AAAAAA" 8 | color_bad = "#555555" 9 | interval = 5 10 | } 11 | 12 | order += "cpu_temperature 0" 13 | order += "load" 14 | 15 | load { 16 | format = "%5min" 17 | } 18 | 19 | cpu_temperature 0 { 20 | format = "T: %degrees °C" 21 | path = "/sys/devices/platform/coretemp.0/temp1_input" 22 | } 23 | -------------------------------------------------------------------------------- /tests/examples/4-schemes/config: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Built by i3configger /from/some/directory/who/cares (some time after 1972) # 3 | ############################################################################### 4 | 5 | ### some-key.value2.conf ### 6 | # This is integrated, because it had been selected by a remembered message. 7 | 8 | # var will also be replaced here - although this is just a comment: default 9 | default_orientation horizontal 10 | workspace_layout default 11 | -------------------------------------------------------------------------------- /tests/test_build.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from i3configger import build, config, ipc 6 | 7 | HERE = Path(__file__).parent 8 | EXAMPLES = HERE.parent / "examples" 9 | REFERENCE = HERE / "examples" 10 | FAKE_HEADER = """\ 11 | ############################################################################### 12 | # Built by i3configger /from/some/directory/who/cares (some time after 1972) # 13 | ############################################################################### 14 | """ 15 | TEST_FOLDER_NAMES = sorted( 16 | list( 17 | [ 18 | d.name 19 | for d in EXAMPLES.iterdir() 20 | if d.is_dir() and not str(d.name).startswith("_") 21 | ] 22 | ) 23 | ) 24 | 25 | 26 | @pytest.mark.parametrize("container", TEST_FOLDER_NAMES) 27 | def test_build(container, monkeypatch): 28 | ipc.configure(deactivate=True) 29 | monkeypatch.setattr(config, "get_i3wm_config_path", lambda: EXAMPLES / container) 30 | monkeypatch.setattr(build, "make_header", lambda _: FAKE_HEADER) 31 | monkeypatch.setattr(build, "check_config", lambda _: True) 32 | config.ensure_i3_configger_sanity() 33 | cnf = config.I3configgerConfig() 34 | assert cnf.configPath.exists() and cnf.configPath.is_file() 35 | build.build_all() 36 | buildPath = cnf.configPath.parents[1] 37 | referencePath = REFERENCE / container 38 | names = [p.name for p in referencePath.iterdir()] 39 | assert names 40 | for name in names: 41 | resultFilePath = buildPath / name 42 | referenceFilePath = referencePath / name 43 | assert resultFilePath != referenceFilePath 44 | result = resultFilePath.read_text() 45 | reference = referenceFilePath.read_text() 46 | assert result == reference 47 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import subprocess 3 | from contextlib import suppress 4 | 5 | import pytest 6 | 7 | from i3configger import exc, watch, config 8 | 9 | 10 | class Runner: 11 | COMMAND_NAME = "i3configger" 12 | 13 | def __init__(self, cwd): 14 | self.cwd = cwd 15 | 16 | def __call__(self, args=None, otherCwd=None): 17 | cmd = [self.COMMAND_NAME] 18 | if args: 19 | cmd += args 20 | cp = subprocess.run( 21 | cmd, 22 | timeout=1, 23 | stdout=subprocess.PIPE, 24 | stderr=subprocess.PIPE, 25 | cwd=otherCwd or self.cwd, 26 | ) 27 | return cp.returncode, cp.stdout.decode(), cp.stderr.decode() 28 | 29 | 30 | @pytest.fixture(scope="session", name="configPath", autouse=True) 31 | def ensure_config_path_exists(): 32 | try: 33 | yield config.get_i3wm_config_path() 34 | except exc.ConfigError: 35 | path = config.CONFIG_CANDIDATES[0] 36 | path.mkdir() 37 | yield path 38 | shutil.rmtree(path, ignore_errors=True) 39 | 40 | 41 | @pytest.fixture(name="runner") 42 | def create_i3configger_runner(monkeypatch, tmp_path) -> Runner: 43 | with suppress(exc.UserError): 44 | watch.exorcise() 45 | assert not watch.get_i3configger_process() 46 | monkeypatch.chdir(tmp_path) 47 | return Runner(tmp_path) 48 | 49 | 50 | def test_help(runner): 51 | ret, out, err = runner(["--help"]) 52 | assert ret == 0 53 | assert not err 54 | assert "usage:" in out 55 | 56 | 57 | def test_daemon(runner): 58 | ret, out, err = runner(["--daemon"]) 59 | assert ret == 0 60 | assert not err 61 | assert not out 62 | assert watch.get_i3configger_process() 63 | watch.exorcise() 64 | assert not watch.get_i3configger_process() 65 | 66 | 67 | def test_kill_with_running_i3configger_works(runner): 68 | runner(["--daemon"]) 69 | assert watch.get_i3configger_process() 70 | ret, out, err = runner(["--kill"]) 71 | assert ret == 0 72 | assert not err 73 | assert not out 74 | assert not watch.get_i3configger_process() 75 | 76 | 77 | def test_kill_without_running_i3configger_gives_good_error(runner): 78 | ret, out, err = runner(["--kill"]) 79 | assert ret == 1 80 | assert "no daemon running - nothing to kill" in err 81 | assert not out 82 | assert not watch.get_i3configger_process() 83 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from i3configger import config 5 | from i3configger.build import persist_results 6 | 7 | 8 | def test_initialization(tmp_path, monkeypatch): 9 | """Given empty sources directory a new config is created from defaults""" 10 | monkeypatch.setattr(config, "get_i3wm_config_path", lambda: tmp_path) 11 | assert not (tmp_path / "config.d").exists() 12 | config.ensure_i3_configger_sanity() 13 | cnf = config.I3configgerConfig() 14 | assert cnf.configPath.exists() 15 | assert cnf.configPath.is_file() 16 | assert cnf.configPath.name == config.I3configgerConfig.CONFIG_NAME 17 | payload = json.loads(cnf.configPath.read_text()) 18 | assert "main" in payload 19 | assert "bars" in payload 20 | assert "targets" in payload["bars"] 21 | assert "set" not in payload 22 | assert "select" not in payload 23 | assert "shadow" not in payload 24 | 25 | 26 | def test_config_backup_is_not_overwritten(tmp_path): 27 | """Given an existing backup it is not overwritten by subsequent builds.""" 28 | firstThing = "first thing" 29 | somePath = tmp_path / "some-path.txt" 30 | backupPath = Path(str(somePath) + ".bak") 31 | somePath.write_text(firstThing) 32 | persist_results({somePath: firstThing}) 33 | assert backupPath.read_text() == firstThing 34 | otherThing = "other thing" 35 | persist_results({somePath: otherThing}) 36 | assert somePath.read_text() == otherThing 37 | assert backupPath.read_text() == firstThing 38 | -------------------------------------------------------------------------------- /tests/test_context.py: -------------------------------------------------------------------------------- 1 | import pytest as pytest 2 | 3 | from i3configger import context, exc 4 | 5 | verySimple = {"$k1": "v1", "$k2": "v2"} 6 | simple = {"$k1": "v1", "$k2": "v1"} 7 | undef = {"$k1": "v1", "$k2": "$undef"} 8 | oneIndirection = {"$k1": "$k2", "$k2": "v1"} 9 | twoIndirections = {"$k1": "$k2", "$k2": "$k3", "$k3": "v1"} 10 | unresolvedIndirection = {"$k1": "$k2", "$k2": "$k3", "$k3": "$k4"} 11 | 12 | 13 | @pytest.mark.parametrize( 14 | "ctx, exp", 15 | ( 16 | (verySimple, verySimple), 17 | (simple, {"$k1": "v1", "$k2": "v1"}), 18 | (undef, exc.ContextError), 19 | (oneIndirection, {"$k1": "v1", "$k2": "v1"}), 20 | (twoIndirections, {"$k1": "v1", "$k2": "v1", "$k3": "v1"}), 21 | (unresolvedIndirection, exc.ContextError), 22 | ), 23 | ) 24 | def test_context(ctx, exp): 25 | if isinstance(exp, dict): 26 | ctx = context.resolve_variables(ctx) 27 | assert ctx == exp 28 | else: 29 | with pytest.raises(exc.ContextError): 30 | context.resolve_variables(ctx) 31 | -------------------------------------------------------------------------------- /tests/test_message.py: -------------------------------------------------------------------------------- 1 | from i3configger.message import Messenger 2 | 3 | 4 | def test_shadow(tmp_path): 5 | messenger = Messenger(tmp_path / "dontcare", [], message=["shadow", "k", "v"]) 6 | messenger.digest_message() 7 | -------------------------------------------------------------------------------- /tests/test_partials.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from i3configger import exc, partials 6 | 7 | SCHEMES = Path(__file__).parents[1] / "examples" / "4-schemes" / "config.d" 8 | 9 | 10 | def test_create(): 11 | prts = partials.create(SCHEMES) 12 | assert len(prts) == 3 13 | 14 | 15 | @pytest.mark.parametrize( 16 | "key, value, exp", 17 | ( 18 | ("", "", exc.ConfigError), 19 | ("some-key", "", exc.ConfigError), 20 | ("", "", exc.ConfigError), 21 | (None, None, exc.ConfigError), 22 | ("non-existing-key", "some-value", exc.ConfigError), 23 | ("non-existing-key", "none-existing-value", exc.ConfigError), 24 | ("some-category", "none-existing-value", exc.ConfigError), 25 | ("some-key", "value1", True), 26 | ("some-key", "value2", True), 27 | ), 28 | ) 29 | def test_select(key, value, exp): 30 | prts = partials.create(SCHEMES) 31 | selector = {key: value} 32 | if not isinstance(exp, bool): 33 | with pytest.raises(exp): 34 | partials.select(prts, selector) 35 | else: 36 | selected = partials.select(prts, selector) 37 | found = partials.find(prts, key, value) 38 | assert all(isinstance(p, partials.Partial) for p in selected) 39 | assert selected[0] == found 40 | assert selected[0].key == key 41 | assert selected[0].value == value 42 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = lint,test 3 | 4 | [flake8] 5 | max_line_length = 89 6 | exclude = .eggs,.tox,.cache,inotify_simple.py,__scratchpad.py 7 | 8 | [pytest] 9 | addopts = -lvv 10 | xfail_strict = True 11 | 12 | [testenv] 13 | basepython = python3.7 14 | extras = 15 | lint: lint 16 | test: test 17 | docs: docs 18 | 19 | [testenv:i3configger] 20 | description = run e.g. `tox -e i3configger -- --daemon` 21 | commands = i3configger {posargs} 22 | 23 | [testenv:lint] 24 | description = run pre-commit fixes and checks 25 | commands = 26 | pre-commit run --all-files 27 | python -c 'print("install hook: {envdir}/bin/pre-commit install")' 28 | 29 | [testenv:test] 30 | description = run tests with pytest 31 | commands = 32 | coverage run -m pytest 33 | coverage html 34 | coverage report 35 | 36 | [testenv:coveralls] 37 | description = send report to coveralls 38 | setenv = COVERALLS_REPO_TOKEN = zCycry7u83alwoxtvxKfZPMNGXVM37qzq 39 | deps = coveralls 40 | commands = coveralls 41 | 42 | [testenv:release] 43 | description = release to PyPI 44 | extras = release 45 | commands = python {toxinidir}/release.py {posargs} 46 | 47 | [testenv:docs-auto] 48 | envdir = {toxworkdir}/docs 49 | description = start a reloading server for the docs 50 | commands = 51 | mkdocs build --clean 52 | python -c 'print("### Start local server. Press Control+C to stop ###")' 53 | mkdocs serve -a localhost:8080 54 | 55 | [testenv:docs-clean] 56 | envdir = {toxworkdir}/docs 57 | description = remove documentation build folder 58 | skip_install = True 59 | whitelist_externals = rm 60 | commands = rm -rf {envdir}/build 61 | 62 | [testenv:docs-deploy] 63 | envdir = {toxworkdir}/docs 64 | description = push the docs online 65 | commands = mkdocs gh-deploy --clean 66 | 67 | [testenv:docs-deploy-force] 68 | envdir = {toxworkdir}/docs 69 | description = push the docs online (with extra ooomph!) 70 | whitelist_externals = git 71 | commands = 72 | - git branch -D gh-pages 73 | - git push origin --delete gh-pages 74 | mkdocs gh-deploy --clean 75 | 76 | [testenv:dev] 77 | description = dev env at {envpython} 78 | usedevelop = True 79 | extras = 80 | lint 81 | test 82 | docs 83 | release 84 | --------------------------------------------------------------------------------