yap-shell 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/Gemfile +5 -0
  4. data/WISHLIST.md +14 -0
  5. data/addons/history/Gemfile +2 -0
  6. data/addons/history/history.rb +101 -0
  7. data/addons/history/lib/history/buffer.rb +204 -0
  8. data/addons/history/lib/history/events.rb +13 -0
  9. data/addons/keyboard_macros/keyboard_macros.rb +295 -0
  10. data/addons/prompt/Gemfile +1 -0
  11. data/addons/prompt/right_prompt.rb +17 -0
  12. data/addons/prompt_updates/prompt_updates.rb +28 -0
  13. data/addons/tab_completion/Gemfile +0 -0
  14. data/addons/tab_completion/lib/tab_completion/completer.rb +62 -0
  15. data/addons/tab_completion/lib/tab_completion/custom_completion.rb +33 -0
  16. data/addons/tab_completion/lib/tab_completion/dsl_methods.rb +7 -0
  17. data/addons/tab_completion/lib/tab_completion/file_completion.rb +75 -0
  18. data/addons/tab_completion/tab_completion.rb +157 -0
  19. data/bin/yap +13 -4
  20. data/lib/tasks/addons.rake +51 -0
  21. data/lib/yap.rb +4 -55
  22. data/lib/yap/shell.rb +51 -10
  23. data/lib/yap/shell/builtins.rb +2 -2
  24. data/lib/yap/shell/builtins/alias.rb +2 -2
  25. data/lib/yap/shell/builtins/cd.rb +9 -11
  26. data/lib/yap/shell/builtins/env.rb +11 -0
  27. data/lib/yap/shell/commands.rb +29 -18
  28. data/lib/yap/shell/evaluation.rb +185 -68
  29. data/lib/yap/shell/evaluation/shell_expansions.rb +85 -0
  30. data/lib/yap/shell/event_emitter.rb +18 -0
  31. data/lib/yap/shell/execution/builtin_command_execution.rb +1 -1
  32. data/lib/yap/shell/execution/command_execution.rb +3 -3
  33. data/lib/yap/shell/execution/context.rb +32 -9
  34. data/lib/yap/shell/execution/file_system_command_execution.rb +12 -7
  35. data/lib/yap/shell/execution/ruby_command_execution.rb +6 -6
  36. data/lib/yap/shell/execution/shell_command_execution.rb +17 -2
  37. data/lib/yap/shell/prompt.rb +21 -0
  38. data/lib/yap/shell/repl.rb +179 -18
  39. data/lib/yap/shell/version.rb +1 -1
  40. data/lib/yap/world.rb +149 -15
  41. data/lib/yap/world/addons.rb +135 -0
  42. data/rcfiles/.yaprc +240 -10
  43. data/test.rb +206 -0
  44. data/update-rawline.sh +6 -0
  45. data/yap-shell.gemspec +11 -3
  46. metadata +101 -10
  47. data/addons/history.rb +0 -171
@@ -1,5 +1,5 @@
1
1
  module Yap
2
2
  module Shell
3
- VERSION = "0.1.1"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
data/lib/yap/world.rb CHANGED
@@ -1,44 +1,178 @@
1
1
  require 'term/ansicolor'
2
2
  require 'forwardable'
3
+
4
+ require 'rawline'
3
5
  require 'yap/shell/execution'
6
+ require 'yap/shell/prompt'
7
+ require 'yap/world/addons'
8
+ require 'termios'
4
9
 
5
10
  module Yap
6
11
  class World
7
12
  include Term::ANSIColor
8
13
  extend Forwardable
9
14
 
10
- attr_accessor :prompt, :contents, :addons
15
+ DEFAULTS = {
16
+ primary_prompt_text: "yap> ",
17
+ secondary_prompt_text: "> "
18
+ }
19
+
20
+ attr_accessor :prompt, :secondary_prompt, :contents, :repl, :editor, :env
21
+ attr_reader :addons
22
+
23
+ attr_accessor :last_result
24
+
25
+ def self.instance(*args)
26
+ @instance ||= new(*args)
27
+ end
28
+
29
+ def initialize(addons:)
30
+ @env = ENV.to_h.dup
31
+ dom = build_editor_dom
32
+
33
+ @editor = RawLine::Editor.create(dom: dom)
34
+
35
+ self.prompt = Yap::Shell::Prompt.new(text: DEFAULTS[:primary_prompt_text])
36
+ self.secondary_prompt = Yap::Shell::Prompt.new(text: DEFAULTS[:secondary_prompt_text])
11
37
 
