Skip to content
This repository has been archived by the owner on Nov 7, 2021. It is now read-only.
/ dotty Public archive

A delightfully lispy dotfile manager 🏠

License

Notifications You must be signed in to change notification settings

mohkale/dotty

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

header

Table of Contents

Features

  • modular by design
  • gotta go fast, fast, fast
  • builtin support for package managers
  • (recursive (recursive (recursive (...))))

Example

Here's a basic config file for setting up python, alongside some python packages. It should give a high level overview of what your dotfile config will look like using dotty.

(
 #dot/link-gen
 (:link "~/.pdbrc"
        "~/.config/pdbrc.py"
        {:src "pythonrc" :dest "~/.config/pythonrc.py"})

 ;; install python itself
 (:packages (:apt "python3" "python3-pip")
            (:msys "python" "python-pip")
            (:choco "python")
            (:pacman "python3" "python-pip"))

 ;; install the python packages I always want :-)
 (:packages
  (:pip "requests"
        "youtube-dl"
        "beautifulsoup4"
        "edn_format"
        {:pkg "RequestMixin" :git "mohkale"}
        {:pkg "DownloadQueue" :git "mohkale"}))
)

Setup

I recommend creating a script at the root of your dotfiles repository to automatically fetch dotty when it's not available on your system.

#!/usr/bin/sh

# exit on any errors
set -e

# which release of dotty you want to use
dotty_url=https://github.com/mohkale/dotty/releases/download/1.0.0/dotty-linux-arm64.tar.gz

# where to put the release
dest_path=./setup/dotty

# download dotty when you don't have it available.
if ! [ -e "$dest_path" ]; then
  curl -o "$dest_path" -L "$dotty_url"
fi

"$dest_path" "$@"

If you intend to use dotty across multiple platforms or architectures, you may find it easier to build dotty from source or to retrieve a dotty version for your current platform.

By convention each release contains compiled executables for a majority of kernels and architectures. You can create a script such as this and this to determine your current architecture and kernel name, and substitute these into the url above to support cross platform and cross architecture installations.

kernel=$(./setup/kernel)
arch=$(./setup/arch)
dotty_url=https://github.com/mohkale/dotty/releases/download/1.0.0/dotty-$kernel-$arch.tar.gz

...

Now you create a file at the root of your repository named config.edn and dotty will automatically import it when installing.

(
 (:info "hello new dotfiles :grin:")
)

How it works?

dotty reads a configuration file in clojure-style edn format which you can scatter across your dotfiles. Each config should be a list containing a series of directives (or actions) that dotty should perform.

For example, this config has a single directive which tells dotty to create some directories.

(
 (:mkdir "~/.config" "~/.local")
)

Directive Format

Most directives support a single argument or extended mapping form. In the single argument form you simply pass a string or number or basic value, such as:

(
 (:mkdir "foo")
)

The extended form lets you supply more options to dotty for the argument. Each directive has it's own definitions as to what options it accepts. The :mkdir directive for example accepts a :chmod option to let you set the file permissions of the directory in octal notation.

(
 (:mkdir {:path "foo" :chmod 700})
)

Most directives also support two extra options in the mapping form, :when and :if-bots.

  • :when is just a shortcut for the :when directive.
  • :if-bots is a shortcut for (:when (:bots)). I.E. {:if-bots "foo", ...rest} is equivalent to (:when (:bots "foo") {...rest})

File Paths

Most directives let you specify filepaths in a conveniently recursive form. For example:

(
 (:mkdir "foo" ("bar" ("baz" "bag"))))
)

creates the directories foo and bar/baz, bar/bag. The syntax is predictable and can extend upto an arbitrary depth. Paths wrapped into a lower depth are joined into all possible combinations of earlier paths:

