Stefan Scherfke

The macOS Dark Mode, your Terminal and Vim

The new Dark Mode in macOS Mojave is a nice addition and is – especially in the night hours — more pleasing to your eyes than the light mode.

MacOS light mode with a light Terminal profile and a light Vim theme.
MacOS light mode with a light Terminal profile and a light Vim theme.

However, enabling Dark Mode will not change the Terminal profile, which is a little bit annoying – especially if your color theme has a light and a dark variant (like the infamous Solarized, Snow, One, or my own Rasta theme).

MacOS dark mode with a light Terminal profile and a light Vim theme.
MacOS dark mode with a light Terminal profile and a light Vim theme.

If you change your Terminal profile to something dark, Vim still doesn’t look right because it uses its own mechanism for light/dark backgrounds (see :help 'background' for details) and doesn’t know about the changes you made to the Terminal profile.

MacOS dark mode with a dark Terminal profile and a light Vim theme.
MacOS dark mode with a dark Terminal profile and a light Vim theme.

If you execute :set background=dark in Vim (and if you color scheme supports it), Vim looks nice and dark now, too.

MacOS dark mode with a dark Terminal profile and a dark Vim theme.
MacOS dark mode with a dark Terminal profile and a dark Vim theme.

However, on the next day, the fun begins again when you want to switch everything back to light mode …

Wouldn’t it be nice if this could all be accomplished with a single command?

There are tools, that help you with switching to/from macOS Dark Mode (e.g., NightOwl or Shifty), but they can’t change your Terminal profile or notify Vim.

As it turns out, it’s not too hard to implement a little program that does exactly this:

  • You can uses the defaults command to get the current macOS Dark Theme mode:

    $ defaults read -g AppleInterfaceStyle
    Dark
    
  • You can use AppleScript (oh, how I love this language …) to set Dark Mode and update the Terminal profile:

    # Set Dark Mode
    tell application "System Events"
        tell appearance preferences
            set dark mode to true  # Can be one of: true, false, not dark
        end tell
    end tell
    
    # Update default settings (for new windows/tabs)
    tell application "Terminal"
        set default settings to settings set "Rasta"
    end tell
    
    # Update settings for exsting windows/tabs
    tell application "Terminal"
        set current settings of tabs of windows to settings set "Rasta"  # Theme name
    end tell
    
  • You can wrap both things with a Python script:

    # toggle-macos-dark-mode.py
    import subprocess
    
    
    OSASCRIPT = """
    tell application "System Events"
        tell appearance preferences
            set dark mode to {mode}
        end tell
    end tell
    
    tell application "Terminal"
        set default settings to settings set "{theme}"
    end tell
    
    tell application "Terminal"
        set current settings of tabs of windows to settings set "{theme}"
    end tell
    """
    
    TERMINAL_THEMES = {
        False: 'Rasta light',
        True: 'Rasta',
    }
    
    
    def is_dark_mode() -> bool:
        """Return the current Dark Mode status."""
        result = subprocess.run(
            ['defaults', 'read', '-g', 'AppleInterfaceStyle'],
            text=True,
            capture_output=True,
        )
        return result.returncode == 0 and result.stdout.strip() == 'Dark'
    
    
    def set_interface_style(dark: bool):
        """Enable/disable dark mode."""
        mode = 'true' if dark else 'false'  # mode can be {true, false, not dark}
        script = OSASCRIPT.format(mode=mode, theme=TERMINAL_THEMES[dark])
        result = subprocess.run(
            ['osascript', '-e', script],
            text=True,
            capture_output=True,
        )
        assert result.returncode == 0, result
    
    
    if __name__ == '__main__':
        set_interface_style(not is_dark_mode())
    
  • You can use the timer_start() function introduced in Vim 8 and neovim to regularly check for the current Dark Mode settings. Put this into your Vim config:

    function! SetBackgroundMode(...)
        let s:new_bg = "light"
        if $TERM_PROGRAM ==? "Apple_Terminal"
            let s:mode = systemlist("defaults read -g AppleInterfaceStyle")[0]
            if s:mode ==? "dark"
                let s:new_bg = "dark"
            else
                let s:new_bg = "light"
            endif
        else
            " This is for Linux where I use an environment variable for this:
            if $VIM_BACKGROUND ==? "dark"
                let s:new_bg = "dark"
            else
                let s:new_bg = "light"
            endif
        endif
        if &background !=? s:new_bg
            let &background = s:new_bg
        endif
    endfunction
    call SetBackgroundMode()
    call timer_start(3000, "SetBackgroundMode", {"repeat": -1})
    
  • You can create an Automator action that runs the Python script and that can be activated with a global shortcut. I use ⌥⌘D (you need to deactivate this shortcut for showing/hiding the Dock first). This is the AppleScript I used:

    do shell script "/usr/local/bin/python3 ~/toggle-macos-dark-mode.py"
    
    Yo dawg, I heard you like AppleScript … so I wrote some AppleScript that wraps your Python that wraps your AppleScript

The drawback of this method is that the current application (at the time you press ⌥⌘D) is used as “source” of the action you get two dialogs asking you to give that app permissions to remote control the System Settings and Terminal.

A better solution would be if the authors of NightOwl and Shifty would integrated this into their tools. I’m gonna contact them and see what happens. :-)

Update:

MacVim got an OSAppearanceChanged event that is emitted every time MacVim changes its appearance.

Thanks to Frank for the heads up!