tty_string 1.1.1 → 2.0.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 +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: []
|