terminal_rb 0.11.1 → 0.12.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.
@@ -9,7 +9,8 @@ module Terminal
9
9
  # @attribute [r] input_mode
10
10
  #
11
11
  # @return [:csi_u]
12
- # when CSIu protocol support
12
+ # when [CSIu protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol)
13
+ # supported
13
14
  # @return [:legacy]
14
15
  # for standard terminal
15
16
  # @return [:dumb]
@@ -25,40 +26,43 @@ module Terminal
25
26
  # The input will be returned as named key codes like "Ctrl+c" by default.
26
27
  # This can be changed by `mode`.
27
28
  #
29
+ # @deprecated Use arther {read_key_event} for better input handling.
30
+ #
28
31
  # @param [:named, :raw, :both] mode modifies the result
29
32
  # @return [String] key code ("as is") in `:raw` mode
30
33
  # @return [String] key name in `:named` mode
31
34
  # @return [[String, String]] key code and key name in `:both` mode
35
+ # @return [nil] in any error case
32
36
  def read_key(mode: :named)
33
- return if input_mode == :error
34
- return read_dumb(mode == :both) if @input_mode == :dumb
35
- return read_tty&.join if mode == :raw
36
- key, name = read_tty_with(@input_mode == :csi_u ? CSIuKeys : LegacyKeys)
37
- return unless key
37
+ event = read_key_event or return
38
+ return event.raw if mode == :raw
39
+ key, name = event
38
40
  mode == :both ? [key, name] : name || key
39
41
  end
40
42
 
41
- private
42
-
43
- def read_dumb(both)
44
- inp = STDIN.getc
45
- name = DumbKeys.key_name(inp)
46
- both ? [inp, name] : name || inp
47
- rescue Interrupt
48
- DumbKeys.key_name("\x05")
43
+ # Read next {KeyEvent} from standard input.
44
+ #
45
+ # @return [KeyEvent] next event
46
+ # @return [nil] on any error
47
+ def read_key_event
48
+ case input_mode
49
+ when :dumb
50
+ raw = read_dumb or return
51
+ opts = DUMB_KEYS[raw.ord] if raw.size == 1
52
+ KeyEvent.new(raw, *opts)
53
+ when :csi_u, :legacy
54
+ # raw = with_mouse ? read_tty_with_mouse : read_tty
55
+ AsKeyEvent[raw] if (raw = read_tty)
56
+ end
49
57
  end
50
58
 
51
- def read_tty_with(trans)
52
- key, *esc = read_tty
53
- return unless key
54
- return key, trans.key_name(key) if esc.empty?
55
- [key + esc.join, trans.translate(esc)]
56
- end
59
+ private
57
60
 
58
61
  def find_input_mode
59
62
  im = ENV['INPUT_MODE']
60
- return :dumb if im == 'dumb' || !STDIN.tty?
63
+ return :dumb if im == 'dumb'
61
64
  return :legacy if im == 'legacy'
65
+ return :dumb unless STDIN.tty?
62
66
  if ansi? && _write("\e[>1u\e[?u\e[c") && csi_u?
63
67
  at_exit { _write("\e[<u") }
64
68
  :csi_u
@@ -72,35 +76,174 @@ module Terminal
72
76
  end
73
77
 
74
78
  def csi_u?
75
- # iTerm2 returns result in two chunks :/
76
- found = false
77
- while true
78
- inp = read_tty&.join or return false
79
- found = true if inp.include?("\e[?1u")
80
- return found if inp[-1] == 'c'
81
- end
79
+ inp = +''
80
+ STDIN.raw { inp << _1.getch until inp.rindex('c') }
81
+ inp.include?("\e[?1u")
82
+ end
83
+
84
+ def read_dumb
85
+ STDIN.getc
86
+ rescue Interrupt
87
+ "\x05"
88
+ rescue IOError, SystemCallError
89
+ @input_mode = :error
90
+ nil
91
+ end
92
+
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 = _write("\e[?#{opts}h") ? "\e[?#{opts}l" : nil
103
+ read_tty
104
+ ensure
105
+ _write(opts) if opts
82
106
  end
83
107
 
84
108
  def read_tty
