tmb 0.0.5 → 0.0.6

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.
data/lib/tmb.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'rubygems'
2
+ require 'json'
3
+ require 'open-uri'
4
+ require 'uri'
5
+ require 'yaml'
6
+
7
+ module TMB
8
+
9
+
10
+ ROOT = File.expand_path( File.join( File.dirname(__FILE__), ".."))
11
+
12
+
13
+ class << self
14
+
15
+
16
+ def require_lib(*paths)
17
+ paths.each do |path|
18
+ path = File.join([ ROOT, "lib", "tmb", path.to_s + ".rb" ].flatten)
19
+ require path
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ TMB.require_lib(:bundle, :bundles, :commands)
data/lib/tmb/bundle.rb ADDED
@@ -0,0 +1,376 @@
1
+ module TM
2
+
3
+ TextWrap = 78
4
+ IndentSpacing = 10
5
+ Indent = (" " * IndentSpacing)
6
+ Justify = 15
7
+ Delimiter = ": "
8
+ BundleDirectory = "/Library/Application Support/TextMate/Bundles"
9
+ App = File.basename(__FILE__)
10
+ DB = ".tmbdb"
11
+ SearchDB = ".searchdb"
12
+
13
+ Help = <<-eos
14
+
15
+ \033[1m#{App}\033[0m is a utility to search for textmate bundles, download and install
16
+ them, all via a convenient command line interface, much like rubygems.
17
+
18
+
19
+ Usage:
20
+ ======================================
21
+
22
+ # Search for bundles containing the word 'webrat' in
23
+ # the title, description, or author's name.
24
+
25
+ \033[1m#{App} search webrat\033[0m
26
+
27
+
28
+ # Search for bundles containing the word 'rspec' OR
29
+ # 'cucumber' OR 'shoulda' in their title, description,
30
+ # or author's name.
31
+
32
+ \033[1m#{App} search rspec cucumber shoulda\033[0m
33
+
34
+
35
+ # Install a bundle containing the word rspec in its
36
+ # search fields. If multiple matches exist, #{App}
37
+ # will let you choose which version to install.
38
+
39
+ \033[1m#{App} install rspec\033[0m
40
+
41
+
42
+ # List all installed bundles
43
+
44
+ \033[1m#{App} list\033[0m
45
+
46
+
47
+ # Uninstall bundle matching 'json'. If you type in a
48
+ # fragment and there are multiple installed bundles that begin
49
+ # with that fragment, #{App} will let you choose which version
50
+ # you'd like to destroy
51
+
52
+ \033[1m#{App} uninstall json\033[0m
53
+
54
+
55
+ # Tell textmate (if it's open) to reload its bundle information,
56
+ # and update menus & controls accordingly
57
+
58
+ \033[1m#{App} reload\033[0m
59
+
60
+
61
+ # Print this help information
62
+
63
+ \033[1m#{App} help\033[0m
64
+
65
+
66
+
67
+ eos
68
+
69
+ end
70
+
71
+
72
+ class String
73
+
74
+ def indent(indent=TM::Indent)
75
+ indent + self
76
+ end
77
+
78
+ def text_wrap(width=TM::TextWrap, indent=TM::Indent)
79
+ self.gsub /(.{1,#{width}})(\s+|\Z)/, "\\1\n".indent(indent)
80
+ end
81
+
82
+ def bold
83
+ "\033[1m" + self + "\033[0m"
84
+ end
85
+
86
+ end
87
+
88
+ module TM
89
+
90
+ class Bundle
91
+
92
+
93
+ attr_accessor :result, :repository, :install_output, :before_hooks, :output
94
+
95
+ def self.before(instance, hook, method, args={})
96
+ instance.before_hooks[hook] = {:method => method, :args => args}
97
+ end
98
+
99
+ def before(hook, method, args=nil)
100
+ self.before_hooks[hook] ||= {}
101
+ self.before_hooks[hook] = { :method => method, :args => args }
102
+ end
103
+
104
+ def run_before(method)
105
+ self.send(self.before_hooks[method][:method], self.before_hooks[method][:args]) if self.before_hooks[method]
106
+ end
107
+
108
+ def initialize(result=nil, options={})
109
+ @before_hooks = @result = result
110
+ @repository = options[:repo] || git_repo
111
+ before(:git_install_script, :update_db)
112
+ end
113
+
114
+ def self.installed
115
+ Dir.glob("#{BundleDirectory}/*.tmbundle").sort{|a,b| a.downcase <=> b.downcase }
116
+ end
117
+
118
+ def self.db_filename
119
+ File.join(BundleDirectory, DB)
120
+ end
121
+
122
+ def self.db(settings=nil)
123
+ f = File.open(db_filename,"w+")
124
+ unless settings.nil?
125
+ parsed = settings.merge(YAML::parse( f.read ) || {})
126
+ f.puts YAML::dump( parsed )
127
+ end
128
+ db_content = f.read
129
+ f.close
130
+ YAML::parse( db_content )
131
+ end
132
+
133
+ def self.info
134
+ File.read(db_filename)
135
+ end
136
+
137
+ def self.list
138
+ installed.each do |b|
139
+ puts File.basename(b)
140
+ end
141
+ end
142
+
143
+ def self.select(bundle)
144
+ installed.select{|b| b =~ Regexp.new(bundle + ".*\.tmbundle$") }
145
+ end
146
+
147
+ def self.uninstall(bundle)
148
+ bundle_dir = File.basename(bundle)
149
+ FileUtils.rm_r(File.join(BundleDirectory, bundle_dir))
150
+ end
151
+
152
+ def short_result
153
+ {:description => result["description"], :repo => repository }
154
+ end
155
+
156
+ def name
157
+ result["name"] || repository.scan(/(\w\-0-9)\.git$/)
158
+ end
159
+
160
+ def bundle_name
161
+ name.gsub("tmbundle",'').gsub(/^[[:punct:]]+|[[:punct:]]+$/,'')
162
+ end
163
+
164
+ def git_repo
165
+ "https://github.com/#{@result["username"]}/#{@result["name"]}.git"
166
+ end
167
+
168
+ def display_key(key, options={})
169
+ defaults = {:ljust => Justify, :rjust => Justify, :delimiter => Delimiter, :key_prefix => "", :key_suffix => ""}
170
+ options = defaults.merge options
171
+
172
+ if options[:bold_key]
173
+ options[:key_prefix] = "\033[1m" + options[:key_prefix]
174
+ options[:key_suffix] = options[:key_suffix] +"\033[0m"
175
+ end
176
+ if options[:title].nil? || options[:title].strip.length == 0
177
+ options[:title] = ""
178
+ options[:delimiter] = ""#(" " * options[:delimiter].length)
179
+ options[:ljust] = 0
180
+ end
181
+ options[:title] ||= key.to_s
182
+ options[:key_prefix] + (options[:title].capitalize + options[:delimiter]).ljust(options[:ljust]) + options[:key_suffix]
183
+ end
184
+
185
+ def display_keypair(key, options={})
186
+ defaults = {:title => key.to_s, :ljust => Justify, :rjust => Justify, :delimiter => Delimiter, :key_prefix => "", :key_suffix => "", :value_prefix => "", :value_suffix => ""}
187
+ options = defaults.merge(options)
188
+ if options[:bold]
189
+ options[:bold_value] ||= true
190
+ options[:bold_key] ||= true
191
+ end
192
+ if options[:bold_value]
193
+ options[:value_prefix] = "\033[1m" + options[:value_prefix]
194
+ options[:value_suffix] = options[:value_suffix] +"\033[0m"
195
+ end
196
+ [
197
+ display_key(options[:title], options),
198
+ options[:value_prefix] + ((options[:value] || (key.is_a?(Symbol) && self.methods.include?(key.to_s)) ? self.send(key) : @result[key.to_s]).to_s) + options[:value_suffix]
199
+ ].compact.reject{|s| s.strip == "" }.join
200
+ end
201
+
202
+
203
+
204
+ def display_value(key, options={})
205
+ display_keypair(key, options)
206
+ end
207
+
208
+ def extended_display(key, options={})
209
+ #options[:indent] ||= true
210
+ ed = display_value(key, options).to_s
211
+ unless options[:indent] == false
212
+ ed = ed.indent
213
+ end
214
+ if options[:wrap]
215
+ ed = ed.text_wrap
216
+ end
217
+ if options[:newline]
218
+ ed = ("\n" * options[:newline]) + ed
219
+ end
220
+ ed
221
+ end
222
+
223
+ def stats
224
+ "\n" + [
225
+ display_value(:followers, :ljust => Justify ),
226
+ display_value(:forks, :ljust => Justify ),
227
+ display_value(:watchers, :ljust => Justify )
228
+ ].map{|v| v.indent(" " * (IndentSpacing + Justify))}.join("\n")
229
+ end
230
+
231
+ def popularity
232
+ result["watchers"].to_i || 0
233
+ end
234
+
235
+ def short_stats
236
+ "(" + ["followers", "forks", "watchers"].map{|s| result[s.to_s].to_s + " " + s.to_s}.join(", ") + ")"
237
+ end
238
+
239
+ def as_selection(index)
240
+ "#{(index + 1).to_s}) #{git_repo.ljust(60)} \033[1m#{short_stats}\033[0m"
241
+ end
242
+
243
+ def display_map
244
+ [
245
+ {:v=> "name", :bold => true, :indent => true, :title => "", :value_prefix => "\e[1;32m"},
246
+ {:v => "username", :title => "Author", :bold_value => true, :newline => 1},
247
+ {:v => "homepage"},
248
+ {:v => :repository, :bold_value => true},
249
+ {:v => :stats, :delimiter => "", :title => ""},
250
+ {:v => "created_at", :newline => 1},
251
+ {:v=> "description", :indent => false, :newline => 2, :title => "", :wrap => true}
252
+ ]
253
+ end
254
+
255
+ def to_s
256
+ display_map.map{|d| extended_display(d.delete(:v), d) }.join("\n") + "\n"
257
+ end
258
+
259
+ def destination
260
+ File.join(BundleDirectory, "#{bundle_name}.tmbundle")
261
+ end
262
+
263
+ def common_install_script
264
+ <<-eos
265
+ bundle_dir="#{BundleDirectory}"
266
+ mkdir -p "$bundle_dir"
267
+ file_name=$(echo -e #{@repository} | grep -o -P "[-\w\.]+$")
268
+ bundle_name=$(echo -e $file_name | sed -E -e 's/\.[a-zA-Z0-9]+$//g' -e 's/\.tmbundle//g')
269
+ dest="#{destination}"
270
+ rm -Rf "$dest"
271
+ eos
272
+ end
273
+
274
+
275
+ def git_install_script
276
+ <<-eos
277
+ #{common_install_script}
278
+ git clone #{@repository} "$dest"
279
+ eos
280
+ end
281
+
282
+ def bundle_reload_script
283
+ "osascript -e 'tell app \"TextMate\" to reload bundles'"
284
+ end
285
+
286
+ def smart_extract_script(archive)
287
+ <<-eos
288
+ arch=#{archive}
289
+ if [ -f $arch ]; then
290
+ case $arch in
291
+ *.tar.bz2) tar -jxvf $arch ;;
292
+ *.tar.gz) tar -zxvf $arch ;;
293
+ *.bz2) bunzip2 $arch ;;
294
+ *.dmg) hdiutil mount $arch ;;
295
+ *.gz) gunzip $arch ;;
296
+ *.tar) tar -xvf $arch ;;
297
+ *.tbz2) tar -jxvf $arch ;;
298
+ *.tgz) tar -zxvf $arch ;;
299
+ *.zip) unzip $arch ;;
300
+ *.Z) uncompress $arch ;;
301
+ *) echo "'$arch' cannot be extracted/mounted via smartextract()" ;;
302
+ esac
303
+ else
304
+ echo "'$arch' is not a valid file"
305
+ fi
306
+ eos
307
+ end
308
+
309
+ def archive_install_script
310
+ <<-eos
311
+ #{common_install_script}
312
+ cd $bundle_dir
313
+ if [[ -n $bundle_name ]]
314
+ then
315
+ rm -R "$bundle_dir/$bundle_name"*
316
+ fi
317
+ curl -o "$bundle_dir/$file_name" $1
318
+ #{ smart_extract_script('$bundle_dir/$file_name')}
319
+ bundle=$(find $bundle_dir/$bundle_name | grep -P "tmbundle$")
320
+ if [[ -n $bundle ]]
321
+ then
322
+ cp -R $bundle $bundle_dir
323
+ fi
324
+ non_bundles=$(find $bundle_dir -d 1 | grep -v -P "tmbundle$|^\.")
325
+ echo $non_bundles | xargs -Ixxx rm -Rf "xxx"
326
+ cd $current
327
+ osascript -e 'tell app "TextMate" to reload bundles'
328
+ eos
329
+ end
330
+
331
+ def git_repo?
332
+ File.exists?(File.join(destination,".git"))
333
+ end
334
+
335
+ def hash(digester="sha1")
336
+ if git_repo?
337
+ op = File.read(File.join(destination,".git","refs","heads","master"))
338
+ else
339
+ op = IO.popen("cat `ls -F #{destination} | grep -v -E '\/$'` | #{digester} | tr '\n' ''").read
340
+ end
341
+ op.strip
342
+ end
343
+
344
+ def db_hash
345
+ { name => {"url" => repository, "dir" => destination, "installed" => Time.now.to_s, "sha1" => hash}}
346
+ end
347
+
348
+ def update_db
349
+ self.class.db(db_hash)
350
+ end
351
+
352
+ def run_script(method)
353
+ @output = IO.popen(self.send(method)).read
354
+ end
355
+
356
+ def bundle_exists?
357
+ File.exists?(destination)
358
+ end
359
+
360
+ def install
361
+ if bundle_exists?
362
+ puts "That bundle already appears to be installed at #{destination}.\n\nReinstall? [Yn]"
363
+ answer = STDIN.gets.chomp
364
+ return if answer == "n"
365
+ end
366
+ puts "Installing from #{@repository}"
367
+ run_script(:git_install_script)
368
+ puts "Bundle installed to #{destination}. \n\nReloading bundles..."
369
+ run_script(:bundle_reload_script)
370
+ puts "All done!"
371
+ end
372
+
373
+
374
+ end
375
+ end
376
+
@@ -0,0 +1,205 @@
1
+
2
+ module TM
3
+ class Bundles
4
+
5
+ @@search_file = File.join(TM::BundleDirectory, TM::SearchDB)
6
+
7
+ attr_accessor :response, :results, :search_url, :search_terms, :full_set
8
+
9
+ def self.searchdb_file
10
+ @@search_file
11
+ end
12
+
13
+ def self.searchdb?
14
+ File.exists?(searchdb_file)
15
+ end
16
+
17
+ def initialize(search_terms=nil)
18
+ @search_terms = search_terms
19
+ @full_set = nil
20
+ @results = []
21
+ end
22
+
23
+ def search_db(new_entries=nil)
24
+ f = File.open(self.class.searchdb_file,"w+")
25
+ file_contents = f.read
26
+ if new_entries
27
+
28
+ entries = file_contents.length < 2 ? [] : JSON.parse(f.read)
29
+ all_entries = [new_entries, entries].flatten.uniq
30
+ to_file = JSON.generate(all_entries)
31
+ f.puts to_file
32
+ else
33
+ all_entries = file_contents.length < 2 ? [] : JSON.parse(f.read)
34
+ end
35
+ f.close
36
+ return all_entries
37
+ end
38
+
39
+ def self.handle_connection_error(e, url)
40
+ error = e.class.name.split("::")
41
+ exception = error[-1]
42
+ lib = error[0]
43
+ lib = lib == exception ? "OpenURI" : lib
44
+ host = URI.parse(url).host
45
+ error_message = case exception
46
+ when "SocketError" : socket_error(e,url)
47
+ when "HTTPError" : http_error(e,url)
48
+ end
49
+ puts error_message.
50
+ gsub(/#URL/,url).
51
+ gsub(/#LIB/,lib).
52
+ gsub(/#MESSAGE/,e.message).
53
+ gsub(/#EXCEPTION/,exception).
54
+ gsub(/#HOST/,host)
55
+ exit 0
56
+ end
57
+
58
+ def self.socket_error(e, url)
59
+ <<-eos
60
+
61
+ #LIB is raising a #EXCEPTION: \033[1m#{e.message}\033[0m
62
+
63
+ Either \033[1m#HOST\033[0m is currently unavailable or your internet connection is down.
64
+
65
+ (We were trying to access \033[1m#URL\033[0m)
66
+
67
+ eos
68
+ end
69
+
70
+ def self.http_code_messages
71
+ {
72
+ "404" => "the page you're attempting to access doesn't exist"
73
+ }
74
+ end
75
+
76
+ def self.http_error(e, url)
77
+ <<-eos
78
+
79
+ #LIB is raising an #EXCEPTION: \033[1m#MESSAGE\033[0m
80
+
81
+ That means #{http_code_messages[e.message.match(/\d{3}/).to_s]}
82
+
83
+ (We were trying to access \033[1m#URL\033[0m)
84
+
85
+ eos
86
+ end
87
+
88
+ def search_description(terms=@search_terms)
89
+ "Searching for \033[1m#{terms.to_a.join(', ')}\033[0m"
90
+ end
91
+
92
+ def put_search_description
93
+ puts "\n" + search_description + "\n"
94
+ end
95
+
96
+
97
+ def short_result(result)
98
+ {:description => result["description"], :repo => git_repo(result) }
99
+ end
100
+
101
+ def sample_response
102
+ '{"repositories": [{"name": "haml", "username": "justin"}]}'
103
+ end
104
+
105
+ def get_search_pages(url, page_range=1)
106
+ result_set = []
107
+ page_range.times do |i|
108
+ page = i + 1
109
+ full_url = url + "?start_page=#{page}"
110
+ puts "Reading page #{full_url}"
111
+ begin
112
+ res = open(full_url).read
113
+ rescue => e
114
+ self.class.handle_connection_error(e,search_url)
115
+ end
116
+ json_res = JSON.parse(res)
117
+ repos = json_res["repositories"] || []
118
+ #puts repos.map{|r| r["name"]}.join(",")
119
+ puts "#{repos.size} more repositories found, #{result_set.size} total #{i==page_range ? '' : '(still searching...)'}"
120
+ if repos.size > 0
121
+ result_set << repos
122
+ result_set = result_set.flatten.uniq
123
+
124
+ else
125
+ puts "End of results"
126
+ break
127
+ end
128
+ end
129
+ result_set
130
+ end
131
+
132
+ def search(search_terms = @search_terms, options={})
133
+
134
+ bundle_suffix = ["textmate"]
135
+ bundle_identifier = /bundle|tmbundle|textmate/
136
+ options[:additive] ||= true
137
+ # Don't add tmbundle term if its already in the users search
138
+ regex_terms = Regexp.new(search_terms.to_a.select{|s| s.length > 1 }.join("|"))
139
+ actual_search_terms = search_terms.nil? ? bundle_suffix : [ search_terms ]
140
+ formatted_terms = actual_search_terms.flatten.compact.uniq.join(' ')
141
+ search_url="http://github.com/api/v2/json/repos/search/#{URI.escape(formatted_terms)}"
142
+ #puts "Searching for #{formatted_terms}"
143
+ if search_terms.nil? && @full_set.nil?
144
+ #puts "Getting full set"
145
+ @full_set = get_search_pages(search_url, 20)
146
+ puts "#{@full_set.size} Repositories found"
147
+ search_db(@full_set)
148
+ @full_set.each_with_index do |r,i|
149
+ @full_set[i]["search_terms"] = r.values_at("username", "name", "description").join
150
+ end
151
+ @results = @full_set
152
+ else
153
+ # puts "Searching for matching results for #{search_terms.to_a.join(',')}..."
154
+ @results = @results.to_a.select{|r| r["search_terms"] =~ regex_terms}
155
+ end
156
+ @results = @results.uniq
157
+ current_results = search_terms.nil? ? @full_set : @results
158
+ return current_results.uniq
159
+ end
160
+
161
+ def search_local(terms)
162
+ @results = @full_set = JSON.parse( File.read(@@search_file) )
163
+ puts "#{@full_set.size} local results"
164
+ @full_set.each_with_index do |r,i|
165
+ @full_set[i]["search_terms"] = r.values_at("username", "name", "description").join
166
+ end
167
+ @search_terms = terms
168
+ search(terms)
169
+ end
170
+
171
+
172
+ def display_results(res=nil)
173
+ res ||= results
174
+ to_display = res || search(search_terms)
175
+ to_display.sort{|a,b| a["name"].downcase <=> b["name"].downcase }.map{|r| TM::Bundle.new(r).to_s }.join("\n\n")
176
+ end
177
+
178
+ def bash_install_script_from_repo(repo)
179
+ <<-eos
180
+ bundle_dir="/Library/Application Support/TextMate/Bundles"
181
+ mkdir -p "$bundle_dir"
182
+ file_name=$(echo -e #{repo} | grep -o -P "[-\w\.]+$")
183
+ bundle_name=$(echo -e $file_name | sed -E -e 's/\.[a-zA-Z0-9]+$//g' -e 's/\.tmbundle//g')
184
+ dest=$bundle_dir/$bundle_name.tmbundle
185
+ rm -Rf "$dest"
186
+ git clone #{repo} "$dest"
187
+ osascript -e 'tell app "TextMate" to reload bundles'
188
+ eos
189
+ end
190
+
191
+ def install(repo)
192
+ puts repo
193
+ if repo.match(/^(http|git|https):.*\.git$/)
194
+ matching_repo = TM::Bundle.new([], :repo => repo)
195
+ elsif repo.match(/^(http|ftp|https):.*\.(zip|gz|tar.gz|bz2|tar.bz2)$/)
196
+ matching_repo = TM::Bundle.new([], :repo => repo)
197
+ else
198
+ matching_repo = TM::Bundle.new(search(repo).first)
199
+ end
200
+ install_output = matching_repo.install
201
+ puts install_output
202
+ end
203
+
204
+ end
205
+ end