terminal_rb 0.14.0 → 0.16.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.
@@ -2,9 +2,85 @@
2
2
 
3
3
  module Terminal
4
4
  #
5
- # Key event reported from {read_key_event}.
5
+ # Key event reported from {read_key_event} and {on_key_event}.
6
6
  #
7
7
  class KeyEvent
8
+ # ANSI code sequence received from standard input.
9
+ # This can be a simple value like `"a"`or more complex input like
10
+ # `"\e[24;6~"` (for Shift+Ctrl+F12).
11
+ #
12
+ # @return [String] received ANSI code sequence
13
+ attr_reader :raw
14
+
15
+ # Pressed key without any modifiers.
16
+ # This can be a string for simple keys like `"a"` or a Symbol like `:F12`
17
+ # (see {.key_names}).
18
+ # @return [String, Symbol] key without modifiers
19
+ attr_reader :key
20
+
21
+ # Modifier key code. This represents the encoded key modifier like `Shift`
22
+ # or `Alt`.
23
+ #
24
+ # @return [Integer] modifier key code
25
+ attr_reader :modifier
26
+
27
+ # Mouse event position.
28
+ #
29
+ # @return [[Integer,Integer]] row and column
30
+ # @return nil when no coordinates are assigned
31
+ attr_reader :position
32
+
33
+ # Name of the key event.
34
+ # This can be a simple name like `"a"` or `"Shift+Ctrl+F12"` for combined
35
+ # keys.
36
+ #
37
+ # @return [String] key name
38
+ attr_reader :name
39
+
40
+ # @attribute [r] modifier?
41
+ # @return [true, false] whether a key modifier was pressed
42
+ def modifier? = @modifier != 0
43
+
44
+ # @attribute [r] simple?
45
+ # @return [true, false] whether a simple key was pressed
46
+ def simple? = @raw == @name
47
+
48
+ # All pressed keys.
49
+ # This is composed by all {modifier} and the {key}.
50
+ #
51
+ # @return [Array<Symbol, String>] all pressed keys
52
+ def to_a = @ary.dup
53
+
54
+ # @private
55
+ def to_ary = simple? ? [@raw] : [@raw, @name]
56
+
57
+ # @private
58
+ def to_s = @name.dup
59
+
60
+ # @private
61
+ def inspect = "<#{self.class.name} #{to_ary.map(&:inspect).join(' ')}>"
62
+
63
+ # @private
64
+ def freeze
65
+ @raw.freeze
66
+ @key.freeze
67
+ @position.freeze
68
+ @name.freeze
69
+ super
70
+ end
71
+
72
+ private
73
+
74
+ def initialize(raw, key, modifier, position)
75
+ @raw = raw
76
+ @key = key
77
+ @modifier = modifier
78
+ @position = position
79
+ @ary = MODIFIERS.filter_map { |b, n| n if modifier.allbits?(b) }
80
+ @ary << key if key
81
+ @name = @ary.join('+') # .encode(Encoding::UTF_8)
82
+ end
83
+
8
84
  class << self
9
85
  # @attribute [w] caching
10
86
  # @return [true, false] whether {KeyEvent}s should be cached
@@ -12,24 +88,33 @@ module Terminal
12
88
 
13
89
  # @attribute [w] caching
14
90
  def caching=(value)
15
- if value
16
- @cache ||= {}
17
- else
18
- @cache = nil
19
- end
91
+ value ? @cache ||= {} : @cache = nil
20
92
  end
21
93
 
22
- # Translate a keyboard input string into a related KeyEvent
94
+ # @attribute [r] key_names
95
+ # @return [Array<Symbol>] list of all avilable key names (see {key})
96
+ def key_names
97
+ (
98
+ @names ||=
99
+ (@csiu.values + @csi.values + @ss3.values)
100
+ .uniq
101
+ .keep_if { Symbol === _1 }
102
+ .sort!
103
+ ).dup
104
+ end
105
+
106
+ # Translate a ANSI input code sequence into a related KeyEvent.
23
107
  #
24
108
  # @param raw [String] keyboard input
25
109
  # @return [KeyEvent] related key event
26
110
  def [](raw)
