lib/dev/ui/frame.rb in dev-ui-0.1.0 vs lib/dev/ui/frame.rb in dev-ui-0.1.1

- old
+ new

@@ -1,19 +1,54 @@ require 'dev/ui' module Dev module UI module Frame + class UnnestedFrameException < StandardError; end class << self DEFAULT_FRAME_COLOR = Dev::UI.resolve_color(:cyan) + # 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. + # * 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 + # + # ==== Example + # + # ===== Block Form (Assumes +Dev::UI::StdoutRouter.enable+ has been called) + # + # Dev::UI::Frame.open('Open') { puts 'hi' } + # + # Output: + # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # ┃ hi + # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (0.0s) ━━ + # + # ===== Blockless Form + # + # Dev::UI::Frame.open('Open') + # + # Output: + # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # + # def open( text, color: DEFAULT_FRAME_COLOR, failure_text: nil, success_text: nil, @@ -62,10 +97,30 @@ end end end 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 +DEFAULT_FRAME_COLOR+ + # * +:elapsed+ - How long did the frame take? Defaults to nil + # + # ==== Example + # + # Dev::UI::Frame.close('Close') + # + # Output: + # ┗━━ Close ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # + # def close(text, color: DEFAULT_FRAME_COLOR, elapsed: nil) color = Dev::UI.resolve_color(color) FrameStack.pop kwargs = {} @@ -75,20 +130,52 @@ Dev::UI.raw do puts edge(text, color: color, first: Dev::UI::Box::Heavy::BL, **kwargs) 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+ + # + # ==== Example + # + # Dev::UI::Frame.open('Open') { Dev::UI::Frame.divider('Divider') } + # + # Output: + # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # + # ==== Raises + # + # MUST be inside an open frame or it raises a +UnnestedFrameException+ + # def divider(text, color: nil) + fs_item = FrameStack.pop + raise UnnestedFrameException, "no frame nesting to unnest" unless fs_item color = Dev::UI.resolve_color(color) - item = Dev::UI.resolve_color(FrameStack.pop) + item = Dev::UI.resolve_color(fs_item) Dev::UI.raw do puts edge(text, color: (color || item), first: Dev::UI::Box::Heavy::DIV) end FrameStack.push(item) 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[:devui_frame_color_override]+ or nil + # def prefix(color: nil) pfx = String.new items = FrameStack.items items[0..-2].each do |item| pfx << Dev::UI.resolve_color(item).code << Dev::UI::Box::Heavy::VERT @@ -99,18 +186,26 @@ << Dev::UI::Box::Heavy::VERT << ' ' << Dev::UI::Color::RESET.code end pfx end + # Override a color for a given thread. + # + # ==== Attributes + # + # * +color+ - The color to override to + # def with_frame_color_override(color) prev = Thread.current[:devui_frame_color_override] Thread.current[:devui_frame_color_override] = color yield ensure Thread.current[:devui_frame_color_override] = prev end + # The width of a prefix given the number of Frames in the stack + # def prefix_width w = FrameStack.items.size w.zero? ? 0 : w + 1 end @@ -152,19 +247,42 @@ # next line and call it poor usage of this API. end o = String.new + is_ci = ![0, '', nil].include?(ENV['CI']) + + # Jumping around the line can cause some unwanted flashes + o << Dev::UI::ANSI.hide_cursor + + if is_ci + # In CI, we can't use absolute horizontal positions because of timestamps. + # So we move around the line by offset from this cursor position. + o << Dev::UI::ANSI.cursor_save + else + # Outside of CI, we reset to column 1 so that things like ^C don't + # cause output misformatting. + o << "\r" + end + o << color.code o << Dev::UI::Box::Heavy::HORZ * termwidth # draw a full line - o << Dev::UI::ANSI.cursor_horizontal_absolute(1 + prefix_start) - o << prefix - o << Dev::UI::ANSI.cursor_horizontal_absolute(1 + suffix_start) - o << suffix + o << print_at_x(prefix_start, prefix, is_ci) + o << color.code + o << print_at_x(suffix_start, suffix, is_ci) o << Dev::UI::Color::RESET.code + o << Dev::UI::ANSI.show_cursor o << "\n" o + end + + def print_at_x(x, str, is_ci) + if is_ci + Dev::UI::ANSI.cursor_restore + Dev::UI::ANSI.cursor_forward(x) + str + else + Dev::UI::ANSI.cursor_horizontal_absolute(1 + x) + str + end end module FrameStack ENVVAR = 'DEV_FRAME_STACK'