tty-reader 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,120 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module TTY
5
+ class Reader
6
+ module Codes
7
+ def ctrl_keys
8
+ {
9
+ ctrl_a: ?\C-a,
10
+ ctrl_b: ?\C-b,
11
+ ctrl_c: ?\C-c,
12
+ ctrl_d: ?\C-d,
13
+ ctrl_e: ?\C-e,
14
+ ctrl_f: ?\C-f,
15
+ ctrl_g: ?\C-g,
16
+ ctrl_h: ?\C-h,
17
+ ctrl_i: ?\C-i,
18
+ ctrl_j: ?\C-j,
19
+ ctrl_k: ?\C-k,
20
+ ctrl_l: ?\C-l,
21
+ ctrl_m: ?\C-m,
22
+ ctrl_n: ?\C-n,
23
+ ctrl_o: ?\C-o,
24
+ ctrl_p: ?\C-p,
25
+ ctrl_q: ?\C-q,
26
+ ctrl_r: ?\C-r,
27
+ ctrl_s: ?\C-s,
28
+ ctrl_t: ?\C-t,
29
+ ctrl_u: ?\C-u,
30
+ ctrl_v: ?\C-v,
31
+ ctrl_w: ?\C-w,
32
+ ctrl_x: ?\C-x,
33
+ ctrl_y: ?\C-y,
34
+ ctrl_z: ?\C-z
35
+ }
36
+ end
37
+ module_function :ctrl_keys
38
+
39
+ def keys
40
+ {
41
+ tab: "\t",
42
+ enter: "\n",
43
+ return: "\r",
44
+ escape: "\e",
45
+ space: " ",
46
+ backspace: ?\C-?,
47
+ home: "\e[1~",
48
+ insert: "\e[2~",
49
+ delete: "\e[3~",
50
+ end: "\e[4~",
51
+ page_up: "\e[5~",
52
+ page_down: "\e[6~",
53
+
54
+ up: "\e[A",
55
+ down: "\e[B",
56
+ right: "\e[C",
57
+ left: "\e[D",
58
+ clear: "\e[E",
59
+
60
+ f1_xterm: "\eOP",
61
+ f2_xterm: "\eOQ",
62
+ f3_xterm: "\eOR",
63
+ f4_xterm: "\eOS",
64
+
65
+ f1: "\e[11~",
66
+ f2: "\e[12~",
67
+ f3: "\e[13~",
68
+ f4: "\e[14~",
69
+ f5: "\e[15~",
70
+ f6: "\e[17~",
71
+ f7: "\e[18~",
72
+ f8: "\e[19~",
73
+ f9: "\e[20~",
74
+ f10: "\e[21~",
75
+ f11: "\e[23~",
76
+ f12: "\e[24~"
77
+ }.merge(ctrl_keys)
78
+ end
79
+ module_function :keys
80
+
81
+ def win_keys
82
+ {
83
+ tab: "\t",
84
+ enter: "\r",
85
+ return: "\r",
86
+ escape: "\e",
87
+ space: " ",
88
+ backspace: "\b",
89
+ home: [224, 71].pack('U*'),
90
+ end: [224, 79].pack('U*'),
91
+ insert: [224, 82].pack('U*'),
92
+ delete: [224, 83].pack('U*'),
93
+ page_up: [224, 73].pack('U*'),
94
+ page_down: [224, 81].pack('U*'),
95
+
96
+ up: [224, 72].pack('U*'),
97
+ down: [224, 80].pack('U*'),
98
+ right: [224, 77].pack('U*'),
99
+ left: [224, 75].pack('U*'),
100
+ clear: [224, 83].pack('U*'),
101
+
102
+ f1: "\x00;",
103
+ f2: "\x00<",
104
+ f3: "\x00",
105
+ f4: "\x00=",
106
+ f5: "\x00?",
107
+ f6: "\x00@",
108
+ f7: "\x00A",
109
+ f8: "\x00B",
110
+ f9: "\x00C",
111
+ f10: "\x00D",
112
+ f11: "\x00\x85",
113
+ f12: "\x00\x86"
114
+ }.merge(ctrl_keys)
115
+ end
116
+ module_function :win_keys
117
+
118
+ end # Codes
119
+ end # Reader
120
+ end # TTY
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'codes'
5
+ require_relative 'mode'
6
+
7
+ module TTY
8
+ class Reader
9
+ class Console
10
+ ESC = "\e".freeze
11
+ CSI = "\e[".freeze
12
+
13
+ # Key codes
14
+ #
15
+ # @return [Hash[Symbol]]
16
+ #
17
+ # @api public
18
+ attr_reader :keys
19
+
20
+ # Escape codes
21
+ #
22
+ # @return [Array[Integer]]
23
+ #
24
+ # @api public
25
+ attr_reader :escape_codes
26
+
27
+ def initialize(input)
28
+ @input = input
29
+ @mode = Mode.new(input)
30
+ @keys = Codes.keys
31
+ @escape_codes = [[ESC.ord], CSI.bytes.to_a]
32
+ end
33
+
34
+ # Get a character from console with echo
35
+ #
36
+ # @param [Hash[Symbol]] options
37
+ # @option options [Symbol] :echo
38
+ # the echo toggle
39
+ #
40
+ # @return [String]
41
+ #
42
+ # @api private
43
+ def get_char(options)
44
+ mode.raw(options[:raw]) do
45
+ mode.echo(options[:echo]) { input.getc }
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ attr_reader :mode
52
+
53
+ attr_reader :input
54
+ end # Console
55
+ end # Reader
56
+ end # TTY
@@ -0,0 +1,144 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'forwardable'
5
+
6
+ module TTY
7
+ class Reader
8
+ # A class responsible for storing a history of all lines entered by
9
+ # user when interacting with shell prompt.
10
+ #
11
+ # @api private
12
+ class History
13
+ include Enumerable
14
+ extend Forwardable
15
+
16
+ # Default maximum size
17
+ DEFAULT_SIZE = 32 << 4
18
+
19
+ def_delegators :@history, :size, :length, :to_s, :inspect
20
+
21
+ # Set and retrieve the maximum size of the buffer
22
+ attr_accessor :max_size
23
+
24
+ attr_reader :index
25
+
26
+ attr_accessor :cycle
27
+
28
+ attr_accessor :duplicates
29
+
30
+ attr_accessor :exclude
31
+
32
+ # Create a History buffer
33
+ #
34
+ # param [Integer] max_size
35
+ # the maximum size for history buffer
36
+ #
37
+ # param [Hash[Symbol]] options
38
+ # @option options [Boolean] :duplicates
39
+ # whether or not to store duplicates, true by default
40
+ #
41
+ # @api public
42
+ def initialize(max_size = DEFAULT_SIZE, options = {})
43
+ @max_size = max_size
44
+ @index = 0
45
+ @history = []
46
+ @duplicates = options.fetch(:duplicates) { true }
47
+ @exclude = options.fetch(:exclude) { proc {} }
48
+ @cycle = options.fetch(:cycle) { false }
49
+ yield self if block_given?
50
+ end
51
+
52
+ # Iterates over history lines
53
+ #
54
+ # @api public
55
+ def each
56
+ if block_given?
57
+ @history.each { |line| yield line }
58
+ else
59
+ @history.to_enum
60
+ end
61
+ end
62
+
63
+ # Add the last typed line to history buffer
64
+ #
65
+ # @param [String] line
66
+ #
67
+ # @api public
68
+ def push(line)
69
+ @history.delete(line) unless @duplicates
70
+ return if line.to_s.empty? || @exclude[line]
71
+
72
+ @history.shift if size >= max_size
73
+ @history << line
74
+ @index = @history.size - 1
75
+
76
+ self
77
+ end
78
+ alias << push
79
+
80
+ # Move the pointer to the next line in the history
81
+ #
82
+ # @api public
83
+ def next
84
+ return if size.zero?
85
+ if @index == size - 1
86
+ @index = 0 if @cycle
87
+ else
88
+ @index += 1
89
+ end
90
+ end
91
+
92
+ def next?
93
+ size > 0 && !(@index == size - 1 && !@cycle)
94
+ end
95
+
96
+ # Move the pointer to the previous line in the history
97
+ def previous
98
+ return if size.zero?
99
+ if @index.zero?
100
+ @index = size - 1 if @cycle
101
+ else
102
+ @index -= 1
103
+ end
104
+ end
105
+
106
+ def previous?
107
+ size > 0 && !(@index < 0 && !@cycle)
108
+ end
109
+
110
+ # Return line at the specified index
111
+ #
112
+ # @raise [IndexError] index out of range
113
+ #
114
+ # @api public
115
+ def [](index)
116
+ if index < 0
117
+ index += @history.size if index < 0
118
+ end
119
+ line = @history[index]
120
+ if line.nil?
121
+ raise IndexError, 'invalid index'
122
+ end
123
+ line.dup
124
+ end
125
+
126
+ # Get current line
127
+ #
128
+ # @api public
129
+ def get
130
+ return if size.zero?
131
+
132
+ self[@index]
133
+ end
134
+
135
+ # Empty all history lines
136
+ #
137
+ # @api public
138
+ def clear
139
+ @history.clear
140
+ @index = 0
141
+ end
142
+ end # History
143
+ end # Reader
144
+ end # TTY
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module TTY
5
+ class Reader
6
+ # Responsible for meta-data information about key pressed
7
+ #
8
+ # @api private
9
+ class Key < Struct.new(:name, :ctrl, :meta, :shift)
10
+ def initialize(*)
11
+ super(nil, false, false, false)
12
+ end
13
+ end
14
+
15
+ # Represents key event emitted during keyboard press
16
+ #
17
+ # @api public
18
+ class KeyEvent < Struct.new(:value, :key)
19
+ # Create key event from read input codes
20
+ #
21
+ # @param [Hash[Symbol]] keys
22
+ # the keys and codes mapping
23
+ # @param [Array[Integer]] codes
24
+ #
25
+ # @return [KeyEvent]
26
+ #
27
+ # @api public
28
+ def self.from(keys, char)
29
+ key = Key.new
30
+ ctrls = keys.keys.grep(/ctrl/)
31
+
32
+ case char
33
+ when keys[:return] then key.name = :return
34
+ when keys[:enter] then key.name = :enter
35
+ when keys[:tab] then key.name = :tab
36
+ when keys[:backspace] then key.name = :backspace
37
+ when keys[:delete] then key.name = :delete
38
+ when keys[:space] then key.name = :space
39
+ when keys[:escape] then key.name = :escape
40
+ when proc { |c| c =~ /^[a-z]{1}$/ }
41
+ key.name = :alpha
42
+ when proc { |c| c =~ /^[A-Z]{1}$/ }
43
+ key.name = :alpha
44
+ key.shift = true
45
+ when proc { |c| c =~ /^\d+$/ }
46
+ key.name = :num
47
+ # arrows
48
+ when keys[:up] then key.name = :up
49
+ when keys[:down] then key.name = :down
50
+ when keys[:left] then key.name = :left
51
+ when keys[:right] then key.name = :right
52
+ # editing
53
+ when keys[:clear] then key.name = :clear
54
+ when keys[:end] then key.name = :end
55
+ when keys[:home] then key.name = :home
56
+ when keys[:insert] then key.name = :insert
57
+ when keys[:page_up] then key.name = :page_up
58
+ when keys[:page_down] then key.name = :page_down
59
+ when proc { |cs| ctrls.any? { |name| keys[name] == cs } }
60
+ key.name = keys.key(char)
61
+ key.ctrl = true
62
+ # f1 - f12
63
+ when keys[:f1], keys[:f1_xterm] then key.name = :f1
64
+ when keys[:f2], keys[:f2_xterm] then key.name = :f2
65
+ when keys[:f3], keys[:f3_xterm] then key.name = :f3
66
+ when keys[:f4], keys[:f4_xterm] then key.name = :f4
67
+ when keys[:f5] then key.name = :f5
68
+ when keys[:f6] then key.name = :f6
69
+ when keys[:f7] then key.name = :f7
70
+ when keys[:f8] then key.name = :f8
71
+ when keys[:f9] then key.name = :f9
72
+ when keys[:f10] then key.name = :f10
73
+ when keys[:f11] then key.name = :f11
74
+ when keys[:f12] then key.name = :f12
75
+ end
76
+
77
+ new(char, key)
78
+ end
79
+
80
+ # Check if key event can be triggered
81
+ #
82
+ # @return [Boolean]
83
+ #
84
+ # @api public
85
+ def trigger?
86
+ !key.nil? && !key.name.nil?
87
+ end
88
+ end # KeyEvent
89
+ end # Reader
90
+ end # TTY
@@ -0,0 +1,161 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'forwardable'
5
+
6
+ module TTY
7
+ class Reader
8
+ class Line
9
+ extend Forwardable
10
+
11
+ def_delegators :@text, :size, :length, :to_s, :inspect,
12
+ :slice!, :empty?
13
+
14
+ attr_accessor :text
15
+
16
+ attr_accessor :cursor
17
+
18
+ def initialize(text = "")
19
+ @text = text
20
+ @cursor = [0, @text.length].max
21
+ yield self if block_given?
22
+ end
23
+
24
+ # Check if cursor reached beginning of the line
25
+ #
26
+ # @return [Boolean]
27
+ #
28
+ # @api public
29
+ def start?
30
+ @cursor == 0
31
+ end
32
+
33
+ # Check if cursor reached end of the line
34
+ #
35
+ # @return [Boolean]
36
+ #
37
+ # @api public
38
+ def end?
39
+ @cursor == @text.length
40
+ end
41
+
42
+ # Move line position to the left by n chars
43
+ #
44
+ # @api public
45
+ def left(n = 1)
46
+ @cursor = [0, @cursor - n].max
47
+ end
48
+
49
+ # Move line position to the right by n chars
50
+ #
51
+ # @api public
52
+ def right(n = 1)
53
+ @cursor = [@text.length, @cursor + n].min
54
+ end
55
+
56
+ # Move cursor to beginning position
57
+ #
58
+ # @api public
59
+ def move_to_start
60
+ @cursor = 0
61
+ end
62
+
63
+ # Move cursor to end position
64
+ #
65
+ # @api public
66
+ def move_to_end
67
+ @cursor = @text.length # put cursor outside of text
68
+ end
69
+
70
+ # Insert characters inside a line. When the lines exceeds
71
+ # maximum length, an extra space is added to accomodate index.
72
+ #
73
+ # @param [Integer] i
74
+ # the index to insert at
75
+ #
76
+ # @example
77
+ # text = 'aaa'
78
+ # line[5]= 'b'
79
+ # => 'aaa b'
80
+ #
81
+ # @api public
82
+ def []=(i, chars)
83
+ if i.is_a?(Range)
84
+ @text[i] = chars
85
+ @cursor += chars.length
86
+ return
87
+ end
88
+
89
+ if i <= 0
90
+ before_text = ''
91
+ after_text = @text.dup
92
+ elsif i == @text.length - 1
93
+ before_text = @text.dup
94
+ after_text = ''
95
+ elsif i > @text.length - 1
96
+ before_text = @text.dup
97
+ after_text = ?\s * (i - @text.length)
98
+ @cursor += after_text.length
99
+ else
100
+ before_text = @text[0..i-1].dup
101
+ after_text = @text[i..-1].dup
102
+ end
103
+
104
+ if i > @text.length - 1
105
+ @text = before_text + after_text + chars
106
+ else
107
+ @text = before_text + chars + after_text
108
+ end
109
+
110
+ @cursor = i + chars.length
111
+ end
112
+
113
+ # Read character
114
+ #
115
+ # @api public
116
+ def [](i)
117
+ @text[i]
118
+ end
119
+
120
+ # Replace current line with new text
121
+ #
122
+ # @param [String] text
123
+ #
124
+ # @api public
125
+ def replace(text)
126
+ @text = text
127
+ @cursor = @text.length # put cursor outside of text
128
+ end
129
+
130
+ # Insert char(s) at cursor position
131
+ #
132
+ # @api public
133
+ def insert(chars)
134
+ self[@cursor] = chars
135
+ end
136
+
137
+ # Add char and move cursor
138
+ #
139
+ # @api public
140
+ def <<(char)
141
+ @text << char
142
+ @cursor += 1
143
+ end
144
+
145
+ # Remove char from the line at current position
146
+ #
147
+ # @api public
148
+ def delete
149
+ @text.slice!(@cursor, 1)
150
+ end
151
+
152
+ # Remove char from the line in front of the cursor
153
+ #
154
+ # @api public
155
+ def remove
156
+ left
157
+ @text.slice!(@cursor, 1)
158
+ end
159
+ end # Line
160
+ end # Reader
161
+ end # TTY