profile

Hi! I'm Moncef Belyamani

Automate Context Switching With Bunch

Published over 1 year ago • 12 min read

Hi Reader!

This week's automation guide is about a free but powerful Mac app called Bunch. I had heard of it years ago but never took the time to explore it in detail. Until now, and it has proven very useful so far. I'm not sure how the code samples will look like in your email, so you might prefer to read this on my site by clicking the title below.

Automate Context Switching With Bunch

You sit down to work on a feature, and wake up your Mac. Oh hey, Slack is open. You decide to check it real quick. 30 minutes later, you remember what you wanted to do and close Slack.

Then you switch to the terminal, cd to the project directory, open up the project in your favorite text editor, run the server, open the app in a browser, and arrange all the windows on your screen the way you like them. Then you get an email notification, so you quit all communication apps and turn on Do Not Disturb.

Or maybe you manage multiple projects like a blog, a product, a podcast, and a livestream. Each one has its own context with app layout, development, and macOS settings. And you find yourself setting everything up manually each time.

If this sounds familiar, you’ll want to add the Bunch app to your automation toolbox.

Unlike other GUI-based automation apps like Alfred, Keyboard Maestro, and Shortcuts, a Bunch is just a text file. The Bunch documentation is great and includes sample bunches to get you started.

I created one for myself called Coding.bunch that showcases some of the cool features and the syntax. Below is the full file, which I’ll go over step by step. And here’s a video that shows it in action.

// Hide all visible apps
@@

<quit_comms.snippet

project = ?[
  Blog
  Ruby on Mac Site
] "Whatcha workin' on?"

<<#${project}

soundcloud = $ osascript ~/Documents/Bunches/search_open_tabs.applescript "soundcloud"
___

#[Blog]
Tower
- ~/projects/monfresh/blog

iTerm
- [blog\\n]
- {shift-command-d}

port = 4567
path = archives
<<#Arrange Safari and Sublime

#[Ruby on Mac Site]
iTerm
- {"cd ~/projects/rubyonmac/rubyonmac-site" return "git pull && bundle && bin/bridgetown start" return }

port = 4000
<<#Arrange Safari and Sublime

#[Arrange Safari and Sublime]
if ${soundcloud}
    (log soundcloud is already open)
else
    %Safari
    - https://soundcloud.com/discover/sets/new-for-you::monfresh
end

%Safari
- (pause 2)
- http://localhost:${port}/${path}
- {command-r}

@Sublime Text

* tell application "Moom" to arrange windows according to snapshot "Safari and Sublime Side by Side"

Brett Terpstra (the author of Bunch) also created packages for various IDEs that include highlighting and completions, which was nice when working on the Bunch in Sublime Text. He also graciously shared his Rouge lexer for Bunch so I could have syntax highlighting on this page.

The first thing this Bunch does is hide all visible apps with the @@ command. Alternatively, to start with a clean slate, you can quit everything:

(quit everything)

You can also exclude certain apps from quitting:

(quit everything except Safari)

Brett also supports some fun variations for the quit everything command.

Check out all the commands you can run.

Next, I wanted to try quitting all distractions at once, which for me are communication apps. Thinking about reusability in other bunches, I created a snippet that quits all these apps, and I named it quit_comms.snippet:

!Discord
!Mail
!Messages
!Slack
!Stellar
!Telegram
!Twitter
!WhatsApp

And then I called it in my Coding Bunch like this:

<quit_comms.snippet

A snippet file can be named with any extension other than .bunch, and should be stored in your Bunches Folder (~/Documents/Bunches by default I think.)

At first, I thought I would be able to create a Comms.bunch that launched all of these apps, and that I could tell it to close all of those apps in my Coding.bunch, like in the Podcasting sample Bunch in the Bunch documentation:

!Comms.bunch

However, that’s not how Bunch works. If one of those communication apps is already running, starting the Coding bunch will not quit that app. And when the Coding bunch is stopped, the Comms bunch will automatically start and open all the communication apps! That’s definitely not what I wanted.

The next part of the Coding bunch displays a prompt that asks me which project I’m working on:

project = ?[
  Blog
  Ruby on Mac Site
] "Whatcha workin' on?"

<<#${project}

Here, we’re assigning the name of the project to a project variable, and then we’re telling the Bunch to run the actions that are specific to that project. Each project’s actions is defined within a “fragment”. The fragment header can start with either a #, -, =, or >, followed by the fragment name enclosed in square brackets. In this example, there are 2 fragments: #[Blog] and #[Ruby on Mac Site].

You call fragments within a Bunch with <<# followed by the fragment name, and you can even add a delay to that fragment with ~ followed by the number of seconds. For example:

<<#Blog ~2

Let’s look at the Blog fragment:

