Setting up a Mac Laptop for a Software Engineer Coming from a Linux Desktop

I’ve recently started a new job and am setting up a MacBook Pro M4. I’ve been using either a native Linux Desktop, or a Linux Desktop as a VM on a Windows box for most of my career. There are a number things about the Mac that are different than Intel based machines running Linux following are the list of changes that I have made to make it usable as a development environment.

Show all files in an “Open” dialog box

Press Cmd + Shift + .

Set system time to UTC

sudo ln -sf /usr/share/zoneinfo/UTC /etc/localtime

To display time in seconds go to System Settings > Menu Bar > Clock and select “Display the time with seconds”

If you also want to display a clock that shows your local time install Clocker.

Disable the Mission Control space-switching hotkeys

These four shortcuts are what stop Ctrl+Left and Ctrl+Right ever reaching an application.

┌─────┬──────────────────┬────────────────────┐
│ ID  │     Shortcut     │    macOS action    │
├─────┼──────────────────┼────────────────────┤
│ 79  │ Ctrl+Left        │ Move left a space  │
├─────┼──────────────────┼────────────────────┤
│ 80  │ Ctrl+Shift+Left  │ Move left a space  │
├─────┼──────────────────┼────────────────────┤
│ 81  │ Ctrl+Right       │ Move right a space │
├─────┼──────────────────┼────────────────────┤
│ 82  │ Ctrl+Shift+Right │ Move right a space │
└─────┴──────────────────┴────────────────────┘

# Run the following in a terminal:

for id in 36 37 79 80 81 82; do
  defaults write com.apple.symbolichotkeys AppleSymbolicHotKeys -dict-add "$id" '<dict><key>enabled</key><false/></dict>'
done

/System/Library/PrivateFrameworks/SystemAdministration.framework/Resources/activateSettings -u

# Run the following to verify it
defaults read com.apple.symbolichotkeys AppleSymbolicHotKeys | grep -A2 -E '^\s+(36|37|79|80|81|82) ='

# The previous command should output the following:
    36 =     {
        enabled = 0;
    };
    37 =     {
        enabled = 0;
    };
    79 =     {
        enabled = 0;
    };
    80 =     {
        enabled = 0;
    };
    81 =     {
        enabled = 0;
    };
    82 =     {
        enabled = 0;
    };

Disable the “Press and Hold” Feature

By default, holding down a given key will display a pop-up/tooltip that displays accented version of the given character. I use a Vim plugin with the IDEs that I use and when trying to navigate by holding down either the w or b chars the cursor only moves by one work and then shows an accented character. To disable it

  1. Run the following command in a terminal and then restarting applications. This turns off the feature that will show accented characters: defaults write -g ApplePressAndHoldEnabled -bool false
  2. Turn up the Key Repeat rate by going to System Setting > Keyboard and adjust the slider for the “Key repeat rate”. If this still feels too slow you can crank it up with the following terminal settings which enable values much lower than the UI allows to be set
# Sets a very short delay before repeating
defaults write -g InitialKeyRepeat -int 15 

# Sets a very fast repeat rate
defaults write -g KeyRepeat -int 2

Configuring Home and End Keys

This is configuration for Safari, Mail, Notes and most other native, Cocoa based applications.

  1. Create the following directory: mkdir -p ~/Library/KeyBindings
  2. Create and edit the binding file vi ~/Library/KeyBindings/DefaultKeyBinding.dict
  3. Paste the following content into it
{
    "\UF729"   = "moveToBeginningOfLine:";
    "\UF72B"   = "moveToEndOfLine:";
    "$\UF729"  = "moveToBeginningOfLineAndModifySelection:";
    "$\UF72B"  = "moveToEndOfLineAndModifySelection:";

    "^\UF702"  = "moveWordLeft:";
    "^\UF703"  = "moveWordRight:";
    "^$\UF702" = "moveWordLeftAndModifySelection:";
    "^$\UF703" = "moveWordRightAndModifySelection:";

    "^\U007F"  = "deleteWordBackward:";
    "^\UF728"  = "deleteWordForward:";
}

Then save and close that file and restart applications and/or restart the Mac.

iTerm2 Specific Configurations