85
- STDIN.raw do |raw|
86
- key = [raw.getch]
87
- return key if key[0] != "\e"
88
- while (nc = raw.read_nonblock(1, exception: false))
109
+ if (key = STDIN.getch) == "\e"
110
+ while (nc = STDIN.read_nonblock(1, exception: false))
89
111
  String === nc ? key << nc : break
90
112
  end
91
- key
92
113
  end
114
+ key
93
115
  rescue Interrupt
94
116
  nil
95
117
  rescue IOError, SystemCallError
96
118
  @input_mode = :error
97
119
  nil
98
120
  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
+ end
132
+
133
+ #
134
+ # Key event reported from {read_key_event}.
135
+ #
136
+ class KeyEvent
137
+ class << self
138
+ # @attribute [w] caching
139
+ # @return [true, false] whether KeyCodes should be cached
140
+ def caching = !!@cache
141
+
142
+ # @attribute [w] caching
143
+ def caching=(value)
144
+ if value
145
+ @cache ||= {}
146
+ else
147
+ @cache = nil
148
+ end
149
+ end
150
+
151
+ # @!visibility private
152
+ def new(raw, key = raw, modifier = 0, extra = nil)
153
+ @cache ? (@cache[raw] ||= super.freeze) : super.freeze
154
+ end
155
+
156
+ # @!visibility private
157
+ def unknown(raw) = new(raw, nil)
158
+ end
159
+
160
+ # Event string received from standard input.
161
+ # This can be a simple value like `"a"`or `"\e[24;6~"` (for Shift+Ctrl+F12).
162
+ #
163
+ # @return [String] received event string
164
+ attr_reader :raw
165
+
166
+ # Pressed key without any modifiers.
167
+ # This can be a string for simple keys like `"a"` or a Symbol like `:F12`.
168
+ # @return [String, Symbol] key without modifiers
169
+ attr_reader :key
170
+
171
+ # Modifier key code. This represents the encoded key modifier like `Shift`
172
+ # or `Alt`.
173
+ #
174
+ # @return [Integer] modifier key code
175
+ attr_reader :modifier
176
+
177
+ # @comment for mouse events
178
+ # @!visibility private
179
+ attr_reader :extra
180
+
181
+ # Name of the key event.
182
+ # This can be a simple name like `"a"` or `"Shift+Ctrl+F12"` for combined
183
+ # keys.
184
+ #
185
+ # @return [String] key name
186
+ attr_reader :name
187
+
188
+ # @attribute [r] modifier?
189
+ # @return [true, false] whether a key modifier was pressed
190
+ def modifier? = @modifier != 0
191
+
192
+ # @attribute [r] simple?
193
+ # @return [true, false] whether a simple char was pressed
194
+ def simple? = @raw == @name
195
+
196
+ # All pressed keys.
197
+ # This is composed by all {modifier} and the {key}.
198
+ #
199
+ # @return [Array<Symbol, String>] all pressed keys
200
+ def to_a = @ary.dup
201
+
202
+ # @!visibility private
203
+ def to_ary = simple? ? [@raw] : [@raw, @name]
204
+
205
+ # @!visibility private
206
+ def to_s = @name.dup
207
+
208
+ # @!visibility private
209
+ def inspect = "<#{self.class.name} #{to_ary.map(&:inspect).join(' ')}>"
210
+
211
+ # @!visibility private
212
+ def freeze
213
+ @raw.freeze
214
+ @key.freeze
215
+ @extra.freeze
216
+ @name.freeze
217
+ super
218
+ end
219
+
220
+ private
221
+
222
+ def initialize(raw, key, modifier, extra)
223
+ @raw = raw
224
+ @key = key
225
+ @modifier = modifier
226
+ @extra = extra
227
+ @ary = MODIFIERS.filter_map { |b, n| n if modifier.allbits?(b) }
228
+ @ary << key if key
229
+ @name = @ary.join('+').encode(Encoding::UTF_8)
230
+ end
231
+
232
+ @cache = {}
233
+
234
+ MODIFIERS = {
235
+ 1 => :Shift,
236
+ 2 => :Alt,
237
+ 4 => :Ctrl,
238
+ 8 => :Super,
239
+ 16 => :Hyper,
240
+ 32 => :Meta,
241
+ 64 => :Caps,
242
+ 128 => :Num
243
+ }.freeze
244
+ private_constant :MODIFIERS
99
245
  end
