terminal_rb 0.19.0 → 1.0.3

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.
data/lib/terminal/ansi.rb CHANGED
@@ -1,97 +1,67 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Terminal
4
+ # ANSI escape code generation and BBCode-to-ANSI conversion.
4
5
  #
5
- # Fast ANSI control code and BBCode processing.
6
+ # Provides methods for text decoration, color, cursor control, screen
7
+ # manipulation, hyperlinks, notifications, and progress indicators.
8
+ # All methods return ANSI escape sequence strings — they do not write
9
+ # to the terminal directly.
10
+ #
11
+ # Colors can be specified as named symbols, 256-color indices, hex
12
+ # strings, or CSS/X11 color names.
13
+ #
14
+ # @example Generate ANSI codes
15
+ # Terminal::Ansi[:bold, :red] # => "\e[1;31m"
16
+ # Terminal::Ansi[:on_blue] # => "\e[44m"
17
+ #
18
+ # @example BBCode to ANSI
19
+ # Terminal::Ansi.bbcode('[bold]Hello[/bold]') # => "\e[1mHello\e[m"
6
20
  #
7
21
  module Ansi
8
22
  class << self
9
- # Supported attribute names.
10
- #
11
- # @see []
23
+ # All known text attribute names (bold, italic, underline, etc.).
12
24
  #
13
25
  # @attribute [r] attributes
14
- # @return [Array<Symbol>] all attribute names
26
+ # @return [Array<Symbol>]
15
27
  def attributes = @attributes.dup
16
28
 
17
- # Supported 3/4-bit color names.
18
- #
19
- # @see []
29
+ # All known basic color names.
20
30
  #
21
31
  # @attribute [r] colors
22
- # @return [Array<Symbol>] all color names
32
+ # @return [Array<Symbol>]
23
33
  def colors = @colors.dup
24
34
 
25
- # Supported basic 24-bit (Kitty compatible) color names.
26
- #
27
- # @see []
35
+ # All CSS/X11 named colors.
28
36
  #
29
37
  # @attribute [r] named_colors
30
- # @return [Array<Symbol>] all basic named_colors names
38
+ # @return [Array<Symbol>]
31
39
  def named_colors = NAMED_COLORS.keys.map!(&:to_sym)
32
40
 
33
41
  #
34
- # @!group ANSI control code generator functions
35
- #
36
-
37
- # Combine given {attributes}, {colors}, {named_colors} and color codes to
38
- # an ANSI control code sequence.
39
- #
40
- # Colors can specified by their name for ANSI 3-bit and 4-bit colors.
41
- # For 8-bit ANSI colors use 2-digit hexadecimal values `00`...`ff`.
42
- #
43
- # To use RGB ANSI colors (24-bit colors) specify 3-digit or 6-digit
44
- # hexadecimal values `000`...`fff` or `000000`...`ffffff`.
45
- # This represent the `RRGGBB` values (or `RGB` for short version) like you
46
- # may known from CSS color notation.
47
- #
48
- # To use a color as background color prefix the color attribute with `bg_`
49
- # or `on_`.
50
- # To use a color as underline color prefix the color attribute with `ul_`.
51
- # To clarify that a color attribute have to be used as foreground
52
- # color use the prefix `fg_`.
53
- #
54
- # @example Valid Foreground Color Attributes
55
- # Terminal::Ansi[:yellow]
56
- # Terminal::Ansi[:fg_fab]
57
- # Terminal::Ansi[:fg_00aa00]
58
- # Terminal::Ansi[:af]
59
- # Terminal::Ansi[:fg_af]
60
- # Terminal::Ansi['#fab']
61
- # Terminal::Ansi['#00aa00']
62
- # Terminal::Ansi['lightblue']
63
- #
64
- # @example Valid Background Color Attributes
65
- # Terminal::Ansi[:bg_yellow]
66
- # Terminal::Ansi[:bg_fab]
67
- # Terminal::Ansi[:bg_00aa00]
68
- # Terminal::Ansi[:bg_af]
69
- # Terminal::Ansi['bg#00aa00']
70
- # Terminal::Ansi['bg_lightblue']
71
- #
72
- # Terminal::Ansi[:on_yellow]
73
- # Terminal::Ansi[:on_fab]
74
- # Terminal::Ansi[:on_00aa00]
75
- # Terminal::Ansi[:on_af]
76
- # Terminal::Ansi['on#00aa00']
77
- # Terminal::Ansi['on_lightblue']
78
- #
79
- # @example Valid Underline Color Attributes
80
- # Terminal::Ansi[:underline, :ul_yellow]
81
- # Terminal::Ansi[:underline, :ul_fab]
82
- # Terminal::Ansi[:underline, :ul_00aa00]
83
- # Terminal::Ansi[:underline, :ul_fa]
84
- # Terminal::Ansi[:underline, :ul_bright_yellow]
85
- # Terminal::Ansi[:underline, 'ul#00aa00']
86
- # Terminal::Ansi['underline', 'ul_lightblue']
87
- #
88
- # @example Combined attributes:
89
- # Terminal::Ansi[:bold, :italic, :bright_white, :on_0000cc]
90
- #
91
- # @see valid?
92
- #
93
- # @param attributes [Array<Symbol, String>] attribute names to be used
94
- # @return [String] combined ANSI attributes
42
+ # @!group ANSI Control Code Generation
43
+ #
44
+
45
+ # Generate an ANSI escape sequence from attribute names or color
46
+ # indices.
47
+ #
48
+ # Accepts symbols (`:bold`, +:red+), strings (`"bold"`, +"#ff0000"+),
49
+ # or integers for 256-color palette (0-255 foreground, 256-511
50
+ # background, 512-767 underline color).
51
+ #
52
+ # @example Named attributes
53
+ # Terminal::Ansi[:bold, :italic, :green] # => "\e[1;3;32m"
54
+ # @example 256-color palette
55
+ # Terminal::Ansi[196] # foreground color 196
56
+ # Terminal::Ansi[256 + 21] # background color 21
57
+ # @example Hex colors
58
+ # Terminal::Ansi[:ff8800] # => "\e[38;2;255;136;0m"
59
+ # @example Named CSS colors
60
+ # Terminal::Ansi[:coral] # => "\e[38;2;255;127;80m"
61
+ #
62
+ # @param attributes [Array<String, Symbol, Integer>] ANSI attributes
63
+ # @return [String] ANSI escape sequence
64
+ # @raise [ArgumentError] for unknown attributes
95
65
  def [](*attributes)
96
66
  return +'' if attributes.empty?
97
67
  "\e[#{
@@ -116,134 +86,155 @@ module Terminal
116
86
  }m"
117
87
  end
118
88
 
119
- # Test if given String contains ANSI control codes.
120
- #
121
- # @param str [#to_s] object to be tested
122
- # @return [true, false] whether if attributes are found
123
- def ansi?(str) = @re_test.match?(str.to_s)
124
-
125
- # Decorate given argument with ANSI attributes and colors.
89
+ # Wrap a string with ANSI escape codes.
126
90
  #