Configure Home and End keys to work properly

  1. Go to Settings > Profiles > Keys > Key Bindings and click the + at the bottom left.
  2. For the Home Key:
    • Keyboard Shortcut: Press your physical Home key.
    • Action: Select Send Hex Code.
    • Code: 0x01 (This is the hex for Ctrl+A).
  3. For the End Key:
    • Keyboard Shortcut: Press your physical End key.
    • Action: Select Send Hex Code.
    • Code: 0x05 (This is the hex for Ctrl+E).

Using ghostty

I am giving ghostty a try as it plays nice with herdr. Following are my configurations for it.

This is dependent on disabling the Mission Control hotkeys as described above.

~/.zshrc Entries

# Word motion and word delete on the Ctrl keys, the Linux terminal
# convention. zsh ships no binding for any of these sequences, so the keys
# do nothing until bound here. ^[[1;5D is Ctrl+Left, ^[[1;5C is Ctrl+Right
# and ^[[3;5~ is Ctrl+Delete.
bindkey "^[[1;5D" backward-word
bindkey "^[[1;5C" forward-word
bindkey "^[[3;5~" kill-word

ghostty config file

Write the following config into ~/.config/ghostty/config and restart ghostty

# Home and End emit ESC[H and ESC[F by default. Send the control characters
# that readline and the zsh line editor act on directly instead: 0x01 is
# Ctrl-A, beginning of line, and 0x05 is Ctrl-E, end of line.
keybind = home=text:\x01
keybind = end=text:\x05

# Alt plus a delete key removes a whole word. ESC d is the readline
# kill-word function, which deletes forward from the cursor. 0x1b 0x7f is ESC
# followed by DEL, which readline binds to backward-kill-word.
keybind = alt+delete=esc:d
keybind = alt+backspace=text:\x1b\x7f

# Let these Cmd keys reach Herdr and Neovim.
keybind = super+t=unbind
keybind = super+w=unbind
keybind = super+k=unbind
keybind = super+e=unbind
keybind = super+shift+g=unbind

# Look and feel
background = #14151c
foreground = #FDFDFD
font-size = 14
cursor-style = bar
cursor-style-blink = false
scrollback-limit = 100000
# Option sends Alt rather than composing a Unicode character, so Alt+B and
# Alt+F reach the shell as ESC b and ESC f. Readline binds those to
# backward-word and forward-word.
macos-option-as-alt = true
palette = 0=#282A2B
palette = 1=#F3977F
palette = 2=#5ADECD
palette = 3=#F272A2
palette = 4=#886F97
palette = 5=#C574DD
palette = 6=#79E6F3
palette = 7=#FDFDFD
palette = 8=#415844
palette = 9=#FF4971
palette = 10=#18E3C8
palette = 11=#FF8037
palette = 12=#556FFF
palette = 13=#B0D143
palette = 14=#3FDCEE
palette = 15=#BEBEC1

# Draw the line between splits in a color that stands off the background,
# rather than the near-background default that is hard to pick out.
split-divider-color = #3f435e

# Fade inactive splits harder than the 0.7 default, so the focused split is
# obvious at a glance.
unfocused-split-opacity = 0.5

# Widen the gap between the text and the split edge, so each pane reads as
# its own block instead of text running up against the divider.
window-padding-x = 6
window-padding-y = 4

# ---------------------------------------------------------------------------
# Linux terminal key conventions.
#
# Linux terminals keep bare Ctrl for control codes and put application
# shortcuts on Ctrl+Shift. The Cmd bindings Ghostty ships stay in place, so
# both conventions work.
# ---------------------------------------------------------------------------

# Clipboard. Bare Ctrl+C has to stay SIGINT, which is why Linux puts copy on
# Ctrl+Shift+C.
keybind = ctrl+shift+c=copy_to_clipboard
keybind = ctrl+shift+v=paste_from_clipboard
keybind = ctrl+shift+a=select_all

# Tabs and windows.
keybind = ctrl+shift+t=new_tab
keybind = ctrl+shift+n=new_window
keybind = ctrl+shift+w=close_surface
keybind = ctrl+shift+q=quit

