tty-reader 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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