127
91
  # @example
128
- # Terminal::Ansi.decorate(
129
- # 'Hello World!',
130
- # :bold, :italic, :bright_white, :on_00c
131
- # )
132
- # # => "\e[1;3;97;48;2;0;0;204mHello World!\e[m"
133
- #
134
- # @see []
135
- # @see undecorate
136
- #
137
- # @param str [#to_s] object to be decorated
138
- # @param attributes [Array<Symbol, String>] attribute names to be used
139
- # @param reset [true, false] whether to include reset code for ANSI attributes
140
- # @return [String] `str` converted and decorated with the ANSI `attributes`
92
+ # Terminal::Ansi.decorate('Hello', :bold, :red)
93
+ # # => "\e[1;31mHello\e[m"
94
+ # @example Without reset
95
+ # Terminal::Ansi.decorate('Hello', :bold, reset: false)
96
+ # # => "\e[1mHello"
97
+ #
98
+ # @param str [#to_s] the string to decorate
99
+ # @param attributes [Array<String, Symbol, Integer>] ANSI attributes
100
+ # to apply
101
+ # @param reset [true, false] append a reset code at the end
102
+ # @return [String] the decorated string
141
103
  def decorate(str, *attributes, reset: true)
142
104
  attributes = self[*attributes]
143
105
  attributes.empty? ? "#{str}" : "#{attributes}#{str}#{"\e[m" if reset}"
144
106
  end
145
107
 
146
- # Remove ANSI functions, attributes and colors from given string.
108
+ # Remove all ANSI escape codes from a string.
147
109
  #
148
110
  # @example
149
- # Terminal::Ansi.undecorate("\e[1;3;97;48;2;0;0;204mHello World!\e[m")
150
- # # => "Hello World!"
111
+ # Terminal::Ansi.undecorate("\e[1;31mHello\e[m")
112
+ # # => "Hello"
151
113
  #
152
- # @see decorate
153
- #
154
- # @param str [#to_s] string to be modified
155
- # @return [String] string without ANSI attributes
114
+ # @param str [#to_s] the string to clean
115
+ # @return [String] a copy with all ANSI codes stripped
156
116
  def undecorate(str)
157
117
  (str = str.to_s).index("\e") ? str.gsub(@re_test, '') : str.dup
158
118
  end
159
119
 
160
- # Try to combine given ANSI attributes and colors.
161
- # The attributes and colors have to be separated by given `separator``.
162
- #
163
- # @example Valid Attribute String
164
- # Terminal::Ansi.try_convert('bold italic blink red on#00ff00')
165
- # # => "\e[1;3;5;31;48;2;0;255;0m"
166
- #
167
- # @example Invalid Attribute String
168
- # Terminal::Ansi.try_convert('cool bold on green')
169
- # # => nil
120
+ # Try to convert a space-separated attribute string to an ANSI
121
+ # escape sequence.
170
122
  #
171
- # @see []
123
+ # @example
124
+ # Terminal::Ansi.try_convert('bold red') # => "\e[1;31m"
125
+ # Terminal::Ansi.try_convert('invalid') # => nil
172
126
  #
173
- # @param attributes [#to_s] attributes separated by given `separator`
174
- # @param separator [String] attribute separator char
175
- # @return [String] combined ANSI attributes
176
- # @return [nil] when string does not contain valid attributes
127
+ # @param attributes [#to_s] space-separated attribute names
128
+ # @param separator [String] delimiter for splitting
129
+ # @return [String, nil] ANSI escape sequence, or +nil+ if any
130
+ # attribute is invalid
177
131
  def try_convert(attributes, separator: ' ')
178
132
  return unless attributes
179
133
  return if (attributes = attributes.to_s.split(separator)).empty?
180
- "\e[#{attributes.map! { @attr_map[_1] || return }.join(';')}m"
134
+ attributes.uniq!
135
+ "\e[#{attributes.map! { @attr_map[it] || return }.join(';')}m"
181
136
  end
182
137
 
183
- # Test if all given attributes are valid.
138
+ # Generate rainbow-colored text using true color (24-bit) ANSI
139
+ # codes.
184
140
  #
185
- # @see []
141
+ # @example
142
+ # puts Terminal::Ansi.rainbow('Hello, World!')
186
143
  #
187
- # @param attributes [Array<Symbol, String>] attribute names to be used
188
- # @return [true, false] whether if all given attributes are valid
189
- def valid?(*attributes)
190
- attributes.all? do |arg|
191
- case arg
192
- when String
193
- @attr_map[arg]
194
- when Symbol
195
- @attrs_map[arg]
196
- when (0..767)
197
- true
198
- end
199
- end
144
+ # @param str [#to_s] text to colorize
145
+ # @param frequency [Float] color wave frequency
146
+ # @param spread [Float] color wave spread
147
+ # @param seed [Float] starting position in the color cycle
148
+ # @return [String] text with per-character RGB foreground colors
149
+ def rainbow(str, frequency: 0.3, spread: 0.8, seed: 1.1)
150
+ pos = -1
151
+ @pi2_third ||= 2.0 * Math::PI / 3.0
152
+ @pi4_third ||= 4.0 * Math::PI / 3.0
153
+ (
154
+ str.to_s.chars.map! do |char|
155
+ i = (seed + ((pos += 1) / spread)) * frequency
156
+ "\e[38;2;#{(Math.sin(i) * 255).round.abs};#{
157
+ (Math.sin(i + @pi2_third) * 255).round.abs
158
+ };#{(Math.sin(i + @pi4_third) * 255).round.abs}m#{char}"
159
+ end << RESET_FG
160
+ ).join
200
161
  end
201
162
 
202
163
  #
203
164
  # @!endgroup
204
165
  #
205
- # @!group BBcode related functions
166
+ # @!group BBcode Generation
206
167
  #
207
168
 
208
- # Replace embedded BBCode-like attributes with ANSI control codes.
169
+ # Convert BBCode markup to ANSI escape codes.
209
170
  #
210
- # @example
211
- # Terminal::Ansi.bbcode "[b]Bold[/b] Text"
212
- # # => "\e[1mBold\e[22m Text"
171
+ # Tags like +[bold]+ are converted to their ANSI equivalents;
172
+ # +[/bold]+ or +[/]+ resets. Unknown tags are left unchanged.
173
+ # Escape a tag with a backslash: +[\\bold]+ renders as +[bold]+.
213
174
  #
214
- # @see unbbcode
215
- # @see []
175
+ # @see .unbbcode
176
+ # @see .plain
216
177
  #
217
- # @param str [#to_s] string to be modified
218
- # @return [String] string with ANSI attributes
178
+ # @example
179
+ # Terminal::Ansi.bbcode('[bold red]Hello[/] World')
180
+ # # => "\e[1;31mHello\e[m World"
181
+ # @example Escaped tag
182
+ # Terminal::Ansi.bbcode('[\\bold]') # => "[bold]"
183
+ #
184
+ # @param str [#to_s] text with BBCode markup
185
+ # @return [String] text with ANSI escape codes
219
186
  def bbcode(str)
