alecgerona.io
Article photo
Love it or hate it: here's how I setup my Mac
October 07, 20219 min read

Updates

17 Apr 2023: Use starship prompt

15 Aug 2022: Update the fzf setup command

Rationale

Gotta be honest. For the longest time, I’ve avoided the Apple ecosystem. I’m still not that okay with it, but we have to try ‘em all, right? What with overheating MacBook Pros, to class action lawsuits over cracked M1 screens, there remains a lot to be desired in the so-called walled garden.

Setup

With that out of the way, this guide is how I set up my development environment on a MacBook Pro 2019 16in the way I want it, so I’d have something to reference should I need to do it all over again. Hope someone else finds it useful as well.

1. Xcode

Let’s start simple by installing Apple’s command line tools. Basically, things you need to get any sort of development work on your machine.

xcode-select --install

2. iTerm2

I believe it’s already tradition by now (for developers at least) to immediately replace macOS’s default terminal with iTerm2. Some would say it’s a little bloated for what you use it for, but you’re using a Mac already, so you shouldn’t complain.

Install

Installation is as simple as downloading the .dmg and installing through that.

Key repeat rate

A severely underrated QoL change, this set up requires a restart after execution, but you’d be able to glide on your keyboard once your key repeat rate is up. A boon to Vim users, terminal junkies, and fast typists alike.

defaults write NSGlobalDomain KeyRepeat -int 1
defaults write NSGlobalDomain InitialKeyRepeat -int 10
defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool false

Skin

For my terminal I’m using iterm2-snazzy. Just download the page and import in iTerm2 (Preferences > Profiles > Colors > Color Presets > Import).

neofetch

Hotkey summon

Coming from yakuake and guake, the ability to summon the terminal from any desktop with just a hotkey is a godsend. Here’s how to do it with iTerm2.

Go to Preferences > Keys > Hotkey > Create dedicated Hotkey window. This will create a dedicated profile that iTerm2 will use when you summon it this way. You can then edit this profile as you wish.