#[Blog]
iTerm
- [blog\\n]
- {shift-command-d}

port = 4567
path = archives
<<#Arrange Safari and Sublime

First, it launches iTerm, and then if you want to perform certain actions within an application, you start a new line with a - followed by an action. To type a string, you put it between square brackets. In this case, I want Bunch to type blog followed by the return key:

- [blog\\n]

blog is an alias in my Fish shell that runs these commands:

cd ~/projects/monfresh/blog && git pull && chruby (cat .ruby-version) \
&& bundle && subl . && bundle exec middleman serve

Next, I want to open a new pane in iTerm under the current one by pressing shift-⌘-D. That way, I have the server running in the top pane, and in the bottom pane I can run commands if I need to. To tell Bunch to send keystrokes, you enclose them in curly brackets:

- {shift-command-d}

You can also combine typing and keystrokes, as shown in the Ruby on Mac Site fragment:

- {"cd ~/projects/rubyonmac/rubyonmac-site" return "git pull && bundle && bin/bridgetown start" return }

Finally, I call another fragment called Arrange Safari and Sublime, and I pass in the port value of 4000 and path value of “archives”:

port = 4000
path = archives
<<#Arrange Safari and Sublime

Here’s what the fragment looks like:

#[Arrange Safari and Sublime]
if ${soundcloud}
    (log soundcloud is already open)
else
    %Safari
    - https://soundcloud.com/discover/sets/new-for-you::monfresh
end

%Safari
- (pause 2)
- http://localhost:${port}/${path}
- {command-r}

@Sublime Text

* tell application "Moom" to arrange windows according to snapshot "Safari and Sublime Side by Side"

This one ended up being tricky to figure out due to the way conditionals and variables work, so pay attention.

I like to listen to “The Upload” playlist that SoundCloud curates for me, and when I was testing opening the SoundCloud URL, it was already playing, but when I ran the Bunch, it stopped the playback because it refreshed the page. 😢

That’s when I looked into Bunch’s conditional logic and variables. I didn’t find a built-in way to check if a browser tab is already open, so I asked DuckDuckGo how to do that with AppleScript. The examples I found were pretty much the same, but they did more than I needed, so I trimmed it down and saved it to a file called search_open_tabs.applescript in my Bunches folder.

on run argv
    set searchText to (item 1 of argv)
    tell application "Safari"
        set winlist to every window
        set tabmatchlist to {}
        repeat with win in winlist
            if (count of tabs of win) is not equal to 0 then
                set tablist to every tab of win
                repeat with t in tablist
                    if (searchText is in (name of t as string)) or (searchText is in (URL of t as string)) then
                        set end of tabmatchlist to t
                    end if
                end repeat
            end if
        end repeat
        if (count of tabmatchlist) = 1 then
            return true
        else
            return false
        end if
    end tell
end run

At first, I tried defining the variable inside the #[Arrange Safari and Sublime] fragment:

#[Arrange Safari and Sublime]
soundcloud = $ osascript ~/Documents/Bunches/search_open_tabs.applescript "soundcloud"

and then using it like this:

if soundcloud
    (log soundcloud is already open)
else
    %Safari
    - https://soundcloud.com/discover/sets/new-for-you::monfresh
end

but for some reason Bunch didn’t like that. What was so strange is that Bunch could see that the value was true, but it would still execute the else statement. Here’s how I added logging to debug:

(log soundcloud is ${soundcloud})
if soundcloud
    (log soundcloud is ${soundcloud})
    (log soundcloud is already open)
else
    (log soundcloud is ${soundcloud})
    %Safari
    - https://soundcloud.com/discover/sets/new-for-you::monfresh
end

So then I tried variations on the variable name and comparison:

  • if $soundcloud
  • if ${soundcloud}
  • if soundcloud is true
  • if soundcloud is "true"
  • else if soundcloud is false

and so on, but nothing worked. That’s when I realized that maybe the variable needs to be defined outside the fragment, and that worked!

If you scroll up to the full file, you’ll see the soundcloud variable is defined before any fragments:

<<#${project}

soundcloud = $ osascript ~/Documents/Bunches/search_open_tabs.applescript "soundcloud"
___

#[Blog]

...

The other thing to watch out for with conditionals is that indentation needs to be 4 spaces.

Next, I tell Safari to pause for 2 seconds before opening localhost with the port number and path that were passed in earlier when the fragment was called. The reason for the pause is because I want localhost to be the last tab opened, but Bunch kept opening it first, even though it comes after SoundCloud in the Bunch file.

Then I send the ⌘-R keystroke to refresh the page because sometimes it visits localhost before the server has finished loading. The % before Safari tells the Bunch to leave Safari open after the Bunch is closed. Otherwise, any apps that are opened by the Bunch get quit when the Bunch is closed.