12
- def initialize(options)
13
- (options || {}).each do |k,v|
14
- self.send "#{k}=", v
38
+ @repl = Yap::Shell::Repl.new(world:self)
39
+
40
+ @addons = addons.reduce(Hash.new) do |hsh, addon|
41
+ hsh[addon.addon_name] = addon
42
+ hsh
15
43
  end
16
44
 
17
- addons.each do |addon|
18
- addon.initialize_world(self)
45
+ # initialize after they are all loaded in case they reference each other.
46
+ addons.each { |addon| addon.initialize_world(self) }
47
+ end
48
+
49
+ def [](addon_name)
50
+ @addons.fetch(addon_name){ raise(ArgumentError, "No addon loaded registered as #{addon_name}") }
51
+ end
52
+
53
+ def events
54
+ @editor.events
55
+ end
56
+
57
+ def bind(key, &blk)
58
+ @editor.bind(key) do
59
+ blk.call self
19
60
  end
20
61
  end
21
62
 
63
+ def unbind(key)
64
+ @editor.unbind(key)
65
+ end
66
+
22
67
  def func(name, &blk)
23
68
  Yap::Shell::ShellCommand.define_shell_function(name, &blk)
24
69
  end
25
70
 
26
- def readline
27
- ::Readline
71
+ def shell(statement)
72
+ context = Yap::Shell::Execution::Context.new(
73
+ stdin: $stdin,
74
+ stdout: $stdout,
75
+ stderr: $stderr
76
+ )
77
+ if statement.nil?
78
+ @last_result = Yap::Shell::Execution::Result.new(
79
+ status_code: 1,
80
+ directory: Dir.pwd,
81
+ n: 1,
82
+ of: 1
83
+ )
84
+ else
85
+ evaluation = Yap::Shell::Evaluation.new(stdin:$stdin, stdout:$stdout, stderr:$stderr, world:self)
86
+ evaluation.evaluate(statement) do |command, stdin, stdout, stderr, wait|
87
+ context.clear_commands
88
+ context.add_command_to_run command, stdin:stdin, stdout:stdout, stderr:stderr, wait:wait
89
+ @last_result = context.execute(world:self)
90
+ end
91
+ end
92
+
93
+ @last_result
94
+ end
95
+
96
+ def foreground?
97
+ Process.getpgrp == Termios.tcgetpgrp($stdout)
98
+ end
99
+
100
+ def history
101
+ @editor.history
102
+ end
103
+
104
+ def interactive!
105
+ refresh_prompt
106
+ @editor.start
28
107
  end
29
108
 
30
109
  def prompt
31
- if @prompt.respond_to? :call
32
- @prompt.call
33
- else
34
- @prompt
110
+ @prompt
111
+ end
112
+
113
+ def prompt=(prompt=nil, &blk)
114
+ # TODO if prompt_controller then undefine, cancel events, etc
115
+ if prompt.is_a?(Yap::Shell::Prompt)
116
+ @prompt = prompt
117
+ elsif prompt.respond_to?(:call) # proc
118
+ @prompt = Yap::Shell::Prompt.new(text:prompt.call, &prompt)
119
+ else # text
120
+ @prompt = Yap::Shell::Prompt.new(text:prompt, &blk)
35
121
  end
36
122
  end
37
123
 
38
- (String.instance_methods - Object.instance_methods).each do |m|
39
- next if [:object_id, :__send__, :initialize].include?(m)
40
- def_delegator :@contents, m
124
+ def refresh_prompt
125
+ @editor.prompt = @prompt.update.text
41
126
  end
42
127
 