111
+ cached = @cache[raw] and return cached if @cache
27
112
  return new(raw, *@single_key[raw.ord]) if raw.size == 1
28
113
  return unknown(raw) if raw[0] != "\e"
29
114
  return esc1(raw, raw[1]) if raw.size == 2 # ESC ?
30
115
  case raw[1]
31
- when "\e" # ESC ESC ...
32
- return esc_esc(raw)
116
+ when "\e"
117
+ return esc_esc(raw) # ESC ESC ...
33
118
  when 'O'
34
119
  return new(raw, *@ss3[raw[2].ord]) if raw.size == 3 # ESC O ?
35
120
  when '['
@@ -37,15 +122,17 @@ module Terminal
37
122
  if raw.size == 6 && raw[2] == 'M' # ESC [ M b c r
38
123
  return mouse_vt200(raw)
39
124
  end
40
- return csi1(raw) if raw.start_with?("\e[1;") # ESC [ 1 ; ...
125
+ if raw.size > 4 && raw[2] == '1' && raw[3] == ';'
126
+ return csi(raw) # ESC [ 1 ; ...
127
+ end
41
128
  case raw[-1]
42
- when '~' # ESC [ ... ~
43
- return legacy(raw)
44
- when 'u' # ESC [ ... u
45
- return csi_u(raw)
46
- when 'M' # ESC [ ... M
47
- return raw[2] == '<' ? mouse_sgr(raw) : mouse_urxvt(raw)
48
- when 'm' # ESC [ ... m
129
+ when '~'
130
+ return legacy(raw) # ESC [ ... ~
131
+ when 'u'
132
+ return csi_u(raw) # ESC [ ... u
133
+ when 'M', 'm'
134
+ # ESC [ ... M
135
+ # ESC [ ... m
49
136
  return mouse_sgr(raw) if raw[2] == '<'
50
137
  end
51
138
  end
@@ -53,40 +140,29 @@ module Terminal
53
140
  end
54
141
 
55
142
  # @private
56
- def new(raw, key = raw, modifier = 0, extra = nil)
57
- @cache ? (@cache[raw] ||= super.freeze) : super.freeze
143
+ def new(raw, key = raw, modifier = 0, position = nil)
144
+ (@cache && position.nil? ? @cache[raw] ||= super : super).freeze
58
145
  end
59
146
 
60
147
  private
61
148
 
62
149
  def unknown(raw) = new(raw, nil)
63
150
 
64
- def csi_u(raw)
65
- # ESC [ <code> u
66
- # ESC [ <code> ; <modifier> u
67
- return unknown(raw) if raw.size < 4
68
- idx = raw.index(';')
69
- code = raw[2..(idx ? idx - 1 : -2)].to_i
70
- new(
71
- raw,
72
- @csiu[code] || code.chr(Encoding::UTF_8),
73
- idx ? [raw[(idx + 1)..-2].to_i - 1, 0].max : 0
74
- )
151
+ def esc1(raw, char)
152
+ # ESC ?
153
+ key, modifier = @esc1[char.ord]
154
+ new(raw, key || char, modifier || 2)
75
155
  end
76
156
 
77
- def legacy(raw)
78
- # ESC [ <code> ~
79
- # ESC [ <code> ; <modifier> ~
80
- return unknown(raw) if raw.size < 4
81
- idx = raw.index(';')
82
- new(
83
- raw,
84
- @csi[raw[2..(idx ? idx - 1 : -2)].to_i],
85
- idx ? [raw[(idx + 1)..-2].to_i - 1, 0].max : 0
86
- )
157
+ def esc_esc(raw)
158
+ # ESC ESC ?
159
+ # ESC ESC ...
160
+ return esc1(raw, raw[2]) if raw.size == 3
161
+ ret = self[raw[1..]]
162
+ new(raw, ret.key, ret.modifier | 2)
87
163
  end
88
164
 
89
- def csi1(raw)
165
+ def csi(raw)
90
166
  # ESC [ 1 ; [~ABCDEFHPQRS]
91
167
  # ESC [ 1 ; <modifier> [~ABCDEFHPQRS]
92
168
  return unknown(raw) if raw.size < 5
@@ -97,19 +173,33 @@ module Terminal
97
173
  new(raw, key, modifier)