Here’s the Safari bit again:

%Safari
- (pause 2)
- http://localhost:${port}/${path}
- {command-r}

Then I focus Sublime Text with the @ prefix, and finally, I run an AppleScript inline that tells Moom to arrange Safari on the right half of the screen, and Sublime Text on the left half:

@Sublime Text

* tell application "Moom" to arrange windows according to snapshot "Safari and Sublime Side by Side"

I was hoping to be able to send the Moom keyboard shortcuts I have for sending windows left or right, but it doesn’t look like Bunch supports that. I think keystrokes have to be sent in an app’s context, and it seems like the keystroke has to be recognized by the app, and since ⌃-⌥-⌘-← doesn’t exist as a Safari or Sublime keyboard shortcut, I got an error in the Bunch log that said this:

Could not convert control-option-command-left into key codes

So, for window management, I had to set Safari and Sublime side by side the way I wanted them, and then save a snapshot in Moom.

And that’s it! I’ve been using this Bunch regularly now and it’s been working great. One improvement I need to make is to first check if Git is clean before running git pull because if there are unstaged changes, git pull will fail and the server won’t be started.

By default, to start and stop Bunches, you have to do it via the menu bar icon. To speed things up, Bunch makes it easy to integrate with other automation tools by providing its own x-bunch: URL scheme.

I chose to set up a hotkey in Keyboard Maestro that will open x-bunch://open?bunch=Coding when I press shift-control-option-command-C. Here’s what the macro looks like:

Keyboard Maestro Macro to open a Bunch

To take this to another level, I used Karabiner-Elements to turn the caps lock key into a hyper key that combines the shift-control-option-command keys. That way, all I have to do to open the Coding Bunch is to press caps lock and C.

Using Bunch to set up a live stream

I ran into some issues with another Bunch I created, and wanted you to be aware of them. No one else has reported them in the Issue Tracker or the Discussions, so I’m not sure if they’re bugs or something on my end.

I recently started live coding on my YouTube channel in the spirit of building in public. So far, I’ve been working on Ruby on Mac, and automating my daily and business workflows. I also plan on sharing how I maintain Rails apps, and recording video versions of my automation tutorials.

To reduce the chances of accidentally showing something sensitive or personal on the screen during the live stream, I like to quit all applications except the ones I’ll be using, which are typically Sublime Text, iTerm, and Safari.

I also like to hide all desktop icons, and Ecamm Live, the app I use to stream, has a preference to automatically hide desktop icons when I’m sharing my screen. I also noticed that Bunch has a command to do the same thing, and wanted to try it even though I don’t need it when using Ecamm Live:

(hide desktop)

However, it didn’t work for me. I tried on my M1 Macbook Air and Intel iMac, both running macOS 12.5 (Monterey). I even tried with a Bunch whose only action was to hide the desktop. Both Macs are running the latest version of Bunch: 1.4.9 (148).

If I told the Bunch to directly run the command to hide the desktop icons, and then restore them when the Bunch is closed, it worked:

$ defaults write com.apple.finder CreateDesktop -bool false && killall Finder

!$ defaults write com.apple.finder CreateDesktop -bool true && killall Finder

As you might guess from the example above, to perform an action when the Bunch closes, you precede it with an exclamation point. The documentation has more examples of ways to run on close.

Here’s the full livestreaming Bunch I created, and then I’ll go over it, and a couple of things that tripped me up:

---
title: 🎥Livestream
open on: Mon 12:30pm, Wed 12:30pm
---

# Close all Finder windows
Finder
- XX

(quit everything except Safari, Moom, Keyboard Maestro, iTerm)
(hide desktop)
# Turn on Do Not Disturb
(dnd on)

Ecamm Live
iTerm

# Ask about specific project, handle setup
?{
    Ruby on Mac => <<#Ruby on Mac
    Rails app maintenance => <<#Ohana
} "Whatcha workin' on?"

# Quit the bandwidth heavy apps
$ killall Dropbox
$ /Library/Backblaze.bzpkg/bztransmit -pausebackup

# Focus ECamm Live, hiding other apps
@ECamm Live

# Turn Backblaze and Dropbox back on when this Bunch closes
!$ /Library/Backblaze.bzpkg/bztransmit -completesync
## Double negatives (!!) to launch apps and other Bunches when closing
!!Dropbox
___

#[Ruby on Mac]
Tower
- ~/projects/rubyonmac/rom-ultimate

$ subl ~/projects/rubyonmac/rom-ultimate

#[Ohana]
Tower
- ~/projects/codeforamerica/ohana-api

iTerm
- {command-t}
- [cfaoa\\n]

