summaryrefslogtreecommitdiff
path: root/lib/chef/formatters/indentable_output_stream.rb
blob: 73fc7ecf9a0bfa4d4327c266914843c5af06e0fb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
class Chef
  module Formatters
    # Handles basic indentation and colorization tasks
    class IndentableOutputStream

      attr_reader :out, :err, :line_started, :semaphore

      attr_accessor :indent, :current_stream

      def initialize(out, err)
        @out, @err = out, err
        @indent = 0
        @line_started = false
        @semaphore = Mutex.new
      end

      # pastel.decorate is a lightweight replacement for highline.color
      def pastel
        @pastel ||= begin
          require "pastel"
          Pastel.new
        end
      end

      # Print the start of a new line.  This will terminate any existing lines and
      # cause indentation but will not move to the next line yet (future 'print'
      # and 'puts' statements will stay on this line).
      #
      # @param string [String]
      # @param args [Array<Hash,Symbol>]
      def start_line(string, *args)
        print(string, from_args(args, start_line: true))
      end

      # Print a line.  This will continue from the last start_line or print,
      # or start a new line and indent if necessary.
      #
      # @param string [String]
      # @param args [Array<Hash,Symbol>]
      def puts(string, *args)
        print(string, from_args(args, end_line: true))
      end

      # Print an entire line from start to end.  This will terminate any existing
      # lines and cause indentation.
      #
      # @param string [String]
      # @param args [Array<Hash,Symbol>]
      def puts_line(string, *args)
        print(string, from_args(args, start_line: true, end_line: true))
      end

      # Print a raw chunk
      def <<(obj)
        print(obj)
      end

      # Print a string.
      #
      # == Arguments
      # string: string to print.
      # options: a hash with these possible options:
      # - :stream => OBJ: unique identifier for a stream. If two prints have
      #           different streams, they will print on separate lines.
      #           Otherwise, they will stay together.
      # - :start_line => BOOLEAN: if true, print will begin on a blank (indented) line.
      # - :end_line => BOOLEAN: if true, current line will be ended.
      # - :name => STRING: a name to prefix in front of a stream. It will be printed
      #           once (with the first line of the stream) and subsequent lines
      #           will be indented to match.
      #
      # == Alternative
      #
      # You may also call print('string', :red) (https://github.com/piotrmurach/pastel#3-supported-colors)
      def print(string, *args)
        options = from_args(args)

        # Make sure each line stays a unit even with threads sending output
        semaphore.synchronize do
          if should_start_line?(options)
            move_to_next_line
          end

          print_string(string, options)

          if should_end_line?(options)
            move_to_next_line
          end
        end
      end

      private

      def should_start_line?(options)
        options[:start_line] || @current_stream != options[:stream]
      end

      def should_end_line?(options)
        options[:end_line] && @line_started
      end

      def from_args(colors, merge_options = {})
        if colors.size == 1 && colors[0].is_a?(Hash)
          merge_options.merge(colors[0])
        else
          merge_options.merge({ colors: colors })
        end
      end

      def print_string(string, options)
        if string.empty?
          if options[:end_line]
            print_line("", options)
          end
        else
          string.lines.each do |line|
            print_line(line, options)
          end
        end
      end

      def print_line(line, options)
        indent_line(options)

        # Note that the next line will need to be started
        if line[-1..-1] == "\n"
          @line_started = false
        end

        if Chef::Config[:color] && options[:colors]
          @out.print pastel.decorate(line, *options[:colors])
        else
          @out.print line
        end
      end

      def move_to_next_line
        if @line_started
          @out.puts ""
          @line_started = false
        end
      end

      def indent_line(options)
        unless @line_started

          # Print indents.  If there is a stream name, either print it (if we're
          # switching streams) or print enough blanks to match
          # the indents.
          if options[:name]
            if @current_stream != options[:stream]
              @out.print "#{(" " * indent)}[#{options[:name]}] "
            else
              @out.print " " * (indent + 3 + options[:name].size)
            end
          else
            # Otherwise, just print indents.
            @out.print " " * indent
          end

          if @current_stream != options[:stream]
            @current_stream = options[:stream]
          end

          @line_started = true
        end
      end
    end
  end
end