cli-ui 1.2.0 → 1.4.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 +5 -5
- data/.dependabot/config.yml +8 -0
- data/.github/CODEOWNERS +1 -0
- data/.github/probots.yml +2 -0
- data/.gitignore +0 -1
- data/.rubocop.yml +23 -2
- data/.travis.yml +4 -2
- data/Gemfile.lock +56 -0
- data/README.md +50 -3
- data/Rakefile +1 -1
- data/cli-ui.gemspec +4 -4
- data/dev.yml +1 -1
- data/lib/cli/ui.rb +61 -20
- data/lib/cli/ui/ansi.rb +9 -3
- data/lib/cli/ui/color.rb +12 -7
- data/lib/cli/ui/formatter.rb +34 -21
- data/lib/cli/ui/frame.rb +108 -149
- data/lib/cli/ui/frame/frame_stack.rb +98 -0
- data/lib/cli/ui/frame/frame_style.rb +120 -0
- data/lib/cli/ui/frame/frame_style/box.rb +166 -0
- data/lib/cli/ui/frame/frame_style/bracket.rb +139 -0
- data/lib/cli/ui/glyph.rb +22 -17
- data/lib/cli/ui/os.rb +63 -0
- data/lib/cli/ui/printer.rb +47 -0
- data/lib/cli/ui/progress.rb +10 -8
- data/lib/cli/ui/prompt.rb +88 -17
- data/lib/cli/ui/prompt/interactive_options.rb +261 -50
- data/lib/cli/ui/prompt/options_handler.rb +7 -2
- data/lib/cli/ui/spinner.rb +23 -5
- data/lib/cli/ui/spinner/spin_group.rb +39 -11
- data/lib/cli/ui/stdout_router.rb +12 -7
- data/lib/cli/ui/terminal.rb +26 -16
- data/lib/cli/ui/truncater.rb +3 -3
- data/lib/cli/ui/version.rb +1 -1
- data/lib/cli/ui/widgets.rb +77 -0
- data/lib/cli/ui/widgets/base.rb +27 -0
- data/lib/cli/ui/widgets/status.rb +61 -0
- metadata +21 -24
- data/lib/cli/ui/box.rb +0 -15
    
        data/lib/cli/ui/color.rb
    CHANGED
    
    | @@ -31,19 +31,24 @@ module CLI | |
| 31 31 | 
             
                  BOLD    = new('1',  :bold)
         | 
| 32 32 | 
             
                  WHITE   = new('97', :white)
         | 
| 33 33 |  | 
| 34 | 
            +
                  # 240 is very dark gray; 255 is very light gray. 244 is somewhat dark.
         | 
| 35 | 
            +
                  GRAY = new('38;5;244', :grey)
         | 
| 36 | 
            +
             | 
| 34 37 | 
             
                  MAP = {
         | 
| 35 | 
            -
                    red: | 
| 36 | 
            -
                    green: | 
| 37 | 
            -
                    yellow: | 
| 38 | 
            -
                    blue: | 
| 38 | 
            +
                    red: RED,
         | 
| 39 | 
            +
                    green: GREEN,
         | 
| 40 | 
            +
                    yellow: YELLOW,
         | 
| 41 | 
            +
                    blue: BLUE,
         | 
| 39 42 | 
             
                    magenta: MAGENTA,
         | 
| 40 | 
            -
                    cyan: | 
| 41 | 
            -
                    reset: | 
| 42 | 
            -
                    bold: | 
| 43 | 
            +
                    cyan: CYAN,
         | 
| 44 | 
            +
                    reset: RESET,
         | 
| 45 | 
            +
                    bold: BOLD,
         | 
| 46 | 
            +
                    gray: GRAY,
         | 
| 43 47 | 
             
                  }.freeze
         | 
| 44 48 |  | 
| 45 49 | 
             
                  class InvalidColorName < ArgumentError
         | 
| 46 50 | 
             
                    def initialize(name)
         | 
| 51 | 
            +
                      super
         | 
| 47 52 | 
             
                      @name = name
         | 
| 48 53 | 
             
                    end
         | 
| 49 54 |  | 
    
        data/lib/cli/ui/formatter.rb
    CHANGED
    
    | @@ -1,7 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            require | 
| 4 | 
            -
            require 'strscan'
         | 
| 2 | 
            +
            require('cli/ui')
         | 
