hairballs 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.
@@ -0,0 +1,17 @@
1
+ require_relative '../../hairballs'
2
+
3
+ # Adds the Object#require_project_lib method as a shortcut for
4
+ #
5
+ # require 'lib/my_library' # Or...
6
+ # require 'lib/my/nested/project' # i.e. gem = my-nested-project
7
+ #
8
+ Hairballs.add_plugin(:require_project_lib) do |plugin|
9
+ plugin.on_load do
10
+ Object.class_eval do
11
+ def require_project_lib
12
+ require_dir = File.join(*Hairballs.project_name.split('-'))
13
+ require_relative "#{Dir.pwd}/lib/#{require_dir}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ require_relative '../../hairballs'
2
+
3
+ # Adds the ability to tab-complete files that are in the current directory.
4
+ #
5
+ # Example:
6
+ #
7
+ # # If in the hairballs project root, doing this...
8
+ # irb> File.read("READ⇥
9
+ # # Will complete like this...
10
+ # irb> File.read("README.md"
11
+ #
12
+ Hairballs.add_plugin(:tab_completion_for_files) do |plugin|
13
+ plugin.on_load do
14
+ Hairballs.completion_procs << proc do |string|
15
+ Dir['*'].grep(/^#{Regexp.escape(string)}*/)
16
+ end
17
+
18
+ if defined? ::IRB::InputCompletor::CompletionProc
19
+ Hairballs.completion_procs << ::IRB::InputCompletor::CompletionProc
20
+ end
21
+
22
+ completion_proc = Proc.new do |string|
23
+ Hairballs.completion_procs.map do |proc|
24
+ proc.call(string)
25
+ end.flatten.uniq
26
+ end
27
+
28
+ if Readline.respond_to?('basic_word_break_characters=')
29
+ Readline.basic_word_break_characters= " \"'\t\n`><=;|&{("
30
+ end
31
+
32
+ Readline.completion_append_character = nil
33
+ Readline.completion_proc = completion_proc
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ require_relative '../../hairballs'
2
+
3
+ # Just loads Wirble.
4
+ Hairballs.add_plugin(:wirble) do |plugin|
5
+ plugin.libraries %w(wirble)
6
+
7
+ plugin.on_load do
8
+ Wirble.init
9
+ Wirble.colorize
10
+ end
11
+ end
@@ -0,0 +1,64 @@
1
+ class Hairballs
2
+ # Hairballs representation of IRB.conf[:PROMPT]. Method names here make
3
+ # prompt types clearer.
4
+ #
5
+ # TODO: Make it nicer to define Pry prompts.
6
+ class Prompt
7
+ # @param [Boolean]
8
+ attr_accessor :auto_indent
9
+
10
+ # The normal prompt string. Same as
11
+ # IRB.conf[:PROMPT][ prompt name ][:PROMPT_I]
12
+ #
13
+ # @param [String]
14
+ attr_accessor :normal
15
+ alias_method :i, :normal
16
+ alias_method :i=, :normal=
17
+
18
+ # The prompt for when strings wrap multiple lines. Same as
19
+ # IRB.conf[:PROMPT][ prompt name ][:PROMPT_S]
20
+ #
21
+ # @param [String]
22
+ attr_accessor :continued_string
23
+ alias_method :s, :continued_string
24
+ alias_method :s=, :continued_string=
25
+
26
+ # The prompt for when statements wrap multiple lines. Same as
27
+ # IRB.conf[:PROMPT][ prompt name ][:PROMPT_C]
28
+ #
29
+ # @param [String]
30
+ attr_accessor :continued_statement
31
+ alias_method :c, :continued_statement
32
+ alias_method :c=, :continued_statement=
33
+
34
+ # The prompt for when statements include indentation. Same as
35
+ # IRB.conf[:PROMPT][ prompt name ][:PROMPT_N]
36
+ #
37
+ # @param [String]
38
+ attr_accessor :indented_code
39
+ alias_method :n, :indented_code
40
+ alias_method :n=, :indented_code=
41
+
42
+ # The prompt for return values. Same as
43
+ # IRB.conf[:PROMPT][ prompt name ][:RETURN]
44
+ #
45
+ # @param [String]
46
+ attr_accessor :return_format
47
+
48
+ # @return [Hash] A set of key/value pairs that can be used to pass to a
49
+ # IRB.conf[:PROMPT][ prompt name ].
50
+ def irb_configuration
51
+ vputs 'Setting up prompt...'
52
+
53
+ prompt_values = {}
54
+ prompt_values[:AUTO_INDENT] = @auto_indent if @auto_indent
55
+ prompt_values[:PROMPT_C] = continued_statement unless continued_statement.empty?
56
+ prompt_values[:PROMPT_I] = normal unless normal.empty?
57
+ prompt_values[:PROMPT_N] = indented_code unless indented_code.empty?
58
+ prompt_values[:PROMPT_S] = continued_string unless continued_string.empty?
59
+ prompt_values[:RETURN] = @return_format if @return_format
60
+
61
+ prompt_values
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,110 @@
1
+ require_relative 'library_helpers'
2
+ require_relative 'prompt'
3
+
4
+ class Hairballs
5
+ # Themes primarily provide a means for customizing the look of your IRB
6
+ # prompt, although you're at liberty to also do any other Ruby stuff you'd
7
+ # like, including load up common Hairballs::Plugins.
8
+ #
9
+ # Unless you know you do, you probably don't need to use this directly;
10
+ # +Hairballs.add_theme()+ and +Hairballs.use_theme()+ should cover most use
11
+ # cases.
12
+ class Theme
13
+ include LibraryHelpers
14
+
15
+ # Just an identifier for the Theme. Don't name two themes the same
16
+ # name--that will cause problems.
17
+ #
18
+ # @param [Symbol]
19
+ attr_accessor :name
20
+
21
+ # Tells Hairballs to do some hackery to let Themes use gems that aren't
22
+ # specified in your app's Gemfile. This alleviates you from having to
23
+ # declare gems in your Gemfile simply for the sake of managing your personal
24
+ # IRB preferences.
25
+ #
26
+ # @param [Boolean]
27
+ attr_accessor :extend_bundler
28
+
29
+ # @param name [Symbol]
30
+ def initialize(name)
31
+ @name = name
32
+ @prompt = Prompt.new
33
+ @extend_bundler = false
34
+ end
35
+
36
+ # Tell IRB to use this Theme.
37
+ def use!
38
+ do_bundler_extending if @extend_bundler
39
+ require_libraries
40
+ set_up_prompt
41
+ end
42
+
43
+ # The name of the Theme, but in the format that IRB.conf[:PROMPT] likes (an
44
+ # all-caps Symbol).
45
+ #
46
+ # @return [Symbol]
47
+ def irb_name
48
+ @name.to_s.upcase.to_sym
49
+ end
50
+
51
+ # @return [Hairballs::Prompt]
52
+ def prompt(&block)
53
+ if block_given?
54
+ @prompt_block = block
55
+ else
56
+ @prompt
57
+ end
58
+ end
59
+
60
+ #---------------------------------------------------------------------------
61
+ # PRIVATES
62
+ #---------------------------------------------------------------------------
63
+
64
+ private
65
+
66
+ # Does all the things that are required for getting IRB to use your Theme.
67
+ def set_up_prompt
68
+ @prompt_block.call(@prompt)
69
+
70
+ if IRB.conf[:PROMPT].nil? && defined?(Pry)
71
+ set_up_pry_prompt
72
+ set_up_pry_printer
73
+ elsif IRB.conf[:PROMPT]
74
+ set_up_irb_prompt
75
+ else
76
+ message = "[Hairballs] Seems like you're not using Pry *or* IRB."
77
+ puts "#{message} Skipping prompt setup."
78
+ end
79
+ end
80
+
81
+ def set_up_irb_prompt
82
+ IRB.conf[:PROMPT][irb_name] = @prompt.irb_configuration
83
+ IRB.conf[:PROMPT_MODE] = irb_name
84
+ IRB.CurrentContext.prompt_mode = irb_name if IRB.CurrentContext
85
+ end
86
+
87
+ def set_up_pry_prompt
88
+ if @prompt.normal && @prompt.continued_statement
89
+ ::Pry.config.prompt = [
90
+ proc { @prompt.normal }, proc { @prompt.continued_statement }
91
+ ]
92
+ elsif @prompt.normal
93
+ ::Pry.config.prompt = -> { @prompt.normal }
94
+ else
95
+ vputs 'Neither "normal" nor "continued_statement" prompts configured.'
96
+ end
97
+ end
98
+
99
+ def set_up_pry_printer
100
+ puts "@pompt return format: #{@prompt.return_format}"
101
+ if @prompt.return_format
102
+ Pry.config.print = proc do |output, value|
103
+ output.printf @prompt.return_format, value.inspect
104
+ end
105
+ else
106
+ vputs '"return_format" not configured.'
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,34 @@
1
+ require_relative '../../hairballs'
2
+
3
+ Hairballs.add_theme(:turboladen) do |theme|
4
+ theme.libraries do
5
+ libs_to_require = %w(
6
+ irb/completion
7
+ looksee
8
+ colorize
9
+ )
10
+
11
+ libs_to_require +
12
+ case RUBY_PLATFORM
13
+ when /mswin32|mingw32/
14
+ %w(win32console)
15
+ when /darwin/
16
+ %w(terminal-notifier)
17
+ else
18
+ []
19
+ end
20
+ end
21
+
22
+ theme.prompt do |prompt|
23
+ preface = proc do |status = ' '|
24
+ "⟪#{Hairballs.project_name.light_blue}⟫#{status}%03n"
25
+ end
26
+
27
+ prompt.auto_indent = true
28
+ prompt.normal = "#{preface.call}:%i> "
29
+ prompt.continued_string = "#{preface.call('❊%l'.yellow)}:%i> "
30
+ prompt.continued_statement = "#{preface.call('❊?'.yellow)}:%i> "
31
+ prompt.indented_code = "#{preface.call('✚ '.yellow)}:%i> "
32
+ prompt.return_format = "➥ %s\n"
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ require_relative '../../hairballs'
2
+
3
+ Hairballs.add_theme(:turboladen_rails) do |theme|
4
+ theme.libraries do
5
+ libs_to_require = %w(
6
+ irb/completion
7
+ looksee
8
+ colorize
9
+ )
10
+
11
+ libs_to_require +
12
+ case RUBY_PLATFORM
13
+ when /mswin32|mingw32/
14
+ %w(win32console)
15
+ when /darwin/
16
+ %w(terminal-notifier)
17
+ else
18
+ []
19
+ end
20
+ end
21
+
22
+ theme.extend_bundler = true
23
+
24
+ theme.prompt do |prompt|
25
+ prompt.auto_indent = true
26
+ preface = Hairballs.project_name.light_blue
27
+ prompt.normal = "#{preface}> "
28
+ prompt.continued_string = "#{preface}#{'❊%l'.yellow}> "
29
+ prompt.continued_statement = "#{preface}#{'⇥'.yellow} %i> "
30
+ prompt.indented_code = "#{preface}#{'⇥'.yellow} %i> "
31
+ prompt.return_format = "➥ %s\n"
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ class Hairballs
2
+ VERSION = '0.0.1'
3
+ end
data/lib/hairballs.rb ADDED
@@ -0,0 +1,113 @@
1
+ require_relative 'hairballs/ext/kernel_vputs'
2
+ require_relative 'hairballs/exceptions'
3
+ require_relative 'hairballs/theme'
4
+ require_relative 'hairballs/plugin'
5
+ require_relative 'hairballs/version'
6
+
7
+ # Home of the Hairballs DSL for defining and using Themes and Plugins.
8
+ class Hairballs
9
+ class << self
10
+ # @return [Hairballs::Theme]
11
+ attr_reader :current_theme
12
+
13
+ # @return [Array<Hairballs::Theme>]
14
+ def themes
15
+ @themes ||= []
16
+ end
17
+
18
+ # Builds a new Hairballs::Theme and adds it to the list of `.themes` that
19
+ # can be used.
20
+ #
21
+ # @param name [Symbol] Name to give the new Theme.
22
+ # @return [Theme]
23
+ def add_theme(name)
24
+ theme = Theme.new(name)
25
+ yield theme
26
+ themes << theme
27
+ vputs "Added theme: #{name}"
28
+
29
+ theme
30
+ end
31
+
32
+ # Tells IRB to use the Hairballs::Theme of +theme_name+.
33
+ #
34
+ # @param theme_name [Symbol] The name of the Theme to use/switch to.
35
+ def use_theme(theme_name)
36
+ switch_to = themes.find { |theme| theme.name == theme_name }
37
+ fail ThemeUseFailure.new(theme_name) unless switch_to
38
+ vputs "Switched to theme: #{theme_name}"
39
+
40
+ switch_to.use!
41
+ @current_theme = switch_to
42
+
43
+ true
44
+ end
45
+
46
+ # All Hairballs::Plugins available to be loaded.
47
+ #
48
+ # @return [Array<Hairballs::Plugin>]
49
+ def plugins
50
+ @plugins ||= []
51
+ end
52
+
53
+ # All Hairballs::Plugins that have been loaded.
54
+ #
55
+ # @return [Array<Hairballs::Plugin>]
56
+ def loaded_plugins
57
+ @loaded_plugins ||= []
58
+ end
59
+
60
+ # Builds a new Hairballs::Plugin and adds it to thelist of `.plugins` that
61
+ # can be used.
62
+ #
63
+ # @param name [Symbol]
64
+ # @param options [Hash] Plugin-dependent options to define. These get
65
+ # passed along the the Hairballs::Plugin object and are used as attributes
66
+ # of the plugin.
67
+ def add_plugin(name, **options)
68
+ plugin = Plugin.new(name, options)
69
+ yield plugin
70
+ plugins << plugin
71
+ vputs "Added plugin: #{name}"
72
+
73
+ plugin
74
+ end
75
+
76
+ # Searches for the Hairballs::Plugin by the +plugin_name+, then loads it.
77
+ # Raises
78
+ # @param plugin_name [Symbol]
79
+ def load_plugin(plugin_name, **options)
80
+ plugin_to_use = plugins.find { |plugin| plugin.name == plugin_name }
81
+ fail PluginNotFound.new(plugin_name) unless plugin_to_use
82
+ vputs "Using plugin: #{plugin_name}"
83
+
84
+ plugin_to_use.load!(options)
85
+ loaded_plugins << plugin_to_use
86
+
87
+ true
88
+ end
89
+
90
+ # Name of the relative directory.
91
+ #
92
+ # @return [String]
93
+ def project_name
94
+ @project_name ||= File.basename(Dir.pwd)
95
+ end
96
+
97
+ # Is IRB getting loaded for a rails console?
98
+ #
99
+ # @return [Boolean]
100
+ def rails?
101
+ ENV.has_key?('RAILS_ENV') || !!defined?(Rails)
102
+ end
103
+
104
+ # Used for maintaining all possible completion Procs, thus allowing many
105
+ # different plugins to define a Proc for completion without overriding Procs
106
+ # defined for other plugins.
107
+ #
108
+ # @return [Array<Proc>]
109
+ def completion_procs
110
+ @completion_procs ||= []
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,185 @@
1
+ require 'spec_helper'
2
+ require 'hairballs'
3
+
4
+ describe Hairballs do
5
+ subject { described_class }
6
+
7
+ describe '.themes' do
8
+ subject { described_class.themes }
9
+ it { is_expected.to be_an Array }
10
+ end
11
+
12
+ describe '.add_theme' do
13
+ let(:theme) { double 'Hairballs::Theme' }
14
+ context 'block given and name param is a Symbol' do
15
+ before do
16
+ expect(Hairballs::Theme).to receive(:new).with(:test).and_return theme
17
+ end
18
+
19
+ it 'yields the Theme' do
20
+ expect { |b| subject.add_theme(:test, &b) }.to yield_with_args(theme)
21
+ end
22
+
23
+ it 'adds the Theme to @themes' do
24
+ expect(subject.themes).to receive(:<<).with(theme)
25
+ subject.add_theme(:test) { |_| }
26
+ end
27
+
28
+ it 'returns the Theme' do
29
+ expect(subject.add_theme(:test) { |_| }).to eq theme
30
+ end
31
+ end
32
+
33
+ context 'no block given' do
34
+ before do
35
+ allow(Hairballs::Theme).to receive(:new).and_return theme
36
+ end
37
+
38
+ it 'raises a LocalJumpError' do
39
+ expect { subject.add_theme(:test) }.to raise_exception(LocalJumpError)
40
+ end
41
+ end
42
+ end
43
+
44
+ describe '.use_theme' do
45
+ let(:theme) { double 'Hairballs::Theme', name: :test }
46
+
47
+ context 'theme_name represents an added theme' do
48
+ before(:example) do
49
+ expect(subject.themes).to receive(:find).and_return theme
50
+ end
51
+
52
+ it 'does not raise a ThemeUseFailure' do
53
+ allow(theme).to receive(:use!)
54
+
55
+ expect do
56
+ subject.use_theme(:test)
57
+ end.to_not raise_exception
58
+ end
59
+
60
+ it 'tells the theme to #use!' do
61
+ expect(theme).to receive(:use!)
62
+ subject.use_theme(:test)
63
+ end
64
+
65
+ it 'sets @current_theme to the new theme' do
66
+ allow(theme).to receive(:use!)
67
+
68
+ expect { subject.use_theme(:test) }.
69
+ to change { subject.instance_variable_get(:@current_theme) }.
70
+ to(theme)
71
+ end
72
+ end
73
+
74
+ context 'theme_name does not represent an added theme' do
75
+ it 'raises a ThemeUseFailure' do
76
+ expect do
77
+ described_class.use_theme(:meow)
78
+ end.to raise_exception(Hairballs::ThemeUseFailure)
79
+ end
80
+ end
81
+ end
82
+
83
+ describe '.plugins' do
84
+ subject { described_class.plugins }
85
+ it { is_expected.to be_an Array }
86
+ end
87
+
88
+ describe '.loaded_plugins' do
89
+ subject { described_class.loaded_plugins }
90
+ it { is_expected.to be_an Array }
91
+ end
92
+
93
+ describe '.add_plugin' do
94
+ let(:plugin) { double 'Hairballs::Plugin' }
95
+
96
+ context 'block given and name param is a Symbol' do
97
+ before do
98
+ expect(Hairballs::Plugin).to receive(:new).with(:test, {}).and_return plugin
99
+ end
100
+
101
+ it 'yields the Plugin' do
102
+ expect { |b| subject.add_plugin(:test, &b) }.to yield_with_args(plugin)
103
+ end
104
+
105
+ it 'adds the Plugin to @plugins' do
106
+ expect(subject.plugins).to receive(:<<).with(plugin)
107
+ subject.add_plugin(:test) { |_| }
108
+ end
109
+
110
+ it 'returns the Plugin' do
111
+ expect(subject.add_plugin(:test) { |_| }).to eq plugin
112
+ end
113
+ end
114
+
115
+ context 'no block given' do
116
+ before do
117
+ allow(Hairballs::Plugin).to receive(:new).and_return plugin
118
+ end
119
+
120
+ it 'raises a LocalJumpError' do
121
+ expect { subject.add_plugin(:test) }.to raise_exception(LocalJumpError)
122
+ end
123
+ end
124
+ end
125
+
126
+ describe '.load_plugin' do
127
+ let(:plugin) { double 'Hairballs::Plugin', name: :test }
128
+
129
+ context 'theme_name represents an added plugin' do
130
+ before(:example) do
131
+ expect(subject.plugins).to receive(:find).and_return plugin
132
+ end
133
+
134
+ it 'does not raise an exception' do
135
+ allow(plugin).to receive(:load!)
136
+
137
+ expect do
138
+ subject.load_plugin(:test)
139
+ end.to_not raise_exception
140
+ end
141
+
142
+ it 'tells the plugin to #load!' do
143
+ expect(plugin).to receive(:load!)
144
+ subject.load_plugin(:test)
145
+ end
146
+
147
+ it 'updates @loaded_plugins with the new theme' do
148
+ allow(plugin).to receive(:load!)
149
+
150
+ expect { subject.load_plugin(:test) }.
151
+ to change { subject.loaded_plugins.size }.
152
+ by(1)
153
+ end
154
+ end
155
+
156
+ context 'plugin_name does not represent an added plugin' do
157
+ it 'raises a PluginNotFound' do
158
+ expect do
159
+ described_class.load_plugin(:meow)
160
+ end.to raise_exception(Hairballs::PluginNotFound)
161
+ end
162
+ end
163
+ end
164
+
165
+ describe '.project_name' do
166
+ subject { described_class.project_name }
167
+ it { is_expected.to eq 'hairballs' }
168
+ end
169
+
170
+ describe '.rails?' do
171
+ subject { described_class.rails? }
172
+
173
+ context 'not using Rails' do
174
+ it { is_expected.to eq false }
175
+ end
176
+
177
+ context 'using Rails' do
178
+ context 'RAILS_ENV is defined' do
179
+ before { ENV['RAILS_ENV'] = 'blargh' }
180
+ after { ENV.delete('RAILS_ENV') }
181
+ it { is_expected.to eq true }
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,64 @@
1
+ $:.unshift(File.expand_path('../lib/hairballs', File.dirname(__FILE__)))
2
+
3
+ RSpec.configure do |config|
4
+ config.expect_with :rspec do |expectations|
5
+ # This option will default to `true` in RSpec 4. It makes the `description`
6
+ # and `failure_message` of custom matchers include text for helper methods
7
+ # defined using `chain`, e.g.:
8
+ # be_bigger_than(2).and_smaller_than(4).description
9
+ # # => "be bigger than 2 and smaller than 4"
10
+ # ...rather than:
11
+ # # => "be bigger than 2"
12
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
13
+ end
14
+
15
+ config.mock_with :rspec do |mocks|
16
+ # Prevents you from mocking or stubbing a method that does not exist on
17
+ # a real object. This is generally recommended, and will default to
18
+ # `true` in RSpec 4.
19
+ mocks.verify_partial_doubles = true
20
+ end
21
+
22
+ # The settings below are suggested to provide a good initial experience
23
+ # with RSpec, but feel free to customize to your heart's content.
24
+ # These two settings work together to allow you to limit a spec run
25
+ # to individual examples or groups you care about by tagging them with
26
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
27
+ # get run.
28
+ # config.filter_run :focus
29
+ # config.run_all_when_everything_filtered = true
30
+
31
+ # Limits the available syntax to the non-monkey patched syntax that is recommended.
32
+ # For more details, see:
33
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
34
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
35
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
36
+ # config.disable_monkey_patching!
37
+
38
+ # Many RSpec users commonly either run the entire suite or an individual
39
+ # file, and it's useful to allow more verbose output when running an
40
+ # individual spec file.
41
+ # if config.files_to_run.one?
42
+ # # Use the documentation formatter for detailed output,
43
+ # # unless a formatter has already been configured
44
+ # # (e.g. via a command-line flag).
45
+ # config.default_formatter = 'doc'
46
+ # end
47
+
48
+ # Print the 10 slowest examples and example groups at the
49
+ # end of the spec run, to help surface which specs are running
50
+ # particularly slow.
51
+ config.profile_examples = 3
52
+
53
+ # Run specs in random order to surface order dependencies. If you find an
54
+ # order dependency and want to debug it, you can fix the order by providing
55
+ # the seed, which is printed after each run.
56
+ # --seed 1234
57
+ config.order = :random
58
+
59
+ # Seed global randomization in this process using the `--seed` CLI option.
60
+ # Setting this allows you to use `--seed` to deterministically reproduce
61
+ # test failures related to randomization by passing the same `--seed` value
62
+ # as the one that triggered the failure.
63
+ # Kernel.srand config.seed
64
+ end