Skip to content

Instantly share code, notes, and snippets.

@ttscoff

ttscoff/README.md

Last active Nov 22, 2020
Embed
What would you like to do?

A CLI for Bunch.app by Brett Terpstra

Save the file below as bunch in your path. Run bunch -h for help.

CLI for Bunch.app
    -h, --help                       Display this screen
    -f, --force-refresh              Force refresh cached preferences
    -l, --list                       List available Bunches
    -o, --open                       Open Bunch ignoring "Toggle Bunches" preference
    -c, --close                      Close Bunch ignoring "Toggle Bunches" preference
    -t, --toggle                     Toggle Bunch ignoring "Toggle Bunches" preference
    -s, --show BUNCH                 Show contents of Bunch
        --show-config                Display configuration values

Usage: bunch [options] BUNCH_NAME|PATH_TO_FILE

Bunch names are case insensitive and will execute first match

2019-06-13:

  • Use defaults read directly on the preference plist and avoid domain lookup for speed
  • Add caching: store preferences from defaults read in a YAML file, refresh once a day
  • Option to force refresh (-f) can be used in combination with any other option
  • If the BUNCH_NAME is a filepath instead, execute it using the new raw URL method
#!/usr/bin/env ruby
# frozen_string_literal: true
# A CLI for [Bunch.app](https://brettterpstra.com/projects/bunch/) by Brett Terpstra
# With tweaks from jlw (https://gist.github.com/jlw/28ab2591f8d14c7799a9e18580279f08#gistcomment-2924428)
CACHE_TIME = 86400 #seconds, 1 day = 86400
CACHE_FILE = "~/.bunch_cli_cache"
require 'optparse'
require 'yaml'
class Bunch
attr_writer :url_method
def initialize
@bunch_dir = nil
@url_method = nil
@bunches = nil
get_cache
end
def update_cache
@bunch_dir = nil
@url_method = nil
@bunches = nil
target = File.expand_path(CACHE_FILE)
settings = {
'bunchDir' => bunch_dir,
'method' => url_method,
'bunches' => bunches,
'updated' => Time.now.strftime('%s').to_i
}
File.open(target,'w') do |f|
f.puts YAML.dump(settings)
end
return settings
end
def get_cache
target = File.expand_path(CACHE_FILE)
if File.exists?(target)
settings = YAML.load(IO.read(target))
now = Time.now.strftime('%s').to_i
if now - settings['updated'].to_i > CACHE_TIME
settings = update_cache
end
else
settings = update_cache
end
@bunch_dir = settings['bunchDir'] || bunch_dir
@url_method = settings['method'] || url_method
@bunches = settings['bunches'] || generate_bunch_list
end
# items.push({title: 0})
def generate_bunch_list
items = []
Dir.glob(File.join(bunch_dir, '*.bunch')).each do |f|
items.push(
path: f,
title: File.basename(f, '.bunch')
)
end
items
end
def bunch_dir
@bunch_dir ||= begin
dir = `/usr/bin/defaults read #{ENV['HOME']}/Library/Preferences/com.brettterpstra.Bunch.plist configDir`.strip
File.expand_path(dir)
end
end
def url_method
@url_method ||= `/usr/bin/defaults read #{ENV['HOME']}/Library/Preferences/com.brettterpstra.Bunch.plist toggleBunches`.strip == '1' ? 'toggle' : 'open'
end
def bunches
@bunches ||= generate_bunch_list
end
def url(bunch)
if url_method == 'file'
%(x-bunch://raw?file=#{bunch})
else
%(x-bunch://#{url_method}?bunch=#{bunch[:title]})
end
end
def bunch_list
list = []
bunches.each { |bunch| list.push(bunch[:title]) }
list
end
def list_bunches
$stdout.puts bunch_list.join("\n")
end
def find_bunch(str)
found_bunch = false
bunches.each do |bunch|
if bunch[:title].downcase =~ /.*?#{str}.*?/i
found_bunch = bunch
break
end
end
found_bunch
end
def human_action
(url_method.gsub(/e$/, '') + 'ing').capitalize
end
def open(str)
# get front app
front_app = %x{osascript -e 'tell application "System Events" to return name of first application process whose frontmost is true'}.strip
bunch = find_bunch(str)
unless bunch
if File.exists?(str)
@url_method = 'file'
warn "Opening file"
`open '#{url(str)}'`
else
warn 'No matching Bunch found'
Process.exit 1
end
else
warn "#{human_action} #{bunch[:title]}"
`open "#{url(bunch)}"`
end
# attempt to restore front app
%x{osascript -e 'delay 2' -e 'tell application "#{front_app}" to activate'}
end
def show(str)
bunch = find_bunch(str)
output = `cat "#{bunch[:path]}"`.strip
puts output
end
def show_config
puts "Bunches Folder: #{bunch_dir}"
puts "Default URL Method: #{url_method}"
puts "Cached Bunches"
bunches.each {|b|
puts " - #{b[:title]}"
}
end
end
def help
puts "\nUsage: #{File.basename(__FILE__)} [options] BUNCH_NAME|PATH_TO_FILE"
puts "\nBunch names are case insensitive and will execute first match"
end
bunch = Bunch.new
optparse = OptionParser.new do |opts|
opts.banner = 'CLI for Bunch.app'
opts.on('-h', '--help', 'Display this screen') do |_opt|
puts opts
help
Process.exit 0
end
opts.on('-f', '--force-refresh', 'Force refresh cached preferences') do |opt|
bunch.update_cache
end
opts.on('-l', '--list', 'List available Bunches') do |_opt|
bunch.list_bunches
Process.exit 0
end
opts.on('-o', '--open', 'Open Bunch ignoring "Toggle Bunches" preference') do |_opt|
bunch.url_method = 'open'
end
opts.on('-c', '--close', 'Close Bunch ignoring "Toggle Bunches" preference') do |_opt|
bunch.url_method = 'close'
end
opts.on('-t', '--toggle', 'Toggle Bunch ignoring "Toggle Bunches" preference') do |_opt|
bunch.url_method = 'toggle'
end
opts.on('-s', '--show BUNCH', 'Show contents of Bunch') do |opt|
bunch.show(opt)
Process.exit 0
end
opts.on('--show-config', 'Display configuration values') do |opt|
bunch.show_config
Process.exit 0
end
end
optparse.parse!
unless ARGV.length > 0
puts "CLI for Bunches.app"
help
else
ARGV.map { |arg| bunch.open(arg) }
end
@njm2112

This comment has been minimized.

Copy link

@njm2112 njm2112 commented Mar 24, 2020

what is the proper ownership for the script once accessible through $PATH?

bunch -h is throwing a permission denied error for me.

@ttscoff

This comment has been minimized.

Copy link
Owner Author

@ttscoff ttscoff commented Mar 24, 2020

@jonathan-dejong

This comment has been minimized.

Copy link

@jonathan-dejong jonathan-dejong commented Jun 18, 2020

Pretty excited to see what I can automate with this 👍
Something that came to mind: would it be possible to pass/pipe an argument or more to the bunch through the CLI?
Without any real knowledge I figure it could be something like the ${KEY} syntax?
A couple of example use cases :

I want to startup dev env for a specific project. By passing the projects name I could fire up iTerm and VSC on that specific project, start NPM, close down any other running node etc. etc.

bunch dev --f=<clientfolder>

in bunch:

%iTerm
- {@n}
- [cd path/to/dev/${f}]
- [composer install]
- [npm install && npm start]
- [vsc]

I want to check the JIRA board of a specific client. I could pass the projects JIRA tag (2-5 letter shorthand) and bring up my browser to that address. Like https://jirainstance.com/projects/<shorthand>/issues/

bunch jira --p=<shorthand>

in bunch:

https://jirainstance.com/projects/${p}/issues/

I want to discuss something with a coworker in a project.

bunch slack --dm=erik

in bunch:

%Slack
- {@k}
- [${dm}]
@ttscoff

This comment has been minimized.

Copy link
Owner Author

@ttscoff ttscoff commented Jun 19, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.