220
- str = str.to_s
221
- return str.dup unless str.index('[')
187
+ return str.dup unless (str = str.to_s).index('[')
222
188
  str.gsub(@re_bbcode) do |match_str|
223
189
  match = Regexp.last_match(1) or next match_str
224
- next "[#{match[1..]}]" if match[0] == '\\'
225
- try_convert(match) || match_str
190
+ next try_convert(match) || match_str if match[0] != '\\'
191
+ match[0] = ''
192
+ "[#{match}]"
226
193
  end
227
194
  end
228
195
 
229
- # Remove embedded BBCode-like attributes.
196
+ # Remove BBCode tags from a string, keeping the enclosed text.
230
197
  #
231
- # @example
232
- # Terminal::Ansi.unbbcode "[b]Bold[/b] Text"
233
- # # => "Bold Text"
198
+ # @see .bbcode
234
199
  #
235
- # @see bbcode
200
+ # @example
201
+ # Terminal::Ansi.unbbcode('[bold]Hello[/bold]') # => "Hello"
236
202
  #
237
- # @param str [#to_s] string to be modified
238
- # @return [String] string without BBCode
203
+ # @param str [#to_s] text with BBCode markup
204
+ # @return [String] text with tags removed
239
205
  def unbbcode(str)
240
- str = str.to_s
241
- return str.dup unless str.index('[')
206
+ return str.dup unless (str = str.to_s).index('[')
242
207
  str.gsub(@re_bbcode) do |match_str|
243
208
  match = Regexp.last_match(1) or next match_str
244
- next "[#{match[1..]}]" if match[0] == '\\'
209
+ if match[0] == '\\'
210
+ match[0] = ''
211
+ next "[#{match}]"
212
+ end
245
213
  next match_str if (match = match.split).empty?
246
- next if match.all? { @attr_map[_1] }
214
+ next if match.all? { @attr_map[it] }
215
+ match_str
216
+ end
217
+ end
218
+
219
+ # Escape BBCode tags so they render as literal text after
220
+ # {.bbcode} processing.
221
+ #
222
+ # @see .bbcode
223
+ #
224
+ # @example
225
+ # Terminal::Ansi.escape_bbcode('[bold]Hi[/bold]')
226
+ # # => "[\\bold]Hi[\\/bold]"
227
+ #
228
+ # @param str [#to_s] text with BBCode markup
229
+ # @return [String] text with BBCode tags escaped
230
+ def escape_bbcode(str)
231
+ return str.dup unless (str = str.to_s).index('[')
232
+ str.gsub(@re_bbcode) do |match_str|
233
+ fc = match_str[1]
234
+ next match_str if fc == '\\' || fc == ']'
235
+ match = Regexp.last_match(1) or next match_str
236
+ next match_str if (parts = match.split).empty?
237
+ next "[\\#{match}]" if parts.all? { @attr_map[it] }
247
238
  match_str
248
239
  end
249
240
  end
@@ -251,120 +242,124 @@ module Terminal
251
242
  #
252
243
  # @!endgroup
253
244
  #
254
- # @!group Other tool functions
245
+ # @!group Tool Functions
246
+ #
247
+
248
+ # Test whether a string contains ANSI escape codes.
249
+ #
250
+ # @example
251
+ # Terminal::Ansi.ansi?("\e[1mHi\e[m") # => true
252
+ # Terminal::Ansi.ansi?("Hello") # => false
255
253
  #
254
+ # @param str [#to_s] the string to test
255
+ # @return [true, false]
256
+ def ansi?(str) = @re_test.match?(str.to_s)
256
257
 
257
- # Remove any BBCode-like and/or ANSI attributes.
258
+ # Check whether all given attributes are valid.
258
259
  #
259
- # @see undecorate
260
- # @see unbbcode
260
+ # @example
261
+ # Terminal::Ansi.valid?(:bold, :red) # => true
262
+ # Terminal::Ansi.valid?(:nope) # => false
261
263
  #
262
- # @param str [#to_s] string to be modified
263
- # @return [String] string without BBCode and ANSI control codes.
264
- def plain(str)
265
- unless (str = str.to_s).index('[')
266
- return str.index("\e") ? str.gsub(@re_test, '') : str.dup
267
- end
268
- str =
269
- str.gsub(@re_bbcode) do |match_str|
270
- match = Regexp.last_match(1) or next match_str
271
- next "[#{match[1..]}]" if match[0] == '\\'
272
- next match_str if (match = match.split).empty?
273
- next if match.all? { @attr_map[_1] }
274
- match_str
264
+ # @param attributes [Array<String, Symbol, Integer>] attributes to
265
+ # validate
266
+ # @return [true, false]
267
+ def valid?(*attributes)
268
+ attributes.all? do |arg|
269
+ case arg
270
+ when String
271
+ @attr_map[arg]
272
+ when Symbol
273
+ @attrs_map[arg]
274
+ when (0..767)
275
+ true
275
276
  end
276
- str.index("\e") ? str.gsub!(@re_test, '') : str
277
+ end
277
278
  end
278
279
 
279
- # Create nice colored text.
280
+ # Remove both BBCode tags and ANSI escape codes, returning plain
281
+ # text.
280
282
  #
281
- # @param str [#to_s] string to enrich with color
282
- # @param frequency [Float] color change frequency
283
- # @param spread [Float] number of chars with same color
284
- # @param seed [Float] start index on sinus curve
285
- # @return [String] fancy text
286
- def rainbow(str, frequency: 0.3, spread: 0.8, seed: 1.1)
287
- pos = -1
288
- @pi2_third ||= 2.0 * Math::PI / 3.0
289
- @pi4_third ||= 4.0 * Math::PI / 3.0
290
- (
291
- str.to_s.chars.map! do |char|
292
- i = (seed + ((pos += 1) / spread)) * frequency
293
- "\e[38;2;#{(Math.sin(i) * 255).to_i.abs};" \
294
- "#{(Math.sin(i + @pi2_third) * 255).to_i.abs};" \
295
- "#{(Math.sin(i + @pi4_third) * 255).to_i.abs}m#{char}"
296
- end << RESET
297
- ).join
283
+ # @example
284
+ # Terminal::Ansi.plain('[bold]Hello[/bold]') # => "Hello"
285
+ # Terminal::Ansi.plain("\e[1mHello\e[m") # => "Hello"
286
+ #
287
+ # @param str [#to_s] text with BBCode and/or ANSI codes
288
+ # @return [String] plain text
289
+ def plain(str)
290
+ str.gsub!(@re_test, '') if (str = unbbcode(str)).index("\e")
291
+ str
298
292
  end
299
293
 
300
294
  #
