Developer environment from scratch

Rinse & repeat instructions

Xavier Décoret
15 min readMar 6, 2021

In the course of my developer’s journey over several decades, I have honed my tools and tuned my workflow to make me comfortable and efficient. Although it will probably keep evolving, my current setup suits me, and I want to be able to use it everywhere I do development: my work desktop & laptop, my personal laptop, my account — for assistance— on friends’ machines.

But what exactly makes my setup?

It’s a set of software and tools, sometimes tweaked and recompiled, along with their various configuration files. All grown organically. Many long forgotten about. Definitely not easy to reconstruct.

This article is about re-constructing my setup from scratch, understanding what goes in it and why & being confident I have not forgotten any step. It’s a story I can reenact — not simply reread but redo — in the future. You may also find it useful and learn a few things along the way.

There is a second story to be told after it. It will be a similar but shorter story, telling how to deploy an environment from Github. This first story is the prequel that will help you understand the characters in the second one.

In a Nutshell

Without going into details or justification — it’s often a matter of taste — here are the general principles behind my setup. Knowing them helps following the story the way it is narrated.

  1. I prefer the keyboard over the mouse
  2. I want to see information not distraction
  3. I want to learn few things but know them well

Therefore I use a tiled window manager (i3) so I don’t have to bother about window placement. I use a terminal (urxvt) and a shell (fish)that let me display just what I need, and assist me just the right way. I use a text editor (vim) that I can finely tune because typing & navigating code is the core of my activity.

Each of these tools (i3, rxvt, fish, vim) does one main thing and does it well. The terminal doesn’t need tabs support, since the window manager provides it. The shell lets me use vim keybindings. The window manager, the shell, and the text editor can all delegate to powerline for their status bars & prompts. This avoids having to learn multiple ways of doing the same thing, each slightly different from the others, none being truly perfect.

Last but not least, these tools are lightweight in size and blazing fast to run.

Getting a fresh Ubuntu machine

The first step is to be able to quickly create a fresh Ubuntu install that can easily be wiped and recreated anew. Virtualization is the way, and I’ve chose to go with QEmu because it’s powerful, simple & free.

The instructions below are for the setup on iOS, assuming Homebrew is installed, because that’s my setup. You will have to slightly adapt it to your setup if it differs. After that, the machine you’re working on won’t matter as everything will be done in the virtual machine.

First we install qemuand create a directory to hold what we’ll need:

$ brew install qemu
$ mkdir ~/qemu && cd ~/qemu

Then we create a disk image that our virtual machine can use:

~/qemu$ qemu-img create -f qcow2 ubuntu-20.10.qcow2 30G

Next we download the ISO image of Ubuntu:

~/qemu$ wget https://releases.ubuntu.com/20.10/ubuntu-20.10-desktop-amd64.iso

Finally, we create a helper script to start the virtual machine.

~/qemu/start.sh

Now we can proceed with the one time setup. The script is run with an argument to mount the ISO image as a CD-ROM:

~/qemu$ ./start.sh -cdrom ubuntu-20.10-desktop-amd64.iso

NOTE: if this commands fail with HV_ERROR see the instructions here.

The machine will start and will propose to install Ubuntu.

Follow the instructions, choosing the minimal installation and no updates; we will do them manually later as part of the learning process.

Specify a name for the machine. I like to use vmfor virtual machine. Also keep the default “Require my password to login” as it will make is easier to pick the window manager we want later on.

Once it finishes, click the “Restart Now” button. It will ask you to remove the CDROM and press Enter.

Configuring a fresh Ubuntu machine

Once the machine is started with start.sh, when we log in as our created user, we end up in the default Gnome desktop environment. The first login will propose you extra installation steps. Skip them and instead, open a terminal, using the lower-left “Show Applications” button and searching for “Terminal”. That will be a Gnome terminal, and you’ll be using Bash.

We’re going to switch to i3 (window manager), URxvt (terminal), and Fish (shell) instead but they first need to be installed. As it’s the first use of the package manager, we’re first doing an update manually (ignore the Auto Updater UI that may pop up):

xavier@vm:~$ sudo apt update
xavier@vm:~$ sudo apt full-upgrade

Then we install the packages:

xavier@vm:~$ sudo apt install fish rxvt i3

And we can configure those as defaults for our user:

xavier@vm:~$ chsh -s /usr/bin/fish
xavier@vm:~$ sudo update-alternatives --set x-terminal-emulator /usr/bin/urxvt

Log out and log again, but selecting i3 as your window manager.

After the initial setup dialog, press ⌘+RETURN to open a terminal:

Exit i3 with ⌘+SHIFT+E. Re-log and re-open a terminal. That’s it. We have our working environment. The next steps are about making it good-looking and tailor it to my tastes. You are free to adapt to yours. But before we proceed, let’s do two more things. First, let’s remove everything that was created by Gnome and bash, since we are no longer using them:

