boom 0.0.1

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/License ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) Zach Holman, http://zachholman.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,150 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'date'
4
+
5
+ #############################################################################
6
+ #
7
+ # Helper functions
8
+ #
9
+ #############################################################################
10
+
11
+ def name
12
+ @name ||= Dir['*.gemspec'].first.split('.').first
13
+ end
14
+
15
+ def version
16
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
17
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
18
+ end
19
+
20
+ def date
21
+ Date.today.to_s
22
+ end
23
+
24
+ def rubyforge_project
25
+ name
26
+ end
27
+
28
+ def gemspec_file
29
+ "#{name}.gemspec"
30
+ end
31
+
32
+ def gem_file
33
+ "#{name}-#{version}.gem"
34
+ end
35
+
36
+ def replace_header(head, header_name)
37
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
38
+ end
39
+
40
+ #############################################################################
41
+ #
42
+ # Standard tasks
43
+ #
44
+ #############################################################################
45
+
46
+ task :default => :test
47
+
48
+ require 'rake/testtask'
49
+ Rake::TestTask.new(:test) do |test|
50
+ test.libs << 'lib' << 'test'
51
+ test.pattern = 'test/**/test_*.rb'
52
+ test.verbose = true
53
+ end
54
+
55
+ desc "Generate RCov test coverage and open in your browser"
56
+ task :coverage do
57
+ require 'rcov'
58
+ sh "rm -fr coverage"
59
+ sh "rcov test/test_*.rb"
60
+ sh "open coverage/index.html"
61
+ end
62
+
63
+ require 'rake/rdoctask'
64
+ Rake::RDocTask.new do |rdoc|
65
+ rdoc.rdoc_dir = 'rdoc'
66
+ rdoc.title = "#{name} #{version}"
67
+ rdoc.rdoc_files.include('README*')
68
+ rdoc.rdoc_files.include('lib/**/*.rb')
69
+ end
70
+
71
+ desc "Open an irb session preloaded with this library"
72
+ task :console do
73
+ sh "irb -rubygems -r ./lib/#{name}.rb"
74
+ end
75
+
76
+ #############################################################################
77
+ #
78
+ # Custom tasks (add your own tasks here)
79
+ #
80
+ #############################################################################
81
+
82
+
83
+
84
+ #############################################################################
85
+ #
86
+ # Packaging tasks
87
+ #
88
+ #############################################################################
89
+
90
+ desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
91
+ task :release => :build do
92
+ unless `git branch` =~ /^\* master$/
93
+ puts "You must be on the master branch to release!"
94
+ exit!
95
+ end
96
+ sh "git commit --allow-empty -a -m 'Release #{version}'"
97
+ sh "git tag v#{version}"
98
+ sh "git push origin master"
99
+ sh "git push origin v#{version}"
100
+ sh "gem push pkg/#{name}-#{version}.gem"
101
+ end
102
+
103
+ desc "Build #{gem_file} into the pkg directory"
104
+ task :build => :gemspec do
105
+ sh "mkdir -p pkg"
106
+ sh "gem build #{gemspec_file}"
107
+ sh "mv #{gem_file} pkg"
108
+ end
109
+
110
+ desc "Generate #{gemspec_file}"
111
+ task :gemspec => :validate do
112
+ # read spec file and split out manifest section
113
+ spec = File.read(gemspec_file)
114
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
115
+
116
+ # replace name version and date
117
+ replace_header(head, :name)
118
+ replace_header(head, :version)
119
+ replace_header(head, :date)
120
+ #comment this out if your rubyforge_project has a different name
121
+ replace_header(head, :rubyforge_project)
122
+
123
+ # determine file list from git ls-files
124
+ files = `git ls-files`.
125
+ split("\n").
126
+ sort.
127
+ reject { |file| file =~ /^\./ }.
128
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
129
+ map { |file| " #{file}" }.
130
+ join("\n")
131
+
132
+ # piece file back together and write
133
+ manifest = " s.files = %w[\n#{files}\n ]\n"
134
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
135
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
136
+ puts "Updated #{gemspec_file}"
137
+ end
138
+
139
+ desc "Validate #{gemspec_file}"
140
+ task :validate do
141
+ libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
142
+ unless libfiles.empty?
143
+ puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
144
+ exit!
145
+ end
146
+ unless Dir['VERSION*'].empty?
147
+ puts "A `VERSION` file at root level violates Gem best practices."
148
+ exit!
149
+ end
150
+ end
data/Readme.markdown ADDED
@@ -0,0 +1,116 @@
1
+ # B O O M
2
+
3
+ See? Look, a tiny explosion: \*
4
+
5
+ ## What's a boom?
6
+
7
+ boom lets you access text snippets over your command line. I'm personally
8
+ aiming for exactly two use cases, but I'm almost positive there are thirteen
9
+ more. Here's a couple examples:
10
+
11
+ - **Your own [del.icio.us](http://delicious.com)-esque URL tracker.** When I
12
+ make [clever animated
13
+ gifs](http://github.com/holman/dotfiles/blob/master/bin/gifme) of my
14
+ coworkers, I tend to lose the URL, which is a total bummer since I want to
15
+ repeatedly repost these images well past their funny expiration date. boom
16
+ lets me easily access the good stuff for years to come.
17
+ - **Commonly-used email replies.** Everyone's got those stock replies in their
18
+ pocket for a few common use cases. Rather than keep some files strewn about
19
+ with the responses, boom gives me them on my ever-present command line.
20
+ - **Simple todos.** You can super-quickly drop items into lists and remove them
21
+ when finished. I'm a big fan of simple, straightforward stuff. Plus, it's a
22
+ Dropbox away from simple cloud syncing. Someone get Cultured Code on the line
23
+ THIS MAY BE RELEVANT TO THEIR INTERESTS!
24
+
25
+ We store everything in one JSON file in your home directory: `~/.boom`. The
26
+ structure is simple, too. Each individual item is tossed on a `list`, and you
27
+ can have multiple lists.
28
+
29
+ ## Show me the boom
30
+
31
+ ** Overview **
32
+
33
+ $ boom
34
+ gifs (5)
35
+ email (4)
36
+
37
+ ** Create a list **
38
+
39
+ $ boom urls
40
+ Boom! Created a new list called "urls".
41
+
42
+ ** Add an item **
43
+
44
+ # boom <list> <name> <value>
45
+ $ boom urls github https://github.com
46
+ Boom! "github" in "urls" is "https://github.com". Got it.
47
+
48
+ ** Copy an item's value to your clipboard **
49
+
50
+ $ boom github
51
+ Boom! Just copied https://github.com to your clipboard.
52
+
53
+ $ boom urls github
54
+ Boom! Just copied https://github.com to your clipboard.
55
+
56
+ ** List items in a list **
57
+
58
+ $ boom urls
59
+ blog http://zachholman.com
60
+ github http://github.com
61
+
62
+ ** Delete a list **
63
+
64
+ $ boom urls delete
65
+ You sure you want to delete everything in "urls"? (y/n): y
66
+ Boom! Deleted all your urls.
67
+
68
+ ** Delete an item **
69
+
70
+ # boom urls github delete
71
+ Boom! "github" is gone forever.
72
+
73
+ ** List everything **
74
+
75
+ $ boom all
76
+ enemies
77
+ @kneath: he's got dreamy eyes. he must die.
78
+ @rtomayko: i must murder him for his mac and cheese recipe.
79
+ @luckiestmonkey: she hates recycling.
80
+ urls
81
+ blog: http://zachholman.com
82
+ github: https://github.com
83
+
84
+ ** It's just the command line, silly **
85
+
86
+ So don't forget all your other favorites are there to help you, too:
87
+
88
+ $ boom all | grep @luckiestmonkey
89
+ @luckiestmonkey: she hates recycling.
90
+
91
+ ## Install
92
+
93
+ gem install boom
94
+
95
+ ## Current Status
96
+
97
+ Precarious. I'm starting to use this a bunch now, but if you're tossing in
98
+ important business information (say, a carefully cultivated list of animated
99
+ .gifs), you'd be sitting pretty if you made a backup of `~/.boom` every now and
100
+ then, just in case. We're living on the edge, baby.
101
+
102
+ Also, don't mistype anything. Everything should be 100% perfect if you type all
103
+ the commands correctly. If you don't, it'll probably ungracefully error out and
104
+ a puppy in Kansas will probably be murdered. You should be fine, though. As
105
+ long as you don't have butter fingers.
106
+
107
+ Soon enough, though, this'll be stable to the point where I can truthfully tell
108
+ myself that this time baby, `boom` will be, bulletttttttttttttproof ♫.
109
+
110
+ ## I love you
111
+
112
+ [Zach Holman](http://zachholman.com) made this. Ping me on Twitter —
113
+ [@holman](http://twitter.com/holman) — if you're having issues, want me to
114
+ merge in your pull request, or are using boom in a cool way. I'm kind of hoping
115
+ this is generic enough that people do some fun things with it. First one to use
116
+ `boom` to calculate their tax liability wins.
data/bin/boom ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
+
5
+ require 'boom'
6
+
7
+ storage = Boom::Storage.new
8
+ Boom::Command.execute(storage,*ARGV)
data/boom.gemspec ADDED
@@ -0,0 +1,90 @@
1
+ ## This is the rakegem gemspec template. Make sure you read and understand
2
+ ## all of the comments. Some sections require modification, and others can
3
+ ## be deleted if you don't need them. Once you understand the contents of
4
+ ## this file, feel free to delete any comments that begin with two hash marks.
5
+ ## You can find comprehensive Gem::Specification documentation, at
6
+ ## http://docs.rubygems.org/read/chapter/20
7
+ Gem::Specification.new do |s|
8
+ s.specification_version = 2 if s.respond_to? :specification_version=
9
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.rubygems_version = '1.3.5'
11
+
12
+ ## Leave these as is they will be modified for you by the rake gemspec task.
13
+ ## If your rubyforge_project name is different, then edit it and comment out
14
+ ## the sub! line in the Rakefile
15
+ s.name = 'boom'
16
+ s.version = '0.0.1'
17
+ s.date = '2010-11-24'
18
+ s.rubyforge_project = 'boom'
19
+
20
+ ## Make sure your summary is short. The description may be as long
21
+ ## as you like.
22
+ s.summary = "boom lets you access text snippets over your command line."
23
+ s.description = "God it's about every day where I think to myself, gadzooks,
24
+ I keep typing *REPETITIVE_BORING_TASK* over and over. Wouldn't it be great if
25
+ I had something like boom to store all these commonly-used text snippets for
26
+ me? Then I realized that was a worthless idea since boom hadn't been created
27
+ yet and I had no idea what that statement meant. At some point I found the
28
+ code for boom in a dark alleyway and released it under my own name because I
29
+ wanted to look smart."
30
+
31
+ ## List the primary authors. If there are a bunch of authors, it's probably
32
+ ## better to set the email to an email list or something. If you don't have
33
+ ## a custom homepage, consider using your GitHub URL or the like.
34
+ s.authors = ["Zach Holman"]
35
+ s.email = 'github.com@zachholman.com'
36
+ s.homepage = 'https://github.com/holman/boom'
37
+
38
+ ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
39
+ ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
40
+ s.require_paths = %w[lib]
41
+
42
+ ## This sections is only necessary if you have C extensions.
43
+ #s.require_paths << 'ext'
44
+ #s.extensions = %w[ext/extconf.rb]
45
+
46
+ ## If your gem includes any executables, list them here.
47
+ s.executables = ["boom"]
48
+ s.default_executable = 'boom'
49
+
50
+ ## Specify any RDoc options here. You'll want to add your README and
51
+ ## LICENSE files to the extra_rdoc_files list.
52
+ s.rdoc_options = ["--charset=UTF-8"]
53
+ s.extra_rdoc_files = %w[Readme.markdown License]
54
+
55
+ ## List your runtime dependencies here. Runtime dependencies are those
56
+ ## that are needed for an end user to actually USE your code.
57
+ s.add_dependency('yajl-ruby', "~> 0.7.8")
58
+
59
+ ## List your development dependencies here. Development dependencies are
60
+ ## those that are only needed during development
61
+ s.add_development_dependency('mocha', "~> 0.9.9")
62
+
63
+ ## Leave this section as-is. It will be automatically generated from the
64
+ ## contents of your Git repository via the gemspec task. DO NOT REMOVE
65
+ ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
66
+ # = MANIFEST =
67
+ s.files = %w[
68
+ License
69
+ Rakefile
70
+ Readme.markdown
71
+ bin/boom
72
+ boom.gemspec
73
+ lib/boom.rb
74
+ lib/boom/clipboard.rb
75
+ lib/boom/command.rb
76
+ lib/boom/item.rb
77
+ lib/boom/list.rb
78
+ lib/boom/storage.rb
79
+ test/examples/urls.json
80
+ test/helper.rb
81
+ test/test_command.rb
82
+ test/test_item.rb
83
+ test/test_list.rb
84
+ ]
85
+ # = MANIFEST =
86
+
87
+ ## Test files will be grabbed from the file list. Make sure the path glob
88
+ ## matches what you actually use.
89
+ s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
90
+ end
@@ -0,0 +1,22 @@
1
+ # Clipboard is a centralized point to shell out to each individual platform's
2
+ # clipboard, pasteboard, or whatever they decide to call it.
3
+ #
4
+ module Boom
5
+ class Clipboard
6
+ class << self
7
+
8
+ # Public: copies a given Item's value to the clipboard. This method is
9
+ # designed to handle multiple platforms.
10
+ #
11
+ # Returns nothing.
12
+ def copy(item)
13
+ if RUBY_PLATFORM =~ /darwin/
14
+ `echo '#{item.value}' | tr -d "\n" | pbcopy`
15
+ end
16
+
17
+ "Boom! We just copied #{item.value} to your clipboard."
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,212 @@
1
+ # Command is the main point of entry for boom commands; shell arguments are
2
+ # passd through to Command, which then filters and parses through indivdual
3
+ # commands and reroutes them to constituent object classes.
4
+ #
5
+ # Command also keeps track of one connection to Storage, which is how new data
6
+ # changes are persisted to disk. It takes care of any data changes by calling
7
+ # Boom::Command#save!.
8
+ #
9
+ module Boom
10
+ class Command
11
+ class << self
12
+
13
+ attr_accessor :storage
14
+
15
+ # Public: executes a command.
16
+ #
17
+ # storage - The Storage instance off which to run commands. This is
18
+ # likely just Boom::Storage.new, since Boom::Storage should
19
+ # pick up the appropriate JSON file paths on its own.
20
+ # args - The actual commands to operate on. Can be as few as zero
21
+ # arguments or as many as three.
22
+ def execute(storage,*args)
23
+ @storage = storage
24
+
25
+ command = args[0]
26
+ major = args[1]
27
+ minor = args[2]
28
+
29
+ return overview unless command
30
+ delegate(command, major, minor)
31
+ end
32
+
33
+ # Public: prints any given string.
34
+ #
35
+ # s = String output
36
+ #
37
+ # Prints to STDOUT and returns. This method exists to standardize output
38
+ # and for easy mocking or overriding.
39
+ def output(s)
40
+ puts(s)
41
+ end
42
+
43
+ # Public: prints a tidy overview of your Lists in descending order of
44
+ # number of Items.
45
+ #
46
+ # Returns nothing.
47
+ def overview
48
+ storage.lists.each do |list|
49
+ output " #{list.name} (#{list.items.size})"
50
+ end
51
+ end
52
+
53
+ # Public: prints the detailed view of all your Lists and all their
54
+ # Items.
55
+ #
56
+ # Returns nothing.
57
+ def all
58
+ storage.lists.each do |list|
59
+ output " #{list.name}"
60
+ list.items.each do |item|
61
+ output " #{item.name}: #{item.value}"
62
+ end
63
+ end
64
+ end
65
+
66
+ # Public: allows main access to most commands.
67
+ #
68
+ # Returns output based on method calls.
69
+ def delegate(command, major, minor)
70
+ return all if command == 'all'
71
+
72
+ # if we're operating on a List
73
+ if storage.list_exists?(command)
74
+ return list_delete(command) if major == 'delete'
75
+ return list_detail(command) unless major
76
+ unless minor == 'delete'
77
+ return add_item(command,major,minor) if minor
78
+ return search_list_for_item(command, major)
79
+ end
80
+ end
81
+
82
+ return search_items(command) if storage.item_exists?(command)
83
+
84
+ if minor == 'delete' and storage.item_exists?(major)
85
+ return item_delete(major)
86
+ end
87
+
88
+ return list_create(command)
89
+ end
90
+
91
+ # Public: prints all Items over a List.
92
+ #
93
+ # list - the List object to iterate over
94
+ #
95
+ # Returns nothing.
96
+ def list_detail(list_name)
97
+ list = storage.lists.first { |list| list.name == list_name }
98
+ list.items.sort{ |x,y| x.name <=> y.name }.each do |item|
99
+ output " #{item.name}: #{item.value}"
100
+ end
101
+ end
102
+
103
+ # Public: add a new List.
104
+ #
105
+ # name - the String name of the List.
106
+ #
107
+ # Example
108
+ #
109
+ # Commands.list_create("snippets")
110
+ #
111
+ # Returns the newly created List.
112
+ def list_create(name)
113
+ lists = (storage.lists << List.new(name))
114
+ storage.lists = lists
115
+ output "Boom! Created a new list called \"#{name}\"."
116
+ save!
117
+ end
118
+
119
+ # Public: remove a named List.
120
+ #
121
+ # name - the String name of the List.
122
+ #
123
+ # Example
124
+ #
125
+ # Commands.delete_list("snippets")
126
+ #
127
+ # Returns nothing.
128
+ def list_delete(name)
129
+ lists = storage.lists.reverse.reject { |list| list.name == name }
130
+ output "You sure you want to delete everything in \"#{name}\"? (y/n):"
131
+ if $stdin.gets.chomp == 'y'
132
+ storage.lists = lists
133
+ output "Boom! Deleted all your #{name}."
134
+ save!
135
+ else
136
+ output "Just kidding then."
137
+ end
138
+ end
139
+
140
+ # Public: add a new Item to a list.
141
+ #
142
+ # list - the String name of the List to associate with this Item
143
+ # name - the String name of the Item
144
+ # value - the String value of the Item
145
+ #
146
+ # Example
147
+ #
148
+ # Commands.add_item("snippets","sig","- @holman")
149
+ #
150
+ # Returns the newly created Item.
151
+ def add_item(list,name,value)
152
+ list = storage.lists.find{|storage_list| storage_list.name == list}
153
+ list.add_item(Item.new(name,value))
154
+ output "Boom! \"#{name}\" in \"#{list.name}\" is \"#{value}\". Got it."
155
+ save!
156
+ end
157
+
158
+ # Public: remove a named Item.
159
+ #
160
+ # name - the String name of the Item.
161
+ #
162
+ # Example
163
+ #
164
+ # Commands.delete_item("an-item-name")
165
+ #
166
+ # Returns nothing.
167
+ def item_delete(name)
168
+ storage.lists = storage.lists.each do |list|
169
+ list.items.reject! { |item| item.name == name }
170
+ end
171
+ output "Boom! \"#{name}\" is gone forever."
172
+ save!
173
+ end
174
+
175
+ # Public: search for an Item in all lists by name. Drops the
176
+ # corresponding entry into your clipboard.
177
+ #
178
+ # name - the String term to search for in all Item names
179
+ #
180
+ # Returns the matching Item.
181
+ def search_items(name)
182
+ item = storage.items.detect do |item|
183
+ item.name == name
184
+ end
185
+
186
+ output Clipboard.copy(item)
187
+ end
188
+
189
+ # Public: search for an Item in a particular list by name. Drops the
190
+ # corresponding entry into your clipboard.
191
+ #
192
+ # list_name - the String name of the List in which to scope the search
193
+ # item_name - the String term to search for in all Item names
194
+ #
195
+ # Returns the matching Item.
196
+ def search_list_for_item(list_name, item_name)
197
+ list = storage.lists.first { |list| list.name == list_name }
198
+ item = list.items.first { |item| item.name == item_name }
199
+
200
+ output Clipboard.copy(item)
201
+ end
202
+
203
+ # Public: save in-memory data to disk.
204
+ #
205
+ # Returns whether or not data was saved.
206
+ def save!
207
+ storage.save!
208
+ end
209
+
210
+ end
211
+ end
212
+ end
data/lib/boom/item.rb ADDED
@@ -0,0 +1,37 @@
1
+ # The representation of the base unit in boom. An Item contains just a name and
2
+ # a value. It doesn't know its parent relationship explicitly; the parent List
3
+ # object instead knows which Items it contains.
4
+ #
5
+ module Boom
6
+ class Item
7
+
8
+ # Public: the String name of the Item
9
+ attr_accessor :name
10
+
11
+ # Public: the String value of the Item
12
+ attr_accessor :value
13
+
14
+ # Public: creates a new Item object.
15
+ #
16
+ # name - the String name of the Item
17
+ # value - the String value of the Item
18
+ #
19
+ # Examples
20
+ #
21
+ # Item.new("github", "http://github.com")
22
+ #
23
+ # Returns the newly initialized Item.
24
+ def initialize(name,value)
25
+ @name = name
26
+ @value = value
27
+ end
28
+
29
+ # Public: creates a Hash for this Item.
30
+ #
31
+ # Returns a Hash of its data.
32
+ def to_hash
33
+ { @name => @value }
34
+ end
35
+
36
+ end
37
+ end
data/lib/boom/list.rb ADDED
@@ -0,0 +1,44 @@
1
+ # The List contains many Items. They exist as buckets in which to categorize
2
+ # individual Items. The relationship is maintained in a simple array on the
3
+ # List-level.
4
+ #
5
+ module Boom class List
6
+
7
+ # Public: creates a new List instance in-memory.
8
+ #
9
+ # name - The name of the List. Fails if already used.
10
+ #
11
+ # Returns the unpersisted List instance.
12
+ def initialize(name)
13
+ @items = []
14
+ @name = name
15
+ end
16
+
17
+ # Public: lets you access the array of items contained within this List.
18
+ #
19
+ # Returns an Array of Items.
20
+ attr_accessor :items
21
+
22
+ # Public: the name of the List.
23
+ #
24
+ # Returns the String name.
25
+ attr_accessor :name
26
+
27
+ # Public: associates an Item with this List.
28
+ #
29
+ # item - the Item object to associate with this List.
30
+ #
31
+ # Returns the current set of items.
32
+ def add_item(item)
33
+ @items << item
34
+ end
35
+
36
+ # Public: a Hash representation of this List.
37
+ #
38
+ # Returns a Hash of its own data and its child Items.
39
+ def to_hash
40
+ { name => items.collect(&:to_hash) }
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,125 @@
1
+ # Storage is the middleman between changes the client makes in-memory and how
2
+ # it's actually persisted to disk (and vice-versa). There are also a few
3
+ # convenience methods to run searches and operations on the in-memory hash.
4
+ #
5
+ module Boom
6
+ class Storage
7
+
8
+ JSON_FILE = "#{ENV['HOME']}/.boom"
9
+
10
+ # Public: the path to the JSON file used by boom.
11
+ #
12
+ # Returns the String path of boom's JSON representation.
13
+ def json_file
14
+ JSON_FILE
15
+ end
16
+
17
+ # Public: initializes a Storage instance by loading in your persisted data.
18
+ #
19
+ # Returns the Storage instance.
20
+ def initialize
21
+ @lists = []
22
+ explode_json(json_file)
23
+ end
24
+
25
+ # Public: the in-memory collection of all Lists attached to this Storage
26
+ # instance.
27
+ #
28
+ # lists - an Array of individual List items
29
+ #
30
+ # Returns nothing.
31
+ attr_writer :lists
32
+
33
+ # Public: the list of Lists in your JSON data, sorted by number of items
34
+ # descending.
35
+ #
36
+ # Returns an Array of List objects.
37
+ def lists
38
+ @lists.sort_by { |list| -list.items.size }
39
+ end
40
+
41
+ # Public: tests whether a named List exists.
42
+ #
43
+ # name - the String name of a List
44
+ #
45
+ # Returns true if found, false if not.
46
+ def list_exists?(name)
47
+ @lists.detect { |list| list.name == name }
48
+ end
49
+
50
+ # Public: all Items in storage.
51
+ #
52
+ # Returns an Array of all Items.
53
+ def items
54
+ @lists.collect(&:items).flatten
55
+ end
56
+
57
+ # Public: tests whether a named Item exists.
58
+ #
59
+ # name - the String name of an Item
60
+ #
61
+ # Returns true if found, false if not.
62
+ def item_exists?(name)
63
+ items.detect { |item| item.name == name }
64
+ end
65
+
66
+ # Public: persists your in-memory objects to disk in JSON format.
67
+ #
68
+ # Returns true if successful, false if unsuccessful.
69
+ def save!
70
+ File.open(json_file, 'w') {|f| f.write(to_json) }
71
+ end
72
+
73
+ # Public: the JSON representation of the current List and Item assortment
74
+ # attached to the Storage instance.
75
+ #
76
+ # Returns a String JSON representation of its Lists and their Items.
77
+ def to_json
78
+ Yajl::Encoder.encode(to_hash)
79
+ end
80
+
81
+ # Public: creates a Hash of the representation of the in-memory data
82
+ # structure. This percolates down to Items by calling to_hash on the List,
83
+ # which in tern calls to_hash on individual Items.
84
+ #
85
+ # Returns a Hash of the entire data set.
86
+ def to_hash
87
+ { :lists => lists.collect(&:to_hash) }
88
+ end
89
+
90
+
91
+ # INTERNAL METHODS ##########################################################
92
+
93
+ # Take a JSON representation of data and explode it out into the consituent
94
+ # Lists and Items for the given Storage instance.
95
+ #
96
+ # Returns nothing.
97
+ def explode_json(json)
98
+ bootstrap_json unless File.exist?(json)
99
+
100
+ storage = Yajl::Parser.new.parse(File.new(json, 'r'))
101
+
102
+ storage['lists'].each do |lists|
103
+ lists.each do |list_name, items|
104
+ @lists << list = List.new(list_name)
105
+
106
+ items.each do |item|
107
+ item.each do |name,value|
108
+ list.add_item(Item.new(name,value))
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ # Takes care of bootstrapping the JSON file, both in terms of creating the
116
+ # file and in terms of creating a skeleton JSON schema.
117
+ #
118
+ # Return true if successfully saved.
119
+ def bootstrap_json
120
+ FileUtils.touch json_file
121
+ save!
122
+ end
123
+
124
+ end
125
+ end
data/lib/boom.rb ADDED
@@ -0,0 +1,19 @@
1
+ begin
2
+ require 'rubygems'
3
+ rescue LoadError
4
+ end
5
+
6
+ require 'fileutils'
7
+ require 'yajl'
8
+
9
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
10
+
11
+ require 'boom/clipboard'
12
+ require 'boom/command'
13
+ require 'boom/item'
14
+ require 'boom/list'
15
+ require 'boom/storage'
16
+
17
+ module Boom
18
+ VERSION = '0.0.1'
19
+ end
@@ -0,0 +1 @@
1
+ {"lists":[{"urls":[{"github":"https://github.com"},{"blog":"http://zachholman.com"}]}]}
data/test/helper.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'test/unit'
2
+
3
+ begin
4
+ require 'rubygems'
5
+ require 'redgreen'
6
+ rescue LoadError
7
+ end
8
+
9
+ require 'mocha'
10
+
11
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
12
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13
+
14
+ require 'boom'
15
+
16
+ # test/spec/mini 3
17
+ # http://gist.github.com/25455
18
+ # chris@ozmm.org
19
+ # file:lib/test/spec/mini.rb
20
+ def context(*args, &block)
21
+ return super unless (name = args.first) && block
22
+ require 'test/unit'
23
+ klass = Class.new(defined?(ActiveSupport::TestCase) ? ActiveSupport::TestCase : Test::Unit::TestCase) do
24
+ def self.test(name, &block)
25
+ define_method("test_#{name.gsub(/\W/,'_')}", &block) if block
26
+ end
27
+ def self.xtest(*args) end
28
+ def self.setup(&block) define_method(:setup, &block) end
29
+ def self.teardown(&block) define_method(:teardown, &block) end
30
+ end
31
+ (class << klass; self end).send(:define_method, :name) { name.gsub(/\W/,'_') }
32
+ klass.class_eval &block
33
+ end
@@ -0,0 +1,85 @@
1
+ require 'helper'
2
+
3
+ # Intercept STDOUT and collect it
4
+ class Boom::Command
5
+
6
+ def self.capture_output
7
+ @output = ''
8
+ end
9
+
10
+ def self.captured_output
11
+ @output
12
+ end
13
+
14
+ def self.output(s)
15
+ @output << s
16
+ end
17
+
18
+ def self.save!
19
+ # no-op
20
+ end
21
+
22
+ end
23
+
24
+ class TestCommand < Test::Unit::TestCase
25
+
26
+ def setup
27
+ Boom::Storage.any_instance.stubs(:json_file).
28
+ returns('test/examples/urls.json')
29
+ @storage = Boom::Storage.new
30
+ end
31
+
32
+ def command(cmd)
33
+ cmd = cmd.split(' ') if cmd
34
+ Boom::Command.capture_output
35
+ Boom::Command.execute(@storage,*cmd)
36
+ Boom::Command.captured_output
37
+ end
38
+
39
+ def test_overview
40
+ assert_equal ' urls (2)', command(nil)
41
+ end
42
+
43
+ def test_list_detail
44
+ assert_match /github/, command('urls')
45
+ end
46
+
47
+ def test_list_all
48
+ cmd = command('all')
49
+ assert_match /urls/, cmd
50
+ assert_match /github/, cmd
51
+ end
52
+
53
+ def test_list_creation
54
+ assert_match /a new list called "newlist"/, command('newlist')
55
+ end
56
+
57
+ def test_item_access
58
+ assert_match /copied https:\/\/github\.com to your clipboard/,
59
+ command('github')
60
+ end
61
+
62
+ def test_item_access_scoped_by_list
63
+ assert_match /copied https:\/\/github\.com to your clipboard/,
64
+ command('urls github')
65
+ end
66
+
67
+ def test_item_creation
68
+ assert_match /"twitter" in "urls"/,
69
+ command('urls twitter http://twitter.com/holman')
70
+ end
71
+
72
+ def test_list_deletion_no
73
+ STDIN.stubs(:gets).returns('n')
74
+ assert_match /Just kidding then/, command('urls delete')
75
+ end
76
+
77
+ def test_list_deletion_yes
78
+ STDIN.stubs(:gets).returns('y')
79
+ assert_match /Deleted all your urls/, command('urls delete')
80
+ end
81
+
82
+ def test_item_deletion
83
+ assert_match /"github" is gone forever/, command('urls github delete')
84
+ end
85
+ end
data/test/test_item.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'helper'
2
+
3
+ class TestItem < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @item = Boom::Item.new('github','https://github.com')
7
+ end
8
+
9
+ def test_name
10
+ assert_equal 'github', @item.name
11
+ end
12
+
13
+ def test_value
14
+ assert_equal 'https://github.com', @item.value
15
+ end
16
+
17
+ def test_to_hash
18
+ assert_equal 1, @item.to_hash.size
19
+ end
20
+
21
+ end
data/test/test_list.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'helper'
2
+
3
+ class TestList < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @list = Boom::List.new('urls')
7
+ @item = Boom::Item.new('github','https://github.com')
8
+ end
9
+
10
+ def test_name
11
+ assert_equal 'urls', @list.name
12
+ end
13
+
14
+ def test_add_items
15
+ assert_equal 0, @list.items.size
16
+ @list.add_item(@item)
17
+ assert_equal 1, @list.items.size
18
+ end
19
+
20
+ def test_to_hash
21
+ assert_equal 0, @list.to_hash[@list.name].size
22
+ @list.add_item(@item)
23
+ assert_equal 1, @list.to_hash[@list.name].size
24
+ end
25
+
26
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: boom
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Zach Holman
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-11-24 00:00:00 -08:00
19
+ default_executable: boom
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: yajl-ruby
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 19
30
+ segments:
31
+ - 0
32
+ - 7
33
+ - 8
34
+ version: 0.7.8
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: mocha
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 41
46
+ segments:
47
+ - 0
48
+ - 9
49
+ - 9
50
+ version: 0.9.9
51
+ type: :development
52
+ version_requirements: *id002
53
+ description: |-
54
+ God it's about every day where I think to myself, gadzooks,
55
+ I keep typing *REPETITIVE_BORING_TASK* over and over. Wouldn't it be great if
56
+ I had something like boom to store all these commonly-used text snippets for
57
+ me? Then I realized that was a worthless idea since boom hadn't been created
58
+ yet and I had no idea what that statement meant. At some point I found the
59
+ code for boom in a dark alleyway and released it under my own name because I
60
+ wanted to look smart.
61
+ email: github.com@zachholman.com
62
+ executables:
63
+ - boom
64
+ extensions: []
65
+
66
+ extra_rdoc_files:
67
+ - Readme.markdown
68
+ - License
69
+ files:
70
+ - License
71
+ - Rakefile
72
+ - Readme.markdown
73
+ - bin/boom
74
+ - boom.gemspec
75
+ - lib/boom.rb
76
+ - lib/boom/clipboard.rb
77
+ - lib/boom/command.rb
78
+ - lib/boom/item.rb
79
+ - lib/boom/list.rb
80
+ - lib/boom/storage.rb
81
+ - test/examples/urls.json
82
+ - test/helper.rb
83
+ - test/test_command.rb
84
+ - test/test_item.rb
85
+ - test/test_list.rb
86
+ has_rdoc: true
87
+ homepage: https://github.com/holman/boom
88
+ licenses: []
89
+
90
+ post_install_message:
91
+ rdoc_options:
92
+ - --charset=UTF-8
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ hash: 3
101
+ segments:
102
+ - 0
103
+ version: "0"
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ hash: 3
110
+ segments:
111
+ - 0
112
+ version: "0"
113
+ requirements: []
114
+
115
+ rubyforge_project: boom
116
+ rubygems_version: 1.3.7
117
+ signing_key:
118
+ specification_version: 2
119
+ summary: boom lets you access text snippets over your command line.
120
+ test_files:
121
+ - test/test_command.rb
122
+ - test/test_item.rb
123
+ - test/test_list.rb