301
295
  # @!endgroup
302
296
  #
303
- # @!group Cursor manipulation
297
+ # @!group ANSI Control Code Generation
304
298
  #
305
299
 
306
- # Move cursor given lines up.
300
+ # Move cursor up.
307
301
  #
308
- # @param lines [Integer] number of lines to move
309
- # @return [String] ANSI control code
302
+ # @param lines [Integer] number of lines
303
+ # @return (see .[])
310
304
  def cursor_up(lines = 1) = "\e[#{lines}A"
311
305
 
312
- # Move cursor given lines down.
306
+ # Move cursor down.
313
307
  #
314
- # @param (see cursor_up)
315
- # @return (see cursor_up)
308
+ # @param (see .cursor_up)
309
+ # @return (see .[])
316
310
  def cursor_down(lines = 1) = "\e[#{lines}B"
317
311
 
318
- # Move cursor given columns forward.
312
+ # Move cursor right.
319
313
  #
320
- # @param columns [Integer] number of columns to move
321
- # @return (see cursor_up)
314
+ # @param columns [Integer] number of columns
315
+ # @return (see .[])
322
316
  def cursor_forward(columns = 1) = "\e[#{columns}C"
323
317
 
324
- # Move cursor given columns back.
318
+ # Move cursor left.
325
319
  #
326
- # @param (see forward)
327
- # @return (see cursor_up)
320
+ # @param (see .cursor_forward)
321
+ # @return (see .[])
328
322
  def cursor_back(columns = 1) = "\e[#{columns}D"
329
323
 
330
- # Move cursor to the beginning of the given next line.
324
+ # Move cursor to beginning of line, N lines down.
331
325
  #
332
- # @param (see up)
333
- # @return (see cursor_up)
326
+ # @param (see .cursor_up)
327
+ # @return (see .[])
334
328
  def cursor_next_line(lines = 1) = "\e[#{lines}E"
335
329
 
336
- # Move cursor to the beginning of the given previous line.
330
+ # Move cursor to beginning of line, N lines up.
337
331
  #
338
- # @param (see up)
339
- # @return (see cursor_up)
332
+ # @param (see .cursor_up)
333
+ # @return (see .[])
340
334
  def cursor_prev_line(lines = 1) = "\e[#{lines}F"
341
335
 
342
- # Move cursor to given column in the current row.
336
+ # Move cursor to absolute column.
343
337
  #
344
- # @param column [Integer] column index
345
- # @return (see cursor_up)
338
+ # @param column [Integer] column number
339
+ # @return (see .[])
346
340
  def cursor_column(column = 1) = "\e[#{column}G"
347
341
 
348
- # Move cursor to given column in the current row relative to the current
349
- # position.
350
- # (Skip some columns.)
342
+ # Move cursor right by relative columns.
351
343
  #
352
- # @param (see cursor_column)
353
- # @return (see cursor_up)
344
+ # @param column [Integer] number of columns
345
+ # @return (see .[])
354
346
  def cursor_column_rel(column = 1) = "\e[#{column}a"
355
347
 
356
- # Move cursor to given row relative to the current position.
357
- # (Skip some rows.)
348
+ # Move cursor down by relative rows.
358
349
  #
359
- # @param row [Integer] row index
360
- # @return (see cursor_up)
350
+ # @param row [Integer] number of rows
351
+ # @return (see .[])
361
352
  def cursor_row_rel(row = 1) = "\e[#{row}e"
362
353
 
363
- # Move to given row and column.
354
+ # Move cursor to absolute position.
355
+ #
356
+ # @example
357
+ # Terminal::Ansi.cursor_pos(1, 1) # home position
358
+ # Terminal::Ansi.cursor_pos(10, 5) # row 10, column 5
364
359
  #
365
- # @param row [Integer] row index
366
- # @param column [Integer] column index
367
- # @return (see cursor_up)
360
+ # @param row [Integer, nil] row number; +nil+ moves to home
361
+ # @param column [Integer, nil] column number
362
+ # @return (see .[])
368
363
  def cursor_pos(row, column = nil)
369
364
  return column ? "\e[;#{column}H" : "\e[H" unless row
370
365
  column ? "\e[#{row};#{column}H" : "\e[#{row}H"
@@ -372,142 +367,162 @@ module Terminal
372
367
 
373
368
  # Show cursor.
374
369
  #
375
- # @return (see cursor_up)
370
+ # @return (see .[])
376
371
  def cursor_show = +CURSOR_SHOW
377
372
 
378
373
  # Hide cursor.
379
374
  #
380
- # @return (see cursor_up)
375
+ # @return (see .[])
381
376
  def cursor_hide = +CURSOR_HIDE
382
377
 
383
378
  # Save current cursor position.
384
379
  #
385
- # @return (see cursor_up)
386
- def cursor_save_pos = +CURSOR_POS_SAVE
387
-
388
- # Restore saved cursor position.
380
+ # @see .cursor_restore_pos
389
381
  #
390
- # @return (see cursor_up)
391
- def cursor_restore_pos = +CURSOR_POS_RESTORE
382
+ # @return (see .[])
383
+ def cursor_save_pos = +CURSOR_POS_SAVE
392
384
 
385
+ # Restore previously saved cursor position.
393
386
  #
394
- # @!endgroup
395
- #
396
- # @!group Screen manipulation
387
+ # @see .cursor_save_pos
397
388
  #
389
+ # @return (see .[])
390
+ def cursor_restore_pos = +CURSOR_POS_RESTORE
398
391
 
399
- # Erase screen part.
392
+ # Erase part of the screen.
400
393
  #
401
- # @param part [:below, :above, :all, :scrollback] screen part to erase
402
- # @return (see cursor_up)
394
+ # @param part [Symbol] area to erase:
395
+ # +:all+, +:below+, +:above+, or +:scrollback+
396
+ # @return (see .[])
403
397
  def screen_erase(part = :all) = "\e[#{@screen_erase[part]}J"
404
398
 
405
- # Safe current screen.
399
+ # Save screen state.
400
+ #
401
+ # @see .screen_restore
406
402
  #
407
- # @return (see cursor_up)
403
+ # @return (see .[])
408
404
  def screen_save = +SCREEN_SAVE
409
405
 
410
- # Restore current screen.
406
+ # Restore previously saved screen state.
411
407
  #
412
- # @return (see cursor_up)
408
+ # @see .screen_save
409
+ #
410
+ # @return (see .[])
413
411
  def screen_restore = +SCREEN_RESTORE
414
412
 
415
- # Use alternative screen buffer.
413
+ # Switch to the alternate screen buffer.
414
+ #
415
+ # @see .screen_alternate_off
416
416
  #
417
- # @return (see cursor_up)
417
+ # @return (see .[])
418
418
  def screen_alternate = +SCREEN_ALTERNATE
419
419
 
420
- # Do not longer use alternative screen buffer.
420
+ # Switch back from the alternate screen buffer.
421
+ #
422
+ # @see .screen_alternate
421
423
  #