98
174
  end
99
175
 
100
- def esc_esc(raw)
101
- return esc1(raw, raw[2]) if raw.size == 3 # ESC ESC ?
102
- ret = self[raw[1..]]
103
- new(raw, ret.key, ret.modifier | 2) # ESC ESC ...
176
+ def legacy(raw)
177
+ # ESC [ <code> ~
178
+ # ESC [ <code> ; <modifier> ~
179
+ return unknown(raw) if raw.size < 4
180
+ idx = raw.index(';')
181
+ new(
182
+ raw,
183
+ @csi[raw[2..(idx ? idx - 1 : -2)].to_i],
184
+ idx ? [raw[(idx + 1)..-2].to_i - 1, 0].max : 0
185
+ )
104
186
  end
105
187
 
106
- def esc1(raw, char)
107
- key, modifier = @esc1[char.ord]
108
- new(raw, key || char, modifier || 2)
188
+ def csi_u(raw)
189
+ # ESC [ <code> u
190
+ # ESC [ <code> ; <modifier> u
191
+ return unknown(raw) if raw.size < 4
192
+ idx = raw.index(';')
193
+ code = raw[2..(idx ? idx - 1 : -2)].to_i
194
+ new(
195
+ raw,
196
+ @csiu[code] || code.chr(Encoding::UTF_8),
197
+ idx ? [raw[(idx + 1)..-2].to_i - 1, 0].max : 0
198
+ )
109
199
  end
110
200
 
111
201
  def mouse_vt200(raw)
112
- # ESC [ M b c r
202
+ # ESC [ M <btn> <col> <row>
113
203
  mouse_event(raw, *raw[3..].chars.map! { _1.ord - 32 })
114
204
  end
115
205
 
@@ -118,117 +208,40 @@ module Terminal
118
208
  # ESC [ < <code> ; <col> ; <row> m
119
209
  return unknown(raw) if raw.size < 8
120
210
  bcr = raw[3..-2].split(';', 3).map!(&:to_i)
121
- bcr.size == 3 ? mouse_event(raw, *bcr) : unknown(raw)
211
+ bcr.size == 3 ? mouse_event(raw, *bcr, raw[-1] == 'm') : unknown(raw)
122
212
  end
123
213
 
124
- def mouse_urxvt(raw)
125
- # ESC [ <code> ; <col> ; <row> M
126
- return unknown(raw) if raw.size < 8
127
- bcr = raw[2..-2].split(';', 3).map!(&:to_i)
128
- bcr.size == 3 ? mouse_event(raw, *bcr) : unknown(raw)
129
- end
214
+ # def mouse_urxvt(raw)
215
+ # # ESC [ <code> ; <col> ; <row> M
216
+ # unknown(raw) # currently not supported
217
+ # end
130
218
 
131
- def mouse_event(raw, btn, col, row)
219
+ def mouse_event(raw, btn, col, row, up = false)
132
220
  return unknown(raw) if btn < 0 || col < 1 || row < 1
133
- key, btn = kind_of_mouse_event(btn)
221
+ key = (btn & 1).nonzero? ? 1 : 0
222
+ key += 2 if btn.allbits?(2)
134
223
  modifier = btn.allbits?(4) ? 1 : 0
135
224
  modifier += 2 if btn.allbits?(8)
136
225
  modifier += 4 if btn.allbits?(16)
137
- new(raw, key, modifier, [col, row])
226
+ modifier += 256 if moved = btn.allbits?(32)
227
+ key += 4 if btn.allbits?(64)
228
+ key += 8 if btn.allbits?(128)
229
+ new(raw, mouse_name(key, up, moved), modifier, [row, col])
138
230
  end
139
231
 
140
- def mouse_event_key(btn)
141
- kind = btn & 3
142
- key = (btn >= 64 ? @mouse_wheel_kind : @mouse_kind)[kind]
143
- # TODO: what about btn >= 128 for more mouse wheel events?
144
- [btn - kind, key]
232
+ def mouse_name(key, up, moved)
233
+ return up ? :MBLeftUp : :MBLeft if key == 0
234
+ return up ? :MBMiddleUp : :MBMiddle if key == 1
235
+ return up ? :MBRightUp : :MBRight if key == 2
236
+ return moved ? :Mouse : :MBUp if key == 3
237
+ return :MWUp if key == 4
238
+ return :MWDown if key == 5
239
+ return :MWLeft if key == 6
240
+ return :MWRight if key == 7
241
+ :"MB_#{key - 8}#{'Up' if up}"
145
242
  end