128
+ def right_prompt_text=(str)
129
+ @right_status_float.width = str.length
130
+ @right_status_box.content = str
131
+ end
132
+
133
+ def subscribe(*args, &blk)
134
+ @editor.subscribe(*args, &blk)
135
+ end
136
+
137
+ def build_editor_dom
138
+ @left_status_box = TerminalLayout::Box.new(content: "", style: {display: :inline})
139
+ @right_status_box = TerminalLayout::Box.new(content: "", style: {display: :inline})
140
+ @prompt_box = TerminalLayout::Box.new(content: "yap>", style: {display: :inline})
141
+ @input_box = TerminalLayout::InputBox.new(content: "", style: {display: :inline})
142
+
143
+ @content_box = TerminalLayout::Box.new(content: "", style: {display: :block})
144
+ @bottom_left_status_box = TerminalLayout::Box.new(content: "", style: {display: :inline})
145
+ @bottom_right_status_box = TerminalLayout::Box.new(content: "", style: {display: :inline})
146
+
147
+ @right_status_float = TerminalLayout::Box.new(style: {display: :float, float: :right, width: @right_status_box.content.length},
148
+ children: [
149
+ @right_status_box
150
+ ]
151
+ )
152
+
153
+ RawLine::DomTree.new(
154
+ children:[
155
+ @right_status_float,
156
+ @left_status_box,
157
+ @prompt_box,
158
+ @input_box,
159
+ @content_box,
160
+ TerminalLayout::Box.new(style: {display: :float, float: :left, width: @bottom_left_status_box.content.length},
161
+ children: [
162
+ @bottom_left_status_box
163
+ ]
164
+ ),
165
+ TerminalLayout::Box.new(style: {display: :float, float: :right, width: @bottom_right_status_box.content.length},
166
+ children: [
167
+ @bottom_right_status_box
168
+ ]
169
+ )
170
+ ]
171
+ ).tap do |dom|
172
+ dom.prompt_box = @prompt_box
173
+ dom.input_box = @input_box
174
+ dom.content_box = @content_box
175
+ end
176
+ end
43
177
  end
44
178
  end