422
- # @return (see cursor_up)
424
+ # @return (see .[])
423
425
  def screen_alternate_off = +SCREEN_ALTERNATE_OFF
424
426
 
425
- # Scroll window given lines up.
427
+ # Scroll the screen up.
426
428
  #
427
- # @param lines [Integer] number of lines to scroll
428
- # @return (see cursor_up)
429
+ # @param (see .cursor_up)
430
+ # @return (see .[])
429
431
  def screen_scroll_up(lines = 1) = "\e[#{lines}S"
430
432
 
431
- # Scroll window given lines down.
433
+ # Scroll the screen down.
432
434
  #
433
- # @param (see scroll_up)
434
- # @return (see cursor_up)
435
+ # @param (see .cursor_up)
436
+ # @return (see .[])
435
437
  def screen_scroll_down(lines = 1) = "\e[#{lines}T"
436
438
 
439
+ # Repeat the last printed character.
437
440
  #
438
- # @!endgroup
441
+ # @param count [Integer] number of repetitions
442
+ # @return (see .[])
443
+ def char_repeat(count = 1) = "\e[#{count}b"
444
+
445
+ # Erase part of the current line.
446
+ #
447
+ # @param part [Symbol] area to erase:
448
+ # +:all+, +:to_end+, or +:to_start+
449
+ # @return (see .[])
450
+ def line_erase(part = :all) = "\e[#{@line_erase[part]}K"
451
+
452
+ # Set the terminal window title.
453
+ # @note Supported by Hyper, iTerm2, Kitty, macOS Terminal, Tabby,
454
+ # WezTerm.
439
455
  #
440
- # @!group Other ANSI control functions
456
+ # @example
457
+ # Terminal.raw_write(Terminal::Ansi.title('My App'))
441
458
  #
459
+ # @param title [String] the title text
460
+ # @return (see .[])
461
+ def title(title) = "\e]0;#{title}\a"
442
462
 
443
- # Repeat last char.
463
+ # Start a hyperlink (OSC 8).
444
464
  #
445
- # @param count [Integer] repeat count
446
- # @return (see cursor_up)
447
- def char_repeat(count = 1) = "\e[#{count}b"
465
+ # @note Supported by Ghostty, iTerm2, Kitty, Rio, Tabby, WezTerm.
466
+ #
467
+ # @see .link
468
+ # @see .link_end
469
+ #
470
+ # @param url [#to_s] the link URL
471
+ # @param params [Hash] optional link parameters (e.g., +id:+)
472
+ # @return (see .[])
473
+ def link_start(url, **params)
474
+ "\e]8;#{params.map { it.join('=') }.join(':')};#{url}\a"
475
+ end
448
476
 
449
- # Erase part of line.
477
+ # End a hyperlink.
450
478
  #
451
- # @param part [:to_end, :to_start, :all] line part to erase
452
- # @return (see cursor_up)
453
- def line_erase(part = :all) = "\e[#{@line_erase[part]}K"
479
+ # @see .link_start
480
+ #
481
+ # @return (see .[])
482
+ def link_end = +LINK_END
454
483
 
455
- # Set (tab) title.
456
- # This is not widely supported; works for
457
- # Hyper,
458
- # iTerm2,
459
- # Kitty,
460
- # MacOS Terminal,
461
- # Tabby,
462
- # WezTerm.
463
- #
464
- # @param title [#to_s] text
465
- # @return (see cursor_up)
466
- def title(title) = "\e]0;#{title}\a"
484
+ # Create a complete hyperlink (OSC 8).
485
+ #
486
+ # @see .link_start
487
+ # @see .link_end
488
+ #
489
+ # @example
490
+ # Terminal::Ansi.link('https://example.com', 'Example')
491
+ #
492
+ # @param (see .link_start)
493
+ # @param text [String] the visible link text
494
+ # @return (see .[])
495
+ def link(url, text, **params)
496
+ "#{link_start(url, **params)}#{text}#{LINK_END}"
497
+ end
467
498
 
468
- # Create a hyperlink.
469
- # This is not widely supported; works for
470
- # Ghostty,
471
- # iTerm2,
472
- # Kitty,
473
- # Rio,
474
- # Tabby,
475
- # WezTerm.
476
- #
477
- # @param url [#to_s] URL to link to
478
- # @param text [#to_s] text to display for the link
479
- # @return (see cursor_up)
480
- def link(url, text) = "\e]8;;#{url}\a#{text}\e]8;;\a"
481
-
482
- # Show a simple notification.
483
- # This is not widely supported; works for
484
- # Ghostty,
485
- # iTerm2,
486
- # Kitty,
487
- # WezTerm.
488
- #
489
- # @param text [#to_s] text to display
490
- # @return (see cursor_up)
499
+ # Send a desktop notification via the terminal.
500
+ # @note Supported by Ghostty, iTerm2, Kitty, WezTerm.
501
+ #
502
+ # @example
503
+ # Terminal.raw_write(Terminal::Ansi.notify('Build complete!'))
504
+ #
505
+ # @param text [to_s] the notification text
506
+ # @return (see .[])
491
507
  def notify(text) = "\e]9;#{text}\a"
492
508
 
493
- # Set task progress state.
494
- # This is not widely supported; works for
495
- # Ghostty,
496
- # iTerm2,
497
- # Kitty.
498
- #
499
- # @param state [:show, :warning, :error, :indeterminate, :hide]
500
- # - `:show`
501
- # show progress indicator with given percent value in default mode
502
- # - `:warning`
503
- # show progress with given percent value in warning mode
504
- # - `:error`
505
- # show progress with given percent value in error mode
506
- # - `:indeterminate`
507
- # show progress in indeterminate state (percent value is ignored)
508
- # - `:hide`
509
- # hide progress indicator (percent value is ignored)
510
- # @return (see cursor_up)
509
+ # Show or update a progress indicator in the terminal tab/title
510
+ # bar.
511
+ #
512
+ # @note Supported by Ghostty, iTerm2, Kitty.
513
+ #
514
+ # @example
515
+ # Terminal.raw_write(Terminal::Ansi.progress(50)) # 50%
516
+ # Terminal.raw_write(Terminal::Ansi.progress(:error)) # error state
517
+ # Terminal.raw_write(Terminal::Ansi.progress(nil)) # hide
518
+ #
519
+ # @param state [Symbol, Numeric, Boolean] progress state:
520
+ # - +:show+ or +true+ to show, `:err`/`:error`, `:warn`/`:warning`,
521
+ # +:indeterminate+, a +Numeric+ (0-100) to set percentage, or
522
+ # any other value to hide
523
+ # @param percent [Integer] progress percentage (0-100, used with
524
+ # +:show+ state)
525
+ # @return (see .[])
511
526
  def progress(state, percent = 0)
512
527
  case state
513
528
  when :show, true