# Tab cycling. Ctrl plus a page key moves between tabs. Adding Shift moves the
# current tab itself along the tab bar.
keybind = ctrl+page_up=previous_tab
keybind = ctrl+page_down=next_tab
keybind = ctrl+shift+page_up=move_tab:-1
keybind = ctrl+shift+page_down=move_tab:1

# Find. Ctrl+Shift+G is find-next and Ctrl+Shift+H is find-previous. These also
# restore search, which the super+e and super+shift+g unbinds above gave up.
keybind = ctrl+shift+f=start_search
keybind = ctrl+shift+g=navigate_search:next
keybind = ctrl+shift+h=navigate_search:previous

# Font size. Ctrl+= and Ctrl++ both grow the font, because the plus sign needs
# Shift on a US layout and either form can arrive here.
keybind = ctrl+==increase_font_size:1
keybind = ctrl++=increase_font_size:1
keybind = ctrl+-=decrease_font_size:1
keybind = ctrl+0=reset_font_size

# Scrollback. Ghostty binds Shift plus a page key to extend the selection.
# Linux terminals scroll with them instead.
keybind = shift+page_up=scroll_page_up
keybind = shift+page_down=scroll_page_down

keybind = f11=toggle_fullscreen

# Splits, following Terminator. Ctrl+Shift+E splits side by side and
# Ctrl+Shift+O stacks one split above the other.
keybind = ctrl+shift+e=new_split:right
keybind = ctrl+shift+o=new_split:down

# Ctrl+Backspace has no standard terminal sequence, unlike Ctrl+Left,
# Ctrl+Right and Ctrl+Delete, which .zshrc binds by their sequences. Emit
# ESC DEL, which zsh already binds to backward-kill-word.
keybind = ctrl+backspace=text:\x1b\x7f

# Move between splits with Alt and an arrow. This replaces Ghostty's default
# alt+arrow_left and alt+arrow_right, which sent ESC b and ESC f for word
# motion. Word motion stays on Alt+B and Alt+F, which zsh binds natively, and
# on the Ctrl arrows bound in .zshrc.
keybind = alt+arrow_left=goto_split:left
keybind = alt+arrow_right=goto_split:right
keybind = alt+arrow_up=goto_split:up
keybind = alt+arrow_down=goto_split:down

# Highlight to copy, middle-click to paste, the X11 primary selection
# workflow. macOS has no separate selection clipboard, so both ends route
# through the system clipboard, and highlighting text overwrites whatever was
# last copied. The primary-paste and primary values these replace are
# documented as having no effect on macOS.
copy-on-select = clipboard
middle-click-action = clipboard-paste

# Advertise a terminal type every remote host already knows. Ghostty's own
# xterm-ghostty record exists only inside the app bundle, at
# Contents/Resources/terminfo, and ssh forwards TERM but not TERMINFO. A
# remote shell given xterm-ghostty therefore finds no record and loses arrow
# keys, color and the alternate screen. The xterm prefix is still required,
# because vim keys off it to enable modifyOtherKeys.
term = xterm-256color

Configure F-keys to work without pressing fn

  1. Open System Setting > Keyboard and click on Keyboard shortcuts
  2. Click on Function Keys in the left-hand nav
  3. Toggle the Use F1, F2, etc. keys as standard function keys

Getting Alt-Tab to Switch Applications

Download and install the AltTab application.

Configuring Primary Selection and Middle Click Paste

One of the things that I use all of the time under Linux is to select text and then middle-click to paste it. This is one of those Linux features that once you get used to, you just have to have.

Configure iTerm2

  1. Open iTerm2 settings
  2. Go to General > Selections
  3. Check the box “Applications in terminal may access clipboard”
  4. Check the box “Copy to clipboard on selection”
  5. Go to Pointer > Bindings
  6. If “Middle Button” is not there, click the “+” button at the bottom of the window
  7. Set the following
    • Button/Gesture: Middle Button
    • Click Type: Single Click
    • Action: Paste from Selection…: There are a whole host of additional options from there that you can choose from. I left the default settings.

Screenshots

Configure the OS to write screenshots to a specific directory

# Set the new location
mkdir -p $HOME/Pictures/Screenshots
defaults write com.apple.screencapture location $HOME/Pictures/Screenshots

# Apply the changes (restarts the UI server)
killall SystemUIServer

Additional brew Packages

  • watch

