tty_string 1.1.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +23 -12
- data/lib/tty_string/cell.rb +16 -0
- data/lib/tty_string/code.rb +1 -1
- data/lib/tty_string/code_definitions.rb +2 -2
- data/lib/tty_string/csi_code.rb +13 -12
- data/lib/tty_string/csi_code_definitions.rb +93 -57
- data/lib/tty_string/cursor.rb +1 -1
- data/lib/tty_string/null_style.rb +15 -0
- data/lib/tty_string/parser.rb +33 -7
- data/lib/tty_string/screen.rb +31 -15
- data/lib/tty_string/style.rb +153 -0
- data/lib/tty_string/version.rb +2 -2
- data/lib/tty_string.rb +22 -16
- data/tty_string.gemspec +2 -1
- metadata +24 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 73cc746b842f0d06ee1999b3f489a6ff01dc369dab4541bc35b095fe48e040ca
|
4
|
+
data.tar.gz: 5e4ab9ee53fc27904a98e59c8f264fe14618ed324e9fc2097dd408da9614ff9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: df7fa737a041d1593609f342e060c5a92a78c17fb8b7df8658c72c803d9f8ca72a6c8137ccda4ecdfadd9f042a9652c6ec33c554ad090edc9d2aad7eef1deb03
|
7
|
+
data.tar.gz: bd1246abfe031c1206d932e30a724b64e14455c99e9d7298774dbf8c76157ec365de312f32173b0adcfed11f906100bee7ded4151c82699332255d4b0d4cdd54
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
# v2.0.0
|
2
|
+
- TTYString is now a module not a class.
|
3
|
+
- Address the issue where preserved styles and unknown codes would cause the cursor to be misaligned when moving, and potentially overwriting styles unexpectedly:
|
4
|
+
- `clear_style: false` is now `style: TTYString::RENDER`, which modifies styles to display as they would rather than just passing them through them unprocessed
|
5
|
+
- `clear_style: true` is now `style: TTYString::DROP` (and still the default)
|
6
|
+
- Unknown codes are now dropped by default
|
7
|
+
- it's now possible to set `unknown: TTYString::RAISE` to raise on unrecognized CSI codes (`unknown: TTYString::DROP` is the default)
|
8
|
+
- drop more codes that are known to do nothing for the display of text `\e[?5l`,`\e[?5h`,`\e[?25l`,`\e[?25h`,`\e[?1004l`,`\e[?1004h`,`\e[?1049l`,`\e[?1049h`
|
9
|
+
|
1
10
|
# v1.1.1
|
2
11
|
- i forgot how arity works
|
3
12
|
|
data/README.md
CHANGED
@@ -32,11 +32,20 @@ Intended for use in tests of command line interfaces.
|
|
32
32
|
| `\e[nK` | _n_=`0`: clear the line from the cursor forward <br>_n_=`1`: clear the line from the cursor backward <br>_n_=`2`: clear the line | _n_=`0` |
|
33
33
|
| `\e[nS` | scroll up _n_ rows | _n_=`1` |
|
34
34
|
| `\e[nT` | scroll down _n_ rows | _n_=`1` |
|
35
|
-
| `\e[m` | styling codes:
|
36
|
-
| `\e[?
|
37
|
-
| `\e[?
|
38
|
-
| `\e[
|
39
|
-
| `\e[
|
35
|
+
| `\e[m` | styling codes: dropped with `style: :drop` (default), rendered with `style: :render`. | |
|
36
|
+
| `\e[?5h` | reverse the screen: dropped | |
|
37
|
+
| `\e[?5l` | normal the screen: dropped | |
|
38
|
+
| `\e[?25h` | show the cursor: dropped | |
|
39
|
+
| `\e[?25l` | hide the cursor: dropped | |
|
40
|
+
| `\e[?1004h` | enable reporting focus: dropped | |
|
41
|
+
| `\e[?1004l` | disable reporting focus: dropped | |
|
42
|
+
| `\e[?1049h` | enable alternate screen buffer: dropped | |
|
43
|
+
| `\e[?1049l` | disable alternate screen buffer: dropped | |
|
44
|
+
| `\e[?2004h` | enable bracketed paste mode: dropped | |
|
45
|
+
| `\e[?2004l` | disable bracketed paste mode: dropped | |
|
46
|
+
| `\e[200~` | bracketed paste start: dropped | |
|
47
|
+
| `\e[201~` | bracketed paste end: dropped | |
|
48
|
+
| `\e[` | any other valid CSI code: dropped with `unknown: :drop` (default), raises TTYString::Error with `unknown: :raise`. | |
|
40
49
|
|
41
50
|
## Installation
|
42
51
|
|
@@ -61,14 +70,19 @@ TTYString.parse("th\ta string\e[3Gis is")
|
|
61
70
|
=> "this is a string"
|
62
71
|
```
|
63
72
|
|
64
|
-
Styling information is
|
73
|
+
Styling information is dropped by default:
|
65
74
|
```ruby
|
66
75
|
TTYString.parse("th\ta \e[31mstring\e[0m\e[3Gis is")
|
67
76
|
=> "this is a string"
|
68
77
|
```
|
69
|
-
But can be
|
78
|
+
But can be rendered:
|
70
79
|
```ruby
|
71
|
-
TTYString.parse("th\ta \e[31mstring\e[0m\e[3Gis is",
|
80
|
+
TTYString.parse("th\ta \e[31mstring\e[0m\e[3Gis is", style: :render)
|
81
|
+
=> "this is a \e[31mstring\e[0m"
|
82
|
+
```
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
TTYString.parse("th\ta \e[31mstring\e[0m\e[3Gis is", style: :render)
|
72
86
|
=> "this is a \e[31mstring\e[0m"
|
73
87
|
```
|
74
88
|
|
@@ -81,10 +95,7 @@ Just for fun TTYString.to_proc provides the `parse` method as a lambda, so:
|
|
81
95
|
## Limitations
|
82
96
|
|
83
97
|
- Various terminals are wildly variously permissive with what they accept,
|
84
|
-
so this doesn't even try to cover all possible cases
|
85
|
-
instead it covers the narrowest possible case, and leaves the codes in place when unrecognized
|
86
|
-
|
87
|
-
- `clear_style: false` treats the style codes as regular text which may work differently when rendering codes that move the cursor.
|
98
|
+
so this doesn't even try to cover all possible cases
|
88
99
|
|
89
100
|
## Development
|
90
101
|
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTYString
|
4
|
+
class Cell
|
5
|
+
attr_reader :style, :value
|
6
|
+
|
7
|
+
def initialize(value, style: NullStyle)
|
8
|
+
@style = style
|
9
|
+
@value = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s(style_context: NullStyle)
|
13
|
+
"#{style.to_s(context: style_context)}#{value}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/tty_string/code.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require_relative 'code'
|
4
4
|
require_relative 'csi_code'
|
5
5
|
|
6
|
-
|
6
|
+
module TTYString
|
7
7
|
class Code
|
8
8
|
class SlashA < TTYString::Code # leftovers:allow
|
9
9
|
char "\a"
|
@@ -32,7 +32,7 @@ class TTYString
|
|
32
32
|
def action
|
33
33
|
cursor.down
|
34
34
|
cursor.col = 0
|
35
|
-
screen.
|
35
|
+
screen.ensure_row
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
data/lib/tty_string/csi_code.rb
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
require_relative 'code'
|
4
4
|
|
5
|
-
|
5
|
+
module TTYString
|
6
6
|
class CSICode < TTYString::Code
|
7
7
|
class << self
|
8
8
|
def default_arg(value = nil)
|
9
9
|
@default_arg ||= value
|
10
|
-
@default_arg ||
|
10
|
+
@default_arg || '0'
|
11
11
|
end
|
12
12
|
|
13
13
|
private
|
@@ -19,7 +19,7 @@ class TTYString
|
|
19
19
|
def args(parser)
|
20
20
|
a = parser.matched.slice(2..-2).split(';')
|
21
21
|
a = a.slice(0, max_args) unless max_args == -1
|
22
|
-
a.map! { |n| n.empty? ? default_arg : n
|
22
|
+
a.map! { |n| n.empty? ? default_arg : n }
|
23
23
|
a
|
24
24
|
end
|
25
25
|
|
@@ -27,19 +27,14 @@ class TTYString
|
|
27
27
|
@re ||= /\e\[#{args_re}#{char}/
|
28
28
|
end
|
29
29
|
|
30
|
-
def args_re
|
30
|
+
def args_re
|
31
31
|
case max_args
|
32
|
-
when 0 then
|
33
|
-
when 1 then
|
34
|
-
|
35
|
-
else /(#{arg_re}?(;#{arg_re}){0,#{max_args - 1}})?/
|
32
|
+
when 0, 1 then /(?:[0-:<-?]*){0,#{max_args}}/
|
33
|
+
when -1 then %r{[0-?]*[ -/]*}
|
34
|
+
else /(?:(?:[0-:<-?]*)?(?:;(?:[0-:<-?]*)?){0,#{max_args - 1}})?/
|
36
35
|
end
|
37
36
|
end
|
38
37
|
|
39
|
-
def arg_re
|
40
|
-
/\d*/
|
41
|
-
end
|
42
|
-
|
43
38
|
def max_args
|
44
39
|
@max_args ||= begin
|
45
40
|
params = instance_method(:action).parameters
|
@@ -49,6 +44,12 @@ class TTYString
|
|
49
44
|
end
|
50
45
|
end
|
51
46
|
end
|
47
|
+
|
48
|
+
def integer(value)
|
49
|
+
return value.to_i if value.match?(/\A\d+\z/)
|
50
|
+
|
51
|
+
parser.unknown
|
52
|
+
end
|
52
53
|
end
|
53
54
|
end
|
54
55
|
|
@@ -2,63 +2,103 @@
|
|
2
2
|
|
3
3
|
require_relative 'csi_code'
|
4
4
|
|
5
|
-
|
5
|
+
module TTYString
|
6
6
|
class CSICode
|
7
7
|
class A < TTYString::CSICode # leftovers:allow
|
8
|
-
def action(rows = 1)
|
9
|
-
|
8
|
+
def action(rows = '1')
|
9
|
+
rows = integer(rows)
|
10
|
+
cursor.up(rows) if rows
|
10
11
|
end
|
11
12
|
end
|
12
13
|
|
13
14
|
class B < TTYString::CSICode # leftovers:allow
|
14
|
-
|
15
|
-
|
15
|
+
default_arg '1'
|
16
|
+
|
17
|
+
def action(rows = '1')
|
18
|
+
rows = integer(rows)
|
19
|
+
cursor.down(rows) if rows
|
16
20
|
end
|
17
21
|
end
|
18
22
|
|
19
23
|
class C < TTYString::CSICode # leftovers:allow
|
20
|
-
|
21
|
-
|
24
|
+
default_arg 1
|
25
|
+
|
26
|
+
def action(cols = '1')
|
27
|
+
cols = integer(cols)
|
28
|
+
cursor.right(cols) if cols
|
22
29
|
end
|
23
30
|
end
|
24
31
|
|
25
32
|
class D < TTYString::CSICode # leftovers:allow
|
26
|
-
|
27
|
-
|
33
|
+
default_arg '1'
|
34
|
+
|
35
|
+
def action(cols = '1')
|
36
|
+
cols = integer(cols)
|
37
|
+
cursor.left(cols) if cols
|
28
38
|
end
|
29
39
|
end
|
30
40
|
|
31
41
|
class E < TTYString::CSICode # leftovers:allow
|
32
|
-
|
42
|
+
default_arg '1'
|
43
|
+
|
44
|
+
def action(rows = '1')
|
45
|
+
rows = integer(rows)
|
46
|
+
return unless rows
|
47
|
+
|
33
48
|
cursor.down(rows)
|
34
49
|
cursor.col = 0
|
35
50
|
end
|
36
51
|
end
|
37
52
|
|
38
53
|
class F < TTYString::CSICode # leftovers:allow
|
39
|
-
|
54
|
+
default_arg '1'
|
55
|
+
|
56
|
+
def action(rows = '1')
|
57
|
+
rows = integer(rows)
|
58
|
+
return unless rows
|
59
|
+
|
40
60
|
cursor.up(rows)
|
41
61
|
cursor.col = 0
|
42
62
|
end
|
43
63
|
end
|
44
64
|
|
45
65
|
class G < TTYString::CSICode # leftovers:allow
|
46
|
-
|
66
|
+
default_arg '1'
|
67
|
+
|
68
|
+
def action(col = '1')
|
69
|
+
col = integer(col)
|
70
|
+
return unless col
|
71
|
+
|
47
72
|
# cursor is zero indexed, arg is 1 indexed
|
48
|
-
cursor.col = col
|
73
|
+
cursor.col = col - 1
|
49
74
|
end
|
50
75
|
end
|
51
76
|
|
52
77
|
class H < TTYString::CSICode # leftovers:allow
|
53
|
-
|
78
|
+
default_arg '1'
|
79
|
+
|
80
|
+
def action(row = '1', col = '1')
|
81
|
+
col = integer(col)
|
82
|
+
row = integer(row)
|
83
|
+
return unless col && row
|
84
|
+
|
54
85
|
# cursor is zero indexed, arg is 1 indexed
|
55
|
-
cursor.row = row
|
56
|
-
cursor.col = col
|
86
|
+
cursor.row = row - 1
|
87
|
+
cursor.col = col - 1
|
57
88
|
end
|
58
89
|
end
|
59
90
|
|
60
91
|
class LowH < TTYString::CSICode # leftovers:allow
|
61
|
-
char(
|
92
|
+
char('h')
|
93
|
+
|
94
|
+
def action(code)
|
95
|
+
case code
|
96
|
+
when '?5', '?25', '?1004', '?1049', '?2004'
|
97
|
+
# drop
|
98
|
+
else
|
99
|
+
parser.unknown
|
100
|
+
end
|
101
|
+
end
|
62
102
|
end
|
63
103
|
|
64
104
|
class LowF < TTYString::CSICode::H # leftovers:allow
|
@@ -66,74 +106,70 @@ class TTYString
|
|
66
106
|
end
|
67
107
|
|
68
108
|
class J < TTYString::CSICode # leftovers:allow
|
69
|
-
|
70
|
-
|
71
|
-
def self.arg_re
|
72
|
-
/[0-3]?/
|
73
|
-
end
|
74
|
-
|
75
|
-
def action(mode = 0)
|
76
|
-
# :nocov: else won't ever be called. don't worry about it
|
109
|
+
def action(mode = '0') # rubocop:disable Metrics/MethodLength
|
77
110
|
case mode
|
78
|
-
|
79
|
-
when
|
80
|
-
when
|
81
|
-
|
111
|
+
when '0' then screen.clear_forward
|
112
|
+
when '1' then screen.clear_backward
|
113
|
+
when '2', '3' then screen.clear
|
114
|
+
else parser.unknown
|
82
115
|
end
|
83
116
|
end
|
84
117
|
end
|
85
118
|
|
86
119
|
class K < TTYString::CSICode # leftovers:allow
|
87
|
-
|
88
|
-
|
89
|
-
def self.arg_re
|
90
|
-
/[0-2]?/
|
91
|
-
end
|
92
|
-
|
93
|
-
def action(mode = 0)
|
94
|
-
# :nocov: else won't ever be called. don't worry about it
|
120
|
+
def action(mode = '0') # rubocop:disable Metrics/MethodLength
|
95
121
|
case mode
|
96
|
-
|
97
|
-
when
|
98
|
-
when
|
99
|
-
|
122
|
+
when '0' then screen.clear_line_forward
|
123
|
+
when '1' then screen.clear_line_backward
|
124
|
+
when '2' then screen.clear_line
|
125
|
+
else parser.unknown
|
100
126
|
end
|
101
127
|
end
|
102
128
|
end
|
103
129
|
|
104
|
-
class LowL < TTYString::CSICode # leftovers:allow
|
105
|
-
char(
|
130
|
+
class LowL < TTYString::CSICode::LowH # leftovers:allow
|
131
|
+
char('l')
|
106
132
|
end
|
107
133
|
|
108
134
|
class LowM < TTYString::CSICode # leftovers:allow
|
109
135
|
char 'm'
|
110
136
|
|
111
|
-
def
|
112
|
-
|
113
|
-
/(?:\d|\d\d|1\d\d|2[0-4]\d|25[0-5])?/
|
114
|
-
end
|
115
|
-
|
116
|
-
def self.render(renderer)
|
117
|
-
super if renderer.clear_style
|
137
|
+
def action(arg = '0', *args)
|
138
|
+
screen.style(args.unshift(arg))
|
118
139
|
end
|
119
|
-
|
120
|
-
def action(*args); end
|
121
140
|
end
|
122
141
|
|
123
142
|
class S < TTYString::CSICode # leftovers:allow
|
124
|
-
def action(rows = 1)
|
125
|
-
rows
|
143
|
+
def action(rows = '1')
|
144
|
+
integer(rows)&.times { screen.scroll_up }
|
126
145
|
end
|
127
146
|
end
|
128
147
|
|
129
148
|
class T < TTYString::CSICode # leftovers:allow
|
130
|
-
def action(rows = 1)
|
131
|
-
rows
|
149
|
+
def action(rows = '1')
|
150
|
+
integer(rows)&.times { screen.scroll_down }
|
132
151
|
end
|
133
152
|
end
|
134
153
|
|
135
154
|
class Tilde < TTYString::CSICode # leftovers:allow
|
136
|
-
char(
|
155
|
+
char('~')
|
156
|
+
|
157
|
+
def action(arg)
|
158
|
+
case arg
|
159
|
+
when '200', '201'
|
160
|
+
# bracketed paste
|
161
|
+
else
|
162
|
+
parser.unknown
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class Unknown < TTYString::CSICode # leftovers:allow
|
168
|
+
char(/[@-~]/)
|
169
|
+
|
170
|
+
def action(*)
|
171
|
+
parser.unknown
|
172
|
+
end
|
137
173
|
end
|
138
174
|
end
|
139
175
|
end
|
data/lib/tty_string/cursor.rb
CHANGED
data/lib/tty_string/parser.rb
CHANGED
@@ -3,18 +3,21 @@
|
|
3
3
|
require 'strscan'
|
4
4
|
require_relative 'code_definitions'
|
5
5
|
require_relative 'csi_code_definitions'
|
6
|
-
|
6
|
+
require_relative 'style'
|
7
|
+
require_relative 'null_style'
|
7
8
|
require_relative 'screen'
|
8
9
|
|
9
|
-
|
10
|
+
module TTYString
|
10
11
|
# Reads the text string a
|
11
12
|
class Parser < StringScanner
|
12
|
-
|
13
|
-
|
13
|
+
attr_reader :style_handler, :screen
|
14
|
+
|
15
|
+
def render(style:, unknown:) # rubocop:disable Metrics/MethodLength
|
16
|
+
@style_handler = style
|
17
|
+
@unknown_handler = unknown
|
14
18
|
|
15
|
-
def render
|
16
19
|
reset
|
17
|
-
@screen = Screen.new
|
20
|
+
@screen = Screen.new(initial_style: initial_style)
|
18
21
|
read until eos?
|
19
22
|
screen.to_s
|
20
23
|
end
|
@@ -23,14 +26,37 @@ class TTYString
|
|
23
26
|
screen.cursor
|
24
27
|
end
|
25
28
|
|
29
|
+
def unknown # rubocop:disable Metrics/MethodLength
|
30
|
+
case unknown_handler
|
31
|
+
when RAISE
|
32
|
+
raise(
|
33
|
+
UnknownCodeError,
|
34
|
+
if block_given?
|
35
|
+
yield(matched)
|
36
|
+
else
|
37
|
+
"Unknown code #{matched.inspect}"
|
38
|
+
end
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def initial_style
|
44
|
+
@initial_style ||= case style_handler
|
45
|
+
when RENDER then Style.new(parser: self)
|
46
|
+
else NullStyle
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
26
50
|
private
|
27
51
|
|
52
|
+
attr_reader :unknown_handler
|
53
|
+
|
28
54
|
def write(string)
|
29
55
|
screen.write(string)
|
30
56
|
end
|
31
57
|
|
32
58
|
def read
|
33
|
-
|
59
|
+
Code.descendants.any? { |c| c.render(self) } || default
|
34
60
|
end
|
35
61
|
|
36
62
|
def default
|
data/lib/tty_string/screen.rb
CHANGED
@@ -1,29 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'cursor'
|
4
|
+
require_relative 'cell'
|
5
|
+
require_relative 'style'
|
4
6
|
|
5
|
-
|
7
|
+
module TTYString
|
6
8
|
# a grid to draw on
|
7
9
|
class Screen
|
8
10
|
attr_reader :cursor
|
9
11
|
|
10
|
-
def initialize
|
12
|
+
def initialize(initial_style:)
|
11
13
|
@cursor = Cursor.new
|
12
14
|
@screen = []
|
15
|
+
@current_style = @initial_style = initial_style
|
13
16
|
end
|
14
17
|
|
15
|
-
def to_s
|
16
|
-
|
18
|
+
def to_s # rubocop:disable Metrics/MethodLength
|
19
|
+
style_context = initial_style
|
20
|
+
screen.map do |row|
|
21
|
+
Array(row).map do |cell|
|
22
|
+
if cell
|
23
|
+
value = cell.to_s(style_context: style_context)
|
24
|
+
style_context = cell.style
|
25
|
+
value
|
26
|
+
else
|
27
|
+
' '
|
28
|
+
end
|
29
|
+
end.join.rstrip
|
30
|
+
end.join("\n") + current_style.to_s(context: style_context)
|
17
31
|
end
|
18
32
|
|
19
|
-
def []=((row, col),
|
33
|
+
def []=((row, col), value)
|
20
34
|
screen[row] ||= []
|
21
|
-
screen[row][col] = value
|
22
|
-
end
|
23
|
-
|
24
|
-
def []((row, col))
|
25
|
-
screen[row] ||= []
|
26
|
-
screen[row][col]
|
35
|
+
screen[row][col] = value
|
27
36
|
end
|
28
37
|
|
29
38
|
def clear_at_cursor
|
@@ -74,18 +83,25 @@ class TTYString
|
|
74
83
|
clear_line_forward
|
75
84
|
end
|
76
85
|
|
77
|
-
def
|
78
|
-
|
86
|
+
def ensure_row
|
87
|
+
screen[row] ||= []
|
88
|
+
end
|
79
89
|
|
90
|
+
def write(string)
|
80
91
|
string.each_char do |char|
|
81
|
-
self[cursor] = char
|
92
|
+
self[cursor] = Cell.new(char, style: current_style)
|
82
93
|
cursor.right
|
83
94
|
end
|
84
95
|
end
|
85
96
|
|
97
|
+
def style(style_codes)
|
98
|
+
self.current_style = current_style.new(style_codes)
|
99
|
+
end
|
100
|
+
|
86
101
|
private
|
87
102
|
|
88
|
-
attr_reader :screen
|
103
|
+
attr_reader :screen, :initial_style
|
104
|
+
attr_accessor :current_style
|
89
105
|
|
90
106
|
def row
|
91
107
|
cursor.row
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTYString
|
4
|
+
class Style # rubocop:disable Metrics/ClassLength
|
5
|
+
attr_reader :properties
|
6
|
+
|
7
|
+
def initialize(style_codes = ['0'], parser:, properties: {})
|
8
|
+
@properties = properties.dup
|
9
|
+
@parser = parser
|
10
|
+
parse_code(style_codes)
|
11
|
+
end
|
12
|
+
|
13
|
+
def new(style_codes)
|
14
|
+
self.class.new(style_codes, properties: properties, parser: parser)
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s(context:)
|
18
|
+
return '' if self == context
|
19
|
+
|
20
|
+
values = properties.map { |k, v| v if context.properties[k] != v }.compact.uniq
|
21
|
+
return '' if values.empty?
|
22
|
+
|
23
|
+
"\e[#{values.join(';')}m"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :parser
|
29
|
+
|
30
|
+
# delete then write to keep the order
|
31
|
+
def set(*new_properties, code)
|
32
|
+
new_properties.each do |property|
|
33
|
+
properties.delete(property)
|
34
|
+
properties[property] = code
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def slurp_color(enum, code)
|
39
|
+
case (subcode = enum.next)
|
40
|
+
when '2' then "#{code};#{subcode};#{color_param(enum)};#{color_param(enum)};#{color_param(enum)}"
|
41
|
+
when '5' then "#{code};#{subcode};#{color_param(enum)}"
|
42
|
+
else unknown(subcode)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def color_param(enum)
|
47
|
+
code = enum.next
|
48
|
+
return unknown(code) unless code.match?(/\A\d*\z/)
|
49
|
+
|
50
|
+
code_i = code.to_i
|
51
|
+
return unknown(code) unless code_i < 256
|
52
|
+
|
53
|
+
code_i
|
54
|
+
end
|
55
|
+
|
56
|
+
def unknown(style_code)
|
57
|
+
parser.unknown { |code| "Unknown style code #{style_code.inspect} in #{code.inspect}" }
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_code(style_codes) # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
|
61
|
+
enum = style_codes.each
|
62
|
+
loop do # rubocop:disable Metrics/BlockLength
|
63
|
+
case (code = enum.next)
|
64
|
+
when '0'
|
65
|
+
set(
|
66
|
+
:background,
|
67
|
+
:blink,
|
68
|
+
:bold,
|
69
|
+
:color,
|
70
|
+
:conceal,
|
71
|
+
:dim,
|
72
|
+
:encircle,
|
73
|
+
:font,
|
74
|
+
:frame,
|
75
|
+
:ideogram_overline,
|
76
|
+
:ideogram_stress,
|
77
|
+
:ideogram_underline,
|
78
|
+
:italic,
|
79
|
+
:overline,
|
80
|
+
:vertical_position,
|
81
|
+
:proportional,
|
82
|
+
:reverse,
|
83
|
+
:strike,
|
84
|
+
:underline_color,
|
85
|
+
:underline,
|
86
|
+
:double_underline_or_not_bold,
|
87
|
+
code
|
88
|
+
)
|
89
|
+
when '1'
|
90
|
+
set(:bold, code)
|
91
|
+
when '2'
|
92
|
+
set(:dim, code)
|
93
|
+
when '3'
|
94
|
+
set(:italic, code)
|
95
|
+
when '4', '24'
|
96
|
+
set(:underline, code)
|
97
|
+
when '5', '6', '25'
|
98
|
+
set(:blink, code)
|
99
|
+
when '7', '27'
|
100
|
+
set(:reverse, code)
|
101
|
+
when '8', '28'
|
102
|
+
set(:conceal, code)
|
103
|
+
when '9', '29'
|
104
|
+
set(:strike, code)
|
105
|
+
when '10'..'20'
|
106
|
+
set(:font, code)
|
107
|
+
when '21'
|
108
|
+
set(:double_underline_or_not_bold, code)
|
109
|
+
when '22'
|
110
|
+
set(:bold, :dim, code)
|
111
|
+
when '23'
|
112
|
+
set(:font, :italic, code)
|
113
|
+
when '26', '50'
|
114
|
+
set(:proportional, code)
|
115
|
+
when '30'..'37', '39', '90'..'97'
|
116
|
+
set(:color, code)
|
117
|
+
when '38'
|
118
|
+
set(:color, slurp_color(enum, code))
|
119
|
+
when '40'..'47', '49', '100'..'107'
|
120
|
+
set(:background, code)
|
121
|
+
when '48'
|
122
|
+
set(:background, slurp_color(enum, code))
|
123
|
+
when '51'
|
124
|
+
set(:frame, code)
|
125
|
+
when '52'
|
126
|
+
set(:encircle, code)
|
127
|
+
when '53', '55'
|
128
|
+
set(:overline, code)
|
129
|
+
when '54'
|
130
|
+
set(:frame, :encircle, code)
|
131
|
+
when '58'
|
132
|
+
set(:underline_color, slurp_color(enum, code))
|
133
|
+
when '59'
|
134
|
+
set(:underline_color, code)
|
135
|
+
when '60', '61'
|
136
|
+
set(:ideogram_underline, code)
|
137
|
+
when '62', '63'
|
138
|
+
set(:ideogram_overline, code)
|
139
|
+
when '64'
|
140
|
+
set(:ideogram_stress, code)
|
141
|
+
when '65'
|
142
|
+
set(:ideogram_stress, :ideogram_overline, :ideogram_underline, code)
|
143
|
+
when '73', '74', '75'
|
144
|
+
set(:vertical_position, code)
|
145
|
+
else
|
146
|
+
unknown(code)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
rescue StopIteration
|
150
|
+
# go until the end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/lib/tty_string/version.rb
CHANGED
data/lib/tty_string.rb
CHANGED
@@ -4,27 +4,33 @@ require_relative 'tty_string/parser'
|
|
4
4
|
|
5
5
|
# Renders a string taking into ANSI escape codes and \t\r\n etc
|
6
6
|
# Usage: TTYString.parse("This\r\e[KThat") => "That"
|
7
|
-
|
7
|
+
module TTYString
|
8
|
+
class Error < StandardError; end
|
9
|
+
class UnknownCodeError < Error; end
|
10
|
+
|
11
|
+
RENDER = :render
|
12
|
+
RAISE = :raise
|
13
|
+
DROP = :drop
|
14
|
+
|
15
|
+
UNKNOWN_OPTIONS = [RAISE, DROP].freeze
|
16
|
+
private_constant :UNKNOWN_OPTIONS
|
17
|
+
STYLE_OPTIONS = [RENDER, DROP].freeze
|
18
|
+
private_constant :STYLE_OPTIONS
|
19
|
+
|
8
20
|
class << self
|
9
|
-
def parse(input_string,
|
10
|
-
|
21
|
+
def parse(input_string, style: DROP, unknown: DROP) # rubocop:disable Metrics/MethodLength
|
22
|
+
unless STYLE_OPTIONS.include?(style)
|
23
|
+
raise ArgumentError, '`style:` must be either TTYString::RENDER or TTYString::DROP (default)'
|
24
|
+
end
|
25
|
+
unless UNKNOWN_OPTIONS.include?(unknown)
|
26
|
+
raise ArgumentError, '`unknown:` must be either TTYString::RAISE or TTYString::DROP (default)'
|
27
|
+
end
|
28
|
+
|
29
|
+
Parser.new(input_string).render(style: style, unknown: unknown)
|
11
30
|
end
|
12
31
|
|
13
32
|
def to_proc
|
14
33
|
method(:parse).to_proc
|
15
34
|
end
|
16
35
|
end
|
17
|
-
|
18
|
-
def initialize(input_string, clear_style: true)
|
19
|
-
@parser = Parser.new(input_string)
|
20
|
-
@parser.clear_style = clear_style
|
21
|
-
end
|
22
|
-
|
23
|
-
def to_s
|
24
|
-
parser.render
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
attr_reader :clear_style, :parser
|
30
36
|
end
|
data/tty_string.gemspec
CHANGED
@@ -30,7 +30,8 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.required_ruby_version = '>= 2.4'
|
31
31
|
spec.require_paths = ['lib']
|
32
32
|
|
33
|
-
spec.add_development_dependency '
|
33
|
+
spec.add_development_dependency 'base64'
|
34
|
+
spec.add_development_dependency 'bundler'
|
34
35
|
spec.add_development_dependency 'fast_ignore', '>= 0.15.1'
|
35
36
|
spec.add_development_dependency 'leftovers', '>= 0.2.0'
|
36
37
|
spec.add_development_dependency 'pry', '~> 0.12'
|
metadata
CHANGED
@@ -1,29 +1,42 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tty_string
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dana Sherson
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-01-25 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: base64
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :development
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
13
26
|
- !ruby/object:Gem::Dependency
|
14
27
|
name: bundler
|
15
28
|
requirement: !ruby/object:Gem::Requirement
|
16
29
|
requirements:
|
17
|
-
- - "
|
30
|
+
- - ">="
|
18
31
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
32
|
+
version: '0'
|
20
33
|
type: :development
|
21
34
|
prerelease: false
|
22
35
|
version_requirements: !ruby/object:Gem::Requirement
|
23
36
|
requirements:
|
24
|
-
- - "
|
37
|
+
- - ">="
|
25
38
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
39
|
+
version: '0'
|
27
40
|
- !ruby/object:Gem::Dependency
|
28
41
|
name: fast_ignore
|
29
42
|
requirement: !ruby/object:Gem::Requirement
|
@@ -178,7 +191,6 @@ dependencies:
|
|
178
191
|
- - ">="
|
179
192
|
- !ruby/object:Gem::Version
|
180
193
|
version: 0.8.1
|
181
|
-
description:
|
182
194
|
email:
|
183
195
|
- robot@dana.sh
|
184
196
|
executables: []
|
@@ -190,13 +202,16 @@ files:
|
|
190
202
|
- LICENSE.txt
|
191
203
|
- README.md
|
192
204
|
- lib/tty_string.rb
|
205
|
+
- lib/tty_string/cell.rb
|
193
206
|
- lib/tty_string/code.rb
|
194
207
|
- lib/tty_string/code_definitions.rb
|
195
208
|
- lib/tty_string/csi_code.rb
|
196
209
|
- lib/tty_string/csi_code_definitions.rb
|
197
210
|
- lib/tty_string/cursor.rb
|
211
|
+
- lib/tty_string/null_style.rb
|
198
212
|
- lib/tty_string/parser.rb
|
199
213
|
- lib/tty_string/screen.rb
|
214
|
+
- lib/tty_string/style.rb
|
200
215
|
- lib/tty_string/version.rb
|
201
216
|
- tty_string.gemspec
|
202
217
|
homepage: https://github.com/robotdana/tty_string
|
@@ -206,7 +221,6 @@ metadata:
|
|
206
221
|
homepage_uri: https://github.com/robotdana/tty_string
|
207
222
|
source_code_uri: https://github.com/robotdana/tty_string
|
208
223
|
changelog_uri: https://github.com/robotdana/tty_string/blob/main/CHANGELOG.md
|
209
|
-
post_install_message:
|
210
224
|
rdoc_options: []
|
211
225
|
require_paths:
|
212
226
|
- lib
|
@@ -221,8 +235,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
221
235
|
- !ruby/object:Gem::Version
|
222
236
|
version: '0'
|
223
237
|
requirements: []
|
224
|
-
rubygems_version: 3.
|
225
|
-
signing_key:
|
238
|
+
rubygems_version: 3.6.2
|
226
239
|
specification_version: 4
|
227
240
|
summary: Render a string using ANSI TTY codes
|
228
241
|
test_files: []
|