├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── Rakefile ├── bin └── boom ├── boom.gemspec ├── completion ├── README.md ├── boom.bash └── boom.zsh ├── lib ├── boom.rb └── boom │ ├── color.rb │ ├── command.rb │ ├── core_ext │ └── symbol.rb │ ├── item.rb │ ├── list.rb │ ├── platform.rb │ └── storage.rb └── test ├── cli.sh ├── examples └── data.json ├── item.sh ├── list.sh ├── roundup └── run /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg 2 | test/examples/empty.json 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 2.2.0 3 | script: "./test/run" 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # boom changes 2 | ## head 3 | 4 | ## 0.5.0 5 | - Bump Ruby versions and gems. ([@wjzijderveld](https://github.com/holman/boom/pull/115), #115). 6 | 7 | ## 0.4.0 8 | - Returns an error if you try to `open` something that doesn't exist. 9 | ([@romainberger](https://github.com/romainberger), #90). 10 | - Removes `--delete` in favor of going to `boom delete x` 11 | ([@eugeneius](https://github.com/holman/boom/pull/89), #89). 12 | 13 | ## 0.3.0 14 | - 0.3 removes all the Storage nonsense in favor of clean and easy JSON. Less 15 | code is best code. 16 | - It also removes all the Ruby tests and instead does them in shell with roundup. 17 | - It also changes delete to use `--delete` so there's conceptually less of a 18 | problem. 19 | 20 | ## 0.2.4 21 | - [@Dlom](https://github.com/Dlom) fixed some troubles in #49. 22 | - Some additional test coverage added by 23 | [@nickhammond](https://github.com/nickhammond). 24 | 25 | ## 0.2.3 26 | - [@culvr](https://github.com/culvr) added `boom random ` that lets you 27 | quickly pull a random item. 28 | - Fixed so that opening a specific item (`boom open urls github`) will only 29 | open that item. 30 | - [@culvr](https://github.com/culvr) fixed STDIN list+item creation. 31 | - HTTParty should only be a soft dependency. (#46) 32 | 33 | ## 0.2.2 (July 29, 2011) 34 | - [@jimmycuadra](https://github.com/jimmycuadra) went to town on this beast and 35 | hooked up a Gist backend to boom. Killer. 36 | - Small Windows fix. 37 | 38 | ## 0.2.1 (July 16, 2011) 39 | - boom displays colors thanks to [@dandorman](https://github.com/dandorman). 40 | - Windows support, brah. [@tombell](https://github.com/tombell). 41 | - boom accepts piped-in data (like `cat admins.txt | boom groups admins`). 42 | Thanks [@brettbuddin](https://github.com/brettbuddin) and 43 | [@antonlindstrom](https://github.com/antonlindstrom). Tag-team action. 44 | - `boom ` will actually create the list and item now. 45 | [@jmazzi](https://github.com/jmazzi). 46 | 47 | ## 0.2.0 (April 12, 2011) 48 | - Add Keychain storage to store Boom data securely in OS X's Keychain.app. 49 | Thanks, [@davidtrogers](https://github.com/davidtrogers)! 50 | - Switch from yajl-ruby because [OS X isn't for 51 | developers](http://zachholman.com/2011/03/osx-isnt-for-developers/). Thanks 52 | for finishing it up, [@antonlindstrom](https://github.com/antonlindstrom). 53 | - Move dependencies to Bundler, because it's the One True Way™ at this point. 54 | - Some README updates. 55 | - `boom -v`. 56 | 57 | ## 0.1.2 58 | - Copy to clipboard doesn't hang anymore. Sweet. Thanks, 59 | [mcollina](https://github.com/mcollina). 60 | - Holy hell, [brettbuddin](https://github.com/brettbuddin) added completion for 61 | zsh. Pretty awesome too. 62 | - [antonlindstrom](https://github.com/antonlindstrom) fixed up the MongoDB 63 | backend. 64 | 65 | ## 0.1.1 66 | - Don't force Redis on everyone. 67 | 68 | ## 0.1.0 69 | - boom has been rewritten to use multiple backends. Use `boom switch ` 70 | to switch from the default JSON backend. Currently only Redis is supported. 71 | Pull Requests are welcome. 72 | 73 | ## 0.0.10 74 | - `boom open` will open the Item's URL in a browser, or it'll open all the URLs 75 | in a List for you. Thanks [lwe](https://github.com/lwe). 76 | - Values for item creation can have spaces, and then they get concat'ed as one 77 | value. Thanks [lwe](https://github.com/lwe). 78 | - Replacing an item no longer dupes the item; it'll just replace the value. 79 | Thank god, finally. Thanks [thbishop](https://github.com/thbishop). 80 | - Also started `completion/`, a place to drop in scripts to set up completion 81 | support for boom. Starting out with [thbishop](https://github.com/thbishop)'s 82 | bash script, but if anyone has something for zsh I'd kiss them a bit. 83 | - `boom echo` (and `boom e`) just echos the value; great for command-line 84 | scripts and junk! Thanks [bschaeffer](https://github.com/bschaeffer). 85 | 86 | ## 0.0.9 87 | - Backport `Symbol#to_proc` for 1.8.6 support (thanks 88 | [kastner](https://github.com/kastner) and 89 | [DeMarko](https://github.com/DeMarko)). 90 | 91 | ## 0.0.8 92 | - Support for Ruby 1.9 (thanks [jimmycuadra](https://github.com/jimmycuadra)). 93 | 94 | ## 0.0.7 95 | - Reverts item creation from stdin, since it broke regular item creation. 96 | 97 | ## 0.0.6 98 | - Searching for an item that doesn't exist doesn't murder puppies anymore 99 | (thanks [jimmycuadra](https://github.com/jimmycuadra)). 100 | - Output is a bit cleaner with a constrained `name` column. 101 | - Adds items from stdin (thanks 102 | [MichaelXavier](https://github.com/MichaelXavier)). 103 | 104 | ## 0.0.5 105 | - Item deletes are now scoped by list rather than GLOBAL DESTRUCTION! (thanks 106 | [natebean](https://github.com/natebean)). 107 | - Command line options, like `boop --help` are translated into `boom help`. In 108 | the future we play around with options a bit more. 109 | - Non-Mac-based platforms get clipboard support with `xclip`. If it's 110 | problematic (which it almost certainly is; I'm breaking this more or less on 111 | purpose), please patch it and send me a pull request for your particular 112 | platform. 113 | 114 | ## 0.0.4 115 | - Adds `boom help`. You know, for help. 116 | 117 | ## 0.0.3 118 | - `boom edit` to edit your stuff in a friendly $EDITOR. 119 | - Class-level accessors in List for ActiveRecordesque actions. 120 | 121 | ## 0.0.2 122 | - Fix for list selection (thanks [bgkittrell](https://github.com/bgkittrell)). 123 | 124 | ## 0.0.1 125 | - BOOM! 126 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | boom (0.4.0) 5 | yajl-ruby (~> 1.1) 6 | 7 | GEM 8 | remote: http://rubygems.org/ 9 | specs: 10 | mocha (0.9.12) 11 | rake (0.9.6) 12 | yajl-ruby (1.3.0) 13 | 14 | PLATFORMS 15 | ruby 16 | 17 | DEPENDENCIES 18 | boom! 19 | mocha (~> 0.9.9) 20 | rake (~> 0.9.2) 21 | 22 | BUNDLED WITH 23 | 1.12.5 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # B O O M 2 | 3 | ## About 4 | 5 | boom manages your text snippets on your command line. You can stash away text 6 | like URLs, canned responses, and important notes and then quickly copy them 7 | onto your clipboard, ready for pasting. 8 | 9 | For more details about what boom is and how it works, check out 10 | [boom's website](https://zachholman.com/boom/). 11 | 12 | ## Install 13 | 14 | gem install boom 15 | 16 | ## Quick and Dirty 17 | 18 | $ boom gifs 19 | Boom! Created a new list called "gifs". 20 | 21 | $ boom gifs shirt http://cl.ly/NwCS/shirt.gif 22 | Boom! "shirt" in "gifs" is "http://cl.ly/NwCS/shirt.gif". Got it. 23 | 24 | $ boom shirt 25 | Boom! Just copied http://cl.ly/NwCS/shirt.gif to your clipboard. 26 | 27 | $ boom delete gifs shirt 28 | Boom! shirt is gone forever. 29 | 30 | And that's just a taste! I know, you're salivating, I can hear you from here. 31 | (Why your saliva is noisy is beyond me.) Check out the [full list of 32 | commands](https://github.com/holman/boom/wiki/Commands). 33 | 34 | ## Contribute 35 | 36 | Want to join the [Pantheon of 37 | Boom'ers](https://github.com/holman/boom/contributors)? I'd love to include 38 | your contributions, friend. 39 | 40 | Clone this repository, then run `bundle install`. That'll install all the gem 41 | dependencies. Make sure your methods are [TomDoc](http://tomdoc.org)'d 42 | properly, that existing tests pass (`rake`), and that any new functionality 43 | includes appropriate tests. 44 | 45 | The tests are written in shell for 46 | [roundup](https://github.com/bmizerany/roundup), since boom is basically just 47 | Ruby pretending to be shell. `rake` should run them all for you just fine. 48 | 49 | All good? Cool! Then [send me a pull request](https://github.com/holman/boom/pull/new/master)! 50 | 51 | ## I love you 52 | 53 | [Zach Holman](http://zachholman.com) made this. Ping me on Twitter — 54 | [@holman](http://twitter.com/holman) — if you're having issues, want me to 55 | merge in your pull request, or are using boom in a cool way. I'm kind of hoping 56 | this is generic enough that people do some fun things with it. First one to use 57 | `boom` to calculate their tax liability wins. 58 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 | desc "Run tests for boom" 49 | task :test do 50 | exec "test/run" 51 | end 52 | 53 | desc "Open an irb session preloaded with this library" 54 | task :console do 55 | sh "irb -rubygems -r ./lib/#{name}.rb" 56 | end 57 | 58 | ############################################################################# 59 | # 60 | # Custom tasks (add your own tasks here) 61 | # 62 | ############################################################################# 63 | 64 | 65 | 66 | ############################################################################# 67 | # 68 | # Packaging tasks 69 | # 70 | ############################################################################# 71 | 72 | desc "Create tag v#{version} and build and push #{gem_file} to Rubygems" 73 | task :release => :build do 74 | unless `git branch` =~ /^\* master$/ 75 | puts "You must be on the master branch to release!" 76 | exit! 77 | end 78 | sh "git commit --allow-empty -a -m 'Release #{version}'" 79 | sh "git tag v#{version}" 80 | sh "git push origin master" 81 | sh "git push origin v#{version}" 82 | sh "gem push pkg/#{name}-#{version}.gem" 83 | end 84 | 85 | desc "Build #{gem_file} into the pkg directory" 86 | task :build => :gemspec do 87 | sh "mkdir -p pkg" 88 | sh "gem build #{gemspec_file}" 89 | sh "mv #{gem_file} pkg" 90 | end 91 | 92 | desc "Generate #{gemspec_file}" 93 | task :gemspec => :validate do 94 | # read spec file and split out manifest section 95 | spec = File.read(gemspec_file) 96 | head, manifest, tail = spec.split(" # = MANIFEST =\n") 97 | 98 | # replace name version and date 99 | replace_header(head, :name) 100 | replace_header(head, :version) 101 | replace_header(head, :date) 102 | #comment this out if your rubyforge_project has a different name 103 | replace_header(head, :rubyforge_project) 104 | 105 | # determine file list from git ls-files 106 | files = `git ls-files`. 107 | split("\n"). 108 | sort. 109 | reject { |file| file =~ /^\./ }. 110 | reject { |file| file =~ /^(rdoc|pkg)/ }. 111 | map { |file| " #{file}" }. 112 | join("\n") 113 | 114 | # piece file back together and write 115 | manifest = " s.files = %w[\n#{files}\n ]\n" 116 | spec = [head, manifest, tail].join(" # = MANIFEST =\n") 117 | File.open(gemspec_file, 'w') { |io| io.write(spec) } 118 | puts "Updated #{gemspec_file}" 119 | end 120 | 121 | desc "Validate #{gemspec_file}" 122 | task :validate do 123 | libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"] 124 | unless libfiles.empty? 125 | puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir." 126 | exit! 127 | end 128 | unless Dir['VERSION*'].empty? 129 | puts "A `VERSION` file at root level violates Gem best practices." 130 | exit! 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /bin/boom: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # coding: utf-8 3 | 4 | $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib]) 5 | 6 | require 'boom' 7 | 8 | Boom::Command.execute(*ARGV) 9 | -------------------------------------------------------------------------------- /boom.gemspec: -------------------------------------------------------------------------------- 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.5.0' 17 | s.date = '2017-03-31' 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 = 'zach@zachholman.com' 36 | s.homepage = 'https://github.com/holman/boom' 37 | s.license = 'MIT' 38 | 39 | ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as 40 | ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb' 41 | s.require_paths = %w[lib] 42 | 43 | ## This sections is only necessary if you have C extensions. 44 | #s.require_paths << 'ext' 45 | #s.extensions = %w[ext/extconf.rb] 46 | 47 | ## If your gem includes any executables, list them here. 48 | s.executables = ["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.md LICENSE.md] 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', "~> 1.1") 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 | s.add_development_dependency('rake', "~> 0.9.2") 63 | 64 | ## Leave this section as-is. It will be automatically generated from the 65 | ## contents of your Git repository via the gemspec task. DO NOT REMOVE 66 | ## THE MANIFEST COMMENTS, they are used as delimiters by the task. 67 | # = MANIFEST = 68 | s.files = %w[ 69 | CHANGELOG.md 70 | Gemfile 71 | Gemfile.lock 72 | LICENSE.md 73 | README.md 74 | Rakefile 75 | bin/boom 76 | boom.gemspec 77 | completion/README.md 78 | completion/boom.bash 79 | completion/boom.zsh 80 | lib/boom.rb 81 | lib/boom/color.rb 82 | lib/boom/command.rb 83 | lib/boom/core_ext/symbol.rb 84 | lib/boom/item.rb 85 | lib/boom/list.rb 86 | lib/boom/platform.rb 87 | lib/boom/storage.rb 88 | test/cli.sh 89 | test/examples/data.json 90 | test/item.sh 91 | test/list.sh 92 | test/roundup 93 | test/run 94 | ] 95 | # = MANIFEST = 96 | 97 | ## Test files will be grabbed from the file list. Make sure the path glob 98 | ## matches what you actually use. 99 | s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ } 100 | end 101 | -------------------------------------------------------------------------------- /completion/README.md: -------------------------------------------------------------------------------- 1 | # boom completion 2 | 3 | To add autocompletion to your shell, look in this directory and grab the script 4 | that matches your corresponding shell. 5 | 6 | I use the Z shell, so hey, I picked `boom.zsh`. And trust me: after a couple minutes of 7 | playing with it, you'll never look back. NEVER! 8 | -------------------------------------------------------------------------------- /completion/boom.bash: -------------------------------------------------------------------------------- 1 | _boom_complete() { 2 | local cur prev lists curr_list items 3 | COMPREPLY=() 4 | cur="${COMP_WORDS[COMP_CWORD]}" 5 | prev="${COMP_WORDS[COMP_CWORD-1]}" 6 | curr_list=`eval echo "$prev"` 7 | local IFS=$'\n' 8 | 9 | if [ $COMP_CWORD -eq 1 ]; then 10 | lists=`boom | sed 's/^ \(.*\) ([0-9]\{1,\})$/\1/'` 11 | COMPREPLY=( $( compgen -W '${lists}' -- ${cur} ) ) 12 | elif [ $COMP_CWORD -eq 2 ]; then 13 | items=`boom $curr_list | sed 's/^ \(.\{0,16\}\):.*$/\1/'` 14 | COMPREPLY=( $( compgen -W '${items}' -- ${cur} ) ) 15 | fi 16 | } 17 | complete -o filenames -F _boom_complete boom 18 | -------------------------------------------------------------------------------- /completion/boom.zsh: -------------------------------------------------------------------------------- 1 | #compdef boom 2 | 3 | local state line cmds ret=1 4 | 5 | _arguments -C '1: :->cmds' '*: :->args' 6 | 7 | case $state in 8 | cmds) 9 | local -a cmds 10 | cmds=( 11 | 'all:show all items in all lists' 12 | 'edit:edit the boom JSON file in $EDITOR' 13 | 'help:help text' 14 | ) 15 | _describe -t commands 'boom command' cmds && ret=0 16 | _values 'lists' $(boom | awk '{print $1}') 17 | ;; 18 | args) 19 | case $line[1] in 20 | (boom|all|edit|help) 21 | ;; 22 | *) 23 | _values 'items' `boom $line[1] | awk '{print $1}' | sed -e 's/://'` 2>/dev/null && ret=0 24 | ;; 25 | esac 26 | ;; 27 | esac 28 | 29 | return ret 30 | -------------------------------------------------------------------------------- /lib/boom.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | begin 4 | require 'rubygems' 5 | rescue LoadError 6 | end 7 | 8 | require 'fileutils' 9 | require 'yajl' 10 | 11 | $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib]) 12 | 13 | require 'boom/color' 14 | require 'boom/platform' 15 | require 'boom/command' 16 | require 'boom/item' 17 | require 'boom/list' 18 | require 'boom/storage' 19 | 20 | require 'boom/core_ext/symbol' 21 | 22 | module Boom 23 | VERSION = '0.5.0' 24 | 25 | def self.storage 26 | @storage ||= Storage.new 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/boom/color.rb: -------------------------------------------------------------------------------- 1 | module Boom 2 | # Color collects some methods for colorizing terminal output. 3 | module Color 4 | extend self 5 | 6 | CODES = { 7 | :reset => "\e[0m", 8 | 9 | :cyan => "\e[36m", 10 | :magenta => "\e[35m", 11 | :red => "\e[31m", 12 | :yellow => "\e[33m" 13 | } 14 | 15 | # Tries to enable Windows support if on that platform. 16 | # 17 | # Returns nothing. 18 | def self.included(other) 19 | if RUBY_PLATFORM =~ /win32/ || RUBY_PLATFORM =~ /mingw32/ 20 | require 'Win32/Console/ANSI' 21 | end 22 | rescue LoadError 23 | # Oh well, we tried. 24 | end 25 | 26 | # Wraps the given string in ANSI color codes 27 | # 28 | # string - The String to wrap. 29 | # color_code - The String representing he ANSI color code 30 | # 31 | # Examples 32 | # 33 | # colorize("Boom!", :magenta) 34 | # # => "\e[35mBoom!\e[0m" 35 | # 36 | # Returns the wrapped String unless the the platform is windows and 37 | # does not have Win32::Console, in which case, returns the String. 38 | def colorize(string, color_code) 39 | if !defined?(Win32::Console) && !!(RUBY_PLATFORM =~ /win32/ || RUBY_PLATFORM =~ /mingw32/) 40 | # looks like this person doesn't have Win32::Console and is on windows 41 | # just return the uncolorized string 42 | return string 43 | end 44 | "#{CODES[color_code] || color_code}#{string}#{CODES[:reset]}" 45 | end 46 | 47 | # Set up shortcut methods to all the codes define in CODES. 48 | self.class_eval(CODES.keys.reject {|color| color == :reset }.map do |color| 49 | "def #{color}(string); colorize(string, :#{color}); end" 50 | end.join("\n")) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/boom/command.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Command is the main point of entry for boom commands; shell arguments are 4 | # passed through to Command, which then filters and parses through individual 5 | # commands and reroutes them to constituent object classes. 6 | # 7 | # Command also keeps track of one connection to Storage, which is how new data 8 | # changes are persisted to disk. It takes care of any data changes by calling 9 | # Boom::Command#save. 10 | # 11 | module Boom 12 | class Command 13 | class << self 14 | include Boom::Color 15 | 16 | # Public: accesses the in-memory JSON representation. 17 | # 18 | # Returns a Storage instance. 19 | def storage 20 | Boom.storage 21 | end 22 | 23 | # Public: executes a command. 24 | # 25 | # args - The actual commands to operate on. Can be as few as zero 26 | # arguments or as many as three. 27 | def execute(*args) 28 | command = args.shift 29 | major = args.shift 30 | minor = args.empty? ? nil : args.join(' ') 31 | 32 | return overview unless command 33 | delegate(command, major, minor) 34 | end 35 | 36 | # Public: prints any given string. 37 | # 38 | # s = String output 39 | # 40 | # Prints to STDOUT and returns. This method exists to standardize output 41 | # and for easy mocking or overriding. 42 | def output(s) 43 | puts(s) 44 | end 45 | 46 | # Public: gets $stdin. 47 | # 48 | # Returns the $stdin object. This method exists to help with easy mocking 49 | # or overriding. 50 | def stdin 51 | $stdin 52 | end 53 | 54 | # Public: prints a tidy overview of your Lists in descending order of 55 | # number of Items. 56 | # 57 | # Returns nothing. 58 | def overview 59 | storage.lists.each do |list| 60 | output " #{list.name} (#{list.items.size})" 61 | end 62 | s = "You don't have anything yet! To start out, create a new list:" 63 | s << "\n $ boom " 64 | s << "\nAnd then add something to your list!" 65 | s << "\n $ boom " 66 | s << "\nYou can then grab your new item:" 67 | s << "\n $ boom " 68 | output s if storage.lists.size == 0 69 | end 70 | 71 | # Public: prints the detailed view of all your Lists and all their 72 | # Items. 73 | # 74 | # Returns nothing. 75 | def all 76 | storage.lists.each do |list| 77 | output " #{list.name}" 78 | list.items.each do |item| 79 | output " #{item.short_name}:#{item.spacer} #{item.value}" 80 | end 81 | end 82 | end 83 | 84 | # Public: allows main access to most commands. 85 | # 86 | # Returns output based on method calls. 87 | def delegate(command, major, minor) 88 | return all if command == 'all' 89 | return edit if command == 'edit' 90 | return version if command == "-v" 91 | return version if command == "--version" 92 | return help if command == 'help' 93 | return help if command[0] == 45 || command[0] == '-' # any - dash options are pleas for help 94 | return echo(major,minor) if command == 'echo' || command == 'e' 95 | return copy(major,minor) if command == 'copy' || command == 'c' 96 | return open(major,minor) if command == 'open' || command == 'o' 97 | return random(major) if command == 'random' || command == 'rand' || command == 'r' 98 | 99 | if command == 'delete' || command == 'd' 100 | if minor 101 | return delete_item(major, minor) 102 | else 103 | return delete_list(major) 104 | end 105 | end 106 | 107 | # if we're operating on a List 108 | if storage.list_exists?(command) 109 | return detail_list(command) unless major 110 | return add_item(command,major,minor) if minor 111 | return add_item(command,major,stdin.read) if stdin.stat.size > 0 112 | return search_list_for_item(command, major) 113 | end 114 | 115 | return search_items(command) if storage.item_exists?(command) and !major 116 | 117 | return create_list(command, major, stdin.read) if !minor && stdin.stat.size > 0 118 | return create_list(command, major, minor) 119 | end 120 | 121 | # Public: prints all Items over a List. 122 | # 123 | # name - the List object to iterate over 124 | # 125 | # Returns nothing. 126 | def detail_list(name) 127 | list = List.find(name) 128 | list.items.sort{ |x,y| x.name <=> y.name }.each do |item| 129 | output " #{item.short_name}:#{item.spacer} #{item.value}" 130 | end 131 | end 132 | 133 | # Public: opens the Item. 134 | # 135 | # Returns nothing. 136 | def open(major, minor) 137 | if storage.list_exists?(major) 138 | list = List.find(major) 139 | if minor 140 | item = storage.items.detect { |item| item.name == minor } 141 | if item 142 | output "#{cyan("Boom!")} We just opened #{yellow(Platform.open(item))} for you." 143 | else 144 | output "Couldn't find #{yellow(minor)}." 145 | end 146 | else 147 | list.items.each { |item| Platform.open(item) } 148 | output "#{cyan("Boom!")} We just opened all of #{yellow(major)} for you." 149 | end 150 | else 151 | item = storage.items.detect { |item| item.name == major } 152 | if item 153 | output "#{cyan("Boom!")} We just opened #{yellow(Platform.open(item))} for you." 154 | else 155 | output "Couldn't find #{yellow(major)}." 156 | end 157 | end 158 | end 159 | 160 | # Public: Opens a random item 161 | # 162 | # Returns nothing. 163 | def random(major) 164 | if major.nil? 165 | index = rand(storage.items.size) 166 | item = storage.items[index] 167 | elsif storage.list_exists?(major) 168 | list = List.find(major) 169 | index = rand(list.items.size) 170 | item = list.items[index] 171 | else 172 | output "We couldn't find that list." 173 | end 174 | open(item.name, nil) unless item.nil? 175 | end 176 | 177 | # Public: echoes only the Item's value without copying 178 | # 179 | # item_name - the String term to search for in all Item names 180 | # 181 | # Returns nothing 182 | def echo(major, minor) 183 | unless minor 184 | item = storage.items.detect do |item| 185 | item.name == major 186 | end 187 | return output "#{yellow(major)} #{red("not found")}" unless item 188 | else 189 | list = List.find(major) 190 | item = list.find_item(minor) 191 | return output "#{yellow(minor)} #{red("not found in")} #{yellow(major)}" unless item 192 | end 193 | output item.value 194 | end 195 | 196 | # Public: Copies to clipboard the Item's value without printing to screen 197 | # 198 | # Returns nothing 199 | def copy(major, minor) 200 | unless minor 201 | item = storage.items.detect do |item| 202 | item.name == major 203 | end 204 | return output "#{yellow(major)} #{red("not found")}" unless item 205 | else 206 | list = List.find(major) 207 | item = list.find_item(minor) 208 | return output "#{yellow(minor)} #{red("not found in")} #{yellow(major)}" unless item 209 | end 210 | Platform.copy(item) 211 | end 212 | 213 | # Public: add a new List. 214 | # 215 | # name - the String name of the List. 216 | # item - the String name of the Item 217 | # value - the String value of Item 218 | # 219 | # Example 220 | # 221 | # Commands.list_create("snippets") 222 | # Commands.list_create("hotness", "item", "value") 223 | # 224 | # Returns the newly created List and creates an item when asked. 225 | def create_list(name, item = nil, value = nil) 226 | lists = (storage.lists << List.new(name)) 227 | storage.lists = lists 228 | output "#{cyan("Boom!")} Created a new list called #{yellow(name)}." 229 | save 230 | add_item(name, item, value) unless value.nil? 231 | end 232 | 233 | # Public: remove a named List. 234 | # 235 | # name - the String name of the List. 236 | # 237 | # Example 238 | # 239 | # Commands.delete_list("snippets") 240 | # 241 | # Returns nothing. 242 | def delete_list(name) 243 | if storage.list_exists?(name) 244 | printf "You sure you want to delete everything in #{yellow(name)}? (y/n): " 245 | if $stdin.gets.chomp == 'y' 246 | List.delete(name) 247 | output "#{cyan("Boom!")} Deleted all your #{yellow(name)}." 248 | save 249 | else 250 | output "Just kidding then." 251 | end 252 | else 253 | output "We couldn't find that list." 254 | end 255 | end 256 | 257 | # Public: add a new Item to a list. 258 | # 259 | # list - the String name of the List to associate with this Item 260 | # name - the String name of the Item 261 | # value - the String value of the Item 262 | # 263 | # Example 264 | # 265 | # Commands.add_item("snippets","sig","- @holman") 266 | # 267 | # Returns the newly created Item. 268 | def add_item(list,name,value) 269 | list = List.find(list) 270 | list.add_item(Item.new(name,value)) 271 | output "#{cyan("Boom!")} #{yellow(name)} in #{yellow(list.name)} is #{yellow(value)}. Got it." 272 | save 273 | end 274 | 275 | # Public: remove a named Item. 276 | # 277 | # list_name - the String name of the List. 278 | # name - the String name of the Item. 279 | # 280 | # Example 281 | # 282 | # Commands.delete_item("a-list-name", "an-item-name") 283 | # 284 | # Returns nothing. 285 | def delete_item(list_name,name) 286 | if storage.list_exists?(list_name) 287 | list = List.find(list_name) 288 | if list.delete_item(name) 289 | output "#{cyan("Boom!")} #{yellow(name)} is gone forever." 290 | save 291 | else 292 | output "#{yellow(name)} #{red("not found in")} #{yellow(list_name)}" 293 | end 294 | else 295 | output "We couldn't find that list." 296 | end 297 | end 298 | 299 | # Public: search for an Item in all lists by name. Drops the 300 | # corresponding entry into your clipboard. 301 | # 302 | # name - the String term to search for in all Item names 303 | # 304 | # Returns the matching Item. 305 | def search_items(name) 306 | item = storage.items.detect do |item| 307 | item.name == name 308 | end 309 | 310 | output "#{cyan("Boom!")} We just copied #{yellow(Platform.copy(item))} to your clipboard." 311 | end 312 | 313 | # Public: search for an Item in a particular list by name. Drops the 314 | # corresponding entry into your clipboard if found. 315 | # 316 | # list_name - the String name of the List in which to scope the search 317 | # item_name - the String term to search for in all Item names 318 | # 319 | # Returns the matching Item if found. 320 | def search_list_for_item(list_name, item_name) 321 | list = List.find(list_name) 322 | item = list.find_item(item_name) 323 | 324 | if item 325 | output "#{cyan("Boom!")} We just copied #{yellow(Platform.copy(item))} to your clipboard." 326 | else 327 | output "#{yellow(item_name)} #{red("not found in")} #{yellow(list_name)}" 328 | end 329 | end 330 | 331 | # Public: save in-memory data to disk. 332 | # 333 | # Returns whether or not data was saved. 334 | def save 335 | storage.save 336 | end 337 | 338 | # Public: the version of boom that you're currently running. 339 | # 340 | # Returns a String identifying the version number. 341 | def version 342 | output "You're running boom #{Boom::VERSION}. Congratulations!" 343 | end 344 | 345 | # Public: launches JSON file in an editor for you to edit manually. 346 | # 347 | # Returns nothing. 348 | def edit 349 | output "#{cyan("Boom!")} #{Platform.edit(storage.json_file)}" 350 | end 351 | 352 | # Public: prints all the commands of boom. 353 | # 354 | # Returns nothing. 355 | def help 356 | text = %{ 357 | - boom: help --------------------------------------------------- 358 | 359 | boom display high-level overview 360 | boom all show all items in all lists 361 | boom edit edit the boom JSON file in $EDITOR 362 | boom help this help text 363 | 364 | boom create a new list 365 | boom show items for a list 366 | boom delete deletes a list 367 | 368 | boom create a new list item 369 | boom copy item's value to clipboard 370 | boom copy item's value to clipboard 371 | boom open open item's url in browser 372 | boom open open all item's url in browser for a list 373 | boom random open a random item's url in browser 374 | boom random open a random item's url for a list in browser 375 | boom echo echo the item's value without copying 376 | boom echo echo the item's value without copying 377 | boom copy copy the item's value without echo 378 | boom copy copy the item's value without echo 379 | boom delete deletes an item 380 | 381 | all other documentation is located at: 382 | https://github.com/holman/boom 383 | }.gsub(/^ {8}/, '') # strip the first eight spaces of every line 384 | 385 | output text 386 | end 387 | 388 | end 389 | end 390 | end 391 | -------------------------------------------------------------------------------- /lib/boom/core_ext/symbol.rb: -------------------------------------------------------------------------------- 1 | unless Symbol.method_defined?(:to_proc) 2 | class Symbol 3 | def to_proc 4 | Proc.new { |obj, *args| obj.send(self, *args) } 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/boom/item.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # The representation of the base unit in boom. An Item contains just a name and 4 | # a value. It doesn't know its parent relationship explicitly; the parent List 5 | # object instead knows which Items it contains. 6 | # 7 | module Boom 8 | class Item 9 | # Public: the String name of the Item 10 | attr_accessor :name 11 | 12 | # Public: the String value of the Item 13 | attr_accessor :value 14 | 15 | # Public: creates a new Item object. 16 | # 17 | # name - the String name of the Item 18 | # value - the String value of the Item 19 | # 20 | # Examples 21 | # 22 | # Item.new("github", "https://github.com") 23 | # 24 | # Returns the newly initialized Item. 25 | def initialize(name,value) 26 | @name = name 27 | @value = value 28 | end 29 | 30 | # Public: the shortened String name of the Item. Truncates with ellipses if 31 | # larger. 32 | # 33 | # Examples 34 | # 35 | # item = Item.new("github's home page","https://github.com") 36 | # item.short_name 37 | # # => 'github's home p…' 38 | # 39 | # item = Item.new("github","https://github.com") 40 | # item.short_name 41 | # # => 'github' 42 | # 43 | # Returns the shortened name. 44 | def short_name 45 | name.length > 15 ? "#{name[0..14]}…" : name[0..14] 46 | end 47 | 48 | # Public: the amount of consistent spaces to pad based on Item#short_name. 49 | # 50 | # Returns a String of spaces. 51 | def spacer 52 | name.length > 15 ? '' : ' '*(15-name.length+1) 53 | end 54 | 55 | # Public: only return url part of value - if no url has been 56 | # detected it'll return the value. 57 | # 58 | # Returns a String which preferably is a URL. 59 | def url 60 | @url ||= value.split(/\s+/).detect { |v| v =~ %r{\A[a-z0-9]+:\S+}i } || value 61 | end 62 | 63 | # Public: creates a Hash for this Item. 64 | # 65 | # Returns a Hash of its data. 66 | def to_hash 67 | { @name => @value } 68 | end 69 | end 70 | end -------------------------------------------------------------------------------- /lib/boom/list.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # The List contains many Items. They exist as buckets in which to categorize 4 | # individual Items. The relationship is maintained in a simple array on the 5 | # List-level. 6 | # 7 | module Boom 8 | class List 9 | 10 | # Public: creates a new List instance in-memory. 11 | # 12 | # name - The name of the List. Fails if already used. 13 | # 14 | # Returns the unpersisted List instance. 15 | def initialize(name) 16 | @items = [] 17 | @name = name 18 | end 19 | 20 | # Public: accesses the in-memory JSON representation. 21 | # 22 | # Returns a Storage instance. 23 | def self.storage 24 | Boom.storage 25 | end 26 | 27 | # Public: lets you access the array of items contained within this List. 28 | # 29 | # Returns an Array of Items. 30 | attr_accessor :items 31 | 32 | # Public: the name of the List. 33 | # 34 | # Returns the String name. 35 | attr_accessor :name 36 | 37 | # Public: associates an Item with this List. If the item name is already 38 | # defined, then the value will be replaced 39 | # 40 | # item - the Item object to associate with this List. 41 | # 42 | # Returns the current set of items. 43 | def add_item(item) 44 | delete_item(item.name) if find_item(item.name) 45 | @items << item 46 | end 47 | 48 | # Public: finds any given List by name. 49 | # 50 | # name - String name of the list to search for 51 | # 52 | # Returns the first instance of List that it finds. 53 | def self.find(name) 54 | storage.lists.find { |list| list.name == name } 55 | end 56 | 57 | # Public: deletes a List by name. 58 | # 59 | # name - String name of the list to delete 60 | # 61 | # Returns whether one or more lists were removed. 62 | def self.delete(name) 63 | previous = storage.lists.size 64 | storage.lists = storage.lists.reject { |list| list.name == name } 65 | previous != storage.lists.size 66 | end 67 | 68 | # Public: deletes an Item by name. 69 | # 70 | # name - String name of the item to delete 71 | # 72 | # Returns whether an item was removed. 73 | def delete_item(name) 74 | previous = items.size 75 | items.reject! { |item| item.name == name} 76 | previous != items.size 77 | end 78 | 79 | # Public: finds an Item by name. If the name is typically truncated, also 80 | # allow a search based on that truncated name. 81 | # 82 | # name - String name of the Item to find 83 | # 84 | # Returns the found item. 85 | def find_item(name) 86 | items.find do |item| 87 | item.name == name || 88 | item.short_name.gsub('…','') == name.gsub('…','') 89 | end 90 | end 91 | 92 | # Public: a Hash representation of this List. 93 | # 94 | # Returns a Hash of its own data and its child Items. 95 | def to_hash 96 | { name => items.collect(&:to_hash) } 97 | end 98 | 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/boom/platform.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Platform is a centralized point to shell out platform specific functionality 4 | # like clipboard access or commands to open URLs. 5 | # 6 | # 7 | # Clipboard is a centralized point to shell out to each individual platform's 8 | # clipboard, pasteboard, or whatever they decide to call it. 9 | # 10 | module Boom 11 | class Platform 12 | class << self 13 | # Public: tests if currently running on cygwin. 14 | # 15 | # Returns true if running on Cygwin, else false 16 | def cygwin? 17 | !!(RbConfig::CONFIG['host_os'] =~ /cygwin/) 18 | end 19 | 20 | # Public: tests if currently running on darwin. 21 | # 22 | # Returns true if running on darwin (MacOS X), else false 23 | def darwin? 24 | !!(RbConfig::CONFIG['host_os'] =~ /darwin/) 25 | end 26 | 27 | # Public: tests if currently running on windows. 28 | # 29 | # Apparently Windows RUBY_PLATFORM can be 'win32' or 'mingw32' 30 | # 31 | # Returns true if running on windows (win32/mingw32), else false 32 | def windows? 33 | !!(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/) 34 | end 35 | 36 | # Public: returns the command used to open a file or URL 37 | # for the current platform. 38 | # 39 | # Currently only supports MacOS X and Linux with `xdg-open`. 40 | # 41 | # Returns a String with the bin 42 | def open_command 43 | if darwin? 44 | 'open' 45 | elsif windows? 46 | 'start' 47 | elsif cygwin? 48 | 'cygstart' 49 | else 50 | 'xdg-open' 51 | end 52 | end 53 | 54 | # Public: opens a given Item's value in the browser. This 55 | # method is designed to handle multiple platforms. 56 | # 57 | # Returns a String of the Item value. 58 | def open(item) 59 | unless windows? 60 | system("#{open_command} '#{item.url.gsub("\'","'\\\\''")}'") 61 | else 62 | system("#{open_command} #{item.url.gsub("\'","'\\\\''")}") 63 | end 64 | 65 | item.value 66 | end 67 | 68 | # Public: returns the command used to copy a given Item's value to the 69 | # clipboard for the current platform. 70 | # 71 | # Returns a String with the bin 72 | def copy_command 73 | if darwin? 74 | 'pbcopy' 75 | elsif windows? || cygwin? 76 | 'clip' 77 | else 78 | 'xclip -selection clipboard' 79 | end 80 | end 81 | 82 | # Public: copies a given Item's value to the clipboard. This method is 83 | # designed to handle multiple platforms. 84 | # 85 | # Returns the String value of the Item. 86 | def copy(item) 87 | begin 88 | IO.popen(copy_command,"w") {|cc| cc.write(item.value)} 89 | item.value 90 | rescue Errno::ENOENT 91 | puts item.value 92 | puts "Please install #{copy_command[0..5]} to copy this item to your clipboard" 93 | exit 94 | end 95 | end 96 | 97 | # Public: opens the JSON file in an editor for you to edit. Uses the 98 | # $EDITOR environment variable, or %EDITOR% on Windows for editing. 99 | # This method is designed to handle multiple platforms. 100 | # If $EDITOR is nil, try to open using the open_command. 101 | # 102 | # Returns a String with a helpful message. 103 | def edit(json_file) 104 | unless ENV['EDITOR'].nil? 105 | unless windows? 106 | system("`echo $EDITOR` #{json_file} &") 107 | else 108 | system("start %EDITOR% #{json_file}") 109 | end 110 | else 111 | system("#{open_command} #{json_file}") 112 | end 113 | 114 | "Make your edits, and do be sure to save." 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/boom/storage.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Storage is the interface between multiple Backends. You can use Storage 3 | # directly without having to worry about which Backend is in use. 4 | # 5 | module Boom 6 | class Storage 7 | JSON_FILE = "#{ENV['HOME']}/.boom" 8 | 9 | # Public: the path to the Json file used by boom. 10 | # 11 | # Returns the String path of boom's Json representation. 12 | def json_file 13 | ENV['BOOMFILE'] || JSON_FILE 14 | end 15 | 16 | # Public: initializes a Storage instance by loading in your persisted data from adapter. 17 | # 18 | # Returns the Storage instance. 19 | def initialize 20 | @lists = [] 21 | bootstrap 22 | populate 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: creates a Hash of the representation of the in-memory data 67 | # structure. This percolates down to Items by calling to_hash on the List, 68 | # which in turn calls to_hash on individual Items. 69 | # 70 | # Returns a Hash of the entire data set. 71 | def to_hash 72 | { :lists => lists.collect(&:to_hash) } 73 | end 74 | 75 | # Takes care of bootstrapping the Json file, both in terms of creating the 76 | # file and in terms of creating a skeleton Json schema. 77 | # 78 | # Return true if successfully saved. 79 | def bootstrap 80 | return if File.exist?(json_file) and !File.zero?(json_file) 81 | FileUtils.touch json_file 82 | File.open(json_file, 'w') {|f| f.write(to_json) } 83 | save 84 | end 85 | 86 | # Take a JSON representation of data and explode it out into the constituent 87 | # Lists and Items for the given Storage instance. 88 | # 89 | # Returns nothing. 90 | def populate 91 | file = File.new(json_file, 'r') 92 | storage = Yajl::Parser.parse(file) 93 | 94 | storage['lists'].each do |lists| 95 | lists.each do |list_name, items| 96 | @lists << list = List.new(list_name) 97 | 98 | items.each do |item| 99 | item.each do |name,value| 100 | list.add_item(Item.new(name,value)) 101 | end 102 | end 103 | end 104 | end 105 | end 106 | 107 | # Public: persists your in-memory objects to disk in Json format. 108 | # 109 | # lists_Json - list in Json format 110 | # 111 | # Returns true if successful, false if unsuccessful. 112 | def save 113 | File.open(json_file, 'w') {|f| f.write(to_json) } 114 | end 115 | 116 | # Public: the Json representation of the current List and Item assortment 117 | # attached to the Storage instance. 118 | # 119 | # Returns a String Json representation of its Lists and their Items. 120 | def to_json 121 | Yajl::Encoder.encode(to_hash, :pretty => true) 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /test/cli.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env roundup 2 | export BOOMFILE=test/examples/data.json 3 | boom="./bin/boom" 4 | 5 | describe "cli" 6 | 7 | it_shows_help() { 8 | $boom help | grep "boom: help" 9 | } 10 | 11 | it_shows_a_version() { 12 | $boom --version | grep "running boom" 13 | } -------------------------------------------------------------------------------- /test/examples/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "lists": [ 3 | { 4 | "urls": [ 5 | { "blog": "https://github.com/holman" }, 6 | { "twitter": "https://twitter.com/holman"}, 7 | { "site": "http://zachholman.com"} 8 | ] 9 | }, 10 | { 11 | "jokes": [ 12 | { "none": "What do you call a cow with no legs? Ground beef." }, 13 | { "three": "What do you call a cow with three legs? Lean beef." } 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /test/item.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env roundup 2 | export BOOMFILE=test/examples/data.json 3 | boom="./bin/boom" 4 | 5 | describe "items" 6 | 7 | it_adds_an_item() { 8 | $boom urls google 'http://google.com' 9 | $boom urls | grep google.com 10 | } 11 | 12 | it_deletes_an_item() { 13 | yes | $boom delete urls google | grep 'gone forever' 14 | $boom urls google | grep 'not found' 15 | } 16 | 17 | it_echos_an_item() { 18 | $boom echo site | grep 'zachholman.com' 19 | } 20 | 21 | it_handles_open_on_nonexistent_item() { 22 | $boom open nadda | grep "nadda" 23 | } 24 | -------------------------------------------------------------------------------- /test/list.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env roundup 2 | export BOOMFILE=test/examples/data.json 3 | boom="./bin/boom" 4 | 5 | describe "lists" 6 | 7 | it_shows_all_lists_by_default() { 8 | $boom | grep "urls" 9 | $boom | grep "jokes" 10 | } 11 | 12 | it_adds_a_list() { 13 | $boom enemies | grep "Created a new list" 14 | $boom | grep "enemies (0)" 15 | } 16 | 17 | it_adds_a_list_with_a_duplicate_name() { 18 | $boom urls github 'http://github.com/about' 19 | $boom github | grep '/about' 20 | } 21 | 22 | it_shows_a_list() { 23 | $boom urls | grep 'zachholman' 24 | } 25 | 26 | it_deletes_a_list() { 27 | $boom | grep "enemies" 28 | yes | $boom delete enemies | grep "Deleted" 29 | ! $boom | grep "enemies" 30 | } 31 | 32 | it_handles_delete_on_nonexistent_list() { 33 | ! $boom | grep "enemies" 34 | $boom delete "enemies" | grep "We couldn't find that list" 35 | } 36 | 37 | it_handles_empty_boomfile() { 38 | cp /dev/null test/examples/empty.json 39 | export BOOMFILE=test/examples/empty.json 40 | $boom heynow | grep "Created a new list" 41 | export BOOMFILE=test/examples/data.json 42 | } 43 | -------------------------------------------------------------------------------- /test/roundup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # [r5]: roundup.5.html 3 | # [r1t]: roundup-1-test.sh.html 4 | # [r5t]: roundup-5-test.sh.html 5 | # 6 | # _(c) 2010 Blake Mizerany - MIT License_ 7 | # 8 | # Spray **roundup** on your shells to eliminate weeds and bugs. If your shells 9 | # survive **roundup**'s deathly toxic properties, they are considered 10 | # roundup-ready. 11 | # 12 | # **roundup** reads shell scripts to form test plans. Each 13 | # test plan is sourced into a sandbox where each test is executed. 14 | # 15 | # See [roundup-1-test.sh.html][r1t] or [roundup-5-test.sh.html][r5t] for example 16 | # test plans. 17 | # 18 | # __Install__ 19 | # 20 | # git clone http://github.com/bmizerany/roundup.git 21 | # cd roundup 22 | # make 23 | # sudo make install 24 | # # Alternatively, copy `roundup` wherever you like. 25 | # 26 | # __NOTE__: Because test plans are sourced into roundup, roundup prefixes its 27 | # variable and function names with `roundup_` to avoid name collisions. See 28 | # "Sandbox Test Runs" below for more insight. 29 | 30 | # Usage and Prerequisites 31 | # ----------------------- 32 | 33 | # Exit if any following command exits with a non-zero status. 34 | set -e 35 | 36 | # The current version is set during `make version`. Do not modify this line in 37 | # anyway unless you know what you're doing. 38 | ROUNDUP_VERSION="0.0.5" 39 | export ROUNDUP_VERSION 40 | 41 | # Usage is defined in a specific comment syntax. It is `grep`ed out of this file 42 | # when needed (i.e. The Tomayko Method). See 43 | # [shocco](http://rtomayko.heroku.com/shocco) for more detail. 44 | #/ usage: roundup [--help|-h] [--version|-v] [plan ...] 45 | 46 | roundup_usage() { 47 | grep '^#/' <"$0" | cut -c4- 48 | } 49 | 50 | while test "$#" -gt 0 51 | do 52 | case "$1" in 53 | --help|-h) 54 | roundup_usage 55 | exit 0 56 | ;; 57 | --version|-v) 58 | echo "roundup version $ROUNDUP_VERSION" 59 | exit 0 60 | ;; 61 | --color) 62 | color=always 63 | shift 64 | ;; 65 | -) 66 | echo >&2 "roundup: unknown switch $1" 67 | exit 1 68 | ;; 69 | *) 70 | break 71 | ;; 72 | esac 73 | done 74 | 75 | # Consider all scripts with names matching `*-test.sh` the plans to run unless 76 | # otherwise specified as arguments. 77 | if [ "$#" -gt "0" ] 78 | then 79 | roundup_plans="$@" 80 | else 81 | roundup_plans="$(ls *-test.sh)" 82 | fi 83 | 84 | : ${color:="auto"} 85 | 86 | # Create a temporary storage place for test output to be retrieved for display 87 | # after failing tests. 88 | roundup_tmp="$PWD/.roundup.$$" 89 | mkdir -p "$roundup_tmp" 90 | 91 | trap "rm -rf \"$roundup_tmp\"" EXIT INT 92 | 93 | # __Tracing failures__ 94 | roundup_trace() { 95 | # Delete the first two lines that represent roundups execution of the 96 | # test function. They are useless to the user. 97 | sed '1d' | 98 | # Delete the last line which is the "set +x" of the error trap 99 | sed '$d' | 100 | # Replace the rc=$? of the error trap with an verbose string appended 101 | # to the failing command trace line. 102 | sed '$s/.*rc=/exit code /' | 103 | # Trim the two left most `+` signs. They represent the depth at which 104 | # roundup executed the function. They also, are useless and confusing. 105 | sed 's/^++//' | 106 | # Indent the output by 4 spaces to align under the test name in the 107 | # summary. 108 | sed 's/^/ /' | 109 | # Highlight the last line in front of the exit code to bring notice to 110 | # where the error occurred. 111 | # 112 | # The sed magic puts every line into the hold buffer first, then 113 | # substitutes in the previous hold buffer content, prints that and starts 114 | # with the next cycle. At the end the last line (in the hold buffer) 115 | # is printed without substitution. 116 | sed -n "x;1!{ \$s/\(.*\)/$mag\1$clr/; };1!p;\$x;\$p" 117 | } 118 | 119 | # __Other helpers__ 120 | 121 | # Track the test stats while outputting a real-time report. This takes input on 122 | # **stdin**. Each input line must come in the format of: 123 | # 124 | # # The plan description to be displayed 125 | # d 126 | # 127 | # # A passing test 128 | # p 129 | # 130 | # # A failed test 131 | # f 132 | roundup_summarize() { 133 | set -e 134 | 135 | # __Colors for output__ 136 | 137 | # Use colors if we are writing to a tty device. 138 | if (test -t 1) || (test $color = always) 139 | then 140 | red=$(printf "\033[31m") 141 | grn=$(printf "\033[32m") 142 | mag=$(printf "\033[35m") 143 | clr=$(printf "\033[m") 144 | cols=$(tput cols) 145 | fi 146 | 147 | # Make these available to `roundup_trace`. 148 | export red grn mag clr 149 | 150 | ntests=0 151 | passed=0 152 | failed=0 153 | 154 | : ${cols:=10} 155 | 156 | while read status name 157 | do 158 | case $status in 159 | p) 160 | ntests=$(expr $ntests + 1) 161 | passed=$(expr $passed + 1) 162 | printf " %-48s " "$name:" 163 | printf "$grn[PASS]$clr\n" 164 | ;; 165 | f) 166 | ntests=$(expr $ntests + 1) 167 | failed=$(expr $failed + 1) 168 | printf " %-48s " "$name:" 169 | printf "$red[FAIL]$clr\n" 170 | roundup_trace < "$roundup_tmp/$name" 171 | ;; 172 | d) 173 | printf "%s\n" "$name" 174 | ;; 175 | esac 176 | done 177 | # __Test Summary__ 178 | # 179 | # Display the summary now that all tests are finished. 180 | yes = | head -n 57 | tr -d '\n' 181 | printf "\n" 182 | printf "Tests: %3d | " $ntests 183 | printf "Passed: %3d | " $passed 184 | printf "Failed: %3d" $failed 185 | printf "\n" 186 | 187 | # Exit with an error if any tests failed 188 | test $failed -eq 0 || exit 2 189 | } 190 | 191 | # Sandbox Test Runs 192 | # ----------------- 193 | 194 | # The above checks guarantee we have at least one test. We can now move through 195 | # each specified test plan, determine its test plan, and administer each test 196 | # listed in a isolated sandbox. 197 | for roundup_p in $roundup_plans 198 | do 199 | # Create a sandbox, source the test plan, run the tests, then leave 200 | # without a trace. 201 | ( 202 | # Consider the description to be the `basename` of the plan minus the 203 | # tailing -test.sh. 204 | roundup_desc=$(basename "$roundup_p" -test.sh) 205 | 206 | # Define functions for 207 | # [roundup(5)][r5] 208 | 209 | # A custom description is recommended, but optional. Use `describe` to 210 | # set the description to something more meaningful. 211 | # TODO: reimplement this. 212 | describe() { 213 | roundup_desc="$*" 214 | } 215 | 216 | # Provide default `before` and `after` functions that run only `:`, a 217 | # no-op. They may or may not be redefined by the test plan. 218 | before() { :; } 219 | after() { :; } 220 | 221 | # Seek test methods and aggregate their names, forming a test plan. 222 | # This is done before populating the sandbox with tests to avoid odd 223 | # conflicts. 224 | 225 | # TODO: I want to do this with sed only. Please send a patch if you 226 | # know a cleaner way. 227 | roundup_plan=$( 228 | grep "^it_.*()" $roundup_p | 229 | sed "s/\(it_[a-zA-Z0-9_]*\).*$/\1/g" 230 | ) 231 | 232 | # We have the test plan and are in our sandbox with [roundup(5)][r5] 233 | # defined. Now we source the plan to bring its tests into scope. 234 | . ./$roundup_p 235 | 236 | # Output the description signal 237 | printf "d %s" "$roundup_desc" | tr "\n" " " 238 | printf "\n" 239 | 240 | for roundup_test_name in $roundup_plan 241 | do 242 | # Any number of things are possible in `before`, `after`, and the 243 | # test. Drop into an subshell to contain operations that may throw 244 | # off roundup; such as `cd`. 245 | ( 246 | # Output `before` trace to temporary file. If `before` runs cleanly, 247 | # the trace will be overwritten by the actual test case below. 248 | { 249 | # redirect tracing output of `before` into file. 250 | { 251 | set -x 252 | # If `before` wasn't redefined, then this is `:`. 253 | before 254 | } &>"$roundup_tmp/$roundup_test_name" 255 | # disable tracing again. Its trace output goes to /dev/null. 256 | set +x 257 | } &>/dev/null 258 | 259 | # exit subshell with return code of last failing command. This 260 | # is needed to see the return code 253 on failed assumptions. 261 | # But, only do this if the error handling is activated. 262 | set -E 263 | trap 'rc=$?; set +x; set -o | grep "errexit.*on" >/dev/null && exit $rc' ERR 264 | 265 | # If `before` wasn't redefined, then this is `:`. 266 | before 267 | 268 | # Momentarily turn off auto-fail to give us access to the tests 269 | # exit status in `$?` for capturing. 270 | set +e 271 | ( 272 | # Set `-xe` before the test in the subshell. We want the 273 | # test to fail fast to allow for more accurate output of 274 | # where things went wrong but not in _our_ process because a 275 | # failed test should not immediately fail roundup. Each 276 | # tests trace output is saved in temporary storage. 277 | set -xe 278 | $roundup_test_name 279 | ) >"$roundup_tmp/$roundup_test_name" 2>&1 280 | 281 | # We need to capture the exit status before returning the `set 282 | # -e` mode. Returning with `set -e` before we capture the exit 283 | # status will result in `$?` being set with `set`'s status 284 | # instead. 285 | roundup_result=$? 286 | 287 | # It's safe to return to normal operation. 288 | set -e 289 | 290 | # If `after` wasn't redefined, then this runs `:`. 291 | after 292 | 293 | # This is the final step of a test. Print its pass/fail signal 294 | # and name. 295 | if [ "$roundup_result" -ne 0 ] 296 | then printf "f" 297 | else printf "p" 298 | fi 299 | 300 | printf " $roundup_test_name\n" 301 | ) 302 | done 303 | ) 304 | done | 305 | 306 | # All signals are piped to this for summary. 307 | roundup_summarize -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # A shim to run our tests in shell. Boom is written for the shell, might as well 4 | # test it in shell. 5 | 6 | ./test/roundup test/*.sh 7 | 8 | git checkout test/examples --------------------------------------------------------------------------------