yap-shell 0.1.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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