bunchcli 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '064668e5dae28ec08fe4a939dc270e9b1880116f68f8ad39fc3c2252cd7609fa'
4
- data.tar.gz: be7be6b0cf5404ae193aec36d50b4efa4b6672b03be4e875f60cd89fcf3a07b9
3
+ metadata.gz: 010c20799d4e9e35409737ee3dfd5e3a755096ff212eb9a02787dfcee16dc5e2
4
+ data.tar.gz: daccb516131e6816f4ea5cd153e14df691e3089600e77114e1755cf502efe131
5
5
  SHA512:
6
- metadata.gz: 59abf612ddfe2eff6f5b54bf7952e08f67cad5bc7c2ec06ca9eb6d15aa097798c6bfaf6686292041fba6dd4fd34e3dc013ef3ffdff60c271a85b0fa99b64b144
7
- data.tar.gz: e888ed65aef4791420808cc5220f275c9e3c9c618ba931d273f4b118f3ffdcecb978b641e298ba0d18024758977ba3e883095daca71859af25e381291f903ce9
6
+ metadata.gz: b9f94711eadea1d3cf6cd3ebef2244663cc793b1dc3bd121ac2bb1409431c24215d03b172db13d76966143529c99578bfce000bebbba89dbbf16410d1653ab93
7
+ data.tar.gz: 2923722c6988c146be4eaa19b20b732413eb95fad52c890a0bd47165e50a61e576042d4934d376a6f5ed992723f5d843ee645b3eaa7eb10cdb677628f996ce99
@@ -0,0 +1,19 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ bunchcli (1.0.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ rake (12.3.3)
10
+
11
+ PLATFORMS
12
+ x86_64-darwin-19
13
+
14
+ DEPENDENCIES
15
+ bunchcli!
16
+ rake (~> 12.0)
17
+
18
+ BUNDLED WITH
19
+ 2.2.4
data/bin/bunch CHANGED
@@ -13,18 +13,13 @@ bunch = Bunch.new
13
13
  optparse = OptionParser.new do |opts|
14
14
  opts.banner = 'CLI for Bunch.app'
15
15
 
16
- opts.on('-h', '--help', 'Display this screen') do |_opt|
17
- puts opts
18
- help
16
+ opts.on('-l', '--list', 'List available Bunches') do |_opt|
17
+ bunch.list_bunches
19
18
  Process.exit 0
20
19
  end
21
20
 
22
- opts.on('-f', '--force-refresh', 'Force refresh cached preferences') do |opt|
23
- bunch.update_cache
24
- end
25
-
26
- opts.on('-l', '--list', 'List available Bunches') do |_opt|
27
- bunch.list_bunches
21
+ opts.on('-s', '--show BUNCH', 'Show contents of Bunch') do |opt|
22
+ bunch.show(opt)
28
23
  Process.exit 0
29
24
  end
30
25
 
@@ -40,8 +35,24 @@ optparse = OptionParser.new do |opts|
40
35
  bunch.url_method = 'toggle'
41
36
  end
42
37
 
43
- opts.on('-s', '--show BUNCH', 'Show contents of Bunch') do |opt|
44
- bunch.show(opt)
38
+ opts.on('--snippet', 'Load as snippet') do |opt|
39
+ bunch.url_method = 'snippet'
40
+ end
41
+
42
+ opts.on('--fragment=FRAGMENT', 'Run a specific section') do |opt|
43
+ bunch.fragment = opt
44
+ end
45
+
46
+ opts.on('--vars=VARS', 'Variables to pass to a snippet, comma-separated') do |opt|
47
+ bunch.variables = opt
48
+ end
49
+
50
+ opts.on('-u', '--url', 'Output URL instead of opening') do |_opt|
51
+ bunch.show_url = true
52
+ end
53
+
54
+ opts.on('-i','--interactive', 'Interactively generate a Bunch url') do |opt|
55
+ BunchURLGenerator.new.generate
45
56
  Process.exit 0
46
57
  end
47
58
 
@@ -49,6 +60,16 @@ optparse = OptionParser.new do |opts|
49
60
  bunch.show_config
50
61
  Process.exit 0
51
62
  end
63
+
64
+ opts.on('-f', '--force-refresh', 'Force refresh cached preferences') do |opt|
65
+ bunch.update_cache
66
+ end
67
+
68
+ opts.on('-h', '--help', 'Display this screen') do |_opt|
69
+ puts opts
70
+ help
71
+ Process.exit 0
72
+ end
52
73
  end
53
74
 
54
75
  optparse.parse!
@@ -4,4 +4,5 @@ CACHE_FILE = "~/.bunch_cli_cache"
4
4
  require "bunch/version"
5
5
  require 'yaml'
6
6
  require 'cgi'
7
+ require 'bunch/url_generator'
7
8
  require 'bunch/bunchCLI'
@@ -1,10 +1,15 @@
1
1
  class Bunch
2
- attr_writer :url_method
2
+ include Util
3
+ attr_writer :url_method, :fragment, :variables, :show_url
3
4
 
4
5
  def initialize
5
6
  @bunch_dir = nil
6
7
  @url_method = nil
7
8
  @bunches = nil
9
+ @fragment = nil
10
+ @variables = nil
11
+ @success = nil
12
+ @show_url = false
8
13
  get_cache
9
14
  end
10
15
 
@@ -41,6 +46,18 @@ class Bunch
41
46
  @bunches = settings['bunches'] || generate_bunch_list
42
47
  end
43
48
 
49
+ def variable_query
50
+ vars = @variables.split(/,/).map { |v| v.strip }
51
+ query = []
52
+ vars.each { |v|
53
+ parts = v.split(/=/).map { |v| v.strip }
54
+ k = parts[0]
55
+ v = parts[1]
56
+ query << "#{k}=#{CGI.escape(v)}"
57
+ }
58
+ query
59
+ end
60
+
44
61
  # items.push({title: 0})
45
62
  def generate_bunch_list
46
63
  items = []
@@ -69,12 +86,15 @@ class Bunch
69
86
  end
70
87
 
71
88
  def url(bunch)
89
+ params = "&x-success=#{@success}" if @success
72
90
  if url_method == 'file'
73
- %(x-bunch://raw?file=#{bunch})
91
+ %(x-bunch://raw?file=#{bunch}#{params})
74
92
  elsif url_method == 'raw'
75
- %(x-bunch://raw?txt=#{bunch})
93
+ %(x-bunch://raw?txt=#{bunch}#{params})
94
+ elsif url_method == 'snippet'
95
+ %(x-bunch://snippet?file=#{bunch}#{params})
76
96
  else
77
- %(x-bunch://#{url_method}?bunch=#{bunch[:title]})
97
+ %(x-bunch://#{url_method}?bunch=#{bunch[:title]}#{params})
78
98
  end
79
99
  end
80
100
 
@@ -107,28 +127,54 @@ class Bunch
107
127
  def open(str)
108
128
  # get front app
109
129
  front_app = %x{osascript -e 'tell application "System Events" to return name of first application process whose frontmost is true'}.strip
130
+ bid = bundle_id(front_app)
131
+ @success = bid if (bid)
132
+
110
133
  if @url_method == 'raw'
111
134
  warn 'Running raw string'
112
- `open '#{url(str)}'`
135
+ if @show_url
136
+ $stdout.puts url(str)
137
+ else
138
+ `open '#{url(str)}'`
139
+ end
140
+ elsif @url_method == 'snippet'
141
+ _url = url(str)
142
+ params = []
143
+ params << "fragment=#{CGI.escape(@fragment)}" if @fragment
144
+ params.concat(variable_query) if @variables
145
+ _url += '&' + params.join('&')
146
+ if @show_url
147
+ $stdout.puts _url
148
+ else
149
+ warn "Opening snippet"
150
+ `open '#{_url}'`
151
+ end
113
152
  else
114
153
  bunch = find_bunch(str)
115
154
  unless bunch
116
155
  if File.exists?(str)
117
156
  @url_method = 'file'
118
- warn "Opening file"
119
- `open '#{url(str)}'`
157
+ if @show_url
158
+ $stdout.puts url(str)
159
+ else
160
+ warn "Opening file"
161
+ `open '#{url(str)}'`
162
+ end
120
163
  else
121
164
  warn 'No matching Bunch found'
122
165
  Process.exit 1
123
166
  end
124
167
  else
125
- warn "#{human_action} #{bunch[:title]}"
126
-
127
- `open "#{url(bunch)}"`
168
+ if @show_url
169
+ $stdout.puts url(str)
170
+ else
171
+ warn "#{human_action} #{bunch[:title]}"
172
+ `open '#{url(bunch)}'`
173
+ end
128
174
  end
129
175
  end
130
176
  # attempt to restore front app
131
- %x{osascript -e 'delay 2' -e 'tell application "#{front_app}" to activate'}
177
+ # %x{osascript -e 'delay 2' -e 'tell application "#{front_app}" to activate'}
132
178
  end
133
179
 
134
180
  def show(str)
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'readline'
4
+ require 'cgi'
5
+
6
+ # String additions
7
+ class String
8
+ def text?
9
+ res = `file "#{self}"`
10
+ res =~ /text/
11
+ end
12
+ end
13
+
14
+ # misc utils
15
+ module Util
16
+ def bundle_id(app)
17
+ shortname = app.sub(/\.app$/, '')
18
+ apps = `mdfind -onlyin /Applications -onlyin /Applications/Setapp -onlyin /Applications/Utilities -onlyin ~/Applications -onlyin /Developer/Applications 'kMDItemKind==Application'`
19
+
20
+ app = apps.split(/\n/).select! { |line| line.chomp =~ /#{shortname}\.app$/ }[0]
21
+
22
+ if app
23
+ bid = `mdls -name kMDItemCFBundleIdentifier -r "#{app}"`.chomp
24
+ else
25
+ warn "Could not locate bundle id for #{shortname}"
26
+ end
27
+ bid
28
+ end
29
+ end
30
+
31
+ # CLI Prompt utilities
32
+ module Prompt
33
+ def choose_number(query = '->', max)
34
+ stty_save = `stty -g`.chomp
35
+ sel = nil
36
+ begin
37
+ while !sel =~ /^\d+$/ || sel.to_i <= 0 || sel.to_i > max
38
+ sel = Readline.readline("#{query}", true)
39
+ return nil if sel =~ /^\s*$/
40
+ end
41
+ rescue Interrupt
42
+ system('stty', stty_save) # Restore
43
+ exit
44
+ end
45
+ sel ? sel.to_i : nil
46
+ end
47
+
48
+ def get_line(query = '->')
49
+ stty_save = `stty -g`.chomp
50
+ begin
51
+ line = Readline.readline("#{query}: ", true)
52
+ rescue Interrupt
53
+ system('stty', stty_save) # Restore
54
+ exit
55
+ end
56
+ line.chomp
57
+ end
58
+
59
+ def get_text(query = 'Enter text, ^d to end')
60
+ stty_save = `stty -g`.chomp
61
+ lines = []
62
+ puts query
63
+ begin
64
+ while (line = Readline.readline)
65
+ lines << line
66
+ end
67
+ rescue Interrupt
68
+ system('stty', stty_save) # Restore
69
+ exit
70
+ end
71
+ lines.join("\n").chomp
72
+ end
73
+
74
+ def url_encode_text
75
+ text = get_text
76
+ puts
77
+ CGI.escape(text)
78
+ end
79
+ end
80
+
81
+ # Single menu item
82
+ class MenuItem
83
+ attr_accessor :id, :title, :value
84
+
85
+ def initialize(id, title, value)
86
+ @id = id
87
+ @title = title
88
+ @value = value
89
+ end
90
+ end
91
+
92
+ # Collection of menu items
93
+ class Menu
94
+ include Prompt
95
+ attr_accessor :items
96
+
97
+ def initialize(items)
98
+ @items = items
99
+ end
100
+
101
+ def choose(query = 'Select an item')
102
+ throw 'No items initialized' if @items.nil?
103
+ STDERR.puts
104
+ STDERR.puts "┌#{("─" * 74)}┐"
105
+ intpad = Math::log10(@items.length).to_i + 1
106
+ @items.each_with_index do |item, idx|
107
+ idxstr = "%#{intpad}d" % (idx + 1)
108
+ line = "#{idxstr}: #{item.title}"
109
+ pad = 74 - line.length
110
+ STDERR.puts "│#{line}#{" " * pad}│"
111
+ end
112
+ STDERR.puts "└┤ #{query} ├#{"─" * (70 - query.length)}┘"
113
+ sel = choose_number("> ", @items.length)
114
+ sel ? @items[sel.to_i - 1] : nil
115
+ end
116
+ end
117
+
118
+ class Snippet
119
+ attr_accessor :fragments, :contents
120
+
121
+ def initialize(file)
122
+ if File.exist?(File.expand_path(file))
123
+ @contents = IO.read(File.expand_path(file))
124
+ @fragments = fragments
125
+ else
126
+ throw ('Tried to initialize snippet with invalid file')
127
+ end
128
+ end
129
+
130
+ def fragments
131
+ rx = /(?i-m)(?:[-#]+)\[([\s\S]*?)\][-# ]*\n([\s\S]*?)(?=\n(?:-+\[|#+\[|$))/
132
+ matches = @contents.scan(rx)
133
+ fragments = {}
134
+ matches.each do |m|
135
+ key = m[0]
136
+ value = m[1]
137
+
138
+ fragments[key] = value
139
+ end
140
+ fragments
141
+ end
142
+
143
+ def choose_fragment
144
+ unless @fragments.empty?
145
+ items = []
146
+ @fragments.each { |k, v| items << MenuItem.new(k, k, v) }
147
+ menu = Menu.new(items)
148
+ return menu.choose('Select fragment')
149
+ end
150
+ nil
151
+ end
152
+ end
153
+
154
+ # File search functions
155
+ class BunchFinder
156
+ include Prompt
157
+ attr_accessor :config_dir
158
+
159
+ def initialize
160
+ config_dir = `defaults read com.brettterpstra.bunch configDir`.strip
161
+ config_dir = File.expand_path(config_dir)
162
+ if File.directory?(config_dir)
163
+ @config_dir = config_dir
164
+ else
165
+ throw 'Unable to retrieve Bunches Folder'
166
+ end
167
+ end
168
+
169
+ def files_to_items(dir, pattern)
170
+ Dir.chdir(dir)
171
+ items = []
172
+ Dir.glob(pattern) do |f|
173
+ if f.text?
174
+ filename = File.basename(f)
175
+ items << MenuItem.new(filename, filename, filename)
176
+ end
177
+ end
178
+ items
179
+ end
180
+
181
+ def choose_bunch
182
+ items = files_to_items(@config_dir, '*.bunch')
183
+ items.map! do |item|
184
+ item.title = File.basename(item.title, '.bunch')
185
+ item.value = File.basename(item.title, '.bunch')
186
+ item
187
+ end
188
+ menu = Menu.new(items)
189
+ menu.choose('Select a Bunch')
190
+ end
191
+
192
+ def choose_snippet
193
+ items = files_to_items(@config_dir, '*')
194
+ menu = Menu.new(items)
195
+ menu.choose('Select a Snippet')
196
+ end
197
+
198
+ def expand_path(file)
199
+ File.join(@config_dir, file)
200
+ end
201
+
202
+ def contents(snippet)
203
+ IO.read(File.join(@config_dir, snippet))
204
+ end
205
+
206
+ def variables(content)
207
+ matches = content.scan(/\$\{(\S+)(:.*?)?\}/)
208
+ variables = []
209
+ matches.each { |m| variables << m[0].sub(/:\S+$/, '') }
210
+ variables.uniq
211
+ end
212
+
213
+ def fill_variables(text)
214
+ vars = variables(text)
215
+ output = []
216
+ unless vars.empty?
217
+ puts 'Enter values for variables'
218
+ vars.each do |var|
219
+ res = get_line(var)
220
+ output << [var, CGI.escape(res)] unless res.empty?
221
+ end
222
+ end
223
+ output
224
+ end
225
+ end
226
+
227
+ class BunchURLGenerator
228
+ include Prompt
229
+ include Util
230
+
231
+ def generate
232
+ menu_items = [
233
+ MenuItem.new('open', 'Open a Bunch', 'open'),
234
+ MenuItem.new('close', 'Close a Bunch', 'close'),
235
+ MenuItem.new('toggle', 'Toggle a Bunch', 'toggle'),
236
+ MenuItem.new('snippet', 'Load a Snippet', 'snippet'),
237
+ MenuItem.new('raw', 'Load raw text', 'raw')
238
+ ]
239
+
240
+ menu = Menu.new(menu_items)
241
+ finder = BunchFinder.new
242
+
243
+ selection = menu.choose
244
+ Process.exit 0 unless selection
245
+ url = "x-bunch://#{selection.value}"
246
+ parameters = []
247
+ case selection.id
248
+ when /(open|close|toggle)/
249
+ parameters << ['bunch', CGI.escape(finder.choose_bunch.value)]
250
+ when /snippet/
251
+ filename = finder.choose_snippet.value
252
+ parameters << ['file', filename]
253
+ filename = finder.expand_path(filename)
254
+ snippet = Snippet.new(filename)
255
+ fragment = snippet.choose_fragment
256
+ if fragment
257
+ parameters << ['fragment', CGI.escape(fragment.title)]
258
+ contents = fragment.value
259
+ else
260
+ contents = snippet.contents
261
+ end
262
+ variables = finder.fill_variables(contents)
263
+ parameters.concat(variables) if variables.length
264
+ when /raw/
265
+ parameters << ['text', menu.url_encode_text]
266
+ else
267
+ Process.exit 0
268
+ end
269
+
270
+ menu.items = [
271
+ MenuItem.new('app', 'Application', 'find_bid'),
272
+ MenuItem.new('url', 'URL', 'get_line(')
273
+ ]
274
+
275
+ selection = menu.choose('Add success action? (Enter to skip)')
276
+
277
+ if selection
278
+ case selection.id
279
+ when /app/
280
+ app = get_line('Application name')
281
+ value = bundle_id(app)
282
+ when /url/
283
+ value = get_line('URL')
284
+ end
285
+
286
+ parameters << ['x-success', value] if value
287
+
288
+ delay = get_line('Delay for success action')
289
+ parameters << ['x-delay', delay.to_s] if delay =~ /^\d+$/
290
+ end
291
+
292
+ query_string = parameters.map { |param| "#{param[0]}=#{param[1]}" }.join('&')
293
+
294
+ puts url + '?' + query_string
295
+ end
296
+ end
@@ -1,3 +1,3 @@
1
1
  module BunchCLI
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bunchcli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-19 00:00:00.000000000 Z
11
+ date: 2021-01-24 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -21,6 +21,7 @@ files:
21
21
  - ".gitignore"
22
22
  - CHANGELOG.md
23
23
  - Gemfile
24
+ - Gemfile.lock
24
25
  - LICENSE.txt
25
26
  - README.md
26
27
  - Rakefile
@@ -28,6 +29,7 @@ files:
28
29
  - bunchcli.gemspec
29
30
  - lib/bunch.rb
30
31
  - lib/bunch/bunchCLI.rb
32
+ - lib/bunch/url_generator.rb
31
33
  - lib/bunch/version.rb
32
34
  homepage: https://brettterpstra.com/projects/bunch
33
35
  licenses: