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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 246c3bc588c78cfde392f379b5ca086e6aac79e0
|
4
|
+
data.tar.gz: f1c54403abc8e95a96f255582b6b3af4fcb511ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61414d9cc1b15a58661bca3aac9911798fe6b8aabc99e4d03732109c29d4c2fa2d9928f2c43d0cadfb736a45f4ca056c41657226d654c3b04d2dc9ce14f1ae35
|
7
|
+
data.tar.gz: a3999e8f5fcbcaab03450309ecfc1a7942d3bf2f072a75862e96f203a5b9c3b89ee31ffde11ba3eedd59780a1f75a519107f04b9aa816b48a16cceef6b510eaa
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2.3
|
data/Gemfile
CHANGED
@@ -2,3 +2,8 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in yap.gemspec
|
4
4
|
gemspec
|
5
|
+
# gem 'yap-shell-parser', path: "../yap-shell-parser"
|
6
|
+
# gem 'yap-shell-parser', git: 'git@github.com:zdennis/yap-shell-parser.git'
|
7
|
+
# gem "rawline", git: 'git@github.com:zdennis/rawline.git', branch: 'issues/input-issues'
|
8
|
+
# gem "rawline", path: '/Users/zdennis/source/opensource_projects/rawline'
|
9
|
+
# gem "pry-byebug"
|
data/WISHLIST.md
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
CLI
|
2
|
+
|
3
|
+
* yap install right-prompt
|
4
|
+
|
1
5
|
|
2
6
|
* need to add custom tab completion for aliases, builtins, shell commands, and file system commands
|
3
7
|
* support user-specified gems to install (so they're available for rc files)
|
@@ -38,3 +42,13 @@ Others requests.
|
|
38
42
|
* @dylanized: themeable
|
39
43
|
* @dylanized: browser-based
|
40
44
|
* @dylanized: bookmarks
|
45
|
+
|
46
|
+
|
47
|
+
### Tab completion
|
48
|
+
* sort completions for printing
|
49
|
+
* print completion in groups?
|
50
|
+
* match on command, command with arguments, arguments, env var args
|
51
|
+
* supply the completion text
|
52
|
+
* supply the descriptive text
|
53
|
+
* alias one completion for another
|
54
|
+
* cache completions? invalidate cached completions
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'term/ansicolor'
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
class History < Addon
|
6
|
+
require 'history/buffer'
|
7
|
+
require 'history/events'
|
8
|
+
|
9
|
+
Color = Object.extend Term::ANSIColor
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_accessor :history_item_formatter, :ignore_history_item
|
13
|
+
end
|
14
|
+
|
15
|
+
self.ignore_history_item = ->(item:) do
|
16
|
+
item.command == "exit"
|
17
|
+
end
|
18
|
+
|
19
|
+
self.history_item_formatter = ->(item:, options:{}) do
|
20
|
+
if item.duration
|
21
|
+
sprintf(
|
22
|
+
"%#{options[:max_position_width]}d %-s %s",
|
23
|
+
item.position,
|
24
|
+
item.command,
|
25
|
+
Color.negative(Color.intense_black(item.duration))
|
26
|
+
)
|
27
|
+
else
|
28
|
+
sprintf("%#{options[:max_position_width]}d %-s", item.position, item.command)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize_world(world)
|
33
|
+
@world = world
|
34
|
+
load_history
|
35
|
+
|
36
|
+
world.editor.bind(:ctrl_h) { show_history(@world.editor) }
|
37
|
+
|
38
|
+
world.func(:history) do |args:, stdin:, stdout:, stderr:|
|
39
|
+
first_arg = args.first
|
40
|
+
case first_arg
|
41
|
+
when String
|
42
|
+
if first_arg.start_with?("/")
|
43
|
+
regex = first_arg.gsub(/(^\|\/$)/, '')
|
44
|
+
ignore_history_item = ->(item:, options:{}) do
|
45
|
+
item.command !~ /#{regex}/
|
46
|
+
end
|
47
|
+
else
|
48
|
+
ignore_history_item = ->(item:, options:{}) do
|
49
|
+
item.command !~ /#{first_arg}/
|
50
|
+
end
|
51
|
+
end
|
52
|
+
show_history(world.editor, redraw_prompt:false, ignore_history_item:ignore_history_item)
|
53
|
+
else
|
54
|
+
show_history(world.editor, redraw_prompt:false)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def show_history(editor, redraw_prompt:true, ignore_history_item:nil, history_item_formatter:nil)
|
60
|
+
editor.puts @history
|
61
|
+
end
|
62
|
+
|
63
|
+
def executing(command:, started_at:)
|
64
|
+
# raise "Cannot acknowledge execution beginning of a command when no group has been started!" unless history.last
|
65
|
+
end
|
66
|
+
|
67
|
+
def executed(command:, stopped_at:)
|
68
|
+
# raise "Cannot complete execution of a command when no command has been started!" unless history.last
|
69
|
+
end
|
70
|
+
|
71
|
+
def save
|
72
|
+
File.open(history_file, "a") do |file|
|
73
|
+
# Don't write the YAML header because we're going to append to the
|
74
|
+
# history file, not overwrite. YAML works fine without it.
|
75
|
+
file.write @world.editor.history.to_yaml(@history_start_position..-1).gsub(/^---.*/, '')
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def history
|
82
|
+
@history
|
83
|
+
end
|
84
|
+
|
85
|
+
def history_file
|
86
|
+
@history_file ||= File.expand_path('~') + '/.yap-history'
|
87
|
+
end
|
88
|
+
|
89
|
+
def load_history
|
90
|
+
@world.editor.history = @history = History::Buffer.new(Float::INFINITY)
|
91
|
+
@history_start_position = 0
|
92
|
+
|
93
|
+
at_exit { save }
|
94
|
+
|
95
|
+
return unless File.exists?(history_file) && File.readable?(history_file)
|
96
|
+
|
97
|
+
history_elements = YAML.load_file(history_file) || []
|
98
|
+
@history_start_position = history_elements.length
|
99
|
+
@world.editor.history.replace(history_elements)
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
class History
|
2
|
+
class Buffer < Array
|
3
|
+
attr_reader :position, :size
|
4
|
+
attr_accessor :duplicates, :exclude, :cycle
|
5
|
+
|
6
|
+
#
|
7
|
+
# Create an instance of History::Buffer.
|
8
|
+
# This method takes an optional block used to override the
|
9
|
+
# following instance attributes:
|
10
|
+
# * <tt>@duplicates</tt> - whether or not duplicate items will be stored in the buffer.
|
11
|
+
# * <tt>@exclude</tt> - a Proc object defining exclusion rules to prevent items from being added to the buffer.
|
12
|
+
# * <tt>@cycle</tt> - Whether or not the buffer is cyclic.
|
13
|
+
#
|
14
|
+
def initialize(size)
|
15
|
+
@duplicates = true
|
16
|
+
@exclude = lambda{|a|}
|
17
|
+
@cycle = false
|
18
|
+
yield self if block_given?
|
19
|
+
@size = size
|
20
|
+
@position = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def supports_partial_text_matching?
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def supports_matching_text?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Clears the current position on the history object. Useful when deciding
|
33
|
+
# to cancel/reset history navigation.
|
34
|
+
#
|
35
|
+
def clear_position
|
36
|
+
@position = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def searching?
|
40
|
+
!!@position
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Resize the buffer, resetting <tt>@position</tt> to nil.
|
45
|
+
#
|
46
|
+
def resize(new_size)
|
47
|
+
if new_size < @size
|
48
|
+
@size-new_size.times { pop }
|
49
|
+
end
|
50
|
+
@size = new_size
|
51
|
+
@position = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Clear the content of the buffer and reset <tt>@position</tt> to nil.
|
56
|
+
#
|
57
|
+
def empty
|
58
|
+
@position = nil
|
59
|
+
clear
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Retrieve a copy of the element at <tt>@position</tt>.
|
64
|
+
#
|
65
|
+
def get
|
66
|
+
return nil unless length > 0
|
67
|
+
return nil unless @position
|
68
|
+
at(@position).dup
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Return true if <tt>@position</tt> is at the end of the buffer.
|
73
|
+
#
|
74
|
+
def end?
|
75
|
+
@position == length-1
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Return true if <tt>@position</tt> is at the start of the buffer.
|
80
|
+
#
|
81
|
+
def start?
|
82
|
+
@position == 0
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# Decrement <tt>@position</tt>. By default the history will become
|
87
|
+
# positioned at the previous item.
|
88
|
+
#
|
89
|
+
def back(matching_text: nil)
|
90
|
+
return nil unless length > 0
|
91
|
+
@position = search_back(matching_text: matching_text) || @position
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# Increment <tt>@position</tt>. By default the history will become
|
96
|
+
# positioned at the next item.
|
97
|
+
#
|
98
|
+
def forward(matching_text: nil)
|
99
|
+
return nil unless length > 0
|
100
|
+
@position = search_forward(matching_text: matching_text) || @position
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# Add a new item to the buffer.
|
105
|
+
#
|
106
|
+
def push(item)
|
107
|
+
item = item.without_ansi if item.respond_to?(:without_ansi)
|
108
|
+
|
109
|
+
if !@duplicates && self[-1] == item
|
110
|
+
# skip adding this line
|
111
|
+
return
|
112
|
+
end
|
113
|
+
|
114
|
+
unless @exclude.call(item)
|
115
|
+
# Remove the oldest element if size is exceeded
|
116
|
+
if @size <= length
|
117
|
+
reverse!.pop
|
118
|
+
reverse!
|
119
|
+
end
|
120
|
+
# Add the new item and reset the position
|
121
|
+
super(item)
|
122
|
+
@position = nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
alias << push
|
127
|
+
|
128
|
+
def to_yaml(range=(0..-1))
|
129
|
+
self[range].map do |element|
|
130
|
+
element.kind_of?(String) ? element : element.to_h
|
131
|
+
end.to_yaml
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def search_back(matching_text:)
|
137
|
+
if !matching_text
|
138
|
+
matching_text = ""
|
139
|
+
elsif matching_text.respond_to?(:without_ansi)
|
140
|
+
matching_text = matching_text.without_ansi
|
141
|
+
end
|
142
|
+
|
143
|
+
command_history = self
|
144
|
+
upto_index = (position || length) - 1
|
145
|
+
current = get
|
146
|
+
|
147
|
+
# $z.puts
|
148
|
+
# $z.puts <<-EOS.gsub(/^\s*\|/, '')
|
149
|
+
# |Search backward:
|
150
|
+
# | current:#{current.inspect}
|
151
|
+
# | history: #{command_history.inspect}
|
152
|
+
# | history position: #{position}
|
153
|
+
# | matching_text: #{matching_text.inspect}
|
154
|
+
# | upto_index: #{upto_index}
|
155
|
+
# | snapshot: #{command_history[0..upto_index].reverse.inspect}
|
156
|
+
# EOS
|
157
|
+
|
158
|
+
return position unless upto_index >= 0
|
159
|
+
|
160
|
+
snapshot = command_history[0..upto_index].reverse
|
161
|
+
no_match = nil
|
162
|
+
|
163
|
+
position = snapshot.each_with_index.reduce(no_match) do |no_match, (text, i)|
|
164
|
+
# $z.print " - matching #{text.inspect} =~ /^#{matching_text.to_s}/ && #{current} != #{text} : "
|
165
|
+
if text =~ /^#{Regexp.escape(matching_text)}/ && current != text
|
166
|
+
# $z.puts " match #{i}, returning position #{snapshot.length - (i + 1)}"
|
167
|
+
|
168
|
+
# convert to non-reversed indexing
|
169
|
+
position = snapshot.length - (i + 1)
|
170
|
+
break position
|
171
|
+
else
|
172
|
+
# $z.puts " no match."
|
173
|
+
no_match
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def search_forward(matching_text:)
|
179
|
+
if !matching_text
|
180
|
+
matching_text = ""
|
181
|
+
elsif matching_text.respond_to?(:without_ansi)
|
182
|
+
matching_text = matching_text.without_ansi
|
183
|
+
end
|
184
|
+
|
185
|
+
command_history = self
|
186
|
+
return nil unless position
|
187
|
+
|
188
|
+
start_index = position + 1
|
189
|
+
snapshot = command_history[start_index..-1].dup
|
190
|
+
no_match = nil
|
191
|
+
current = get
|
192
|
+
|
193
|
+
position = snapshot.each_with_index.reduce(no_match) do |no_match, (text, i)|
|
194
|
+
if text =~ /^#{Regexp.escape(matching_text.to_s)}/ && current != text
|
195
|
+
position = start_index + i
|
196
|
+
break position
|
197
|
+
else
|
198
|
+
no_match
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Yap::Shell::Execution::Context.on(:before_statements_execute) do |world|
|
2
|
+
end
|
3
|
+
|
4
|
+
Yap::Shell::Execution::Context.on(:after_statements_execute) do |world|
|
5
|
+
end
|
6
|
+
|
7
|
+
Yap::Shell::Execution::Context.on(:before_execute) do |world, command:|
|
8
|
+
# world[:history].executing command:command.str, started_at:Time.now
|
9
|
+
end
|
10
|
+
|
11
|
+
Yap::Shell::Execution::Context.on(:after_execute) do |world, command:, result:|
|
12
|
+
# world[:history].executed command:command.str, stopped_at:Time.now
|
13
|
+
end
|
@@ -0,0 +1,295 @@
|
|
1
|
+
class KeyboardMacros < Addon
|
2
|
+
DEFAULT_TRIGGER_KEY = :ctrl_g
|
3
|
+
DEFAULT_CANCEL_KEY = " "
|
4
|
+
DEFAULT_TIMEOUT_IN_MS = 500
|
5
|
+
|
6
|
+
def self.load_addon
|
7
|
+
@instance ||= new
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :world
|
11
|
+
attr_accessor :timeout_in_ms
|
12
|
+
attr_accessor :cancel_key, :trigger_key
|
13
|
+
attr_accessor :cancel_on_unknown_sequences
|
14
|
+
|
15
|
+
def initialize_world(world)
|
16
|
+
@world = world
|
17
|
+
@configurations = []
|
18
|
+
@stack = []
|
19
|
+
@timeout_in_ms = DEFAULT_TIMEOUT_IN_MS
|
20
|
+
@cancel_key = DEFAULT_CANCEL_KEY
|
21
|
+
@trigger_key = DEFAULT_TRIGGER_KEY
|
22
|
+
@cancel_on_unknown_sequences = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def configure(cancel_key: nil, trigger_key: nil, &blk)
|
26
|
+
cancel_key ||= @cancel_key
|
27
|
+
trigger_key ||= @trigger_key
|
28
|
+
|
29
|
+
cancel_blk = lambda do
|
30
|
+
world.editor.event_loop.clear @event_id
|
31
|
+
cancel_processing
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
configuration = Configuration.new(
|
36
|
+
keymap: world.editor.terminal.keys,
|
37
|
+
trigger_key: trigger_key,
|
38
|
+
cancellation: Cancellation.new(cancel_key: cancel_key, &cancel_blk)
|
39
|
+
)
|
40
|
+
|
41
|
+
blk.call configuration if blk
|
42
|
+
|
43
|
+
world.unbind(trigger_key)
|
44
|
+
world.bind(trigger_key) do
|
45
|
+
begin
|
46
|
+
@stack << configuration
|
47
|
+
configuration.start.call if configuration.start
|
48
|
+
world.editor.keyboard_input_processors.push(self)
|
49
|
+
world.editor.input.wait_timeout_in_seconds = 0.1
|
50
|
+
ensure
|
51
|
+
queue_up_remove_input_processor(&configuration.stop)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
@configurations << configuration
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# InputProcessor Methods
|
60
|
+
#
|
61
|
+
|
62
|
+
def read_bytes(bytes)
|
63
|
+
configuration = @stack.last
|
64
|
+
bytes.each do |byte|
|
65
|
+
definition = configuration[byte]
|
66
|
+
if !definition
|
67
|
+
cancel_processing if cancel_on_unknown_sequences
|
68
|
+
break
|
69
|
+
end
|
70
|
+
configuration = definition.configuration
|
71
|
+
if configuration
|
72
|
+
configuration.start.call if configuration.start
|
73
|
+
@stack << configuration
|
74
|
+
end
|
75
|
+
|
76
|
+
result = definition.process
|
77
|
+
|
78
|
+
if result =~ /\n$/
|
79
|
+
world.editor.write result.chomp
|
80
|
+
world.editor.event_loop.clear @event_id if @event_id
|
81
|
+
cancel_processing
|
82
|
+
world.editor.newline # add_to_history
|
83
|
+
world.editor.process_line
|
84
|
+
break
|
85
|
+
end
|
86
|
+
|
87
|
+
@stack.pop if definition.fragment?
|
88
|
+
|
89
|
+
if @event_id
|
90
|
+
world.editor.event_loop.clear @event_id
|
91
|
+
@event_id = queue_up_remove_input_processor
|
92
|
+
end
|
93
|
+
world.editor.write result if result.is_a?(String)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def queue_up_remove_input_processor(&blk)
|
100
|
+
return unless @timeout_in_ms
|
101
|
+
|
102
|
+
event_args = {
|
103
|
+
name: 'remove_input_processor',
|
104
|
+
source: self,
|
105
|
+
interval_in_ms: @timeout_in_ms,
|
106
|
+
}
|
107
|
+
@event_id = world.editor.event_loop.once(event_args) do
|
108
|
+
cancel_processing
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def cancel_processing
|
113
|
+
@event_id = nil
|
114
|
+
@stack.reverse.each do |configuration|
|
115
|
+
configuration.stop.call if configuration.stop
|
116
|
+
end
|
117
|
+
@stack.clear
|
118
|
+
if world.editor.keyboard_input_processors.last == self
|
119
|
+
world.editor.keyboard_input_processors.pop
|
120
|
+
world.editor.input.restore_default_timeout
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class Cancellation
|
125
|
+
attr_reader :cancel_key
|
126
|
+
|
127
|
+
def initialize(cancel_key: , &blk)
|
128
|
+
@cancel_key = cancel_key
|
129
|
+
@blk = blk
|
130
|
+
end
|
131
|
+
|
132
|
+
def call
|
133
|
+
@blk.call
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class Configuration
|
138
|
+
attr_reader :cancellation, :trigger_key, :keymap
|
139
|
+
|
140
|
+
def initialize(cancellation: nil, keymap: {}, trigger_key: nil)
|
141
|
+
@cancellation = cancellation
|
142
|
+
@keymap = keymap
|
143
|
+
@trigger_key = trigger_key
|
144
|
+
@storage = {}
|
145
|
+
@on_start_blk = nil
|
146
|
+
@on_stop_blk = nil
|
147
|
+
|
148
|
+
if @cancellation
|
149
|
+
define @cancellation.cancel_key, -> { @cancellation.call }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def start(&blk)
|
154
|
+
@on_start_blk = blk if blk
|
155
|
+
@on_start_blk
|
156
|
+
end
|
157
|
+
|
158
|
+
def stop(&blk)
|
159
|
+
@on_stop_blk = blk if blk
|
160
|
+
@on_stop_blk
|
161
|
+
end
|
162
|
+
|
163
|
+
def fragment(sequence, result)
|
164
|
+
define(sequence, result).tap do |definition|
|
165
|
+
definition.fragment!
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def define(sequence, result, &blk)
|
170
|
+
unless result.respond_to?(:call)
|
171
|
+
string_result = result
|
172
|
+
result = -> { string_result }
|
173
|
+
end
|
174
|
+
|
175
|
+
case sequence
|
176
|
+
when String
|
177
|
+
recursively_define_sequence_for_bytes(
|
178
|
+
self,
|
179
|
+
sequence.bytes,
|
180
|
+
result,
|
181
|
+
&blk
|
182
|
+
)
|
183
|
+
when Symbol
|
184
|
+
recursively_define_sequence_for_bytes(
|
185
|
+
self,
|
186
|
+
@keymap.fetch(sequence){
|
187
|
+
fail "Cannot bind unknown sequence #{sequence.inspect}"
|
188
|
+
},
|
189
|
+
result,
|
190
|
+
&blk
|
191
|
+
)
|
192
|
+
when Regexp
|
193
|
+
define_sequence_for_regex(sequence, result, &blk)
|
194
|
+
else
|
195
|
+
raise NotImplementedError, <<-EOT.gsub(/^\s*/, '')
|
196
|
+
Don't know how to define macro for sequence: #{sequence.inspect}
|
197
|
+
EOT
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def [](byte)
|
202
|
+
@storage.values.detect { |definition| definition.matches?(byte) }
|
203
|
+
end
|
204
|
+
|
205
|
+
def []=(key, definition)
|
206
|
+
@storage[key] = definition
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
def define_sequence_for_regex(regex, result, &blk)
|
212
|
+
@storage[regex] = Definition.new(
|
213
|
+
configuration: Configuration.new(
|
214
|
+
cancellation: @cancellation,
|
215
|
+
keymap: @keymap
|
216
|
+
),
|
217
|
+
sequence: regex,
|
218
|
+
result: result,
|
219
|
+
&blk
|
220
|
+
)
|
221
|
+
end
|
222
|
+
|
223
|
+
def recursively_define_sequence_for_bytes(configuration, bytes, result, &blk)
|
224
|
+
byte, rest = bytes[0], bytes[1..-1]
|
225
|
+
if rest.any?
|
226
|
+
definition = Definition.new(
|
227
|
+
configuration: Configuration.new(
|
228
|
+
cancellation: @cancellation,
|
229
|
+
keymap: @keymap
|
230
|
+
),
|
231
|
+
sequence: byte,
|
232
|
+
result: nil,
|
233
|
+
&blk
|
234
|
+
)
|
235
|
+
configuration[byte] = definition
|
236
|
+
recursively_define_sequence_for_bytes(
|
237
|
+
definition.configuration,
|
238
|
+
rest,
|
239
|
+
result,
|
240
|
+
&blk
|
241
|
+
)
|
242
|
+
else
|
243
|
+
configuration[byte] = Definition.new(
|
244
|
+
configuration: Configuration.new(keymap: @keymap),
|
245
|
+
sequence: byte,
|
246
|
+
result: result,
|
247
|
+
&blk
|
248
|
+
)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
class Definition
|
254
|
+
attr_reader :bytes, :configuration, :result, :sequence
|
255
|
+
|
256
|
+
def initialize(configuration: nil, sequence:, result: nil, &blk)
|
257
|
+
@fragment = false
|
258
|
+
@configuration = configuration
|
259
|
+
@sequence = sequence
|
260
|
+
@result = result
|
261
|
+
blk.call(@configuration) if blk
|
262
|
+
end
|
263
|
+
|
264
|
+
def fragment?
|
265
|
+
@fragment
|
266
|
+
end
|
267
|
+
|
268
|
+
def fragment!
|
269
|
+
@fragment = true
|
270
|
+
end
|
271
|
+
|
272
|
+
def matches?(byte)
|
273
|
+
if @sequence.is_a?(Regexp)
|
274
|
+
@match_data = @sequence.match(byte.chr)
|
275
|
+
else
|
276
|
+
@sequence == byte
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def process
|
281
|
+
if @result
|
282
|
+
if @match_data
|
283
|
+
if @match_data.captures.empty?
|
284
|
+
@result.call(@match_data[0])
|
285
|
+
else
|
286
|
+
@result.call(*@match_data.captures)
|
287
|
+
end
|
288
|
+
else
|
289
|
+
@result.call
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
end
|