xavier@vm ~> rm -rf * .bash*

Second, copy-pasting examples inside QEmu doesn’t work out of the box and I couldn’t find an easy solution that doesn’t involve the installation of a third party kit. So instead, the solution is to work from a terminal on my MacBook, and ssh into the virtual machine. For that, we need to install open-ssh:

xavier@vm ~> sudo apt install openssh-server

Then on the MacBook terminal (the one from which you ran start.ssh), copy your ssh keys (read https://www.ssh.com/ssh/keygen/ if you have not created any such keys already), and add your identity to the ssh-agent:

$ brew install ssh-copy-id
$ ssh-copy-id -p 2222 xavier@localhost
$ ssh-add

Now you can quickly connect to your vm using:

$ ssh xavier@localhost -p 2222
xavier@vm ~>

Now that we have a way to connect on our machine even without a graphical user interface, we can remove the heavy weight Gnome environment — we use i3 from now on — and install a lighter login manager.

xavier@vm ~> sudo apt remove ubuntu-gnome-desktop gnome-shell
xavier@vm ~> sudo apt purge --auto-remove ubuntu-gnome-desktop gnome-shell
xavier@vm ~> sudo apt install lightdm
xavier@vm ~> sudo shutdown now

At that point, I like to do a copy of the .qcow2 file that correspond to my starting point for messing with configuration files and installed tools. It’s like having a hard-drive with a freshly installed Ubuntu.

~qemu$ cp ubuntu-20.10.qcow2 ubuntu-20.10.qcow2.fresh
~qemu$ ./start.sh

Setting up urxvt

Installing Nerd fonts

Working efficiently in a terminal requires having a font suitable to read code and containing symbols to display useful information. Such fonts are available at nerdfonts.com. There are multiple ways to install them. We’ll go with the simplest although brute-force one, using git:

xavier@vm ~> sudo apt install git
xavier@vm ~> git clone --depth 1 https://github.com/ryanoasis/nerd-fonts.git
xavier@vm ~> ./nerd-fonts/install.sh FantasqueSansMono
xavier@vm ~> ./nerd-fonts/install.sh DejaVuSansMono
xavier@vm ~> ./nerd-fonts/install.sh FiraCode
xavier@vm ~> ./nerd-fonts/install.sh Monoid
xavier@vm ~> ./nerd-fonts/install.sh RobotoMono
xavier@vm ~> ./nerd-fonts/install.sh SourceCodePro

We use multiple commands because install.sh has a bug and doesn’t support lists of more than two fonts names. We install a few popular fonts from this list, although we’ll use only FantasqueSansMono, but that’s so it’s easy to try other fonts and see which one you like the most. Here is what they look like:

If you want to find which font names are available:

xavier@vm ~> ./nerd-fonts/install.sh _

and look at the list in the error message. The fonts are installed under your ~/.local/share/fonts directory. You can list the files with:

xavier@vm ~> fc-list | grep NerdFonts

Later on, we’ll specify fonts using their xft names. Those resemble the filenames but are not the same! One trick to find them is to use fc-match and grep for a file. For example:

xavier@vm ~> fc-match --all | grep "Fantasque Regular.*Mono\.ttf"

Installing extensions

Urxvt supports extensions written in Perl. Ubuntu package contains the default one but is missing a handy one, which we install with:

xavier@vm ~> mkdir -p ~/.urxvt/ext
xavier@vm ~> sudo apt install curl
xavier@vm ~> curl -fLo ~/.urxvt/ext/font-size https://raw.githubusercontent.com/simmel/urxvt-resize-font/master/resize-font

Configuring the look and feel:

We are going to configure the font and colors, maximize the screen real estate available for content, and fix some default behavior that is annoying. Edit the ~/.Xresources file to add the lines below:

~/.Xresources

Notice on line 14 the font-lineextension that we have just installed. Now reload the resources:

xavier@mv ~> xrdb -merge -display :0 ~/.Xresources

and open a new terminal with ⌘-RETURN. This should look like this:

You can adjust the font size with Ctrl +/-, reset it with Ctrl = and display its current value with Ctrl ?.

Setting up fish

Installing Powerline

Powerline is a utility to generate beautiful and useful prompts for shells. Sadly, the version of powerline packaged in Ubuntu 20.10 is 2.8.1. But we need 2.8.2 because it contains a necessary fix to work with i3. So we can’t simply do sudo apt install powerline. Instead, we’re going to build an updated package, but it turns out to be an interesting exercise.

First, we need to download the source package of 2.8.1. For that, we need to enable the repositories by running:

xavier@vm ~> sudo vi /etc/apt/sources.list

And uncommenting (removing the #) every line with # dep-src. Then we update the package list and install the source package. This required first installing some dev tools and and dependencies for building the package:

xavier@vm ~> sudo apt update
xavier@vm ~> sudo apt install dpkg-dev devscripts
xavier@vm ~> sudo apt build-dep powerline

Then we can install the package in our home directory. Notice that the command below is not run as root (no sudo):

xavier@vm ~> apt source powerline

This creates a bunch of 2.8.1 files and directories. Let’s now download the 2.8.2 version from Github:

xavier@vm ~> wget https://github.com/powerline/powerline/archive/2.8.2.zip -O powerline-2.8.2.zip

With this in place, we can use tools to “update” the 2.8.1 sources from the downloaded 2.8.2 ones:

xavier@vm ~> cd powerline-2.8.1
xavier@vm ~powerline-2.8.1> uupdate ../powerline-2.8.2.zip
xavier@vm ~powerline-2.8.1> cd ../powerline-2.8.2/
xavier@vm ~powerline-2.8.2> dpkg-buildpackage -b -us -uc -rfakeroot

This builds unsigned and unverified — but that is ok — packages in our home directory. They can be installed with apt:

xavier@vm ~> sudo apt install ~/powerline_2.8.2-0ubuntu1_amd64.deb  
xavier@vm ~> sudo apt install ~/powerline-doc_2.8.2-0ubuntu1_all.deb
xavier@vm ~> sudo apt install ~/python3-powerline_2.8.2-0ubuntu1_all.deb

Finally, we can clean our home directory:

xavier@vm ~> rm -rf *powerline*

Configuring prompts

Now we can configure fish prompts, using the bindings distributed with powerline. Add the following to ~/.config/fish/config.fish:

After that step, your shell should look like below. The left prompt will show a segment with the username and one with the current directory. If you’re in a git repository, it will show you a segment with the branch with a nice symbol.

If you’re remotely logged in — as explained with ssh earlier — it will show you the host, with a lock icon to indicate it’s encrypted. The screenshot below is of my iTerm2 on my MacBook, unlike other screenshots which are of QEmu:

The configuration of segments is done via config files. By default those are the ones in /usr/share/powerline/config_files. Here is a tip to find them:

xavier@vm ~> sudo apt install mlocate
xavier@vm ~> locate powerline

You can define your own configuration in ~/.config/powerline/. Configuring powerline is a long topic, and we’ll refer you to the official documentation. We’re only going to do three things:

  1. copy some of the shared config files so we can tune them;
  2. configuring logging to go to an easy-to-find location
  3. setup better default themes.

For 1, we use rsync for simplicity:

xavier@vm ~> rsync -avm \
-f '+ */' \
-f '+ **/shell/__main__.json' \
-f '+ **/shell/default.json' \
-f '+ **/shell/solarized.json' \
-f '- **/vim/plugin_*.json' \
-f '+ **/vim/*.json' \
-f '+ **/wm/default.json' \
-f '+ **/colorschemes/solarized.json' \
-f '- *' \
/usr/share/powerline/config_files/ \
~/.config/powerline

For 2 and 3 we add the following in ~/.config/powerline/config.json:

Finally, we restart the powerline daemon:

xavier@vm ~> powerline-daemon --replace

Using vim key bindings

Because I want to be able to edit commands in the shell with the same muscle memory than when I edit code, I configure fish to use vim key bindings. As we’ll see in next section, I’m using vim without the esckey, so I want the same thing in fish shell. We edit the fish configuration functions with:

xavier@vm ~> funced escape_insert_mode
xavier@vm ~> funcsave escape_insert_mode

and the content below (repeat the above commands for fish_user_key_bindings).

After you’re done, you can check the layout of fish config directory:

xavier@vm ~> sudo apt install tree
xavier@vm ~> tree ~/.config/fish
/home/xavier/.config/fish/
├── config.fish
├── fish_variables
└── functions
├── escape_insert_mode.fish
└── fish_user_key_bindings.fish

Your terminal now has a nice prompt. The first segment shows the vi mode you are in. The screenshot below shows those different modes (INSERT, DEFAULT, VISUAL).

Let’s finish with a bonus feature by installing colorls for fancy listings:

xavier@vm ~> sudo apt install ruby-dev gcc make
xavier@vm ~> sudo gem install colorls
xavier@vm ~> alias -s lt='colorls --tree --sd --sf'

Here is the final result, with the rich prompt and nice listing.

Setting up vim

First we install vim which surprisingly is not part of the minimal install.

xavier@vm ~> sudo apt install vim

We’re going to install Vundle for managing packages.

xavier@vm ~> git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim

Next we’re setting a minimalistic .vimrc file. Notice line 5, needed because we use fish. We only install two packages to start with: one to use the solarized theme, since that’s what we configured for the terminal; and one for easy editing of fish configuration files.

We can now perform the one time setup & download of the packages:

xavier@vm ~>  vim +PluginInstall +qall

The last step is to add a minimal config to use the solarized theme, display line numbers, and make jk behave like the esckey, because it allows me to key my fingers on the home row when touch typing. Remember that we configured fish similarly earlier. Add the following lines to your .vimrc file:

Now that vim is nicely configured, we can make it the default editor for fish — for example when using funced — and other tools like git. Simply add to your ~/.config/fish/config.fish the following:

set -gx EDITOR /usr/bin/vim

A very nice tip with that setup is that ALT+E lets you edit the current Fish command line with vim, which is very useful for long commands!

Setting up i3

The default configuration that i3 created upon our first login is pretty good and self-documenting. We’re only going to change the use of the home row keys to match our vim configuration:

Once you’ve edited the file, reload i3’s config with ⌘+SHIFT+R or from the command line with:

xavier@vm ~> i3-msg reload

The next step is to configure the status bar at the bottom of i3’s screen. The default one is i3status but we’re going to change for lemonbar. Sadly, here again, the packaged version of lemonbar has a problem: it doesn’t (and will probably never will) support xft. So we can’t simply do sudo apt install lemonbar. We have to use a fork and compile it ourselves, a bit like what we’ve done for powerline (but simpler since we only compile one binary and don’t build a debian package). I followed the instructions here. They can be summarized as:

  • clone the fork locally;
  • install the packages to build lemonbar and the development packages needed by th;e fork
  • build locally;
  • install in our .local directory, which is by default in our path.

which translates to the following commands:

xavier@vm ~> git clone https://gitlab.com/protesilaos/lemonbar-xft.git
xavier@vm ~> sudo apt build-dep lemonbar
xavier@vm ~> sudo apt install libxft-dev libx11-xcb-dev
xavier@vm ~> make -C lemonbar-xft
xavier@vm ~> mkdir ~/.local/bin
xavier@vm ~> cp ~/lemonbar-xft/lemonbar ~/.local/bin/
xavier@vm ~> rm -f ~/lemonbar-xft

Now let’s tell i3 to run lemonbar on startup. Add this to ~/.config/i3/config:

exec "/usr/share/powerline/bindings/lemonbar/powerline-lemonbar.py -- -b -f 'FantasqueSansMono Nerd Font:size=8'"

It uses the existing binding provided by the powerline package. It finds our custom version of lemonbar because it’s installed in a standard per-user location. We could explicit it with--bar-command /home/xavier/.local/bin/lemonbar. This custom version allows us to use the Nerdfonts we installed.

The last step is to configure powerline for lemonbar. We replace ~/.config/powerline/wm/default.jsonwith a better config:

We also defined the solarized color theme, because surprisingly powerline doesn’t have a default one. Create the following file (create the directory first) ~/.config/powerline/colorthemes/wm/solarized.json, with this content:

The powerline binding for lemonbar and i3 requires a library for inter-process communication (IPC). It’s installed with:

xavier@vm ~> sudo apt install python3-pip
xavier@vm ~> pip3 install i3ipc

Now quit i3 (⌘+SHIFT+E) and re-log in. You now have a bottom bar that looks like below:

And that terminates our journey!

Bonus

There is one last place we can configure to use vim bindings, a powerline-like prompt showing the edit mode, and solarized colors. This is GNU readline, which is used by many tools, notably Python. By default, launching python3 will give you a colorless>>> promt, and will use Emacs-like key bindings:

We can configure readline to use vi key-bindings and a nicer prompt, vi the ~/.inputrcfile (the box character is a right-pointing triangle available in the Nerdfonts installed earlier, but that neither Github or Medium can display):

Now our Python prompt will look like this, in insert and command modes:

If the >>> and ... prompts feel redundant, see how to change them.

Summary

What we have seen along the way is that:

  1. we had to install several packages;
  2. we did so in 3 different ways: with apt, with gem and with pip3;
  3. we had to recompile a package with a more recent version of its source;
  4. we had to recompile a package from a fork of its source;
  5. we had to write a couple of configuration files.
  6. we had to install fonts;

The first steps can be condensed in a post-install script. The last 3 steps created a set of dotfiles and user installs in ~/.config/ and ~/.local/. This is where the real story begins. Those configuration files needs to be customized and will grow waaaaay bigger than the simple versions described here. And they will be unique to you, aggregating pieces you’ve found in various places. Once this is done, you want to save these (precious) configuration in a way that makes it easy to deploy them in other machines. We’re not describing how to do here, as Atlassian does it way better.

Conclusion

This was a long story, as I wanted to keep track of every steps along the way. But this can be condensed in a few sets of steps without explanations, followed by a checkout of your dotfiles. You can find that in Part 2.

Appendix

As usual, I was able to achieve my goal thanks to pre-existing pages sharing their experience and knowledge.

--

--