So you’ve installed homectl according to the README instructions; now what? How do you apply it to your setup with a minimum of fuss?
This document describes how to create your own packages for homectl, or otherwise extend it to your liking. We’ll start by defining some key concepts, then dig into how homectl packages are constructed, and finally discuss various extension points ("hooks" and "triggers") that are available for you to use in the construction of your own packages.
Before we begin, you should know a few important things:
-
Everything you keep in homectl lives in a package, without exception. Packages provide functionality (including the
hc
command itself) for a specific purpose. -
Typically, packages live in your homectl git repository, which is called your setup.
-
You use the
hc
command to enable and disable packages, or refresh your home directory after you make changes to a package.
When you first hc enable
a package, homectl symlinks the contents of the
package into ~/.homectl
. Files from all your enabled packages will be mixed
together under this directory. Then, files from your package’s overlay
(discussed later) will be linked directly into your home directory.
When you hc disable
a package, its files are unlinked from your home directory
and from ~/.homectl
, thus undoing the effects of hc enable
.
As you modify your packages by adding/deleting/renaming files, you can use hc
refresh
to re-scan all your enabled packages and update your home directory and
~/.homectl
.
Before we talk about packages themselves, it’s worth taking a moment to think about how you want to organize your stuff. Consider splitting your stuff along these lines:
-
What it does (e.g. emacs vs. screen vs. ssh-agent vs. …).
-
What hat you’re wearing (e.g. work, personal, charity).
-
How often you use it (keep rarely-used things out of your environment).
Don’t worry about platform compatibility at this stage (e.g. "this only works on Linux"). As we’ll see later, homectl can automatically include/ignore content based on platform.
- dev.hcpkg
-
Things related to software development. This includes scripts, Emacs packages and tweaks, etc.
- emacs-ux.hcpkg
-
Themes, usability customizations and other enhancements for Emacs.
- editor.hcpkg
-
Script for finding and starting an editor, depending on what environment he’s working in (GUI, text, …).
- fs.hcpkg
-
Various scripts for working with lots of files (renaming, moving, etc.)
- mail.hcpkg
-
Various scripts for processing and archiving e-mail.
- music.hcpkg
-
Scripts for manipulating the author’s music collection.
- oh-my-zsh.hcpkg
-
A set of extensions to zsh maintained by robbyrussell, plus configuration.
- root.hcpkg
-
Scripts needed only by the root user (e.g. backup scripts).
- screen.hcpkg
-
.screenrc
- ssh-agent.hcpkg
-
Ensures an ssh-agent is available in the author’s shell.
- work.hcpkg
-
Scripts and configuration files relevant only to the author’s employment.
- zsh.hcpkg
-
Shell customizations (on top of oh-my-zsh).
homectl packages are just directories with a .hcpkg
extension. To create a
new, empty package, all you need to do is:
$ mkdir my-stuff.hcpkg
The contents of a homectl package are broken down by system and by hook.
A system is a particular environment, such as a machine with a particular
hostname, a particular OS, or a particular OS/CPU-architecture combination.
Except for the default common
system, which is always available no matter what
machine you’re on, system names begin with an upper-case letter.
A hook is an extension point. Hooks are typically given well-known names,
such as bin
, lib
or emacs
. Hook names always begin with a lower-case
letter.
All files in a homectl package belong to a particular system and hook.
Except for the special common
system, all files live in a directory hierarchy
inside the package which follows the pattern:
example.hcpkg/$System/$hook/my-file.txt
Files in the common
system can omit the $System
directory entirely:
example.hcpkg/$hook/my-file.txt # this file is in the "common" system
Any system or hook whose name begins with _
is ignored by homectl. So you can
put stuff which you don’t want linked into ~/.homectl
into one of these
directories. Note that this does not apply to nested directories underneath
hooks/systems; here are some examples:
example.hcpkg/_data # not linked into ~/.homectl example.hcpkg/foo/_data # will be linked into ~/.homectl example.hcpkg/Linux/_data # not linked example.hcpkg/Linux/foo/_data # will be linked
example.hcpkg/ bin/ <--- hook (in the "common" system) my-script emacs-startup/ my-settings.el Linux/ <--- system bin/ <--- system-specific hook my-binary lib/ libfoo.so ... _*/ <--- Directories matching this pattern are ignored [A-Z]*/ <--- this is what a system name looks like [a-z]*/ <--- this is what a hook name looks like ... _*/ <--- A sytem-specific ignored directory *.trigger <--- Trigger files will be discussed later _trigger
When a package is enabled, homectl symlinks the contents of each package into
~/.homectl
, following the pattern: ~/.homectl/$SYSTEM/$HOOK
. From the
example above, if you were to create a script example.hcpkg/bin/my-script
,
homectl would create the following link in ~/.homectl
:
~/.homectl/common/bin/my-script -> path/to/example.hcpkg/bin/my-script
Note that unlike in the package, the common
system is explicit here; this is
so homectl can place its configuration files directly under ~/.homectl
without fear of name clashes.
Similarly, if you were to place a file under a system-specific hook, you would see a symlink like so:
~/.homectl/Linux/bin/my-binary -> path/to/example.hcpkg/Linux/bin/my-binary
Symlinks created by homectl use relative paths when they are within your home directory, and absolute paths otherwise. This is done to accommodate a user whose home directory may be in different locations on different systems.
You may also place files in subdirectories inside a hook. Those files will be
individually linked into ~/.homectl
. This allows you to build entire trees
with files pulled from different packages. For example, a binary package may
place manpages under share/man/manX
:
$ hc enable my-stuff.hcpkg update /home/me/.homectl/enabled-pkgs mkdir -p /home/me/.homectl/common/share/man/man1 ln -s ../../../home-setup/my-stuff.hcpkg/share/man/man1/foo.1 /home/me/.homectl/common/share/man/man1/foo.1 ln -s ../../../home-setup/my-stuff.hcpkg/share/man/man1/bar.1 /home/me/.homectl/common/share/man/man1/bar.1
The special overlay
hook contains files that will be linked directly into your
home directory when you enable the package. Typically, you would place
dot-files here (e.g. .vimrc
, .screenrc
, etc.).
The overlay
hook is only special in the common
system — that is,
system-specific overlay hooks will not be linked into your home directory.
$ mkdir screen.hcpkg $ mkdir screen.hcpkg/overlay $ touch screen.hcpkg/overlay/.screenrc $ hc enable screen.hcpkg ... ln -s ../../../home-setup/screen.hcpkg/overlay/.screenrc /home/me/.screenrc ...
If you place a .screenrc
into the overlay, hc enable
will link it into your
home directory automatically.
As with subdirectories inside other hooks, only individual files are linked into
your home directory; if you create a directory inside overlay/
, a separate
directory will be created in ~
, and the files inside the overlay will be
linked into that directory.
$ mkdir unison.hcpkg $ mkdir unison.hcpkg/overlay $ mkdir unison.hcpkg/overlay/.unison $ touch unison.hcpkg/overlay/.unison/default.prf $ hc enable unison.hcpkg ... mkdir -p /home/me/.unison ln -s ../home-setup/unison.hcpkg/overlay/.unison/default.prf /home/me/.unison/default.prf ...
homectl will create the directory if it doesn’t already exist, and place a symlink in that directory. We can also see that homectl has adjusted the symlink’s target path to account for the fact that it lives in a subdirectory.
Do you have a configuration file (or set of files) you’d like to keep in
homectl? Now is a good time to try creating your own package. Let’s take
tmux
as an example; it keeps a configuration file in ~/.tmux.conf
.
-
Create a package for your tmux configuration:
$ cd my-homectl-setup $ mkdir tmux.hcpkg
-
Enable your new tmux package:
$ hc enable tmux.hcpkg # You can also shorten this to just "hc en".
-
Create an overlay directory to hold your
.tmux.conf
:$ mkdir tmux.hcpkg/overlay
-
Move your
.tmux.conf
into your homectl package:$ mv ~/.tmux.conf tmux.hcpkg/overlay/ $ git add tmux.hcpkg/overlay/.tmux.conf $ git commit -m "Add my tmux.conf"
-
Now, refresh your home directory so that homectl will re-scan your enabled packages and find your
.tmux.conf
:$ hc refresh # or "hc ref", for short $ update /home/me/.homectl/enabled-pkgs $ ln -s ../../../my-homectl-setup/tmux.hcpkg/overlay/.tmux.conf /home/me/.homectl/common/overlay/.tmux.conf $ ln -s .homectl/common/overlay/.tmux.conf /home/me/.tmux.conf
-
Notice that homectl created two symlinks: one from
~/.homectl
to your.tmux.conf
, and one from~/.tmux.conf
to~/.homectl
.
Often, you will want to write code that searches through all enabled packages looking for things. The hc path
and hc tree
commands help you do this by finding directories and/or files which pertain to specific hooks on the current system.
You can use the hc path
command to generate a list of directories to search
for a particular hook. hc path
will always return hook directories in the
common
system, as well as hooks in other available systems (as outlined in the
"Systems" reference later in this document).
For example, if you want to find all the available bin
hooks on the current system, you might do this:
$ hc path bin /home/me/.homectl/common/bin:/home/me/.homectl/Linux/bin
You can even use hc path
to update environment variables:
$ echo $PATH /usr/bin:/bin $ hc path bin PATH /home/me/.homectl/common/bin:/home/me/.homectl/Linux/bin:/usr/bin:/bin
If duplicates are present, hc path
will helpfully remove them:
$ echo $PATH /home/me/.homectl/common/bin:/home/me/.homectl/Linux/bin:/usr/bin:/bin $ hc path bin PATH /home/me/.homectl/common/bin:/home/me/.homectl/Linux/bin:/usr/bin:/bin
You can use hc tree
to look for specific files inside hooks. For example, if
you want to find all *.el
files in the emacs-startup
hook, you might do
this:
$ hc tree emacs-startup '*.el' /home/me/.homectl/common/emacs-startup/commit-message-mode.el /home/me/.homectl/common/emacs-startup/graphviz-dot-mode.el ...
Some packages (called loader packages) exist only to provide a way for other
packages to insert their functionality into your environment. For example,
homectl comes with the loader-bash.hcpkg
package. loader-bash.hcpkg
replaces your existing .bashrc
and .bash_profile
with scripts that do
nothing but load bash
customizations from all your enabled homectl packages.
homectl includes several loader packages by default, to help get you started and to ease the task of breaking up your configuration into packages. You should choose the ones that apply to you and enable them with:
$ hc enable homectl/loader-foo.hcpkg
These packages replace your shell’s standard profile and rc-files with stubs that search through your enabled homectl packages and load any shell customizations they find (environment variables, functions, shell scripts, snazzy prompts, etc.).
They look for shell customizations in the shell-env
and shell-rc
hooks, and load anything they find there. loader-bash.hcpkg
will look for files named .sh
or .bash
, while loader-zsh.hcpkg
will look for files named .sh
or +.zsh*.
As the name implies, shell-env/*
files are generally expected to contain
environment variable or other settings that could apply to both interactive and
non-interactive shells. They should not produce any output, nor expect any
input. They may be run even as part of shell scripts, so it’s best to keep them
as small as possible.
shell-rc/*
, on the other hand, contain things one might use while sitting at a
shell prompt. This would be a good place to change your prompt, set up an
ssh-agent
, or add shell aliases.
If the loader sees a bin/
hook inside your package, that directory will
be automatically added to your PATH. Similarly, lib/
, lib64/
, etc. are
added to your linker path. This helps you to package 3rd-party programs for use
in homectl with a minimum of fuss.
The Emacs loader replaces your ~/.emacs
file with a script that loads Emacs
packages and customizations from your enabled homectl packages. It also
provides a convenient way to download and install package.el
packages from
third-party sources.
You can customize your Emacs by writing small a Emacs package (just a foo.el
file with (provide 'foo)
at the end) and placing it in the emacs-startup/
hook.
hc path
, hc files
and related commands will search for hooks in the
following "system" subdirectories of ~/.homectl
:
-
common
-
$system
-
e.g.
Linux
,Darwin
-
-
$system-$arch
-
e.g.
Linux-i686
,Darwin-x86_64
-
-
$system-$release
-
e.g.
Linux-2.6.32
,Darwin-13.3.0
-
-
$system-$release-$arch
-
e.g.
Linux-2.6.32-i686
,Darwin-13.3.0-x86_64
-
The loaders that come with homectl support the following hooks. Any loaders you write should follow these conventions as well.
overlay/
-
Files in
overlay
are symlinked directly into your home directory. [common
system only] bin/
-
Added to
$PATH
. Contains scripts or binaries that belong to this package. lib/
lib64/
lib32/
-
Added to
$LD_LIBRARY_PATH
,$DYLD_LIBRARY_PATH
or the equivalent on your platform. Contains libraries used by binaries in this package. emacs/
-
Added to Emacs’s
load-path
. You can place Emacs packages here and they will be accessible with(require)
. emacs-startup/
-
Added to Emacs’s
load-path
. You can place Emacs packages here and they will be automatically loaded with(require)
at startup. shell-env/
-
Defines shell environment variables and other settings which apply to both interactive and non-interactive shells. Files in this hook should have an extension which matches the shell (e.g.
*.sh
,*.bash
, etc.) shell-rc/
-
Defines shell aliases, functions, prompts, and other settings which apply to interactive shells. Files in this hook should have an extension which matches the shell (e.g.
*.sh
,*.bash
, etc)
If simple symlinking isn’t enough to deploy a package, homectl provides a way to
run programs when packages are enabled (actually, refreshed) or disabled. Just
create a program named _trigger
in the top-level package directory.
Triggers are currently not system-specific, so they should be written in such a way as to apply to all platforms. They are also non-interactive (user input is not supported).
Triggers are run with the trigger name as their sole parameter (for example:
_trigger build
). They are always run from the top-level *.hcpkg
directory
(so pwd
or the equivalent will tell you where your package lives).
The following triggers are supported:
build
-
Prior to symlinking the contents of a package into
~/.homectl
, thebuild
trigger is run. This trigger may create generated files inside the package itself, and homectl will pick up those files and symlink them. When a package is enabled, or when an already-enabled package is updated viahc refresh
, homectl will look in the top level of each package directory for a file calledrefresh.trigger
. If this file exists and is executable, homectl will run it after symlinking the package’s files into~/.homectl
. refresh
-
After a package has been symlinked into
~/.homectl
, therefresh
trigger is run. Ifrefresh.trigger
modifies any of the files in the homectl package, homectl will not notice this until the next timehc refresh
is run. disable
-
Before a package is disabled and unlinked from
~/.homectl
, thedisable
trigger is run. clean
-
After a package is disabled and has been unlinked from
~/.homectl
, theclean
trigger is run. This is the place to remove build artifacts and do general cleanup of the package directory, if required.
Here are a couple examples showing how to write shell scripts and/or makefiles to be run as triggers.
#!/bin/sh case "$1" in build) # Things to do prior to enabling/refreshing the package ;; refresh) # Things to do when the package has just been refreshed/enabled ;; disable) # Things to do before the package is disabled ;; clean) # Things to do to cleanup after the package is disabled ;; esac
#!/usr/bin/env make -f # Put this file in the top level of your package's directory, name it # "_trigger", and make it executable. build: # Things to do prior to enabling/refreshing the package refresh: # Things to do when the package has just been refreshed/enabled disable: # Things to do before the package is disabled clean: # Things to do to cleanup after the package is disabled .PHONY: build refresh disable clean