@@ -526,32 +541,28 @@ module Terminal
526
541
  "\e]9;4;#{state};#{percent.to_i.clamp(0, 100)}\a"
527
542
  end
528
543
 
529
- # Create scaled text.
530
- # It uses the
544
+ # Scale text using the
531
545
  # [text sizing protocol](https://sw.kovidgoyal.net/kitty/text-sizing-protocol).
532
- # This is not widely supported; works for Kitty.
546
+ #
547
+ # @note Only supported by Kitty.
533
548
  #
534
549
  # @example Double-height Greeting
535
550
  # Terminal::Ansi.scale('Hello Ruby!', scale: 2)
536
551
  #
537
552
  # @example Half-height Greeting
538
- # Terminal::Ansi.scale('Hello Ruby!', fracn: 1, fracd: 2, vertical: :centered)
539
- #
540
- # @param text [#to_s]
541
- # text to scale
542
- # @param scale [Integer, nil]
543
- # overall scale size, range 1..7
544
- # @param width [Integer, nil]
545
- # with in cells, range 0..7
546
- # @param fracn [Integer, nil]
547
- # numerator for the fractional scale, range: 0..15
548
- # @param fracd [Integer, nil]
549
- # denominator for the fractional scale, range: 0..15, > fracn
550
- # @param vertical [:top, :bottom, :centered, nil]
551
- # vertical alignment to use for fractionally scaled text
552
- # @param horizontal [:left, :right, :centered, nil]
553
- # horizontal alignment to use for fractionally scaled text
554
- # @return (see cursor_up)
553
+ # Terminal::Ansi.scale('Hello Ruby!', fracn: 1, fracd: 2, vertical: :middle)
554
+ #
555
+ # @param text [#to_s] text to scale
556
+ # @param scale [Integer, nil] scale multiplier (1-7)
557
+ # @param width [Integer, nil] width mode (0-7)
558
+ # @param fracn [Integer, nil] fractional numerator (0-15)
559
+ # @param fracd [Integer, nil] fractional denominator
560
+ # (must be > fracn, max 15)
561
+ # @param vertical [Symbol, Integer, nil] vertical alignment:
562
+ # +:top+, +:bottom+, +:middle+
563
+ # @param horizontal [Symbol, Integer, nil] horizontal alignment:
564
+ # +:left+, +:right+, +:center+
565
+ # @return (see .[])
555
566
  def scale(
556
567
  text,
557
568
  scale: nil,
@@ -571,7 +582,7 @@ module Terminal
571
582
  opts << 'v=0'
572
583
  when 1, :bottom
573
584
  opts << 'v=1'
574
- when 2, :centered
585
+ when 2, :middle
575
586
  opts << 'v=2'
576
587
  end
577
588
  case horizontal
@@ -579,7 +590,7 @@ module Terminal
579
590
  opts << 'h=0'
580
591
  when 1, :right
581
592
  opts << 'h=1'
582
- when 2, :centered
593
+ when 2, :center
583
594
  opts << 'h=2'
584
595
  end
585
596
  end
@@ -601,10 +612,6 @@ module Terminal
601
612
  end
602
613
  end
603
614
 
604
- @cbase = { 'bg' => '48', 'on' => '48', 'ul' => '58' }
605
- @cbase.default = '38'
606
- @cbase.freeze
607
-
608
615
  @re_test =
609
616
  /
610
617
  (?:\e\[[\x30-\x3f]*[\x20-\x2f]*[a-zA-Z])
@@ -616,139 +623,128 @@ module Terminal
616
623
 
617
624
  clr_map = {
618
625
  # foreground
619
- '30' => 'black',
620
- '31' => 'red',
621
- '32' => 'green',
622
- '33' => 'yellow',
623
- '34' => 'blue',
624
- '35' => 'magenta',
625
- '36' => 'cyan',
626
- '37' => 'white',
627
- '39' => 'default', # reset
626
+ 'black' => '30',
627
+ 'red' => '31',
628
+ 'green' => '32',
629
+ 'yellow' => '33',
630
+ 'blue' => '34',
631
+ 'magenta' => '35',
632
+ 'cyan' => '36',
633
+ 'white' => '37',
634
+ 'default' => '39',
628
635
  # background
629
- '40' => 'on_black',
630
- '41' => 'on_red',
631
- '42' => 'on_green',
632
- '43' => 'on_yellow',
633
- '44' => 'on_blue',
634
- '45' => 'on_magenta',
635
- '46' => 'on_cyan',
636
- '47' => 'on_white',
637
- '49' => 'on_default', # reset
636
+ 'on_black' => '40',
637
+ 'on_red' => '41',
638
+ 'on_green' => '42',
639
+ 'on_yellow' => '43',
640
+ 'on_blue' => '44',
641
+ 'on_magenta' => '45',
642
+ 'on_cyan' => '46',
643
+ 'on_white' => '47',
644
+ 'on_default' => '49',
638
645
  # underline
639
- '58;2;0;0;0' => 'ul_black',
640
- '58;2;0;0;128' => 'ul_blue',
641
- '58;2;0;0;255' => 'ul_bright_blue',
642
- '58;2;0;128;0' => 'ul_green',
643
- '58;2;0;128;128' => 'ul_cyan',
644
- '58;2;0;255;0' => 'ul_bright_green',
645
- '58;2;0;255;255' => 'ul_bright_cyan',
646
- '58;2;128;0;0' => 'ul_red',
647
- '58;2;128;0;128' => 'ul_magenta',
648
- '58;2;128;128;0' => 'ul_yellow',
649
- '58;2;128;128;128' => 'ul_white',
650
- '58;2;255;0;0' => 'ul_bright_red',
651
- '58;2;255;0;255' => 'ul_bright_magenta',
652
- '58;2;255;255;0' => 'ul_bright_yellow',
653
- '58;2;255;255;255' => 'ul_bright_white',
654
- '58;2;64;64;64' => 'ul_bright_black',
655
- '59' => 'ul_default', # reset
646
+ 'ul_black' => '58;2;0;0;0',
647
+ 'ul_blue' => '58;2;0;0;128',
648
+ 'ul_bright_blue' => '58;2;0;0;255',
649
+ 'ul_green' => '58;2;0;128;0',
650
+ 'ul_cyan' => '58;2;0;128;128',
651
+ 'ul_bright_green' => '58;2;0;255;0',
652
+ 'ul_bright_cyan' => '58;2;0;255;255',
653
+ 'ul_red' => '58;2;128;0;0',
654
+ 'ul_magenta' => '58;2;128;0;128',
655
+ 'ul_yellow' => '58;2;128;128;0',
656
+ 'ul_white' => '58;2;128;128;128',
657
+ 'ul_bright_red' => '58;2;255;0;0',
658
+ 'ul_bright_magenta' => '58;2;255;0;255',
659
+ 'ul_bright_yellow' => '58;2;255;255;0',
660
+ 'ul_bright_white' => '58;2;255;255;255',
661
+ 'ul_bright_black' => '58;2;64;64;64',
662
+ 'ul_default' => '59',
656
663
  # bright foreground
657
- '90' => 'bright_black',
658
- '91' => 'bright_red',
659
- '92' => 'bright_green',
660
- '93' => 'bright_yellow',
661
- '94' => 'bright_blue',
662
- '95' => 'bright_magenta',
663
- '96' => 'bright_cyan',
664
- '97' => 'bright_white',
664
+ 'bright_black' => '90',
665
+ 'bright_red' => '91',
666
+ 'bright_green' => '92',
667
+ 'bright_yellow' => '93',
668
+ 'bright_blue' => '94',
669
+ 'bright_magenta' => '95',
670
+ 'bright_cyan' => '96',
671
+ 'bright_white' => '97',
665
672
  # bright background
666
- '100' => 'on_bright_black',
667
- '101' => 'on_bright_red',
668
- '102' => 'on_bright_green',
669
- '103' => 'on_bright_yellow',
670
- '104' => 'on_bright_blue',
671
- '105' => 'on_bright_magenta',
672
- '106' => 'on_bright_cyan',
673
- '107' => 'on_bright_white'
674
- }.invert
675
- clr_alias = ->(t, s) { clr_map[t] = clr_map[s] }
676
-
677
- %w[black red green yellow blue magenta cyan white].each do |name|
678
- clr_alias["fg_#{name}", name]
679
- clr_alias["bg_#{name}", "on_#{name}"]
680
- clr_alias["fg_bright_#{name}", "bright_#{name}"]
681
- clr_alias["bg_bright_#{name}", "on_bright_#{name}"]
682
- end
683
- clr_alias['fg_default', 'default']
684
- clr_alias['bg_default', 'on_default']
673
+ 'on_bright_black' => '100',
674
+ 'on_bright_red' => '101',
675
+ 'on_bright_green' => '102',
676
+ 'on_bright_yellow' => '103',
677
+ 'on_bright_blue' => '104',
678
+ 'on_bright_magenta' => '105',
679
+ 'on_bright_cyan' => '106',
680
+ 'on_bright_white' => '107'
681
+ }
682
+
683
+ @colors = clr_map.keys.map!(&:to_sym).sort!.freeze
685
684
 
686
685
  attr_map = {
687
- '' => 'reset',
688
- '1' => 'bold',
689
- '2' => 'faint',
690
- '3' => 'italic',
691
- '4' => 'underline',
692
- '5' => 'blink',
693
- '6' => 'rapid_blink',
694
- '7' => 'invert',
695
- '8' => 'hide',
696
- '9' => 'strike',
697
- '10' => 'primary_font',
698
- '11' => 'font1',
699
- '12' => 'font2',
700
- '13' => 'font3',
701
- '14' => 'font4',
702
- '15' => 'font5',
703
- '16' => 'font6',
704
- '17' => 'font7',
705
- '18' => 'font8',
706
- '19' => 'font9',
707
- '20' => 'fraktur',
708
- '21' => 'double_underline',
709
- '22' => 'bold_off', # faint_off
710
- '23' => 'italic_off', # fraktur_off
711
- '24' => 'underline_off', # double_underline_off
712
- '25' => 'blink_off', # rapid_blink_off
713
- '26' => 'proportional',
714
- '27' => 'invert_off',
715
- '28' => 'hide_off',
716
- '29' => 'strike_off',
717
- # colors ...
718
- '50' => 'proportional_off',
719
- '51' => 'framed',
720
- '52' => 'encircled',
721
- '53' => 'overlined',
722
- '54' => 'framed_off', # encircled_off
723
- '55' => 'overlined_off',
686
+ 'reset' => '',
687
+ 'bold' => '1',
688
+ 'faint' => '2',
689
+ 'italic' => '3',
690
+ 'underline' => '4',
691
+ 'blink' => '5',
692
+ 'rapid_blink' => '6',
693
+ 'invert' => '7',
694
+ 'hide' => '8',
695
+ 'strike' => '9',
696
+ 'primary_font' => '10',
697
+ 'font1' => '11',
698
+ 'font2' => '12',
699
+ 'font3' => '13',
700
+ 'font4' => '14',
701
+ 'font5' => '15',
702
+ 'font6' => '16',
703
+ 'font7' => '17',
704
+ 'font8' => '18',
705
+ 'font9' => '19',
706
+ 'fraktur' => '20',
707
+ 'double_underline' => '21',
708
+ 'bold_off' => '22',
709
+ 'faint_off' => '22',
710
+ 'italic_off' => '23',
711
+ 'fraktur_off' => '23',
712
+ 'underline_off' => '24',
713
+ 'double_underline_off' => '24',
714
+ 'blink_off' => '25',
715
+ 'rapid_blink_off' => '25',
716
+ 'proportional' => '26',
717
+ 'invert_off' => '27',
718
+ 'hide_off' => '28',
719
+ 'strike_off' => '29',
720
+ # ...
721
+ 'proportional_off' => '50',
722
+ 'framed' => '51',
723
+ 'encircled' => '52',
724
+ 'overlined' => '53',
725
+ 'framed_off' => '54',
726
+ 'encircled_off' => '54',
727
+ 'overlined_off' => '55',
724
728
  # ...
725
- '73' => 'superscript',
726
- '74' => 'subscript',
727
- '75' => 'superscript_off', # subscript_off
729
+ 'superscript' => '73',
730
+ 'subscript' => '74',
731
+ 'superscript_off' => '75',
732
+ 'subscript_off' => '75',
728
733
  # special underline
729
- '4:3' => 'curly_underline',
730
- '4:4' => 'dotted_underline',
731
- '4:5' => 'dashed_underline',
732
- '4:0' => 'curly_underline_off' # dotted_underline_off, dashed_underline_off
733
- }.invert
734
- attr_alias = ->(t, s) { attr_map[t] = attr_map[s] }
734
+ 'curly_underline' => '4:3',
735
+ 'dotted_underline' => '4:4',
736
+ 'dashed_underline' => '4:5',
737
+ 'curly_underline_off' => '4:0',
738
+ 'dotted_underline_off' => '4:0',
739
+ 'dashed_underline_off' => '4:0'
740
+ }
735
741
 