My hotkey is ⌘+`

I set my transparency to 10% and window size to maximized.

Shell Integration

This is a utility that “keep track of your command history, current working directory, host name, and more—even over ssh.”

This will be useful for the following feature of iTerm2.

SSH workflow

Traditionally, we SSH to other machines by writing the full command such as ssh user@host. We then learn to alias this by using the .ssh/config file and writing ssh alias instead. We can go much further in iTerm2 by not even needing to type anything. Simply make a profile and make it run the ssh command you need.

For my setup I still prefer working mostly in my keyboard and having to open the profile from the tab is just extra overhead that I want to avoid. That said, there is still an advantage to using profiles and that is to visibly notify you that you are working on a different terminal by either having a badge or a different theme or both.

For production terminals I use the red alert theme. I also use the instance name as its badge.

Finally, since I prefer having to type the ssh command myself, I have to set the profile up so it would switch profiles once it detects that I’ve changed hosts. To do that, we need to implement Automatic Profile Switching.

This iTerm2 feature requires Shell Integration which we did above. We then need to add the expected user/host entry iTerm2 needs to switch profiles.

It’ll be something like this. It should end in a glob (*) or it won’t work.

user@host*

3. Homebrew

Now that we have our initial cli tools and a spiffy terminal emulator to work in, we need one more tool to be fully integrated in the macOS (sorta open source) ecosystem—homebrew, the package manager that, like most things in a Pro Apple product, never had built in.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Afterwards you get access to package manager staples such as brew install <package-name> among others.

4. ZSH

zsh is the default (and better) shell in macOS. Aside from its benefits over bash, we’d be making it even better with some choice improvements.

starship

Before anything else, we need to de-clutter our terminal prompt. What better way to do so than by installing starship?

brew install starship

This provides a minimalist yet information-filled prompt that changes with your context.

Make sure that your iTerm2’s font has an alternative option for non-Ascii characters since Monaco (Apple’s default) does not support powerline characters that spaceship needs. I chose to go with Fira code. Just download and install.

zsh-autosuggest

Wouldn’t it be great if we had autosuggest? This may be the single most useful plugin for zsh. If you’d only install one plugin for zsh, this should be it. This allows zsh to suggest commands as you type them based on your history.

Create a ~/.zshrc.local if you haven’t already. Since by default most general-purpose settings will be in .zshrc, we’ll be using the local file for our own custom settings. Let’s also create a ~/.zsh directory. This is where zsh plugins are commonly installed by default. I prefer manually installing plugins manually (as opposed to using zsh managers such as Oh My Zsh since I think it’s easier to maintain that way) so we’re gonna clone the autosuggest repository ourselves.

git clone https://github.com/zsh-users/zsh-autosuggestions ~/.zsh/zsh-autosuggestions

Then add the following line to the top of your .zshrc.local file.

source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh

Restart your terminal for it to take effect.

zsh-syntax-highlighting

Next is syntax highlighting. A minor QoL sure, but still important for those long-winded command strings.

Installation is pretty much similar.

git clone https://github.com/zsh-users/zsh-syntax-highlighting ~/.zsh/zsh-syntax-highlighting

Add the following line near the top of your .zshrc.local.

source ~/.zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

As usual, restart your terminal for it to take effect.

Vim mode+

Since I’m a Vim user (and disciple, please use Vim), I tend to want to use Vim everywhere I can get it to work. And that means here in the terminal.

Add these in .zshrc.local

##### Enable zsh vim mode
bindkey -v

##### Change cursor shape for different vi modes.
function zle-keymap-select {
  if [[ ${KEYMAP} == vicmd ]] ||
     [[ $1 = 'block' ]]; then
    echo -ne '\e[1 q'
  elif [[ ${KEYMAP} == main ]] ||
       [[ ${KEYMAP} == viins ]] ||
       [[ ${KEYMAP} = '' ]] ||
       [[ $1 = 'beam' ]]; then
    echo -ne '\e[5 q'
  fi
}
zle -N zle-keymap-select
zle-line-init() {
    zle -K viins # initiate `vi insert` as keymap (can be removed if `bindkey -V` has been set elsewhere)
    echo -ne "\e[5 q"
}
zle -N zle-line-init
echo -ne '\e[5 q' # Use beam shape cursor on startup.
preexec() { echo -ne '\e[5 q' ;} # Use beam shape cursor for each new prompt.

##### Use vim keys traversing history the smart way:
autoload -U history-search-end
zle -N history-beginning-search-backward-end history-search-end
zle -N history-beginning-search-forward-end history-search-end
bindkey "^[[A" history-beginning-search-backward-end
bindkey "^[[B" history-beginning-search-forward-end
bindkey -M vicmd 'k' history-beginning-search-backward-end
bindkey -M vicmd 'j' history-beginning-search-forward-end

A bit of a mouthful I know but bear with me.

As commented, these changes allow you to use Vim in the terminal, change the cursor shape in your terminal as you switch modes, and finally (a personal favorite) allow the down(j) and up(k) keys on Vim to search your history based on your already typed command!

This means that if you typed git and went to command mode and pressed k, this will show the previous command you started with git as well! Personally I use this feature just about every day.

Again, restart your terminal for the changes to take effect.

fzf

We’re still not done with shell goodness with fzf or fuzzy finder. This little tool allows you to zip around your terminal whether you need to quickly change directories or open a file you simply couldn’t remember the full path/name of. Just fuzzy find it!

First of, install it.

brew install fzf
$(brew --prefix)/opt/fzf/install

This allows usage of the fzf cli command and enables a bunch of commands out of the gate.

Ctrl + T allows you to search for any file and selecting one returns it in your buffer.

Opt + C allows you to search for any directory and selecting one moves you to it.

By the way, to actually use Opt as a modifier instead of inserting special characters, go to iTerm2’s Preferences -> Profiles -> Profile -> Keys and set your Option key to Esc+.

These alone however are not enough to maximize your productivity with fzf. By default, fzf uses GNU find to execute its searches. Albeit well-featured, it’s not fast. To effectively fuzzy find, fzf will have to crawl over a lot of files to get you the most accurate match. To achieve this, you need to give it a helping hand (or two).

First, use ripgrep. Arguably the fastest cli search tool out there, this makes it the best default go-to command for fzf. To do this, simply set the following environment variable in your .zshrc.local.

export FZF_DEFAULT_COMMAND='rg --files --hidden --follow -g "!{.git,node_modules}/*" 2> /dev/null'
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"

These let fzf know to use rg as its default search tool. Next is to exclude certain paths from search. Basically paths where you know you won’t deal with in the terminal yourself. Examples of this are the /Library and /Pictures directories.

These come with macOS with its implementation of iCloud and certain apps. If you don’t exclude these, fzf will have to go through a lot of files to even get to the relevant ones (probably the one in your ~ directory). So if you do need to access those files, skip this step.

Create a .rgignore file. Any files/directories in this file will be ignored by ripgrep.

Sample file:

/Library
/Pictures

More experienced users might note that rg won’t work for directories and you’re right. This is well documented in ripgrep#169 and ripgrep#388.

So if we can’t use ripgrep and by default fzf’s change directory feature is still using find that doesn’t ignore directories, we can’t keep waiting for it to index all those files/directories all day now can we?

We can’t. So we need another tool. Namely, fd. Touted as the simple and more user-friendly alternative to find, fd has exactly the feature we need: an ignore file.

brew install fd

So much like before, we set the environment variable:

export FZF_ALT_C_COMMAND="fd --type d"

Then we create a .fdignore file that has the same contents as .rgignore. And we’re done.

Okay we’re halfway done. The following steps for this guide are specific to the tech stack I commonly use.

5. Node

As one of the most common developer tools, there are a lot of ways to install this. My favorite is nvm. This allows you to have multiple node versions in your machine and can easily switch between them with just a simple command.

Let’s get started.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash

Afterwards, make sure the following snippet is in your .zshrc file.

export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

Restart your terminal and execute command -v nvm. This should output “nvm” to tell you it’s working.

Running nvm install node will install the latest version and set it as your default Node version.

6. Python

macOS should already come packaged with Python by default. But any Python dev can tell you that it’s a bad idea to have all of your projects depend on a single version of Python, let alone the system-installed one.

So how do we fix this? We use a tool much like nvm—pyenv.

brew install pyenv pyenv-virtualenv

Make sure that the following line is in your .zshrc.

eval "$(pyenv init -)"

Restart your terminal and pyenv should be working.

Usage

Start with setting the default python version you want to use (which is not the system-installed one).

##### As of the time of writing, this is the latest I want my system python to run on
pyenv install 3.9.7
pyenv global 3.9.7

Running python in your terminal should use the version you’ve set as global.

Now say you’ve got a project that runs on Python 3.8.12 that as the experienced Python developer that you are, need to use a virtualenv on?

##### Install the needed Python version first
pyenv install 3.8.12 # 1
cd my-python-project # 2
pyenv virtualenv 3.8.12 my-python-project # 3
pyenv local my-python-project # 4

So what exactly happens after you run these series of commands? 1 and 2 are pretty self-explanatory. 3 just creates the virtualenv with the name you specified (by convention it shares the name of the project) This is possible since we installed pyenv-virtualenv along with pyenv using Homebrew. And 4 creates a .python-version file that has the name of the virtualenv you want to be activated as soon as you cd into that directory. Neat, huh?

8. Docker

Last, but not least, is Docker. Thankfully this is the easiest to install as it’s literally just going to Docker for Desktop’s website and just downloading and installing. Once it’s good to go it gives you a UI that is sort of Portainer-lite that has brief information regarding the logged in Docker user, running containers, and downloaded images.

Along with this of course is the Docker runtime that lets you run the docker command on your terminal.

And with that, we finally come to the end of this so-called 12-minute read which took anything but to write. I hope this helps you and my future self in setting up your development environment, be it in macOS or Linux, the way you want it.

As long as it uses Vim, you’re cool.

Questions? Typos? Violent reactions? Help me improve this post.
© 2024, Alexandre Gerona