Helpful Keyboard Shortcuts

  • Enter a path to a file in a dialog box: By default, Mac makes you click everything. Many times you know exactly where the file is on the filesystem and you just want to type it in and hit Enter. To do so, you need to open the dialog box and then press Command + Shift + G and then you can enter the path to the file.
  • View hidden files and directories in a dialog box: By default, Mac always hides “dotfiles” and directories in a dialog box. To see them open the dialog box and then press Command + Shift + Period.

Development SDKs

Installing Go

Initially, I just went with brew install go, but this turned into a big mess. While working on a project where I was writing a K8s Custom Resource Definition I ran into a buzz saw of mismatched versions and edge cases with the Apple Silicon/ARM architectural problems with the go development toolchain.

Instead of using brew for the go SDK

  1. Download the .tar.gz file from https://go.dev/dl/ for your architecture
  2. Unpack it, rename the go directory go1.x.x and move the directory to /usr/local/
  3. Create a symlink, /usr/local/go to point to the version specific go directory
  4. Ensure that /usr/local/go/bin is in your PATH
  5. Install any other required go libraries with go install or by downloading the binaries and putting them in your $GOPATH
    • go install golang.org/x/tools/gopls@latest
    • go install github.com/go-delve/delve/cmd/dlv@latest

How to Install Windows 11 on a Dell Optiplex 990 (and perhaps other older hardware)

To perform a clean install

  1. Configure the BIOS
    • Set boot mode to UEFI
    • Set SATA configuration to AHCI
    • Enable Secure Boot/TPM
  2. Prepare a USB drive
    • Use Ventoy to create a USB drive
    • Copy the win11 ISO to the data partition of the drive
  3. Wipe the drive during the install process
    • Boot from the USB
    • When you see the “Where do you want to install Windows?” screen press Shift + F10 to open a command prompt
    • Then type the following commands, in this order
      • diskpart
      • list disk and choose the drive onto which you want to install. Most likely Disk 0
      • clean, this will remove all data from the drive
      • convert gpt, this is the command that will make the drive UEFI-compatible
      • exit
      • exit
  4. Continue with the installation. When it asks you where to install, select the drive you just wiped. It should list a large “Unallocated Space”.
  5. Select it and click Next
  6. It will then show a blue screen with “n% installing”
  7. Keep an eye on it and when it reboots, remove the installation USB
  8. You should see a “Getting Ready”, or “Installing 0%, Please keep your computer on” message. If you get there you on your way to installing Win 11 on your computer.

Mount External, Encrypted, LMV drives

First decrypt the drive. When I plugged it in GNOME displayed a dialog and asked for the key. I’m guessing that cryptsetup luksOpen /dev/device device_crypt would work as well.

Run lvdisplay to get the LV Path for the logical volume that you want to mount.

Then, activate the volumes , there is a way to activate a specific one volume, but the following activates them all.

vgchange -ay

Then mount it

mount <lv-path> /mnt

PDF Commands Cheat Sheet

Following are a number of common PDF processing commands on a Linux box

Convert a series of images to a PDF

convert image-1 image-2 ... images.pdf

