Today we are going to learn how we can automatize all our macOS settings, apps and homebrew packages using bash and stow (and a few other cli tools).
Intro
As developers we might encounter ourselves setting up a new macOS environment quite often (I had to do it 2 times so far this year). We tend to accumulate dotfiles pretty easily and keeping them in sync across multiple machines becomes painful.
And not only that. We also accumulate a galaxy of different packages, tools and apps that we use on daily bases to work.
Oh and all the macOS settings that we surely need? our poor souls.
In order to reduce the pain and enjoy life we can make use of different tools and our coding skills to automatize all this process.
The objective
When I receive a new Macbook at a new job, all I want to do is:
Login in the app store
Download and run a magic script
Go out and enjoy the sun while the macbook is being set up
Bootstraping
So first we need a few tools to start setting up everything. As we already know xcode, brew and git are a must. Here we start with:
OK. But some of my apps are not available in hombrew 😡
Installing App Store apps
For all the apps that cannot be found as brew casks we can use mas (Mac App Store cli).
This cli tool needs the IDs of the apps. To check the apps you have installed simply run:
mas list
If not all your apps are listed you can also search for them and get their IDs:
mas search Goodnotes
So again, writing some pretty code:
masApps=("937984704"# Amphetamine"1444383602"# Good Notes 5"768053424"# Gappling (svg viewer))
install_masApps(){
info "Installing App Store apps..."for app in$masApps;do
mas install$appdone}
Neat!
So far we have installed all our apps and cli tools.
Let's see how we can automatize our macOS settings.
defaults: macOS settings from the terminal
This tool will help us to read and write settings. For that, you need to know the domain, key and type of the setting. Usually a google/duckduckgo/ecosia search should help you to find this information. If not, defaults provides some commands to help you search for the settings.
Let's say I want to change some finder setting. First, find its domain:
defaults domains | grep finder
Read all the settings available for finder
defaults read com.apple.finder
I want to change ShowExternalHardDrivesOnDesktop but I don't know its type
(You can see all my settings in my dotfiles repo,link below)
Lastly, and this is a personal preference, I want to set vscode as default application for all my source code files. To do so we will use duti.
code_as_default_text_editor(){local extensions=(".c"".cpp"".js"".jsx"".ts"".tsx"".json"".md"".sql"".html"".css"".scss"".sass"".py"".sum"".rs"".go"".sh"".log"".toml"".yml"".yaml""public.plain-text""public.unix-executable""public.data")for ext in$extensions;do
duti -s com.microsoft.VSCode $ext all
done}
Settings: check.
Managing dotfiles with GNU Stow
stow is simply a symlink manager that let us define a folder structure (package) for our dotfiles and then symlink them following that structure. So let's say one of your dotfiles is ~/.config/kitty/kitty.conf, this will be our kittypackage. In order to place this file in the correct path, we define the package as as
dotfiles/kitty/.config/kitty/kitty.conf
(assuming all our dotfiles are in a folder called dotfiles)
We can simulate stow to see the resulting symlinks
cd dotfiles
stow -nSv kitty
If the output looks correct, we symlink the package with
stow --target$HOME kitty
Note: we want to use stow only for files, since a folder like .config will always contain many other files/folders, we don't want to symlink this folder. Therefor we need to first remove existing files (the ones we want to stow) and check that the needed directories exist. So, some lovely code for that:
stow_dotfiles(){local files=(".zprofile"".gitconfig"".zshrc"".vimrc")local folders=(".config/nvim"".config/kitty")
info "Removing existing config files"for f in$files;do
rm-f"$HOME/$f"||true
done
for d in$folders;do
rm-rf"$HOME/$d"||true
mkdir-p"$HOME/$d"done
local dotfiles="kitty nvim"
info "Stowing: $dotfiles"
stow -d stow --verbose 1 --target$HOME$dotfiles}
So far so good, all our dotfiles are versioned and any change on them can be pushed and synced in all our macOS machines with git.
What else did I want to do?
...
Setting up Oh My Zsh
I use oh my zsh along with powerlevel10k and this is how my terminal looks
pretty, right?
So I need to install this scripts on my system, for which I have more lovely code:
install_oh_my_zsh(){if[[!-f ~/.zshrc ]];then
info "Installing oh my zsh..."ZSH=~/.oh-my-zsh ZSH_DISABLE_COMPFIX=true sh -c"$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"chmod 744 ~/.oh-my-zsh/oh-my-zsh.sh
info "Installing powerlevel10k"
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ~/.oh-my-zsh/custom/themes/powerlevel10k
else
warn "oh-my-zsh already installed"fi}
Note: my powerlevel10k configuration is part of my dotfiles. If you don't have any, you can run p10k configure and follow the configuration process.
Installing (Neo)Vim plugins
Lastly, I also want to automatically install all my neovim plugins. First I need to install the plugin manager (I use vim-plug) and then I want to install all the plugins defined on my dotfiles
install_neovim(){
info "Installing NeoVim"
install_brew_formulas neovim
info "Installing Vim Plugged"
sh -c'curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'}# Install plugins and quit
nvim +PlugInstall +qall
Eureka. My dev environment is (almost) fully automated. So coming back to the objectives, this is the magic script:
My automatic dev machine setup scripts and configs
dotfiles
Set up dev environment in a macOS machine.
This script installs all the packages and apps I use, stow my dotfiles and sets all my preffered macOS configurations.
Note: I have added a lot of read -p on my script to debug each section but they can all be removed so there is no need for interaction. Prompting for the sudo password could be than an enable it for the rest of command.
What I'm missing?
configuration for different apps like clipy. defaults show some configuration but not all of them.
some macOS config like disable autocapitalization don't seem to be available through defaults
I'd love to hear some ideas to improve this current setup!
Currently I'm checking ansible so stay tuned for a possible upgrade.