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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/Gemfile +5 -0
- data/WISHLIST.md +14 -0
- data/addons/history/Gemfile +2 -0
- data/addons/history/history.rb +101 -0
- data/addons/history/lib/history/buffer.rb +204 -0
- data/addons/history/lib/history/events.rb +13 -0
- data/addons/keyboard_macros/keyboard_macros.rb +295 -0
- data/addons/prompt/Gemfile +1 -0
- data/addons/prompt/right_prompt.rb +17 -0
- data/addons/prompt_updates/prompt_updates.rb +28 -0
- data/addons/tab_completion/Gemfile +0 -0
- data/addons/tab_completion/lib/tab_completion/completer.rb +62 -0
- data/addons/tab_completion/lib/tab_completion/custom_completion.rb +33 -0
- data/addons/tab_completion/lib/tab_completion/dsl_methods.rb +7 -0
- data/addons/tab_completion/lib/tab_completion/file_completion.rb +75 -0
- data/addons/tab_completion/tab_completion.rb +157 -0
- data/bin/yap +13 -4
- data/lib/tasks/addons.rake +51 -0
- data/lib/yap.rb +4 -55
- data/lib/yap/shell.rb +51 -10
- data/lib/yap/shell/builtins.rb +2 -2
- data/lib/yap/shell/builtins/alias.rb +2 -2
- data/lib/yap/shell/builtins/cd.rb +9 -11
- data/lib/yap/shell/builtins/env.rb +11 -0
- data/lib/yap/shell/commands.rb +29 -18
- data/lib/yap/shell/evaluation.rb +185 -68
- data/lib/yap/shell/evaluation/shell_expansions.rb +85 -0
- data/lib/yap/shell/event_emitter.rb +18 -0
- data/lib/yap/shell/execution/builtin_command_execution.rb +1 -1
- data/lib/yap/shell/execution/command_execution.rb +3 -3
- data/lib/yap/shell/execution/context.rb +32 -9
- data/lib/yap/shell/execution/file_system_command_execution.rb +12 -7
- data/lib/yap/shell/execution/ruby_command_execution.rb +6 -6
- data/lib/yap/shell/execution/shell_command_execution.rb +17 -2
- data/lib/yap/shell/prompt.rb +21 -0
- data/lib/yap/shell/repl.rb +179 -18
- data/lib/yap/shell/version.rb +1 -1
- data/lib/yap/world.rb +149 -15
- data/lib/yap/world/addons.rb +135 -0
- data/rcfiles/.yaprc +240 -10
- data/test.rb +206 -0
- data/update-rawline.sh +6 -0
- data/yap-shell.gemspec +11 -3
- metadata +101 -10
- data/addons/history.rb +0 -171
data/lib/yap/shell/version.rb
CHANGED
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
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
18
|
-
|
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
|
27
|
-
::
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
39
|
-
|
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
|
-
#
|
7
|
-
#
|
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
|
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
|
-
|
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
|