just-ansi 0.1.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.
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../just-ansi'
4
+
5
+ # Ruby String class ANSI extension.
6
+ class String
7
+ # Test if String contains ANSI codes.
8
+ #
9
+ # @see JustAnsi.ansi?
10
+ #
11
+ # @return [true, false] whether if attributes are found
12
+ def ansi? = JustAnsi.ansi?(self)
13
+
14
+ # Decorate self with ANSI attributes and colors.
15
+ #
16
+ # @see JustAnsi.decorate
17
+ #
18
+ # @param attributes [Array<Symbol, String>] attribute names to be used
19
+ # @param reset [true, false] whether to include reset code for ANSI attributes
20
+ # @return [String] `str` converted and decorated with the ANSI `attributes`
21
+ def ansi(*attributes, reset: true)
22
+ JustAnsi.decorate(self, *attributes, reset: reset)
23
+ end
24
+
25
+ # Remove ANSI functions, attributes and colors from self.
26
+ #
27
+ # @see JustAnsi.undecorate
28
+ #
29
+ # @return [String] string without ANSI attributes
30
+ def unansi = JustAnsi.undecorate(self)
31
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JustAnsi
4
+ # The version number of the gem.
5
+ VERSION = '0.1.1'
6
+ end
data/lib/just-ansi.rb ADDED
@@ -0,0 +1,470 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+
4
+ #
5
+ # Simple and fast ANSI control code processing.
6
+ #
7
+ module JustAnsi
8
+ class << self
9
+ # Supported attribute names.
10
+ #
11
+ # @see []
12
+ #
13
+ # @attribute [r] attributes
14
+ # @return [Array<Symbol>] all attribute names
15
+ def attributes = ATTRIBUTES_S.keys
16
+
17
+ # Supported 3/4-bit color names.
18
+ #
19
+ # @see []
20
+ #
21
+ # @attribute [r] colors
22
+ # @return [Array<Symbol>] all color names
23
+ def colors = COLORS_S.keys
24
+
25
+ # Supported basic 24-bit color names.
26
+ #
27
+ # @see []
28
+ #
29
+ # @attribute [r] named_colors
30
+ # @return [Array<Symbol>] all basic named_colors names
31
+ def named_colors = NAMED_COLORS.keys.map!(&:to_sym)
32
+
33
+ # Combine given ANSI {.attributes}, {.colors}, {.named_colors} and color
34
+ # codes.
35
+ #
36
+ # Colors can specified by their name for ANSI 3-bit and 4-bit colors.
37
+ # For 8-bit ANSI colors use 2-digit hexadecimal values `00`...`ff`.
38
+ #
39
+ # To use RGB ANSI colors (24-bit colors) specify 3-digit or 6-digit
40
+ # hexadecimal values `000`...`fff` or `000000`...`ffffff`.
41
+ # This represent the `RRGGBB` values (or `RGB` for short version) like you
42
+ # may known from CSS color notation.
43
+ #
44
+ # To use a color as background color prefix the color attribute with `bg_`
45
+ # or `on_`.
46
+ # To use a color as underline color prefix the color attribute with `ul_`.
47
+ # To clarify that a color attribute have to be used as foreground
48
+ # color use the prefix `fg_`.
49
+ #
50
+ # @example Valid Foreground Color Attributes
51
+ # JustAnsi[:yellow]
52
+ # JustAnsi[:fg_fab]
53
+ # JustAnsi[:fg_00aa00]
54
+ # JustAnsi[:af]
55
+ # JustAnsi[:fg_af]
56
+ # JustAnsi['#fab']
57
+ # JustAnsi['#00aa00']
58
+ # JustAnsi['lightblue']
59
+ #
60
+ # @example Valid Background Color Attributes
61
+ # JustAnsi[:bg_yellow]
62
+ # JustAnsi[:bg_fab]
63
+ # JustAnsi[:bg_00aa00]
64
+ # JustAnsi[:bg_af]
65
+ # JustAnsi['bg#00aa00']
66
+ # JustAnsi['bg_lightblue']
67
+ #
68
+ # JustAnsi[:on_yellow]
69
+ # JustAnsi[:on_fab]
70
+ # JustAnsi[:on_00aa00]
71
+ # JustAnsi[:on_af]
72
+ # JustAnsi['on#00aa00']
73
+ # JustAnsi['on_lightblue']
74
+ #
75
+ # @example Valid Underline Color Attributes
76
+ # JustAnsi[:underline, :ul_yellow]
77
+ # JustAnsi[:underline, :ul_fab]
78
+ # JustAnsi[:underline, :ul_00aa00]
79
+ # JustAnsi[:underline, :ul_fa]
80
+ # JustAnsi[:underline, :ul_bright_yellow]
81
+ # JustAnsi[:underline, 'ul#00aa00']
82
+ # JustAnsi['underline', 'ul_lightblue']
83
+ #
84
+ # @example Combined attributes:
85
+ # JustAnsi[:bold, :italic, :bright_white, :on_0000cc]
86
+ #
87
+ # @param attributes [Array<Symbol, String>] attribute names to be used
88
+ # @return [String] combined ANSI attributes
89
+ def [](*attributes)
90
+ return +'' if attributes.empty?
91
+ "\e[#{
92
+ attributes
93
+ .map do |arg|
94
+ case arg
95
+ when Symbol
96
+ ATTRIBUTES_S[arg] || COLORS_S[arg] || _color(arg) || _invalid(arg)
97
+ when String
98
+ ATTRIBUTES[arg] || COLORS[arg] || _color(arg) || _invalid(arg)
99
+ when (0..255)
100
+ "38;5;#{arg}"
101
+ when (256..511)
102
+ "48;5;#{arg - 256}"
103
+ when (512..767)
104
+ "58;5;#{arg - 512}"
105
+ else
106
+ _invalid(arg)
107
+ end
108
+ end
109
+ .join(';')
110
+ }m"
111
+ end
112
+
113
+ # Test if all given attributes are valid.
114
+ #
115
+ # @see []
116
+ #
117
+ # @param attributes [Array<Symbol, String>] attribute names to be used
118
+ # @return [true, false] whether if all given attributes are valid
119
+ def valid?(*attributes)
120
+ attributes.all? do |arg|
121
+ case arg
122
+ when Symbol
123
+ ATTRIBUTES_S[arg] || COLORS_S[arg] || _color(arg) || false
124
+ when String
125
+ ATTRIBUTES[arg] || COLORS[arg] || _color(arg) || false
126
+ when (0..767)
127
+ true
128
+ else
129
+ false
130
+ end
131
+ end
132
+ end
133
+
134
+ # Test if given String contains ANSI codes.
135
+ #
136
+ # @param str [#to_s] object to be tested
137
+ # @return [true, false] whether if attributes are found
138
+ def ansi?(str) = TEST.match?(str.to_s)
139
+
140
+ # Decorate given `str` with ANSI attributes and colors.
141
+ #
142
+ # @see []
143
+ # @see undecorate
144
+ #
145
+ # @param str [#to_s] object to be decorated
146
+ # @param attributes [Array<Symbol, String>] attribute names to be used
147
+ # @param reset [true, false] whether to include reset code for ANSI attributes
148
+ # @return [String] `str` converted and decorated with the ANSI `attributes`
149
+ def decorate(str, *attributes, reset: true)
150
+ attributes = self[*attributes]
151
+ attributes.empty? ? "#{str}" : "#{attributes}#{str}#{"\e[m" if reset}"
152
+ end
153
+
154
+ # Remove ANSI functions, attributes and colors from given string.
155
+ #
156
+ # @see decorate
157
+ #
158
+ # @param str [#to_s] string to be modified
159
+ # @return [String] string without ANSI attributes
160
+ def undecorate(str) = str.to_s.gsub(TEST, '')
161
+
162
+ # Try to combine given ANSI attributes and colors.
163
+ # The attributes and colors have to be seperated by given `seperator``.
164
+ #
165
+ # @example Valid Attribute String
166
+ # JustAnsi.try_convert('bold italic blink red on#00ff00')
167
+ # # => ANSI attribute string for bold, italic text which blinks red on
168
+ # # green background
169
+ #
170
+ # @example Invalid Attribute String
171
+ # JustAnsi.try_convert('cool bold on green')
172
+ # # => nil
173
+ #
174
+ # @see []
175
+ #
176
+ # @param attributes [#to_s] attributes separated by given `seperator`
177
+ # @param seperator [String] attribute seperator char
178
+ # @return [String] combined ANSI attributes
179
+ # @return [nil] when string does not contain valid attributes
180
+ def try_convert(attributes, seperator: ' ')
181
+ return unless attributes
182
+ return if (attributes = attributes.to_s.split(seperator)).empty?
183
+ "\e[#{
184
+ attributes
185
+ .map! { ATTRIBUTES[_1] || COLORS[_1] || _color(_1) || return }
186
+ .join(';')
187
+ }m"
188
+ end
189
+
190
+ # @!group Control functions
191
+
192
+ # Move cursor given lines up.
193
+ #
194
+ # @param lines [Integer] number of lines to move
195
+ # @return [String] ANSI control code
196
+ def cursor_up(lines = 1) = "\e[#{lines}A"
197
+
198
+ # Move cursor given lines down.
199
+ #
200
+ # @param (see cursor_up)
201
+ # @return (see cursor_up)
202
+ def cursor_down(lines = 1) = "\e[#{lines}B"
203
+
204
+ # Move cursor given colums forward.
205
+ #
206
+ # @param columns [Integer] number of columns to move
207
+ # @return (see cursor_up)
208
+ def cursor_forward(columns = 1) = "\e[#{columns}C"
209
+
210
+ # Move cursor given colums back.
211
+ #
212
+ # @param (see cursor_forward)
213
+ # @return (see cursor_up)
214
+ def cursor_back(columns = 1) = "\e[#{columns}D"
215
+
216
+ # Move cursor of beginning of the given next line.
217
+ #
218
+ # @param (see cursor_up)
219
+ # @return (see cursor_up)
220
+ def cursor_next_line(lines = 1) = "\e[#{lines}E"
221
+
222
+ # Move cursor of beginning of the given previous line.
223
+ #
224
+ # @param (see cursor_up)
225
+ # @return (see cursor_up)
226
+ def cursor_previous_line(lines = 1) = "\e[#{lines}F"
227
+ alias cursor_prev_line cursor_previous_line
228
+
229
+ # Move cursor to given column in the current row.
230
+ #
231
+ # @param column [Integer] column index
232
+ # @return (see cursor_up)
233
+ def cursor_column(column = 1) = "\e[#{column}G"
234
+
235
+ # Move cursor to given row and column counting from the top left corner.
236
+ #
237
+ # @param row [Integer] row index
238
+ # @param column [Integer] column index
239
+ # @return (see cursor_up)
240
+ def cursor_pos(row, column = nil)
241
+ return column ? "\e[;#{column}H" : "\e[H" unless row
242
+ column ? "\e[#{row};#{column}H" : "\e[#{row}H"
243
+ end
244
+
245
+ # Show cursor.
246
+ #
247
+ # @return (see cursor_up)
248
+ def cursor_show = +CURSOR_SHOW
249
+
250
+ # Hide cursor.
251
+ #
252
+ # @return (see cursor_up)
253
+ def cursor_hide = +CURSOR_HIDE
254
+
255
+ # Safe current cursor position.
256
+ #
257
+ # @return (see cursor_up)
258
+ def cursor_pos_safe = +CURSOR_POS_SAFE
259
+
260
+ # Restore safed cursor position.
261
+ #
262
+ # @return (see cursor_up)
263
+ def cursor_pos_restore = +CURSOR_POS_RESTORE
264
+
265
+ # Erase screen below current cursor line.
266
+ #
267
+ # @return (see cursor_up)
268
+ def screen_erase_below = _screen_erase(0)
269
+
270
+ # Erase screen above current cursor line.
271
+ #
272
+ # @return (see cursor_up)
273
+ def screen_erase_above = _screen_erase(1)
274
+
275
+ # Erase complete screen.
276
+ #
277
+ # @return (see cursor_up)
278
+ def screen_erase = _screen_erase(2)
279
+
280
+ # Erase screen scrollback buffer.
281
+ #
282
+ # @return (see cursor_up)
283
+ def screen_erase_scrollback = _screen_erase(3)
284
+
285
+ # Safe current screen.
286
+ #
287
+ # @return (see cursor_up)
288
+ def screen_save = +SCREEN_SAVE
289
+
290
+ # Restore current screen.
291
+ #
292
+ # @return (see cursor_up)
293
+ def screen_restore = +SCREEN_RESTORE
294
+
295
+ # Use alternative screen buffer.
296
+ #
297
+ # @return (see cursor_up)
298
+ def screen_alternate = +SCREEN_ALTERNATE
299
+
300
+ # Do not longer use alternative screen buffer.
301
+ #
302
+ # @return (see cursor_up)
303
+ def screen_alternate_off = +SCREEN_ALTERNATE_OFF
304
+
305
+ # Erase line from current column to end of line.
306
+ #
307
+ # @return (see cursor_up)
308
+ def line_erase_to_end = _line_erase(0)
309
+
310
+ # Erase line from current column to start of line.
311
+ #
312
+ # @return (see cursor_up)
313
+ def line_erase_to_start = _line_erase(1)
314
+
315
+ # Erase current line.
316
+ #
317
+ # @return (see cursor_up)
318
+ def line_erase = _line_erase(2)
319
+
320
+ # Scroll window given lines up.
321
+ #
322
+ # @param lines [Integer] number of lines to scroll
323
+ # @return (see cursor_up)
324
+ def scroll_up(lines = 1) = "\e[;#{lines}S"
325
+
326
+ # Scroll window given lines down.
327
+ #
328
+ # @param (see scroll_up)
329
+ # @return (see cursor_up)
330
+ def scroll_down(lines = 1) = "\e[;#{lines}T"
331
+
332
+ # Change window title.
333
+ # This is not widely supported.
334
+ #
335
+ # @param [String] title text
336
+ # @return (see cursor_up)
337
+ def window_title(title) = "\e]2;#{title}\e\\"
338
+
339
+ # Change tab title.
340
+ # This is not widely supported.
341
+ #
342
+ # @param [String] title text
343
+ # @return (see cursor_up)
344
+ def tab_title(title) = "\e]0;#{title}\a"
345
+
346
+ # Create a hyperlink.
347
+ # This is not widely supported.
348
+ def link(url, text) = "\e]8;;#{url}\e\\#{text}\e]8;;\e\\"
349
+
350
+ # @!endgroup
351
+
352
+ # @comment seems not widely supported:
353
+ # @comment doubled!? def cursor_column(column = 1) = "\e[#{column}`"
354
+ # @comment doubled!? def cursor_row(row = 1) = "\e[#{row}d"
355
+ # @comment def cursor_column_rel(columns = 1) = "\e[#{columns}a"
356
+ # @comment def cursor_row_rel(rows = 1) = "\e[#{rows}e"
357
+ # @comment def cursor_tab(count = 1) = "\e[#{column}I"
358
+ # @comment def cursor_reverse_tab(count = 1) = "\e[#{count}Z"
359
+ # @comment def line_insert(lines = 1) = "\e[#{lines}L"
360
+ # @comment def line_delete(lines = 1) = "\e[#{lines}M"
361
+ # @comment def chars_delete(count = 1) = "\e[#{count}P"
362
+ # @comment def chars_erase(count = 1) = "\e[#{count}X"
363
+
364
+ private
365
+
366
+ def _invalid(name)
367
+ raise(
368
+ ArgumentError,
369
+ "unknown ANSI attribute - #{name.inspect}",
370
+ caller(1)
371
+ )
372
+ end
373
+
374
+ def _color(str)
375
+ if /\A(?<base>fg|bg|on|ul)?_?#?(?<val>[[:xdigit:]]{1,6})\z/ =~ str
376
+ return(
377
+ case val.size
378
+ when 1, 2
379
+ "#{_color_base(base)};5;#{val.hex}"
380
+ when 3
381
+ "#{_color_base(base)};2;#{(val[0] * 2).hex};#{(val[1] * 2).hex};#{
382
+ (val[2] * 2).hex
383
+ }"
384
+ when 6
385
+ "#{_color_base(base)};2;#{val[0, 2].hex};#{val[2, 2].hex};#{
386
+ val[4, 2].hex
387
+ }"
388
+ end
389
+ )
390
+ end
391
+ if /\A(?<base>fg|bg|on|ul)?_?(?<val>[a-z]{3,}[0-9]{0,3})\z/ =~ str
392
+ val = NAMED_COLORS[val] and return "#{_color_base(base)};#{val}"
393
+ end
394
+ end
395
+
396
+ def _color_base(base)
397
+ return '48' if base == 'bg' || base == 'on'
398
+ base == 'ul' ? '58' : '38'
399
+ end
400
+
401
+ def _screen_erase(part) = "\e[#{part}J"
402
+ def _line_erase(part) = "\e[#{part}K"
403
+ end
404
+
405
+ TEST =
406
+ /\e
407
+ (?:\[[\d;:\?]*[ABCDEFGHJKSTfminsuhl])
408
+ |
409
+ (?:\]\d+(?:;[^;\a\e]+)*(?:\a|\e\\))
410
+ /x
411
+
412
+ require_relative 'just-ansi/attributes'
413
+ autoload :NAMED_COLORS, File.join(__dir__, 'just-ansi', 'named_colors')
414
+ private_constant :TEST, :NAMED_COLORS
415
+
416
+ # @!visibility private
417
+ RESET = self[:reset].freeze
418
+
419
+ # @!visibility private
420
+ CURSOR_HOME = cursor_pos(nil, nil).freeze
421
+ # @!visibility private
422
+ CURSOR_FIRST_ROW = cursor_pos(1).freeze
423
+ # @!visibility private
424
+ CURSOR_FIRST_COLUMN = cursor_column(1).freeze
425
+
426
+ # @!visibility private
427
+ CURSOR_SHOW = "\e[?25h"
428
+ # @!visibility private
429
+ CURSOR_HIDE = "\e[?25l"
430
+
431
+ # CURSOR_POS_SAFE_SCO = "\e[s"
432
+ # CURSOR_POS_SAFE_DEC = "\e7"
433
+ # @!visibility private
434
+ CURSOR_POS_SAFE = "\e7"
435
+
436
+ # CURSOR_POS_RESTORE_SCO = "\e[u"
437
+ # CURSOR_POS_RESTORE_DEC = "\e8"
438
+ # @!visibility private
439
+ CURSOR_POS_RESTORE = "\e8"
440
+
441
+ # @!visibility private
442
+ SCREEN_ERASE_BELOW = screen_erase_below.freeze
443
+ # @!visibility private
444
+ SCREEN_ERASE_ABOVE = screen_erase_above.freeze
445
+ # @!visibility private
446
+ SCREEN_ERASE = screen_erase.freeze
447
+ # @!visibility private
448
+ SCREEN_ERASE_SCROLLBACK = screen_erase_scrollback.freeze
449
+
450
+ # @!visibility private
451
+ SCREEN_SAVE = "\e[?47h"
452
+ # @!visibility private
453
+ SCREEN_RESTORE = "\e[?47l"
454
+
455
+ # @!visibility private
456
+ SCREEN_ALTERNATE = "\e[?1049h"
457
+ # @!visibility private
458
+ SCREEN_ALTERNATE_OFF = "\e[?1049l"
459
+
460
+ # @!visibility private
461
+ LINE_ERASE_TO_END = line_erase_to_end.freeze
462
+ # @!visibility private
463
+ LINE_ERASE_TO_START = line_erase_to_start.freeze
464
+ # @!visibility private
465
+ LINE_ERASE = line_erase.freeze
466
+ # @!visibility private
467
+ LINE_PREVIOUS = cursor_previous_line(1).freeze
468
+ # @!visibility private
469
+ LINE_NEXT = cursor_next_line(1).freeze
470
+ end
data/lib/just_ansi.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'just-ansi'
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: just-ansi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Mike Blumtritt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-09-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'Simple and fast ANSI control code processing.
14
+
15
+ '
16
+ email:
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files:
20
+ - README.md
21
+ - LICENSE
22
+ files:
23
+ - LICENSE
24
+ - README.md
25
+ - lib/just-ansi.rb
26
+ - lib/just-ansi/attributes.rb
27
+ - lib/just-ansi/named_colors.rb
28
+ - lib/just-ansi/string.rb
29
+ - lib/just-ansi/version.rb
30
+ - lib/just_ansi.rb
31
+ homepage: https://github.com/mblumtritt/just-ansi
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ source_code_uri: https://github.com/mblumtritt/just-ansi
36
+ bug_tracker_uri: https://github.com/mblumtritt/just-ansi/issues
37
+ documentation_uri: https://rubydoc.info/gems/just-ansi
38
+ rubygems_mfa_required: 'true'
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 3.5.18
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: Simple fast ANSI
58
+ test_files: []