@@ -0,0 +1,135 @@
1
+ require 'pathname'
2
+
3
+ module Yap
4
+ class World
5
+ module UserAddons
6
+ end
7
+
8
+ module AddonMethods
9
+ module ClassMethods
10
+ def load_addon
11
+ # no-op, override in subclass if you need to do anything special
12
+ # when your addon is first loaded when the shell starts
13
+ end
14
+
15
+ def addon_name
16
+ @addon_name ||= self.name.split(/::/).last.scan(/[A-Z][^A-Z]+/).map(&:downcase).reject{ |f| f == "addon" }.join("_").to_sym
17
+ end
18
+
19
+ def require(name)
20
+ directory = File.dirname caller[0].split(':').first
21
+ lib_path = File.join directory, "lib"
22
+ support_file = File.join lib_path, "#{name}.rb"
23
+ namespace = self.name.split('::').reduce(Object) do |context,n|
24
+ o = context.const_get(n)
25
+ break o if o.is_a?(Namespace)
26
+ o
27
+ end
28
+ if File.exists?(support_file) && namespace
29
+ namespace.module_eval IO.read(support_file), support_file, lineno=1
30
+ else
31
+ super(name)
32
+ end
33
+ end
34
+ end
35
+
36
+ module InstanceMethods
37
+ def addon_name
38
+ @addon_name ||= self.class.addon_name
39
+ end
40
+ end
41
+ end
42
+
43
+ module Namespace
44
+ end
45
+
46
+ class Addon
47
+ extend AddonMethods::ClassMethods
48
+ include AddonMethods::InstanceMethods
49
+ end
50
+
51
+ module Addons
52
+ def self.syntax_ok?(file)
53
+ `ruby -c #{file}`
54
+ $?.exitstatus == 0
55
+ end
56
+
57
+ def self.load_rcfiles(files)
58
+ files.map do |file|
59
+ RcFile.new IO.read(file)
60
+ end
61
+ end
62
+
63
+ def self.load_directories(directories)
64
+ directories.map do |d|
65
+ next unless File.directory?(d)
66
+ load_directory(d).map(&:new)
67
+ end.flatten
68
+ end
69
+
70
+ class RcFile < Addon
71
+ def initialize(contents)
72
+ @contents = contents
73
+ end
74
+
75
+ def initialize_world(world)
76
+ world.instance_eval @contents
77
+ end
78
+ end
79
+
80
+ def self.load_directory(directory)
81
+ namespace = File.basename(directory).
82
+ split(/[_-]/).
83
+ map(&:capitalize).join
84
+ namespace = "#{namespace}Addon"
85
+
86
+ if Yap::World::UserAddons.const_defined?(namespace)
87
+ raise LoadError, "#{namespace} is already defined! Failed loading #{file}"
88
+ end
89
+
90
+ # Create a wrapper module for every add-on. This is to eliminate
91
+ # namespace collision.
92
+ addon_module = Module.new do
93
+ extend Namespace
94
+ extend AddonMethods::ClassMethods
95
+ const_set :Addon, Addon
96
+ end
97
+
98
+ Yap::World::UserAddons.const_set namespace, addon_module
99
+
100
+ lib_path = File.join directory, "lib"
101
+ $LOAD_PATH.unshift lib_path
102
+
103
+ gemfiles = Dir["#{directory}/Gemfile"]
104
+ gemfiles.each do |gemfile|
105
+ eval File.read(gemfile)
106
+ end
107
+
108
+ Dir["#{directory}/*.rb"].map do |addon_file|
109
+ load_file(addon_file, namespace:namespace, dir:directory, addon_module:addon_module)
110
+ end
111
+ ensure
112
+ $LOAD_PATH.delete(lib_path) if lib_path
113
+ end
114
+
115
+ def self.load_file(file, dir:, namespace:, addon_module:)
116
+ klass_name = file.sub(dir, "").
117
+ sub(/^#{Regexp.escape(File::Separator)}/, "").
118
+ sub(File.extname(file.to_s), "").
119
+ split(File::Separator).
120
+ map{ |m| m.split(/[_-]/).map(&:capitalize).join }.
121
+ join("::")
122
+
123
+ addon_module.module_eval IO.read(file), file, lineno=1
124
+
125
+ klass_name.split("::").reduce(addon_module) do |ns,name|
126
+ if ns.const_defined?(name)
127
+ ns.const_get(name)
128
+ else
129
+ raise("Did not find #{klass_name} in #{file}")
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
data/rcfiles/.yaprc CHANGED
@@ -1,25 +1,80 @@
1
1
  #!/usr/bin/ruby
2
2
 
3
- ENV["PATH"] = "/Applications/Postgres.app/Contents/MacOS/bin:/usr/local/share/npm/bin/:/usr/local/heroku/bin:/Users/zdennis/.bin:/Users/zdennis/.rvm/gems/ruby-2.1.5/bin:/Users/zdennis/.rvm/gems/ruby-2.1.5@global/bin:/Users/zdennis/.rvm/rubies/ruby-2.1.5/bin:/usr/local/bin:/usr/local/sbin:/Users/zdennis/bin:/bin:/opt/local/bin:/opt/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/CrossPack-AVR/bin:/private/tmp/.tidbits/bin:/Users/zdennis/source/playground/AdobeAir/AdobeAIRSDK/bin:/Users/zdennis/.rvm/bin:/Users/zdennis/Downloads/adt-bundle-mac-x86_64-20130219/sdk/tools/:/Users/zdennis/.rvm/bin"
4
- ENV["GEM_HOME"] = "/Users/zdennis/.rvm/gems/ruby-2.1.5:/Users/zdennis/.rvm/gems/ruby-2.1.5@global"
3
+ # ENV["PATH"] = "/Applications/Postgres.app/Contents/MacOS/bin:/usr/local/share/npm/bin/:/usr/local/heroku/bin:/Users/zdennis/.bin:/Users/zdennis/.rvm/gems/ruby-2.1.5/bin:/Users/zdennis/.rvm/gems/ruby-2.1.5@global/bin:/Users/zdennis/.rvm/rubies/ruby-2.1.5/bin:/usr/local/bin:/usr/local/sbin:/Users/zdennis/bin:/bin:/opt/local/bin:/opt/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/CrossPack-AVR/bin:/private/tmp/.tidbits/bin:/Users/zdennis/source/playground/AdobeAir/AdobeAIRSDK/bin:/Users/zdennis/.rvm/bin:/Users/zdennis/Downloads/adt-bundle-mac-x86_64-20130219/sdk/tools/:/Users/zdennis/.rvm/bin"
4
+ # ENV["GEM_HOME"] = "/Users/zdennis/.rvm/gems/ruby-2.1.5:/Users/zdennis/.rvm/gems/ruby-2.1.5@global"
5
5
 
6
- # require 'chronic'
7
- # require 'term/ansicolor'
6
+ # This is only necessary when starting yap from another shell as it inherits that shell's
7
+ # environment.
8
+ keys2keep = %w(COLORFGBG DISPLAY EDITOR HOME LANG LOGNAME MAIL PATH PS1 PWD SHELL SHLVL SSH_AUTH_SOCK SUDO_COMMAND SUDO_GID SUDO_UID SUDO_USER TERM USER USERNAME _ __CF_USER_TEXT_ENCODING)
9
+ world.env.keys.sort.each do |key|
10
+ unless keys2keep.include?(key)
11
+ world.env.delete(key)
12
+ end
13
+ end
14
+
15
+ world.env["PATH"] = [
16
+ world.env["HOME"] + "/.rbenv/shims",
17
+ "/usr/local/bin",
18
+ world.env["PATH"]
19
+ ].join(":")
20
+
21
+ world.env.delete("RBENV_DIR")
22
+ world.env.delete("RBENV_DIR")
23
+ world.env.delete("RBENV_HOOK_PATH")
24
+ world.env.delete("RBENV_ROOT")
25
+ world.env.delete("RBENV_VERSION")
26
+
27
+ require 'chronic'
28
+ require 'term/ansicolor'
29
+ require 'terminfo'
30
+
31
+ env.delete("BUNDLE_GEMFILE")
32
+ # env.delete("BUNDLE_BIN_PATH")
33
+
34
+ func :reload! do |args:, stdin:, stdout:, stderr:|
35
+ stdout.puts "Reloading shell:"
36
+ stdout.print " Saving history "
37
+ world.addons[:history].save
38
+ stdout.puts Term::ANSIColor.green("done")
39
+ exec File.expand_path($0)
40
+ end
41
+
42
+ completion_cache = {}
43
+
44
+ # tab_completion_display do |matches|
45
+ #
46
+ # end
47
+
48
+ tab_completion :rake, /^(rake|be rake)\s+(.*)/ do |input_fragment, match_data|
49
+ # |config|
50
+ # config.completions do |input_fragment, match_data|
51
+ results = completion_cache[Dir.pwd] ||= `bundle exec rake -T`.gsub(/^rake\s*/, '').split(/\n/)
52
+
53
+ task_rgx = /^#{Regexp.escape(input_fragment.word[:text])}/
54
+ results.grep(task_rgx).map do |text|
55
+ {
56
+ type: :rake,
57
+ text: text.gsub(/\s*#.*$/, ''),
58
+ descriptive_text: Term::ANSIColor.yellow(text)
59
+ }
60
+ # end
61
+ end
62
+ end
8
63
 
9
64
  #
10
65
  # Configuring your prompt. This can be set to a static value or to a
11
66
  # Proc like object that responds to #call. If it responds to call it will
12
67
  # be used every time the prompt is to be re-drawn
13
68
  #
14
-
69
+ last_prompt = nil
15
70
  self.prompt = -> do
16
71
  pwd = Dir.pwd.sub Regexp.new(ENV['HOME']), '~'
17
72
 
18
- git_current_branch = `git cbranch 2>/dev/null`.chomp
73
+ git_current_branch = `git branch 2>/dev/null | sed -n '/\* /s///p'`.chomp
19
74
  if git_current_branch.length > 0
20
75
  git_current_branch += " "
21
- git_dirty_not_cached = `git diff --shortstat`.length > 0
22
- git_dirty_cached = `git diff --shortstat --cached`.length > 0
76
+ git_dirty_not_cached = `git diff --shortstat 2>/dev/null`.length > 0
77
+ git_dirty_cached = `git diff --shortstat --cached 2>/dev/null`.length > 0
23
78
 
24
79
  if git_dirty_not_cached || git_dirty_cached
25
80
  git_branch = intense_cyan(git_current_branch)
@@ -33,11 +88,186 @@ self.prompt = -> do
33
88
  arrow = '➜'
34
89
 
35
90
  # ~/source/playground/yap master ➜
36
- "#{dark(green('£'))} #{yellow(pwd)} #{git_branch}#{red(arrow)} "
91
+ last_prompt = "#{dark(green('£'))} #{yellow(pwd)} #{git_branch}#{red(arrow)} "
37
92
  end
38
93
 
39
94
 
40
- func :upcase do |args:, stdin:, stdout:, stderr:|
95
+ ###############################################################################
96
+ # KEYBOARD MACROS
97
+ #------------------------------------------------------------------------------
98
+ # Keyboard macros allow you to define key/byte sequences that run code
99
+ # when typed. Perhaps the simpest macro is one that takes the tediousness
100
+ # out of typing a long command. For example, pressing "Ctrl-g l" might
101
+ # type in "git log --name-status -n100" just as if the user had typed it.
102
+ #
103
+ # There are five things to know about macros in Yap:
104
+ #
105
+ # * Macros are initialized by a trigger key. The default is Ctrl-g.
106
+ # * Macros require at least one character/key/byte sequence beyond the trigger \
107
+ # key in order to fire
108
+ # * Macros can be bound to code blocks or a string.
109
+ # * When a macro returns a string that string is inserted as user input \
110
+ # at the current cursor position
111
+ # * When a macro returns a string that ends in a new line it will process the \
112
+ # line as if the user hit enter
113
+ #
114
+ # == Example
115
+ #
116
+ # world.addons[:keyboard_macros].configure(trigger_key: :ctrl_g) do |macros|
117
+ # macros.define :z, 'git open-pull'
118
+ # macros.define 'l', "git log -n1\n"
119
+ # end
120
+ #
121
+ # It's a little bit wordy right now to setup because macros are not special
122
+ # in Yap. They are provided as a standard yap-addon. You could even provide
123
+ # your own macro addon replacement if you so desired.
124
+ #
125
+ # Following, are a few examples showcasing macros.
126
+ ###############################################################################
127
+
128
+ # Sets the default trigger key for all keyboard macros
129
+ world.addons[:keyboard_macros].trigger_key = ?\C-g
130
+
131
+ # Sets the default cancel key for all keyboard macros
132
+ world.addons[:keyboard_macros].cancel_key = " "
133
+
134
+ # Sets the default timeout for macros. When set to nil you will have to
135
+ # use the cancel key to exit out of macros.
136
+ world.addons[:keyboard_macros].timeout_in_ms = nil
137
+
138
+ # Forgiveness-mode: Automatically cancel if the sequence is unknown. When
139
+ # set to false you can keep attempting to type in your macro.
140
+ world.addons[:keyboard_macros].cancel_on_unknown_sequences = true
141
+
142
+ # Or, you can set the trigger key for a particular set of macros
143
+ # by specifying it when you call .configure(...).
144
+ world.addons[:keyboard_macros].configure(trigger_key: ?\C-g) do |macro|
145
+ macro.start do
146
+ world.editor.content_box.children = [
147
+ TerminalLayout::Box.new(content: "am i floating1?", style: {display: :float, float: :right, height: 1, width: "am i floating1?".length}),
148
+ TerminalLayout::Box.new(content: "What up12?", style: {display: :block}),
149
+ TerminalLayout::Box.new(content: "Not much21", style: {display: :block}),
150
+ TerminalLayout::Box.new(content: "am i floating3?", style: {display: :float, float: :left, height: 1, width: "am i floating1?".length}),
151
+ ]
152
+ end
153
+
154
+ macro.stop do
155
+ world.editor.content_box.children = []
156
+ end
157
+
158
+ macro.define 'z', 'git open-pull'
159
+ # macro.define 'abc', 'echo abc'
160
+ # macro.define 'u', -> { world.editor.undo }
161
+ macro.define :up_arrow, -> { }
162
+
163
+ macro.define 'l', 'git log ' do |macro|
164
+ macro.fragment 'n', '--name-status '
165
+ macro.fragment 'o', '--oneline '
166
+ macro.fragment /\d/, -> (a) { "-n#{a} " }
167
+ end
168
+
169
+ macro.define 'd', 'git diff ' do |macro|
170
+ macro.define 'n', '--name-status ' do |macro|
171
+ macro.define 'm', "master..HEAD"
172
+ end
173
+ macro.define 'm', "master..HEAD"
174
+ end
175
+ end
176
+
177
+ # world.addons[:keyboard_macros].configure(trigger_key: :ctrl_h) do |macros|
178
+ # macros.define 'h123', -> {
179
+ # box = TerminalLayout::Box.new(content: "Right?", style: { display: :block, float: :right, height: 1, width: 50 })
180
+ # world.editor.content_box.children = [box]
181
+ # 'echo this was with a code block'
182
+ # }
183
+ # end
184
+
185
+
186
+ ###############################################################################
187
+ # USER-DEFINED FUNCTIONS
188
+ #------------------------------------------------------------------------------
189
+ # User-defined functions can be accessed in the shell like any command. They
190
+ # take precedence over programs found on the file-system, but they do not
191
+ # take precedent over user-defined aliases.
192
+ #
193
+ # For example, take `upcase` below:
194
+ #
195
+ # func :upcase do |stdin:, stdout:|
196
+ # str = stdin.read
197
+ # stdout.puts str.upcase
198
+ # end
199
+ #
200
+ # You can issue "upcase" in the shell where-ever you'd expect to place the name
201
+ # of a command.
202
+ #
203
+ # == Function Parameters
204
+ #
205
+ # User-defined functions can receive the following arguments:
206
+ #
207
+ # * command: the name of the command the user-entered
208
+ # * args: the list of arguments supplied to the command
209
+ # * stdin: the way to access stdin (e.g. DO NOT CALL STDIN or $stdin)
210
+ # * stdout: the way to access stdout (e.g. DO NOT CALL STDOUT or $stdout)
211
+ # * stderr: the way to access stderr (e.g. DO NOT CALL STDERR or $stderr)
212
+ # * world: the Shell's currently known world
213
+ #
214
+ # These arguments are all optional. You only need to specify what your
215
+ # function is going to use.
216
+ #
217
+ # Following, are a number of examples showcasing their power and flexibility.
218
+ ###############################################################################
219
+
220
+ # upcase is reads from stdin and upcases every letter.
221
+ #
222
+ # Example:
223
+ # yap> echo "hi there" | upcase
224
+ # HI THERE
225
+ func :upcase do |stdin:, stdout:|
41
226
  str = stdin.read
42
227
  stdout.puts str.upcase
43
228
  end
229
+
230
+ func :'run-modified-specs' do |stdin:, stdout:|
231
+ str = `git status`
232
+ specs = str.scan(/\S+_spec.rb/)
233
+ cmd = "bundle exec rspec #{specs.join(' ')}"
234
+ stdout.puts cmd
235
+ shell cmd
236
+ end
237
+
238
+ # This shell function uses a Regexp to match on a command of 2 or more dots.
239
+ # It's for traversing up N directories. Two dots ("..") is the minimum and
240
+ # is used to go to the parent. Every dot after that goes up one more directory
241
+ # level.
242
+ #
243
+ # Example:
244
+ # ~/foo/bar/baz> ..
245
+ # ~/foo/bar> ...
246
+ # ~/
247
+ func /^\.{2,}$/ do |command:|
248
+ (command.length - 1).times { Dir.chdir("..") }
249
+ end
250
+
251
+ func /^\+(.*)/ do |command:, args:|
252
+ puts command
253
+ puts args.inspect
254
+ end
255
+
256
+ # This shell function uses a custom object that responds to the #match(...)
257
+ # method. This is nothing more than an basic "history" implementation.
258
+ #
259
+ history_matcher = Object.new
260
+ def history_matcher.match(command)
261
+ command == ".h"
262
+ end
263
+
264
+ # Allows for a single numeric argument which will be used to determine
265
+ # how many history items to show (not including this command). If no argument
266
+ # if provided then it will show the entire shell history.
267
+ func history_matcher do |world:, args:, stdout:|
268
+ num_commands = args.first.to_i
269
+ num_commands = world.history.length - 1 if num_commands == 0
270
+ world.history[-(num_commands + 1)...-1].each_with_index do |command, i|
271
+ stdout.puts " #{i} #{command}"
272
+ end
273
+ end