# coding: utf-8 require 'cli/ui' require 'cli/ui/frame/frame_stack' require 'cli/ui/frame/frame_style' module CLI module UI module Frame class UnnestedFrameException < StandardError; end class << self DEFAULT_FRAME_COLOR = CLI::UI.resolve_color(:cyan) def frame_style @frame_style ||= FrameStyle::Box end # Set the default frame style. # # Raises ArgumentError if +frame_style+ is not valid # # ==== Attributes # # * +symbol+ or +FrameStyle+ - the default frame style to use for frames # def frame_style=(frame_style) @frame_style = CLI::UI.resolve_style(frame_style) end # Opens a new frame. Can be nested # Can be invoked in two ways: block and blockless # * In block form, the frame is closed automatically when the block returns # * In blockless form, caller MUST call +Frame.close+ when the frame is logically done # * Blockless form is strongly discouraged in cases where block form can be made to work # # https://user-images.githubusercontent.com/3074765/33799861-cb5dcb5c-dd01-11e7-977e-6fad38cee08c.png # # The return value of the block determines if the block is a "success" or a "failure" # # ==== Attributes # # * +text+ - (required) the text/title to output in the frame # # ==== Options # # * +:color+ - The color of the frame. Defaults to +DEFAULT_FRAME_COLOR+ # * +:failure_text+ - If the block failed, what do we output? Defaults to nil # * +:success_text+ - If the block succeeds, what do we output? Defaults to nil # * +:timing+ - How long did the frame content take? Invalid for blockless. Defaults to true for the block form # * +frame_style+ - The frame style to use for this frame # # ==== Example # # ===== Block Form (Assumes +CLI::UI::StdoutRouter.enable+ has been called) # # CLI::UI::Frame.open('Open') { puts 'hi' } # # Default Output: # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # ┃ hi # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (0.0s) ━━ # # ===== Blockless Form # # CLI::UI::Frame.open('Open') # # Default Output: # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # # def open( text, color: DEFAULT_FRAME_COLOR, failure_text: nil, success_text: nil, timing: nil, frame_style: self.frame_style ) frame_style = CLI::UI.resolve_style(frame_style) color = CLI::UI.resolve_color(color) unless block_given? if failure_text raise ArgumentError, 'failure_text is not compatible with blockless invocation' elsif success_text raise ArgumentError, 'success_text is not compatible with blockless invocation' elsif timing raise ArgumentError, 'timing is not compatible with blockless invocation' end end t_start = Time.now CLI::UI.raw do print(prefix.chop) puts frame_style.open(text, color: color) end FrameStack.push(color: color, style: frame_style) return unless block_given? closed = false begin success = false success = yield rescue closed = true t_diff = elasped(t_start, timing) close(failure_text, color: :red, elapsed: t_diff) raise else success ensure unless closed t_diff = elasped(t_start, timing) if success != false close(success_text, color: color, elapsed: t_diff) else close(failure_text, color: :red, elapsed: t_diff) end end end end # Adds a divider in a frame # Used to separate information within a single frame # # ==== Attributes # # * +text+ - (required) the text/title to output in the frame # # ==== Options # # * +:color+ - The color of the frame. Defaults to +DEFAULT_FRAME_COLOR+ # * +frame_style+ - The frame style to use for this frame # # ==== Example # # CLI::UI::Frame.open('Open') { CLI::UI::Frame.divider('Divider') } # # Default Output: # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # # ==== Raises # # MUST be inside an open frame or it raises a +UnnestedFrameException+ # def divider(text, color: nil, frame_style: nil) fs_item = FrameStack.pop raise UnnestedFrameException, 'No frame nesting to unnest' unless fs_item color = CLI::UI.resolve_color(color) || fs_item.color frame_style = CLI::UI.resolve_style(frame_style) || fs_item.frame_style CLI::UI.raw do print(prefix.chop) puts frame_style.divider(text, color: color) end FrameStack.push(fs_item) end # Closes a frame # Automatically called for a block-form +open+ # # ==== Attributes # # * +text+ - (required) the text/title to output in the frame # # ==== Options # # * +:color+ - The color of the frame. Defaults to nil # * +:elapsed+ - How long did the frame take? Defaults to nil # * +frame_style+ - The frame style to use for this frame. Defaults to nil # # ==== Example # # CLI::UI::Frame.close('Close') # # Default Output: # ┗━━ Close ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # # ==== Raises # # MUST be inside an open frame or it raises a +UnnestedFrameException+ # def close(text, color: nil, elapsed: nil, frame_style: nil) fs_item = FrameStack.pop raise UnnestedFrameException, 'No frame nesting to unnest' unless fs_item color = CLI::UI.resolve_color(color) || fs_item.color frame_style = CLI::UI.resolve_style(frame_style) || fs_item.frame_style kwargs = {} if elapsed kwargs[:right_text] = "(#{elapsed.round(2)}s)" end CLI::UI.raw do print(prefix.chop) puts frame_style.close(text, color: color, **kwargs) end end # Determines the prefix of a frame entry taking multi-nested frames into account # # ==== Options # # * +:color+ - The color of the prefix. Defaults to +Thread.current[:cliui_frame_color_override]+ # def prefix(color: Thread.current[:cliui_frame_color_override]) +''.tap do |output| items = FrameStack.items items[0..-2].each do |item| output << item.color.code << item.frame_style.prefix end if (item = items.last) final_color = color || item.color output << CLI::UI.resolve_color(final_color).code \ << item.frame_style.prefix \ << ' ' \ << CLI::UI::Color::RESET.code end end end # The width of a prefix given the number of Frames in the stack def prefix_width w = FrameStack.items.reduce(0) do |width, item| width + item.frame_style.prefix_width end w.zero? ? w : w + 1 end # Override a color for a given thread. # # ==== Attributes # # * +color+ - The color to override to # def with_frame_color_override(color) prev = Thread.current[:cliui_frame_color_override] Thread.current[:cliui_frame_color_override] = color yield ensure Thread.current[:cliui_frame_color_override] = prev end private # If timing is: # Numeric: return it # false: return nil # true or nil: defaults to Time.new # Time: return the difference with start def elasped(start, timing) return timing if timing.is_a?(Numeric) return if timing.is_a?(FalseClass) timing = Time.new if timing.is_a?(TrueClass) || timing.nil? timing - start end end end end end