736
- attr_alias['faint_off', 'bold_off']
737
- attr_alias['fraktur_off', 'italic_off']
738
- attr_alias['double_underline_off', 'underline_off']
739
- attr_alias['rapid_blink_off', 'blink_off']
740
- attr_alias['encircled_off', 'framed_off']
741
- attr_alias['subscript_off', 'superscript_off']
742
- attr_alias['dotted_underline_off', 'curly_underline_off']
743
- attr_alias['dashed_underline_off', 'curly_underline_off']
742
+ attr_alias = ->(t, s) { attr_map[t] = attr_map[s] }
744
743
 
745
- # extra aliases:
744
+ # aliases:
746
745
  attr_alias['off', 'reset']
747
746
  attr_alias['dim', 'faint']
748
747
  attr_alias['dim_off', 'faint_off']
749
- attr_alias['conceal', 'hide']
750
- attr_alias['conceal_off', 'hide_off']
751
- attr_alias['reveal', 'hide_off']
752
748
  attr_alias['spacing', 'proportional']
753
749
  attr_alias['spacing_off', 'proportional_off']
754
750
 
@@ -761,20 +757,26 @@ module Terminal
761
757
  attr_alias['h', 'hide']
762
758
  attr_alias['s', 'strike']
763
759
  attr_alias['uu', 'double_underline']