(
 (:mkdir ("foo" ("bar" "baz" ("bag") "bam"))
)

Will create: foo/bar/bag foo/baz/bag, foo/bam.

You can also substitute environment variables into paths:

(
 (:mkdir "${XDG_CONFIG_HOME}/")
)

Directives

:mkdir

Creates a directory on your file system.

Aliases:

  • :mkdirs
Option Is Default Default Value Description
:path Yes The path to the directory to create
:chmod 0744 Permissions of the newly made directory

:import

The :import directive lets dotty include other config files. This can be chained with :when to conditionally configure dotfiles.

Option Is Default Default Value Description
:path Yes The path to the files to import
(
 ;; you supply one or more paths to configs you want to import
 (:import "foo" "bar" "baz")

 ;; you can conditionally include/exclude a package
 (:import
   {:path "foo"
    :when "[ $HOST -eq foohost ]" })

 ;; you can nest paths using the same syntax as :mkdir
 ;; eg. imports foo/bar and foo/baz
 (:import ("foo" ("bar" "baz")))
)

See also gen-bots.

Import Resolution

dotty has a permissive import system relying on little user configuration. For eg. to import the foo config, dotty follows the following process:

  1. look for a file named foo.dotty.edn or foo.edn from the current directory.
  2. look for a directory called foo containing a dotty.edn or foo.edn or a .config file.

:link

Create a link from one file to another.

Option Is Default Default Value Description
:src Yes The path to the file (or files) that is being linked
:dest Yes The path (or paths) where :src is linked to
:mkdirs true Automatically create parent directories for :dest
:relink false If :dest exists and is a symlink, overwrite it
:force false Overwrite :dest if it exists and is not a directory (implies :relink)
:glob false :src is a glob path, link all found globs into :dest
:ignore-missing false If :src is not found, create a link anyways
:symbolic true Whether to create a symlink or a hardlink

The syntax of the :link tag is slightly more peculiar, you specify :src then :dest in pairs. If a src is given without a destination, an error is thrown.

(
 (:link "./src" "~/dest"
        {:src "foo" :dest "~/bar"})
)

You can specify multiple sources or multiple destinations and dotty will handle it predictably.

(
 (:link
   ;; multiple sources, dotty creates a directory at :dest
   ;; and links each source into it.
   ("foo" "bar" "baz") "~/all-my-foos"

   ;; multiple destinations, dotty links src to each :dest
   "foo" ("~/foo1" "~/foo2" "~/foo3")

   ;; multiple sources and destinations, dotty creates a
   ;; directory at each destination and links each src into
   ;; it.
   ("foo" "bar") ("~/foo" "~/bar"))
)

If :dest exists and is already a directory (or has a trailing slash) dotty will link the :src files into it.

(
 (:link
   ;; destination has a trailing slash, make a directory and
   ;; link foo to ~/foo/foo
   "foo" "~/bar/"

   ;; destination exists and is a directory, link baz into it.
   "baz" "~/.config")
)

See also link-gen.

:clean

Finds and remove any broken links that point to your dotfiles. The format is the same as :mkdir

Option Is Default Default Value Description
:path yes The path to the directories to clean
:recursive false Recursively search for dead links
:force false Remove broken links even if they don't point to dotfiles
(
 (:clean {:path "~/.local/bin" :recursive true})
)

:shell

Lets you execute arbitrary shell code.

Option Is Default Default Value Description
:cmd yes The shell command to run
:desc A brief description of what this command does (for logging)
:quiet false Don't log the command, or it's exit code
:stdin :interactive Connect this commands :stdin to dottys stdin
:stdout :interactive Connect this commands :stdout to dottys stdout
:stderr :interactive Connect this commands :stderr to dottys stderr
:interactive false Set the defaults for :stdin, :stdout, :stderr

Each argument passed to this directive is executed in its own subshell. To run multiple commands in the same subshell, pass it as a single (long string) or as multiple entries in a list.

(
 ;; doesn't output anything because :stdout is false and each argument
 ;; is run in a different subshell.
 (:shell "foo=hello" "echo $foo")

 ;; doesn't output anything because :stdout is false
 (:shell ("foo=hello" "echo $foo"))
 (:shell "foo=hello
          echo $foo")

 ;; outputs correctly
 (:shell {:desc "Prints foo"
          :stdout true
          :cmd "foo=hello
                echo $foo"})
)

:def

Assign default options for directives or environment variables.

This directive doesn't support an extended map form. It accepts only key value pairs, with default behaviour being assigning each key to it's value in the environment variables exposed to subprocesses.

(
 (:def
   ;; export foo=bar and "bag=bag" as environment variables
   "foo" "bar"
   "baz" "bag"

   ;; set the default value of some options for the :shell directive.
   (:shell :quiet       false
           :interactive true))

 ;; outputs without issue
 (:shell "echo $foo; echo $baz")
)

Directives that support the :def directive are:

  • :mkdir
  • :link
  • :clean
  • :shell
  • :package

:when

Conditionally execute some directives.

This directive lets you use :shell to conditionally perform tasks. It takes the same arguments as the :shell directive and supports chaining and conditionals using :and, :not and :or.

(
 (:when (:not "uname -a | grep linux")
   (:warn "You're not in linux, why?"))
)

The when directive changes the defaults of :shell to make :quiet true by default. This is because conditionals are expected to pass or fail, it's not an error if they fail. You can override this change if you prefer:

(
 ;; an error log is thrown if this command fails
 (:when {:cmd "uname -a | grep linux" :quiet false}
   (:warn "You're in linux, Yippee"))
)

:debug, :info, :warn

These directives let you hook into dottys logger to produce your own logging output.

(
 (:debug "hello world")
 ;; supports printf style formatting
 (:info "my name is: %s" "mohkale")
 (:warn "you're environment isn't setup correctly")
)

:package

Use an external package manager to install a package.

Aliases:

  • :packages

The format of this directive resembles a switch statement, dotty will try each package manager in turn until it finds one that exists and then it'll try to install all supplied packages with that manager.

(
 (:packages
   ;; if pip is available, try to install foo and bar with it.
   (:pip "foo" "bar")

   ;; otherwise if rubygems is available, try to install these gems.
   (:gem "baz" "bag")

   ;; the :default clause runs a shell command when no managers were
   ;; found.
   (:default "echo failed to find a package manager, (;_・)")
   )
)

Each package supports the extended map form with the following options:

Option Is Default Default Value Description
:pkg yes The package to install
:manual A shell command to use to install the package instead of the manager.
:before A shell command to run before installing the package. Installation is skipped if this fails
:after A shell command to run after installing the package.
:stdin :interactive Connect the package install commands :stdin to dottys stdin
:stdout :interactive Connect the package install commands :stdout to dottys stdout
:stderr :interactive Connect the package install commands :stderr to dottys stderr
:interactive true Set the defaults for :stdin, :stdout, :stderr

For example, here's a config for installing nodejs:

(
 (:packages
  (:apt {:pkg "nodejs"
         :before "[ -z \"$(which node)\" ] || exit 0 # already installed
                  curl -sL https://deb.nodesource.com/setup_13.x | sudo bash -"})
  (:choco "nodejs")
  (:pacman "nodejs"))
)

Package Managers

I've only ever used windows & ubuntu/arch so the only package managers I have configured is for them. If you'd like to add support for a newer package managers, simply navigate to d_pacmans.go and add a new struct to the packageManagers map.

dotty currently supports:

WARN: Some of these package managers may require elevated user privilages. When possible dotty will ask for sudo privilages automatically.

And the following managers support extended options.

:gem
Option Default Value Description
:global false Whether to install this gem globally
:pip
Option Default Value Description
:global false Whether to install this python package globally
:git Install from git instead of PyPI

NOTE: the :git option has two forms, you can supply it as as a single string, in which case dotty will assume a default host of github. Or you can supply it as a map with both the git host and user name for where the package can be found.

;; both of these forms install: git+https://github.com/mohkale/foo
{:pkg "foo" :git "mohkale"}
{:pkg "foo" :git {:host "github" :user "mohkale"}}

Tags

Tags are a good way to preprocess some data before interpreting it. It's an edn construct.

Dotty offers the following tags to simplify configurations.

Tag Affect
#dot/only-windows Only run this directive when on Microsoft windows.
#dot/only-linux Only run this directive when on a Linux system.
#dot/only-darwin Only run this directive when on a MacOS system.
#dot/only-unix Only run this directive when on a Linux or MacOS system.
#dot/link-gen Automatically generate link srcs (or destinations).
#dot/gen-bots Automatically generate :if-bots options for imports.

Link Generation

Quite often when you're linking files the destination matches the source file (likely without a leading '.'). To avoid having to repeat the same name multiple times, you can attach the #dot/link-gen tag to a link directive and dotty will try guess the src from your destinations.

(
 #dot/link-gen
 (:link
  "~/.bashrc"                            ; :src is bashrc
  {:src "bash_logout"}                   ; :dest is ~/.bash_logout

  ;; when both are provided, dotty leaves them alone.
  {:src "profile" :dest "~/.bash_profile"})
)

Bot Generation

Quite often you're likely to end up with multiple sub configs for different programs that you want to let users choose to setup. To do so you'd have to specify a :if-bots options for every import.

(
 (:import
   {:path "lf"            :if-bots "lf"}
   {:path "ranger"        :if-bots "ranger"}
   {:path "langs/python"  :if-bots "python"}
   {:path "langs/node"    :if-bots "node"})
)

This quickly becomes a mess. The #dot/gen-bots tag automatically inserts a :if-bots option for each entry in an import directive (unless one is already found).

(
 #dot/gen-bots
 (:import
   "lf"
   "ranger"
   "langs/python"
   "langs/node")
)

This is equivalent to the previous configuration (and I think unquestionably nicer to read).

Using dotty

bots

As someone who jumps between different platforms as a creature of habit, I've grown accustomed to only configuring what I end up using. dotty is designed to make this easier by letting you specify (at dotfile installation) what you want installed? or what you have available to install.

This simple mechanism is called bots. You specify which bots you want to install when you invoke dotty (using the -b flag). For example, if I want to install the python and ruby bot I can pass:

dotty install -b python,ruby

Now in our configuration we can use the :when directive to conditionally configure logic when we're installing these bots:

(
 (:when (:bot "python")
   (:info "Installing python"))
)

Better yet, we can modularise our config into a subconfig and only import it when we're installing python:

(
 (:import {:path "langs/python" :if-bots "python"})
)

Once dotty is finished installing your dotfiles, it'll append the bots you just installed into a file local to your dotfiles (see dotty install -h) which you can use to resync them at a later date.

dotty install --only link -b "$(cat .dotty.bots)"

This will run any link directives for any bots we've installed in the past and save to .dotty.bots. NOTE: you can override the default file name/path for the bots file using the DOTTY_BOTS_FILE environment variable.

dotty can also traverse your dotfiles and list any bots you're checking for at any stage. This can let you see what bots your dotfiles have available. For more information, see dotty list-bots.

.dotty.env

At startup dotty can read environment configurations from a file at one of .dotty.env.edn, .dotty.env, .dotty. This file is a essentially a dotty configuration automatically wrapped in a :def tag. For example:

(
 ;; specify environment variables
 "XDG_CONFIG_HOME" "~/.config"

 ;; change directive defaults
 (:shell :interactive true)
)

Credits

dotty takes more than a little inspiration from dotbot, the dotfile management solution I was using before creating this. Give that project some love if you can ❤️.