100
246
 
101
- dir = __dir__
102
- autoload :CSIuKeys, "#{dir}/input/csiu_keys.rb"
103
- autoload :DumbKeys, "#{dir}/input/dumb_keys.rb"
104
- autoload :LegacyKeys, "#{dir}/input/legacy_keys.rb"
105
- private_constant :CSIuKeys, :DumbKeys, :LegacyKeys
247
+ autoload :AsKeyEvent, "#{__dir__}/input/as_key_event.rb"
248
+ private_constant :AsKeyEvent
106
249
  end
@@ -17,14 +17,13 @@ RSpec.shared_context 'with Terminal.rb' do |ansi: true, application: :kitty, col
17
17
  allow(Terminal).to receive(:hide_cursor).with(no_args).and_return(Terminal)
18
18
  allow(Terminal).to receive(:show_cursor).with(no_args).and_return(Terminal)
19
19
 
20
- bbc =
21
- if ansi
22
- ->(s) { Terminal::Ansi.bbcode(s) }
23
- else
24
- ->(s) { Terminal::Ansi.plain(s) }
25
- end
26
-
27
- nobbc = ansi ? lambda(&:to_s) : ->(s) { Terminal::Ansi.undecorate(s) }
20
+ if ansi
21
+ bbc = ->(s) { Terminal::Ansi.bbcode(s) }
22
+ nobbc = lambda(&:to_s)
23
+ else
24
+ bbc = ->(s) { Terminal::Ansi.plain(s) }
25
+ nobbc = ->(s) { Terminal::Ansi.undecorate(s) }
26
+ end
28
27
 
29
28
  allow(Terminal).to receive(:<<) do |object|
30
29
  stdout.push(bbcode[object]) unless object.nil?
@@ -6,9 +6,9 @@ module Terminal
6
6
  # Generated file; based on Unicode v16.0.0
7
7
  #
8
8
  module CharWidth
9
- def self.[](ord) = WIDTH[LAST.bsearch_index { ord <= _1 }]
9
+ def self.[](ord) = @width[@last.bsearch_index { ord <= _1 }]
10
10
 