| 3 | 
            +
            require('strscan')
         | 
| 5 4 |  | 
| 6 5 | 
             
            module CLI
         | 
| 7 6 | 
             
              module UI
         | 
| @@ -13,39 +12,40 @@ module CLI | |
| 13 12 | 
             
                  #
         | 
| 14 13 | 
             
                  SGR_MAP = {
         | 
| 15 14 | 
             
                    # presentational
         | 
| 16 | 
            -
                    'red' | 
| 17 | 
            -
                    'green' | 
| 18 | 
            -
                    'yellow' | 
| 19 | 
            -
             | 
| 20 | 
            -
                    'blue' | 
| 21 | 
            -
                    'magenta' | 
| 22 | 
            -
                    'cyan' | 
| 23 | 
            -
                    'bold' | 
| 24 | 
            -
                    'italic' | 
| 15 | 
            +
                    'red' => '31',
         | 
| 16 | 
            +
                    'green' => '32',
         | 
| 17 | 
            +
                    'yellow' => '33',
         | 
| 18 | 
            +
                    # default blue is low-contrast against black in some default terminal color scheme
         | 
| 19 | 
            +
                    'blue' => '94', # 9x = high-intensity fg color x
         | 
| 20 | 
            +
                    'magenta' => '35',
         | 
| 21 | 
            +
                    'cyan' => '36',
         | 
| 22 | 
            +
                    'bold' => '1',
         | 
| 23 | 
            +
                    'italic' => '3',
         | 
| 25 24 | 
             
                    'underline' => '4',
         | 
| 26 | 
            -
                    'reset' | 
| 25 | 
            +
                    'reset' => '0',
         | 
| 27 26 |  | 
| 28 27 | 
             
                    # semantic
         | 
| 29 | 
            -
                    'error' | 
| 28 | 
            +
                    'error' => '31', # red
         | 
| 30 29 | 
             
                    'success' => '32', # success
         | 
| 31 30 | 
             
                    'warning' => '33', # yellow
         | 
| 32 | 
            -
                    'info' | 
| 31 | 
            +
                    'info' => '94', # bright blue
         | 
| 33 32 | 
             
                    'command' => '36', # cyan
         | 
| 34 33 | 
             
                  }.freeze
         | 
| 35 34 |  | 
| 36 35 | 
             
                  BEGIN_EXPR = '{{'
         | 
| 37 36 | 
             
                  END_EXPR   = '}}'
         | 
| 38 37 |  | 
| 38 | 
            +
                  SCAN_WIDGET   = %r[@widget/(?<handle>\w+):(?<args>.*?)}}]
         | 
| 39 39 | 
             
                  SCAN_FUNCNAME = /\w+:/
         | 
| 40 40 | 
             
                  SCAN_GLYPH    = /.}}/
         | 
| 41 | 
            -
                  SCAN_BODY     =  | 
| 41 | 
            +
                  SCAN_BODY     = %r{
         | 
| 42 42 | 
             
                    .*?
         | 
| 43 43 | 
             
                    (
         | 
| 44 44 | 
             
                      #{BEGIN_EXPR} |
         | 
| 45 45 | 
             
                      #{END_EXPR}   |
         | 
| 46 46 | 
             
                      \z
         | 
| 47 47 | 
             
                    )
         | 
| 48 | 
            -
                   | 
| 48 | 
            +
                  }mx
         | 
| 49 49 |  | 
| 50 50 | 
             
                  DISCARD_BRACES = 0..-3
         | 
| 51 51 |  | 
| @@ -123,7 +123,7 @@ module CLI | |
| 123 123 | 
             
                  end
         | 
| 124 124 |  | 
| 125 125 | 
             
                  def parse_expr(sc, stack)
         | 
| 126 | 
            -
                    if match = sc.scan(SCAN_GLYPH)
         | 
| 126 | 
            +
                    if (match = sc.scan(SCAN_GLYPH))
         | 
| 127 127 | 
             
                      glyph_handle = match[0]
         | 
| 128 128 | 
             
                      begin
         | 
| 129 129 | 
             
                        glyph = Glyph.lookup(glyph_handle)
         | 
| @@ -136,7 +136,20 @@ module CLI | |
| 136 136 | 
             
                          index
         | 
| 137 137 | 
             
                        )
         | 
| 138 138 | 
             
                      end
         | 
