terminal_rb 0.12.1 → 0.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 834587f3bf9e66077d0ad33b8391100863ec8932e4eafd31077684c8a982d03c
4
- data.tar.gz: fd95fac927978b7bdbc39d71797d165b42a787a6a35139418c6a445192258e83
3
+ metadata.gz: 756d1e0e016472a063f9d64650f4731ef6e8fe272d6a84c4e001119f050af3ea
4
+ data.tar.gz: 74c24ba64e08e6dc79605c17b0ded59ecb8784a45f95dbe5be7f231c889fdab7
5
5
  SHA512:
6
- metadata.gz: 7f5284ff90de5ee142d3e11414df40148eef0dca7bb216e000f4f7a3474c416bba644cdb2a44656612eae33bb7e0933424ef34161a7cd247b89054ab99538bbe
7
- data.tar.gz: 979db236870e067f58d25e18ca4bc5ce088c1558b8db8c59e95017656c28cf6e762593849fa4e9d44a2d716654b81e02d643ca43d2ba83528410070c8a8e70c7
6
+ metadata.gz: 68bb81c425cb5df44650acc351a28cbbb3eccaea53b405167f1207ba3dc528093de6322bac087fad9301da7470711b4cd399f120233637e9b92f1727e9e2d4c9
7
+ data.tar.gz: 9e855ec0401d22c615fd6ff25e2adae7717ecbf3578ea40425e5ef580b9520ee23130c8d49814034f682590166d2319070c17c18407962fc8782b18c1fffe4fa
data/README.md CHANGED
@@ -14,7 +14,7 @@ Terminal access with super fast ANSI control codes support and [BBCode-like](htt
14
14
  - [NO_COLOR convention](https://no-color.org) support
15
15
  - markup of text attributes and colors using a [BBCode](https://en.wikipedia.org/wiki/BBCode)-like syntax
16
16
  - calculation for correct display width of strings containing Unicdode chars inclusive emojis
17
- - generation of word-by-word wrapped text
17
+ - word-wise line break generator
18
18
  - supports [CSIu protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol)
19
19
 
20
20
  ## Examples
data/examples/bbcode.rb CHANGED
@@ -7,12 +7,13 @@ Terminal.puts <<~TEXT
7
7
  ✅ [b bright_green]Terminal.rb::Ansi[/b] — BBCode:[/]
8
8
 
9
9
  [b]Bold[/b] [\\b]...[\\/b] or [\\bold]...[\\/bold]
10
- [faint]Faint[/faint] [\\faint]...[\\/faint]
11
- [i]Italic[/i] [\\i]...[\\/i] or [\\|italic]...[\\/italic]
10
+ [dim]Dim[/dim] [\\d]...[\\/d] or [\\dim]...[\\/dim]
11
+ [i]Italic[/i] [\\i]...[\\/i] or [\\italic]...[\\/italic]
12
+ [u]Underline[/u] [\\u]...[\\/u] or [\\|undeline]...[\\/undeline]
12
13
  [inv]Invert[/inv] [\\inv]...[\\/inv] or [\\invert]...[\\/invert]
13
- [strike]Strike[/strike] [\\strike]...[\\/strike]
14
14
  [h]Hide[/h] [\\h]...[\\/h] or [\\hide]...[\\/hide] or [\\conceal]...[\\/conceal]
15
- [blink]Slow blink[/blink] [\\blink]...[\\/blink] or [\\slow_blink]...[\\/slow_blink]
15
+ [strike]Strike[/strike] [\\strike]...[\\/strike]
16
+ [blink]Blink[/blink] [\\blink]...[\\/blink]
16
17
  [u]Underline[/u] [\\u]...[\\/u] or [\\underline]...[\\/underline]
17
18
  [uu]Double underline[/uu] [\\uu]...[\\/uu] or [\\double_underline]...[\\/double_underline]
18
19
  [cu]Curly underline[/cu] [\\cu]...[\\/cu] or [\\curly_underline]...[\\/curly_underline]
data/lib/terminal/ansi.rb CHANGED
@@ -12,7 +12,7 @@ module Terminal
12
12
  #
13
13
  # @attribute [r] attributes
14
14
  # @return [Array<Symbol>] all attribute names
15
- def attributes = @attr_sym.keys
15
+ def attributes = @attributes.dup
16
16
 
17
17
  # Supported 3/4-bit color names.
18
18
  #
@@ -20,7 +20,7 @@ module Terminal
20
20
  #
21
21
  # @attribute [r] colors
22
22
  # @return [Array<Symbol>] all color names
23
- def colors = @colors_sym.keys
23
+ def colors = @colors.dup
24
24
 
25
25
  # Supported basic 24-bit (Kitty compatible) color names.
26
26
  #
@@ -99,10 +99,9 @@ module Terminal
99
99
  .map do |arg|
100
100
  case arg
101
101
  when String
102
- @attr[arg] || @colors[arg] || _color(arg) || _invalid(arg)
102
+ @attr_map[arg] || _invalid(arg)
103
103
  when Symbol
104
- @attr_sym[arg] || @colors_sym[arg] || _color(arg) ||
105
- _invalid(arg)
104
+ @attrs_map[arg] || _invalid(arg)
106
105
  when (0..255)
107
106
  "38;5;#{arg}"
108
107
  when (256..511)
@@ -179,11 +178,7 @@ module Terminal
179
178
  def try_convert(attributes, separator: ' ')
180
179
  return unless attributes
181
180
  return if (attributes = attributes.to_s.split(separator)).empty?
182
- "\e[#{
183
- attributes
184
- .map! { @attr[_1] || @colors[_1] || _color(_1) || return }
185
- .join(';')
186
- }m"
181
+ "\e[#{attributes.map! { @attr_map[_1] || return }.join(';')}m"
187
182
  end
188
183
 
189
184
  # Test if all given attributes are valid.
@@ -196,9 +191,9 @@ module Terminal
196
191
  attributes.all? do |arg|
197
192
  case arg
198
193
  when String
199
- @attr[arg] || @colors[arg] || _color(arg)
194
+ @attr_map[arg]
200
195
  when Symbol
201
- @attr_sym[arg] || @colors_sym[arg] || _color(arg)
196
+ @attrs_map[arg]
202
197
  when (0..767)
203
198
  true
204
199
  end
@@ -249,7 +244,7 @@ module Terminal
249
244
  match = Regexp.last_match(1) or next match_str
250
245
  next "[#{match[1..]}]" if match[0] == '\\'
251
246
  next match_str if (match = match.split).empty?
252
- next if match.all? { @attr[_1] || @colors[_1] || _color(_1) }
247
+ next if match.all? { @attr_map[_1] }
253
248
  match_str
254
249
  end
255
250
  end
@@ -277,7 +272,7 @@ module Terminal
277
272
  match = Regexp.last_match(1) or next match_str
278
273
  next "[#{match[1..]}]" if match[0] == '\\'
279
274
  next match_str if (match = match.split).empty?
280
- next if match.all? { @attr[_1] || @colors[_1] || _color(_1) }
275
+ next if match.all? { @attr_map[_1] }
281
276
  match_str
282
277
  end
283
278
  str.index("\e") ? str.gsub!(@re_test, '') : str
@@ -586,9 +581,9 @@ module Terminal
586
581
  end
587
582
  end
588
583
 
589
- @cbase =
590
- { 'bg' => '48', 'on' => '48', 'ul' => '58' }.tap { _1.default = '38' }
591
- .freeze
584
+ @cbase = { 'bg' => '48', 'on' => '48', 'ul' => '58' }
585
+ @cbase.default = '38'
586
+ @cbase.freeze
592
587
 
593
588
  @re_test =
594
589
  /
@@ -599,7 +594,195 @@ module Terminal
599
594
 
600
595
  @re_bbcode = /(?:\[((?~[\[\]]))\])/
601
596
 
602
- require_relative 'ansi/attributes'
597
+ attr_map = {
598
+ '' => 'reset',
599
+ '1' => 'bold',
600
+ '2' => 'faint',
601
+ '3' => 'italic',
602
+ '4' => 'underline',
603
+ '5' => 'blink',
604
+ '6' => 'rapid_blink',
605
+ '7' => 'invert',
606
+ '8' => 'hide',
607
+ '9' => 'strike',
608
+ '10' => 'primary_font',
609
+ '11' => 'font1',
610
+ '12' => 'font2',
611
+ '13' => 'font3',
612
+ '14' => 'font4',
613
+ '15' => 'font5',
614
+ '16' => 'font6',
615
+ '17' => 'font7',
616
+ '18' => 'font8',
617
+ '19' => 'font9',
618
+ '20' => 'fraktur',
619
+ '21' => 'double_underline',
620
+ '22' => 'bold_off', # faint_off
621
+ '23' => 'italic_off', # fraktur_off
622
+ '24' => 'underline_off', # double_underline_off
623
+ '25' => 'blink_off', # rapid_blink_off
624
+ '26' => 'proportional',
625
+ '27' => 'invert_off',
626
+ '28' => 'hide_off',
627
+ '29' => 'strike_off',
628
+ # colors ...
629
+ '50' => 'proportional_off',
630
+ '51' => 'framed',
631
+ '52' => 'encircled',
632
+ '53' => 'overlined',
633
+ '54' => 'framed_off', # encircled_off
634
+ '55' => 'overlined_off',
635
+ # ...
636
+ '73' => 'superscript',
637
+ '74' => 'subscript',
638
+ '75' => 'superscript_off', # subscript_off
639
+ # special underline
640
+ '4:3' => 'curly_underline',
641
+ '4:4' => 'dotted_underline',
642
+ '4:5' => 'dashed_underline',
643
+ '4:0' => 'curly_underline_off' # dotted_underline_off, dashed_underline_off
644
+ }.invert
645
+ attr_alias = ->(t, s) { attr_map[t] = attr_map[s] }
646
+
647
+ clr_map = {
648
+ # foreground
649
+ '30' => 'black',
650
+ '31' => 'red',
651
+ '32' => 'green',
652
+ '33' => 'yellow',
653
+ '34' => 'blue',
654
+ '35' => 'magenta',
655
+ '36' => 'cyan',
656
+ '37' => 'white',
657
+ '39' => 'default', # reset
658
+ # background
659
+ '40' => 'on_black',
660
+ '41' => 'on_red',
661
+ '42' => 'on_green',
662
+ '43' => 'on_yellow',
663
+ '44' => 'on_blue',
664
+ '45' => 'on_magenta',
665
+ '46' => 'on_cyan',
666
+ '47' => 'on_white',
667
+ '49' => 'on_default', # reset
668
+ # underline
669
+ '58;2;0;0;0' => 'ul_black',
670
+ '58;2;0;0;128' => 'ul_blue',
671
+ '58;2;0;0;255' => 'ul_bright_blue',
672
+ '58;2;0;128;0' => 'ul_green',
673
+ '58;2;0;128;128' => 'ul_cyan',
674
+ '58;2;0;255;0' => 'ul_bright_green',
675
+ '58;2;0;255;255' => 'ul_bright_cyan',
676
+ '58;2;128;0;0' => 'ul_red',
677
+ '58;2;128;0;128' => 'ul_magenta',
678
+ '58;2;128;128;0' => 'ul_yellow',
679
+ '58;2;128;128;128' => 'ul_white',
680
+ '58;2;255;0;0' => 'ul_bright_red',
681
+ '58;2;255;0;255' => 'ul_bright_magenta',
682
+ '58;2;255;255;0' => 'ul_bright_yellow',
683
+ '58;2;255;255;255' => 'ul_bright_white',
684
+ '58;2;64;64;64' => 'ul_bright_black',
685
+ '59' => 'ul_default', # reset
686
+ # bright foreground
687
+ '90' => 'bright_black',
688
+ '91' => 'bright_red',
689
+ '92' => 'bright_green',
690
+ '93' => 'bright_yellow',
691
+ '94' => 'bright_blue',
692
+ '95' => 'bright_magenta',
693
+ '96' => 'bright_cyan',
694
+ '97' => 'bright_white',
695
+ # bright background
696
+ '100' => 'on_bright_black',
697
+ '101' => 'on_bright_red',
698
+ '102' => 'on_bright_green',
699
+ '103' => 'on_bright_yellow',
700
+ '104' => 'on_bright_blue',
701
+ '105' => 'on_bright_magenta',
702
+ '106' => 'on_bright_cyan',
703
+ '107' => 'on_bright_white'
704
+ }.invert
705
+ clr_alias = ->(t, s) { clr_map[t] = clr_map[s] }
706
+
707
+ %w[black red green yellow blue magenta cyan white].each do |name|
708
+ clr_alias["fg_#{name}", name]
709
+ clr_alias["bg_#{name}", "on_#{name}"]
710
+ clr_alias["fg_bright_#{name}", "bright_#{name}"]
711
+ clr_alias["bg_bright_#{name}", "on_bright_#{name}"]
712
+ end
713
+ clr_alias['fg_default', 'default']
714
+ clr_alias['bg_default', 'on_default']
715
+
716
+ attr_alias['faint_off', 'bold_off']
717
+ attr_alias['fraktur_off', 'italic_off']
718
+ attr_alias['double_underline_off', 'underline_off']
719
+ attr_alias['rapid_blink_off', 'blink_off']
720
+ attr_alias['encircled_off', 'framed_off']
721
+ attr_alias['subscript_off', 'superscript_off']
722
+ attr_alias['dotted_underline_off', 'curly_underline_off']
723
+ attr_alias['dashed_underline_off', 'curly_underline_off']
724
+
725
+ # extra aliases:
726
+ attr_alias['off', 'reset']
727
+ attr_alias['dim', 'faint']
728
+ attr_alias['dim_off', 'faint_off']
729
+ attr_alias['conceal', 'hide']
730
+ attr_alias['conceal_off', 'hide_off']
731
+ attr_alias['reveal', 'hide_off']
732
+ attr_alias['spacing', 'proportional']
733
+ attr_alias['spacing_off', 'proportional_off']
734
+
735
+ # shortcuts:
736
+ attr_alias['b', 'bold']
737
+ attr_alias['d', 'dim']
738
+ attr_alias['i', 'italic']
739
+ attr_alias['u', 'underline']
740
+ attr_alias['inv', 'invert']
741
+ attr_alias['h', 'hide']
742
+ attr_alias['s', 'strike']
743
+ attr_alias['uu', 'double_underline']
744
+ attr_alias['ovr', 'overlined']
745
+ attr_alias['sup', 'superscript']
746
+ attr_alias['sub', 'subscript']
747
+ attr_alias['cu', 'curly_underline']
748
+ attr_alias['dau', 'dashed_underline']
749
+ attr_alias['dou', 'dotted_underline']
750
+
751
+ @colors = clr_map.keys.map!(&:to_sym).sort!.freeze
752
+ @attributes = attr_map.keys.map!(&:to_sym).sort!.freeze
753
+
754
+ # shortcuts disable:
755
+ attr_map.keys.each do |n|
756
+ attr_alias["/#{n.delete_suffix('_off')}", n] if n.end_with?('_off')
757
+ end
758
+ attr_alias['/b', 'bold_off']
759
+ attr_alias['/d', 'dim_off']
760
+ attr_alias['/i', 'italic_off']
761
+ attr_alias['/u', 'underline_off']
762
+ attr_alias['/inv', 'invert_off']
763
+ attr_alias['/h', 'hide_off']
764
+ attr_alias['/s', 'strike_off']
765
+ attr_alias['/uu', 'double_underline_off']
766
+ attr_alias['/ovr', 'overlined_off']
767
+ attr_alias['/sup', 'superscript_off']
768
+ attr_alias['/sub', 'subscript_off']
769
+ attr_alias['/cu', 'curly_underline_off']
770
+ attr_alias['/dau', 'dashed_underline_off']
771
+ attr_alias['/dou', 'dotted_underline_off']
772
+
773
+ # additional shortcuts disable:
774
+ attr_alias['/', 'reset']
775
+ attr_map['/fg'] = clr_map['default']
776
+ attr_map['/bg'] = clr_map['on_default']
777
+ attr_map['/ul'] = clr_map['ul_default']
778
+
779
+ @attr_map = Hash.new { _color(_2) }
780
+ attr_map.merge!(clr_map).keys.sort!.each { @attr_map[_1] = attr_map[_1] }
781
+ @attr_map.freeze
782
+
783
+ @attrs_map = @attr_map.transform_keys(&:to_sym)
784
+ @attrs_map.default_proc = @attr_map.default_proc
785
+ @attrs_map.compare_by_identity.freeze
603
786
 
604
787
  autoload :NAMED_COLORS, "#{__dir__}/ansi/named_colors.rb"
605
788
  private_constant :NAMED_COLORS
@@ -689,14 +872,6 @@ module Terminal
689
872
  #
690
873
  # def set_scroll_region(top = nil, bottom = nil) = "\e[#{top};#{bottom}r"
691
874
 
692
- # @comment TODO:
693
- # https://sw.kovidgoyal.net/kitty/desktop-notifications
694
- # https://sw.kovidgoyal.net/kitty/pointer-shapes
695
- # https://sw.kovidgoyal.net/kitty/unscroll
696
- # https://sw.kovidgoyal.net/kitty/color-stack
697
- # https://sw.kovidgoyal.net/kitty/deccara
698
- # https://sw.kovidgoyal.net/kitty/clipboard
699
-
700
875
  # @comment other:
701
876
  # "\eE" same as "\r\n"
702
877
  # "\eD" same as "\n" but preserves X coord
@@ -705,5 +880,13 @@ module Terminal
705
880
  # "\e[21t report window’s title as ESC ] l title ESC \"
706
881
  # "\e[?1004h" report focus lost "\e[O" and focus get "\e[I"
707
882
  # (disable by "\e[?1004l")
883
+
884
+ # @comment TODO:
885
+ # https://sw.kovidgoyal.net/kitty/desktop-notifications
886
+ # https://sw.kovidgoyal.net/kitty/pointer-shapes
887
+ # https://sw.kovidgoyal.net/kitty/unscroll
888
+ # https://sw.kovidgoyal.net/kitty/color-stack
889
+ # https://sw.kovidgoyal.net/kitty/deccara
890
+ # https://sw.kovidgoyal.net/kitty/clipboard
708
891
  end
709
892
  end
@@ -1,19 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Terminal
4
- module AsKeyEvent
4
+ #
5
+ # Key event reported from {read_key_event}.
6
+ #
7
+ class KeyEvent
5
8
  class << self
9
+ # @attribute [w] caching
10
+ # @return [true, false] whether KeyCodes should be cached
11
+ def caching = !!@cache
12
+
13
+ # @attribute [w] caching
14
+ def caching=(value)
15
+ if value
16
+ @cache ||= {}
17
+ else
18
+ @cache = nil
19
+ end
20
+ end
21
+
22
+ # Translate a keyboard input string into a related KeyEvent
23
+ #
24
+ # @param [String] raw keyboard input
25
+ # @return [KeyEvent] related key event
6
26
  def [](raw)
7
- return KeyEvent.new(raw, *@single_key[raw.ord]) if raw.size == 1
8
- return KeyEvent.unknown(raw) if raw[0] != "\e"
27
+ return new(raw, *@single_key[raw.ord]) if raw.size == 1
28
+ return unknown(raw) if raw[0] != "\e"
9
29
  return esc1(raw, raw[1]) if raw.size == 2 # ESC ?
10
30
  case raw[1]
11
31
  when "\e" # ESC ESC ...
12
32
  return esc_esc(raw)
13
33
  when 'O'
14
- return KeyEvent.new(raw, *@ss3[raw[2].ord]) if raw.size == 3 # ESC O ?
34
+ return new(raw, *@ss3[raw[2].ord]) if raw.size == 3 # ESC O ?
15
35
  when '['
16
- return KeyEvent.new(raw, *@ss3[raw[2].ord]) if raw.size == 3 # ESC [ ?
36
+ return new(raw, *@ss3[raw[2].ord]) if raw.size == 3 # ESC [ ?
17
37
  if raw.size == 6 && raw[2] == 'M' # ESC [ M b c r
18
38
  return mouse_vt200(raw)
19
39
  end
@@ -29,18 +49,25 @@ module Terminal
29
49
  return mouse_sgr(raw) if raw[2] == '<'
30
50
  end
31
51
  end
32
- KeyEvent.unknown(raw)
52
+ unknown(raw)
53
+ end
54
+
55
+ # @!visibility private
56
+ def new(raw, key = raw, modifier = 0, extra = nil)
57
+ @cache ? (@cache[raw] ||= super.freeze) : super.freeze
33
58
  end
34
59
 
35
60
  private
36
61
 
62
+ def unknown(raw) = new(raw, nil)
63
+
37
64
  def csi_u(raw)
38
65
  # ESC [ <code> u
39
66
  # ESC [ <code> ; <modifier> u
40
- return KeyEvent.unknown(raw) if raw.size < 4
67
+ return unknown(raw) if raw.size < 4
41
68
  idx = raw.index(';')
42
69
  code = raw[2..(idx ? idx - 1 : -2)].to_i
43
- KeyEvent.new(
70
+ new(
44
71
  raw,
45
72
  @csiu[code] || code.chr(Encoding::UTF_8),
46
73
  idx ? [raw[(idx + 1)..-2].to_i - 1, 0].max : 0
@@ -50,9 +77,9 @@ module Terminal
50
77
  def legacy(raw)
51
78
  # ESC [ <code> ~
52
79
  # ESC [ <code> ; <modifier> ~
53
- return KeyEvent.unknown(raw) if raw.size < 4
80
+ return unknown(raw) if raw.size < 4
54
81
  idx = raw.index(';')
55
- KeyEvent.new(
82
+ new(
56
83
  raw,
57
84
  @csi[raw[2..(idx ? idx - 1 : -2)].to_i],
58
85
  idx ? [raw[(idx + 1)..-2].to_i - 1, 0].max : 0
@@ -62,23 +89,23 @@ module Terminal
62
89
  def csi1(raw)
63
90
  # ESC [ 1 ; [~ABCDEFHPQRS]
64
91
  # ESC [ 1 ; <modifier> [~ABCDEFHPQRS]
65
- return KeyEvent.unknown(raw) if raw.size < 5
92
+ return unknown(raw) if raw.size < 5
66
93
  key, modifier = @ss3[raw[-1].ord]
67
94
  modifier ||= 0
68
- return KeyEvent.new(raw, nil, modifier) unless key
95
+ return new(raw, nil, modifier) unless key
69
96
  modifier |= [raw[4..-2].to_i - 1, 0].max if raw.size > 5
70
- KeyEvent.new(raw, key, modifier)
97
+ new(raw, key, modifier)
71
98
  end
72
99
 
73
100
  def esc_esc(raw)
74
101
  return esc1(raw, raw[2]) if raw.size == 3 # ESC ESC ?
75
102
  ret = self[raw[1..]]
76
- KeyEvent.new(raw, ret.key, ret.modifier | 2) # ESC ESC ...
103
+ new(raw, ret.key, ret.modifier | 2) # ESC ESC ...
77
104
  end
78
105
 
79
106
  def esc1(raw, char)
80
107
  key, modifier = @esc1[char.ord]
81
- KeyEvent.new(raw, key || char, modifier || 2)
108
+ new(raw, key || char, modifier || 2)
82
109
  end
83
110
 
84
111
  def mouse_vt200(raw)
@@ -89,25 +116,25 @@ module Terminal
89
116
  def mouse_sgr(raw)
90
117
  # ESC [ < <code> ; <col> ; <row> M
91
118
  # ESC [ < <code> ; <col> ; <row> m
92
- return KeyEvent.unknown(raw) if raw.size < 8
119
+ return unknown(raw) if raw.size < 8
93
120
  bcr = raw[3..-2].split(';', 3).map!(&:to_i)
94
- bcr.size == 3 ? mouse_event(raw, *bcr) : KeyEvent.unknown(raw)
121
+ bcr.size == 3 ? mouse_event(raw, *bcr) : unknown(raw)
95
122
  end
96
123
 
97
124
  def mouse_urxvt(raw)
98
125
  # ESC [ <code> ; <col> ; <row> M
99
- return KeyEvent.unknown(raw) if raw.size < 8
126
+ return unknown(raw) if raw.size < 8
100
127
  bcr = raw[2..-2].split(';', 3).map!(&:to_i)
101
- bcr.size == 3 ? mouse_event(raw, *bcr) : KeyEvent.unknown(raw)
128
+ bcr.size == 3 ? mouse_event(raw, *bcr) : unknown(raw)
102
129
  end
103
130
 
104
131
  def mouse_event(raw, btn, col, row)
105
- return KeyEvent.unknown(raw) if btn < 0 || col < 1 || row < 1
132
+ return unknown(raw) if btn < 0 || col < 1 || row < 1
106
133
  key, btn = kind_of_mouse_event(btn)
107
134
  modifier = btn.allbits?(4) ? 1 : 0
108
135
  modifier += 2 if btn.allbits?(8)
109
136
  modifier += 4 if btn.allbits?(16)
110
- KeyEvent.new(raw, key, modifier, [col, row])
137
+ new(raw, key, modifier, [col, row])
111
138
  end
112
139
 
113
140
  def mouse_event_key(btn)
@@ -118,6 +145,79 @@ module Terminal
118
145
  end
119
146
  end
120
147
 
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
+ # @!visibility 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
+ # @!visibility private
192
+ def to_ary = simple? ? [@raw] : [@raw, @name]
193
+
194
+ # @!visibility private
195
+ def to_s = @name.dup
196
+
197
+ # @!visibility private
198
+ def inspect = "<#{self.class.name} #{to_ary.map(&:inspect).join(' ')}>"
199
+
200
+ # @!visibility 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
+
121
221
  @mouse_kind = %i[
122
222
  MButton1
123
223
  MButton2
@@ -310,6 +410,19 @@ module Terminal
310
410
  0x20 => :Space,
311
411
  0x7f => :Back
312
412
  }.compare_by_identity.freeze
413
+
414
+ @cache = {}
415
+
416
+ MODIFIERS = {
417
+ 1 => :Shift,
418
+ 2 => :Alt,
419
+ 4 => :Ctrl,
420
+ 8 => :Super,
421
+ 16 => :Hyper,
422
+ 32 => :Meta,
423
+ 64 => :Caps,
424
+ 128 => :Num
425
+ }.freeze
426
+ private_constant :MODIFIERS
313
427
  end
314
- private_constant :AsKeyEvent
315
428
  end
@@ -26,13 +26,13 @@ module Terminal
26
26
  # The input will be returned as named key codes like "Ctrl+c" by default.
27
27
  # This can be changed by `mode`.
28
28
  #
29
- # @deprecated Use arther {read_key_event} for better input handling.
29
+ # @deprecated Use rather {read_key_event} for better input handling.
30
30
  #
31
31
  # @param [:named, :raw, :both] mode modifies the result
32
32
  # @return [String] key code ("as is") in `:raw` mode
33
33
  # @return [String] key name in `:named` mode
34
34
  # @return [[String, String]] key code and key name in `:both` mode
35
- # @return [nil] in any error case
35
+ # @return [nil] in error case
36
36
  def read_key(mode: :named)
37
37
  event = read_key_event or return
38
38
  return event.raw if mode == :raw
@@ -43,7 +43,7 @@ module Terminal
43
43
  # Read next {KeyEvent} from standard input.
44
44
  #
45
45
  # @return [KeyEvent] next event
46
- # @return [nil] on any error
46
+ # @return [nil] in error case
47
47
  def read_key_event
48
48
  case input_mode
49
49
  when :dumb
@@ -52,23 +52,23 @@ module Terminal
52
52
  KeyEvent.new(raw, *opts)
53
53
  when :csi_u, :legacy
54
54
  # raw = with_mouse ? read_tty_with_mouse : read_tty
55
- AsKeyEvent[raw] if (raw = read_tty)
55
+ KeyEvent[raw] if (raw = read_tty)
56
56
  end
57
57
  end
58
58
 
59
59
  private
60
60
 
61
61
  def find_input_mode
62
+ # order is important!
62
63
  im = ENV['INPUT_MODE']
63
64
  return :dumb if im == 'dumb'
64
65
  return :legacy if im == 'legacy'
65
66
  return :dumb unless STDIN.tty?
66
67
  if ansi? && _write("\e[>1u\e[?u\e[c") && csi_u?
67
68
  at_exit { _write("\e[<u") }
68
- :csi_u
69
- else
70
- :legacy
69
+ return :csi_u
71
70
  end
71
+ :legacy
72
72
  rescue Interrupt
73
73
  :legacy
74
74
  rescue IOError, SystemCallError
@@ -130,120 +130,5 @@ module Terminal
130
130
  private_constant :DUMB_KEYS
131
131
  end
132
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
245
- end
246
-
247
- autoload :AsKeyEvent, "#{__dir__}/input/as_key_event.rb"
248
- private_constant :AsKeyEvent
133
+ autoload :KeyEvent, "#{__dir__}/input/key_event.rb"
249
134
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  RSpec.shared_context 'with Terminal.rb' do |ansi: true, application: :kitty, colors: 256, size: [25, 80], pos: [1, 1]|
4
4
  let(:stdout) { [] }
5
+ let(:stdin) { [] }
5
6
  let(:stdoutstr) { stdout.join }
6
7
 
7
8
  before do
@@ -9,21 +10,22 @@ RSpec.shared_context 'with Terminal.rb' do |ansi: true, application: :kitty, col
9
10
  allow(Terminal).to receive(:application).with(no_args).and_return(
10
11
  application
11
12
  )
13
+ allow(Terminal).to receive(:input_mode).with(no_args).and_return(
14
+ ansi ? :csi_u : :dumb
15
+ )
12
16
  allow(Terminal).to receive(:colors).with(no_args).and_return(
13
17
  colors == :true_color ? 16_777_216 : colors
14
18
  )
15
19
  allow(Terminal).to receive(:size).with(no_args).and_return(size)
16
20
  allow(Terminal).to receive(:pos).with(no_args).and_return(pos)
17
- allow(Terminal).to receive(:hide_cursor).with(no_args).and_return(Terminal)
18
- allow(Terminal).to receive(:show_cursor).with(no_args).and_return(Terminal)
19
-
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
21
+
22
+ bbcode_ = {
23
+ true =>
24
+ ansi ?
25
+ ->(s) { Terminal::Ansi.bbcode(s) } :
26
+ ->(s) { Terminal::Ansi.plain(s) },
27
+ false => ansi ? lambda(&:to_s) : ->(s) { Terminal::Ansi.undecorate(s) }
28
+ }.compare_by_identity
27
29
 
28
30
  allow(Terminal).to receive(:<<) do |object|
29
31
  stdout.push(bbcode[object]) unless object.nil?
@@ -31,7 +33,7 @@ RSpec.shared_context 'with Terminal.rb' do |ansi: true, application: :kitty, col
31
33
  end
32
34
 
33
35
  allow(Terminal).to receive(:print) do |*objects, bbcode: true|
34
- bbcode = bbcode ? bbc : nobbc
36
+ bbcode = bbcode_[bbcode]
35
37
  objects.flatten.each { stdout.push(bbcode[_1]) unless _1.nil? }
36
38
  nil
37
39
  end
@@ -42,7 +44,7 @@ RSpec.shared_context 'with Terminal.rb' do |ansi: true, application: :kitty, col
42
44
  stdout.push("\n")
43
45
  next
44
46
  end
45
- bbcode = bbcode ? bbc : nobbc
47
+ bbcode = bbcode_[bbcode]
46
48
  objects.each do |s|
47
49
  next stdout.push("\n") if s.nil?
48
50
  stdout.push(s = bbcode[s])
@@ -55,5 +57,9 @@ RSpec.shared_context 'with Terminal.rb' do |ansi: true, application: :kitty, col
55
57
  stdout.push(object = object.to_s)
56
58
  object.bytesize
57
59
  end
60
+
61
+ allow(Terminal).to receive(:read_key_event) do
62
+ Terminal::KeyEvent[stdin.shift || raise('stdin buffer is empty')]
63
+ end
58
64
  end
59
65
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Terminal
4
4
  # The version number of the gem.
5
- VERSION = '0.12.1'
5
+ VERSION = '0.13.0'
6
6
  end
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.12.1
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Blumtritt
@@ -9,9 +9,8 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: |
13
- Terminal access with super fast ANSI control codes support and
14
- BBCode-like embedded text attribute syntax.
12
+ description: 'Terminal access with super fast ANSI control codes support, modern CSIu
13
+ input, word-wise line break, BBCode-like embedded text attribute syntax. '
15
14
  executables:
16
15
  - bbcode
17
16
  extensions: []
@@ -30,11 +29,10 @@ files:
30
29
  - examples/key-codes.rb
31
30
  - lib/terminal.rb
32
31
  - lib/terminal/ansi.rb
33
- - lib/terminal/ansi/attributes.rb
34
32
  - lib/terminal/ansi/named_colors.rb
35
33
  - lib/terminal/detect.rb
36
34
  - lib/terminal/input.rb
37
- - lib/terminal/input/as_key_event.rb
35
+ - lib/terminal/input/key_event.rb
38
36
  - lib/terminal/rspec/helper.rb
39
37
  - lib/terminal/text.rb
40
38
  - lib/terminal/text/char_width.rb
@@ -65,5 +63,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
63
  requirements: []
66
64
  rubygems_version: 3.7.1
67
65
  specification_version: 4
68
- summary: Fast terminal access with ANSI and BBCode support.
66
+ summary: Fast terminal access with ANSI, CSIu, BBCode, word-wise line break support.
69
67
  test_files: []
@@ -1,206 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Terminal
4
- module Ansi
5
- @attr = {
6
- '/' => '',
7
- '/b' => '22',
8
- '/blink' => '25',
9
- '/bold' => '22',
10
- '/conceal' => '28',
11
- '/cu' => '4:0',
12
- '/curly_underline' => '4:0',
13
- '/dashed_underline' => '4:0',
14
- '/dau' => '4:0',
15
- '/dim' => '22',
16
- '/dotted_underline' => '4:0',
17
- '/dou' => '4:0',
18
- '/double_underline' => '24',
19
- '/encircled' => '54',
20
- '/faint' => '22',
21
- '/fraktur' => '23',
22
- '/framed' => '54',
23
- '/h' => '28',
24
- '/hide' => '28',
25
- '/i' => '23',
26
- '/inv' => '27',
27
- '/invert' => '27',
28
- '/italic' => '23',
29
- '/overlined' => '55',
30
- '/ovr' => '55',
31
- '/proportional' => '50',
32
- '/slow_blink' => '25',
33
- '/spacing' => '50',
34
- '/strike' => '29',
35
- '/sub' => '75',
36
- '/subscript' => '75',
37
- '/sup' => '75',
38
- '/superscript' => '75',
39
- '/u' => '24',
40
- '/underline' => '24',
41
- '/uu' => '24',
42
- 'b' => '1',
43
- 'blink' => '5',
44
- 'blink_off' => '25',
45
- 'bold' => '1',
46
- 'bold_off' => '22',
47
- 'conceal' => '8',
48
- 'cu' => '4:3',
49
- 'curly_underline' => '4:3',
50
- 'curly_underline_off' => '4:0',
51
- 'dashed_underline' => '4:5',
52
- 'dashed_underline_off' => '4:0',
53
- 'dau' => '4:5',
54
- 'default_font' => '10',
55
- 'dim' => '2',
56
- 'dotted_underline' => '4:4',
57
- 'dotted_underline_off' => '4:0',
58
- 'dou' => '4:4',
59
- 'double_underline' => '21',
60
- 'double_underline_off' => '24',
61
- 'encircled' => '52',
62
- 'encircled_off' => '54',
63
- 'faint' => '2',
64
- 'faint_off' => '22',
65
- 'font1' => '11',
66
- 'font2' => '12',
67
- 'font3' => '13',
68
- 'font4' => '14',
69
- 'font5' => '15',
70
- 'font6' => '16',
71
- 'font7' => '17',
72
- 'font8' => '18',
73
- 'font9' => '19',
74
- 'fraktur' => '20',
75
- 'fraktur_off' => '23',
76
- 'framed' => '51',
77
- 'framed_off' => '54',
78
- 'h' => '8',
79
- 'hide' => '8',
80
- 'hide_off' => '28',
81
- 'i' => '3',
82
- 'inv' => '7',
83
- 'invert' => '7',
84
- 'invert_off' => '27',
85
- 'italic' => '3',
86
- 'italic_off' => '23',
87
- 'overlined' => '53',
88
- 'overlined_off' => '55',
89
- 'ovr' => '53',
90
- 'primary_font' => '10',
91
- 'proportional' => '26',
92
- 'proportional_off' => '50',
93
- 'rapid_blink' => '6',
94
- 'reset' => '',
95
- 'reveal' => '28',
96
- 'slow_blink' => '5',
97
- 'spacing' => '26',
98
- 'strike' => '9',
99
- 'strike_off' => '29',
100
- 'sub' => '74',
101
- 'subscript' => '74',
102
- 'subscript_off' => '75',
103
- 'sup' => '73',
104
- 'superscript' => '73',
105
- 'superscript_off' => '75',
106
- 'u' => '4',
107
- 'underline' => '4',
108
- 'underline_off' => '24',
109
- 'uu' => '21'
110
- }.freeze
111
-
112
- @colors = {
113
- '/bg' => '49',
114
- '/fg' => '39',
115
- '/ul' => '59',
116
- 'bg_black' => '40',
117
- 'bg_blue' => '44',
118
- 'bg_bright_black' => '100',
119
- 'bg_bright_blue' => '104',
120
- 'bg_bright_cyan' => '106',
121
- 'bg_bright_green' => '102',
122
- 'bg_bright_magenta' => '105',
123
- 'bg_bright_red' => '101',
124
- 'bg_bright_white' => '107',
125
- 'bg_bright_yellow' => '103',
126
- 'bg_cyan' => '46',
127
- 'bg_default' => '49',
128
- 'bg_green' => '42',
129
- 'bg_magenta' => '45',
130
- 'bg_red' => '41',
131
- 'bg_white' => '47',
132
- 'bg_yellow' => '43',
133
- 'black' => '30',
134
- 'blue' => '34',
135
- 'bright_black' => '90',
136
- 'bright_blue' => '94',
137
- 'bright_cyan' => '96',
138
- 'bright_green' => '92',
139
- 'bright_magenta' => '95',
140
- 'bright_red' => '91',
141
- 'bright_white' => '97',
142
- 'bright_yellow' => '93',
143
- 'cyan' => '36',
144
- 'default' => '39',
145
- 'fg_black' => '30',
146
- 'fg_blue' => '34',
147
- 'fg_bright_black' => '90',
148
- 'fg_bright_blue' => '94',
149
- 'fg_bright_cyan' => '96',
150
- 'fg_bright_green' => '92',
151
- 'fg_bright_magenta' => '95',
152
- 'fg_bright_red' => '91',
153
- 'fg_bright_white' => '97',
154
- 'fg_bright_yellow' => '93',
155
- 'fg_cyan' => '36',
156
- 'fg_default' => '39',
157
- 'fg_green' => '32',
158
- 'fg_magenta' => '35',
159
- 'fg_red' => '31',
160
- 'fg_white' => '37',
161
- 'fg_yellow' => '33',
162
- 'green' => '32',
163
- 'magenta' => '35',
164
- 'on_black' => '40',
165
- 'on_blue' => '44',
166
- 'on_bright_black' => '100',
167
- 'on_bright_blue' => '104',
168
- 'on_bright_cyan' => '106',
169
- 'on_bright_green' => '102',
170
- 'on_bright_magenta' => '105',
171
- 'on_bright_red' => '101',
172
- 'on_bright_white' => '107',
173
- 'on_bright_yellow' => '103',
174
- 'on_cyan' => '46',
175
- 'on_default' => '49',
176
- 'on_green' => '42',
177
- 'on_magenta' => '45',
178
- 'on_red' => '41',
179
- 'on_white' => '47',
180
- 'on_yellow' => '43',
181
- 'red' => '31',
182
- 'ul_black' => '58;2;0;0;0',
183
- 'ul_blue' => '58;2;0;0;128',
184
- 'ul_bright_black' => '58;2;64;64;64',
185
- 'ul_bright_blue' => '58;2;0;0;255',
186
- 'ul_bright_cyan' => '58;2;0;255;255',
187
- 'ul_bright_green' => '58;2;0;255;0',
188
- 'ul_bright_magenta' => '58;2;255;0;255',
189
- 'ul_bright_red' => '58;2;255;0;0',
190
- 'ul_bright_white' => '58;2;255;255;255',
191
- 'ul_bright_yellow' => '58;2;255;255;0',
192
- 'ul_cyan' => '58;2;0;128;128',
193
- 'ul_default' => '59',
194
- 'ul_green' => '58;2;0;128;0',
195
- 'ul_magenta' => '58;2;128;0;128',
196
- 'ul_red' => '58;2;128;0;0',
197
- 'ul_white' => '58;2;128;128;128',
198
- 'ul_yellow' => '58;2;128;128;0',
199
- 'white' => '37',
200
- 'yellow' => '33'
201
- }.freeze
202
-
203
- @attr_sym = @attr.transform_keys(&:to_sym).compare_by_identity.freeze
204
- @colors_sym = @colors.transform_keys(&:to_sym).compare_by_identity.freeze
205
- end
206
- end