146
243
  end
147
244
 
148
- # Event string received from standard input.
149
- # This can be a simple value like `"a"`or more complex input like
150
- # `"\e[24;6~"` (for Shift+Ctrl+F12).
151
- #
152
- # @return [String] received event string
153
- attr_reader :raw
154
-
155
- # Pressed key without any modifiers.
156
- # This can be a string for simple keys like `"a"` or a Symbol like `:F12`.
157
- # @return [String, Symbol] key without modifiers
158
- attr_reader :key
159
-
160
- # Modifier key code. This represents the encoded key modifier like `Shift`
161
- # or `Alt`.
162
- #
163
- # @return [Integer] modifier key code
164
- attr_reader :modifier
165
-
166
- # @comment for mouse events
167
- # @private
168
- attr_reader :extra
169
-
170
- # Name of the key event.
171
- # This can be a simple name like `"a"` or `"Shift+Ctrl+F12"` for combined
172
- # keys.
173
- #
174
- # @return [String] key name
175
- attr_reader :name
176
-
177
- # @attribute [r] modifier?
178
- # @return [true, false] whether a key modifier was pressed
179
- def modifier? = @modifier != 0
180
-
181
- # @attribute [r] simple?
182
- # @return [true, false] whether a simple key was pressed
183
- def simple? = @raw == @name
184
-
185
- # All pressed keys.
186
- # This is composed by all {modifier} and the {key}.
187
- #
188
- # @return [Array<Symbol, String>] all pressed keys
189
- def to_a = @ary.dup
190
-
191
- # @private
192
- def to_ary = simple? ? [@raw] : [@raw, @name]
193
-
194
- # @private
195
- def to_s = @name.dup
196
-
197
- # @private
198
- def inspect = "<#{self.class.name} #{to_ary.map(&:inspect).join(' ')}>"
199
-
200
- # @private
201
- def freeze
202
- @raw.freeze
203
- @key.freeze
204
- @extra.freeze
205
- @name.freeze
206
- super
207
- end
208
-
209
- private
210
-
211
- def initialize(raw, key, modifier, extra)
212
- @raw = raw
213
- @key = key
214
- @modifier = modifier
215
- @extra = extra
216
- @ary = MODIFIERS.filter_map { |b, n| n if modifier.allbits?(b) }
217
- @ary << key if key
218
- @name = @ary.join('+') # .encode(Encoding::UTF_8)
219
- end
220
-
221
- @mouse_kind = %i[
222
- MButton1
223
- MButton2
224
- MButton3
225
- MButtonUp
226
- MButton4
227
- MButton5
228
- ].freeze
229
-
230
- @mouse_wheel_kind = %i[MWheelDown MWheelUp MWheelRight MWheelLeft].freeze
231
-
232
245
  @csiu = {
233
246
  0x02 => :Ins,
234
247
  0x09 => :Tab,
@@ -357,6 +370,8 @@ module Terminal
357
370
  0x44 => :Left, # D
358
371
  0x46 => :End, # F
359
372
  0x48 => :Home, # H
373
+ 0x49 => :Focus, # I
374
+ 0x4f => :UnFocus, # O
360
375
  0x50 => :F1, # P
361
376
  0x51 => :F2, # Q
362
377
  0x52 => :F3, # R
@@ -421,7 +436,8 @@ module Terminal
421
436
  16 => :Hyper,
422
437
  32 => :Meta,
423
438
  64 => :Caps,
424
- 128 => :Num
439
+ 128 => :Num,
440
+ 256 => :Move # mouse
425
441
  }.freeze
426
442
  private_constant :MODIFIERS
427
443
  end
@@ -16,7 +16,7 @@ module Terminal
16
16
  # @return [:dumb]
17
17
  # for non-interactive input (pipes etc.)
18
18
  # @return [:error]