| 139 | 
            -
                    elsif match = sc.scan( | 
| 139 | 
            +
                    elsif (match = sc.scan(SCAN_WIDGET))
         | 
| 140 | 
            +
                      match_data = SCAN_WIDGET.match(match) # Regexp.last_match doesn't work here
         | 
| 141 | 
            +
                      widget_handle = match_data['handle']
         | 
| 142 | 
            +
                      begin
         | 
| 143 | 
            +
                        widget = Widgets.lookup(widget_handle)
         | 
| 144 | 
            +
                        emit(widget.call(match_data['args']), stack)
         | 
| 145 | 
            +
                      rescue Widgets::InvalidWidgetHandle
         | 
| 146 | 
            +
                        index = sc.pos - 2 # rewind past '}}'
         | 
| 147 | 
            +
                        raise(FormatError.new(
         | 
| 148 | 
            +
                          "invalid widget handle at index #{index}: '#{widget_handle}'",
         | 
| 149 | 
            +
                          @text, index,
         | 
| 150 | 
            +
                        ))
         | 
| 151 | 
            +
                      end
         | 
| 152 | 
            +
                    elsif (match = sc.scan(SCAN_FUNCNAME))
         | 
| 140 153 | 
             
                      funcname = match.chop
         | 
| 141 154 | 
             
                      stack.push(funcname)
         | 
| 142 155 | 
             
                    else
         | 
| @@ -153,10 +166,10 @@ module CLI | |
| 153 166 |  | 
| 154 167 | 
             
                  def parse_body(sc, stack = [])
         | 
| 155 168 | 
             
                    match = sc.scan(SCAN_BODY)
         | 
| 156 | 
            -
                    if match | 
| 169 | 
            +
                    if match&.end_with?(BEGIN_EXPR)
         | 
| 157 170 | 
             
                      emit(match[DISCARD_BRACES], stack)
         | 
| 158 171 | 
             
                      parse_expr(sc, stack)
         | 
| 159 | 
            -
                    elsif match | 
| 172 | 
            +
                    elsif match&.end_with?(END_EXPR)
         | 
| 160 173 | 
             
                      emit(match[DISCARD_BRACES], stack)
         | 
| 161 174 | 
             
                      if stack.pop == LITERAL_BRACES
         | 
| 162 175 | 
             
                        emit('}}', stack)
         | 
    
        data/lib/cli/ui/frame.rb
    CHANGED
    
    | @@ -1,4 +1,7 @@ | |
| 1 | 
            +
            # coding: utf-8
         | 
| 1 2 | 
             
            require 'cli/ui'
         | 
| 3 | 
            +
            require 'cli/ui/frame/frame_stack'
         | 
| 4 | 
            +
            require 'cli/ui/frame/frame_style'
         | 
| 2 5 |  | 
| 3 6 | 
             
            module CLI
         | 
| 4 7 | 
             
              module UI
         | 
| @@ -7,6 +10,22 @@ module CLI | |
| 7 10 | 
             
                  class << self
         | 
| 8 11 | 
             
                    DEFAULT_FRAME_COLOR = CLI::UI.resolve_color(:cyan)
         | 
| 9 12 |  | 
| 13 | 
            +
                    def frame_style
         | 
| 14 | 
            +
                      @frame_style ||= FrameStyle::Box
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    # Set the default frame style.
         | 
| 18 | 
            +
                    #
         | 
| 19 | 
            +
                    # Raises ArgumentError if +frame_style+ is not valid
         | 
| 20 | 
            +
                    #
         | 
| 21 | 
            +
                    # ==== Attributes
         | 
| 22 | 
            +
                    #
         | 
| 23 | 
            +
                    # * +symbol+ or +FrameStyle+ - the default frame style to use for frames
         | 
| 24 | 
            +
                    #
         | 
| 25 | 
            +
                    def frame_style=(frame_style)
         | 
| 26 | 
            +
                      @frame_style = CLI::UI.resolve_style(frame_style)
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
             | 
| 10 29 | 
             
                    # Opens a new frame. Can be nested
         | 
| 11 30 | 
             
                    # Can be invoked in two ways: block and blockless
         | 
| 12 31 | 
             
                    # * In block form, the frame is closed automatically when the block returns
         | 
| @@ -27,6 +46,7 @@ module CLI | |
| 27 46 | 
             
                    # * +:failure_text+ - If the block failed, what do we output? Defaults to nil
         | 
| 28 47 | 
             
                    # * +:success_text+ - If the block succeeds, what do we output? Defaults to nil
         | 
| 29 48 | 
             
                    # * +:timing+ - How long did the frame content take? Invalid for blockless. Defaults to true for the block form
         | 
| 49 | 
            +
                    # * +frame_style+ - The frame style to use for this frame
         | 
| 30 50 | 
             
                    #
         | 
| 31 51 | 
             
                    # ==== Example
         | 
| 32 52 | 
             
                    #
         | 
| @@ -34,7 +54,7 @@ module CLI | |
| 34 54 | 
             
                    #
         | 
| 35 55 | 
             
                    #   CLI::UI::Frame.open('Open') { puts 'hi' }
         | 
| 36 56 | 
             
                    #
         | 
| 37 | 
            -
                    # Output:
         | 
| 57 | 
            +
                    # Default Output:
         | 
| 38 58 | 
             
                    #   ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
         | 
| 39 59 | 
             
                    #   ┃ hi
         | 
| 40 60 | 
             
                    #   ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (0.0s) ━━
         | 
| @@ -43,7 +63,7 @@ module CLI | |
| 43 63 | 
             
                    #
         | 
| 44 64 | 
             
                    #   CLI::UI::Frame.open('Open')
         | 
| 45 65 | 
             
                    #
         | 
| 46 | 
            -
                    # Output:
         | 
| 66 | 
            +
                    # Default Output:
         | 
| 47 67 | 
             
                    #   ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
         | 
| 48 68 | 
             
                    #
         | 
| 49 69 | 
             
                    #
         | 
| @@ -52,8 +72,10 @@ module CLI | |
| 52 72 | 
             
                      color: DEFAULT_FRAME_COLOR,
         | 
| 53 73 | 
             
                      failure_text: nil,
         | 
| 54 74 | 
             
                      success_text: nil,
         | 
| 55 | 
            -
                      timing:       nil
         | 
| 75 | 
            +
                      timing:       nil,
         | 
| 76 | 
            +
                      frame_style:  self.frame_style
         | 
| 56 77 | 
             
                    )
         | 