764
- attr_alias['ovr', 'overlined']
765
- attr_alias['sup', 'superscript']
766
- attr_alias['sub', 'subscript']
767
760
  attr_alias['cu', 'curly_underline']
768
761
  attr_alias['dau', 'dashed_underline']
769
762
  attr_alias['dou', 'dotted_underline']
763
+ attr_alias['ovr', 'overlined']
764
+ attr_alias['sup', 'superscript']
765
+ attr_alias['sub', 'subscript']
770
766
 
771
- @colors = clr_map.keys.map!(&:to_sym).sort!.freeze
772
767
  @attributes = attr_map.keys.map!(&:to_sym).sort!.freeze
773
768
 
774
769
  # shortcuts disable:
775
770
  attr_map.keys.each do |n|
776
771
  attr_alias["/#{n.delete_suffix('_off')}", n] if n.end_with?('_off')
777
772
  end
773
+
774
+ # additional shortcuts disable:
775
+ attr_alias['/', 'reset']
776
+ attr_map['/bg'] = clr_map['on_default']
777
+ attr_map['/fg'] = clr_map['default']
778
+ attr_map['/ul'] = clr_map['ul_default']
779
+
778
780
  attr_alias['/b', 'bold_off']
779
781
  attr_alias['/d', 'dim_off']
780
782
  attr_alias['/i', 'italic_off']
@@ -783,26 +785,24 @@ module Terminal
783
785
  attr_alias['/h', 'hide_off']
784
786
  attr_alias['/s', 'strike_off']
785
787
  attr_alias['/uu', 'double_underline_off']
786
- attr_alias['/ovr', 'overlined_off']
787
- attr_alias['/sup', 'superscript_off']
788
- attr_alias['/sub', 'subscript_off']
789
788
  attr_alias['/cu', 'curly_underline_off']
790
789
  attr_alias['/dau', 'dashed_underline_off']
791
790
  attr_alias['/dou', 'dotted_underline_off']
791
+ attr_alias['/ovr', 'overlined_off']
792
+ attr_alias['/sup', 'superscript_off']
793
+ attr_alias['/sub', 'subscript_off']
792
794
 
793
- # additional shortcuts disable:
794
- attr_alias['/', 'reset']
795
- attr_map['/fg'] = clr_map['default']
796
- attr_map['/bg'] = clr_map['on_default']
797
- attr_map['/ul'] = clr_map['ul_default']
795
+ @cbase = { 'on' => '48', 'ul' => '58' }
796
+ @cbase.default = '38'
797
+ @cbase.freeze
798
798
 
799
799
  @attr_map =
800
800
  Hash.new do |_, str|
801
- b, v = /\A(fg|bg|on|ul)?_?#?([[:xdigit:]]{1,6})\z/.match(str)&.captures
801
+ b, v = /\A(on|ul)?_?#?([[:xdigit:]]{1,6})\z/.match(str)&.captures
802
802
  unless v
803
- b = /\A(fg|bg|on|ul)?_?([a-z]{3,}[0-9]{0,3})\z/.match(str) or next
804
- name = NAMED_COLORS[b[2]] or next
805
- next "#{@cbase[b[1]]};#{name}"
803
+ b = /\A(on|ul)?_?([a-z]{3,}[0-9]{0,3})\z/.match(str) or next
804
+ v = NAMED_COLORS[b[2]] or next
805
+ next "#{@cbase[b[1]]};#{v}"
806
806
  end
807
807
  case v.size
808
808
  when 1, 2
@@ -813,7 +813,7 @@ module Terminal
813
813
  "#{@cbase[b]};2;#{v[0, 2].hex};#{v[2, 2].hex};#{v[4, 2].hex}"
814
814
  end
815
815
  end
816
- attr_map.merge!(clr_map).keys.sort!.each { @attr_map[_1] = attr_map[_1] }
816
+ attr_map.merge!(clr_map).keys.sort!.each { @attr_map[it] = attr_map[it] }
817
817
  @attr_map.freeze
818
818
 
819
819
  @attrs_map = @attr_map.transform_keys(&:to_sym)
@@ -828,12 +828,15 @@ module Terminal
828
828
  @line_erase.default = '2'
829
829
  @line_erase.compare_by_identity.freeze
830
830
 
831
- autoload :NAMED_COLORS, "#{__dir__}/ansi/named_colors.rb"
832
- private_constant :NAMED_COLORS
833
-
834
831
  # @private
835
832
  RESET = -self[:reset]
836
833
 
834
+ # @private
835
+ RESET_FG = -self[:default]
836
+
837
+ # @private
838
+ RESET_BG = -self[:on_default]
839
+
837
840
  # @private
838
841
  FULL_RESET = "\ec"
839
842
 
@@ -843,6 +846,13 @@ module Terminal
843
846
  CURSOR_FIRST_ROW = -cursor_pos(1)
844
847
  # @private
845
848
  CURSOR_FIRST_COLUMN = -cursor_column(1)
849
+ # @private
850
+ CURSOR_NEXT_LINE = -cursor_next_line(nil)
851
+
852
+ # @private
853
+ CURSOR_BACK = -cursor_back(nil)
854
+ # @private
855
+ CURSOR_FORWARD = -cursor_forward(nil)
846
856
 
847
857
  # @private
848
858
  CURSOR_SHOW = "\e[?25h"
@@ -893,6 +903,9 @@ module Terminal
893
903
  # @private
894
904
  LINE_ERASE_PREV = -"#{cursor_prev_line(nil)}#{LINE_ERASE}"
895
905
 
906
+ # @private
907
+ LINK_END = "\e]8;;\a"
908
+
896
909
  # @private
897
910
  PROGRESS_HIDE = "\e]9;4;0;0\a"
898
911
  # @private
@@ -929,5 +942,10 @@ module Terminal
929
942
  # https://sw.kovidgoyal.net/kitty/color-stack
930
943
  # https://sw.kovidgoyal.net/kitty/deccara
931
944
  # https://sw.kovidgoyal.net/kitty/clipboard
945
+
946
+ dir = "#{__dir__}/ansi"
947
+ autoload :NAMED_COLORS, "#{dir}/named_colors.rb"
948
+ autoload :ScreenViewer, "#{dir}/screen_viewer.rb"
949
+ private_constant :NAMED_COLORS
932
950
  end
933
951
  end