19
- # when input device is closed
19
+ # when input device is not avail (closed)
20
20
  def input_mode
21
21
  @input_mode ||= find_input_mode
22
22
  end
@@ -34,6 +34,10 @@ module Terminal
34
34
  # @return [[String, String]] key code and key name in `:both` mode
35
35
  # @return [nil] in error case
36
36
  def read_key(mode: :named)
37
+ warn(
38
+ 'Terminal.read_key is deprecaded; use Terminal.read_key_event instead.',
39
+ uplevel: 1
40
+ )
37
41
  event = read_key_event or return
38
42
  return event.raw if mode == :raw
39
43
  key, name = event
@@ -48,25 +52,109 @@ module Terminal
48
52
  case input_mode
49
53
  when :dumb
50
54
  raw = read_dumb or return
51
- opts = DUMB_KEYS[raw.ord] if raw.size == 1
55
+ opts = @dumb_keys[raw.ord] if raw.size == 1
52
56
  KeyEvent.new(raw, *opts)
53
57
  when :csi_u, :legacy
54
- # raw = with_mouse ? read_tty_with_mouse : read_tty
55
- KeyEvent[raw] if (raw = read_tty)
58
+ raw = read_tty or return
59
+ KeyEvent[raw]
60
+ end
61
+ end
62
+
63
+ # Event loop for key and mouse events.
64
+ #
65
+ # @param mouse [true, false]
66
+ # whether mouse buttons should be reported
67
+ # @param mouse_move [true, false]
68
+ # whether mouse movement should be reported
69
+ # @param focus [true, false]
70
+ # whether focus/unfocus of terminal window should be reported
71
+ # @yieldparam event [KeyEvent]
72
+ # next event
73
+ # @yieldreturn [true, false]
74
+ # whether the loop should be continued
75
+ # @return [true]
76
+ # when the loop was started
77
+ # @return [false]
78
+ # when the current input device is not available
79
+ # @return [nil]
80
+ # when no block was given
81
+ def on_key_event(mouse: false, mouse_move: false, focus: false, &block)
82
+ return unless block
83
+ case input_mode
84
+ when :dumb
85
+ on_bumb_key_event(&block)
86
+ true
87
+ when :csi_u, :legacy
88
+ on_tty_key_event(mouse, mouse_move, focus, &block)
89
+ true
90
+ else
91
+ false
56
92
  end
57
93
  end
58
94
 
59
95
  private
60
96
 
97
+ def on_tty_key_event(mouse, mouse_move, focus)
98
+ # highlight: '1001'
99
+ # drag: '1002'
100
+ # move: '1003'
101
+ # focus: '1004' - lost: "\e[O", get: "\e[I"
102
+ # ext: '1005'
103
+ # sgr: '1006'
104
+ # urxvt: '1015'
105
+ # pixel: '1016'
106
+ opts =
107
+ if mouse || mouse_move || focus
108
+ opts = mouse ? +"\e[?1006;1000" : +"\e[?1006"
109
+ opts << ';1003' if mouse_move
110
+ opts << ';1004' if focus
111
+ raw_write("#{opts}h") ? "#{opts}l" : nil
112
+ end
113
+ STDIN.noecho do |stdin|
114
+ while (raw = stdin.getch)
115
+ if raw != "\e"
116
+ yield(KeyEvent[raw]) ? next : break
117
+ end
118
+ lesci = 0
119
+ while (nc = stdin.read_nonblock(1, exception: false))
120
+ break unless String === nc
121
+ lesci = raw.size if nc == "\e"
122
+ raw << nc
123
+ end
124
+ if lesci < 2
125
+ yield(KeyEvent[raw]) ? next : break
126
+ end
127
+ break unless raw[1..].split("\e").all? { yield(KeyEvent["\e#{_1}"]) }
128
+ end
129
+ end
130
+ rescue Interrupt
131
+ # nop
132
+ rescue IOError, SystemCallError
133
+ @input_mode = :error
134
+ ensure
135
+ raw_write(opts) if opts
136
+ end
137
+
138
+ def on_bumb_key_event
139
+ while (raw = read_dumb)
140
+ opts = @dumb_keys[raw.ord] if raw.size == 1
141
+ return unless yield(KeyEvent.new(raw, *opts))
142
+ end
143
+ end
144
+
61
145
  def find_input_mode