| 78 | 
            +
                      frame_style = CLI::UI.resolve_style(frame_style)
         | 
| 57 79 | 
             
                      color = CLI::UI.resolve_color(color)
         | 
| 58 80 |  | 
| 59 81 | 
             
                      unless block_given?
         | 
| @@ -61,18 +83,17 @@ module CLI | |
| 61 83 | 
             
                          raise ArgumentError, "failure_text is not compatible with blockless invocation"
         | 
| 62 84 | 
             
                        elsif success_text
         | 
| 63 85 | 
             
                          raise ArgumentError, "success_text is not compatible with blockless invocation"
         | 
| 64 | 
            -
                        elsif  | 
| 86 | 
            +
                        elsif timing
         | 
| 65 87 | 
             
                          raise ArgumentError, "timing is not compatible with blockless invocation"
         | 
| 66 88 | 
             
                        end
         | 
| 67 89 | 
             
                      end
         | 
| 68 90 |  | 
| 69 | 
            -
                       | 
| 70 | 
            -
             | 
| 71 | 
            -
                      t_start = Time.now.to_f
         | 
| 91 | 
            +
                      t_start = Time.now
         | 
| 72 92 | 
             
                      CLI::UI.raw do
         | 
| 73 | 
            -
                         | 
| 93 | 
            +
                        print(prefix.chop)
         | 
| 94 | 
            +
                        puts frame_style.open(text, color: color)
         | 
| 74 95 | 
             
                      end
         | 
| 75 | 
            -
                      FrameStack.push(color)
         | 
| 96 | 
            +
                      FrameStack.push(color: color, style: frame_style)
         | 
| 76 97 |  | 
| 77 98 | 
             
                      return unless block_given?
         | 
| 78 99 |  | 
| @@ -82,14 +103,14 @@ module CLI | |
| 82 103 | 
             
                        success = yield
         | 
| 83 104 | 
             
                      rescue
         | 
| 84 105 | 
             
                        closed = true
         | 
| 85 | 
            -
                        t_diff =  | 
| 106 | 
            +
                        t_diff = elasped(t_start, timing)
         | 
| 86 107 | 
             
                        close(failure_text, color: :red, elapsed: t_diff)
         | 
