keybox 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,744 +0,0 @@
1
- #!/usr/local/bin/ruby -w
2
-
3
- # highline.rb
4
- #
5
- # Created by James Edward Gray II on 2005-04-26.
6
- # Copyright 2005 Gray Productions. All rights reserved.
7
- #
8
- # See HighLine for documentation.
9
- #
10
- # This is Free Software. See LICENSE and COPYING for details.
11
-
12
- require "highline/system_extensions"
13
- require "highline/question"
14
- require "highline/menu"
15
- require "highline/color_scheme"
16
- require "erb"
17
- require "optparse"
18
- require "stringio"
19
- require "abbrev"
20
-
21
- #
22
- # A HighLine object is a "high-level line oriented" shell over an input and an
23
- # output stream. HighLine simplifies common console interaction, effectively
24
- # replacing puts() and gets(). User code can simply specify the question to ask
25
- # and any details about user interaction, then leave the rest of the work to
26
- # HighLine. When HighLine.ask() returns, you'll have the answer you requested,
27
- # even if HighLine had to ask many times, validate results, perform range
28
- # checking, convert types, etc.
29
- #
30
- class HighLine
31
- # The version of the installed library.
32
- VERSION = "1.2.9".freeze
33
-
34
- # An internal HighLine error. User code does not need to trap this.
35
- class QuestionError < StandardError
36
- # do nothing, just creating a unique error type
37
- end
38
-
39
- # The setting used to disable color output.
40
- @@use_color = true
41
-
42
- # Pass +false+ to _setting_ to turn off HighLine's color escapes.
43
- def self.use_color=( setting )
44
- @@use_color = setting
45
- end
46
-
47
- # Returns true if HighLine is currently using color escapes.
48
- def self.use_color?
49
- @@use_color
50
- end
51
-
52
- # The setting used to disable EOF tracking.
53
- @@track_eof = true
54
-
55
- # Pass +false+ to _setting_ to turn off HighLine's EOF tracking.
56
- def self.track_eof=( setting )
57
- @@track_eof = setting
58
- end
59
-
60
- # Returns true if HighLine is currently tracking EOF for input.
61
- def self.track_eof?
62
- @@track_eof
63
- end
64
-
65
- # The setting used to control color schemes.
66
- @@color_scheme = nil
67
-
68
- # Pass ColorScheme to _setting_ to turn set a HighLine color scheme.
69
- def self.color_scheme=( setting )
70
- @@color_scheme = setting
71
- end
72
-
73
- # Returns the current color scheme.
74
- def self.color_scheme
75
- @@color_scheme
76
- end
77
-
78
- # Returns +true+ if HighLine is currently using a color scheme.
79
- def self.using_color_scheme?
80
- not @@color_scheme.nil?
81
- end
82
-
83
- #
84
- # Embed in a String to clear all previous ANSI sequences. This *MUST* be
85
- # done before the program exits!
86
- #
87
- CLEAR = "\e[0m"
88
- # An alias for CLEAR.
89
- RESET = CLEAR
90
- # Erase the current line of terminal output.
91
- ERASE_LINE = "\e[K"
92
- # Erase the character under the cursor.
93
- ERASE_CHAR = "\e[P"
94
- # The start of an ANSI bold sequence.
95
- BOLD = "\e[1m"
96
- # The start of an ANSI dark sequence. (Terminal support uncommon.)
97
- DARK = "\e[2m"
98
- # The start of an ANSI underline sequence.
99
- UNDERLINE = "\e[4m"
100
- # An alias for UNDERLINE.
101
- UNDERSCORE = UNDERLINE
102
- # The start of an ANSI blink sequence. (Terminal support uncommon.)
103
- BLINK = "\e[5m"
104
- # The start of an ANSI reverse sequence.
105
- REVERSE = "\e[7m"
106
- # The start of an ANSI concealed sequence. (Terminal support uncommon.)
107
- CONCEALED = "\e[8m"
108
-
109
- # Set the terminal's foreground ANSI color to black.
110
- BLACK = "\e[30m"
111
- # Set the terminal's foreground ANSI color to red.
112
- RED = "\e[31m"
113
- # Set the terminal's foreground ANSI color to green.
114
- GREEN = "\e[32m"
115
- # Set the terminal's foreground ANSI color to yellow.
116
- YELLOW = "\e[33m"
117
- # Set the terminal's foreground ANSI color to blue.
118
- BLUE = "\e[34m"
119
- # Set the terminal's foreground ANSI color to magenta.
120
- MAGENTA = "\e[35m"
121
- # Set the terminal's foreground ANSI color to cyan.
122
- CYAN = "\e[36m"
123
- # Set the terminal's foreground ANSI color to white.
124
- WHITE = "\e[37m"
125
-
126
- # Set the terminal's background ANSI color to black.
127
- ON_BLACK = "\e[40m"
128
- # Set the terminal's background ANSI color to red.
129
- ON_RED = "\e[41m"
130
- # Set the terminal's background ANSI color to green.
131
- ON_GREEN = "\e[42m"
132
- # Set the terminal's background ANSI color to yellow.
133
- ON_YELLOW = "\e[43m"
134
- # Set the terminal's background ANSI color to blue.
135
- ON_BLUE = "\e[44m"
136
- # Set the terminal's background ANSI color to magenta.
137
- ON_MAGENTA = "\e[45m"
138
- # Set the terminal's background ANSI color to cyan.
139
- ON_CYAN = "\e[46m"
140
- # Set the terminal's background ANSI color to white.
141
- ON_WHITE = "\e[47m"
142
-
143
- #
144
- # Create an instance of HighLine, connected to the streams _input_
145
- # and _output_.
146
- #
147
- def initialize( input = $stdin, output = $stdout,
148
- wrap_at = nil, page_at = nil )
149
- @input = input
150
- @output = output
151
-
152
- self.wrap_at = wrap_at
153
- self.page_at = page_at
154
-
155
- @question = nil
156
- @answer = nil
157
- @menu = nil
158
- @header = nil
159
- @prompt = nil
160
- @gather = nil
161
- @answers = nil
162
- @key = nil
163
- end
164
-
165
- include HighLine::SystemExtensions
166
-
167
- # The current column setting for wrapping output.
168
- attr_reader :wrap_at
169
- # The current row setting for paging output.
170
- attr_reader :page_at
171
-
172
- #
173
- # A shortcut to HighLine.ask() a question that only accepts "yes" or "no"
174
- # answers ("y" and "n" are allowed) and returns +true+ or +false+
175
- # (+true+ for "yes"). If provided a +true+ value, _character_ will cause
176
- # HighLine to fetch a single character response.
177
- #
178
- # Raises EOFError if input is exhausted.
179
- #
180
- def agree( yes_or_no_question, character = nil )
181
- ask(yes_or_no_question, lambda { |yn| yn.downcase[0] == ?y}) do |q|
182
- q.validate = /\Ay(?:es)?|no?\Z/i
183
- q.responses[:not_valid] = 'Please enter "yes" or "no".'
184
- q.responses[:ask_on_error] = :question
185
- q.character = character
186
- end
187
- end
188
-
189
- #
190
- # This method is the primary interface for user input. Just provide a
191
- # _question_ to ask the user, the _answer_type_ you want returned, and
192
- # optionally a code block setting up details of how you want the question
193
- # handled. See HighLine.say() for details on the format of _question_, and
194
- # HighLine::Question for more information about _answer_type_ and what's
195
- # valid in the code block.
196
- #
197
- # If <tt>@question</tt> is set before ask() is called, parameters are
198
- # ignored and that object (must be a HighLine::Question) is used to drive
199
- # the process instead.
200
- #
201
- # Raises EOFError if input is exhausted.
202
- #
203
- def ask( question, answer_type = String, &details ) # :yields: question
204
- @question ||= Question.new(question, answer_type, &details)
205
-
206
- return gather if @question.gather
207
-
208
- # readline() needs to handle it's own output, but readline only supports
209
- # full line reading. Therefore if @question.echo is anything but true,
210
- # the prompt will not be issued. And we have to account for that now.
211
- say(@question) unless (@question.readline and @question.echo == true)
212
- begin
213
- @answer = @question.answer_or_default(get_response)
214
- unless @question.valid_answer?(@answer)
215
- explain_error(:not_valid)
216
- raise QuestionError
217
- end
218
-
219
- @answer = @question.convert(@answer)
220
-
221
- if @question.in_range?(@answer)
222
- if @question.confirm
223
- # need to add a layer of scope to ask a question inside a
224
- # question, without destroying instance data
225
- context_change = self.class.new(@input, @output, @wrap_at, @page_at)
226
- if @question.confirm == true
227
- confirm_question = "Are you sure? "
228
- else
229
- # evaluate ERb under initial scope, so it will have
230
- # access to @question and @answer
231
- template = ERB.new(@question.confirm, nil, "%")
232
- confirm_question = template.result(binding)
233
- end
234
- unless context_change.agree(confirm_question)
235
- explain_error(nil)
236
- raise QuestionError
237
- end
238
- end
239
-
240
- @answer
241
- else
242
- explain_error(:not_in_range)
243
- raise QuestionError
244
- end
245
- rescue QuestionError
246
- retry
247
- rescue ArgumentError
248
- explain_error(:invalid_type)
249
- retry
250
- rescue Question::NoAutoCompleteMatch
251
- explain_error(:no_completion)
252
- retry
253
- rescue NameError
254
- raise if $!.is_a?(NoMethodError)
255
- explain_error(:ambiguous_completion)
256
- retry
257
- ensure
258
- @question = nil # Reset Question object.
259
- end
260
- end
261
-
262
- #
263
- # This method is HighLine's menu handler. For simple usage, you can just
264
- # pass all the menu items you wish to display. At that point, choose() will
265
- # build and display a menu, walk the user through selection, and return
266
- # their choice amoung the provided items. You might use this in a case
267
- # statement for quick and dirty menus.
268
- #
269
- # However, choose() is capable of much more. If provided, a block will be
270
- # passed a HighLine::Menu object to configure. Using this method, you can
271
- # customize all the details of menu handling from index display, to building
272
- # a complete shell-like menuing system. See HighLine::Menu for all the
273
- # methods it responds to.
274
- #
275
- # Raises EOFError if input is exhausted.
276
- #
277
- def choose( *items, &details )
278
- @menu = @question = Menu.new(&details)
279
- @menu.choices(*items) unless items.empty?
280
-
281
- # Set _answer_type_ so we can double as the Question for ask().
282
- @menu.answer_type = if @menu.shell
283
- lambda do |command| # shell-style selection
284
- first_word = command.to_s.split.first || ""
285
-
286
- options = @menu.options
287
- options.extend(OptionParser::Completion)
288
- answer = options.complete(first_word)
289
-
290
- if answer.nil?
291
- raise Question::NoAutoCompleteMatch
292
- end
293
-
294
- [answer.last, command.sub(/^\s*#{first_word}\s*/, "")]
295
- end
296
- else
297
- @menu.options # normal menu selection, by index or name
298
- end
299
-
300
- # Provide hooks for ERb layouts.
301
- @header = @menu.header
302
- @prompt = @menu.prompt
303
-
304
- if @menu.shell
305
- selected = ask("Ignored", @menu.answer_type)
306
- @menu.select(self, *selected)
307
- else
308
- selected = ask("Ignored", @menu.answer_type)
309
- @menu.select(self, selected)
310
- end
311
- end
312
-
313
- #
314
- # This method provides easy access to ANSI color sequences, without the user
315
- # needing to remember to CLEAR at the end of each sequence. Just pass the
316
- # _string_ to color, followed by a list of _colors_ you would like it to be
317
- # affected by. The _colors_ can be HighLine class constants, or symbols
318
- # (:blue for BLUE, for example). A CLEAR will automatically be embedded to
319
- # the end of the returned String.
320
- #
321
- # This method returns the original _string_ unchanged if HighLine::use_color?
322
- # is +false+.
323
- #
324
- def color( string, *colors )
325
- return string unless self.class.use_color?
326
-
327
- colors.map! do |c|
328
- if self.class.using_color_scheme? and self.class.color_scheme.include? c
329
- self.class.color_scheme[c]
330
- elsif c.is_a? Symbol
331
- self.class.const_get(c.to_s.upcase)
332
- else
333
- c
334
- end
335
- end
336
- "#{colors.flatten.join}#{string}#{CLEAR}"
337
- end
338
-
339
- #
340
- # This method is a utility for quickly and easily laying out lists. It can
341
- # be accessed within ERb replacements of any text that will be sent to the
342
- # user.
343
- #
344
- # The only required parameter is _items_, which should be the Array of items
345
- # to list. A specified _mode_ controls how that list is formed and _option_
346
- # has different effects, depending on the _mode_. Recognized modes are:
347
- #
348
- # <tt>:columns_across</tt>:: _items_ will be placed in columns, flowing
349
- # from left to right. If given, _option_ is the
350
- # number of columns to be used. When absent,
351
- # columns will be determined based on _wrap_at_
352
- # or a default of 80 characters.
353
- # <tt>:columns_down</tt>:: Identical to <tt>:columns_across</tt>, save
354
- # flow goes down.
355
- # <tt>:inline</tt>:: All _items_ are placed on a single line. The
356
- # last two _items_ are separated by _option_ or
357
- # a default of " or ". All other _items_ are
358
- # separated by ", ".
359
- # <tt>:rows</tt>:: The default mode. Each of the _items_ is
360
- # placed on it's own line. The _option_
361
- # parameter is ignored in this mode.
362
- #
363
- # Each member of the _items_ Array is passed through ERb and thus can contain
364
- # their own expansions. Color escape expansions do not contribute to the
365
- # final field width.
366
- #
367
- def list( items, mode = :rows, option = nil )
368
- items = items.to_ary.map do |item|
369
- ERB.new(item, nil, "%").result(binding)
370
- end
371
-
372
- case mode
373
- when :inline
374
- option = " or " if option.nil?
375
-
376
- case items.size
377
- when 0
378
- ""
379
- when 1
380
- items.first
381
- when 2
382
- "#{items.first}#{option}#{items.last}"
383
- else
384
- items[0..-2].join(", ") + "#{option}#{items.last}"
385
- end
386
- when :columns_across, :columns_down
387
- max_length = actual_length(
388
- items.max { |a, b| actual_length(a) <=> actual_length(b) }
389
- )
390
-
391
- if option.nil?
392
- limit = @wrap_at || 80
393
- option = (limit + 2) / (max_length + 2)
394
- end
395
-
396
- items = items.map do |item|
397
- pad = max_length + (item.length - actual_length(item))
398
- "%-#{pad}s" % item
399
- end
400
- row_count = (items.size / option.to_f).ceil
401
-
402
- if mode == :columns_across
403
- rows = Array.new(row_count) { Array.new }
404
- items.each_with_index do |item, index|
405
- rows[index / option] << item
406
- end
407
-
408
- rows.map { |row| row.join(" ") + "\n" }.join
409
- else
410
- columns = Array.new(option) { Array.new }
411
- items.each_with_index do |item, index|
412
- columns[index / row_count] << item
413
- end
414
-
415
- list = ""
416
- columns.first.size.times do |index|
417
- list << columns.map { |column| column[index] }.
418
- compact.join(" ") + "\n"
419
- end
420
- list
421
- end
422
- else
423
- items.map { |i| "#{i}\n" }.join
424
- end
425
- end
426
-
427
- #
428
- # The basic output method for HighLine objects. If the provided _statement_
429
- # ends with a space or tab character, a newline will not be appended (output
430
- # will be flush()ed). All other cases are passed straight to Kernel.puts().
431
- #
432
- # The _statement_ parameter is processed as an ERb template, supporting
433
- # embedded Ruby code. The template is evaluated with a binding inside
434
- # the HighLine instance, providing easy access to the ANSI color constants
435
- # and the HighLine.color() method.
436
- #
437
- def say( statement )
438
- statement = statement.to_str
439
- return unless statement.length > 0
440
-
441
- template = ERB.new(statement, nil, "%")
442
- statement = template.result(binding)
443
-
444
- statement = wrap(statement) unless @wrap_at.nil?
445
- statement = page_print(statement) unless @page_at.nil?
446
-
447
- if statement[-1, 1] == " " or statement[-1, 1] == "\t"
448
- @output.print(statement)
449
- @output.flush
450
- else
451
- @output.puts(statement)
452
- end
453
- end
454
-
455
- #
456
- # Set to an integer value to cause HighLine to wrap output lines at the
457
- # indicated character limit. When +nil+, the default, no wrapping occurs. If
458
- # set to <tt>:auto</tt>, HighLine will attempt to determing the columns
459
- # available for the <tt>@output</tt> or use a sensible default.
460
- #
461
- def wrap_at=( setting )
462
- @wrap_at = setting == :auto ? output_cols : setting
463
- end
464
-
465
- #
466
- # Set to an integer value to cause HighLine to page output lines over the
467
- # indicated line limit. When +nil+, the default, no paging occurs. If
468
- # set to <tt>:auto</tt>, HighLine will attempt to determing the rows available
469
- # for the <tt>@output</tt> or use a sensible default.
470
- #
471
- def page_at=( setting )
472
- @page_at = setting == :auto ? output_rows : setting
473
- end
474
-
475
- #
476
- # Returns the number of columns for the console, or a default it they cannot
477
- # be determined.
478
- #
479
- def output_cols
480
- return 80 unless @output.tty?
481
- terminal_size.first
482
- rescue
483
- return 80
484
- end
485
-
486
- #
487
- # Returns the number of rows for the console, or a default if they cannot be
488
- # determined.
489
- #
490
- def output_rows
491
- return 24 unless @output.tty?
492
- terminal_size.last
493
- rescue
494
- return 24
495
- end
496
-
497
- private
498
-
499
- #
500
- # A helper method for sending the output stream and error and repeat
501
- # of the question.
502
- #
503
- def explain_error( error )
504
- say(@question.responses[error]) unless error.nil?
505
- if @question.responses[:ask_on_error] == :question
506
- say(@question)
507
- elsif @question.responses[:ask_on_error]
508
- say(@question.responses[:ask_on_error])
509
- end
510
- end
511
-
512
- #
513
- # Collects an Array/Hash full of answers as described in
514
- # HighLine::Question.gather().
515
- #
516
- # Raises EOFError if input is exhausted.
517
- #
518
- def gather( )
519
- @gather = @question.gather
520
- @answers = [ ]
521
- original_question = @question
522
-
523
- @question.gather = false
524
-
525
- case @gather
526
- when Integer
527
- @answers << ask(@question)
528
- @gather -= 1
529
-
530
- original_question.question = ""
531
- until @gather.zero?
532
- @question = original_question
533
- @answers << ask(@question)
534
- @gather -= 1
535
- end
536
- when String, Regexp
537
- @answers << ask(@question)
538
-
539
- original_question.question = ""
540
- until (@gather.is_a?(String) and @answers.last.to_s == @gather) or
541
- (@gather.is_a?(Regexp) and @answers.last.to_s =~ @gather)
542
- @question = original_question
543
- @answers << ask(@question)
544
- end
545
-
546
- @answers.pop
547
- when Hash
548
- @answers = { }
549
- @gather.keys.sort.each do |key|
550
- @question = original_question
551
- @key = key
552
- @answers[key] = ask(@question)
553
- end
554
- end
555
-
556
- @answers
557
- end
558
-
559
- #
560
- # Read a line of input from the input stream and process whitespace as
561
- # requested by the Question object.
562
- #
563
- # If Question's _readline_ property is set, that library will be used to
564
- # fetch input. *WARNING*: This ignores the currently set input stream.
565
- #
566
- # Raises EOFError if input is exhausted.
567
- #
568
- def get_line( )
569
- if @question.readline
570
- require "readline" # load only if needed
571
-
572
- # capture say()'s work in a String to feed to readline()
573
- old_output = @output
574
- @output = StringIO.new
575
- say(@question)
576
- question = @output.string
577
- @output = old_output
578
-
579
- # prep auto-completion
580
- completions = @question.selection.abbrev
581
- Readline.completion_proc = lambda { |string| completions[string] }
582
-
583
- # work-around ugly readline() warnings
584
- old_verbose = $VERBOSE
585
- $VERBOSE = nil
586
- answer = @question.change_case(
587
- @question.remove_whitespace(
588
- Readline.readline(question, true) ) )
589
- $VERBOSE = old_verbose
590
-
591
- answer
592
- else
593
- raise EOFError, "The input stream is exhausted." if @@track_eof and
594
- @input.eof?
595
-
596
- @question.change_case(@question.remove_whitespace(@input.gets))
597
- end
598
- end
599
-
600
- #
601
- # Return a line or character of input, as requested for this question.
602
- # Character input will be returned as a single character String,
603
- # not an Integer.
604
- #
605
- # This question's _first_answer_ will be returned instead of input, if set.
606
- #
607
- # Raises EOFError if input is exhausted.
608
- #
609
- def get_response( )
610
- return @question.first_answer if @question.first_answer?
611
-
612
- if @question.character.nil?
613
- if @question.echo == true and @question.limit.nil?
614
- get_line
615
- else
616
- raw_no_echo_mode if stty = CHARACTER_MODE == "stty"
617
-
618
- line = ""
619
- backspace_limit = 0
620
- begin
621
-
622
- while character = (stty ? @input.getc : get_character(@input))
623
- # honor backspace and delete
624
- if character == 127 or character == 8
625
- line.slice!(-1, 1)
626
- backspace_limit -= 1
627
- else
628
- line << character.chr
629
- backspace_limit = line.size
630
- end
631
- # looking for carriage return (decimal 13) or
632
- # newline (decimal 10) in raw input
633
- break if character == 13 or character == 10 or
634
- (@question.limit and line.size == @question.limit)
635
- if @question.echo != false
636
- if character == 127 or character == 8
637
- # only backspace if we have characters on the line to
638
- # eliminate, otherwise we'll tromp over the prompt
639
- if backspace_limit >= 0 then
640
- @output.print("\b#{ERASE_CHAR}")
641
- else
642
- # do nothing
643
- end
644
- else
645
- @output.print(@question.echo)
646
- end
647
- @output.flush
648
- end
649
- end
650
- ensure
651
- restore_mode if stty
652
- end
653
- if @question.overwrite
654
- @output.print("\r#{ERASE_LINE}")
655
- @output.flush
656
- else
657
- say("\n")
658
- end
659
-
660
- @question.change_case(@question.remove_whitespace(line))
661
- end
662
- elsif @question.character == :getc
663
- @question.change_case(@input.getc.chr)
664
- else
665
- response = get_character(@input).chr
666
- if @question.overwrite
667
- @output.print("\r#{ERASE_LINE}")
668
- @output.flush
669
- else
670
- echo = if @question.echo == true
671
- response
672
- elsif @question.echo != false
673
- @question.echo
674
- else
675
- ""
676
- end
677
- say("#{echo}\n")
678
- end
679
- @question.change_case(response)
680
- end
681
- end
682
-
683
- #
684
- # Page print a series of at most _page_at_ lines for _output_. After each
685
- # page is printed, HighLine will pause until the user presses enter/return
686
- # then display the next page of data.
687
- #
688
- # Note that the final page of _output_ is *not* printed, but returned
689
- # instead. This is to support any special handling for the final sequence.
690
- #
691
- def page_print( output )
692
- lines = output.scan(/[^\n]*\n?/)
693
- while lines.size > @page_at
694
- @output.puts lines.slice!(0...@page_at).join
695
- @output.puts
696
- # Return last line if user wants to abort paging
697
- return (["...\n"] + lines.slice(-2,1)).join unless continue_paging?
698
- end
699
- return lines.join
700
- end
701
-
702
- #
703
- # Ask user if they wish to continue paging output. Allows them to type "q" to
704
- # cancel the paging process.
705
- #
706
- def continue_paging?
707
- command = HighLine.new(@input, @output).ask(
708
- "-- press enter/return to continue or q to stop -- "
709
- ) { |q| q.character = true }
710
- command !~ /\A[qQ]\Z/ # Only continue paging if Q was not hit.
711
- end
712
-
713
- #
714
- # Wrap a sequence of _lines_ at _wrap_at_ characters per line. Existing
715
- # newlines will not be affected by this process, but additional newlines
716
- # may be added.
717
- #
718
- def wrap( lines )
719
- wrapped = [ ]
720
- lines.each do |line|
721
- while line =~ /([^\n]{#{@wrap_at + 1},})/
722
- search = $1.dup
723
- replace = $1.dup
724
- if index = replace.rindex(" ", @wrap_at)
725
- replace[index, 1] = "\n"
726
- replace.sub!(/\n[ \t]+/, "\n")
727
- line.sub!(search, replace)
728
- else
729
- line[@wrap_at, 0] = "\n"
730
- end
731
- end
732
- wrapped << line
733
- end
734
- return wrapped.join
735
- end
736
-
737
- #
738
- # Returns the length of the passed +string_with_escapes+, minus and color
739
- # sequence escapes.
740
- #
741
- def actual_length( string_with_escapes )
742
- string_with_escapes.gsub(/\e\[\d{1,2}m/, "").length
743
- end
744
- end