62
- # order is important!
63
146
  im = ENV['INPUT_MODE']
64
- return :dumb if im == 'dumb'
65
147
  return :legacy if im == 'legacy'
66
- return :dumb unless STDIN.tty?
67
- if ansi? && raw_write("\e[>1u\e[?u\e[c") && csi_u?
68
- at_exit { raw_write("\e[<u") }
69
- return :csi_u
148
+ return :dumb if im == 'dumb' || !STDIN.tty?
149
+ STDIN.noecho do |stdin|
150
+ if raw_write("\e[>1u\e[?u\e[c")
151
+ inp = +''
152
+ inp << stdin.getch until inp.rindex('c')
153
+ if inp.include?("\e[?1u")
154
+ at_exit { raw_write("\e[<u") }
155
+ return :csi_u
156
+ end
157
+ end
70
158
  end
71
159
  :legacy
72
160
  rescue Interrupt
@@ -75,12 +163,6 @@ module Terminal
75
163
  :error
76
164
  end
77
165
 
78
- def csi_u?
79
- inp = +''
80
- STDIN.raw { inp << _1.getch until inp.rindex('c') }
81
- inp.include?("\e[?1u")
82
- end
83
-
84
166
  def read_dumb
85
167
  STDIN.getc
86
168
  rescue Interrupt
@@ -90,45 +172,31 @@ module Terminal
90
172
  nil
91
173
  end
92
174
 
93
- def read_tty_with_mouse(each_move: false)
94
- # highlight: '1001'
95
- # drag: '1002'
96
- # move: '1003'
97
- # ext: '1005'
98
- # sgr: '1006'
99
- # urxvt: '1015'
100
- # pixel: '1016'
101
- opts = each_move ? '1000;1003;1006;1015' : '1000;1006;1015'
102
- opts = raw_write("\e[?#{opts}h") ? "\e[?#{opts}l" : nil
103
- read_tty
104
- ensure
105
- raw_write(opts) if opts
106
- end
107
-
108
175
  def read_tty
109
- if (key = STDIN.getch) == "\e"
110
- while (nc = STDIN.read_nonblock(1, exception: false))
111
- String === nc ? key << nc : break
176
+ STDIN.noecho do |stdin|
177
+ if (key = stdin.getch) == "\e"
178
+ while (nc = stdin.read_nonblock(1, exception: false))
179
+ String === nc ? key << nc : break
180
+ end
112
181
  end
182
+ key
113
183
  end
114
- key
115
184
  rescue Interrupt
116
185
  nil
117
186
  rescue IOError, SystemCallError
118
187
  @input_mode = :error
119
188
  nil
120
189
  end
121
-
122
- DUMB_KEYS = {
123
- 0x05 => ['c', 4],
124
- 0x08 => :Back,
125
- 0x09 => :Tab,
126
- 0x0a => :Enter,
127
- 0x0d => :Return,
128
- 0x1b => :Esc
129
- }.compare_by_identity.freeze
130
- private_constant :DUMB_KEYS
131
190
  end
132
191
 
192
+ @dumb_keys = {
193
+ 0x05 => ['c', 4],
194
+ 0x08 => :Back,
195
+ 0x09 => :Tab,
196
+ 0x0a => :Enter,
197
+ 0x0d => :Return,
198
+ 0x1b => :Esc
199
+ }.compare_by_identity.freeze
200
+
133
201
  autoload :KeyEvent, "#{__dir__}/input/key_event.rb"
134
202
  end
@@ -116,12 +116,8 @@ module Terminal
116
116
  end
117
117
 
118
118
  def initialize(enum)
119
- @enum =
120
- if enum.respond_to?(:enum_for)
121
- enum.enum_for(:each)
122
- else
123
- Enumerator.new { |y| enum.each { y << _1 } }
124
- end
119
+ return @enum = enum.enum_for(:each) if enum.respond_to?(:enum_for)
120
+ @enum = Enumerator.new { |y| enum.each { y << _1 } }
125
121
  end
126
122
  end
127
123
  end