| 87 108 | 
             
                        raise
         | 
| 88 109 | 
             
                      else
         | 
| 89 110 | 
             
                        success
         | 
| 90 111 | 
             
                      ensure
         | 
| 91 112 | 
             
                        unless closed
         | 
| 92 | 
            -
                          t_diff =  | 
| 113 | 
            +
                          t_diff = elasped(t_start, timing)
         | 
| 93 114 | 
             
                          if success != false
         | 
| 94 115 | 
             
                            close(success_text, color: color, elapsed: t_diff)
         | 
| 95 116 | 
             
                          else
         | 
| @@ -99,8 +120,8 @@ module CLI | |
| 99 120 | 
             
                      end
         | 
| 100 121 | 
             
                    end
         | 
| 101 122 |  | 
| 102 | 
            -
                    #  | 
| 103 | 
            -
                    #  | 
| 123 | 
            +
                    # Adds a divider in a frame
         | 
| 124 | 
            +
                    # Used to separate information within a single frame
         | 
| 104 125 | 
             
                    #
         | 
| 105 126 | 
             
                    # ==== Attributes
         | 
| 106 127 | 
             
                    #
         | 
| @@ -109,31 +130,38 @@ module CLI | |
| 109 130 | 
             
                    # ==== Options
         | 
| 110 131 | 
             
                    #
         | 
| 111 132 | 
             
                    # * +:color+ - The color of the frame. Defaults to +DEFAULT_FRAME_COLOR+
         | 
| 112 | 
            -
                    # *  | 
| 133 | 
            +
                    # * +frame_style+ - The frame style to use for this frame
         | 
| 113 134 | 
             
                    #
         | 
| 114 135 | 
             
                    # ==== Example
         | 
| 115 136 | 
             
                    #
         | 
| 116 | 
            -
                    #   CLI::UI::Frame. | 
| 137 | 
            +
                    #   CLI::UI::Frame.open('Open') { CLI::UI::Frame.divider('Divider') }
         | 
| 117 138 | 
             
                    #
         | 
| 118 | 
            -
                    # Output:
         | 
| 119 | 
            -
                    #    | 
| 139 | 
            +
                    # Default Output:
         | 
| 140 | 
            +
                    #   ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
         | 
| 141 | 
            +
                    #   ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
         | 
| 142 | 
            +
                    #   ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
         | 
| 120 143 | 
             
                    #
         | 
| 144 | 
            +
                    # ==== Raises
         | 
| 121 145 | 
             
                    #
         | 
| 122 | 
            -
                     | 
| 123 | 
            -
             | 
| 146 | 
            +
                    # MUST be inside an open frame or it raises a +UnnestedFrameException+
         | 
| 147 | 
            +
                    #
         | 
| 148 | 
            +
                    def divider(text, color: nil, frame_style: nil)
         | 
| 149 | 
            +
                      fs_item = FrameStack.pop
         | 
| 150 | 
            +
                      raise UnnestedFrameException, "No frame nesting to unnest" unless fs_item
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                      color = CLI::UI.resolve_color(color) || fs_item.color
         | 
| 153 | 
            +
                      frame_style = CLI::UI.resolve_style(frame_style) || fs_item.frame_style
         | 
| 124 154 |  | 
| 125 | 
            -
                      FrameStack.pop
         | 
| 126 | 
            -
                      kwargs = {}
         | 
| 127 | 
            -
                      if elapsed
         | 
| 128 | 
            -
                        kwargs[:right_text] = "(#{elapsed.round(2)}s)"
         | 
| 129 | 
            -
                      end
         | 
| 130 155 | 
             
                      CLI::UI.raw do
         | 
| 131 | 
            -
                         | 
| 156 | 
            +
                        print(prefix.chop)
         | 
| 157 | 
            +
                        puts frame_style.divider(text, color: color)
         | 
| 132 158 | 
             
                      end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                      FrameStack.push(fs_item)
         | 
| 133 161 | 
             
                    end
         | 
| 134 162 |  | 
| 135 | 
            -
                    #  | 
| 136 | 
            -
                    #  | 
| 163 | 
            +
                    # Closes a frame
         | 
| 164 | 
            +
                    # Automatically called for a block-form +open+
         | 
| 137 165 | 
             
                    #
         | 
| 138 166 | 
             
                    # ==== Attributes
         | 
