tty2-reader 0.9.0.1

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,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module TTY2
6
+ class Reader
7
+ class Line
8
+ ANSI_MATCHER = /(\[)?\033(\[)?[;?\d]*[\dA-Za-z](\])?/
9
+
10
+ # The word break characters list used by shell
11
+ DEFAULT_WORD_BREAK_CHARACTERS = " \t\n\"\\'`@$><=|&{("
12
+
13
+ # Strip ANSI characters from the text
14
+ #
15
+ # @param [String] text
16
+ #
17
+ # @return [String]
18
+ #
19
+ # @api public
20
+ def self.sanitize(text)
21
+ text.dup.gsub(ANSI_MATCHER, "")
22
+ end
23
+
24
+ # The editable text
25
+ # @api public
26
+ attr_reader :text
27
+
28
+ # The current cursor position witin the text
29
+ # @api public
30
+ attr_reader :cursor
31
+
32
+ # The line mode
33
+ # @api public
34
+ attr_reader :mode
35
+
36
+ # The prompt displayed before input
37
+ # @api public
38
+ attr_reader :prompt
39
+
40
+ # The prompt size
41
+ # @api public
42
+ attr_reader :prompt_size
43
+
44
+ # The word separator pattern for splitting the text
45
+ #
46
+ # @return [Regexp]
47
+ #
48
+ # @api public
49
+ attr_reader :separator
50
+
51
+ # Create a Line instance
52
+ #
53
+ # @api private
54
+ def initialize(text = "", prompt: "", separator: nil)
55
+ @text = text.dup
56
+ @prompt = prompt.dup
57
+ @prompt_size = prompt_display_width
58
+ break_chars = DEFAULT_WORD_BREAK_CHARACTERS.chars
59
+ @separator = separator || Regexp.union(break_chars)
60
+ @cursor = [0, @text.length].max
61
+ @mode = :edit
62
+
63
+ yield self if block_given?
64
+ end
65
+
66
+ # Prompt display width
67
+ #
68
+ # @return [Integer]
69
+ # the prompt size
70
+ #
71
+ # @api public
72
+ def prompt_display_width
73
+ lines = self.class.sanitize(@prompt).split(/\r?\n/)
74
+ return 0 if lines.empty?
75
+
76
+ # return the length of each line + screen width for every line
77
+ # past the first which accounts for multi-line prompts
78
+ lines.join.length + ((lines.length - 1) * TTY::Screen.width)
79
+ end
80
+
81
+ # Check if line is in edit mode
82
+ #
83
+ # @return [Boolean]
84
+ #
85
+ # @public
86
+ def editing?
87
+ @mode == :edit
88
+ end
89
+
90
+ # Enable edit mode
91
+ #
92
+ # @return [Boolean]
93
+ #
94
+ # @public
95
+ def edit_mode
96
+ @mode = :edit
97
+ end
98
+
99
+ # Check if line is in replace mode
100
+ #
101
+ # @return [Boolean]
102
+ #
103
+ # @public
104
+ def replacing?
105
+ @mode == :replace
106
+ end
107
+
108
+ # Enable replace mode
109
+ #
110
+ # @return [Boolean]
111
+ #
112
+ # @public
113
+ def replace_mode
114
+ @mode = :replace
115
+ end
116
+
117
+ # Check if cursor reached beginning of the line
118
+ #
119
+ # @return [Boolean]
120
+ #
121
+ # @api public
122
+ def start?
123
+ @cursor.zero?
124
+ end
125
+
126
+ # Check if cursor reached end of the line
127
+ #
128
+ # @return [Boolean]
129
+ #
130
+ # @api public
131
+ def end?
132
+ @cursor == @text.length
133
+ end
134
+
135
+ # Move line position to the left by n chars
136
+ #
137
+ # @api public
138
+ def left(n = 1)
139
+ @cursor = [0, @cursor - n].max
140
+ end
141
+
142
+ # Move line position to the right by n chars
143
+ #
144
+ # @api public
145
+ def right(n = 1)
146
+ @cursor = [@text.length, @cursor + n].min
147
+ end
148
+
149
+ # Move cursor to beginning position
150
+ #
151
+ # @api public
152
+ def move_to_start
153
+ @cursor = 0
154
+ end
155
+
156
+ # Move cursor to end position
157
+ #
158
+ # @api public
159
+ def move_to_end
160
+ @cursor = @text.length # put cursor outside of text
161
+ end
162
+
163
+ # Insert characters inside a line. When the lines exceeds
164
+ # maximum length, an extra space is added to accomodate index.
165
+ #
166
+ # @param [Integer] i
167
+ # the index to insert at
168
+ #
169
+ # @param [String] chars
170
+ # the characters to insert
171
+ #
172
+ # @example
173
+ # text = "aaa"
174
+ # line[5]= "b"
175
+ # => "aaa b"
176
+ #
177
+ # @api public
178
+ def []=(i, chars)
179
+ edit_mode
180
+
181
+ if i.is_a?(Range)
182
+ @text[i] = chars
183
+ @cursor += chars.length
184
+ return
185
+ end
186
+
187
+ if i <= 0
188
+ before_text = ""
189
+ after_text = @text.dup
190
+ elsif i > @text.length - 1 # insert outside of line input
191
+ before_text = @text.dup
192
+ after_text = ?\s * (i - @text.length)
193
+ @cursor += after_text.length
194
+ else
195
+ before_text = @text[0..i-1].dup
196
+ after_text = @text[i..-1].dup
197
+ end
198
+
199
+ if i > @text.length - 1
200
+ @text = before_text + after_text + chars
201
+ else
202
+ @text = before_text + chars + after_text
203
+ end
204
+
205
+ @cursor = i + chars.length
206
+ end
207
+
208
+ # Read character
209
+ #
210
+ # @api public
211
+ def [](i)
212
+ @text[i]
213
+ end
214
+
215
+ # Find a word under the cursor based on a separator
216
+ #
217
+ # @param [Boolean] before
218
+ # whether to start searching before or after a break character
219
+ #
220
+ # @return [String]
221
+ #
222
+ # @api public
223
+ def word(before: true)
224
+ start_pos = word_start_pos(before: before)
225
+ @text[start_pos..word_end_pos(from: start_pos)]
226
+ end
227
+
228
+ # Find a subtext under the cursor
229
+ #
230
+ # @param [Boolean] before
231
+ # whether to return the subtext before or after the cursor position
232
+ #
233
+ # @return [String]
234
+ #
235
+ # @api public
236
+ def subtext(before: true)
237
+
238
+ before ? @text[0..@cursor] : @text[@cursor..-1]
239
+
240
+ end
241
+
242
+ # Find a word up to a cursor position
243
+ #
244
+ # @param [Boolean] before
245
+ # whether to start searching before or after a break character
246
+ #
247
+ # @return [String]
248
+ #
249
+ # @api public
250
+ def word_to_complete(before: true)
251
+ @text[word_start_pos(before: before)...@cursor]
252
+ end
253
+
254
+ # Find word start position
255
+ #
256
+ # @param [Integer] from
257
+ # the index to start searching from, defaults to cursor position
258
+ #
259
+ # @param [Symbol] before
260
+ # whether to start search before or after break character
261
+ #
262
+ # @return [Integer]
263
+ #
264
+ # @api public
265
+ def word_start_pos(from: @cursor, before: true)
266
+ # move back or forward by one character when at a word boundary
267
+ if word_boundary?
268
+ from = before ? from - 1 : from + 1
269
+ end
270
+
271
+ start_pos = @text.rindex(separator, from) || 0
272
+ start_pos += 1 unless start_pos.zero?
273
+ start_pos
274
+ end
275
+
276
+ # Find word end position
277
+ #
278
+ # @param [Integer] from
279
+ # the index to start searching from, defaults to cursor position
280
+ #
281
+ # @return [Integer]
282
+ #
283
+ # @api public
284
+ def word_end_pos(from: @cursor)
285
+ end_pos = @text.index(separator, from) || text_size
286
+ end_pos -= 1 unless @text.empty?
287
+ end_pos
288
+ end
289
+
290
+ # Check if cursor is at a word boundary
291
+ #
292
+ # @return [Boolean]
293
+ #
294
+ # @api private
295
+ def word_boundary?
296
+ @text[@cursor] =~ separator
297
+ end
298
+
299
+ # Replace current line with new text
300
+ #
301
+ # @param [String] text
302
+ #
303
+ # @api public
304
+ def replace(text)
305
+ @text = text
306
+ @cursor = @text.length # put cursor outside of text
307
+ replace_mode
308
+ end
309
+
310
+ # Insert char(s) at cursor position
311
+ #
312
+ # @api public
313
+ def insert(chars)
314
+ self[@cursor] = chars
315
+ end
316
+
317
+ # Add char and move cursor
318
+ #
319
+ # @api public
320
+ def <<(char)
321
+ @text << char
322
+ @cursor += 1
323
+ end
324
+
325
+ # Remove char from the line at current position
326
+ #
327
+ # @api public
328
+ def delete(n = 1)
329
+ @text.slice!(@cursor, n)
330
+ end
331
+
332
+ # Remove char from the line in front of the cursor
333
+ #
334
+ # @param [Integer] n
335
+ # the number of chars to remove
336
+ #
337
+ # @api public
338
+ def remove(n = 1)
339
+ left(n)
340
+ @text.slice!(@cursor, n)
341
+ end
342
+
343
+ # Full line with prompt as string
344
+ #
345
+ # @api public
346
+ def to_s
347
+ "#{@prompt}#{@text}"
348
+ end
349
+ alias inspect to_s
350
+
351
+ # Text size
352
+ #
353
+ # @api public
354
+ def text_size
355
+ self.class.sanitize(@text).size
356
+ end
357
+
358
+ # Full line size with prompt
359
+ #
360
+ # @api public
361
+ def size
362
+ prompt_size + text_size
363
+ end
364
+ alias length size
365
+ end # Line
366
+ end # Reader
367
+ end # TTY2
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "io/console"
4
+
5
+ module TTY2
6
+ class Reader
7
+ class Mode
8
+ # Initialize a Terminal
9
+ #
10
+ # @api public
11
+ def initialize(input = $stdin)
12
+ @input = input
13
+ end
14
+
15
+ # Echo given block
16
+ #
17
+ # @param [Boolean] is_on
18
+ #
19
+ # @api public
20
+ def echo(is_on = true, &block)
21
+ if is_on || !@input.tty?
22
+ yield
23
+ else
24
+ @input.noecho(&block)
25
+ end
26
+ end
27
+
28
+ # Use raw mode in the given block
29
+ #
30
+ # @param [Boolean] is_on
31
+ #
32
+ # @api public
33
+ def raw(is_on = true, &block)
34
+ if is_on && @input.tty?
35
+ @input.raw(&block)
36
+ else
37
+ yield
38
+ end
39
+ end
40
+ end # Mode
41
+ end # Reader
42
+ end # TTY2
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY2
4
+ class Reader
5
+ VERSION = "0.9.0.1"
6
+ end # Reader
7
+ end # TTY2
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fiddle"
4
+
5
+ module TTY2
6
+ class Reader
7
+ module WinAPI
8
+ include Fiddle
9
+
10
+ CRT_HANDLE = Fiddle::Handle.new("msvcrt") rescue Fiddle::Handle.new("crtdll")
11
+
12
+ # Get a character from the console without echo.
13
+ #
14
+ # @return [String]
15
+ # return the character read
16
+ #
17
+ # @api public
18
+ def getch
19
+ @@getch ||= Fiddle::Function.new(CRT_HANDLE["_getch"], [], TYPE_INT)
20
+ @@getch.call
21
+ end
22
+ module_function :getch
23
+
24
+ # Gets a character from the console with echo.
25
+ #
26
+ # @return [String]
27
+ # return the character read
28
+ #
29
+ # @api public
30
+ def getche
31
+ @@getche ||= Fiddle::Function.new(CRT_HANDLE["_getche"], [], TYPE_INT)
32
+ @@getche.call
33
+ end
34
+ module_function :getche
35
+
36
+ # Check the console for recent keystroke. If the function
37
+ # returns a nonzero value, a keystroke is waiting in the buffer.
38
+ #
39
+ # @return [Integer]
40
+ # return a nonzero value if a key has been pressed. Otherwirse,
41
+ # it returns 0.
42
+ #
43
+ # @api public
44
+ def kbhit
45
+ @@kbhit ||= Fiddle::Function.new(CRT_HANDLE["_kbhit"], [], TYPE_INT)
46
+ @@kbhit.call
47
+ end
48
+ module_function :kbhit
49
+ end # WinAPI
50
+ end # Reader
51
+ end # TTY2
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "keys"
4
+
5
+ module TTY2
6
+ class Reader
7
+ class WinConsole
8
+ ESC = "\e"
9
+ NUL_HEX = "\x00"
10
+ EXT_HEX = "\xE0"
11
+
12
+ # Key codes
13
+ #
14
+ # @return [Hash[Symbol]]
15
+ #
16
+ # @api public
17
+ attr_reader :keys
18
+
19
+ # Escape codes
20
+ #
21
+ # @return [Array[Integer]]
22
+ #
23
+ # @api public
24
+ attr_reader :escape_codes
25
+
26
+ def initialize(input)
27
+ require_relative "win_api"
28
+ @input = input
29
+ @keys = Keys.ctrl_keys.merge(Keys.win_keys)
30
+ @escape_codes = [[NUL_HEX.ord], [ESC.ord], EXT_HEX.bytes.to_a]
31
+ end
32
+
33
+ # Get a character from console blocking for input
34
+ #
35
+ # @param [Boolean] echo
36
+ # whether to echo input back or not, defaults to true
37
+ # @param [Boolean] raw
38
+ # whether to use raw mode or not, defaults to false
39
+ # @param [Boolean] nonblock
40
+ # whether to wait for input or not, defaults to false
41
+ #
42
+ # @return [String]
43
+ #
44
+ # @api private
45
+ def get_char(echo: true, raw: false, nonblock: false)
46
+ if raw && echo
47
+ if nonblock
48
+ get_char_echo_non_blocking
49
+ else
50
+ get_char_echo_blocking
51
+ end
52
+ elsif raw && !echo
53
+ nonblock ? get_char_non_blocking : get_char_blocking
54
+ elsif !raw && !echo
55
+ nonblock ? get_char_non_blocking : get_char_blocking
56
+ else
57
+ @input.getc
58
+ end
59
+ end
60
+
61
+ # Get the char for last key pressed, or if no keypress return nil
62
+ #
63
+ # @api private
64
+ def get_char_non_blocking
65
+ input_ready? ? get_char_blocking : nil
66
+ end
67
+
68
+ def get_char_echo_non_blocking
69
+ input_ready? ? get_char_echo_blocking : nil
70
+ end
71
+
72
+ def get_char_blocking
73
+ WinAPI.getch.chr
74
+ end
75
+
76
+ def get_char_echo_blocking
77
+ WinAPI.getche.chr
78
+ end
79
+
80
+ # Check if IO has user input
81
+ #
82
+ # @return [Boolean]
83
+ #
84
+ # @api private
85
+ def input_ready?
86
+ !WinAPI.kbhit.zero?
87
+ end
88
+ end # Console
89
+ end # Reader
90
+ end # TTY2