yap-rawline 0.1.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.
@@ -0,0 +1,78 @@
1
+ module RawLine
2
+ class EventLoop
3
+ attr_reader :events
4
+
5
+ def initialize(registry:)
6
+ @registry = registry
7
+ @events = []
8
+ end
9
+
10
+ # event looks like:
11
+ # * name
12
+ # * source
13
+ # * target
14
+ # * payload
15
+ def add_event(**event)
16
+ # if the last event is the same as the incoming then do there is no
17
+ # need to add it again. For example, rendering events that already
18
+ # back can be squashed into a single event.
19
+ if @events.last != event
20
+ @events << event
21
+ end
22
+ end
23
+
24
+ def recur(event:nil, interval_in_ms:, &blk)
25
+ if block_given?
26
+ # TODO: implement
27
+ elsif event
28
+ add_event event.merge(recur: { interval_in_ms: interval_in_ms, recur_at: recur_at(interval_in_ms) })
29
+ else
30
+ raise "Must pass in a block or an event."
31
+ end
32
+ end
33
+
34
+ def start
35
+ loop do
36
+ event = @events.shift
37
+ if event
38
+ recur = event[:recur]
39
+ if recur
40
+ if current_time_in_ms >= recur[:recur_at]
41
+ dispatch_event(event)
42
+ interval_in_ms = recur[:interval_in_ms]
43
+ add_event event.merge(recur: { interval_in_ms: interval_in_ms, recur_at: recur_at(interval_in_ms) } )
44
+ else
45
+ # put it back on the queue
46
+ add_event event
47
+ dispatch_event(default_event)
48
+ end
49
+ else
50
+ dispatch_event(event)
51
+ end
52
+ else
53
+ dispatch_event(default_event)
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def current_time_in_ms
61
+ (Time.now.to_f * 1_000).to_i
62
+ end
63
+
64
+ def default_event
65
+ { name: 'default', source: self }
66
+ end
67
+
68
+ def recur_at(interval_in_ms)
69
+ current_time_in_ms + interval_in_ms
70
+ end
71
+
72
+ def dispatch_event(event)
73
+ @registry.subscribers_for_event(event[:name]).each do |subscriber|
74
+ subscriber.call(event)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,17 @@
1
+ module RawLine
2
+ class EventRegistry
3
+ def initialize(&blk)
4
+ @subscribers = Hash.new{ |h,k| h[k.to_sym] = [] }
5
+ blk.call(self) if block_given?
6
+ end
7
+
8
+ def subscribe(event_name, *subscribers, &blk)
9
+ subscribers << blk if block_given?
10
+ @subscribers[event_name.to_sym].concat(subscribers)
11
+ end
12
+
13
+ def subscribers_for_event(event_name)
14
+ @subscribers[event_name.to_sym]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,177 @@
1
+ #!usr/bin/env ruby
2
+
3
+ #
4
+ # history_buffer.rb
5
+ #
6
+ # Created by Fabio Cevasco on 2008-03-01.
7
+ # Copyright (c) 2008 Fabio Cevasco. All rights reserved.
8
+ #
9
+ # This is Free Software. See LICENSE for details.
10
+ #
11
+ #
12
+ #
13
+ module RawLine
14
+
15
+ #
16
+ # The HistoryBuffer class is used to hold the editor and line histories, as well
17
+ # as word completion matches.
18
+ #
19
+ class HistoryBuffer < Array
20
+
21
+ attr_reader :position, :size
22
+ attr_accessor :duplicates, :exclude, :cycle
23
+
24
+ #
25
+ # Create an instance of RawLine::HistoryBuffer.
26
+ # This method takes an optional block used to override the
27
+ # following instance attributes:
28
+ # * <tt>@duplicates</tt> - whether or not duplicate items will be stored in the buffer.
29
+ # * <tt>@exclude</tt> - a Proc object defining exclusion rules to prevent items from being added to the buffer.
30
+ # * <tt>@cycle</tt> - Whether or not the buffer is cyclic.
31
+ #
32
+ def initialize(size)
33
+ @duplicates = true
34
+ @exclude = lambda{|a|}
35
+ @cycle = false
36
+ yield self if block_given?
37
+ @size = size
38
+ @position = nil
39
+ end
40
+
41
+ #
42
+ # Clears the current position on the history object. Useful when deciding
43
+ # to cancel/reset history navigation.
44
+ #
45
+ def clear_position
46
+ @position = nil
47
+ end
48
+
49
+ def searching?
50
+ !!@position
51
+ end
52
+
53
+ def supports_partial_text_matching?
54
+ false
55
+ end
56
+
57
+ #
58
+ # Resize the buffer, resetting <tt>@position</tt> to nil.
59
+ #
60
+ def resize(new_size)
61
+ if new_size < @size
62
+ @size-new_size.times { pop }
63
+ end
64
+ @size = new_size
65
+ @position = nil
66
+ end
67
+
68
+ #
69
+ # Clear the content of the buffer and reset <tt>@position</tt> to nil.
70
+ #
71
+ def empty
72
+ @position = nil
73
+ clear
74
+ end
75
+
76
+ #
77
+ # Retrieve a copy of the element at <tt>@position</tt>.
78
+ #
79
+ def get
80
+ return nil unless length > 0
81
+ return nil unless @position
82
+ at(@position).dup
83
+ end
84
+
85
+ #
86
+ # Return true if <tt>@position</tt> is at the end of the buffer.
87
+ #
88
+ def end?
89
+ @position == length-1
90
+ end
91
+
92
+ #
93
+ # Return true if <tt>@position</tt> is at the start of the buffer.
94
+ #
95
+ def start?
96
+ @position == 0
97
+ end
98
+
99
+ #
100
+ # Decrement <tt>@position</tt>. By default the history will become
101
+ # positioned at the previous item.
102
+ #
103
+ # If <tt>@cycle</tt> is set to true then the history will cycle to the end
104
+ # when it finds itself at the beginning. If false calling this when
105
+ # at the beginning will result in the position not changing.
106
+ #
107
+ # If a search strategy is assigned then the method <tt>search_backward</tt> will be
108
+ # called on the search strategy to determine the position. This method is
109
+ # given any passed in <tt>options</tt> as well as a <tt>:history</tt> option. The
110
+ # <tt>:history</tt> option will be a reference to self.
111
+ #
112
+ def back(options={})
113
+ return nil unless length > 0
114
+
115
+ case @position
116
+ when nil then
117
+ @position = length-1
118
+ when 0 then
119
+ @position = length-1 if @cycle
120
+ else
121
+ @position -= 1
122
+ end
123
+ end
124
+
125
+ #
126
+ # Increment <tt>@position</tt>. By default the history will become
127
+ # positioned at the next item.
128
+ #
129
+ # If <tt>@cycle</tt> is set to true then the history will cycle back to the
130
+ # beginning when it finds itself at the end. If false calling this when
131
+ # at the end will result in the position not changing.
132
+ #
133
+ # If a search strategy is assigned then the method <tt>search_forward</tt> will be
134
+ # called on the search strategy to determine the position. This method is
135
+ # given any passed in <tt>options</tt> as well as a <tt>:history</tt> option. The
136
+ # <tt>:history</tt> option will be a reference to self. If <tt>
137
+ #
138
+ def forward(options={})
139
+ return nil unless length > 0
140
+
141
+ case @position
142
+ when nil then
143
+ nil
144
+ when length-1 then
145
+ @position = 0 if @cycle
146
+ else
147
+ @position += 1
148
+ end
149
+ end
150
+
151
+ #
152
+ # Add a new item to the buffer.
153
+ #
154
+ def push(item)
155
+
156
+ if !@duplicates && self[-1] == item
157
+ # skip adding this line
158
+ return
159
+ end
160
+
161
+ unless @exclude.call(item)
162
+ # Remove the oldest element if size is exceeded
163
+ if @size <= length
164
+ reverse!.pop
165
+ reverse!
166
+ end
167
+ # Add the new item and reset the position
168
+ super(item)
169
+ @position = nil
170
+ end
171
+ end
172
+
173
+ alias << push
174
+
175
+ end
176
+
177
+ end
@@ -0,0 +1,49 @@
1
+ module RawLine
2
+ class KeycodeParser
3
+ def initialize(keymap)
4
+ @keymap = keymap
5
+ @escape_code = keymap[:escape]
6
+ end
7
+
8
+ def parse_bytes(bytes)
9
+ i = 0
10
+ results = []
11
+ loop do
12
+ byte = bytes[i]
13
+
14
+ keycode = find_keycode_for_multi_byte_sequence(bytes[i..-1])
15
+ if keycode
16
+ results << keycode
17
+ i += keycode.length
18
+ else
19
+ results << byte.ord
20
+ i += 1
21
+ end
22
+
23
+ break if i >= bytes.length
24
+ end
25
+ results
26
+ end
27
+
28
+ private
29
+
30
+ # {:left_arrow=>[27, 91, 68]}
31
+ # [27, 91, 68]
32
+ def find_keycode_for_multi_byte_sequence(bytes)
33
+ i = 0
34
+ sequence = []
35
+ loop do
36
+ byte = bytes[i]
37
+ if @keymap.values.any?{ |arr| arr[i] == byte }
38
+ sequence << byte
39
+ i += 1
40
+ else
41
+ break
42
+ end
43
+ break if i >= bytes.length
44
+ end
45
+
46
+ sequence.any? ? sequence : nil
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,169 @@
1
+ #!usr/bin/env ruby
2
+
3
+ #
4
+ # line.rb
5
+ #
6
+ # Created by Fabio Cevasco on 2008-03-01.
7
+ # Copyright (c) 2008 Fabio Cevasco. All rights reserved.
8
+ #
9
+ # This is Free Software. See LICENSE for details.
10
+ #
11
+
12
+ module RawLine
13
+
14
+ #
15
+ # The Line class is used to represent the current line being processed and edited
16
+ # by RawLine::Editor. It keeps track of the characters typed, the cursor position,
17
+ # the current word and maintains an internal history to allow undos and redos.
18
+ #
19
+ class Line
20
+
21
+ attr_accessor :text, :position, :history, :prompt, :history_size, :word_separator
22
+ attr_reader :offset
23
+
24
+ include HighLine::SystemExtensions
25
+
26
+ #
27
+ # Create an instance of RawLine::Line.
28
+ # This method takes an optional block used to override the
29
+ # following instance attributes:
30
+ # * <tt>@text</tt> - the line text.
31
+ # * <tt>@history_size</tt> - the size of the line history buffer.
32
+ # * <tt>@position</tt> - the current cursor position within the line.
33
+ # * <tt>@prompt</tt> - a prompt to prepend to the line text.
34
+ #
35
+ def initialize(history_size)
36
+ @text = ANSIString.new("")
37
+ @history_size = history_size
38
+ @position = 0
39
+ @prompt = ""
40
+ @word_separator = ' '
41
+ yield self if block_given?
42
+ @history = RawLine::HistoryBuffer.new(@history_size)
43
+ @history << "" # Add empty line for complete undo...
44
+ @offset = @prompt.length
45
+ end
46
+
47
+ #
48
+ # Return the maximum line length. By default, it corresponds to the terminal's
49
+ # width minus the length of the line prompt.
50
+ #
51
+ def max_length
52
+ terminal_size[0]-@offset
53
+ end
54
+
55
+ #
56
+ # Return information about the current word, as a Hash composed by the following
57
+ # elements:
58
+ # * <tt>:start</tt>: The position in the line corresponding to the word start
59
+ # * <tt>:end</tt>: The position in the line corresponding to the word end
60
+ # * <tt>:text</tt>: The word text.
61
+ def word
62
+ return {:start => bol, :end => eol+1, :text => @text} if @word_separator.to_s == ''
63
+ last = @text.index(@word_separator, @position)
64
+ first = @text.rindex(@word_separator, @position)
65
+ # Trim word separators and handle EOL and BOL
66
+ if first then
67
+ first +=1
68
+ else
69
+ first = bol
70
+ end
71
+ if last then
72
+ last -=1
73
+ else
74
+ last = eol+1 unless last
75
+ end
76
+ # Swap if overlapping
77
+ last, first = first, last if last < first
78
+ text = @text[first..last].to_s
79
+ # Repeat the search if within word separator
80
+ if text.match @word_separator then
81
+ last = first
82
+ first = @text.rindex(@word_separator, first)
83
+ if first then first+=1
84
+ else first = bol
85
+ end
86
+ text = @text[first..last]
87
+ end
88
+ {:start => first, :end => last, :text => text}
89
+ end
90
+
91
+ #
92
+ # Return an array containing the words present in the current line
93
+ #
94
+ def words
95
+ @text.split @word_separator
96
+ end
97
+
98
+ #
99
+ # Return the position corresponding to the beginning of the line.
100
+ #
101
+ def bol
102
+ 0
103
+ end
104
+
105
+ #
106
+ # Return true if the cursor is at the beginning of the line.
107
+ #
108
+ def bol?
109
+ @position<=bol
110
+ end
111
+
112
+ #
113
+ # Return the position corresponding to the end of the line.
114
+ #
115
+ def eol
116
+ @text.length-1
117
+ end
118
+
119
+ #
120
+ # Return true if the cursor is at the end of the line.
121
+ #
122
+ def eol?
123
+ @position>=eol
124
+ end
125
+
126
+ #
127
+ # Decrement the line position by <tt>offset</tt>
128
+ #
129
+ def left(offset=1)
130
+ @position = (@position-offset <= 0) ? 0 : @position-offset
131
+ end
132
+
133
+ #
134
+ # Increment the line position by <tt>offset</tt>
135
+ #
136
+ def right(offset=1)
137
+ @position += offset
138
+ end
139
+
140
+ #
141
+ # Add a character (expressed as a character code) to the line text.
142
+ #
143
+ def <<(char)
144
+ @text << char.chr
145
+ end
146
+
147
+ #
148
+ # Access the line text at <tt>@index</tt>
149
+ #
150
+ def [](index)
151
+ @text[index]
152
+ end
153
+
154
+ #
155
+ # Modify the character(s) in the line text at <tt>@index</tt>
156
+ #
157
+ def []=(index, chars)
158
+ @text[index] = chars
159
+ end
160
+
161
+ #
162
+ # Return the length of the line text.
163
+ #
164
+ def length
165
+ @text.length
166
+ end
167
+
168
+ end
169
+ end