| 139 167 | 
             
                    #
         | 
| @@ -141,51 +169,70 @@ module CLI | |
| 141 169 | 
             
                    #
         | 
| 142 170 | 
             
                    # ==== Options
         | 
| 143 171 | 
             
                    #
         | 
| 144 | 
            -
                    # * +:color+ - The color of the frame. Defaults to  | 
| 172 | 
            +
                    # * +:color+ - The color of the frame. Defaults to nil
         | 
| 173 | 
            +
                    # * +:elapsed+ - How long did the frame take? Defaults to nil
         | 
| 174 | 
            +
                    # * +frame_style+ - The frame style to use for this frame.  Defaults to nil
         | 
| 145 175 | 
             
                    #
         | 
| 146 176 | 
             
                    # ==== Example
         | 
| 147 177 | 
             
                    #
         | 
| 148 | 
            -
                    #   CLI::UI::Frame. | 
| 178 | 
            +
                    #   CLI::UI::Frame.close('Close')
         | 
| 149 179 | 
             
                    #
         | 
| 150 | 
            -
                    # Output:
         | 
| 151 | 
            -
                    #    | 
| 152 | 
            -
                    #   ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
         | 
| 153 | 
            -
                    #   ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
         | 
| 180 | 
            +
                    # Default Output:
         | 
| 181 | 
            +
                    #   ┗━━ Close ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
         | 
| 154 182 | 
             
                    #
         | 
| 155 183 | 
             
                    # ==== Raises
         | 
| 156 184 | 
             
                    #
         | 
| 157 185 | 
             
                    # MUST be inside an open frame or it raises a +UnnestedFrameException+
         | 
| 158 186 | 
             
                    #
         | 
| 159 | 
            -
                    def  | 
| 187 | 
            +
                    def close(text, color: nil, elapsed: nil, frame_style: nil)
         | 
| 160 188 | 
             
                      fs_item = FrameStack.pop
         | 
| 161 | 
            -
                      raise UnnestedFrameException, " | 
| 162 | 
            -
             | 
| 163 | 
            -
                       | 
| 189 | 
            +
                      raise UnnestedFrameException, "No frame nesting to unnest" unless fs_item
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                      color = CLI::UI.resolve_color(color) || fs_item.color
         | 
| 192 | 
            +
                      frame_style = CLI::UI.resolve_style(frame_style) || fs_item.frame_style
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                      kwargs = {}
         | 
| 195 | 
            +
                      if elapsed
         | 
| 196 | 
            +
                        kwargs[:right_text] = "(#{elapsed.round(2)}s)"
         | 
| 197 | 
            +
                      end
         | 
| 164 198 |  | 
| 165 199 | 
             
                      CLI::UI.raw do
         | 
| 166 | 
            -
                         | 
| 200 | 
            +
                        print(prefix.chop)
         | 
| 201 | 
            +
                        puts frame_style.close(text, color: color, **kwargs)
         | 
| 167 202 | 
             
                      end
         | 
| 168 | 
            -
                      FrameStack.push(item)
         | 
| 169 203 | 
             
                    end
         | 
| 170 204 |  | 
| 171 205 | 
             
                    # Determines the prefix of a frame entry taking multi-nested frames into account
         | 
| 172 206 | 
             
                    #
         | 
| 173 207 | 
             
                    # ==== Options
         | 
| 174 208 | 
             
                    #
         | 
| 175 | 
            -
                    # * +:color+ - The color of the prefix. Defaults to +Thread.current[:cliui_frame_color_override]+ | 
| 209 | 
            +
                    # * +:color+ - The color of the prefix. Defaults to +Thread.current[:cliui_frame_color_override]+
         | 
| 176 210 | 
             
                    #
         | 