11
- LAST = [
11
+ @last = [
12
12
  0xa0,
13
13
  0xa1,
14
14
  0xa3,
@@ -1293,7 +1293,7 @@ module Terminal
1293
1293
  0x7fffffff
1294
1294
  ].freeze
1295
1295
 
1296
- WIDTH = [
1296
+ @width = [
1297
1297
  1,
1298
1298
  -1,
1299
1299
  1,
@@ -2578,6 +2578,7 @@ module Terminal
2578
2578
  1
2579
2579
  ].freeze
2580
2580
  end
2581
+
2581
2582
  private_constant :CharWidth
2582
2583
  end
2583
2584
  end
data/lib/terminal/text.rb CHANGED
@@ -30,7 +30,7 @@ module Terminal
30
30
  def width(str, bbcode: true)
31
31
  str = bbcode ? Ansi.unbbcode(str) : str.to_s
32
32
  return 0 if str.empty?
33
- str = str.encode(ENC) if str.encoding != ENC
33
+ str = str.encode(@encoding) if str.encoding != @encoding
34
34
  width = 0
35
35
  str.scan(WIDTH_SCANNER) do |sp, gc|
36
36
  next width += char_width(gc) if gc
@@ -113,7 +113,7 @@ module Terminal
113
113
 
114
114
  def char_width(char)
115
115
  ord = char.ord
116
- return CONTROL_CHAR_WIDTH[ord] || 2 if ord < 0x20
116
+ return @ctrlchar_width[ord] || 2 if ord < 0x20
117
117
  return 1 if char.size < 2 && ord < 0xa1
118
118
  width = CharWidth[ord]
119
119
  return @ambiguous_char_width if width == -1
@@ -124,7 +124,7 @@ module Terminal
124
124
  end
125
125
 
126
126
  def lim_pairs(snippeds, limit)
127
- line = EMPTY.dup
127
+ line = @empty.dup
128
128
  size = 0
129
129
  csi = nil
130
130
  snippeds.each do |snipped|
@@ -144,7 +144,7 @@ module Terminal
144
144
 
145
145
  if snipped == :hard_nl
146
146
  line[-1] == ' ' ? yield(line.chop, size - 1) : yield(line, size)
147
- line = EMPTY.dup
147
+ line = @empty.dup
148
148
  csi = nil
149
149
  next size = 0
150
150
  end
@@ -190,7 +190,7 @@ module Terminal
190
190
  end
191
191
 
192
192
  def pairs(snippeds)
193
- line = EMPTY.dup
193
+ line = @empty.dup
194
194
  size = 0
195
195
  csi = nil
196
196
  snippeds.each do |snipped|
@@ -208,7 +208,7 @@ module Terminal
208
208
 
209
209
  if snipped == :hard_nl
210
210
  line[-1] == ' ' ? yield(line.chop, size - 1) : yield(line, size)
211
- line = EMPTY.dup
211
+ line = @empty.dup
212
212
  csi = nil
213
213
  next size = 0
214
214
  end
@@ -229,7 +229,7 @@ module Terminal
229
229
  end
230
230
 
231
231
  def lim_lines(snippeds, limit)
232
- line = EMPTY.dup
232
+ line = @empty.dup
233
233
  size = 0
234
234
  csi = nil
235
235
  snippeds.each do |snipped|
@@ -249,7 +249,7 @@ module Terminal
249
249
 
250
250
  if snipped == :hard_nl
251
251
  yield(line[-1] == ' ' ? line.chop : line)
252
- line = EMPTY.dup
252
+ line = @empty.dup
253
253
  csi = nil
254
254
  next size = 0
255
255
  end
@@ -295,7 +295,7 @@ module Terminal
295
295
  end
296
296
 
297
297
  def lines(snippeds)
298
- line = EMPTY.dup
298
+ line = @empty.dup
299
299
  size = 0
300
300
  csi = nil
301
301
  snippeds.each do |snipped|
@@ -313,7 +313,7 @@ module Terminal
313
313
 
314
314
  if snipped == :hard_nl
315
315
  yield(line[-1] == ' ' ? line.chop : line)
316
- line = EMPTY.dup
316
+ line = @empty.dup
317
317
  csi = nil
318
318
  next size = 0
319
319
  end
@@ -342,7 +342,7 @@ module Terminal
342
342
  next ret << (last = :hard_nl)
343
343
  end
344
344
 
345
- txt = txt.encode(ENC) if txt.encoding != ENC
345
+ txt = txt.encode(@encoding) if txt.encoding != @encoding
346
346
 
347
347
  txt.scan(SCAN_EXPR) do |nl, csi, osc, space, gc|
348
348
  if gc
@@ -445,10 +445,8 @@ module Terminal
445
445
  private_constant :Osc, :CsiEnd, :Csi, :Word, :WordEx
446
446
 
447
447
  @ambiguous_char_width = 1
448
-
449
- ENC = Encoding::UTF_8
450
- EMPTY = String.new(encoding: ENC).freeze
451
- private_constant :ENC, :EMPTY
448
+ @encoding = Encoding::UTF_8
449
+ @empty = String.new(encoding: @encoding).freeze
452
450
 
453
451
  SCAN_EXPR =
454
452
  /\G(?:
@@ -468,7 +466,7 @@ module Terminal
468
466
  )/x
469
467
  private_constant :SCAN_EXPR, :WIDTH_SCANNER
470
468
 
471
- CONTROL_CHAR_WIDTH = {
469
+ @ctrlchar_width = {
472
470
  0x00 => 0,
473
471
  0x01 => 1,
474
472
  0x02 => 1,
@@ -502,7 +500,6 @@ module Terminal
502
500
  0x1e => 1,
503
501
  0x1f => 1
504
502
  }.compare_by_identity.freeze
505
- private_constant :CONTROL_CHAR_WIDTH
506
503
 
507
504
  autoload :CharWidth, "#{__dir__}/text/char_width.rb"
508
505
  private_constant :CharWidth
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Terminal
4
4
  # The version number of the gem.
5
- VERSION = '0.11.1'
5
+ VERSION = '0.12.1'
6
6
  end
data/lib/terminal.rb CHANGED
@@ -11,16 +11,16 @@ require_relative 'terminal/input'
11
11
  # It automagically detects whether your terminal supports ANSI features, like
12
12
  # coloring (see {colors}) or the
13
13
  # [CSIu protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol) support
14
- # (see {read_key} and {input_mode}). It calculates the display width for Unicode
15
- # chars (see {Text.width}) and help you to display text with line formatting
16
- # (see {Text.each_line}).
14
+ # (see {read_key_event} and {input_mode}).
15
+ # It calculates the display width for Unicode chars (see {Text.width}) and help
16
+ # you to display text with line formatting (see {Text.each_line}).
17
17
  #
18
18
  module Terminal
19
19
  class << self
20
- # Return true when the current terminal supports ANSI control codes.
21
- # When the terminal does not support it, {colors} will return `2` as color
22
- # count and all output methods ({<<}, {print}, {puts}) will not forward
23
- # ANSI control codes to the terminal, {read_key} will not support CSIu.
20
+ # Return true if the current terminal supports ANSI control codes.
21
+ # When the terminal does not support it, {colors} will return `2` and all
22
+ # output methods ({<<}, {print}, {puts}) will not forward ANSI control
23
+ # codes to the terminal, {read_key_event} will not support CSIu.
24
24
  #
25
25
  # @attribute [r] ansi?
26
26
  # @return [Boolean] whether ANSI control codes are supported
@@ -123,8 +123,9 @@ module Terminal
123
123
 
124
124
  # Screen size as a tuple of {rows} and {columns}.
125
125
  #
126
- # If the terminal does not support the report for it's dimension or ANSI
127
- # is not supported in general then environment variables will be used.
126
+ # If the terminal does not support the report of it's dimension or ANSI
127
+ # is not supported in general then environment variables `COLUMNS` and
128
+ # `LINES` will be used.
128
129
  # If this failed `[25, 80]` will be returned as default.
129
130
  #
130
131
  # Setting the terminal size is not widely supported.
@@ -156,7 +157,8 @@ module Terminal
156
157
  # Hide the cursor.
157
158
  # Will not send the control code if the cursor is already hidden.
158
159
  #
159
- # To show the cursor again call {show_cursor}.
160
+ # When you called {hide_cursor} n-times you need to call {show_cursor}
161
+ # n-times to show the cursor again.
160
162
  #
161
163
  # @return [Terminal] itself
162
164
  def hide_cursor
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: terminal_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.1
4
+ version: 0.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Blumtritt
@@ -34,9 +34,7 @@ files:
34
34
  - lib/terminal/ansi/named_colors.rb
35
35
  - lib/terminal/detect.rb
36
36
  - lib/terminal/input.rb
37
- - lib/terminal/input/csiu_keys.rb
38
- - lib/terminal/input/dumb_keys.rb
39
- - lib/terminal/input/legacy_keys.rb
37
+ - lib/terminal/input/as_key_event.rb
40
38
  - lib/terminal/rspec/helper.rb
41
39
  - lib/terminal/text.rb
42
40
  - lib/terminal/text/char_width.rb
@@ -46,10 +44,11 @@ homepage: https://codeberg.org/mblumtritt/Terminal.rb
46
44
  licenses:
47
45
  - MIT
48
46
  metadata:
49
- rubygems_mfa_required: 'true'
50
47
  source_code_uri: https://codeberg.org/mblumtritt/Terminal.rb
51
48
  bug_tracker_uri: https://codeberg.org/mblumtritt/Terminal.rb/issues
52
- documentation_uri: https://rubydoc.info/gems/terminal_rb/0.11.1/Terminal
49
+ documentation_uri: https://rubydoc.info/gems/terminal_rb
50
+ rubygems_mfa_required: 'true'
51
+ yard.run: yard
53
52
  rdoc_options: []
54
53
  require_paths:
55
54
  - lib