404 - Not Found
The page you are looking for does not exist or was moved to another location.
Either go back to the start page or search for something like site:seb.xn--ho-hia.de YOUR_SEARCH_TERM
on google.
From f962fc2209d001aee89a6a0bcc4f4ae5918c4bde Mon Sep 17 00:00:00 2001
From: sebhoss The page you are looking for does not exist or was moved to another location. Either go back to the start page or search for something like Sebastian Hoß
+
+
+
+
+
404 - Not Found
site:seb.xn--ho-hia.de YOUR_SEARCH_TERM
on google.NullPointerException
at least once in their life. The exception is thrown every time you try to dereference and use some object before initializing it. The following snippet shows a simple example:
String someName; // value is 'null'
+
+someName.toUpperCase(); // throws NullPointerException
+
Modern IDEs have some sort of detection for this kind of problem and warn developers while they are writing code like this. Those IDEs typically rely on static code analysis to determine if a value is null
and therefore a potential for a NullPointerException
is present in your code. To improve the result of such an analysis, annotations can be placed on your code which signal that a parameter can or can not be null
. Multiple approaches have existed in the past to define a standard set of annotations for such a task, however none of them succeeded.
jspecify is the latest approach that tries to establish a standard. It has gained wide community support and recently celebrated their first public release (0.3.0
).
The following snippet shows the dependency declaration for Maven projects:
+<dependencies>
+ <dependency>
+ <groupId>org.jspecify</groupId>
+ <artifactId>jspecify</artifactId>
+ <version>0.3.0</version>
+ </dependency>
+</dependencies>
+
In case you want to declare that nothing in your module can ever be null
, place the @NullMarked
on your module-info.java
like this:
@org.jspecify.annotations.NullMarked
+module your.module.here {
+
+ requires org.jspecify;
+
+ // ...
+
+}
+
The tooling support is not quiet clear yet, however if you are developing a library there is no harm in adding these annotations now and let your users enjoy their null-free life once tools have caught up.
+]]>gpg
it is much simpler by focusing on the encryption parts only.
+Add the following snippet to your .chezmoi.toml
to configure chezmoi
to use age
:
encryption = "age"
+[age]
+ identity = "path/to/age/private-key"
+ recipient = "age...public...key..."
+
Adding files to your chezmoi
source directory remains the same as compared to using gpg
- just call chezmoi add --encrypt path/to/file
.
[user]
+ name = Your Name Here
+
+[includeIf "gitdir:~/git/personal/"]
+ path = ~/.config/git/personal
+[includeIf "gitdir:~/git/work/"]
+ path = ~/.config/git/work
+
The includeIf directive supports multiple keywords. In my case, work and personal projects have a different root directory, therefore I can filter based on the location using gitdir
. The personal Git configuration simply looks like this:
[user]
+ email = personal.email@example.com
+
and the work related configuration like this using a different email address:
+[user]
+ email = first.last@work.example
+
Additional settings that are different for personal/work accounts can be split the same way, for example to use a different signing key for work.
+]]>fzf
.
+fd --type=file --base-directory="${PASSAGE_DIR:-${HOME}/.passage/store}" .age --exec echo '{.}' | \
+ sk --cycle --layout=reverse --tiebreak=score --no-multi | \
+ xargs --replace --max-args=1 --no-run-if-empty \
+ passage show --clip=1 {}
+
This version requires fd, skim, xargs, and passage itself of course. The detailed breakdown on how it works is as follows:
+fd
to find all files within ${PASSAGE_DIR}
that end in .age
. Each password in passage is inside that folder and has such a file extensions, therefore we are selecting every password we have.--base-directory
and --exec echo '{.}'
ensures that passwords are returned in such form that they can be passed back into passage
again. The placeholder '{.}'
is a feature provided by fd
which strips the file extension from each returned value.sk
to allow to fuzzy search across them all. Setting --no-multi
ensures that only a single password can be selected.xargs
calls passage
and replaces the curly braces with the selected password. Thanks to --clip=1
, the first line in the selected password entry will be copied to the clipboard and automatically cleared after 45 seconds.To call that script, I’ve saved it as passage-fuzzy-search.sh
in my .local/bin
folder and added some checks into it to verify that every required software is actually installed.
#!/usr/bin/env zsh
+
+###############################################################################
+# This shell script presents passwords saved with passage through skim
+#
+# Call it like this:
+# passage-fuzzy-search.sh
+#
+# Required software that isn't in GNU coreutils:
+# - 'passage' to read passwords
+# - 'fd' to find passwords
+# - 'sk' to filter passwords
+###############################################################################
+
+if ! (( ${+commands[passage]} )); then
+ echo 'passage not installed. Please install passage.'
+ exit
+fi
+if ! (( ${+commands[sk]} )); then
+ echo 'sk not installed. Please install skim.'
+ exit
+fi
+if ! (( ${+commands[fd]} )); then
+ echo 'fd not installed. Please install fd-find.'
+ exit
+fi
+
+fd --type=file --base-directory="${PASSAGE_DIR:-${HOME}/.passage/store}" .age --exec echo '{.}' | \
+ sk --cycle --layout=reverse --tiebreak=score --no-multi | \
+ xargs --replace --max-args=1 --no-run-if-empty \
+ passage show --clip=1 {}
+
Since typing passage-fuzzy-search.sh
is way too long, I have added an alias like this:
alias pp='passage-fuzzy-search.sh'
+
systemd
unit:
+[Unit]
+Description=Update chezmoi managed dotfiles
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/chezmoi update --no-tty --force
+RemainAfterExit=false
+
+[Install]
+WantedBy=default.target
+
This unit pulls changes from upstream first and then applies the changes to the current computer after I’m logged in and a network connection is available. The --no-tty
flag is required because there is no tty when systemd executes chezmoi
. Likewise, the --force
flag ensures that no interactive prompt will be displayed which we cannot answer since systemd
is executing this unit without us being involved.
starship init zsh
or zoxide init zsh
. The documentation of these tools usually tell you to put something like eval "$(starship init zsh)"
into your shell RC file. While this approach works fine, it does decrease startup speed of your shell because it needs to run the init
command every time you open a new shell. Given that you open shells much more often than new versions of these tools are released and installed, you can cache the output of these commands to get a bit of speed back.
+chezmoi provides a template function called output which replaces itself with the output of the command you specified. You can use that function this to integrate various tools into your shell as the following example shows while using zsh
:
$ mkdir --parents "${ZDOTDIR}"/tools.d
+
.zshrc
file:
+for init_script in "${ZDOTDIR}"/tools.d/*.sh; do
+ source "${init_script}"
+done
+
.tmpl
files for each tool and place them in the chezmoi source directory that matches the directory you created in step 1:
+{{ output "starship" "init" "zsh" "--print-full-init" }}
+
chezmoi apply
to generate the init scripts.The only downside here is that you have to re-run chezmoi apply
after updating one of the tools because they change their init scripts sometimes. That problem can be solved with chezmoi auto-updates.
AWS_PROFILE
environment variable which is used by many tools that interact with the AWS API, like awscli
or terraform
. My current environment uses AzureAD as a single-sign-on provider, therefore this script uses aws sso login
to perform an MFA login into AWS. The AWS profiles must be set up in such a way that aws configure list-profiles
can detect them, which is typically done by adding them in ${AWS_CONFIG_FILE:-$HOME/.aws/config}
.
+#!/usr/bin/env sh
+
+###############################################################################
+# This script performs an AWS SSO login for the user-selected AWS profile
+# and sets the AWS_PROFILE environment variable afterwards. To use
+# this, create an alias that sources this script like this:
+#
+# alias awsenv='source path/to/this/script.sh'
+#
+# Required software that is not in GNU coreutils:
+# - 'aws' to list profiles & get current caller identity
+# - 'fzf' to list all available AWS profiles
+###############################################################################
+
+# prompt user to select one AWS profile
+profile=$(aws configure list-profiles | \
+ fzf --cycle --layout=reverse --tiebreak=index)
+
+# user can cancel switching profiles by pressing ESC
+if [ -n "${profile}" ]; then
+ # check is access token exists and is valid for selected profile
+ if ! aws --profile "${profile}" sts get-caller-identity >/dev/null 2>&1; then
+ # perform login into profile in case access token is invalid
+ if ! aws sso login --profile "${profile}"; then
+ # short circuit in case login failed
+ return
+ fi
+ fi
+ # AWS_PROFILE is used by many AWS-related tools
+ echo "Setting AWS_PROFILE to [${profile}]"
+ export AWS_PROFILE="${profile}"
+ # do not expose internal variables
+ unset profile
+fi
+
gen-class
Clojure code can be compiled to standard JVM bytecode using gen-class.
+Clojure imposes the concept of immutability. As such Clojure functions are/should be void of any state or side effects and only operate on the given input. Therefore, exporting Clojure functions as static Java methods makes sense. The following example defines a Clojure function, a corresponding Java-callable function and exports the Java function as a static method in the class com.example.Computation
.
(ns com.example.computation
+ (:gen-class
+ :name com.example.Computation
+ :methods [#^{:static true} [incrementRange [int] java.util.List]]))
+
+(defn increment-range
+ "Creates a sequence of numbers up to max and then increments them."
+ [max]
+ (map inc (take max (range))))
+
+(defn -incrementRange
+ "A Java-callable wrapper around the 'increment-range' function."
+ [max]
+ (increment-range max))
+
The Java wrapper has to follow the standard rules for method names. Therefore increment-range
has to be renamed to incrementRange
(or some similar name without the “-” in it). The “-” prefix for the Java wrapper can be configured inside the :gen-class
form and will be removed once gen-class
runs. The usage from Java looks like this:
package com.example
+
+public class ClojureJavaInteropStatic {
+
+ public static void main(String[] args) {
+ List incrementedRange = Computation.incrementRange(10);
+ }
+
+}
+
The returned list in the above code is raw because the method definition doesn’t use generics. To solve this problem declare that the generated class :implements
a certain interface that exposes the desired method definition(s). You won’t be able to declare your methods as static anymore, but get a generified method for all your Java needs.
The Java interface:
+package com.example
+
+public interface RangeIncrementer {
+ List<Long> incrementRange(int max);
+}
+
The changed Clojure namespace:
+(ns com.example.computation
+ (:gen-class
+ :name com.example.Computation
+ :implements [com.example.RangeIncrementer]))
+
+(defn increment-range
+ "Creates a sequence of numbers up to max and then increments them."
+ [max]
+ (map inc (take max (range))))
+
+(defn -incrementRange
+ "A Java-callable wrapper around the 'increment-range' function."
+ [this max]
+ (increment-range max))
+
Finally, the generified usage from Java:
+package com.example
+
+public class ClojureJavaInteropGenerics {
+
+ public static void main(String[] args) {
+ RangeIncrementer incrementer = new Computation();
+ List<Long> incrementedRange = incrementer.incrementRange(10);
+ }
+
+}
+
Couple of notes for this as well: First the generated class still only returns the raw type (List
instead of List<Integer>
). So instead of using the class, use the interface for the variable declaration (RangeIncrementer incrementer = ..
instead of Computation comp = ..
). The interface will return the non-raw List
. Second the function definition for -incrementRange
is now slightly different. It needs an additional parameter (this
) which exposes the current instance to the generated class/method.
Returning an array of something is also possible with the following construct "[Ljava.lang.Object;"
. Need a 2-dim array? Just use "[[Ljava.lang.Object;"
(notice the extra [
) and so on. However, be aware that the method return types have to match, for example you can’t specify a return type of array if your Clojure function does not return an array. In the example above the call to map
returns LazySeq
which itself is a java.util.List
. Therefore, the method declaration is valid, and you won’t get any ClassCastException
when calling incrementRange
from Java.
Instead of defining every Clojure function which should be exported twice (the real function + the Java wrapper), it is possible to use a macro to do that extra work automatically.
+(require '[clojure.string :as string)
+
+(defn camel-case [input]
+ (let [words (string/split input #"[\s_-]+")]
+ (string/join (cons (string/lower-case (first words)) (map string/capitalize (rest words))))))
+
+(defn java-name [clojure-name]
+ (symbol (str "-" (camel-case (str clojure-name)))))
+
+(defmacro defn* [name & declarations]
+ (let [java-name (java-name name)]
+ `(do (defn ~name ~declarations)
+ (defn ~java-name ~declarations))))
+
The macro defn*
replaces defn
and automatically creates a second function with a valid camel-cased Java method name. The macro is available as a small library at Maven Central. The macro won’t add the extra parameter mentioned above to Java wrapper, so it is only useful for declaring static methods.
Using gen-class
imposes certain limitations on calling Clojure code from Java. One of those are functions which make use of Clojure parameter destructuring. To invoke those functions you have to use the Clojure runtime.
// The Clojure 'require' function from the 'clojure.core' namespace.
+Var require = RT.var("clojure.core", "require");
+
+// Your namespace
+Symbol namespace = Symbol.intern("DESIRED.NAMESPACE.HERE");
+
+// Your function
+Var function = RT.var("DESIRED.NAMESPACE.HERE", "DESIRED-FUNCTION");
+
+// The required keyword for the above function
+Keyword keyword = Keyword.intern("REQUIRED-KEYWORD");
+
+// Require/Import your namespace
+require.invoke(namespace);
+
+// Invoke your function with the given keyword and its value
+Object result = function.invoke(keyword, VALUE);
+
The desired namespace has to be on the classpath for this to work. Alternatively it is possible to load an entire Clojure script, as shown in the following example:
+RT.loadResourceScript("DESIRED/NAMESPACE/HERE.clj");
+RT.var("DESIRED.NAMESPACE.HERE", "DESIRED-FUNCTION").invoke(PARAMETER);
+
On a big project it is properly wise to move Java->Clojure interop code into helper classes/methods. Look here for an example.
+]]>In its simplest (shortest) form a render-nothing component looks like the following snippet. It does not actually do anything and is not particularly helpful for anything. You could add it to every other component in your application without breaking or influencing anything.
+const RendersNothing = () => <></>
+
Now consider the following example, that adds some if-then-else
logic to the same component:
const MightRenderSomething = () => {
+ if (someCondition) {
+ return <span>hello world!</span>
+ }
+ return <></>
+}
+
This component encapsulates the if-then-else
logic of conditionally rendering a hello world message. Instead of cluttering your entire app with the same logic, you can now simply re-use that same component that contains this if
condition. To see the full power of this technique, consider the following example. At first, we are going to define a hook that reads the current window width, then define components that conditionally render based on the current window width, and finally use those components in an example application.
const useWindowWidth = () => {
+ const [width, setWidth] = React.useState(0)
+
+ React.useEffect(() => {
+ const handleResize = () => {
+ setWidth(window.innerWidth)
+ }
+ window.addEventListener("resize", handleResize)
+ return () => {
+ window.removeEventListener("resize", handleResize)
+ }
+ }, [])
+
+ return width
+}
+
The following components use that hook to implement UI breakpoints for small (mobile) and large (desktop) screens. Note that the value 768
is just an example - replace it with whatever your design system tells you to.
const ForMobileDevicesOnly = (props) => {
+ const windowWidth = useWindowWidth()
+
+ if (windowWidth < 768) {
+ return <>{props.children}</>
+ }
+ return <></>
+}
+
+const ForDesktopDevicesOnly = (props) => {
+ const windowWidth = useWindowWidth()
+
+ if (windowWidth >= 768) {
+ return <>{props.children}</>
+ }
+ return <></>
+}
+
Both of these components simply render nothing when the window width does not have an appropriate size. If the window width does have the right size, they render their children
. We can use those components in our application like this:
const SomeActualComponent = () => (
+ <div>
+ <h1>common headline</h1>
+ <ForMobileDevicesOnly>
+ <span>only visible on mobile devices</span>
+ </ForMobileDevicesOnly>
+ <ForDesktopDevicesOnly>
+ <span>only visible on desktop devices</span>
+ </ForDesktopDevicesOnly>
+ </div>
+)
+
The above code snippet declares that some part of the UI can only be seen by mobile users, while others can only be seen by desktop users. Parts of the UI that are shared amongst all users are not wrapped by any of the components defined above.
+]]>.home.arpa
to join the fun. In case you have hostnamectl
available on your system run the following command to change the hostname of a device:
+# set hostname
+$ hostnamectl hostname some-device.home.arpa
+
+# check hostname
+$ hostnamectl status
+
~/.vim/pack/*/{start,opt}/*
(Vim) or ~/.local/share/nvim/site/pack/*/{start,opt}/*
(Neovim). All you have to do to install new plugins, is to git clone
their repository into those directories. To automatically update those clones, create the following script:
+#!/usr/bin/env zsh
+
+###############################################################################
+# This script git-pulls all installed nvim plugins which are using the built-in
+# nvim plugin manager. Those plugins are located in .local/share/nvim/site/pack
+#
+# Required software that is not in GNU coreutils:
+# - 'git' to fetch plugin updates from upstream
+###############################################################################
+
+### User specific variables, adjust to your own needs
+
+# folder that contains all nvim plugins
+PLUGIN_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/nvim/site/pack"
+
+### Script logic
+
+echo "updating all plugins in ${PLUGIN_DIR}"
+# iterate through all directories and git pull em
+for directory in "${PLUGIN_DIR}"/*/{start,opt}/*; do
+ if [ -d "${directory}" ]; then
+ plugin=$(basename "${directory}")
+ echo "updating ${plugin}"
+ git -C "${directory}" pull --quiet
+ fi
+done
+
In case you are using Vim, adjust the PLUGIN_DIR
variable to point to your Vim plugin directory instead and save the resulting shell script as a file called update-nvim-plugins.sh
in some folder of your choice. Do not forget to set the executable bit with chmod +x /path/to/your/folder/update-nvim-plugins.sh
. Since all good developers must be lazy, write the following systemd service to execute the above script automatically:
[Unit]
+Description=cron job that triggers an update of all nvim plugins
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+Type=oneshot
+ExecStart=/path/to/your/folder/update-nvim-plugins.sh
+RemainAfterExit=false
+
Adjust the ExecStart
line to match the location where you saved the above script and place that service definition in a file called nvim-plugins-update.service
into your local ~/.config/systemd/user
directory. Add another file called nvim-plugins-update.timer
next to it that defines a systemd timer with the following content:
[Unit]
+Description=Update nvim plugins every week
+
+[Timer]
+OnCalendar=weekly
+Persistent=true
+RandomizedDelaySec=5hours
+
+[Install]
+WantedBy=timers.target
+
Adjust how often you want to update the plugins you are using in the OnCalendar
line. Enable this service/timer with:
$ systemctl --user enable nvim-plugins-update
+
Finally, add the following shell aliases to make it easier to interact with the created systemd units:
+# trigger an update manually
+alias update-nvim-plugins='systemctl --user start nvim-plugins-update'
+# see status of last auto-update
+alias update-nvim-plugins-status='systemctl --user status nvim-plugins-update'
+# see logs of past auto-updates
+alias update-nvim-plugins-logs='journalctl --user --unit nvim-plugins-update'
+
With this setup in place, all your plugins will be automatically updated once per week or however often you have configured in the timer.
+]]>~/.config/tmuxp
which looks like this:
+session_name: cool-app
+start_directory: ~/projects/cool-app
+windows:
+- window_name: backend
+ start_directory: backend
+- window_name: frontend
+ start_directory: frontend
+
In case the name of the file is cool-app.yaml
, you can open the sessions with tmuxp load cool-app --yes
.
$ mvn versions:set -DnewVersion=my.new.version -DgenerateBackupPoms=false
+
This will update the version
property of every module in the reactor to prepare them for the next release. In case you are using GitHub Actions, consider using a timestamp.
~/.m2/settings.xml
:
+<settings>
+ <profiles>
+ <profile>
+ <id>github</id>
+ <repositories>
+ <repository>
+ <id>maven-build-process</id>
+ <name>GitHub maven-build-process Apache Maven Packages</name>
+ <url>https://maven.pkg.github.com/metio/maven-build-process</url>
+ <releases>
+ <enabled>true</enabled>
+ </releases>
+ <snapshots>
+ <enabled>true</enabled>
+ </snapshots>
+ </repository>
+ <repository>
+ <id>hcf4j</id>
+ <name>GitHub hcf4j Apache Maven Packages</name>
+ <url>https://maven.pkg.github.com/metio/hcf4j</url>
+ <releases>
+ <enabled>true</enabled>
+ </releases>
+ <snapshots>
+ <enabled>true</enabled>
+ </snapshots>
+ </repository>
+ </repositories>
+ </profile>
+ </profiles>
+ <servers>
+ <server>
+ <id>maven-build-process</id>
+ <username>USERNAME</username>
+ <password>GITHUB_TOKEN</password>
+ </server>
+ <server>
+ <id>hcf4j</id>
+ <username>USERNAME</username>
+ <password>GITHUB_TOKEN</password>
+ </server>
+ </servers>
+</settings>
+
You will have to add another repository/server for each project you are fetching from GitHub.
+]]>~/.m2/settings.xml
like this:
+<settings>
+ <mirrors>
+ <mirror>
+ <id>google-maven-central</id>
+ <name>Google Maven Central (Asia)</name>
+ <url>https://maven-central-asia.storage-download.googleapis.com/maven2/</url>
+ <mirrorOf>central</mirrorOf>
+ </mirror>
+ <mirror>
+ <id>google-maven-central</id>
+ <name>Google Maven Central (EU)</name>
+ <url>https://maven-central-eu.storage-download.googleapis.com/maven2/</url>
+ <mirrorOf>central</mirrorOf>
+ </mirror>
+ <mirror>
+ <id>google-maven-central</id>
+ <name>Google Maven Central (US)</name>
+ <url>https://maven-central.storage-download.googleapis.com/maven2/</url>
+ <mirrorOf>central</mirrorOf>
+ </mirror>
+ </mirrors>
+</settings>
+
Pick the mirror nearest to your location to get best speeds.
+]]><file>~
. Unfortunately, emacs will not clean those up by default, which annoys me from time to time. Therefore, I’m now using the following configuration to keep those backups in a different folder:
+(setq version-control t ;; Use version numbers for backups.
+ kept-new-versions 10 ;; Number of newest versions to keep.
+ kept-old-versions 0 ;; Number of oldest versions to keep.
+ delete-old-versions t ;; Don't ask to delete excess backup versions.
+ backup-by-copying t) ;; Copy all files, don't rename them.
+
+(setq vc-make-backup-files t)
+
+;; Default and per-save backups go here:
+(setq backup-directory-alist '(("" . "~/.emacs.d/backup/per-save")))
+
+(defun force-backup-of-buffer ()
+ ;; Make a special "per session" backup at the first save of each
+ ;; emacs session.
+ (when (not buffer-backed-up)
+ ;; Override the default parameters for per-session backups.
+ (let ((backup-directory-alist '(("" . "~/.emacs.d/backup/per-session")))
+ (kept-new-versions 3))
+ (backup-buffer)))
+ ;; Make a "per save" backup on each save. The first save results in
+ ;; both a per-session and a per-save backup, to keep the numbering
+ ;; of per-save backups consistent.
+ (let ((buffer-backed-up nil))
+ (backup-buffer)))
+
+(add-hook 'before-save-hook 'force-backup-of-buffer)
+
Thanks to that configuration, backups per-save will be created in ~/.emacs.d/backup/per-save
and backups per-session in ~/.emacs.d/backup/per-session
.
function m-dotfiles-ok {
+ # public
+ chezmoi add ~/.config/zsh --recursive
+ chezmoi add ~/.config/sway --recursive
+ chezmoi add ~/.config/tmux --recursive
+ chezmoi add ....
+
+ # secrets
+ chezmoi add --encrypt ~/.config/npm/npmrc
+ chezmoi add --encrypt ~/.ssh/id_rsa
+ chezmoi add --encrypt ...
+}
+
Whenever you feel happy with your current setup, just call m-dotfiles-ok
to push changes into the chezmoi source directory. Files will automatically be encrypted with gpg and committed/pushed into a Git repository if you have done the necessary configuration beforehand.
In general, editing your dotfiles directly as explained in the second option of the FAQ seems easier though. Refactoring your dotfiles is especially easy when the exact_
prefix is used for directories. As explained in the documentation, all files that are not managed by chezmoi
will be removed, therefore your configuration will always match what is in your source directory.
gpg
.
+chezmoi can use various external tools to keep data private. gpg is used by various other tools as well, so chances are that you already have a functional setup on your system. To configure gpg
with chezmoi
, just set yourself as the recipient like this:
[gpg]
+ recipient = "your.name@example.com"
+
Calling chezmoi add --encrypt /path/to/secret
will now create encrypt the file with your public key which allows you to decrypt them later with your private key.
chezmoi.toml
+[sourceVCS]
+ autoCommit = true
+ autoPush = true
+
Every time you call chezmoi add /path/to/file
will now create a new commit in your local chezmoi repository and push those changes into your configured remote repository.
bar {
+ swaybar_command waybar
+}
+
Configure Waybar itself in ~/.config/waybar/config
:
{
+ "layer": "top",
+ "modules-left": ["sway/workspaces", "sway/mode"],
+ "modules-center": ["sway/window"],
+ "modules-right": ["clock"],
+ "sway/window": {
+ "max-length": 50
+ },
+ "clock": {
+ "format-alt": "{:%a, %d. %b %H:%M}"
+ }
+}
+
]]>$ tmux show-options -g | grep status
+
Change on of those values with in the current tmux session:
+$ tmux set-option status-right ""
+
Persist the change in your tmux.conf
like this:
# disable right side of status bar
+set-option -g status-right ""
+
[Unit]
+Description=Emacs text editor [%I]
+Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/
+
+[Service]
+Type=forking
+ExecStart=/usr/bin/emacs --daemon=%i
+ExecStop=/usr/bin/emacsclient --eval "(kill-emacs)"
+Environment=SSH_AUTH_SOCK=%t/keyring/ssh
+Restart=on-failure
+
+[Install]
+WantedBy=default.target
+
Enable it with systemctl --user enable emacs@user
and define any number of aliases to make connecting to the emacs daemon easier:
alias e='emacsclient --tty --socket-name=user'
+alias vim='emacsclient --tty --socket-name=user'
+alias vi='emacsclient --tty --socket-name=user'
+alias nano='emacsclient --tty --socket-name=user'
+alias ed='emacsclient --tty --socket-name=user'
+
$ git remote add mirrors DISABLED
+$ git remote set-url --add --push mirrors git@codeberg.org:org/repo.git
+$ git remote set-url --add --push mirrors git@gitlab.com:org/repo.git
+$ git remote set-url --add --push mirrors git@bitbucket.org:org/repo.git
+
The above will create a new remote called mirrors
which has no fetch
URL and therefore can only be pushed:
$ git remote -v
+mirrors DISABLED (fetch)
+mirrors git@codeberg.org:org/repo.git (push)
+mirrors git@gitlab.com:org/repo.git (push)
+mirrors git@bitbucket.org:org/repo.git (push)
+
Calling git push mirrors main:main
will push the local main
branch into all defined mirrors.
$ git remote set-url origin --push --add git@example.com/project.git
+$ git remote set-url origin --push --add git@another.com/project.git
+
Note that the first call to set-url
will overwrite an existing remote creating with git clone
. Any additional call will actually recognize the --add
option and add the new target to an existing remote.
alias rancher="kubectl --kubeconfig ~/.kube/rancher.config"
+alias work="kubectl --kubeconfig ~/.kube/work.config"
+alias customer="kubectl --kubeconfig ~/.kube/customer.config"
+
Those aliases allow me to write things like rancher get pods --namespace some-namespace
without worrying the wrong context is active. Using multiple configurations - one for each cluster - seems to be easier to manage since most clusters allow to download a ready-to-use configuration file. Instead of mangling them together manually, I just specify another alias whenever I get to work with another cluster.
<properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+</properties>
+
project.build.outputTimestamp
property like this:
+<properties>
+ <project.build.outputTimestamp>2020</project.build.outputTimestamp>
+</properties>
+
settings.xml
file:
+<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
+ http://maven.apache.org/xsd/settings-1.0.0.xsd">
+
+ <pluginGroups>
+ <pluginGroup>org.sonarsource.scanner.maven</pluginGroup>
+ </pluginGroups>
+
+ <activeProfiles>
+ <activeProfile>sonar</activeProfile>
+ </activeProfiles>
+
+ <profiles>
+ <profile>
+ <id>sonar</id>
+ <properties>
+ <sonar.host.url>https://sonarcloud.io</sonar.host.url>
+ <sonar.organization>YOUR_ORG</sonar.organization>
+ <sonar.projectKey>YOUR_PROJECT</sonar.projectKey>
+ <sonar.login>${env.SONAR_TOKEN}</sonar.login>
+ </properties>
+ </profile>
+ </profiles>
+</settings>
+
Finally, add a step to your workflow:
+- name: Verify Project
+ run: mvn --settings $GITHUB_WORKSPACE/settings.xml verify sonar:sonar
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
chsh
:
+# list all available shells
+$ chsh --list-shells
+/bin/sh
+/bin/bash
+/sbin/nologin
+/usr/bin/sh
+/usr/bin/bash
+/usr/sbin/nologin
+/usr/bin/zsh
+/bin/zsh
+/usr/bin/tmux
+/bin/tmux
+
+# select login shell
+$ chsh --shell /usr/bin/tmux
+
# lock your screen
+bindsym $mod+Ctrl+l exec swaylock --color 000000
+
$mod+Ctrl+l
will lock your screen and turn it to black. The --color
flag allows any color in the form of rrggbb[aa]
.
.bashrc
or similar file.
+peek() { tmux split-window -p 33 "$EDITOR" "$@" }
+
Calling peek <file>
will open <file>
in lower third of tmux window.
# take screenshot of currently focused screen
+bindsym $mod+Print exec /usr/bin/grim -o $(swaymsg -t get_outputs | jq -r '.[] | select(.focused) | .name') $(xdg-user-dir PICTURES)/$(date +'%Y-%m-%d-%H%M%S.png')
+
+# take screenshot of selection
+bindsym $mod+Shift+p exec /usr/bin/grim -g "$(/usr/bin/slurp)" $(xdg-user-dir PICTURES)/$(date +'%Y-%m-%d-%H%M%S.png')
+
README
file typically contain information about the project itself, for example how it can be installed/used/build. Most of the time these files contains command line instructions that users/contributors copy and paste into their terminal. Instead of doing that, consider placing a Makefile
in the root of your project which contains the exact same instructions. Thanks to make
, all your contributors can now use TAB-completion to run any of the pre-defined make
targets.
+The following example is part of one of my projects, and I certainly don’t want to type (or even copy) that all the time:
+.PHONY: release-into-local-nexus
+release-into-local-nexus:
+ mvn versions:set \
+ -DnewVersion=$(TIMESTAMPED_VERSION) \
+ -DgenerateBackupPoms=false
+ -mvn clean deploy scm:tag \
+ -DpushChanges=false \
+ -DskipLocalStaging=true \
+ -Drelease=local
+ mvn versions:set \
+ -DnewVersion=9999.99.99-SNAPSHOT \
+ -DgenerateBackupPoms=false
+
With the above target in place, everyone can now do make release-into-local-nexus
instead of typing/copying the commands themselves. Thanks to TAB-completion you just have to do make r<TAB>
and confirm with >ENTER>
to perform a release.
on: schedule: ...
configuration, and a conditional build step.
+name: <PIPELINE>
+on:
+ schedule:
+ - cron: '<CRON>'
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Count commits in last week
+ id: commits
+ run: echo "::set-output name=count::$(git rev-list --count HEAD --since='<DATE>')"
+ - name: Build project
+ if: steps.commits.outputs.count > 0
+ run: build-project
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<CRON>
: cron expression - use https://crontab.guru/.<DATE>
: Git date expression that matches <CRON>
.Makefile
to define a complex build step - for example start database, run tests, stop database - consider using the -
qualifier in front of your actual build step like this:
+.PHONY: build
+build:
+ start-database
+ -build-software
+ stop-database
+
Thanks to -
, the database will be stopped even if building your software fails, therefore making sure to clean up after ourselves once the build finishes.
config.toml
:
+[mediaTypes."application/javascript"]
+ suffixes = ["js"]
+[outputFormats.ServiceWorker]
+ name = "ServiceWorker"
+ mediaType = "application/javascript"
+ baseName = "serviceworker"
+ isPlainText = false
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/home.serviceworker.js
with the following content:
const CACHE = 'cache-and-update';
+
+self.addEventListener('install', (event) => {
+ event.waitUntil(precache());
+});
+
+self.addEventListener('fetch', (event) => {
+ event.respondWith(fromCache(event.request));
+ event.waitUntil(update(event.request));
+});
+
+const precache = async () => {
+ const cache = await caches.open(CACHE);
+ return await cache.addAll([
+ {{ range $i, $e := .Site.RegularPages }}
+ '{{ $.RelPermalink }}'{{ if $i }}, {{ end }}
+ {{ end }}
+ ]);
+}
+
+const fromCache = async (request) => {
+ const cache = await caches.open(CACHE);
+ const match = await cache.match(request);
+ return match || Promise.reject('no-match');
+}
+
+const update = async (request) => {
+ const cache = await caches.open(CACHE);
+ const response = await fetch(request);
+ return await cache.put(request, response);
+}
+
config.toml
:
+[mediaTypes."application/manifest+json"]
+ suffixes = ["webmanifest"]
+[outputFormats.Webmanifest]
+ name = "Web App Manifest"
+ mediaType = "application/manifest+json"
+ baseName = "manifest"
+ isPlainText = false
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/home.manifest.json
with the following content:
{
+ "name": "{{ .Site.Title }}",
+ "short_name": "{{ .Site.Title }}",
+ "start_url": ".",
+ "display": "minimal-ui",
+ "background_color": "#fff",
+ "description": "{{ .Site.Params.description }}"
+}
+
{{ $normalize := resources.Get "/css/normalize.css" }}
+{{ $font := resources.Get "/css/font.css" }}
+{{ $header := resources.Get "/css/header.css" }}
+{{ $footer := resources.Get "/css/footer.css" }}
+{{ $navigation := resources.Get "/css/navigation.css" }}
+{{ $navigation_mobile := resources.Get "/css/navigation-mobile.css" }}
+{{ $layout := resources.Get "/css/layout.css" }}
+{{ $layout_mobile := resources.Get "/css/layout-mobile.css" }}
+{{ $syntax := resources.Get "/css/syntax.css" }}
+{{ $darkmode := resources.Get "/css/darkmode.css" | resources.Minify | resources.Fingerprint "sha512" }}
+
+{{ $base := slice $normalize $font $header $footer $navigation $layout $syntax | resources.Concat "css/base.css" | resources.Minify | resources.Fingerprint "sha512" }}
+{{ $mobile := slice $navigation_mobile $layout_mobile | resources.Concat "css/mobile.css" | resources.Minify | resources.Fingerprint "sha512" }}
+
+<link href="{{ $base.Permalink }}" integrity="{{ $base.Data.Integrity }}" media="screen" rel="stylesheet">
+<link href="{{ $mobile.Permalink }}" integrity="{{ $mobile.Data.Integrity }}" media="screen and (max-width: 800px)" rel="stylesheet">
+
+<link href="{{ $darkmode.Permalink }}" integrity="{{ $darkmode.Data.Integrity }}" media="screen and (prefers-color-scheme: dark)" rel="stylesheet">
+
]]>config.toml
:
+[mediaTypes."text/plain"]
+ suffixes = ["txt"]
+[outputFormats.Humans]
+ name = "Humans"
+ mediaType = "text/plain"
+ baseName = "humans"
+ isPlainText = true
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/home.humans.txt
with the following content:
/* TEAM */
+{{ range $.Site.Data.contributors }}
+{{ .title }}: {{ .first_name }} {{ .last_name }}
+Site: {{ .website }}
+{{ end }}
+
]]>config.toml
:
+[mediaTypes."application/rdf+xml"]
+ suffixes = ["rdf"]
+[outputFormats.Foaf]
+ name = "FOAF"
+ mediaType = "application/rdf+xml"
+ baseName = "foaf"
+ isPlainText = false
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/home.foaf.rdf
with the following content:
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" xmlns:foaf="http://xmlns.com/foaf/0.1/">
+ <foaf:PersonalProfileDocument rdf:about="">
+ <foaf:maker rdf:resource="#me" />
+ <foaf:primaryTopic rdf:resource="{{ .Site.Title }}" />
+ </foaf:PersonalProfileDocument>
+
+ <foaf:Project rdf:ID="{{ .Site.Title }}">
+ <foaf:name>{{ .Site.Title }}</foaf:name>
+ <foaf:homepage rdf:resource="{{ .Site.BaseURL }}" />
+ </foaf:Project>
+
+ {{ range $.Site.Data.contributors }}
+ <foaf:Person rdf:ID="{{ .id }}">
+ <foaf:name>{{ .first_name }} {{ .last_name }}</foaf:name>
+ <foaf:title>{{ .title }}</foaf:title>
+ <foaf:givenname>{{ .first_name }}</foaf:givenname>
+ <foaf:family_name>{{ .last_name }}</foaf:family_name>
+ <foaf:mbox rdf:resource="mailto:{{ .email }}" />
+ <foaf:homepage rdf:resource="{{ .website }}" />
+ </foaf:Person>
+ {{ end }}
+</rdf:RDF>
+
]]>config.toml
:
+[mediaTypes."application/atom+xml"]
+ suffixes = ["xml"]
+[outputFormats.Atom]
+ name = "Atom"
+ mediaType = "application/atom+xml"
+ baseName = "atom"
+ isPlainText = false
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/list.atom.xml
with the following content:
{{ printf `<?xml version="1.0" encoding="utf-8"?>` | safeHTML }}
+<feed xmlns="http://www.w3.org/2005/Atom"{{ with site.LanguageCode }} xml:lang="{{ . }}"{{ end }}>
+ <generator uri="https://gohugo.io/" version="{{ hugo.Version }}">Hugo</generator>
+ {{- $title := site.Title -}}
+ {{- with .Title -}}
+ {{- if (not (eq . site.Title)) -}}
+ {{- $title = printf `%s %s %s` . (i18n "feed_title_on" | default "on") site.Title -}}
+ {{- end -}}
+ {{- end -}}
+ {{- if .IsTranslated -}}
+ {{ $title = printf "%s (%s)" $title (index site.Data.i18n.languages .Lang) }}
+ {{- end -}}
+ {{ printf `<title type="html"><![CDATA[%s]]></title>` $title | safeHTML }}
+ {{ with (or (.Param "subtitle") (.Param "tagline")) }}
+ {{ printf `<subtitle type="html"><![CDATA[%s]]></subtitle>` . | safeHTML }}
+ {{ end }}
+ {{ $output_formats := .OutputFormats }}
+ {{ range $output_formats -}}
+ {{- $rel := (or (and (eq "atom" (.Name | lower)) "self") "alternate") -}}
+ {{ with $output_formats.Get .Name }}
+ {{ printf `<link href=%q rel=%q type=%q title=%q />` .Permalink $rel .MediaType.Type .Name | safeHTML }}
+ {{- end -}}
+ {{- end }}
+ {{- range .Translations }}
+ {{ $output_formats := .OutputFormats }}
+ {{- $lang := .Lang }}
+ {{- $langstr := index site.Data.i18n.languages .Lang }}
+ {{ range $output_formats -}}
+ {{ with $output_formats.Get .Name }}
+ {{ printf `<link href=%q rel="alternate" type=%q hreflang=%q title="[%s] %s" />` .Permalink .MediaType.Type $lang $langstr .Name | safeHTML }}
+ {{- end -}}
+ {{- end }}
+ {{- end }}
+ <updated>{{ now.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</updated>
+ {{ with site.Copyright }}
+ {{- $copyright := replace . "{year}" now.Year -}} {{/* In case the site.copyright uses a special string "{year}" */}}
+ {{- $copyright = replace $copyright "©" "©" -}}
+ <rights>{{ $copyright | plainify }}</rights>
+ {{- end }}
+ {{ with .Param "feed" }}
+ {{/* For this to work, the $icon file should be present in the assets/ directory */}}
+ {{- $icon := .icon | default "icon.svg" -}}
+ {{- with resources.Get $icon -}}
+ <icon>{{ (. | fingerprint).Permalink }}</icon>
+ {{- end }}
+
+ {{/* For this to work, the $logo file should be present in the assets/ directory */}}
+ {{- $logo := .logo | default "logo.svg" -}}
+ {{- with resources.Get $logo -}}
+ <logo>{{ (. | fingerprint).Permalink }}</logo>
+ {{- end }}
+ {{ end }}
+ {{ with site.Author.name -}}
+ <author>
+ <name>{{ . }}</name>
+ {{ with site.Author.email }}
+ <email>{{ . }}</email>
+ {{ end -}}
+ </author>
+ {{- end }}
+ {{ with site.Params.id }}
+ <id>{{ . | plainify }}</id>
+ {{ else }}
+ <id>{{ .Permalink }}</id>
+ {{ end }}
+ {{- $limit := (cond (le site.Config.Services.RSS.Limit 0) 65536 site.Config.Services.RSS.Limit) }}
+ {{- $feed_sections := site.Params.feedSections | default site.Params.mainSections -}}
+ {{/* Range through only the pages with a Type in $feed_sections. */}}
+ {{- $pages := where .RegularPages "Type" "in" $feed_sections -}}
+ {{- if (eq .Kind "home") -}}
+ {{- $pages = where site.RegularPages "Type" "in" $feed_sections -}}
+ {{- end -}}
+ {{/* Remove the pages that have the disable_feed parameter set to true. */}}
+ {{- $pages = where $pages ".Params.disable_feed" "!=" true -}}
+ {{- range first $limit $pages }}
+ {{ $page := . }}
+ <entry>
+ {{ printf `<title type="html"><![CDATA[%s]]></title>` .Title | safeHTML }}
+ <link href="{{ .Permalink }}?utm_source=atom_feed" rel="alternate" type="text/html" />
+ {{- range .Translations }}
+ {{- $link := printf "%s?utm_source=atom_feed" .Permalink | safeHTML }}
+ {{- printf `<link href=%q rel="alternate" type="text/html" hreflang=%q />` $link .Lang | safeHTML }}
+ {{- end }}
+ {{/* rel=related: See https://validator.w3.org/feed/docs/atom.html#link */}}
+ {{- range first 5 (site.RegularPages.Related .) }}
+ <link href="{{ .Permalink }}?utm_source=atom_feed" rel="related" type="text/html" title="{{ .Title }}" />
+ {{- end }}
+ {{ with .Params.id }}
+ <id>{{ . | plainify }}</id>
+ {{ else }}
+ <id>{{ .Permalink }}</id>
+ {{ end }}
+ {{ with .Params.author -}}
+ {{- range . -}} <!-- Assuming the author front-matter to be a list -->
+ <author>
+ <name>{{ . }}</name>
+ </author>
+ {{- end -}}
+ {{- end }}
+ <published>{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</published>
+ <updated>{{ .Lastmod.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</updated>
+ {{ $description1 := .Description | default "" }}
+ {{ $description := (cond (eq "" $description1) "" (printf "<blockquote>%s</blockquote>" ($description1 | markdownify))) }}
+ {{ printf `<content type="html"><![CDATA[%s%s]]></content>` $description .Content | safeHTML }}
+ {{ with site.Taxonomies }}
+ {{ range $taxo,$_ := . }} <!-- Defaults taxos: "tags", "categories" -->
+ {{ with $page.Param $taxo }}
+ {{ $taxo_list := . }} <!-- $taxo_list will be the tags/categories list -->
+ {{ with site.GetPage (printf "/%s" $taxo) }}
+ {{ $taxonomy_page := . }}
+ {{ range $taxo_list }} <!-- Below, assuming pretty URLs -->
+ <category scheme="{{ printf "%s%s" $taxonomy_page.Permalink (. | urlize) }}" term="{{ (. | urlize) }}" label="{{ . }}" />
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ </entry>
+ {{ end }}
+</feed>
+
]]>name: <NAME>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Publish Toot
+ uses: rzr/fediverse-action@master
+ with:
+ access-token: ${{ secrets.MASTODON_TOKEN }}
+ message: <MESSAGE>
+ host: ${{ secrets.MASTODON_SERVER }}
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<MESSAGE>
: Message for the toot.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Send mail
+ uses: dawidd6/action-send-mail@v3
+ with:
+ server_address: ${{ secrets.MAIL_SERVER }}
+ server_port: ${{ secrets.MAIL_PORT }}
+ username: ${{ secrets.MAIL_USERNAME }}
+ password: ${{ secrets.MAIL_PASSWORD }}
+ subject: <SUBJECT>
+ body: <BODY>
+ to: ${{ secrets.MAIL_RECIPIENT }}
+ from: ${{ secrets.MAIL_SENDER }}
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<SUBJECT>
: Subject for the email.<BODY>
: Body for the email.Create appropriate secrets in your organization or project. In case you are using an organization, but different mailing lists, define MAIL_RECIPIENT
for each project.
name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Upload Release Asset
+ id: upload_release_asset
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ steps.create_release.outputs.upload_url }}
+ asset_path: ./some/path/to/file.zip
+ asset_name: public-name-for-file.zip
+ asset_content_type: application/zip
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Create Release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: <TAG>
+ release_name: <RELEASE>
+ draft: false
+ prerelease: false
+ body: |
+ Your release text here
+
+ Some code block:
+ ```yaml
+ yaml:
+ inside:
+ of:
+ another: yaml
+ ```
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<TAG>
: The Git tag to create.<RELEASE>
: The release name to use.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Deploy Website
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: <PUBLISH_DIR>
+ force_orphan: true
+ cname: <CNAME>
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<PUBLISH_DIR>
: The file system location of the built site.<CNAME>
: The CNAME
of your custom domain.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Cache Maven artifacts
+ uses: actions/cache@v1
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Setup hugo
+ uses: peaceiris/actions-hugo@v2
+ with:
+ hugo-version: <HUGO_VERSION>
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<HUGO_VERSION>
: The released versions or use latest
to always use the latest version of Hugo.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Set up JDK <JDK_VERSION>
+ uses: actions/setup-java@v1
+ with:
+ java-version: <JDK_VERSION>
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<JDK_VERSION>
: The required Java version for your project.# use existing env variables or define new
+[ -z "$XDG_CACHE_HOME" ] && export XDG_CACHE_HOME="$HOME/.cache"
+[ -z "$XDG_CONFIG_DIRS" ] && export XDG_CONFIG_DIRS="/etc/xdg"
+[ -z "$XDG_CONFIG_HOME" ] && export XDG_CONFIG_HOME="$HOME/.config"
+[ -z "$XDG_DATA_DIRS" ] && export XDG_DATA_DIRS="/usr/local/share:/usr/share"
+[ -z "$XDG_DATA_HOME" ] && export XDG_DATA_HOME="$HOME/.local/share"
+
+# gradle
+export GRADLE_USER_HOME="$XDG_DATA_HOME/gradle"
+
+# httpie
+export HTTPIE_CONFIG_DIR="$XDG_CONFIG_HOME/httpie"
+
+# npm
+export NPM_CONFIG_USERCONFIG="$XDG_CONFIG_HOME/npm/npmrc"
+export npm_config_cache="$XDG_CACHE_HOME/npm"
+
+# password-store
+export PASSWORD_STORE_DIR="$XDG_DATA_HOME/password-store"
+
To make your own software XDG-aware, consider using the dirs-dev or configdir libraries.
+]]>While the central server approach is easy to use, it might not work in all scenarios:
+In case of the first scenario, tools like copybara, repoSpanner, or distributed-Git-forks offer a wide range of features to cover most details.
+The second scenario can be solved manually with tools like gitomatic or automatically with GitLab’s mirror feature quite easy. GitLab allows to create a single pull-mirror and multiple push-mirrors. Therefore, it can be used to pull from your central server and push into all mirrors.
+NOTE: This feature was previously available in the free tier but has now moved to GitLab Ultimate.
+ +----------------+
+ | GitHub |
+ +----------------+
+ ^
+ |
+ |
+ +----------------+
+ +-----| GitLab |------+
+ | +----------------+ |
+ | |
+ | |
+ v v
++----------------+ +----------------+
+| Codeberg | | BitBucket |
++----------------+ +----------------+
+
To create such a setup, follow these steps:
+Settings > Repository
and expand Mirroring repositories
+https://github.com/metio/ilo.git
+https://YOUR_USER@codeberg.org/metio.wtf/ilo.git
. Add an access token for each mirror and select password
as authentication method.
+In case you prefer SSH keys over HTTP access tokens, just select SSH public key
as authentication method and verify that your key is both saved in GitLab and all mirrors.
Makefile
:
+GREEN := $(shell tput -Txterm setaf 2)
+WHITE := $(shell tput -Txterm setaf 7)
+YELLOW := $(shell tput -Txterm setaf 3)
+RESET := $(shell tput -Txterm sgr0)
+
+HELP_FUN = \
+ %help; \
+ while(<>) { push @{$$help{$$2 // 'targets'}}, [$$1, $$3] if /^([a-zA-Z\-]+)\s*:.*\#\#(?:@([a-zA-Z\-]+))?\s(.*)$$/ }; \
+ print "usage: make [target]\n\n"; \
+ for (sort keys %help) { \
+ print "${WHITE}$$_:${RESET}\n"; \
+ for (@{$$help{$$_}}) { \
+ $$sep = " " x (32 - length $$_->[0]); \
+ print " ${YELLOW}$$_->[0]${RESET}$$sep${GREEN}$$_->[1]${RESET}\n"; \
+ }; \
+ print "\n"; }
+
To use HELP_FUN
, add the following help
target to the same Makefile
:
.DEFAULT_GOAL := help
+
+.PHONY: help
+help: ##@other Show this help
+ @perl -e '$(HELP_FUN)' $(MAKEFILE_LIST)
+
Each target in the Makefile
is marked as phony to signal that those targets are not actually files that are generated as part of your build process. The optional description of a target can be placed after the ##@
prefix. The first word represents the group of a target and everything that follows is the description of a target. All targets should be formatted just like the help
target:
.PHONY: compile
+compile: ##@hacking Compile your code
+ <compile some code>
+
+.PHONY: test
+test: ##@hacking Test your code
+ <test some code>
+
+.PHONY: sign-cla
+sign-cla: ##@contrib Sign the contributor license agreement
+ <sign some file>
+
Once in place, you can either use make
without any argument to call the help
target or use make help
to see the generated output:
$ make
+usage: make [target]
+
+contrib:
+ sign-cla Sign the contributor license agreement
+
+hacking:
+ compile Compile your code
+ test Test your code
+
+other:
+ help Show this help
+
git clone git@github.com:orga/repo.git
all the time, consider using a custom SSH configuration (~/.ssh/config
) like this:
+Host github
+ HostName github.com
+ User git
+ IdentityFile ~/.ssh/<KEY-FOR-GITHUB>
+
+Host gitlab
+ HostName gitlab.com
+ User git
+ IdentityFile ~/.ssh/<KEY-FOR-GITLAB>
+
+Host bitbucket
+ HostName bitbucket.org
+ User git
+ IdentityFile ~/.ssh/<KEY-FOR-BITBUCKET>
+
+Host codeberg
+ HostName codeberg.org
+ User git
+ IdentityFile ~/.ssh/<KEY-FOR-CODEBERG>
+
Once configured, you can now write:
+$ git clone github:orga/repo
+$ git clone gitlab:orga/repo
+$ git clone bitbucket:orga/repo
+$ git clone codeberg:orga/repo
+
In case you are working with many repositories inside a single organization, consider adding the following Git configuration ($XDG_CONFIG_HOME/git/config
or ~/.gitconfig
):
[url "github:orga/"]
+ insteadOf = orga:
+[url "gitlab:orga/"]
+ insteadOf = orgl:
+[url "bitbucket:orga/"]
+ insteadOf = orgb:
+[url "codeberg:orga/"]
+ insteadOf = orgc:
+
Which allows you to just write:
+$ git clone orga:repo
+$ git clone orgl:repo
+$ git clone orgb:repo
+$ git clone orgc:repo
+
Git will substitute the insteadOf
values like orga:
with the configured url
(for example github:orga/
). The actual clone URL is github:orga/repo
at this point, which can be used by Git together with the SSH configuration mentioned above to clone repositories.
name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Create release version
+ id: <ID>
+ run: echo "::set-output name=<NAME>::$(date +'%Y.%m.%d-%H%M%S')"
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<ID>
: The unique ID of the timestamp step.<NAME>
: The name of the created timestamp.The special syntax ::set-output name=<NAME>::
declares that the output of the command (echo
) should be saved in a variable called <NAME>
. Together with the <ID>
of the pipeline step, this value can be referenced with the expression ${{ steps.<ID>.outputs.<NAME> }}
in the following steps of your pipeline.
$ mvn versions:set -DnewVersion=my.new.version -DgenerateBackupPoms=false
+
This will update the version
property of every module in the reactor to prepare them for the next release. In case you are using GitHub Actions, consider using a timestamp.
name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Create release version
+ id: <ID>
+ run: echo "::set-output name=<NAME>::$(date +'%Y.%m.%d-%H%M%S')"
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<ID>
: The unique ID of the timestamp step.<NAME>
: The name of the created timestamp.The special syntax ::set-output name=<NAME>::
declares that the output of the command (echo
) should be saved in a variable called <NAME>
. Together with the <ID>
of the pipeline step, this value can be referenced with the expression ${{ steps.<ID>.outputs.<NAME> }}
in the following steps of your pipeline.
While the central server approach is easy to use, it might not work in all scenarios:
+In case of the first scenario, tools like copybara, repoSpanner, or distributed-Git-forks offer a wide range of features to cover most details.
+The second scenario can be solved manually with tools like gitomatic or automatically with GitLab’s mirror feature quite easy. GitLab allows to create a single pull-mirror and multiple push-mirrors. Therefore, it can be used to pull from your central server and push into all mirrors.
+NOTE: This feature was previously available in the free tier but has now moved to GitLab Ultimate.
+ +----------------+
+ | GitHub |
+ +----------------+
+ ^
+ |
+ |
+ +----------------+
+ +-----| GitLab |------+
+ | +----------------+ |
+ | |
+ | |
+ v v
++----------------+ +----------------+
+| Codeberg | | BitBucket |
++----------------+ +----------------+
+
To create such a setup, follow these steps:
+Settings > Repository
and expand Mirroring repositories
+https://github.com/metio/ilo.git
+https://YOUR_USER@codeberg.org/metio.wtf/ilo.git
. Add an access token for each mirror and select password
as authentication method.
+In case you prefer SSH keys over HTTP access tokens, just select SSH public key
as authentication method and verify that your key is both saved in GitLab and all mirrors.
$ git remote add mirrors DISABLED
+$ git remote set-url --add --push mirrors git@codeberg.org:org/repo.git
+$ git remote set-url --add --push mirrors git@gitlab.com:org/repo.git
+$ git remote set-url --add --push mirrors git@bitbucket.org:org/repo.git
+
The above will create a new remote called mirrors
which has no fetch
URL and therefore can only be pushed:
$ git remote -v
+mirrors DISABLED (fetch)
+mirrors git@codeberg.org:org/repo.git (push)
+mirrors git@gitlab.com:org/repo.git (push)
+mirrors git@bitbucket.org:org/repo.git (push)
+
Calling git push mirrors main:main
will push the local main
branch into all defined mirrors.
$ git remote set-url origin --push --add git@example.com/project.git
+$ git remote set-url origin --push --add git@another.com/project.git
+
Note that the first call to set-url
will overwrite an existing remote creating with git clone
. Any additional call will actually recognize the --add
option and add the new target to an existing remote.
<properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+</properties>
+
project.build.outputTimestamp
property like this:
+<properties>
+ <project.build.outputTimestamp>2020</project.build.outputTimestamp>
+</properties>
+
settings.xml
file:
+<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
+ http://maven.apache.org/xsd/settings-1.0.0.xsd">
+
+ <pluginGroups>
+ <pluginGroup>org.sonarsource.scanner.maven</pluginGroup>
+ </pluginGroups>
+
+ <activeProfiles>
+ <activeProfile>sonar</activeProfile>
+ </activeProfiles>
+
+ <profiles>
+ <profile>
+ <id>sonar</id>
+ <properties>
+ <sonar.host.url>https://sonarcloud.io</sonar.host.url>
+ <sonar.organization>YOUR_ORG</sonar.organization>
+ <sonar.projectKey>YOUR_PROJECT</sonar.projectKey>
+ <sonar.login>${env.SONAR_TOKEN}</sonar.login>
+ </properties>
+ </profile>
+ </profiles>
+</settings>
+
Finally, add a step to your workflow:
+- name: Verify Project
+ run: mvn --settings $GITHUB_WORKSPACE/settings.xml verify sonar:sonar
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
on: schedule: ...
configuration, and a conditional build step.
+name: <PIPELINE>
+on:
+ schedule:
+ - cron: '<CRON>'
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Count commits in last week
+ id: commits
+ run: echo "::set-output name=count::$(git rev-list --count HEAD --since='<DATE>')"
+ - name: Build project
+ if: steps.commits.outputs.count > 0
+ run: build-project
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<CRON>
: cron expression - use https://crontab.guru/.<DATE>
: Git date expression that matches <CRON>
.name: <NAME>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Publish Toot
+ uses: rzr/fediverse-action@master
+ with:
+ access-token: ${{ secrets.MASTODON_TOKEN }}
+ message: <MESSAGE>
+ host: ${{ secrets.MASTODON_SERVER }}
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<MESSAGE>
: Message for the toot.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Send mail
+ uses: dawidd6/action-send-mail@v3
+ with:
+ server_address: ${{ secrets.MAIL_SERVER }}
+ server_port: ${{ secrets.MAIL_PORT }}
+ username: ${{ secrets.MAIL_USERNAME }}
+ password: ${{ secrets.MAIL_PASSWORD }}
+ subject: <SUBJECT>
+ body: <BODY>
+ to: ${{ secrets.MAIL_RECIPIENT }}
+ from: ${{ secrets.MAIL_SENDER }}
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<SUBJECT>
: Subject for the email.<BODY>
: Body for the email.Create appropriate secrets in your organization or project. In case you are using an organization, but different mailing lists, define MAIL_RECIPIENT
for each project.
name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Upload Release Asset
+ id: upload_release_asset
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ steps.create_release.outputs.upload_url }}
+ asset_path: ./some/path/to/file.zip
+ asset_name: public-name-for-file.zip
+ asset_content_type: application/zip
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Create Release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: <TAG>
+ release_name: <RELEASE>
+ draft: false
+ prerelease: false
+ body: |
+ Your release text here
+
+ Some code block:
+ ```yaml
+ yaml:
+ inside:
+ of:
+ another: yaml
+ ```
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<TAG>
: The Git tag to create.<RELEASE>
: The release name to use.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Deploy Website
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: <PUBLISH_DIR>
+ force_orphan: true
+ cname: <CNAME>
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<PUBLISH_DIR>
: The file system location of the built site.<CNAME>
: The CNAME
of your custom domain.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Cache Maven artifacts
+ uses: actions/cache@v1
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Setup hugo
+ uses: peaceiris/actions-hugo@v2
+ with:
+ hugo-version: <HUGO_VERSION>
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<HUGO_VERSION>
: The released versions or use latest
to always use the latest version of Hugo.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Set up JDK <JDK_VERSION>
+ uses: actions/setup-java@v1
+ with:
+ java-version: <JDK_VERSION>
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<JDK_VERSION>
: The required Java version for your project.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Create release version
+ id: <ID>
+ run: echo "::set-output name=<NAME>::$(date +'%Y.%m.%d-%H%M%S')"
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<ID>
: The unique ID of the timestamp step.<NAME>
: The name of the created timestamp.The special syntax ::set-output name=<NAME>::
declares that the output of the command (echo
) should be saved in a variable called <NAME>
. Together with the <ID>
of the pipeline step, this value can be referenced with the expression ${{ steps.<ID>.outputs.<NAME> }}
in the following steps of your pipeline.
In its simplest (shortest) form a render-nothing component looks like the following snippet. It does not actually do anything and is not particularly helpful for anything. You could add it to every other component in your application without breaking or influencing anything.
+const RendersNothing = () => <></>
+
Now consider the following example, that adds some if-then-else
logic to the same component:
const MightRenderSomething = () => {
+ if (someCondition) {
+ return <span>hello world!</span>
+ }
+ return <></>
+}
+
This component encapsulates the if-then-else
logic of conditionally rendering a hello world message. Instead of cluttering your entire app with the same logic, you can now simply re-use that same component that contains this if
condition. To see the full power of this technique, consider the following example. At first, we are going to define a hook that reads the current window width, then define components that conditionally render based on the current window width, and finally use those components in an example application.
const useWindowWidth = () => {
+ const [width, setWidth] = React.useState(0)
+
+ React.useEffect(() => {
+ const handleResize = () => {
+ setWidth(window.innerWidth)
+ }
+ window.addEventListener("resize", handleResize)
+ return () => {
+ window.removeEventListener("resize", handleResize)
+ }
+ }, [])
+
+ return width
+}
+
The following components use that hook to implement UI breakpoints for small (mobile) and large (desktop) screens. Note that the value 768
is just an example - replace it with whatever your design system tells you to.
const ForMobileDevicesOnly = (props) => {
+ const windowWidth = useWindowWidth()
+
+ if (windowWidth < 768) {
+ return <>{props.children}</>
+ }
+ return <></>
+}
+
+const ForDesktopDevicesOnly = (props) => {
+ const windowWidth = useWindowWidth()
+
+ if (windowWidth >= 768) {
+ return <>{props.children}</>
+ }
+ return <></>
+}
+
Both of these components simply render nothing when the window width does not have an appropriate size. If the window width does have the right size, they render their children
. We can use those components in our application like this:
const SomeActualComponent = () => (
+ <div>
+ <h1>common headline</h1>
+ <ForMobileDevicesOnly>
+ <span>only visible on mobile devices</span>
+ </ForMobileDevicesOnly>
+ <ForDesktopDevicesOnly>
+ <span>only visible on desktop devices</span>
+ </ForDesktopDevicesOnly>
+ </div>
+)
+
The above code snippet declares that some part of the UI can only be seen by mobile users, while others can only be seen by desktop users. Parts of the UI that are shared amongst all users are not wrapped by any of the components defined above.
+]]>.home.arpa
to join the fun. In case you have hostnamectl
available on your system run the following command to change the hostname of a device:
+# set hostname
+$ hostnamectl hostname some-device.home.arpa
+
+# check hostname
+$ hostnamectl status
+
NullPointerException
at least once in their life. The exception is thrown every time you try to dereference and use some object before initializing it. The following snippet shows a simple example:
+String someName; // value is 'null'
+
+someName.toUpperCase(); // throws NullPointerException
+
Modern IDEs have some sort of detection for this kind of problem and warn developers while they are writing code like this. Those IDEs typically rely on static code analysis to determine if a value is null
and therefore a potential for a NullPointerException
is present in your code. To improve the result of such an analysis, annotations can be placed on your code which signal that a parameter can or can not be null
. Multiple approaches have existed in the past to define a standard set of annotations for such a task, however none of them succeeded.
jspecify is the latest approach that tries to establish a standard. It has gained wide community support and recently celebrated their first public release (0.3.0
).
The following snippet shows the dependency declaration for Maven projects:
+<dependencies>
+ <dependency>
+ <groupId>org.jspecify</groupId>
+ <artifactId>jspecify</artifactId>
+ <version>0.3.0</version>
+ </dependency>
+</dependencies>
+
In case you want to declare that nothing in your module can ever be null
, place the @NullMarked
on your module-info.java
like this:
@org.jspecify.annotations.NullMarked
+module your.module.here {
+
+ requires org.jspecify;
+
+ // ...
+
+}
+
The tooling support is not quiet clear yet, however if you are developing a library there is no harm in adding these annotations now and let your users enjoy their null-free life once tools have caught up.
+]]>gpg
it is much simpler by focusing on the encryption parts only.
+Add the following snippet to your .chezmoi.toml
to configure chezmoi
to use age
:
encryption = "age"
+[age]
+ identity = "path/to/age/private-key"
+ recipient = "age...public...key..."
+
Adding files to your chezmoi
source directory remains the same as compared to using gpg
- just call chezmoi add --encrypt path/to/file
.
[user]
+ name = Your Name Here
+
+[includeIf "gitdir:~/git/personal/"]
+ path = ~/.config/git/personal
+[includeIf "gitdir:~/git/work/"]
+ path = ~/.config/git/work
+
The includeIf directive supports multiple keywords. In my case, work and personal projects have a different root directory, therefore I can filter based on the location using gitdir
. The personal Git configuration simply looks like this:
[user]
+ email = personal.email@example.com
+
and the work related configuration like this using a different email address:
+[user]
+ email = first.last@work.example
+
Additional settings that are different for personal/work accounts can be split the same way, for example to use a different signing key for work.
+]]>fzf
.
+fd --type=file --base-directory="${PASSAGE_DIR:-${HOME}/.passage/store}" .age --exec echo '{.}' | \
+ sk --cycle --layout=reverse --tiebreak=score --no-multi | \
+ xargs --replace --max-args=1 --no-run-if-empty \
+ passage show --clip=1 {}
+
This version requires fd, skim, xargs, and passage itself of course. The detailed breakdown on how it works is as follows:
+fd
to find all files within ${PASSAGE_DIR}
that end in .age
. Each password in passage is inside that folder and has such a file extensions, therefore we are selecting every password we have.--base-directory
and --exec echo '{.}'
ensures that passwords are returned in such form that they can be passed back into passage
again. The placeholder '{.}'
is a feature provided by fd
which strips the file extension from each returned value.sk
to allow to fuzzy search across them all. Setting --no-multi
ensures that only a single password can be selected.xargs
calls passage
and replaces the curly braces with the selected password. Thanks to --clip=1
, the first line in the selected password entry will be copied to the clipboard and automatically cleared after 45 seconds.To call that script, I’ve saved it as passage-fuzzy-search.sh
in my .local/bin
folder and added some checks into it to verify that every required software is actually installed.
#!/usr/bin/env zsh
+
+###############################################################################
+# This shell script presents passwords saved with passage through skim
+#
+# Call it like this:
+# passage-fuzzy-search.sh
+#
+# Required software that isn't in GNU coreutils:
+# - 'passage' to read passwords
+# - 'fd' to find passwords
+# - 'sk' to filter passwords
+###############################################################################
+
+if ! (( ${+commands[passage]} )); then
+ echo 'passage not installed. Please install passage.'
+ exit
+fi
+if ! (( ${+commands[sk]} )); then
+ echo 'sk not installed. Please install skim.'
+ exit
+fi
+if ! (( ${+commands[fd]} )); then
+ echo 'fd not installed. Please install fd-find.'
+ exit
+fi
+
+fd --type=file --base-directory="${PASSAGE_DIR:-${HOME}/.passage/store}" .age --exec echo '{.}' | \
+ sk --cycle --layout=reverse --tiebreak=score --no-multi | \
+ xargs --replace --max-args=1 --no-run-if-empty \
+ passage show --clip=1 {}
+
Since typing passage-fuzzy-search.sh
is way too long, I have added an alias like this:
alias pp='passage-fuzzy-search.sh'
+
systemd
unit:
+[Unit]
+Description=Update chezmoi managed dotfiles
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/chezmoi update --no-tty --force
+RemainAfterExit=false
+
+[Install]
+WantedBy=default.target
+
This unit pulls changes from upstream first and then applies the changes to the current computer after I’m logged in and a network connection is available. The --no-tty
flag is required because there is no tty when systemd executes chezmoi
. Likewise, the --force
flag ensures that no interactive prompt will be displayed which we cannot answer since systemd
is executing this unit without us being involved.
starship init zsh
or zoxide init zsh
. The documentation of these tools usually tell you to put something like eval "$(starship init zsh)"
into your shell RC file. While this approach works fine, it does decrease startup speed of your shell because it needs to run the init
command every time you open a new shell. Given that you open shells much more often than new versions of these tools are released and installed, you can cache the output of these commands to get a bit of speed back.
+chezmoi provides a template function called output which replaces itself with the output of the command you specified. You can use that function this to integrate various tools into your shell as the following example shows while using zsh
:
$ mkdir --parents "${ZDOTDIR}"/tools.d
+
.zshrc
file:
+for init_script in "${ZDOTDIR}"/tools.d/*.sh; do
+ source "${init_script}"
+done
+
.tmpl
files for each tool and place them in the chezmoi source directory that matches the directory you created in step 1:
+{{ output "starship" "init" "zsh" "--print-full-init" }}
+
chezmoi apply
to generate the init scripts.The only downside here is that you have to re-run chezmoi apply
after updating one of the tools because they change their init scripts sometimes. That problem can be solved with chezmoi auto-updates.
AWS_PROFILE
environment variable which is used by many tools that interact with the AWS API, like awscli
or terraform
. My current environment uses AzureAD as a single-sign-on provider, therefore this script uses aws sso login
to perform an MFA login into AWS. The AWS profiles must be set up in such a way that aws configure list-profiles
can detect them, which is typically done by adding them in ${AWS_CONFIG_FILE:-$HOME/.aws/config}
.
+#!/usr/bin/env sh
+
+###############################################################################
+# This script performs an AWS SSO login for the user-selected AWS profile
+# and sets the AWS_PROFILE environment variable afterwards. To use
+# this, create an alias that sources this script like this:
+#
+# alias awsenv='source path/to/this/script.sh'
+#
+# Required software that is not in GNU coreutils:
+# - 'aws' to list profiles & get current caller identity
+# - 'fzf' to list all available AWS profiles
+###############################################################################
+
+# prompt user to select one AWS profile
+profile=$(aws configure list-profiles | \
+ fzf --cycle --layout=reverse --tiebreak=index)
+
+# user can cancel switching profiles by pressing ESC
+if [ -n "${profile}" ]; then
+ # check is access token exists and is valid for selected profile
+ if ! aws --profile "${profile}" sts get-caller-identity >/dev/null 2>&1; then
+ # perform login into profile in case access token is invalid
+ if ! aws sso login --profile "${profile}"; then
+ # short circuit in case login failed
+ return
+ fi
+ fi
+ # AWS_PROFILE is used by many AWS-related tools
+ echo "Setting AWS_PROFILE to [${profile}]"
+ export AWS_PROFILE="${profile}"
+ # do not expose internal variables
+ unset profile
+fi
+
gen-class
Clojure code can be compiled to standard JVM bytecode using gen-class.
+Clojure imposes the concept of immutability. As such Clojure functions are/should be void of any state or side effects and only operate on the given input. Therefore, exporting Clojure functions as static Java methods makes sense. The following example defines a Clojure function, a corresponding Java-callable function and exports the Java function as a static method in the class com.example.Computation
.
(ns com.example.computation
+ (:gen-class
+ :name com.example.Computation
+ :methods [#^{:static true} [incrementRange [int] java.util.List]]))
+
+(defn increment-range
+ "Creates a sequence of numbers up to max and then increments them."
+ [max]
+ (map inc (take max (range))))
+
+(defn -incrementRange
+ "A Java-callable wrapper around the 'increment-range' function."
+ [max]
+ (increment-range max))
+
The Java wrapper has to follow the standard rules for method names. Therefore increment-range
has to be renamed to incrementRange
(or some similar name without the “-” in it). The “-” prefix for the Java wrapper can be configured inside the :gen-class
form and will be removed once gen-class
runs. The usage from Java looks like this:
package com.example
+
+public class ClojureJavaInteropStatic {
+
+ public static void main(String[] args) {
+ List incrementedRange = Computation.incrementRange(10);
+ }
+
+}
+
The returned list in the above code is raw because the method definition doesn’t use generics. To solve this problem declare that the generated class :implements
a certain interface that exposes the desired method definition(s). You won’t be able to declare your methods as static anymore, but get a generified method for all your Java needs.
The Java interface:
+package com.example
+
+public interface RangeIncrementer {
+ List<Long> incrementRange(int max);
+}
+
The changed Clojure namespace:
+(ns com.example.computation
+ (:gen-class
+ :name com.example.Computation
+ :implements [com.example.RangeIncrementer]))
+
+(defn increment-range
+ "Creates a sequence of numbers up to max and then increments them."
+ [max]
+ (map inc (take max (range))))
+
+(defn -incrementRange
+ "A Java-callable wrapper around the 'increment-range' function."
+ [this max]
+ (increment-range max))
+
Finally, the generified usage from Java:
+package com.example
+
+public class ClojureJavaInteropGenerics {
+
+ public static void main(String[] args) {
+ RangeIncrementer incrementer = new Computation();
+ List<Long> incrementedRange = incrementer.incrementRange(10);
+ }
+
+}
+
Couple of notes for this as well: First the generated class still only returns the raw type (List
instead of List<Integer>
). So instead of using the class, use the interface for the variable declaration (RangeIncrementer incrementer = ..
instead of Computation comp = ..
). The interface will return the non-raw List
. Second the function definition for -incrementRange
is now slightly different. It needs an additional parameter (this
) which exposes the current instance to the generated class/method.
Returning an array of something is also possible with the following construct "[Ljava.lang.Object;"
. Need a 2-dim array? Just use "[[Ljava.lang.Object;"
(notice the extra [
) and so on. However, be aware that the method return types have to match, for example you can’t specify a return type of array if your Clojure function does not return an array. In the example above the call to map
returns LazySeq
which itself is a java.util.List
. Therefore, the method declaration is valid, and you won’t get any ClassCastException
when calling incrementRange
from Java.
Instead of defining every Clojure function which should be exported twice (the real function + the Java wrapper), it is possible to use a macro to do that extra work automatically.
+(require '[clojure.string :as string)
+
+(defn camel-case [input]
+ (let [words (string/split input #"[\s_-]+")]
+ (string/join (cons (string/lower-case (first words)) (map string/capitalize (rest words))))))
+
+(defn java-name [clojure-name]
+ (symbol (str "-" (camel-case (str clojure-name)))))
+
+(defmacro defn* [name & declarations]
+ (let [java-name (java-name name)]
+ `(do (defn ~name ~declarations)
+ (defn ~java-name ~declarations))))
+
The macro defn*
replaces defn
and automatically creates a second function with a valid camel-cased Java method name. The macro is available as a small library at Maven Central. The macro won’t add the extra parameter mentioned above to Java wrapper, so it is only useful for declaring static methods.
Using gen-class
imposes certain limitations on calling Clojure code from Java. One of those are functions which make use of Clojure parameter destructuring. To invoke those functions you have to use the Clojure runtime.
// The Clojure 'require' function from the 'clojure.core' namespace.
+Var require = RT.var("clojure.core", "require");
+
+// Your namespace
+Symbol namespace = Symbol.intern("DESIRED.NAMESPACE.HERE");
+
+// Your function
+Var function = RT.var("DESIRED.NAMESPACE.HERE", "DESIRED-FUNCTION");
+
+// The required keyword for the above function
+Keyword keyword = Keyword.intern("REQUIRED-KEYWORD");
+
+// Require/Import your namespace
+require.invoke(namespace);
+
+// Invoke your function with the given keyword and its value
+Object result = function.invoke(keyword, VALUE);
+
The desired namespace has to be on the classpath for this to work. Alternatively it is possible to load an entire Clojure script, as shown in the following example:
+RT.loadResourceScript("DESIRED/NAMESPACE/HERE.clj");
+RT.var("DESIRED.NAMESPACE.HERE", "DESIRED-FUNCTION").invoke(PARAMETER);
+
On a big project it is properly wise to move Java->Clojure interop code into helper classes/methods. Look here for an example.
+]]>In its simplest (shortest) form a render-nothing component looks like the following snippet. It does not actually do anything and is not particularly helpful for anything. You could add it to every other component in your application without breaking or influencing anything.
+const RendersNothing = () => <></>
+
Now consider the following example, that adds some if-then-else
logic to the same component:
const MightRenderSomething = () => {
+ if (someCondition) {
+ return <span>hello world!</span>
+ }
+ return <></>
+}
+
This component encapsulates the if-then-else
logic of conditionally rendering a hello world message. Instead of cluttering your entire app with the same logic, you can now simply re-use that same component that contains this if
condition. To see the full power of this technique, consider the following example. At first, we are going to define a hook that reads the current window width, then define components that conditionally render based on the current window width, and finally use those components in an example application.
const useWindowWidth = () => {
+ const [width, setWidth] = React.useState(0)
+
+ React.useEffect(() => {
+ const handleResize = () => {
+ setWidth(window.innerWidth)
+ }
+ window.addEventListener("resize", handleResize)
+ return () => {
+ window.removeEventListener("resize", handleResize)
+ }
+ }, [])
+
+ return width
+}
+
The following components use that hook to implement UI breakpoints for small (mobile) and large (desktop) screens. Note that the value 768
is just an example - replace it with whatever your design system tells you to.
const ForMobileDevicesOnly = (props) => {
+ const windowWidth = useWindowWidth()
+
+ if (windowWidth < 768) {
+ return <>{props.children}</>
+ }
+ return <></>
+}
+
+const ForDesktopDevicesOnly = (props) => {
+ const windowWidth = useWindowWidth()
+
+ if (windowWidth >= 768) {
+ return <>{props.children}</>
+ }
+ return <></>
+}
+
Both of these components simply render nothing when the window width does not have an appropriate size. If the window width does have the right size, they render their children
. We can use those components in our application like this:
const SomeActualComponent = () => (
+ <div>
+ <h1>common headline</h1>
+ <ForMobileDevicesOnly>
+ <span>only visible on mobile devices</span>
+ </ForMobileDevicesOnly>
+ <ForDesktopDevicesOnly>
+ <span>only visible on desktop devices</span>
+ </ForDesktopDevicesOnly>
+ </div>
+)
+
The above code snippet declares that some part of the UI can only be seen by mobile users, while others can only be seen by desktop users. Parts of the UI that are shared amongst all users are not wrapped by any of the components defined above.
+]]>~/.vim/pack/*/{start,opt}/*
(Vim) or ~/.local/share/nvim/site/pack/*/{start,opt}/*
(Neovim). All you have to do to install new plugins, is to git clone
their repository into those directories. To automatically update those clones, create the following script:
+#!/usr/bin/env zsh
+
+###############################################################################
+# This script git-pulls all installed nvim plugins which are using the built-in
+# nvim plugin manager. Those plugins are located in .local/share/nvim/site/pack
+#
+# Required software that is not in GNU coreutils:
+# - 'git' to fetch plugin updates from upstream
+###############################################################################
+
+### User specific variables, adjust to your own needs
+
+# folder that contains all nvim plugins
+PLUGIN_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/nvim/site/pack"
+
+### Script logic
+
+echo "updating all plugins in ${PLUGIN_DIR}"
+# iterate through all directories and git pull em
+for directory in "${PLUGIN_DIR}"/*/{start,opt}/*; do
+ if [ -d "${directory}" ]; then
+ plugin=$(basename "${directory}")
+ echo "updating ${plugin}"
+ git -C "${directory}" pull --quiet
+ fi
+done
+
In case you are using Vim, adjust the PLUGIN_DIR
variable to point to your Vim plugin directory instead and save the resulting shell script as a file called update-nvim-plugins.sh
in some folder of your choice. Do not forget to set the executable bit with chmod +x /path/to/your/folder/update-nvim-plugins.sh
. Since all good developers must be lazy, write the following systemd service to execute the above script automatically:
[Unit]
+Description=cron job that triggers an update of all nvim plugins
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+Type=oneshot
+ExecStart=/path/to/your/folder/update-nvim-plugins.sh
+RemainAfterExit=false
+
Adjust the ExecStart
line to match the location where you saved the above script and place that service definition in a file called nvim-plugins-update.service
into your local ~/.config/systemd/user
directory. Add another file called nvim-plugins-update.timer
next to it that defines a systemd timer with the following content:
[Unit]
+Description=Update nvim plugins every week
+
+[Timer]
+OnCalendar=weekly
+Persistent=true
+RandomizedDelaySec=5hours
+
+[Install]
+WantedBy=timers.target
+
Adjust how often you want to update the plugins you are using in the OnCalendar
line. Enable this service/timer with:
$ systemctl --user enable nvim-plugins-update
+
Finally, add the following shell aliases to make it easier to interact with the created systemd units:
+# trigger an update manually
+alias update-nvim-plugins='systemctl --user start nvim-plugins-update'
+# see status of last auto-update
+alias update-nvim-plugins-status='systemctl --user status nvim-plugins-update'
+# see logs of past auto-updates
+alias update-nvim-plugins-logs='journalctl --user --unit nvim-plugins-update'
+
With this setup in place, all your plugins will be automatically updated once per week or however often you have configured in the timer.
+]]>~/.config/tmuxp
which looks like this:
+session_name: cool-app
+start_directory: ~/projects/cool-app
+windows:
+- window_name: backend
+ start_directory: backend
+- window_name: frontend
+ start_directory: frontend
+
In case the name of the file is cool-app.yaml
, you can open the sessions with tmuxp load cool-app --yes
.
$ mvn versions:set -DnewVersion=my.new.version -DgenerateBackupPoms=false
+
This will update the version
property of every module in the reactor to prepare them for the next release. In case you are using GitHub Actions, consider using a timestamp.
~/.m2/settings.xml
:
+<settings>
+ <profiles>
+ <profile>
+ <id>github</id>
+ <repositories>
+ <repository>
+ <id>maven-build-process</id>
+ <name>GitHub maven-build-process Apache Maven Packages</name>
+ <url>https://maven.pkg.github.com/metio/maven-build-process</url>
+ <releases>
+ <enabled>true</enabled>
+ </releases>
+ <snapshots>
+ <enabled>true</enabled>
+ </snapshots>
+ </repository>
+ <repository>
+ <id>hcf4j</id>
+ <name>GitHub hcf4j Apache Maven Packages</name>
+ <url>https://maven.pkg.github.com/metio/hcf4j</url>
+ <releases>
+ <enabled>true</enabled>
+ </releases>
+ <snapshots>
+ <enabled>true</enabled>
+ </snapshots>
+ </repository>
+ </repositories>
+ </profile>
+ </profiles>
+ <servers>
+ <server>
+ <id>maven-build-process</id>
+ <username>USERNAME</username>
+ <password>GITHUB_TOKEN</password>
+ </server>
+ <server>
+ <id>hcf4j</id>
+ <username>USERNAME</username>
+ <password>GITHUB_TOKEN</password>
+ </server>
+ </servers>
+</settings>
+
You will have to add another repository/server for each project you are fetching from GitHub.
+]]>~/.m2/settings.xml
like this:
+<settings>
+ <mirrors>
+ <mirror>
+ <id>google-maven-central</id>
+ <name>Google Maven Central (Asia)</name>
+ <url>https://maven-central-asia.storage-download.googleapis.com/maven2/</url>
+ <mirrorOf>central</mirrorOf>
+ </mirror>
+ <mirror>
+ <id>google-maven-central</id>
+ <name>Google Maven Central (EU)</name>
+ <url>https://maven-central-eu.storage-download.googleapis.com/maven2/</url>
+ <mirrorOf>central</mirrorOf>
+ </mirror>
+ <mirror>
+ <id>google-maven-central</id>
+ <name>Google Maven Central (US)</name>
+ <url>https://maven-central.storage-download.googleapis.com/maven2/</url>
+ <mirrorOf>central</mirrorOf>
+ </mirror>
+ </mirrors>
+</settings>
+
Pick the mirror nearest to your location to get best speeds.
+]]><file>~
. Unfortunately, emacs will not clean those up by default, which annoys me from time to time. Therefore, I’m now using the following configuration to keep those backups in a different folder:
+(setq version-control t ;; Use version numbers for backups.
+ kept-new-versions 10 ;; Number of newest versions to keep.
+ kept-old-versions 0 ;; Number of oldest versions to keep.
+ delete-old-versions t ;; Don't ask to delete excess backup versions.
+ backup-by-copying t) ;; Copy all files, don't rename them.
+
+(setq vc-make-backup-files t)
+
+;; Default and per-save backups go here:
+(setq backup-directory-alist '(("" . "~/.emacs.d/backup/per-save")))
+
+(defun force-backup-of-buffer ()
+ ;; Make a special "per session" backup at the first save of each
+ ;; emacs session.
+ (when (not buffer-backed-up)
+ ;; Override the default parameters for per-session backups.
+ (let ((backup-directory-alist '(("" . "~/.emacs.d/backup/per-session")))
+ (kept-new-versions 3))
+ (backup-buffer)))
+ ;; Make a "per save" backup on each save. The first save results in
+ ;; both a per-session and a per-save backup, to keep the numbering
+ ;; of per-save backups consistent.
+ (let ((buffer-backed-up nil))
+ (backup-buffer)))
+
+(add-hook 'before-save-hook 'force-backup-of-buffer)
+
Thanks to that configuration, backups per-save will be created in ~/.emacs.d/backup/per-save
and backups per-session in ~/.emacs.d/backup/per-session
.
function m-dotfiles-ok {
+ # public
+ chezmoi add ~/.config/zsh --recursive
+ chezmoi add ~/.config/sway --recursive
+ chezmoi add ~/.config/tmux --recursive
+ chezmoi add ....
+
+ # secrets
+ chezmoi add --encrypt ~/.config/npm/npmrc
+ chezmoi add --encrypt ~/.ssh/id_rsa
+ chezmoi add --encrypt ...
+}
+
Whenever you feel happy with your current setup, just call m-dotfiles-ok
to push changes into the chezmoi source directory. Files will automatically be encrypted with gpg and committed/pushed into a Git repository if you have done the necessary configuration beforehand.
In general, editing your dotfiles directly as explained in the second option of the FAQ seems easier though. Refactoring your dotfiles is especially easy when the exact_
prefix is used for directories. As explained in the documentation, all files that are not managed by chezmoi
will be removed, therefore your configuration will always match what is in your source directory.
gpg
.
+chezmoi can use various external tools to keep data private. gpg is used by various other tools as well, so chances are that you already have a functional setup on your system. To configure gpg
with chezmoi
, just set yourself as the recipient like this:
[gpg]
+ recipient = "your.name@example.com"
+
Calling chezmoi add --encrypt /path/to/secret
will now create encrypt the file with your public key which allows you to decrypt them later with your private key.
chezmoi.toml
+[sourceVCS]
+ autoCommit = true
+ autoPush = true
+
Every time you call chezmoi add /path/to/file
will now create a new commit in your local chezmoi repository and push those changes into your configured remote repository.
bar {
+ swaybar_command waybar
+}
+
Configure Waybar itself in ~/.config/waybar/config
:
{
+ "layer": "top",
+ "modules-left": ["sway/workspaces", "sway/mode"],
+ "modules-center": ["sway/window"],
+ "modules-right": ["clock"],
+ "sway/window": {
+ "max-length": 50
+ },
+ "clock": {
+ "format-alt": "{:%a, %d. %b %H:%M}"
+ }
+}
+
]]>$ tmux show-options -g | grep status
+
Change on of those values with in the current tmux session:
+$ tmux set-option status-right ""
+
Persist the change in your tmux.conf
like this:
# disable right side of status bar
+set-option -g status-right ""
+
[Unit]
+Description=Emacs text editor [%I]
+Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/
+
+[Service]
+Type=forking
+ExecStart=/usr/bin/emacs --daemon=%i
+ExecStop=/usr/bin/emacsclient --eval "(kill-emacs)"
+Environment=SSH_AUTH_SOCK=%t/keyring/ssh
+Restart=on-failure
+
+[Install]
+WantedBy=default.target
+
Enable it with systemctl --user enable emacs@user
and define any number of aliases to make connecting to the emacs daemon easier:
alias e='emacsclient --tty --socket-name=user'
+alias vim='emacsclient --tty --socket-name=user'
+alias vi='emacsclient --tty --socket-name=user'
+alias nano='emacsclient --tty --socket-name=user'
+alias ed='emacsclient --tty --socket-name=user'
+
$ git remote add mirrors DISABLED
+$ git remote set-url --add --push mirrors git@codeberg.org:org/repo.git
+$ git remote set-url --add --push mirrors git@gitlab.com:org/repo.git
+$ git remote set-url --add --push mirrors git@bitbucket.org:org/repo.git
+
The above will create a new remote called mirrors
which has no fetch
URL and therefore can only be pushed:
$ git remote -v
+mirrors DISABLED (fetch)
+mirrors git@codeberg.org:org/repo.git (push)
+mirrors git@gitlab.com:org/repo.git (push)
+mirrors git@bitbucket.org:org/repo.git (push)
+
Calling git push mirrors main:main
will push the local main
branch into all defined mirrors.
$ git remote set-url origin --push --add git@example.com/project.git
+$ git remote set-url origin --push --add git@another.com/project.git
+
Note that the first call to set-url
will overwrite an existing remote creating with git clone
. Any additional call will actually recognize the --add
option and add the new target to an existing remote.
alias rancher="kubectl --kubeconfig ~/.kube/rancher.config"
+alias work="kubectl --kubeconfig ~/.kube/work.config"
+alias customer="kubectl --kubeconfig ~/.kube/customer.config"
+
Those aliases allow me to write things like rancher get pods --namespace some-namespace
without worrying the wrong context is active. Using multiple configurations - one for each cluster - seems to be easier to manage since most clusters allow to download a ready-to-use configuration file. Instead of mangling them together manually, I just specify another alias whenever I get to work with another cluster.
<properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+</properties>
+
project.build.outputTimestamp
property like this:
+<properties>
+ <project.build.outputTimestamp>2020</project.build.outputTimestamp>
+</properties>
+
settings.xml
file:
+<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
+ http://maven.apache.org/xsd/settings-1.0.0.xsd">
+
+ <pluginGroups>
+ <pluginGroup>org.sonarsource.scanner.maven</pluginGroup>
+ </pluginGroups>
+
+ <activeProfiles>
+ <activeProfile>sonar</activeProfile>
+ </activeProfiles>
+
+ <profiles>
+ <profile>
+ <id>sonar</id>
+ <properties>
+ <sonar.host.url>https://sonarcloud.io</sonar.host.url>
+ <sonar.organization>YOUR_ORG</sonar.organization>
+ <sonar.projectKey>YOUR_PROJECT</sonar.projectKey>
+ <sonar.login>${env.SONAR_TOKEN}</sonar.login>
+ </properties>
+ </profile>
+ </profiles>
+</settings>
+
Finally, add a step to your workflow:
+- name: Verify Project
+ run: mvn --settings $GITHUB_WORKSPACE/settings.xml verify sonar:sonar
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
chsh
:
+# list all available shells
+$ chsh --list-shells
+/bin/sh
+/bin/bash
+/sbin/nologin
+/usr/bin/sh
+/usr/bin/bash
+/usr/sbin/nologin
+/usr/bin/zsh
+/bin/zsh
+/usr/bin/tmux
+/bin/tmux
+
+# select login shell
+$ chsh --shell /usr/bin/tmux
+
# lock your screen
+bindsym $mod+Ctrl+l exec swaylock --color 000000
+
$mod+Ctrl+l
will lock your screen and turn it to black. The --color
flag allows any color in the form of rrggbb[aa]
.
.bashrc
or similar file.
+peek() { tmux split-window -p 33 "$EDITOR" "$@" }
+
Calling peek <file>
will open <file>
in lower third of tmux window.
# take screenshot of currently focused screen
+bindsym $mod+Print exec /usr/bin/grim -o $(swaymsg -t get_outputs | jq -r '.[] | select(.focused) | .name') $(xdg-user-dir PICTURES)/$(date +'%Y-%m-%d-%H%M%S.png')
+
+# take screenshot of selection
+bindsym $mod+Shift+p exec /usr/bin/grim -g "$(/usr/bin/slurp)" $(xdg-user-dir PICTURES)/$(date +'%Y-%m-%d-%H%M%S.png')
+
README
file typically contain information about the project itself, for example how it can be installed/used/build. Most of the time these files contains command line instructions that users/contributors copy and paste into their terminal. Instead of doing that, consider placing a Makefile
in the root of your project which contains the exact same instructions. Thanks to make
, all your contributors can now use TAB-completion to run any of the pre-defined make
targets.
+The following example is part of one of my projects, and I certainly don’t want to type (or even copy) that all the time:
+.PHONY: release-into-local-nexus
+release-into-local-nexus:
+ mvn versions:set \
+ -DnewVersion=$(TIMESTAMPED_VERSION) \
+ -DgenerateBackupPoms=false
+ -mvn clean deploy scm:tag \
+ -DpushChanges=false \
+ -DskipLocalStaging=true \
+ -Drelease=local
+ mvn versions:set \
+ -DnewVersion=9999.99.99-SNAPSHOT \
+ -DgenerateBackupPoms=false
+
With the above target in place, everyone can now do make release-into-local-nexus
instead of typing/copying the commands themselves. Thanks to TAB-completion you just have to do make r<TAB>
and confirm with >ENTER>
to perform a release.
on: schedule: ...
configuration, and a conditional build step.
+name: <PIPELINE>
+on:
+ schedule:
+ - cron: '<CRON>'
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Count commits in last week
+ id: commits
+ run: echo "::set-output name=count::$(git rev-list --count HEAD --since='<DATE>')"
+ - name: Build project
+ if: steps.commits.outputs.count > 0
+ run: build-project
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<CRON>
: cron expression - use https://crontab.guru/.<DATE>
: Git date expression that matches <CRON>
.Makefile
to define a complex build step - for example start database, run tests, stop database - consider using the -
qualifier in front of your actual build step like this:
+.PHONY: build
+build:
+ start-database
+ -build-software
+ stop-database
+
Thanks to -
, the database will be stopped even if building your software fails, therefore making sure to clean up after ourselves once the build finishes.
config.toml
:
+[mediaTypes."application/javascript"]
+ suffixes = ["js"]
+[outputFormats.ServiceWorker]
+ name = "ServiceWorker"
+ mediaType = "application/javascript"
+ baseName = "serviceworker"
+ isPlainText = false
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/home.serviceworker.js
with the following content:
const CACHE = 'cache-and-update';
+
+self.addEventListener('install', (event) => {
+ event.waitUntil(precache());
+});
+
+self.addEventListener('fetch', (event) => {
+ event.respondWith(fromCache(event.request));
+ event.waitUntil(update(event.request));
+});
+
+const precache = async () => {
+ const cache = await caches.open(CACHE);
+ return await cache.addAll([
+ {{ range $i, $e := .Site.RegularPages }}
+ '{{ $.RelPermalink }}'{{ if $i }}, {{ end }}
+ {{ end }}
+ ]);
+}
+
+const fromCache = async (request) => {
+ const cache = await caches.open(CACHE);
+ const match = await cache.match(request);
+ return match || Promise.reject('no-match');
+}
+
+const update = async (request) => {
+ const cache = await caches.open(CACHE);
+ const response = await fetch(request);
+ return await cache.put(request, response);
+}
+
config.toml
:
+[mediaTypes."application/manifest+json"]
+ suffixes = ["webmanifest"]
+[outputFormats.Webmanifest]
+ name = "Web App Manifest"
+ mediaType = "application/manifest+json"
+ baseName = "manifest"
+ isPlainText = false
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/home.manifest.json
with the following content:
{
+ "name": "{{ .Site.Title }}",
+ "short_name": "{{ .Site.Title }}",
+ "start_url": ".",
+ "display": "minimal-ui",
+ "background_color": "#fff",
+ "description": "{{ .Site.Params.description }}"
+}
+
{{ $normalize := resources.Get "/css/normalize.css" }}
+{{ $font := resources.Get "/css/font.css" }}
+{{ $header := resources.Get "/css/header.css" }}
+{{ $footer := resources.Get "/css/footer.css" }}
+{{ $navigation := resources.Get "/css/navigation.css" }}
+{{ $navigation_mobile := resources.Get "/css/navigation-mobile.css" }}
+{{ $layout := resources.Get "/css/layout.css" }}
+{{ $layout_mobile := resources.Get "/css/layout-mobile.css" }}
+{{ $syntax := resources.Get "/css/syntax.css" }}
+{{ $darkmode := resources.Get "/css/darkmode.css" | resources.Minify | resources.Fingerprint "sha512" }}
+
+{{ $base := slice $normalize $font $header $footer $navigation $layout $syntax | resources.Concat "css/base.css" | resources.Minify | resources.Fingerprint "sha512" }}
+{{ $mobile := slice $navigation_mobile $layout_mobile | resources.Concat "css/mobile.css" | resources.Minify | resources.Fingerprint "sha512" }}
+
+<link href="{{ $base.Permalink }}" integrity="{{ $base.Data.Integrity }}" media="screen" rel="stylesheet">
+<link href="{{ $mobile.Permalink }}" integrity="{{ $mobile.Data.Integrity }}" media="screen and (max-width: 800px)" rel="stylesheet">
+
+<link href="{{ $darkmode.Permalink }}" integrity="{{ $darkmode.Data.Integrity }}" media="screen and (prefers-color-scheme: dark)" rel="stylesheet">
+
]]>config.toml
:
+[mediaTypes."text/plain"]
+ suffixes = ["txt"]
+[outputFormats.Humans]
+ name = "Humans"
+ mediaType = "text/plain"
+ baseName = "humans"
+ isPlainText = true
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/home.humans.txt
with the following content:
/* TEAM */
+{{ range $.Site.Data.contributors }}
+{{ .title }}: {{ .first_name }} {{ .last_name }}
+Site: {{ .website }}
+{{ end }}
+
]]>config.toml
:
+[mediaTypes."application/rdf+xml"]
+ suffixes = ["rdf"]
+[outputFormats.Foaf]
+ name = "FOAF"
+ mediaType = "application/rdf+xml"
+ baseName = "foaf"
+ isPlainText = false
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/home.foaf.rdf
with the following content:
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" xmlns:foaf="http://xmlns.com/foaf/0.1/">
+ <foaf:PersonalProfileDocument rdf:about="">
+ <foaf:maker rdf:resource="#me" />
+ <foaf:primaryTopic rdf:resource="{{ .Site.Title }}" />
+ </foaf:PersonalProfileDocument>
+
+ <foaf:Project rdf:ID="{{ .Site.Title }}">
+ <foaf:name>{{ .Site.Title }}</foaf:name>
+ <foaf:homepage rdf:resource="{{ .Site.BaseURL }}" />
+ </foaf:Project>
+
+ {{ range $.Site.Data.contributors }}
+ <foaf:Person rdf:ID="{{ .id }}">
+ <foaf:name>{{ .first_name }} {{ .last_name }}</foaf:name>
+ <foaf:title>{{ .title }}</foaf:title>
+ <foaf:givenname>{{ .first_name }}</foaf:givenname>
+ <foaf:family_name>{{ .last_name }}</foaf:family_name>
+ <foaf:mbox rdf:resource="mailto:{{ .email }}" />
+ <foaf:homepage rdf:resource="{{ .website }}" />
+ </foaf:Person>
+ {{ end }}
+</rdf:RDF>
+
]]>config.toml
:
+[mediaTypes."application/atom+xml"]
+ suffixes = ["xml"]
+[outputFormats.Atom]
+ name = "Atom"
+ mediaType = "application/atom+xml"
+ baseName = "atom"
+ isPlainText = false
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/list.atom.xml
with the following content:
{{ printf `<?xml version="1.0" encoding="utf-8"?>` | safeHTML }}
+<feed xmlns="http://www.w3.org/2005/Atom"{{ with site.LanguageCode }} xml:lang="{{ . }}"{{ end }}>
+ <generator uri="https://gohugo.io/" version="{{ hugo.Version }}">Hugo</generator>
+ {{- $title := site.Title -}}
+ {{- with .Title -}}
+ {{- if (not (eq . site.Title)) -}}
+ {{- $title = printf `%s %s %s` . (i18n "feed_title_on" | default "on") site.Title -}}
+ {{- end -}}
+ {{- end -}}
+ {{- if .IsTranslated -}}
+ {{ $title = printf "%s (%s)" $title (index site.Data.i18n.languages .Lang) }}
+ {{- end -}}
+ {{ printf `<title type="html"><![CDATA[%s]]></title>` $title | safeHTML }}
+ {{ with (or (.Param "subtitle") (.Param "tagline")) }}
+ {{ printf `<subtitle type="html"><![CDATA[%s]]></subtitle>` . | safeHTML }}
+ {{ end }}
+ {{ $output_formats := .OutputFormats }}
+ {{ range $output_formats -}}
+ {{- $rel := (or (and (eq "atom" (.Name | lower)) "self") "alternate") -}}
+ {{ with $output_formats.Get .Name }}
+ {{ printf `<link href=%q rel=%q type=%q title=%q />` .Permalink $rel .MediaType.Type .Name | safeHTML }}
+ {{- end -}}
+ {{- end }}
+ {{- range .Translations }}
+ {{ $output_formats := .OutputFormats }}
+ {{- $lang := .Lang }}
+ {{- $langstr := index site.Data.i18n.languages .Lang }}
+ {{ range $output_formats -}}
+ {{ with $output_formats.Get .Name }}
+ {{ printf `<link href=%q rel="alternate" type=%q hreflang=%q title="[%s] %s" />` .Permalink .MediaType.Type $lang $langstr .Name | safeHTML }}
+ {{- end -}}
+ {{- end }}
+ {{- end }}
+ <updated>{{ now.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</updated>
+ {{ with site.Copyright }}
+ {{- $copyright := replace . "{year}" now.Year -}} {{/* In case the site.copyright uses a special string "{year}" */}}
+ {{- $copyright = replace $copyright "©" "©" -}}
+ <rights>{{ $copyright | plainify }}</rights>
+ {{- end }}
+ {{ with .Param "feed" }}
+ {{/* For this to work, the $icon file should be present in the assets/ directory */}}
+ {{- $icon := .icon | default "icon.svg" -}}
+ {{- with resources.Get $icon -}}
+ <icon>{{ (. | fingerprint).Permalink }}</icon>
+ {{- end }}
+
+ {{/* For this to work, the $logo file should be present in the assets/ directory */}}
+ {{- $logo := .logo | default "logo.svg" -}}
+ {{- with resources.Get $logo -}}
+ <logo>{{ (. | fingerprint).Permalink }}</logo>
+ {{- end }}
+ {{ end }}
+ {{ with site.Author.name -}}
+ <author>
+ <name>{{ . }}</name>
+ {{ with site.Author.email }}
+ <email>{{ . }}</email>
+ {{ end -}}
+ </author>
+ {{- end }}
+ {{ with site.Params.id }}
+ <id>{{ . | plainify }}</id>
+ {{ else }}
+ <id>{{ .Permalink }}</id>
+ {{ end }}
+ {{- $limit := (cond (le site.Config.Services.RSS.Limit 0) 65536 site.Config.Services.RSS.Limit) }}
+ {{- $feed_sections := site.Params.feedSections | default site.Params.mainSections -}}
+ {{/* Range through only the pages with a Type in $feed_sections. */}}
+ {{- $pages := where .RegularPages "Type" "in" $feed_sections -}}
+ {{- if (eq .Kind "home") -}}
+ {{- $pages = where site.RegularPages "Type" "in" $feed_sections -}}
+ {{- end -}}
+ {{/* Remove the pages that have the disable_feed parameter set to true. */}}
+ {{- $pages = where $pages ".Params.disable_feed" "!=" true -}}
+ {{- range first $limit $pages }}
+ {{ $page := . }}
+ <entry>
+ {{ printf `<title type="html"><![CDATA[%s]]></title>` .Title | safeHTML }}
+ <link href="{{ .Permalink }}?utm_source=atom_feed" rel="alternate" type="text/html" />
+ {{- range .Translations }}
+ {{- $link := printf "%s?utm_source=atom_feed" .Permalink | safeHTML }}
+ {{- printf `<link href=%q rel="alternate" type="text/html" hreflang=%q />` $link .Lang | safeHTML }}
+ {{- end }}
+ {{/* rel=related: See https://validator.w3.org/feed/docs/atom.html#link */}}
+ {{- range first 5 (site.RegularPages.Related .) }}
+ <link href="{{ .Permalink }}?utm_source=atom_feed" rel="related" type="text/html" title="{{ .Title }}" />
+ {{- end }}
+ {{ with .Params.id }}
+ <id>{{ . | plainify }}</id>
+ {{ else }}
+ <id>{{ .Permalink }}</id>
+ {{ end }}
+ {{ with .Params.author -}}
+ {{- range . -}} <!-- Assuming the author front-matter to be a list -->
+ <author>
+ <name>{{ . }}</name>
+ </author>
+ {{- end -}}
+ {{- end }}
+ <published>{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</published>
+ <updated>{{ .Lastmod.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</updated>
+ {{ $description1 := .Description | default "" }}
+ {{ $description := (cond (eq "" $description1) "" (printf "<blockquote>%s</blockquote>" ($description1 | markdownify))) }}
+ {{ printf `<content type="html"><![CDATA[%s%s]]></content>` $description .Content | safeHTML }}
+ {{ with site.Taxonomies }}
+ {{ range $taxo,$_ := . }} <!-- Defaults taxos: "tags", "categories" -->
+ {{ with $page.Param $taxo }}
+ {{ $taxo_list := . }} <!-- $taxo_list will be the tags/categories list -->
+ {{ with site.GetPage (printf "/%s" $taxo) }}
+ {{ $taxonomy_page := . }}
+ {{ range $taxo_list }} <!-- Below, assuming pretty URLs -->
+ <category scheme="{{ printf "%s%s" $taxonomy_page.Permalink (. | urlize) }}" term="{{ (. | urlize) }}" label="{{ . }}" />
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ </entry>
+ {{ end }}
+</feed>
+
]]>name: <NAME>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Publish Toot
+ uses: rzr/fediverse-action@master
+ with:
+ access-token: ${{ secrets.MASTODON_TOKEN }}
+ message: <MESSAGE>
+ host: ${{ secrets.MASTODON_SERVER }}
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<MESSAGE>
: Message for the toot.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Send mail
+ uses: dawidd6/action-send-mail@v3
+ with:
+ server_address: ${{ secrets.MAIL_SERVER }}
+ server_port: ${{ secrets.MAIL_PORT }}
+ username: ${{ secrets.MAIL_USERNAME }}
+ password: ${{ secrets.MAIL_PASSWORD }}
+ subject: <SUBJECT>
+ body: <BODY>
+ to: ${{ secrets.MAIL_RECIPIENT }}
+ from: ${{ secrets.MAIL_SENDER }}
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<SUBJECT>
: Subject for the email.<BODY>
: Body for the email.Create appropriate secrets in your organization or project. In case you are using an organization, but different mailing lists, define MAIL_RECIPIENT
for each project.
name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Upload Release Asset
+ id: upload_release_asset
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ steps.create_release.outputs.upload_url }}
+ asset_path: ./some/path/to/file.zip
+ asset_name: public-name-for-file.zip
+ asset_content_type: application/zip
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Create Release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: <TAG>
+ release_name: <RELEASE>
+ draft: false
+ prerelease: false
+ body: |
+ Your release text here
+
+ Some code block:
+ ```yaml
+ yaml:
+ inside:
+ of:
+ another: yaml
+ ```
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<TAG>
: The Git tag to create.<RELEASE>
: The release name to use.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Deploy Website
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: <PUBLISH_DIR>
+ force_orphan: true
+ cname: <CNAME>
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<PUBLISH_DIR>
: The file system location of the built site.<CNAME>
: The CNAME
of your custom domain.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Cache Maven artifacts
+ uses: actions/cache@v1
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Setup hugo
+ uses: peaceiris/actions-hugo@v2
+ with:
+ hugo-version: <HUGO_VERSION>
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<HUGO_VERSION>
: The released versions or use latest
to always use the latest version of Hugo.name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Set up JDK <JDK_VERSION>
+ uses: actions/setup-java@v1
+ with:
+ java-version: <JDK_VERSION>
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<JDK_VERSION>
: The required Java version for your project.# use existing env variables or define new
+[ -z "$XDG_CACHE_HOME" ] && export XDG_CACHE_HOME="$HOME/.cache"
+[ -z "$XDG_CONFIG_DIRS" ] && export XDG_CONFIG_DIRS="/etc/xdg"
+[ -z "$XDG_CONFIG_HOME" ] && export XDG_CONFIG_HOME="$HOME/.config"
+[ -z "$XDG_DATA_DIRS" ] && export XDG_DATA_DIRS="/usr/local/share:/usr/share"
+[ -z "$XDG_DATA_HOME" ] && export XDG_DATA_HOME="$HOME/.local/share"
+
+# gradle
+export GRADLE_USER_HOME="$XDG_DATA_HOME/gradle"
+
+# httpie
+export HTTPIE_CONFIG_DIR="$XDG_CONFIG_HOME/httpie"
+
+# npm
+export NPM_CONFIG_USERCONFIG="$XDG_CONFIG_HOME/npm/npmrc"
+export npm_config_cache="$XDG_CACHE_HOME/npm"
+
+# password-store
+export PASSWORD_STORE_DIR="$XDG_DATA_HOME/password-store"
+
To make your own software XDG-aware, consider using the dirs-dev or configdir libraries.
+]]>Makefile
:
+GREEN := $(shell tput -Txterm setaf 2)
+WHITE := $(shell tput -Txterm setaf 7)
+YELLOW := $(shell tput -Txterm setaf 3)
+RESET := $(shell tput -Txterm sgr0)
+
+HELP_FUN = \
+ %help; \
+ while(<>) { push @{$$help{$$2 // 'targets'}}, [$$1, $$3] if /^([a-zA-Z\-]+)\s*:.*\#\#(?:@([a-zA-Z\-]+))?\s(.*)$$/ }; \
+ print "usage: make [target]\n\n"; \
+ for (sort keys %help) { \
+ print "${WHITE}$$_:${RESET}\n"; \
+ for (@{$$help{$$_}}) { \
+ $$sep = " " x (32 - length $$_->[0]); \
+ print " ${YELLOW}$$_->[0]${RESET}$$sep${GREEN}$$_->[1]${RESET}\n"; \
+ }; \
+ print "\n"; }
+
To use HELP_FUN
, add the following help
target to the same Makefile
:
.DEFAULT_GOAL := help
+
+.PHONY: help
+help: ##@other Show this help
+ @perl -e '$(HELP_FUN)' $(MAKEFILE_LIST)
+
Each target in the Makefile
is marked as phony to signal that those targets are not actually files that are generated as part of your build process. The optional description of a target can be placed after the ##@
prefix. The first word represents the group of a target and everything that follows is the description of a target. All targets should be formatted just like the help
target:
.PHONY: compile
+compile: ##@hacking Compile your code
+ <compile some code>
+
+.PHONY: test
+test: ##@hacking Test your code
+ <test some code>
+
+.PHONY: sign-cla
+sign-cla: ##@contrib Sign the contributor license agreement
+ <sign some file>
+
Once in place, you can either use make
without any argument to call the help
target or use make help
to see the generated output:
$ make
+usage: make [target]
+
+contrib:
+ sign-cla Sign the contributor license agreement
+
+hacking:
+ compile Compile your code
+ test Test your code
+
+other:
+ help Show this help
+
git clone git@github.com:orga/repo.git
all the time, consider using a custom SSH configuration (~/.ssh/config
) like this:
+Host github
+ HostName github.com
+ User git
+ IdentityFile ~/.ssh/<KEY-FOR-GITHUB>
+
+Host gitlab
+ HostName gitlab.com
+ User git
+ IdentityFile ~/.ssh/<KEY-FOR-GITLAB>
+
+Host bitbucket
+ HostName bitbucket.org
+ User git
+ IdentityFile ~/.ssh/<KEY-FOR-BITBUCKET>
+
+Host codeberg
+ HostName codeberg.org
+ User git
+ IdentityFile ~/.ssh/<KEY-FOR-CODEBERG>
+
Once configured, you can now write:
+$ git clone github:orga/repo
+$ git clone gitlab:orga/repo
+$ git clone bitbucket:orga/repo
+$ git clone codeberg:orga/repo
+
In case you are working with many repositories inside a single organization, consider adding the following Git configuration ($XDG_CONFIG_HOME/git/config
or ~/.gitconfig
):
[url "github:orga/"]
+ insteadOf = orga:
+[url "gitlab:orga/"]
+ insteadOf = orgl:
+[url "bitbucket:orga/"]
+ insteadOf = orgb:
+[url "codeberg:orga/"]
+ insteadOf = orgc:
+
Which allows you to just write:
+$ git clone orga:repo
+$ git clone orgl:repo
+$ git clone orgb:repo
+$ git clone orgc:repo
+
Git will substitute the insteadOf
values like orga:
with the configured url
(for example github:orga/
). The actual clone URL is github:orga/repo
at this point, which can be used by Git together with the SSH configuration mentioned above to clone repositories.
name: <PIPELINE>
+jobs:
+ build:
+ runs-on: <RUN_ON>
+ steps:
+ - name: Create release version
+ id: <ID>
+ run: echo "::set-output name=<NAME>::$(date +'%Y.%m.%d-%H%M%S')"
+
<PIPELINE>
: The name of your pipeline.<RUN_ON>
: The runner to use, see GitHub’s own documentation for possible values.<ID>
: The unique ID of the timestamp step.<NAME>
: The name of the created timestamp.The special syntax ::set-output name=<NAME>::
declares that the output of the command (echo
) should be saved in a variable called <NAME>
. Together with the <ID>
of the pipeline step, this value can be referenced with the expression ${{ steps.<ID>.outputs.<NAME> }}
in the following steps of your pipeline.
config.toml
:
+[mediaTypes."application/javascript"]
+ suffixes = ["js"]
+[outputFormats.ServiceWorker]
+ name = "ServiceWorker"
+ mediaType = "application/javascript"
+ baseName = "serviceworker"
+ isPlainText = false
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/home.serviceworker.js
with the following content:
const CACHE = 'cache-and-update';
+
+self.addEventListener('install', (event) => {
+ event.waitUntil(precache());
+});
+
+self.addEventListener('fetch', (event) => {
+ event.respondWith(fromCache(event.request));
+ event.waitUntil(update(event.request));
+});
+
+const precache = async () => {
+ const cache = await caches.open(CACHE);
+ return await cache.addAll([
+ {{ range $i, $e := .Site.RegularPages }}
+ '{{ $.RelPermalink }}'{{ if $i }}, {{ end }}
+ {{ end }}
+ ]);
+}
+
+const fromCache = async (request) => {
+ const cache = await caches.open(CACHE);
+ const match = await cache.match(request);
+ return match || Promise.reject('no-match');
+}
+
+const update = async (request) => {
+ const cache = await caches.open(CACHE);
+ const response = await fetch(request);
+ return await cache.put(request, response);
+}
+
config.toml
:
+[mediaTypes."application/manifest+json"]
+ suffixes = ["webmanifest"]
+[outputFormats.Webmanifest]
+ name = "Web App Manifest"
+ mediaType = "application/manifest+json"
+ baseName = "manifest"
+ isPlainText = false
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/home.manifest.json
with the following content:
{
+ "name": "{{ .Site.Title }}",
+ "short_name": "{{ .Site.Title }}",
+ "start_url": ".",
+ "display": "minimal-ui",
+ "background_color": "#fff",
+ "description": "{{ .Site.Params.description }}"
+}
+
{{ $normalize := resources.Get "/css/normalize.css" }}
+{{ $font := resources.Get "/css/font.css" }}
+{{ $header := resources.Get "/css/header.css" }}
+{{ $footer := resources.Get "/css/footer.css" }}
+{{ $navigation := resources.Get "/css/navigation.css" }}
+{{ $navigation_mobile := resources.Get "/css/navigation-mobile.css" }}
+{{ $layout := resources.Get "/css/layout.css" }}
+{{ $layout_mobile := resources.Get "/css/layout-mobile.css" }}
+{{ $syntax := resources.Get "/css/syntax.css" }}
+{{ $darkmode := resources.Get "/css/darkmode.css" | resources.Minify | resources.Fingerprint "sha512" }}
+
+{{ $base := slice $normalize $font $header $footer $navigation $layout $syntax | resources.Concat "css/base.css" | resources.Minify | resources.Fingerprint "sha512" }}
+{{ $mobile := slice $navigation_mobile $layout_mobile | resources.Concat "css/mobile.css" | resources.Minify | resources.Fingerprint "sha512" }}
+
+<link href="{{ $base.Permalink }}" integrity="{{ $base.Data.Integrity }}" media="screen" rel="stylesheet">
+<link href="{{ $mobile.Permalink }}" integrity="{{ $mobile.Data.Integrity }}" media="screen and (max-width: 800px)" rel="stylesheet">
+
+<link href="{{ $darkmode.Permalink }}" integrity="{{ $darkmode.Data.Integrity }}" media="screen and (prefers-color-scheme: dark)" rel="stylesheet">
+
]]>config.toml
:
+[mediaTypes."text/plain"]
+ suffixes = ["txt"]
+[outputFormats.Humans]
+ name = "Humans"
+ mediaType = "text/plain"
+ baseName = "humans"
+ isPlainText = true
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/home.humans.txt
with the following content:
/* TEAM */
+{{ range $.Site.Data.contributors }}
+{{ .title }}: {{ .first_name }} {{ .last_name }}
+Site: {{ .website }}
+{{ end }}
+
]]>config.toml
:
+[mediaTypes."application/rdf+xml"]
+ suffixes = ["rdf"]
+[outputFormats.Foaf]
+ name = "FOAF"
+ mediaType = "application/rdf+xml"
+ baseName = "foaf"
+ isPlainText = false
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/home.foaf.rdf
with the following content:
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" xmlns:foaf="http://xmlns.com/foaf/0.1/">
+ <foaf:PersonalProfileDocument rdf:about="">
+ <foaf:maker rdf:resource="#me" />
+ <foaf:primaryTopic rdf:resource="{{ .Site.Title }}" />
+ </foaf:PersonalProfileDocument>
+
+ <foaf:Project rdf:ID="{{ .Site.Title }}">
+ <foaf:name>{{ .Site.Title }}</foaf:name>
+ <foaf:homepage rdf:resource="{{ .Site.BaseURL }}" />
+ </foaf:Project>
+
+ {{ range $.Site.Data.contributors }}
+ <foaf:Person rdf:ID="{{ .id }}">
+ <foaf:name>{{ .first_name }} {{ .last_name }}</foaf:name>
+ <foaf:title>{{ .title }}</foaf:title>
+ <foaf:givenname>{{ .first_name }}</foaf:givenname>
+ <foaf:family_name>{{ .last_name }}</foaf:family_name>
+ <foaf:mbox rdf:resource="mailto:{{ .email }}" />
+ <foaf:homepage rdf:resource="{{ .website }}" />
+ </foaf:Person>
+ {{ end }}
+</rdf:RDF>
+
]]>config.toml
:
+[mediaTypes."application/atom+xml"]
+ suffixes = ["xml"]
+[outputFormats.Atom]
+ name = "Atom"
+ mediaType = "application/atom+xml"
+ baseName = "atom"
+ isPlainText = false
+ rel = "alternate"
+ isHTML = false
+ noUgly = true
+ permalinkable = false
+
Create a new layout in _default/list.atom.xml
with the following content:
{{ printf `<?xml version="1.0" encoding="utf-8"?>` | safeHTML }}
+<feed xmlns="http://www.w3.org/2005/Atom"{{ with site.LanguageCode }} xml:lang="{{ . }}"{{ end }}>
+ <generator uri="https://gohugo.io/" version="{{ hugo.Version }}">Hugo</generator>
+ {{- $title := site.Title -}}
+ {{- with .Title -}}
+ {{- if (not (eq . site.Title)) -}}
+ {{- $title = printf `%s %s %s` . (i18n "feed_title_on" | default "on") site.Title -}}
+ {{- end -}}
+ {{- end -}}
+ {{- if .IsTranslated -}}
+ {{ $title = printf "%s (%s)" $title (index site.Data.i18n.languages .Lang) }}
+ {{- end -}}
+ {{ printf `<title type="html"><![CDATA[%s]]></title>` $title | safeHTML }}
+ {{ with (or (.Param "subtitle") (.Param "tagline")) }}
+ {{ printf `<subtitle type="html"><![CDATA[%s]]></subtitle>` . | safeHTML }}
+ {{ end }}
+ {{ $output_formats := .OutputFormats }}
+ {{ range $output_formats -}}
+ {{- $rel := (or (and (eq "atom" (.Name | lower)) "self") "alternate") -}}
+ {{ with $output_formats.Get .Name }}
+ {{ printf `<link href=%q rel=%q type=%q title=%q />` .Permalink $rel .MediaType.Type .Name | safeHTML }}
+ {{- end -}}
+ {{- end }}
+ {{- range .Translations }}
+ {{ $output_formats := .OutputFormats }}
+ {{- $lang := .Lang }}
+ {{- $langstr := index site.Data.i18n.languages .Lang }}
+ {{ range $output_formats -}}
+ {{ with $output_formats.Get .Name }}
+ {{ printf `<link href=%q rel="alternate" type=%q hreflang=%q title="[%s] %s" />` .Permalink .MediaType.Type $lang $langstr .Name | safeHTML }}
+ {{- end -}}
+ {{- end }}
+ {{- end }}
+ <updated>{{ now.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</updated>
+ {{ with site.Copyright }}
+ {{- $copyright := replace . "{year}" now.Year -}} {{/* In case the site.copyright uses a special string "{year}" */}}
+ {{- $copyright = replace $copyright "©" "©" -}}
+ <rights>{{ $copyright | plainify }}</rights>
+ {{- end }}
+ {{ with .Param "feed" }}
+ {{/* For this to work, the $icon file should be present in the assets/ directory */}}
+ {{- $icon := .icon | default "icon.svg" -}}
+ {{- with resources.Get $icon -}}
+ <icon>{{ (. | fingerprint).Permalink }}</icon>
+ {{- end }}
+
+ {{/* For this to work, the $logo file should be present in the assets/ directory */}}
+ {{- $logo := .logo | default "logo.svg" -}}
+ {{- with resources.Get $logo -}}
+ <logo>{{ (. | fingerprint).Permalink }}</logo>
+ {{- end }}
+ {{ end }}
+ {{ with site.Author.name -}}
+ <author>
+ <name>{{ . }}</name>
+ {{ with site.Author.email }}
+ <email>{{ . }}</email>
+ {{ end -}}
+ </author>
+ {{- end }}
+ {{ with site.Params.id }}
+ <id>{{ . | plainify }}</id>
+ {{ else }}
+ <id>{{ .Permalink }}</id>
+ {{ end }}
+ {{- $limit := (cond (le site.Config.Services.RSS.Limit 0) 65536 site.Config.Services.RSS.Limit) }}
+ {{- $feed_sections := site.Params.feedSections | default site.Params.mainSections -}}
+ {{/* Range through only the pages with a Type in $feed_sections. */}}
+ {{- $pages := where .RegularPages "Type" "in" $feed_sections -}}
+ {{- if (eq .Kind "home") -}}
+ {{- $pages = where site.RegularPages "Type" "in" $feed_sections -}}
+ {{- end -}}
+ {{/* Remove the pages that have the disable_feed parameter set to true. */}}
+ {{- $pages = where $pages ".Params.disable_feed" "!=" true -}}
+ {{- range first $limit $pages }}
+ {{ $page := . }}
+ <entry>
+ {{ printf `<title type="html"><![CDATA[%s]]></title>` .Title | safeHTML }}
+ <link href="{{ .Permalink }}?utm_source=atom_feed" rel="alternate" type="text/html" />
+ {{- range .Translations }}
+ {{- $link := printf "%s?utm_source=atom_feed" .Permalink | safeHTML }}
+ {{- printf `<link href=%q rel="alternate" type="text/html" hreflang=%q />` $link .Lang | safeHTML }}
+ {{- end }}
+ {{/* rel=related: See https://validator.w3.org/feed/docs/atom.html#link */}}
+ {{- range first 5 (site.RegularPages.Related .) }}
+ <link href="{{ .Permalink }}?utm_source=atom_feed" rel="related" type="text/html" title="{{ .Title }}" />
+ {{- end }}
+ {{ with .Params.id }}
+ <id>{{ . | plainify }}</id>
+ {{ else }}
+ <id>{{ .Permalink }}</id>
+ {{ end }}
+ {{ with .Params.author -}}
+ {{- range . -}} <!-- Assuming the author front-matter to be a list -->
+ <author>
+ <name>{{ . }}</name>
+ </author>
+ {{- end -}}
+ {{- end }}
+ <published>{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</published>
+ <updated>{{ .Lastmod.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</updated>
+ {{ $description1 := .Description | default "" }}
+ {{ $description := (cond (eq "" $description1) "" (printf "<blockquote>%s</blockquote>" ($description1 | markdownify))) }}
+ {{ printf `<content type="html"><![CDATA[%s%s]]></content>` $description .Content | safeHTML }}
+ {{ with site.Taxonomies }}
+ {{ range $taxo,$_ := . }} <!-- Defaults taxos: "tags", "categories" -->
+ {{ with $page.Param $taxo }}
+ {{ $taxo_list := . }} <!-- $taxo_list will be the tags/categories list -->
+ {{ with site.GetPage (printf "/%s" $taxo) }}
+ {{ $taxonomy_page := . }}
+ {{ range $taxo_list }} <!-- Below, assuming pretty URLs -->
+ <category scheme="{{ printf "%s%s" $taxonomy_page.Permalink (. | urlize) }}" term="{{ (. | urlize) }}" label="{{ . }}" />
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ </entry>
+ {{ end }}
+</feed>
+
]]>@!w+zK%774yum9D zy}Wei8+Yn|eCNJ!NC_#DAblp>zm)-?=04ceMz(7g{&LQPMUe>j+kZYgTCz&Y4-fm{ zDg=n!@$MZH#UWv+?~y(w%&_`c>)3*fP;59<>k|zCmYR0YkjuVQynk2C6Xl>F&`0_< z7xz4Wexz^125veII^u{UAfL~})mL8)@4fdP%$ql_vTXT^fk_i5jNvu7Mj7)Zdt?fD zWSHCgT+0rE&k#E~@6hbC&px|j&pr2)F1zfqwnGj%1ik+H>;4ai9&!lW{f9r~j-NNL zZ%)@xQ&S)qw%PW@FJHBJH#(g2dO^reS0&lu+{0K9#29{^?%({cd~S@sPat*69k&`s zjz7{p|KSTFZ>)JEng}G^U(UOFV9(a+<`qv|5na*!X*3pypul%d>@I$;f6h zJeSY&VzI~#!| h<#O4-r%)*HY&Od?nauD3>2%uD5M 8HS0!|Ni?!S63ITShg%OZRSj#u9Q_$b)Ek!B~Xi@pd6#5z!BgVVx_>jL)H&|%uaCt8al|~YW*0f0 z&p7#>UGyX%szsFVEx4q9N#`fRy=UIj(;R6s{_xJdQ?mK2(^hI7^0d+3impir1FMnQ z4`?97bCh9&G2zRlQFh(@oI7JI4`9b4VL0N*BjKZuKJrb#!3Q4 f zxpVDwHX8y${#WrXpN%XAVS}-96<+xjRM1339>FBDYWeE=Z{2y8x^8G) zi+|A7793Un{^M6Pb*4MRb)h;B(E1vE7i00~wrCa*&YOAv%N-+ro`dNzMqMz0{<`?# z);}-&i~g(QuI-r=n7G=lIJ|*~q+OQT4#kgtIxA0k6r=x5EZ(ot0DxF5<`1g9y&dMx zo$C|dbzMlOQXomPAqWIYrm3g$#fs;vQtDxV|Ky-M9L#^O>$+(C X`|MNeCkKHDyhNSC|1<-E0J4 qWZR8R@}OGjswaOmey}hY zj_Oe#OryQOW&fWK%zDNGOpw9bElQh^-Ww?w&B9MUBR=w8bicV&4 z^=tO~&ig2#N?b!YS=^TS^SyuqV*@Vm F~8bKKEvZ%*T4snK3r4&qTm{k1bH?J;EYnckd#JUN_Eyv%~aod}B)UO*{ z7l>+6pL<4v29GHcdK^CPS`vNhjW;xJ?!TsMeH>u?h|Yqiqza5-hXdPH&c8-vG-6_^ zl=dW@b2OZk=iVF=(O7Gii)GFu@mQ=j&(-mH=WDOM7TtH>A6X!hD8BssOKI12RZmi% z)|4_KM20VE1{mXnF@`yC!WkF1AIbm$IC!5it7-w#+c(s(Y{>^h-#+VG>fLwWolPVX zVW<@$dHvaa>QsK`E3XF2j-An58l$yPlgA9a8i?2yI ^5@;pt$aTE vryA%^eIFoJ( +oeX-$Tn?&0GLCWQ*=#FXcUBFe20m(IX4Q7XEDO9 z++%vikl^{^2>F##F2+VC7|c{;Icy6eFR7|WaBu`M!Vn6%7$+zpoFOD&OoU=C(>QAE zxbiQ5{&Rp4l|76xo=hf@EX%a9rHzV`9Psb}s#OGwLXDgw>tjd80Cvg{Xj|FQaOPt; zM7;GgfT*Z)ES^lyn>SBkjD>26GUUBK5{(4EbIv*GyB~VINo#H;wpoc0&KY406VEH+ zEUlVWF - 5EjrRRe_;W zDacJ``3nzfBlFZft*QIT4U@|kA9V3heW=0x{i`=cUSIP@L=hDs5lr~SumWX%-9MD= zihje(H;y}G;voZ99(?(xx7NQMe|Gsl8&pXJiAbCwhQQ{?NZU#na3O|?hxjG5tyYh_ zoJ)q|s&cL4WktzUWJM`xS~?R9aZ42IO-a&i4?S^E5bzN4Y&z$FeN}~2u?nX>Km`@o z6%c2tC`sGYS#5>Sj2ScdjW^z)U-^Tp_{`Y{_0~1C2;Q94qJDr0u15Wk{~zDcL)BO? z0pu5l65s3fYdF36Vw7{vYK>D$Ije<;sY?;1)UUAk_=9(y_A#SEGiT1!NAi$2B(Jr% zk9Udd4yc?p17k|9RuCv5o|9Zxk>t9eaM&A+Lm1O!+ZJWV$%HHATs)tzR~*L(7!_Th zRP#*-#vW5}mqQtVd!AJ_K!%arEQr2c
l&VW?m0eb(nLgEKVI*-U!Kba z)AyJTO%3&A*4)|dL5F{XSf*7C_YyoD{sj*h)#&dD7vGu;s4@T=UNBM%miQ*7Mtt9s zaH$K_brE%3> abG^(m C%B&)$ll!8 zBIFqlN}Pefxw}EvGcU#J%}z~=B7{^=ZDG!F&ECVXS_8&xxw?1(Q$Rv7pX=%A+$5KZ z`I1#Bcw*#X3)vedB7%%D_Tx#Ox4PL=ap-6kUvo8&B3%jz8^8wErQ8Cmq`oF+w|l zKEAbRV%W}mlYkL2F-A7Q$RXJ42J-AK=Cz_uUSkrx%@+7~3!;Ea#bV~#tG+L+U9~dE zIY_SSbw_j&LJVU}OU05(sf%pKaXc3u2{KbvEv{+-6ph!VqOmwpB{?QyOezR*im`yb zx$>~hh`I8W24kgIsc1!VdAfTkpWTPRL=$1+v1$|x=)rud_YdizaB$CHK|M@wYzwrN zkKW_xPM0(E?6PMYHw|qFhtx2LMD)$3rx9(}hN4-b`?b%?UNi5POxO_+dB9J=0>rc! zyt{Ex +xjeqi{6D5UP`^HXmx> zhH^}hVq2MDrD8~qldN-{Vkte;_p)O|r_4GKCu8BBJ*G}H uH`;NKftNL~<{JQLuJ-Vp?l9H$T|6?vkN&qfP{ zm`rIP=(?gnnX!r4>=mOhuHQ1XS)$u>>+gO$x~*+gfH5Z3q}^wK*Ree`?{dxzx-C>W zjF&wE3&6STF)TtsbvW10`DcONXTIUS`rrPV5t6>$rxD-#tCOi;Aq9aza5$kf{1}CE z&-($WtE+ UEo5)>dbqqb5 zsy{CnE*>y?0^C#Iqya(!nK3X40o8GeP3csjA(N4Vm9m5p3Mm+~BIjl&bBga2vO Kx>8-ck6k3`Zxvr{0CZBhPQfbwDR1&Z; zO%IyWl>km6tOMgxP7-seNW|!gC)K>JhcM>?W*jO4u?|o5_Z^!W8ubx%1}^LFh_VKe zC%>JfGFJkCJw?Sm9SNSg&l`C DDyMYNQBZB8XscmRo3*jOCS* zIN;g|B1`n-Z+ e@slzZP-tBV*%)z2K)yo3s+)Us{cHVnfl zm&>eJEb@FlkMjAvkj-YrY%V9KGg-Y@ETM&q78bfXHiu{KF}3vA19#fr`1-*#7z%3L zT|I?&KlqTZTD!jV&_A9Hy|JY$@J2ZgyKABR3|WVEO6t|YL1<2;3cc~c`zXu L)d)xokfM8{PKFV z=-*4UNGwjrx3_0nT3cOI4gC(U;Mq#FT_xLg%=eEZ^wxolxSvvrEXxARvaolyEgSov zZHw5pJ$wkR>k`*-M8=rsKGIr*fpg}a*fwUYRwOwgRkj-9QEOCVoxJ;m_V9^EAGu}L zp+}4XRZ)1c ^RMoq=@(0AZNK_H?JT1@uoHH#d@jOc3!KEI7$?ZB5+{N*|Wc$zw*$_txc&8 za{cqy3-53GAgqe2sLQ%P3uUY90Yd)O7+`sE^U$+{O<$XHsHtgzu)#S@EEfk)>hIAz zZFi{0wAxgRQKw_|Jdz*OOva-7?=y4oH`o5$opr!|2{28`H7$PvYPQjjy$GQo0Ogf; zUV5>kYr|SGmCcI-=?qTi3R13Emhz>NYM7R?P3=Mz)nLSTru2v7^Zc{Susy(oY6w@r z0u@C Koo;u*%k{XEtf;M>eeIQ{q#%FYNcx^paVCp@*n^FaQ3QeZ>%d8 zOTt_|+<%xJNtytn>^RUHPjoLIGd4*WlWihEW4X}r`l2_Q%XNa$cy@zRFbeV6<7Vfs zIr{2iK+@6gU%6TO*RtnB1QR8s1pSeh>@t|P3GH?5xl?AIWPd3WFx)&(f)KGth!FAk zUSh1WS3c)3K)S(Lo&Z7?=h=T&vWiFit1iBv_r@E386XHkb|&i+-5b2;tB*9_PNB#V zCffe{>?OmDN#j7(02H|v2_H@~QUKejmPAm7fYRZz9?yyTD#O^0S8D&q)wlp3$u--b z8QXSv@ALyZwbg*40N{ R5s|+HS32~tl#984U^cei^Bok z9o^KxW>23|*Ib_zlxRo?&TNNL8>4NDXGS(ch_4l_-ac~V@yFC(e&OQ2o9?;4^&_Js zPnNYJrZkuhhOjM)2-iZ~0tZgH3(etXxbKWVq?Yt7ZJKxIG0FboU@)%7kSeNvu`fc% zp4m2|aM{-{;oq3{4O%pd^4)LUV?OoqQ(q1Q4o`yT0%=-UAcBDXQfmOALl4lY`P=|w zEQtX*U9RPs7@ykNe4cXZQXi7;qESk@q-h8ktK4zl!}ON>|61%E7^)jl(6LoxOQI+Q zb#-Wu3FC}edrV2z#iQo# Y)ZXumnuMZ zvAa=CD{yX9q{}Wim)vsygM7d?X`WI$$~nyhgUA*HO>!JR$;N96%3ub)@*h{#uk2gZ z5YwVKuE!u} qc zM8ydCb?PEx(5-5bK8%Z;F{KRP_BfVPoqZ0D 8$ zK )`#RH~JSd*HUJ9S|aHC#hJ|vSHPxEp0!&^H1)t zAAF1~xc83SypxYhIzuVIWS51*2r&+_EX5@vK*2EsMe5+Nq|l*I7)UM91~m;u5P__j z6MKujx<~xHk>|P;uuk9mwDJY}pKmpU8rb4Zi {`_XkuL)2OwRFu!%p`h;+%_`ie7*9jqG>6 zdqv(dD)sYb%^cXPy{#11G~!T7m#p8ceY}2CWJB)&XTSs7W(a}g+g#8b*M;?+-SC}j zZ#G_D_~GDnm!2n2Y-<%oRSi?a`20Y^E-8RK_s@Lk?Zs%-#*VgU-dto?EK9)viU-(y zn_VkRsY)TY>muQDG8~lWOr1Ee``9+nWH~Hg6e1W`y3^V8<8LjFoq55f*5jI%JL2$z zqfWjsGG;LH-9FBdC5hC>WBDyTy%Co&Fl4p2q^k9J$NB;8=ZkR8%nMC=W99UHPAh$T z)>)P;$il<#Kg|ER?77g|{ +iPw {+KA6LX3sPVlbxl-jX*eG^p~mVLUWIFQOj*LL*|RxGR0kCOZQWw{>Zc~Dcy==x;- z(7mRbZS~3M%<*F@t#vUC0UkZ#b>4>{=R9!cA#?3_m#w7l{Qb%PspCeYrk17%vz^bB zpKmRZ;lN$yhw5#abGtVh6(}Z%P #zeM!!lxpcL|YAduJel2*(k4HK)x{17$t z4W=+~_8A$(7$N4kFnRpAhTmNC6YK%2?CMoRLn~LLtU@+{MQJOGV}xS+?>-q0++&J! z?%@aWe7TZd)75J&+q5N7s2HB4T9h&BQtD8~nC-Y^ZFir$Y~4nV5RQG-QY=5 (5uYBSM%BQ_6>s;oFVKodfJ?azQ{hZ3+FUtTmmV>AV1a}OaGmPb)SS&xi zRJ7|w$r&_#fC$BMnQ(`CIN-A&kaN&=O>iw6s9{2&WmJ8nvAJY*cl6}9Yz}e8wyOM^ zx)r0r0K`MV#)+*>aPk4Oybd^?8RI^jsAggSpe)2=Wvr?#v{3v?(hoJMavewb`mC9a zU*C6z$urpqW;85R%YGDpLMa2=b^U&MZ)F^~t|~lG73ZyMcB^qB#zaNQpkRpbK&TLE zYDr3}8so0JQ>4oMy8uy_W?jyb^#HaJOXP~O+(z6g_}e<~j8OqZy2a?MT=?jBTV*O^ z&%S$?P)0fZoZUl)o8SR-KPa(_65_2BQ#Cg#18VOOf9IHUG!zPHR94F23QE-cCsZ~} zGn30_W8q+fhxXA9`p$J-?zumL=UN2=PPStU`gi9*B-PR1pV_#sgsO{-e0$@$iM36h z5UR!85w3-UkRLF>hGBBRm>+)R*XvrF8WhhD_b};59@x4i9PCj2GhEj00aH~$;c!SR z4W;0v R8|~0mMEQc z%84zb_t?D+(rJ&uY9WJaZ+JCl?JrpqFhNLFVEO8`1?D&jNm3zG*O2t%AH$0SY7s2P zr~$x5DXEkbfkeWsxK%oC^IpHO3F%VWf2PMd0ui9LPGI|uhiWTpk>>?yIK>%n@xH^J z1Q3h}rM#l3%0M7!wdT_~k0hLNHIvD(R3>Z3>tg<1ny|I3s}?_rgppGyT1!7#(skzp z4<*-a+?+l9@WVh5MP@~t(_SAarPy&@p;RmhL+KRs_YcC*&=ARFvw>o%6b8;Qt>(Io z@1J>MWnBAc{jHCeZ79C;zJApH`$k8NZcRAFGKikT !3*lhVatvdL}|osGSH#+p?dl{FhSCA{nF;<0=p7Bk}Us1b`q-1=l4 zD6-<0!7;~?hte7R)_d;@UA_GgNs`)b`|~4(OD;IK_p %awr%O0-B~NiV{QshFz(2*ttx}E#wleX@)!xz_jgrFrfB~ z8`Xp+Pa4+-s-p8^agPl>{h23Td249qZWAg8?mw%kkV-?PVixnIa<)*eaKo}78Vo`> zs0)fDDuN(Hyg7a7p~w6CDvoo~3CBmLPnpyv27{hI$;$nM{hPaci)F)fY}*zAkc=@- zFwSj`P;TRf4TbgV)+?!Oj&JJhlvb}>pDdLPTwj;0EPDCrfn}@KN-w_js(kxB_boqm z-jOX6# RNBj!$jr>h^$VuNuLpc*Yi6NXr zvRs7TP}sNKl)4Uw|H9w(139UV((G}w;fSe6bXA;6e&PCsV?OTq*jdxRHeplS&yZ=7 z1_B5i5sYoeD7voWA`uH7ylc*6prGk~Id?|abw}6D=7Nyl$Y_GOZsP-wKaKzK((6Q# z<&vf H1+;ROD6hj0Q^m# ziNDha0;CFrgav^>NY`-CDzCfap#@^aw!@-;HA#}CuT7bl`p2C&sIFz>@uQmKSDbfx z S4NRx}i3xnda=OBK;$h-{%Cy|Z*>={x6s8(n_f z(d_^nvx)(3CWAef^p8(G%eVCOxA^rVL?FBh?!@XAK1lxKz4uC%QEAw5f(xZm_?A2G zHGh5kt#yYSbvWC+Y-P)JciiV1p2=|BV5w43E2b%%rX{(Qs$*MQa wysyxP>j#DF{`ITT9(RpPwwLy3-J^Nd z*nPV${raVL-pCa`>0V}R7}(gbesFytZ{}rz5SlAw0*-9So^7&35rReqBBdf^13_+y zqLhoqU~;;@ywSDdgkap%P(Re!)e|k0%OOD!Ld!7*$8n*pwMl;H&Re(=3`ll9&+A9G zHFU3ApIP|9zr(R;wA9klf~akqh4fGe5N56?X`Wf)T#NzDhg3OC0dm R6Q9(;Lzl@O<%Ugc>zodl*j(n zdHgX)jfP4Ed$!#7lNA|tZ0gK(^$s+SZX4z50UZ}oDehX9L yxTCX^r|uRTGoig<()e)$Prv*I(KSu5 zZ5u>EC{GyEX6!j-V)^W|PSIa@<~h-0$T8!mltociY}*119LA*6&=HSUi|q;KrD87T zS2@|RYnZ=7tL^@VEY||>5T5 0E8^RpizXTbQ)6Oi02Zbo=7w_P77r vAjCt+!UM+fX-@%93a}?61(3EenVsAgf$)UwG}!(w=+m LkX?6^ujy)@|4 Yadg5`5h%bs;NG^-;C+b pES4l0XWXN>DqPy02-`bEBtvxCz$w{ zjxq{aBkK>hHP-6PY2U9TbY;6E(_5yePv85DwTv@iY3I`DU|}G)c=P+wg&W>$%vW;W zGq|d#h!~{e3_0OSW3eyDLm>|t u;EGPc>z=QSA zzWBU6ZSwfQH!B9^ief(g_><*V-&)8tRq+E`SRhc$ eM< hJ0x zj)(7nYa&4!Z~|tf;)sDzsb%sM56Z O%F>p;fj~n`BXHaCR|OZc`ND*$dt~;Rz29g6 zQ2RG+EEe;*q$tTi9ZFPHg+)u3h3 Jde#ybq|RWsY}Env1BaTd)GBTifiGZ3 v5+@& >Mb%LZiu8a$NW z*+1B~=i~`Nu&lsmtA=X~0DcWKYfZ+3E7ony?mct50=}E&f))(v`?OCR%Q=_5OI_P4 zi-ITy0=jKlRv81*yVBX%s#UAV@8_S@IW-#6%3U4liH&uVBuT!z>bZS_C?
=h2d?dbS@mX4BA{h;z>j^aVr8tU$QZQqGk+35x#y_Pb+@veZqt{5}GJ zHBl5xfq?D+5oGRK)yh9Cgn9KZefUI@6h#wcr4W%7g9w6JDwVs_*-V&X2w4NY{DZ}B z*Xw$q{=(TaHjbLIn??vpcwRw$V`GvRyRvQ}3mB>PzgMhD{^;D(RIr_$#@=fj;D;xh z;}QMI50=6F(~hU8x;7ASb-jPpR3L g?&SB%@(M1OnV~J>ole+ lxM<+$m`4GP1tVbAUhL;xgiT|7Z3 zqv4gkpTZL7V^t2xUn{K1x!w<@Vir|OMN!v-U~`6YfgmLFIS3dgs>rgZrnG!Cnwyaw za< bDYA6Z &5!l}o1&1l@KUY3-eEvYThhu?o>+`l(}GzbV9S|TmwIqmyxyyW1ElXbyl zK5yi$W!=l;!ggw~8kHJZXvtMcMVhF2j8QD{f(-z1$FgI#X=DO{P{4E0GQl8>8XACD zu>d8do)gAUXCxBXGZZO4Kae*;5Jo@t_h(Wk9(`mzKkf9~-~ax207SI`90QnPcy1n} zF3n&O>$MF#e2JMnYp&G4X}z P?-Mrn*Ec3Fu&F^ZwTGJ7)j{u4O~}=w{v+2?#GP{D4iEvKwQS ziocUoO^5pxq>`%Wljk0&uYK#)KBrJf#AH$Dp+FPE#NnLd*Ox3C`d80T?r+aN*Cq*q zQ2nD6++X%5iX;jJv(()`h-OTg$WbteZut4N>1%&~TjZ8oZb^kg;o#pEEFjarw!7y8 z6VJ`|+`fiF9`dRR6-ffbm>&tvXVVdn_zlTqrhjlKHaoGeTQ lvG^WU1xYKq7OZ*X`!d8j#xtJiU^l8a; z(vH6-PSx~256V#BATSCI*{r{^r7spo-nlGFyv;ByGnXbSE2YY1-@h_CZv2GeZMWST zUbbXOnouWW6w6j!eVr&Ms)%JRj}VHut_#EOB%-J^jvH?bba(Oou1-f1rE-37u(05t z&pZF@=r7Ix#WmuYr=HqO{e_`W%{&(}gCdyW!cfY-=dovd&OY|Yh)RgEbq)6}#>uM% zAo8eWyz;<5Un}o3c>+m{9wlMdRv8v8E=V%>gP#lnu|&v}HDl?gt2QoLxG?wb(&hRm zo!$9|KUmTy>^2?Ub^inLsgoy%y|EDg&(&bS2(ePB(EW?+f6G$YJpJkA7wNA)_hS0o zOMVc%^2#gAzx?GdLziB93H0{$v-Zh*V9uE0V$7Qgc{Fio05&C(&=(2;;S2~7?5>XW zBDD?Sm?IC{xNPO>`lAj#s(i{RCu_?)KV5s+ltVl(j%`kNSgU(ix4yCFjfUr!zo0Jb zS{}|BxsW%vim2d#tm7}24Dz8m2!MehKoE%pP->{ZVf6Uj988&5QdIC%f234`P^AJz zP2&t9r85{JvqFi=t*cgrmwvJoPCEUJ`tGjI{#$-~WAl**&+UnY1DP9s{aa&vdwa32 zWmNP22OpY~B{4iwRqNqM@}M)$HdEc5l~Oj-ms!8TJn9!W#C|(>ZcAfb(yvP(!q#E9 zG(RN2^X9v$r{DfyaCB=^U0q{+*eRBFivCA7>}vrBri~vH{npoKxj(w;?%XklA8gLr zeX=}mVz2-hheo8I(WKp`)KA)NN^<4eqs*aHN)SXb77Yc>DU&8i!$mEEAW-VM*^Qev zDa%)`N zZQFK%VBBblA}^~d@`#@- !t??u1@SWrX_ zMl?}0u|#96i9a>DCdOEDHN_N5Y*?^&MG+JQq)1tM+hu#5onFtJ{{NhJW|p-^a_{{g zKJ%H~VfM_K^FHrWzt8tEV8H3H)$l^x9y7IFyLQAUPMm1F;;NrH)_wG0^tHEF+t+>k zUdgGmr$f`;{pkaT4@d4?y;|-W44YT}^e3@V4GrZi%Obnou8N|Fd=*u!ucE59X8ff5 z_&`vT$&_kcZYrH-0)asI{r5kJqfnPrUsmM0{fdjr%c?4zYBGtw)%p9X8n7IytV~_F zaE^acU1jKzcRzs-KK?Y~_xoLq6Q=|lM~{+9{KbqY3LcUmI7{&AYpVItBkFWONRefk z?ds|dg~Fj^XID3QthLqK)!l9G*|(n@Q(MJddEvR#-072S;Pp~BUHywlRx_Bt{^c(i zU;FX;&qCI7d1Vc4=sKYz1U6ZQ;$#w%EQ?J5Y3K-%B* $m z=k-vi`4}6IhNvQc876Xy2_lq)L*R?Ypu=B`K?^B{Ws+`>KDNJ2TDN{(@s&TlI+Mvt z;@$WCF>}G<)5&N5^-Ofdm6w-Hn=+Z5Hg}F^u&-ac=+djAr=M{~iM7afyCYd$Q=O`* zuGR&f$D~Y(qM&Rxi=P!$B{WqvdwO~trlv5zzxHzO$IH*Hv3cAq$TG-@1h)I%1lRhm zLrzQN$f~m9zud4w3xp!XhP{W;f&DwPYu fR1lCE@1opCX&*-@7@=GcU<>by?d%_Ptv66Xa qZgwi2#ATdyuR?>D^Q<5<$rUViL#6V&|h-Kn-9drU2fP`u>me?A%a|}yWm6r*z zXk?(cq=E-cr{WF=sDcQ#Oa_~QsLf`n5GvqwQ{|#t`lO{L)YIL`7MGUslcr2{Uw6|l zL%;g%AL#R!oE34noNh=aO@CRb`pGLRlP|sSd{kgr!p39RBE?V?&9gMYa~wtHpua|* z;}@$ck|NVymy5TRmsUY0gFu!sRV>SM|2Z@?*EU2J7khm_T)NQsp`tQ+Izwk=H72WS zCabE)iKvX+vx!g&H?iVMH3pO#EJe^|^>tRP%Ydqygx8b3{qB3UGtW9py?n(Ah{d7< z9o_xPmaSiStLsKHh#;_#SQ@l6k-(05SQLj)K-Q5-CoOH^EH3hSnp@jK_;Lx;X#i$M zIU#;Z>JMxFFhyWQ#L+BC5|kk@g2vN)%E39UpHqsa#Dl32AIgSYv{$w4AjNSkR92M1 z*4;;xk)y|9m~w{Wfp |yHmzw&bsa_ zt(%}+bLT^<&2Cq-S(!FXlO#;N*dg$Oc|05hnj|%n#I4*aO%ggqk~&S1I!%%$Lz6lJ zL} kPR7uLJ=}d+z zsjM@2Ua+f*f=q${do~N+Od5AtTLLiDph^&GpufjQ5-2-$;<(tI_dZk!1jI-$t(uz2 z!M7y4zEuDwR!vmG=?I^Y;!IYRA+4s$P17tjtk}#n3{BSzMN@!8B%)cy5&*TWt&1E# zb`&UEb`)!xCQ$&k6iI+aQy?%j2$>AfL+DkL$g-T>#_ cEAk)C$ znM^8?h(&^Ik;l!~RF=n&9&N2aE`ni77-T5M33HKA3&t!6V`Z9i0%a3SN!N8**0Neg zk&LvGfn+vGrsNcz&ZbOhOs4GMk>;uer=Dben3hQ;!VZTEPYH|D3@V{1oUI}Z12mG% z{jBRIP!!^M0aB?X6nLXT;Sji-PJHu{X$cB&a_j%?Jew<6A_TIkf+VY7>d-APfESp* z(XhopU+MJChls$^6li&M4PQa;ebr4EYBwLsk9_rg!&=cTl}(Xg7uX?Tw60gx^r7pp zzN5% UJ4v;Ad_YL4ID@meQco5h1=bg3)cL_NJ z9(bC9SKoXaikvoBx@aaw=TIbpcmD@F4uY&|pr{6nuPKEI^%bCGl|0DtII&~lXX{zh z^AAkym_xwk@OwJ23=Q3bQ7Ezt;B$zOPdn$iiJ-tygkffd-Am+KzbPX3Ob&lW1dwFz zeGEgxKqv;A54GdJv4m}ULp7B9oS #x=jQFj_(eFBq0_JLwKMI?)lSSfVbJe$q+De{AkeC3_SAJKf&tzZvfiu z1W;8Bks6GpVEvw>_=d9-1vg%}AXm4~lS<)-@#W`Sdv^|2X$g__*{Y@iXS2boch 9E3+4XAAW03y2hB?N&rd*$kp78d4Mq z%{F^}=YI)^32sjj4D_@^Yx7YUGhreGk|_uVgRtVqKZ5`I&2Qkt%{yW7!g(MkQo!2; zIDKjZe6sr}@C*ezo4a6Z(@~f)z7DjknnOShM{*9c>xErQlex|#!DWXdN7~@#KRyg& zYAWF5*^_}#H4s?l%YA3ZCBM+@ahz&ySWEeE>X;lc6an#24Bpzj4|tZrs{hQXW5DgU zgPcg?3#1tajvj1=U58uXSHHUtQ{1+8_kju&bhaIXj3h&)!vzJzwPiLHYZKV)5D`UO zl}gGoOdCIg`~1^2=~>gK(p^1$L?9d@JPUkTjn+%$qzoRLM;_^~*F;vNXp&}%?f!U; zuQpchE|dIrzhPtTG=&pdh~jB3!?V14Vy$6$=%js|os#xTNnZ3Mz5Y@b0b()h69Tl; z?E^vAfHI9d#2ta58z9>3peQnQb+p3NS+gM!i$W?RL0PdMCQg|GZ?63e&OK)#=;;g~ zO~)# >g+rQx j?CP<6vVM&?yoYO=-ZI0-FQV0KzW9>mPjv zg3ST5XU%|QG7Zs`1m2PoNXFwpSZ0 D8t{EKDFgjl2t%;PmOH~BOkx`_gl$^55YPO;~(v$59cgd=* zs##6e({fsmWny$R6Xi}!04ybtH4sz9h*OrH91`QM5`rTLP1Ojz8%%&CMMILJFo5y6 z)abwoB9J5vy OpS@6Q+kAfUe0ZoxuZ980cIBDz%c<$pbfM;mf zcDMuj17Rrjxj<93ZyM+@v_J*3gGdUlzUx6a*wP9Obv3Yf{tPfO5>5`Gp=$#e#$J0u zhqb`Es=?Z(qxg48)u5r$4--dLf{|~Lq)8H#L>k`sXg$oDJqyaq%b~fg12D|RFkzst z7X-nUWA+&K9W3=96Zo7kn RQxRc3PYSXrzS;t(b40giZAMP__shH!l z<_)zeIfa?PG+i?_LqmpXkYIuh5ZDShK js=YWi(!V ziXb5nO~S_gt+)^W-{tkN<4M!-yN92GH`jfJzw}SeUkrAy8#F~LVDj;ME$^M8PE--* zyn2$MpsjlVcC~ba$T6U*IxLtt3V4yn8mMp`!NDNxZE1tE&Nvg_r=CC<*Dq8>hR*g5 zusJ=LG$Y?{a6E#^lTZ-^VUi?a8m38vVhM;v(=zR+lfq1!C`&RO&nB3@SRWTog(+22 zNh@roNQz= zMh=G~Hc=EX>4B8V WY#~pApc~-z6+>rh3&@g`o6bcbNTpJ6(wx~KIvlX$z!8`} zV{#7gpr|mi(hrmCD`DH=cB~mb+I19uFn`i_qM&gWjiKRadpA7z>?`;+j%DD2(@p}= zG)y9iC!kX#_PoQfI7k^8N=tn3{n^FyQbv-nwy7CXk_ U%QzwqZjvvZj3y>6S z+kXWA$tfq#hiEhklBNSi(s hjpl+E>ih=fH2Z84VD5 LHRg!6Y?Z zL#lONL$x%;2{eZSZVWM`Udg!WXE$l5pR>sK+B+X{HPto2v(H>W9X-~bUA=aFIv$C5 z&zN>fpg%Au3=9l9qp>)vX&Py135UxAUF}C89*;p$aS6!TECj<57*SUPBS(#b4Nd#t zx+|7Lu078LMr82~-Fm1EIEI45T?4S^SPx8VtOHe&;hPG4L_v6}%3#AYe}!8gdK5n2 zwFhqf(_`?`Lw8xm6l3Z_ffytsF;G$|APgO3S%$ir3NQ@)--goIgi!<}<0<%TUkh+F z1!-A Wy2$>G4ASh*}86lh{<)Sr)R{EG{q-L~hs1vTSgI zQ?-ONBT0xrh^Ze@J1}wl*mSYqZ@%{G%S5Cv!Y;pfc`(!)q91zjp;G_3rJ|A6ER7=3 z1f{SvrwO!R*jN$loSm>7KN~ADA|vuNNn4?PiYBQnNArqE3*XEW_-ztzxsIAsEpofv z>9fvS?E84*4rG{mc &prN6Y&)%m#hX!4VgC3L@Z=f?_7}2>2J4!R z!n859-g{nfu~=AyZ-(J-2dmt;p!!eAf8I&LH(Aw5-0-FI^>E* zhVi}fD7ZS#(D23XW@zt^fQ@Hx?!huy$g=JE&L=C()Fc^p?LP>!X3qhK!wEYM9RrqO zvEvsFhXAmqG)+m$qFFPhWEY$|FNrDM6a^lB^eNjVKU$tyeA?U!OZ$GY<+JAB{r1<@ zQzuXI-Tmv|4}QAtGuCVBoT@7ThRS8Y1Vf7D!(jtE&xWZNribKHSICI~6MdQlL7Ly+ z2QYI4)`%hG)vH%4rfCik2x$ZTgN9)kN<5w6D4KD2eBPk0>o$@k8J1;fS(Y+*&lm<` zIUXpAhPIX?*zOV(1xZQ9J8I_i=@5*k;8;gDb`Wv?K{LSTal*WDb&!%25I6=l9cYDM zI5uQjzDo#{bOz5b_^+F;giS9#3R{kK!?pv>;NltV{Kz_h%8O9b*Z?&(<=;DtiiJ ;p{CdaGe=irG%?C2gGi2p_O?#g-`W9lXU)V(sdU~4!2>3;3Z7d?AcQnk zQx~3gvU=V*XBID6yr}s6C1)0o9@EfgbGf{RVX~ocROkx?oJX6REww@HKD3)8JVXL_ zAflYlMIgeY3EE^S*5n!9w6iwT$vI7r=rMgZ9~9Y&P?60?`fOg>Be)qS=U~6T03g5) zl)K84+;eab-P6;JL{ZGf6G<9_Eli+nHXGeIZmi{{>*-V~dG8
-b4CYRo2yZ?7JD}Vyh-MT}C rfmOcBHGu z@CX&9IIY(>80AAZ@=AKqLV&B*Cjpg2s+(fJm)8!O5vMe5F~W>^})!IBVp9Yk?@%% z0J@Q9-9%u>3QQSU38Tu3Kv6X;1Rw7{f_(t$Tex< {D@f~2-?{l137-#^HHqr&i5?o1BRV_7j()hG#nnX&HBi5IXr!pQB zKnO&_#3I%y%n}?W-gq==kR%2xxI8`>?C*zoG>SR)E!!DMN%-{Br%%TMpr=x~G%fDf zF@feeSTLypvZ{(Rc>6p0;ZR3E5H$6@!5$c&uj!y=WhnAEfF=pN>jAz-VDg=ZWqLBn z41Bt$1=ljNvH}xo%b=mE7(1WCM;=+fY~HmOPCDr%5JeFNqluj9%5e}tC>X?`8_hJq zrYf1c%Ih6;G-|%{{`* >&c_;DP}F5Po^X)#Ba1{-sEeBr_O|N39*J>KcatVxt>ICwI#6WJe;_!$}f>Sx*cD z38D?6kuU@U{aE$U6b<1-8t259oqrzm2E%Zmy&Dis=cy8~6u|uP^-$!rV>@a&gCFiV zoXg99#|`-E44s1QwRXG-aKgzS%j6)2f^A1Sp=}@x3{BzJ&X_g^t9!jbDQKENa}2b$ zc0zMU7o2_884w7^aCQ&nh0${IECC=1ggI2~iR1v%^^i%DXy(irFc3|silQ2Tz@aD# zU*z*KOV2$we&Mnoh#bQqqJ#($459!3D*(CC)&x;bh;P6Cw$1DHI5utCq;++5bxX2R zgmUN!Bb~`c&RVd5uuq*LzU^XRO@Bw4P}Q`FL4}mbvH~<8Ih;?&nxL2f-TeWWI%P5x zm6XBCx84U3c&wf=SW{7 N zh6!tT9l_Z-Mbn_l?}AxlYQR)gC`6?=n&5d@zhws)Gz$w(I}Hmqk?(Z2zW3n30HhL0 z95t{c1^W^q@4W- 5?Vz?AvP~ z83^Gb%1}`TAvkkdBmSA8NazX0;q!xSfGG0owS2<>nqwiAk#qm|zd#y@1&XGjvv&}- zAMFH@Ww0=w)HnjXUMDs$1!2R9s;mMpez+bdf66PWpeGQ*C23tZa2Bw=wGHP%2+|73 zOl=?@ (dNM?9)FD6vv+Us(Z~L2<6OK$ z*LBiz=<--j+yq*9SZnotNHf1C09Hsb3?+>7$!R5pV%aFlXc=TGCOE1cf@7HrJ-g~z zb??4?^vf^5RM&j?U`P-}= +Tm$YyszUvCdc zQU++6!ST4HnQ*ML2QI$oLWm|)@YbiF;}WWt9{{Z@3QVrAzy%`f##=W0qg{t_WKvil zGhYOvL;(P+^F4?dKA_W2j%%P+Y??HKGz`D%&=8Iw_5qEitV-c&RN!s!SK zr9vo{iJ^>=L7IW@O(8_Yo3HR7a_ekT`7xqcT69xKSv7-HLjw{Kz)~FYiAC{hS8d8I zcpPaZmDCKqmoQ1Qw6RRkXQ ^Oct+%gQU_x?!jsg?P8q*_=tIiPqk3 zHl9f c!2GSCOy*qcoKz~25Z0>IqSw>PSk$(BrR}-~$^>PX*Wyh8+T7OrkA~+oTH?Ln$ zgu_u^NyP{!OL3&Xp(3PL#KNVn5~a7MDCCG6MfM>TO<>) zqBflO&qLyHgG>SJgNko0H5FaPH`&R#q)JzHw#-p3H2BB1@Dwjds^sX1wspp`QLfBh zQEF!$EgqZKNoHtuU{|d4XvYyM915ALqM)plu|9+~!Ni 2e{{ z(3MCi$f~Mph0eMt+8nx}0#XPwIHEI D@;THs22>R^a{)XZP zdy(B&P7zevCAbDnFp17kr|r lGrtQ&4c24o(o zW8Gxy{~4ObPMVaJ@!x-S-oV#{A)me_hLwupYn1Z{*CZfMU#sWgX8Cc2LxN#lBhNCJ z2}Mz4phz-DcT-?H%S|Ky(#Mk|g L#htDC3C-21m7uU?S1Z z+A}WEB?%0#`t5$n#XBLIiD|vDzG78Z#c(PtOKK)r?yjI4ibuKqc7Jq)e?)XplO;wh%g{Gb(R1b5gA83GT6|D48rgt6X6CBP1o?bot#6XNYbqK z)C^9ko@{tUkK`4-fux)q8%alu)?izFXU9(eU@|CZx~6NU4ldrAM7~>!qfXA5o>Dg5 zJ+*vhsLoqY_9c4E!-EHm9o<`tdgI+lGc}H GZ{W6ZI%@! zEW7H*jFu)&na*jd3Ji$`UVY`|3Q-gpufNRBay<4sGMN G2yQZy9W@qIK;3j`i`gZeUb#B9(zy zG6~^G1O`Jvh{qF{fC|^hNfK+FijrcOF}@MXeQvx{$vhIsis@TA`!O`l@>i^dKV`xQ zC@XT~-HFHCYdVIqeX?~I>^a;5SviaUub_R*d=ybtI}%hy0U8&C2+%cE3JvzBBjFI5 zHD^wD-N;dPk|g tIF$@xyA6lgwI_`I{1R|qK zK$8?yxGR(*dl5{ooYXm~a-tzHVsS&s$d06xq&By1t>}&Q(&)8KYtjrs4`$S~u)cMz zvbS%SyCd3Wg%=!4allXvb{z89!Ef}UFwr3cAot0MffxH#!4zzF-g9s4vP+iJ#Xh%j z)m2w1&pr2i>Bz>(>GG-(qP1^={oT;jdJwAXM!?91(J*H0c(B_Y7%jVR&o204^JW<6 z?}gDdRj~KXmB6?h0J4m00ARPl{eOJ{UfZ}E=LGytJG^n%PcZZftf;`cRwD;0(J@-s z+SxLsG8xE7S^R97hVEbl_8x78rbCCJXCMgCWEvLDo&hWWa2t>|0Sdz%fTrQTf4v2- zZ`y-RK#AK4Z~pNY03JI)CJT_x;Jiii-a{~J`85ztWuUQfEG)U;Lh$ #Q@0=bn2G@uJPSeP`3ri TpvG&BDK_Ru?>e(=;&=lqC?BQ?KbN*g3mW@2}64I!na4^(XbuA30A#*FAId z;GX_n)RDnM0)Y@qlMG;vz+96k`nEarUpovi37$ZDSdVo*)uGN9H$&UDV;hYC`BYU) z{pwf0j6J*R)$D{Rvua6-CcD~N;G&B!go`h`0wgJg!y!wB+U<5|JJt%9U3eaxJntm< z@T0YG-l=op#fN@})nzCcg6BTk2-}YKLw7h13~l)eD%^YdIdIyHMqGZ)H<^6N&XPQb zi%dCUMk d E3dp#W^Z%^9rK+mQ^I+Ttd5gVCzACa z2I$2?k&E)Oc$!$gNd?H(+Li~Bazd~%Hj1TK$WzCjIO}A7us!J@dHX1bdiecARCh C+;Uy(1cZTs#LilPw5v2gyf3!unfgdIWapcxvPj~sz3mM?=Fe{mgL zdDT_${PWL3)4{_qVPrid cOBnh=jvfg c0ki}svAt8c@{SA zZN{ZvmSI53WTByP3^+>4AdryY-Oskb8*A3Xvv0fy`;H!i^&2 {(dO=ncaC^KkXWo3+xAy=H4hE3rz)qPsKKsm*kGU9zwGjl;pL+7C!TayO-|kuD zi@FziiU^*t8(HI 3YaEolcPC2O@wE7K()7zF!RHN|L5s4T#rO1DUc}D zmWl+P>t`Rh_kq;^yYv5ft<5B>nzV57GS)B*LJ&k4HL@0x;RyD64Kp`MYwg )I7LQ2u#?af~Rg8=To>PIl9x)MZD08dE? zl$8}@rBIj&z=}NF_2fJ7!S+KSurvsw0I#ij8XPtr+S)r{)v8r+_uY2`&9ZRP?Abul z3_SGbzv8g3ySp16dhn01fA?Mxc{|jPY=9XjodgGuw&4F$RMq&^%|9Pl{)2PN06^XF z>4xAL3(sI27dTD(1gB3+Y57*E|Ge7w?+)s_Z2O5E1p-1zlaLxwp<+yhF!qMV*5BOo z8((!*b#VNY@%FCHZr~Uunvt_!MOMtW-+LYPqc-53B2aD$5-l-sL`6^)4Q{ycM)=EL z9)@eKxeE3lI0(1gd^3Ff@j6)b>`GYq VYx4}{2Y&zjyYRp@%fM6W$F|ui z3Lp?Bj4m&MYtK9xd`>5Py6*_A-L(hiHr7L#zZmv)1)#FX2bFFc1ll`*7kKddTtLz^ z&~_0*U477WtOw%?6;*{PBkSPx*ItGfURVXYcJ6>gA_3E;&w&5_(_f)++*sJNdpF#E z+pnOg$OoG?ZGg6x7C-_Ct`Q#S=sgA-9{Cg|k2x7@JKO-PE6F{3_GCU=|5@y=-`>UY z3q{pF-sxA9D$2v(|IZd5pZt4`jr)$nG8x$OR5JKd8%rCdDOFRtNkl5g60MFMJGKjb zulJ}Zf
fbq)8MS+X-oR6iAiIdDD9|=z6COrcIj;o40Po$ratO z{5l;TdGryu_2!$gQ}^S`uY{2eV_|30ZU_d$!1Fw`w;cl`-UpX1JqvF9@eiQLVTYF8 z`@k)7IEk~jy$1#&F&JA@2`_Efic!gXI=bPS#izkVbEkvua6#4NaRBiI9B%1=mG6H9 z+YYsZF8Fb}8V~`!9nDZyUIL$g_6h#}H{J3pxarngao&NXC>*k$Hg68JwYK8__e?1Q z$2=#1&j3nh!GEX@P?W+mR}9(6?BBU*K+;p3rf8h=bhqT5=_%GFo%&B;>i@~{9|=H# zO+?WZNJ>d?i*94~h*`mzwKG#`Ii30Z*cRWx{)0t{bb=D?B9yQtU74n|6!S&BHoHeI zIW)qGV>vJoXtWBU5uG486Mzf?NR|Yn9iad1ZWvwB0580<3MPyn53-^{Mv`Fl+E3u- z>wgC4o&N)P;;~2I?g#z?@4mMNY&IL1h6Y}j2(ic@bhI3SlP8UXvnGv&@l~Zb %dXiez*naC`6|Z#*UwgA>7uE zyyTLLprWD-etE;S@cTd94~tGe0}_cic3zvCTj1s!u7@wSZU&*q4(?Mtz>Hx)1sTXR z1OAp;;1hPxY0V^c+Kf5+d%Mk}z_?gODC1ow1*{>Tc!%%* PZ+oWhxwP?SM(s zXTicV&W6u7ZG^a_Kvh*`9@mctH$YY+;KGY8g^?qw;LW$*f(Jj?07nlV1eZ;O+F~C} ztFMMPKHmZL IYMQ5G!$%C_6iw(0_mQU`=bVO`u5{SD=a=#o0L{X7>T0^v=$m<$+khLz8qq zMkUF3X`qx;n1HL#U!qewWyPA#z7ixvT##|cA!|#4YR|%m6%7#C5`rIGd;wf>#kFwV zO}By-Jy>42<@Gyx2F5my#@)F_p7&qXRCwdv5238Y4?nr=Vz~C&YeCASVgG>x*lFwP z?uIjdP>a2CmF3{n(?`L``g$1CFbWz*jl$nKFc^Y=KKeL(x^Xj5tPMvMR%BFJS&2Vi z*A3|G3E(KiTCW?gzYbPC{~Rzj4r-S*05yUHQ$oNU6M>IAflu0SL}8FRPzsGCXCf4= z@1RY?6pS<`1t|Rc|NZz@6F?FqPBmIFV?s(miL)fLsBux|qUk@BSc+BN+4Bzf=I*!c zfyAIgWCUyuL`KvWja}GZ;VO@AZQJJE)4i{ZBnV`M9>byxGaq7RUA@HtXHRJ V zw@iSEOdP@?jaERXRUmm1(tT+NcJ;vIX;VND1ZZz B$6rk{PV5YFR-Es zE37OoErH#;c7q}-P*YtAQzlJ->Z%G1%My4lPvAfdW$7$OI1-6LZ*L#8ws*k(gGX@5 zmLdtTIh^>4t#gkVGa5#Z9tHdM?T6OZcDUr?3t{KZCRqF7YH+$-;3_EsQXs*|nm}qa z5ISKR$N&=)gCHPv(oFfHp-^2HZ4kN(>4ixg=>OEy|E}Xp0+7!l;TZweq9cUVdVjsN zV$KTb {jPMDMPt=T)pIZ4(Aul{IJZzq2V5V z&9p*H%j{&cS fG`k)2t*?W z4D1koZIEfM0!n;S>FR%1016F*Sv3pRg0d7NFC4ufxqQY&`utJz_3iCD%ttpo&TMSi z=u~x;@dzG}R0*4elSj>uT{vyo;6Qu;p4<9-Nq3~%;ou!OEF79cWcf7|0z@OQ)>w gu#?D4DF%4gq@1=^}LH<3C=W3 zO!Y#Nq)w5PE-4c89fMbg7)<_CLBRrGEvB{jwZ7Wul8H;SE9d@1cM4AZlf&z&M?QOu zZ0c;Xkp#&(1t)g?qUi`su9}>__LOUa9L32`e*TnuOY7Dmkr5F~v!-t7`LbT_LkNxl zEvCh@9g-#;%B0jmrASFAX5I&(^T;G?t7USp5hTSh12%_S7X;D9u)GTqgia8Igb<;Q zX>3Fs$CHt0%-Z2BA_y@hWwF;zlY~JNkWf_B;8|9098noQaICA!FcGrZ#U#h`z_A=& zF-_BT!_du0I811YipJE}m<+=h?LGaDWLn|~0$Dp-B@n@2#Ixn?+iVG*V|9WgQ6iDx zWLeQ8;b6wlR7TS@QOYF4iC8qPWK;e;v844qh3{Ac>;EOTh)eU0f+{wOHrmfPw2THG z(UV|lR_%%Qc(D&Pq)3ndEl&v<)llfHmSt#yK4Bq1rMH%+Csa;QemeIjnRCW3#t@Zf zwmd68xAi&xQ2$|pX9UL1+CbG+h@_)X>#LS-IPDkFNtF}R&u)8Oe0lq;UI3G|bN1Zs zDqKJgK#B(!C-)}9$<1*>iO9afO|d)dnWCbi!sIVAH^~V|Gnb<_^7$`f$ZP bru4L kNy2uF66v68q1&)cw0qU8N>#h7f-cdW z-qXFOh5#aGn#|+uEhHkfBC&W-Gc>k8+3RIU=9>iN+XY}p`;O?enrV6{6*B*|@d@Mk zZ7-62v0gXN@PwVUgXPsn(h*A l2z7i2y eAZ%d#*7)M(W6IWBHcVeG{EtUH_B^=!p}rr zQ& 2rlU4HYbRNn#WyLIi2*~g*-K|$lKlCp z*U4?6Hn{t}dxfU1T`rg4A}qD^)lsDQ#xewGQ8hOBa=)gARcqH9OP4MU+;h)8X#Du` z9)NEip$@~a{F$sAWf@weNRlZahICywDT*?mf8mAjipwr@m6n#KA9>`FG{rFJs!K1` z7f&87T(V$3nowU$C$qA?`A|#f?{B{E@-|MEZu!mceNraFeKo_D18kV`t=C);bjgXz zc@xG0M^QLT+;glAK0n-|){Y(%{bb$8jIXE&-G0X%@jw6RPvrzbpzE%?F8=Vt4?APY zlpao`_STgZPqADGD$hQm8{ba9GyFR8{)W6|%a-^px7-3>eDQ^!077L)@`2(XmiW_B znwd~OeqjEnd7&4!y*Mf^#VrT>xT(!2 $5;U{P=Q0p za`yD3mI%B!Agf`eh@z>?-~aaa*w23UGnZux&3s6O5HiqLhU^v`F6+575R4q|=*>>7 zuO>^qZfhs=d0JMKOeRCMwY5Sx9JXw$cIA~<2FFd97~imIlPw +#x!?0cPf@;MBzohGH)bDu?6Fb=pxCB614U>1%i_^EIiYfVXxY>s z1fSUSRCQORTX6D@AtA87Q&y9}Z!1YI7_qp!HFDIxrE^22K#O03#C=TwDsHPb8H&O8 zHZ7+?)?{Vg$hpbioc(KkT-jLjv5o&SAO84nVltbwy95{Hdq0rn<>x6#fTK7aHzI;T zn7Szip6!n-gK`N$lBpM0tx8;U(M82pWQINLf5X~s D)plu{iy$?mtHEz&@Nqv#xV_ gwuZd-m+5Z8lNt3WN@M9X3Jax#Et2V0dp!hd8>rOc+&J z#-2z{-`W^??xMtxe)OZvi!Z)dLJ&l%XnC )Hhe8u?q|2yM0 z?VJgV$q)8@p#Sdu-_u71j=EgDi=apf@0LP%XZ?+nb80?&Q8cZj>3BM3Cs+c-Hbw^G z8)AMFO!W7E{5^2}y1b^X3=$`n5z_N^}o~GODIwt$+}U7P+12BA3HYlO(2FHw-ho zao-^|98cOYK$0Zlqbkc%qbo~`5J8adzWZ+Yyz|a;69hr|Tu%7=?Y}^M0|9vB)6H7N zsIiHebLMI*F1yrpyPTeUghAyW*3ZejjdpzHe&Ak)j*ia3zy0GM?5Cf6tcUx$T|YhV zEb6YCu7NurdKCWn%u9IE?&FU?9=Q7ItNpsJYX{o9gJnLC+1%a7_l2V#1fZQfX>7d6 z<#1`bu4ffBI2esn$&BPPF+n$I#U59$$8Pg5G$jtt$QHDmuIrKM)2AD|cklM`6?~xR z(h?^H6dRLbsLE57ec*!oT2_7WQqdbtZ #L35xbPRs6>~19j|E%x|9bl_W6goJPL5_dp5b#U3gsB} zQgRA7nuEy|lX_X2%Qg?Tcq6H>lb{J?qzq}`iC#&QG|wfMT++8{)hf59X*`vmnWGzq zyz6M&Ku<8@({ _m1R6yUI>#`n>QcX4TT;Xc9P8fO+S$|6-B-cTG_p*Ntg7j` z<3=aS{oYbZk`gm#&eRSZIN-5v+SJAdwDYZ%U@8i%-S9cQ@$n{g uZ`ufS{=A*V_EiI7`KUnP)NF>f`97SGq_5$+s zIWvH-sf177`3TOu_BK<~G&Fhg -+WP}#EKPr{+*!!Rsk*N743n%YFU|-oo88^qR!tHFY5BUkVIYUV2Tz_f zmS#G!tkVMf(QeNNWeI+p--t@7@Pl&B$_s zsAxv69gU?agkjjKiX0rnPaZRpJay(&XsD?MPemz!s+z#E=!>m8;GFAjhfpjI48us9 zHf>7Gm@%W+y6yw*UA>1pdde9(7qk>+1R16&tD4SI6q!RN! ~UxJ3?>Ju_x8P46-Wknfe~ diu2Z==J_!24(ZJ??hn);fb3^IjuOIoT*g48qmZ6fSVTxux zq_RFsQ8j&Hea+yg%F+@OK)deRYr~eq#LzURm;2`(H^8-5Tnut7jv-ti@FqCy06dQ% zl>&1x2!r7$#_Y2Ujnm3byA7bk4?q$oL}JLyNI4o9fy1z|6}SHZdV`piU0b ||TM<~{e^(|Olj zchxd>CSGz)IVXd}Hh9OwMdN>Ju59`T>PfU~Z(a9hM{BT^zWJ3~%SeKxhlh!lKq$I` zbVJjxo_<4jnX^*;*Y>}abjI53PSyqJf=8AGHZ*T^&?KEx_xZG{s;hX6Tz%0<>GIht z`kcHoyK4KY@|~T#Ts*^B5kp~Axrr!*^pqYRc&^XTVybWLx^-O(7c8hYF#kJt`t%KZ z4n;$;gpZ<#??uLiZ-QxlodMi5fn%8Pf~n&fhNkSArp4yZpRa7)y47!)8eP}X(|6wn zmoGmb13y!V1iZIlE3DnJ6Jn_pBvNTJk(NNkBouf907uhM MR*iRUK6p z`+8 (51!mNGv&`YsX}(Rkz9 z*E%?wLqC4<($bWi<}6j8?+qLh#H5@w&lq)1VA IuG*QTx{C%BSx(E zTDBpYiGttm&t5(6s?fBmX^A)Yyybpv*K4KLNo<_WGz h11zy61aB&IO)gGi{i%kwu!W87pRA-nX}Bk9$+g=j`j(zS=#yxPiXlu?yY3(LT|| zJBNh8`WBX%kAGdRT>t q$gGRAr+u#y>uB)0{hkhX(gCl
F^4-`cvB`T5J&kLZv0vsMUb#r*nLyhux1SRaxOr!4Q
}|IM(7M^kA^ zk`>7BloPo(UlSM0e9#m{9PR3J`kjt|&*dl{HELARnh!q={qUlTg6-|?meT|Qc@c*0 zm)w3YHn~|@!NnTT^@21he0f8J@l}ch#juRGJj}R?vxp!~#ESBR@BFJG)pyKZ5I(!j zmRb4iv#|> 1Syu6&Mtf;gE zfCL>}IX}-p93_}pkcie`EBBWl{;A{omu{>+|B+=?&s_dw?`OYUpIr9nh1sUA-CmFA z$tUY_L!w-q8(Kn#9l!nfm*mwmZWzSjM@mjvJ6c{kacT0V1vjN)QXKAh>kj{xwk<9P z=K#0hF3^m9>5E!ELNpzPnz9 E}Lru+M(CBNr^PpTV+ zy{Wl9d+L<2Ns=J!)22`NZP~JA@W(&?@xUjad{UAh6q(a%md^IqBz$w1$Azk~KGs)7 zGp=F+;}8s^6igilBc`T?`qn;~rAX4Q={l;Ys0hFK;*0vcdGoCE5&4Xrwzs7t6HjOU zN2>p;hiO{Y%aDgbbWomw)n&8UFm7IpX>n6A>_8yiFm(*=UUzt%k0MCo=@n0QUHbHj zs>`4GN%`NFKO9(n!+XZHtF9k-tLd#`kLW=Kuip{^k+GR^IVS%1#@|&?u}n-kss5zY zuh0B-w#r+jJ-GHk@rCU#xGYc6XY&nl%nd&XMBvv#sSqq^I4%04d6)D)+Vlv%;|#-m zb&DH@VW uEe9& zIZsKfXwg+^_vEugQgl|Ex`9kBYg1E^eBRm2F_Ca0NF93_J=9@2b>@Nv3kIKh>M2xR zUG2~1F(_=W4|o25yj=-+R8^L~_pSBn)sobfN&<>tvy4k?>&(C?GNQDg&5lHry%hvm zTBL1Y9LG^WY;A( 7f =bn4cJ?Eak)^)wkQ=}N3KU14EL+TohL`lj81CMp;*vWU`n**(QPbw)T zB`r?l>LC$jW)~TX(4O;qO{|6mo5rlK9kAr())UuHwuKTw|C*62u@Ymkd*Pu)HWO<~ z7ko%3z`#u8kR;ZP{wnlbhwkFWA2!0_Yety7F^^7}@gRdd-INH7P`v_Cmc;43rnw(3 zc_{k9o)24}xq3PP(%XcMws#c)-~}-TAZRfG$UF$72m(000dwW$>&{<3*haC;DTZN8 zt5>g1m6etGroH=nO{GWVt=)#(#aIv*<6)G|z)7d*kPstjwLQvUkpq |6q(*SZK({tEaVKYxUV@3pQ56oG)j0>A`y?Uh9fCT zA}lf#fz5y0Vqz&4ZF_5rtM|fwB_Hhg&=Te%{!a#eOxW~R--o+q8g#Tyqwr6~6UZWy z6zOf!z-0q9$F3YRDlXuyHKXZGrjtgzQ7`CVZ_mx{Kg)H|T@A*4^;Wxx93gE5wmK)M z&Gme8N)VD6Pq@VfE!!AL5_Gl0RRRD|g#aN$$q3Osd-g2t)~#!3&Z4zwz1In}8VUgn zhnRZ$O0DC{iBW%uyIx64@t_tVL`#>C2iC0oA~s_5>sCd3qz$n&^KaC7YtCH0VIgpg z%^vJ+spkM0f#!v59tCQ&XJ9lMv-+FLCdV`!Nl_Ak-DpQ^j;}TAHF|01gl+B@=l3ou zKQPal5>vi;WpnA0g3|EsH@_=#LPDpdn0i}|=)JhNNs=Yn%$h+1fWbzjFkW#BBgu#~ zwDW8J$ 10^_wlqgDwVc3HInmV35y!f;`Adn@Pkch;t{2)fFjz%IEj@u+ILi0fv36QJ| zs&M4seq-0}Jqi>bA7YrCS@gwfhqLzFHHQUafNg0Z&XTMMfG9SKl4du{3JcR38OcBq z06bUkf+VP#V8OQwtwzRJS21a)_l5Fa)`f=_SrbCiyKLxUO=)3C^v$ovOU{tfXk?5k zuL+u<>Fk=Gixia+Nk)K{)N;#*Esb=tcZ!YwdZPL0rDKJ~20QoZpg%^Q>exjZxMWZk ziK!`~Qa@oLG%Zyk9%6JCkrJA~ulpuPZ?9i@|AaGWcB!EFcKGlk$mTlPP l{G0OylY3TW-2D` z ;iNa3B9iuxY zZVPs>x5vAe_n<3lDzzY^%9^%Cb)5}T_Zy o-fL%XhaiRNc>^z^rDm3PfEKd?eujl?#QkG z-woE+RtyvDMmxU!t*yki44eEKKf 0EJsl!e{X=&3i>jxF6hZj-BG(ymaSh5e+>KWtHK0`HnptI_5+ zH|pNqGVLbT^tN#fQ*-}pdRu7Ao0~!to*fT{eDNyr`N8?c06=_1mtoHRZ|{YNtB;_* ziv}nv)TklUM9_SEff$AnD96a$3(8{n@uFZkalX3X$LgydhSf{GUwY9CWHmp(VH&*p zs@Lmb>sGCbxn#6k!%)?x&X(f Csr J^TVWsr$r`H|j+MPULJ-S=LFM zRBSkE(9*${nt%gTOxh)OZrj=aP1OP0kS~U!E8Z)?G|oLnfLJmHF^I?Kmz8_gj#`DU z`+gmIZRIeXGvq3;8EoNYLze`m^mto8Wz*Eyd)wbPvoxzoBXj59;mc-)6F9EDmxmkj z^sUA#G!%vTBQg6;Z_o|^!bE{rTtERpECp~vfCPy|QX4&a;fgZT3Mhg=ceBBh3HgeO z95h50Y Kd>Pw2BU_gQG!pfgO(1KDkkreHjdrE3|%q2?#QJhc8ue>#DwnS-3O=a zr<57e{ZG#eyU$h>0|ba}?VoY)om>Gs6m`(|Su`N>=TqD5X4Xt9>wWG@dpVz9k&Ofd zP-uQw?+sx%4id2l47Ps8)n@YlfHouN(nZhzwpiQxpW0I95<6+MXk?~<@TVa4Gb(4R zNitk2Rluc^F#wvun@W2{vL_fRNC_g{u-sjv2GaqdjFxE3N1j&?(LfR-;YWQw^xbc6 z!+)~(Q^VJPT`zAO^R;X@76p?+5@w2Y+(yds!%z$gZ~;UA#RKJdGA3^tyI%L3M_+Pa zo5pSk%<4B&zxMleiQWtQ I;X_6G(sl+R>Sx7x7|DD>mieTNgancVo=S T zB8lcyd&baAL^v*
w5>B(Pt=308~vrE z!WPN`%x4%X+JU85R2!@{zq(>rD#0hjFGsDhVE^8sn_ BhDCW2Ho2@&JK`5uK?|-ZvkR=2lA@IpUDH%^^%qc|B z7@;*{gx(AZjZR7+JSoV4zw0(L*~>TUd}dw{Lz#M#jn8dm-iM;`n9CO|Al1s|xub{a zctCCpoNOjFj^kj~o{!Cn?J@P&@6@>?b%x<9M_> wBLp!BC5Oisas`6+yGe#oG<~kU=B(YIXb+m8kt68Py|?}S*k>>Q(fgC@C%E_A zzT%m?XL;Ux=|2jq-4{J8kF73NG }X{}}1;pOfb z+uqY@DXosch`TypcIScm|F-aV_AASP)Un|DcZY@Q`3s4pQ^ojgRsRDB)!IP_$sm?U zqEs@8VVS_o-IAi6Q-Z)AJ##^L`jL)4hNjFY!~8mb$opgU)j|Tp@Mf8x$()vC3CkP{ zXwt{6^;Jd^W@1fX*O}e6ZXLRL77Y13Hh9@9?A-n3#&*_r(LW9T%;XHa{QJ)zuof^z z)eFc)ioL0&v
8ATY%CK zZyHd0?j)0RUbJ91Va&t;>WeocNkl-F0T+*JL{Y$D9xFY6A8M>aS?3GBJ9`07r1$`Y zkiX&f)t;!T*2}V*>U~#JK#~!Z<=e+8V*@~lsVISpMn(nN0_^?m@9V4v8$Z3*AKWsK zndw_+5LM2q`0}AkiAPEw3UdMn@_7azEd=oY0ssL2|M;@2`W=88p8x;=07*qoM6N<$ Eg4mA41poj5 literal 0 HcmV?d00001 diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..63cc2aaeeb93a97c00ad7465ee2a8864f7756e7a GIT binary patch literal 67646 zcmd441zglw_djm8sDLO2k~Sy?3I+-Wpa|G&cPBP>*V?tNyXxw?c6YaeEeImrF*7ja zFvG<8zt0`UbzR+EpYJ};??1k-pP5hJ`#ycny%Z}}9RHRrD~A6qi#04)yqH EZle?#M5y7`<*pU9IcsF&a zk|n4_@nTfGsPX%v>3CbBL~-f46MOUD1YM@U`)9uC9l2jm?wp{v&u?a`lnNQTtdge1 z#iOrzMfcMEb-&N)p58k}Aun$yYc=Xz9y9)EwOX;gbm>wwzhlW#CAAilYU`{Q*3((e ztxGli&2mFi-o l|quC4-&G b0r6aT6-_{Z;3<1^x;9F{iCsXVEo zcGaboskkK1|M<+LYEzB0whOHOlJ9{%nfsfO_x2uXHD;P>Yx-BGX3Lsp-Uz$F_t9N{ zcP*{KLOUtTkiWcF5&qS?n|xLN%Or_u7=vu^!Z*p)zm}Q*P>(#iKu>QT%6YcA6}{W$ zKq1?j>t1het~oQmVcPm(mWd1d)y*5$(O5RLSL?S=?w!v6E1W)mA3pc-?c>_y`0!+g zs|b616(1o7@LwKqO@DglBz^7P2K>WA9s_^+6aHt;oDuk!ELl=#KHf~*e5HMEbb55$ zfFJs0mmOc4`tI(NgzK{Q-rEEHU8Rt*{VSQzctY+||2=(~W^-m8pP6bpy;^?lSvARG zPHn}3M?WWGf5DrC*V4ADTT9JmnvmTB>o4E)AK{+_{7?SB!JldF-bvllO>Gp9*EH5W zU1v{$>mBITre+kn-I2m~x1^Bo8>nCWGAuLb@nu!e{nP)HMxnD$)9b)%IqE`HCRpYp z^JDDvpCW?D%aNynb`+n}kKwlg{|}*oU&R0P>C+$auQa@T_Lb1f(Q7WPPANB`3^}c8 zogvSc%ddu9Nr&E6v)yJr@9VZ3u&3(NjmdYXcWPFCR>r;XyG%Rzhn^fvv7K+3&2j)u zv*7eRewTt|d2(jgO>Pq7sa15gU!nm6{$0P$|3&uS|6Koz@df^Y_f8eQ-qAk!&1QRg zvd)1(1G=Z{9kfr@*=rxKZ>D>?%0?G-XuKlm=?(g)_%koF%ub98i^VuW?FzY%uV4J+ zMT7;Wu#L!A8*n#l1^ze|8TO3%?oa%G^5n@#*8kjAA6pf3HSB6U^gY{v+3!=|OJB#o zl0uIwEaui`ef?!Vg)Pr~4_QyH>)WSGawKw 9gA}EvFzyt}ImMfnL(8PN}5%lN|U5_)F5_vLGiFJZ6SI|1Y=C9$hH>b%oEX)3YqI zKkRCm7q-KZ-fU@3uQoSBYfdlM*H;GpHZ3ve$#us6PxyT4T?PKYEIUgo^#0fIFA@RT zq`0tb))ikG1I8cz5k-f;U|!5I+{>WqnNf=s&14A~30b+y++5g+2`npMFA#V<{asRsEL)M4IsE6L zDJ)Bv4%)AC$$XpmHWk#Z!2Tp-=ogkDA7#w%_(!lE^_l-Cm_d>nt9o?%nEFe7|I*n0 zoBN*JJx$?nZmPzO9g$kDRI%(HtxYm!`P54J&A)#1rG<@BZ!B$6aC=$9yuj1Tql2DY z{ipcr*zSAz_>x>zkk7vJUwrg}Atze~fAE~*OTK^}|Kk)B@hSy3$}OUqkM`fiix=7d z)0Hk&TI(>|KGSG=CEe0 ~TKCOeq^!#s->+eBgJZ1Qn=Eda9V?5;%*@Zo>AB4_ z)o4mZAuE`tAqRk|Mc%>32ica%+ ;h`EsD9SDS7+HS%!jRz0DnQr&k`|$ zBK%Fen0yug^pqG4@c*j)^Kat)`MsXrJ40cwuG5nFlNCMXeBV0403C!qTOR>&%QR zW?eu3Q_}NC=OLf|ET2EWC*vjT_25T5R_yQd=W7Z;?AK`2>F^U&fnTsMe#iecW19Cl z{#UPFjbZ%R{;NEqqPpLYeOTtCjQDw&jCq&&@oU(#56H3s+q_W32Bf)CdAH47(ko6X zM;+I<&oEra^us=XwP{A`k^6^9GxB7T8}F{CK(@#c6JlRMwfV*-es1QjJ3Es>a|M K=%EQ@js_~_TU`dzww(kA?iiymQCwp5DL?lDP2m(F(5r1 z6c^h{m8)o@KD `*vp{%W#wTvdTs5V9wmuNGvKe^)+5LYf)7E>m)Xl<8>S(al+t6HCqGMTI=!lP z>Tgq$wK{Fa>6fQiKIB= ?Yt*Qr)FM==EXbF>4}O>e8c?#F{u&(!{6QxXp@Gb=AMG`K z|LRAl)k=WBxbGL_3Hy;07ZuF0AJymh-@A7&Uc7Iy;@UT1Z&HptKb! jINmPJ4 i1Xp|M=)&4W9oo zj$p9=9zJ}S#P3?6Yz6J!OFzU|1z0FK{wwtF7h?3hpJVtnW|$V<|8OrpF*6~#<%;Gx z%=i8F^pWH#b7ipo^JgBL9{(l$c^}gc-^XZjMV;rCZdu`};qkBmQgGj#%9AUQ<8p_b zw~23(-X^?}G+JI?TE2Zb_EA2w{{;Rj$hE|;`99x*?cbft*h9`}LtfoY2Odn{ki e9V}H}m z@p1Ln?xBD06WYYs&;ow;=lp-?&Yd`Z_mU+_>S~UuUT7Hr?7!mU8}@@&Rp(xxjg8NU zOFNzubh 4S(e`vTevdqt#rCe8h!Qfp;W)h0OA} zAMZQ)?09NYPEu0afo %6axq@e?Ad?fd8jY~YVRMm2 zt5t=c50I|=&Lr8 G{TH*$1cDW{mDqBeqeU^7*xm%gBIzHr|_a0LmM;AMp5tWQ55k z0{Hl?PEPm`2YVtf@PWt+{#W_{TSbYSA^Q`||9`;$*I$1Xy0v7<5;@5K6(axlMf_QB zLpJM>(@jaqN{Mf_)Lv0$d`X&kcpPJ&^*HKL3S>bR`}GE&;bru*l%88XGZ ;?oun9@&BX$FS_wxmH&cw!1v(OSoBfY zPFgLiQ82SYA>XxeN%H;WcABS%^95}X@qgq2np5!B=J|J5HckF%vaM|62rI>g;g(uo z7xU !9NW_9veoXInqF_GkH=t8Xr*s|pm! zhaMeBX&7Le%Q}Jio!jR$z`t1^ux(glrimJKmUQvWh4-!3wa6;nsucUEU&0^u|NH-v z{m*lS%{j26>4gRKjOYYHHt1*04sgl*rvJG|zxN;=Q`ZYdHoi;=hmw4onB4rR;ao zZWvaF^MQp^rc6r4*k>^o*c#x_rY1$kJ-vG}uPBf8Z_5D%Vni|gOr`+--+=$BRjY*j zFIBE&ip7-L`2zn>>^}H_Y^TwbBa>vnKFVoTOU7O^|I}RePox~nK^zDEdfh+egyGw( zdTsBK5ft$xIW;3G-v8)y3GzLKtP_~u4fe#Rv|ykO_62(H^vyi=_GE}#f44V`KjeQb zo@uE6z*o}3o-X*e{eSR1$4qs|nX$}NX^MDWp-^Ou7%?IR?;`TLHI2*37Z0eLgt)&v zbcdrZ1b9FHz8TX1ffjU+&>pOAs(ZP~LAQUZT{hwaoEO%)ySt~w#l`XXk+2E%km%sc zCg8a$mOKAfp8t5g@HNjXU~|Uu^YHlt{!^z;6~1fP>Sg2W`Pa#{UhsQ PHDcpIrQ+}CoCDd!KtY&*Aec_n}slZ9g@|&9hM+2^mC1VZDm818MkcPy+iG37) zFV<^IcRAzBI>CUq;abC%>4RfKE!Q ~a<=$V_R2IIoMPB%> z7R0%L5aa 6dsi%C#!A)+e4NiS zJln8E{EzA_tV30M8Hw7OMvWV#=jZ1O8%*d=bO!l9DQxq9tNyDjdpos#G~2`4_#6DbmMkwJHm=m&$cNpx^(q9$@@AHi-KQd4ujS z{sz61(bl;w+o)vRuA0(znWfHh4sv 5%S|Yy`8DuxJmUMQ z?%#KoF>asRa~uz@fgG>>qDFc9E^LW0*#3+ynvf67_pm1lHeOwycsKNR=(Hn~Q@B=y za|F1b&LBGk4a~I=@!vWl&Dp=sEMK9#CKDM3ygxdium4lz=cb~6xk2dXH}d q*9lwP}T z?F@cT (@Wi3y;;Sr&naCxe#@7(9LpZXJ_{71@;`@o44Ws1x64(3?;h!G?x~w`;9z$jiB~! zzU1iSrfb-wm9BonX1WHA9jSh!=G4TYEw$_9O`|5Qp!4@-^el$xT2MZXn!JKamnmn^ zA%&YaZ;nM@ltnb4i3)$247sLbTjU$*`gcCSwXk71mdx}l kQ1VAo7}UuUElm};i>uI z4ojLSSZ?q+xVMgD5jIOL)s-7o7IJF%$l>fq7h;?YGK?~1$!Xx*f+D-=YvaXZDeCV; zyy(&S9OUD)hOy;w#(s55!UvYeFfMee2@@u;tw*Ixml5&&=>fYbFp}u{b0yt vqdA@``oUY|KU9%n zLGfG%z<6@MKW)5^#}V;X4UcI;?7L*-jiXUxr2!3y@nwDc=GhGicwf{=pFMkq?P67# zGGzol-hIbYP!!SQaMC@CCc5x2leQmxLPN$aCx_PEsHtPef;R2CWw&;A%XaC|ExV(e zcaC#M&zx4y-Ev(z_tgxaw3@d6@&qy !JI%>xy_B40@1Zptfn(EA{Rrn(AMcBq`8 )M z|Ng_8qYGo9Hwr;B;uxR9wW2MSH_JTmct7_kBT5n}8T8}8G*iq2_^1=+)f4t1(*)B1 z>jt0g-bn-Y^o?q<+(BM%P8}U`608FhrOKCLT}6Wi4PqWu6vfkp9)LbTEHEezH8y$R zV-?0n%VPx0)xcT#^2udcB5(&^iCXlys5c3S8|w6V`LhJt$&;mvoDRXy%eJ?-&nU|0 zK$f?pr{LH7Av#*U^BQ^hjxDTb)u7PU#zrz_^2E3$ix(8^{b5(eqlfnr--WzQj){&; zj*j|}82I#2!r9X&lGm+Wl|6dYh}fDnYh;?&wo p8miOhKWDplsfEOXwHw zo1>$nRH;;E0VT@M%a(&)rO;~{5#t0M<9B{)QdAV;9_g=xu4aL@RiIrJ`l&1GFV~MK zpFg}%0Qy0^TCK17E+PPyn@n8CjvZs!riScS)Er JsbJ4Vj-^idJ;x(E=&VLBO2YFHO?)BXq=6PW^JUE+${78~U zqvaT%1~t*F^9wnjTg})^w`9X{8t=aaJl zqkSoF%jan0Vq;Th&zhOpym@n}iCJCska5fCVVI5vjhfFoM%avd_wMDssex4i-ajE* zE{T61{7AyJ-k<{|ezQDRAqSx-P~^rV29k{P5T_LVRmn3_V<5*k#$ R_Vbx;4EXbwuVvkW z_tN0E)@r^5S zqqY__v&5}V!LUnDT$du&w^x+9^TJ-@#0xl zR+c;kGA1o3@M&t-E}hd&%q)~X1E*2dYSl#!*~Z33o|l^|@=VgS_)z3{WAT0p#5xjD zcaoVDAFeE7=dbql^8>)#uqzw=6JrSa&NUxYLt?koTGV91$InLHUnuM`DcfaC69z09 z6ZQ$g|C)t|X9mwbI3*ewso7UB;AS}2um!z{n6ID%$O5JVt|tSXsSs0=p}sUVS`wW+ z`oKue7b=-X_#A@|h^>xgint%_K=uVJrq; AA zoH8oDTzR9kI#!MuwJn=VENvR4xw^W_4; ^)McRG9{w7V!#ImDZIL z?3P(;%%+ (-``Gj+t4wCYk` {RCnP!B4@JwT&Oa1sRll{gx35RS zyB9YU&mNrDLFfLVy?AsHzb_UvZCpRW!p4a_{brGOzlGG@XExb2=|aUylq=+zR&sKR z9M8|1HOrrCaWtR-?UX4~GVxy8?5vE!^1E2m0ry?8idF*r6g z5*35^Vtj*zwmBRhz<$yGaQ``o2^7&l1>5pmyCUM3Wy_US_U+d)m2O}8t*}(-(p(?P z=WEfZc*)Y*(&Z{siPB|--S60 xhUXf{<`@(vkWq|6{t5<0gCrwO-tQ0k>u%GfPJ5-7XFDYuztw~+J@0@}; znxVi_`?-I|vKKtA?Xu1_$#;`y41Z_){%i974>&R1%rejS+}d4!^2Lede3e3`P$@Ei zZ;JDpHXQ#~)k4iU%LBd+bp@CsX0Q0^!M Y*Q0csa*s66#E7?iKh !Do)=t{HeUmgQB0MiMLzV &XQ@9CcpRX z=9=% P(Q?ZK}+az_5*8)98j%UHI+m64Ut}Ydx_`&B;O9Z zmDC?~gU~}N)Rl?YBEJ*Y3H$BrB|j2)_`{~_8$Y0CIGg FwvJ5_x_59~5q+c(`gY{+fPXowYpn+y?Y; zrvW|Psh67zdAYVBpDyhQ_!Dr@`F8C{KKKqC#XjEe 0I +UMk?(Kk};qH~qFq{{M&A^F7!$)HjazF&!{Hm|~6~;>?B61KB^^ z+npAg9GY+swWO1OosbS%Q!u~tdzr&Ffjy!f_VbYRefRf (mnH1)~7nO^Wb@oc_7cwf*FXjF%H*ZI1G<~rkD-1P6mePP-F zEog9`oHj$6M!;^)fE-s@&9Nu|)+(E2;0f@Bz_>_W8t@jjyTIm;m>Z7&JKNv!H_!oZ zK?jKWSqhn8G`SLN0NZ@j{z>nDxR($Z{WNO9>G?^R7X*H VLFIq5TEO~TjMQ% zC>D(ZjUk`*k#H@>s}# zw&dEXDRpks9Q>}wlJ7lx(kS|I`v#giW{BYbUw+&{)5i@b+q&k|%Dy3ac5O>NecDlv z?#|>3`t9ARqYi!38RSUs&aS%7{aQ+F=GBv1E`ZMryg8SP?Wg1Om;C %F~a_QA&o;;)5Vja+kSCF+e@<-u3fv0uXRsisRB6^#95 zbo97iMh9cQVMcBLnnI=-`dM#5T~+%*%@TWea#a`cm#%kb=x|SGYB#7EH5=D}8qBh$ zCJXJTMrRW$R=OBX95<4p!k$w`Y9wt~yOhR_9R<^wDCBj}M;`SC@Arp(7yvmub@UK= za{ms6y?aZ~p9Ru^{rhO?;srEn#9$gfZY+%)F^ndTA4OX>Z=j18&eDeu;o^S$pENy< zRxMjhp5Wtt;9u4yEEl$|UrvggbXv1KfV{hPCRth{r6k7Eu%Uy6PA^xf3^nL$MUCg# zQIl!5 @02-H-!X!?^tW*nW!h*S`NX{-4r; z4e~|YkareyMqw}JFFG5Le)9Q=ckm6PI&XB9ur6R4V0nPK#JT-<_kG`eOV_A|E9~-Z zpxZ10>S&xsG>z-ixnp53@RpF77#Ekp&B$&R<|{3zLzc@hF4HZjRGkuJT*a8qoH|YM zG2wLo=6Onq4WW@k2U3&94N0ZQqLWAV!}fC$dYJiau&)Ol+`EhJ+`2(iCr_Z3En8B# za+S!$v<@|B(t*19%%o~27SzbWjXHUZr)I4@$+mtgs#&u(wZggm`uNbky?ZDnC50lw z-_sz-oL=rO n3vYAT1;a`jerN25By^&FIz?je)@q-jg18zm^v8~a&=_6 z0y&ZSb;)sTL-O)+1|5I~y0sIu-nmc9bi{P?h5TRiiTsC7{om5TKf~WZ2fQ`VL0#A; zRi_xS{h<90IV02=My r6Ux;xL(+nSyu6i2a+_ zE+go0*a)!iVEcX7*O%PgI#KPqmek#QIQcI=MjQ8q(&nGTX!p@1I&p_+((HZIvUL~Q zaVUybZGTMb_PnG$zlP8j j|ysGo_=twPQH72W7^@!u!^_SX^wU31u zzt*i N!FJ#O zjt0J+|3Agw@EtM%^