| 177 | 
            -
                    def prefix(color:  | 
| 178 | 
            -
                       | 
| 179 | 
            -
             | 
| 180 | 
            -
             | 
| 181 | 
            -
                         | 
| 211 | 
            +
                    def prefix(color: Thread.current[:cliui_frame_color_override])
         | 
| 212 | 
            +
                      +''.tap do |output|
         | 
| 213 | 
            +
                        items = FrameStack.items
         | 
| 214 | 
            +
             | 
| 215 | 
            +
                        items[0..-2].each do |item|
         | 
| 216 | 
            +
                          output << item.color.code << item.frame_style.prefix
         | 
| 217 | 
            +
                        end
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                        if (item = items.last)
         | 
| 220 | 
            +
                          final_color = color || item.color
         | 
| 221 | 
            +
                          output << CLI::UI.resolve_color(final_color).code \
         | 
| 222 | 
            +
                            << item.frame_style.prefix \
         | 
| 223 | 
            +
                            << ' ' \
         | 
| 224 | 
            +
                            << CLI::UI::Color::RESET.code
         | 
| 225 | 
            +
                        end
         | 
| 182 226 | 
             
                      end
         | 
| 183 | 
            -
             | 
| 184 | 
            -
             | 
| 185 | 
            -
             | 
| 186 | 
            -
             | 
| 227 | 
            +
                    end
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                    # The width of a prefix given the number of Frames in the stack
         | 
| 230 | 
            +
                    def prefix_width
         | 
| 231 | 
            +
                      w = FrameStack.items.reduce(0) do |width, item|
         | 
| 232 | 
            +
                        width + item.frame_style.prefix_width
         | 
| 187 233 | 
             
                      end
         | 
| 188 | 
            -
             | 
| 234 | 
            +
             | 
| 235 | 
            +
                      w.zero? ? w : w + 1
         | 
| 189 236 | 
             
                    end
         | 
| 190 237 |  | 
| 191 238 | 
             
                    # Override a color for a given thread.
         | 
| @@ -202,107 +249,19 @@ module CLI | |
| 202 249 | 
             
                      Thread.current[:cliui_frame_color_override] = prev
         | 
| 203 250 | 
             
                    end
         | 
| 204 251 |  | 
| 205 | 
            -
                    # The width of a prefix given the number of Frames in the stack
         | 
| 206 | 
            -
                    #
         | 
| 207 | 
            -
                    def prefix_width
         | 
| 208 | 
            -
                      w = FrameStack.items.size
         | 
| 209 | 
            -
                      w.zero? ? 0 : w + 1
         | 
| 210 | 
            -
                    end
         | 
| 211 | 
            -
             | 
| 212 252 | 
             
                    private
         | 
| 213 253 |  | 
| 214 | 
            -
                     | 
| 215 | 
            -
             | 
| 216 | 
            -
             | 
| 217 | 
            -
             | 
| 218 | 
            -
             | 
| 219 | 
            -
             | 
| 220 | 
            -
             | 
| 221 | 
            -
                       | 
| 222 | 
            -
             | 
| 223 | 
            -
                       | 
| 224 | 
            -
                       | 
| 225 | 
            -
                        prefix << ' ' << text << ' '
         | 
| 226 | 
            -
                      end
         | 
| 227 | 
            -
             | 
| 228 | 
            -
                      termwidth = CLI::UI::Terminal.width
         | 
| 229 | 
            -
             | 
| 230 | 
            -
                      suffix = +''
         | 
| 231 | 
            -
                      if right_text
         | 
| 232 | 
            -
                        suffix << ' ' << right_text << ' '
         | 
| 233 | 
            -
                      end
         | 
| 234 | 
            -
             | 
| 235 | 
            -
                      suffix_width = CLI::UI::ANSI.printing_width(suffix)
         | 
| 236 | 
            -
                      suffix_end   = termwidth - 2
         | 
| 237 | 
            -
                      suffix_start = suffix_end - suffix_width
         | 
| 238 | 
            -
             | 
| 239 | 
            -
                      prefix_width = CLI::UI::ANSI.printing_width(prefix)
         | 
| 240 | 
            -
                      prefix_start = 0
         | 
| 241 | 
            -
                      prefix_end   = prefix_start + prefix_width
         | 
| 242 | 
            -
             | 
| 243 | 
            -
                      if prefix_end > suffix_start
         | 
| 244 | 
            -
                        suffix = ''
         | 
| 245 | 
            -
                        # if prefix_end > termwidth
         | 
| 246 | 
            -
                        # we *could* truncate it, but let's just let it overflow to the
         | 
| 247 | 
            -
                        # next line and call it poor usage of this API.
         | 
| 248 | 
            -
                      end
         | 
| 249 | 
            -
             | 
| 250 | 
            -
                      o = +''
         | 
| 251 | 
            -
             | 
| 252 | 
            -
                      is_ci = ![0, '', nil].include?(ENV['CI'])
         | 
| 253 | 
            -
             | 
| 254 | 
            -
                      # Jumping around the line can cause some unwanted flashes
         | 
| 255 | 
            -
                      o << CLI::UI::ANSI.hide_cursor
         | 
| 256 | 
            -
             | 
| 257 | 
            -
                      o << if is_ci
         | 
| 258 | 
            -
                             # In CI, we can't use absolute horizontal positions because of timestamps.
         | 
| 259 | 
            -
                             # So we move around the line by offset from this cursor position.
         | 
| 260 | 
            -
                             CLI::UI::ANSI.cursor_save
         | 
| 261 | 
            -
                           else
         | 
| 262 | 
            -
                             # Outside of CI, we reset to column 1 so that things like ^C don't
         | 
| 263 | 
            -
                             # cause output misformatting.
         | 
| 264 | 
            -
                             "\r"
         | 
| 265 | 
            -
                           end
         | 
| 266 | 
            -
             | 
| 267 | 
            -
                      o << color.code
         | 
| 268 | 
            -
                      o << CLI::UI::Box::Heavy::HORZ * termwidth # draw a full line
         | 
| 269 | 
            -
                      o << print_at_x(prefix_start, prefix, is_ci)
         | 
| 270 | 
            -
                      o << color.code
         | 
| 271 | 
            -
                      o << print_at_x(suffix_start, suffix, is_ci)
         | 
| 272 | 
            -
                      o << CLI::UI::Color::RESET.code
         | 
| 273 | 
            -
                      o << CLI::UI::ANSI.show_cursor
         | 
| 274 | 
            -
                      o << "\n"
         | 
| 275 | 
            -
             | 
| 276 | 
            -
                      o
         | 
| 277 | 
            -
                    end
         | 
| 278 | 
            -
             | 
| 279 | 
            -
                    def print_at_x(x, str, is_ci)
         | 
| 280 | 
            -
                      if is_ci
         | 
| 281 | 
            -
                        CLI::UI::ANSI.cursor_restore + CLI::UI::ANSI.cursor_forward(x) + str
         | 
| 282 | 
            -
                      else
         | 
| 283 | 
            -
                        CLI::UI::ANSI.cursor_horizontal_absolute(1 + x) + str
         | 
| 284 | 
            -
                      end
         | 
| 285 | 
            -
                    end
         | 
| 286 | 
            -
             | 
| 287 | 
            -
                    module FrameStack
         | 
| 288 | 
            -
                      ENVVAR = 'CLI_FRAME_STACK'
         | 
| 289 | 
            -
             | 
| 290 | 
            -
                      def self.items
         | 
| 291 | 
            -
                        ENV.fetch(ENVVAR, '').split(':').map(&:to_sym)
         | 
| 292 | 
            -
                      end
         | 
| 293 | 
            -
             | 
| 294 | 
            -
                      def self.push(item)
         | 
| 295 | 
            -
                        curr = items
         | 
| 296 | 
            -
                        curr << item.name
         | 
| 297 | 
            -
                        ENV[ENVVAR] = curr.join(':')
         | 
| 298 | 
            -
                      end
         | 
| 299 | 
            -
             | 
| 300 | 
            -
                      def self.pop
         | 
| 301 | 
            -
                        curr = items
         | 
| 302 | 
            -
                        ret = curr.pop
         | 
| 303 | 
            -
                        ENV[ENVVAR] = curr.join(':')
         | 
| 304 | 
            -
                        ret.nil? ? nil : ret.to_sym
         | 
| 305 | 
            -
                      end
         | 
| 254 | 
            +
                    # If timing is:
         | 
| 255 | 
            +
                    #   Numeric: return it
         | 
| 256 | 
            +
                    #   false: return nil
         | 
| 257 | 
            +
                    #   true or nil: defaults to Time.new
         | 
| 258 | 
            +
                    #   Time: return the difference with start
         | 
| 259 | 
            +
                    def elasped(start, timing)
         | 
| 260 | 
            +
                      return timing if timing.is_a?(Numeric)
         | 
| 261 | 
            +
                      return if timing.is_a?(FalseClass)
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                      timing = Time.new if timing.is_a?(TrueClass) || timing.nil?
         | 
| 264 | 
            +
                      timing - start
         | 
| 306 265 | 
             
                    end
         | 
| 307 266 | 
             
                  end
         | 
| 308 267 | 
             
                end
         |