Similarly to the Coding.bunch, this one prompts me for the project I’ll be live coding on:

?{
    Ruby on Mac => <<#Ruby on Mac
    Rails app maintenance => <<#Ohana
} "Whatcha workin' on?"

The difference is that this uses a Hash instead of an Array, which allows for the dropdown label to have a different name than the fragment in the Bunch:

Livestream Bunch dropdown

This is just to show you a different way to set up these prompts. In reality, I use the same name for the label and the fragment.

When I first tested this, it wasn’t working. In the log, it kept saying it couldn’t find a matching fragment. It took me a while to realize what I did wrong, which was using 3 dashes (---) above the fragments instead of 3 underscores (___). I knew something was wrong thanks to the syntax highlighting in Sublime Text, which turned off all highlighting in the fragments, but I couldn’t figure out why!

The documentation mentions that the fragment separator should be 3 underscores, but I missed it, and to create the Coding.bunch, I had copied and pasted from the sample Bunches in the documentation, so I had never experienced typing the 3 underscores. This is a reminder of the importance of typing code directly yourself when following tutorials instead of copying and pasting.

Another cool thing you can do is add frontmatter, such as customizing the label in the Bunch menu bar, as well as telling the Bunch to open (and/or close) on certain days and times. When I stream, it’s usually between 1 and 2:30pm, so I set this Bunch to open 30 minutes before, so I can make sure everything is working and ready to go.

---
title: 🎥Livestream
open on: Mon 12:30pm, Wed 12:30pm
---

The rest of the Bunch should be straightforward, but I wanted to point out one last possible bug. The release notes for Bunch version 1.4.9 say that “/opt/homebrew/bin is now included in the default path for M1 users using shell scripting.” However, the subl command below failed when running the livestream Bunch on my M1 Mac. It works fine on my Intel iMac.

$ subl ~/projects/rubyonmac/rom-ultimate

When I run which subl, I get /opt/homebrew/bin/subl so I’m not sure what’s going on. I will open a bug in the Bunch GitHub repo and update this post. It’s no big deal because I can tell Sublime Text to open it, or do it via iTerm:

Sublime Text
- ~/projects/rubyonmac/rom-ultimate

or

iTerm
- {command-t}
- {"subl ~/projects/rubyonmac/rom-ultimate" return}

That wraps it up! I hope I inspired you to give Bunch a try. Let me know if you do.

Finally, I wanted to mention that if you're getting a new Mac, you might be interested in the "Ultimate" version of Ruby on Mac. With a single command, it will set up your Mac just the way you like it with all your dev tools, Mac apps, macOS preferences, and GitHub repos.

It comes with a Brewfile that contains hundreds of dev tools and Mac apps, including all the apps mentioned in this post: Backblaze, Bunch, Ecamm Live, Fish shell, iTerm2, Karabiner-Elements, Moom, Sublime Text, and Tower.

As a current Ruby on Mac customer, you can upgrade to Ultimate with coupon code [ROM_ULTIMATE_UPGRADE_COUPON GOES HERE]

Have a great week!

Moncef

Hi! I'm Moncef Belyamani

Every week, I send out an automation tutorial that will save you time and make you more productive. I also write about being a solopreneur, and building helpful things with Ruby. Join 2853 others who value their time.

Read more from Hi! I'm Moncef Belyamani

Hi Reader 👋🏼 Happy Sunday! I hope you and your loved ones are doing well. Earlier this week, I found a great use case for the 1Password CLI that hadn't occurred to me before. I'm gonna use it a lot more often whenever I can! If this email doesn't look right, or if you prefer reading on my site, you can click the title link below. Automate GitHub API Calls With Ruby, Keyboard Maestro, and 1Password CLI One of the perks of the “Ultimate” version of Ruby on Mac is access to the private GitHub...

over 1 year ago • 5 min read
PopClip default extensions

Hi Reader! This week's automation guide is about a little-known app called PopClip. PopClip was originally released in 2011, but I didn’t hear about it until four years ago, and I’m sure there are still a lot of people who don’t know about it. It’s one of the many useful apps you can discover and quickly install with the “Ultimate” version of Ruby on Mac. You can pick and choose from hundreds of Mac apps, fonts, and dev tools in the included Brewfile, and Ruby on Mac will install them all at...

almost 2 years ago • 2 min read

Hi Reader! This week's automation tip is a simple one but packs a punch. It’s 2022, and there are still annoying sites that block pasting into form fields, for passwords, or your bank’s account and routing numbers. If you look up how to bypass this copy-paste restriction, the three most common solutions all have downsides: Use a browser extension, that might only work in certain browsers and only on some sites Change the Firefox configuration settings, which comes with a “Proceed with...

almost 2 years ago • 2 min read
Share this post