Developer environment from scratch
Rinse & repeat instructions
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.
- I prefer the keyboard over the mouse
- I want to see information not distraction
- 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 qemu
and 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.
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 vm
for 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:
Notice on line 14 the font-line
extension 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:
- copy some of the shared config files so we can tune them;
- configuring logging to go to an easy-to-find location
- 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 esc
key, 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 esc
key, 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.json
with 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 ~/.inputrc
file (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:
- we had to install several packages;
- we did so in 3 different ways: with
apt
, withgem
and withpip3
; - we had to recompile a package with a more recent version of its source;
- we had to recompile a package from a fork of its source;
- we had to write a couple of configuration files.
- 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.
- https://graspingtech.com/ubuntu-desktop-18.04-virtual-machine-macos-qemu/
- https://ethanschoonover.com/solarized/
- https://www.atlassian.com/git/tutorials/dotfiles
- https://protesilaos.com/codelog/2019-06-24-lemonbar-xft-howto/
- https://chrisyeh96.github.io/2020/03/28/terminal-colors.html
- https://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html