convert-im6.q16: attempt to perform an operation not allowed by the security policy `PDF’ @ error/constitute.c/IsCoderAuthorized/426.

ImageMagick, specifically version 6 (im6), is preventing the conversion of PDF files due to a security policy. To address this

  1. As root, edit /etc/ImageMagick-6/policy.xml
  2. Search for the line <policy domain="coder" rights="none" pattern="PDF" />
  3. Update rights="none" to rights="read|write"
  4. Save the xml file

Debian Package Management Cheat Sheet

  • List version of an installed package: dpkg -s docker-ce | grep Version
  • Show all available versions of a package: apt-cache madison <package>

Packages being held back because of phasing

Run the following command replacing the package in question

apt-get --with-new-pkgs upgrade <package-name>

The following packages have been kept back

Try the following, in order. Each is less risky than the next. Stop as soon as you have fixed the problem.

Solution 1

apt-get --with-new-pkgs upgrade <list of packages kept back>

Solution 2

apt-get install <list of packages kept back>

Failed to fetch . . . Hash Sum mismatch

If you see an error similar to the following

Reading package lists... Done
E: Failed to fetch https://packages.cloud.google.com/apt/dists/cloud-sdk/main/binary-all/Packages  Hash Sum mismatch
   Hashes of expected file:
    - Filesize:1677314 [weak]
    - SHA256:09c3377dd2740e63c831dea4a86ccb1253ab6a41ec71eaa1cd4c7e52ee8c9255
    - MD5Sum:16ab83da58155f40c9a5a18a2a8c6587 [weak]
   Hashes of received file:
    - SHA256:ee6eabdfda398ab36e6fa8cd777d5d02b45dbac892dacdf6f1be1ca5ad0cbb65
    - MD5Sum:6dd0c5ac95bc55b7864e66986b71b8fa [weak]
    - Filesize:1677314 [weak]
   Last modification reported: Fri, 04 Apr 2025 01:54:07 +0000
   Release file created at: Tue, 01 Apr 2025 12:57:58 +0000
E: Failed to fetch https://packages.cloud.google.com/apt/dists/cloud-sdk/main/binary-amd64/Packages
E: Some index files failed to download. They have been ignored, or old ones used instead.

Try removing all of the apt list data

rm -rf /var/lib/apt/lists/* && apt-get update

How to partially cherry-pick a commit in git

There are times when you need to cherry-pick a commit from another branch. Then there are times when you only need parts of that commit.

Following is how you can partly cherry-pick a commit to get only the changes that you need.

Get the patch for the entire commit. Include the -n for no commit so that it does not add it as a commit to your branch

git cherry-pick -n <commit>

Then unstage the the changes from the cherry-picked commit so that you can choose which lines/hunks that you want to include

git reset

From here you can either git add -p to specify specific files or use git gui or some other tool to selectively stage and commit lines or hunks of the current set of changes.

Then just use git commit to commit your changes and reset the rest that you do not want and you are done.

Go template that properly renders delimiters for a slice of structs

It is common to write go text/templates that range over a slice of objects to write out a list or array of items that are separated by some delimiter defined in the template.

In the case of a JSON array of objects, the cleanest output would be a , separating each object without a leading or trailing comma.

Because go template if statements are falsey in that a 0 value will evaluate to false you can write a template as follows and it will only render a comma between items as it ranges over the slice

{
  "items":[
  {{- range $index, $item := .Items -}}
    {{- if $index -}},{{ end }}
    {
      "id": {{ $item.Id }}, "name": {{ $item.Name }}
    }
    {{- end }}
  ]
}

Click here for a working example.

How to set up a locally hosted git server and create new repositories

You may have code and configurations that are required to stay on premise. As a result, you will need to setup your own git server and create and manage repositories locally. Following is an overview of how to set that up.

We will leave aside server setup, configuration, and networking and assume that we have a machine on which we will host the repos, git-server, and machines that will clone, pull, and push updates, clients.

Setting up the git-server

On the git-server add git-shell to the list of valid shells.

echo $(which git-shell) >> /etc/shells

Add a git user with a specific shell and setup the directory in which all of the repos will be stored.

useradd --shell $(which git-shell) --home-dir /home/git git
mkdir -p /home/git/.ssh
chown -R git: /home/git/.ssh
touch /home/git/.ssh/authorized_keys
chown -R git: /home/git/.ssh
chmod 600 /home/git/.ssh/authorized_keys
chmod 700 /home/git/.ssh
mkdir -p /var/lib/git_repo
chown git: /var/lib/git_repo
chmod 700 /var/lib/git_repo

Adding a new user

To add a new user that can commit to and pull repos via ssh concatenate their public ssh key to /home/git/.ssh/authorized_keys.

Creating a new repository

As root on the git-server

export GIT_REPO=<new-repo-name>.git
sudo -u git mkdir /var/lib/git_repo/$GIT_REPO
cd /var/lib/git_repo/$GIT_REPO && sudo -u git git init --bare

As an authorized git user on the client machine

mkdir example && cd example
git init
echo "# README" >> README.md
git add README.md
git commit -m 'Initial commit'
git remote add origin git@hpdl01:/var/lib/git_repo/<new-repo-name>.git
git push origin master

Now you can clone with

git clone git@hpdl01:/var/lib/git_repo/example.git