# encoding: utf-8 # # This file is part of the bovem gem. Copyright (C) 2013 and above Shogun . # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php. # module Bovem # List of valid terminal colors. TERM_COLORS = { black: 0, red: 1, green: 2, yellow: 3, blue: 4, magenta: 5, cyan: 6, white: 7, default: 9} # List of valid terminal text effects. TERM_EFFECTS = { reset: 0, bright: 1, italic: 3, underline: 4, blink: 5, inverse: 7, hide: 8 } # Methods of the {Console Console} class. module ConsoleMethods # Methods for handling styles in the terminal. module StyleHandling extend ActiveSupport::Concern # Class methods for handling styles in the terminal. module ClassMethods # Parse a style and returns terminal codes. # # Supported styles and colors are those in {Bovem::TERM\_COLORS} and {Bovem::TERM\_EFFECTS}. You can also prefix colors with `bg_` (like `bg_red`) for background colors. # # @param style [String] The style to parse. # @return [String] A string with ANSI color codes. def parse_style(style) style = style.ensure_string.strip.parameterize if style.present? then Bovem::Console.replace_term_code(Bovem::TERM_EFFECTS, style, 0) || Bovem::Console.replace_term_code(Bovem::TERM_COLORS, style, 30) || Bovem::Console.replace_term_code(Bovem::TERM_COLORS, style.gsub(/^bg_/, ""), 40) || "" else "" end end # Parses a set of styles and returns terminals codes. # Supported styles and colors are those in {Bovem::TERM\_COLORS} and {Bovem::TERM\_EFFECTS}. You can also prefix colors with `bg_` (like `bg_red`) for background colors. # # @param styles [String] The styles to parse. # @return [String] A string with ANSI color codes. def parse_styles(styles) styles.split(/\s*[\s,-]\s*/).map { |s| parse_style(s) }.join("") end # # Replaces a terminal code. # # @param codes [Array] The valid list of codes. # @param code [String] The code to lookup. # @param modifier [Fixnum] The modifier to apply to the code. # @return [String|nil] The terminal code or `nil` if the code was not found. def replace_term_code(codes, code, modifier = 0) sym = code.to_sym codes.include?(sym) ? "\e[#{modifier + codes[sym]}m" : nil end # Replaces colors markers in a string. # # You can specify markers by enclosing in `{mark=[style]}` and `{/mark}` tags. Separate styles with spaces, dashes or commas. Nesting markers is supported. # # Example: # # ```ruby # Bovem::Console.new.replace_markers("{mark=bright bg_red}{mark=green}Hello world!{/mark}{/mark}") # # => "\e[1m\e[41m\e[32mHello world!\e[1m\e[41m\e[0m" # ``` # # @param message [String] The message to analyze. # @param plain [Boolean] If ignore (cleanify) color markers into the message. # @return [String] The replaced message. # @see #parse_style def replace_markers(message, plain = false) stack = [] message.ensure_string.gsub(/((\{mark=([a-z\-_\s,]+)\})|(\{\/mark\}))/mi) do if $1 == "{/mark}" then # If it is a tag, pop from the latest opened. stack.pop plain || stack.blank? ? "" : Bovem::Console.parse_styles(stack.last) else styles = $3.ensure_string replacement = plain ? "" : Bovem::Console.parse_styles(styles) if replacement.length > 0 then stack << "reset" if stack.blank? stack << styles end replacement end end end end # Replaces colors markers in a string. # # @see .replace_markers # # @param message [String] The message to analyze. # @param plain [Boolean] If ignore (cleanify) color markers into the message. # @return [String] The replaced message. def replace_markers(message, plain = false) Bovem::Console.replace_markers(message, plain) end end # Methods for formatting output messages. module Output # Sets the new indentation width. # # @param width [Fixnum] The new width. # @param is_absolute [Boolean] If the new width should not be added to the current one but rather replace it. # @return [Fixnum] The new indentation width. def set_indentation(width, is_absolute = false) @indentation = [(!is_absolute ? @indentation : 0) + width, 0].max.to_i end # Resets indentation width to `0`. # # @return [Fixnum] The new indentation width. def reset_indentation @indentation = 0 end # Starts a indented region of text. # # @param width [Fixnum] The new width. # @param is_absolute [Boolean] If the new width should not be added to the current one but rather replace it. # @return [Fixnum] The new indentation width. def with_indentation(width = 3, is_absolute = false) old = @indentation set_indentation(width, is_absolute) yield set_indentation(old, true) @indentation end # Wraps a message in fixed line width. # # @param message [String] The message to wrap. # @param width [Fixnum] The maximum width of a line. Default to the current line width. # @return [String] The wrapped message. def wrap(message, width = nil) if width.to_integer <= 0 then message else width = (width == true || width.to_integer < 0 ? line_width : width.to_integer) message.split("\n").map { |line| line.length > width ? line.gsub(/(.{1,#{width}})(\s+|$)/, "\\1\n").strip : line }.join("\n") end end # Indents a message. # # @param message [String] The message to indent. # @param width [Fixnum] The indentation width. `true` means to use the current indentation, a negative value of `-x` will indent of `x` absolute spaces. `nil` or `false` will skip indentation. # @param newline_separator [String] The character used for newlines. # @return [String] The indented message. def indent(message, width = true, newline_separator = "\n") if width.to_integer != 0 then width = (width.is_a?(TrueClass) ? 0 : width.to_integer) width = width < 0 ? -width : @indentation + width message = message.split(newline_separator).map {|line| (@indentation_string * width) + line }.join(newline_separator) end message end # Formats a message. # # You can style text by using `{mark}` and `{/mark}` syntax. # # @see #replace_markers # # @param message [String] The message to format. # @param suffix [Object] If not `nil` or `false`, a suffix to add to the message. `true` means to add `\n`. # @param indent [Object] If not `nil` or `false`, the width to use for indentation. `true` means to use the current indentation, a negative value of `-x` will indent of `x` absolute spaces. # @param wrap [Object] If not `nil` or `false`, the maximum length of a line. `true` means the current line width. # @param plain [Boolean] If ignore color markers into the message. # @return [String] The formatted message. def format(message, suffix = "\n", indent = true, wrap = true, plain = false) rv = message rv = replace_markers(rv, plain) # Replace markers # Compute the real width available for the screen, if we both indent and wrap if wrap.is_a?(TrueClass) then wrap = line_width if indent.is_a?(TrueClass) then wrap -= @indentation else indent_i = indent.to_integer wrap -= (indent_i > 0 ? @indentation : 0) + indent_i end end rv = indent(wrap(rv, wrap), indent) # Wrap & Indent rv += (suffix.is_a?(TrueClass) ? "\n" : suffix.ensure_string) if suffix # Add the suffix rv end # Formats a message to be written right-aligned. # # @param message [String] The message to format. # @param width [Fixnum] The screen width. If `true`, it is automatically computed. # @param go_up [Boolean] If go up one line before formatting. # @param plain [Boolean] If ignore color markers into the message. # @return [String] The formatted message. def format_right(message, width = true, go_up = true, plain = false) message = replace_markers(message, plain) width = (width == true || width.to_integer < 1 ? line_width : to_integer) # Get padding padding = width - message.to_s.gsub(/(\e\[[0-9]*[a-z]?)|(\\n)/i, "").length # Return "#{go_up ? "\e[A" : ""}\e[0G\e[#{padding}C#{message}" end # Embeds a message in a style. # # @param message [String] The message to emphasize. # @param style [String] The style to use. # @return [String] The emphasized message. def emphasize(message, style = "bright") "{mark=#{style}}#{message}{/mark}" end end # Methods for logging activities to the user. module Logging extend ActiveSupport::Concern # Class methods for logging activities to the user. module ClassMethods # Returns the minimum length of a banner, not including brackets and leading spaces. # @return [Fixnum] The minimum length of a banner. def min_banner_length 1 end end # Writes a message. # # @param message [String] The message to format. # @param suffix [Object] If not `nil` or `false`, a suffix to add to the message. `true` means to add `\n`. # @param indent [Object] If not `nil` or `false`, the width to use for indentation. `true` means to use the current indentation, a negative value of `-x` will indent of `x` absolute spaces. # @param wrap [Object] If not `nil` or `false`, the maximum length of a line for wrapped text. `true` means the current line width. # @param plain [Boolean] If ignore color markers into the message. # @param print [Boolean] If `false`, the result will be returned instead of be printed. # @return [String] The printed message. # # @see #format def write(message, suffix = "\n", indent = true, wrap = false, plain = false, print = true) rv = format(message, suffix, indent, wrap, plain) Kernel.puts(rv) if print rv end # Writes a message, aligning to a call with an empty banner. # # @param message [String] The message to format. # @param suffix [Object] If not `nil` or `false`, a suffix to add to the message. `true` means to add `\n`. # @param indent [Object] If not `nil` or `false`, the width to use for indentation. `true` means to use the current indentation, a negative value of `-x` will indent of `x` absolute spaces. # @param wrap [Object] If not `nil` or `false`, the maximum length of a line for wrapped text. `true` means the current line width. # @param plain [Boolean] If ignore color markers into the message. # @param print [Boolean] If `false`, the result will be returned instead of be printed. # @return [String] The printed message. # # @see #format def write_banner_aligned(message, suffix = "\n", indent = true, wrap = false, plain = false, print = true) write((" " * (Bovem::Console.min_banner_length + 3)) + message.ensure_string, suffix, indent, wrap, plain, print) end # Writes a status to the output. Valid values are `:ok`, `:pass`, `:fail`, `:warn`. # # @param status [Symbol] The status to write. # @param plain [Boolean] If not use colors. # @param go_up [Boolean] If go up one line before formatting. # @param right [Boolean] If to print results on the right. # @param print [Boolean] If `false`, the result will be returned instead of be printed. # @return [Array] An dictionary with `:label` and `:color` keys for the status. def status(status, plain = false, go_up = true, right = true, print = true) statuses = { ok: {label: " OK ", color: "bright green"}, pass: {label: "PASS", color: "bright cyan"}, warn: {label: "WARN", color: "bright yellow"}, fail: {label: "FAIL", color: "bright red"} } statuses.default = statuses[:ok] rv = statuses[status] if print then banner = get_banner(rv[:label], rv[:color]) if right then Kernel.puts(format_right(banner + " ", true, go_up, plain)) else Kernel.puts(format(banner + " ", "\n", true, true, plain)) end end rv end # Gets a banner for the messages. # # @param label [String] The label for the banner. # @param base_color [String] The color for the label. # @param full_colored [String] If all the message should be of the label color. # @param bracket_color [String] The color of the brackets. # @param brackets [Array] An array of dimension 2 to use for brackets. # @return [String] The banner. # @see #format def get_banner(label, base_color, full_colored = false, bracket_color = "blue", brackets = ["[", "]"]) label = label.rjust(Bovem::Console.min_banner_length, " ") brackets = brackets.ensure_array bracket_color = base_color if full_colored "{mark=%s}%s{mark=%s}%s{/mark}%s{/mark}" % [bracket_color.parameterize, brackets[0], base_color.parameterize, label, brackets[1]] end # Formats a progress for pretty printing. # # @param current [Fixnum] The current progress index (e.g. the number of the current operation). # @param total [Fixnum] The total progress index (e.g. the total number of operations). # @param type [Symbol] The progress type. Can be `:list` (e.g. 01/15) or `:percentage` (e.g. 99.56%). # @param precision [Fixnum] The precision of the percentage to return. *Ignored for list progress.* # @return [String] The formatted progress. def progress(current, total, type = :list, precision = 0) if type == :list then @progress_list_widths ||= {} @progress_list_widths[total] ||= total.to_s.length "%0#{@progress_list_widths[total]}d/%d" % [current, total] else precision = [0, precision].max result = total == 0 ? 100 : (100 * (current.to_f / total)) ("%0.#{precision}f %%" % result.round(precision)).rjust(5 + (precision > 0 ? precision + 1 : 0)) end end # Writes a message prepending a green banner. # # @param message [String] The message to format. # @param suffix [Object] If not `nil` or `false`, a suffix to add to the message. `true` means to add `\n`. # @param indent [Object] If not `nil` or `false`, the width to use for indentation. `true` means to use the current indentation, a negative value of `-x` will indent of `x` absolute spaces. # @param wrap [Object] If not `nil` or `false`, the maximum length of a line for wrapped text. `true` means the current line width. # @param plain [Boolean] If ignore color markers into the message. # @param indented_banner [Boolean] If also the banner should be indented. # @param full_colored [Boolean] If the banner should be fully colored. # @param print [Boolean] If `false`, the result will be returned instead of be printed. # # @see #format def begin(message, suffix = "\n", indent = true, wrap = false, plain = false, indented_banner = false, full_colored = false, print = true) banner = get_banner("*", "bright green", full_colored) message = indent(message, indented_banner ? 0 : indent) write(banner + " " + message, suffix, indented_banner ? indent : 0, wrap, plain, print) end # Writes a message prepending a red banner and then quits the application. # # @param message [String] The message to format. # @param suffix [Object] If not `nil` or `false`, a suffix to add to the message. `true` means to add `\n`. # @param indent [Object] If not `nil` or `false`, the width to use for indentation. `true` means to use the current indentation, a negative value of `-x` will indent of `x` absolute spaces. # @param wrap [Object] If not `nil` or `false`, the maximum length of a line for wrapped text. `true` means the current line width. # @param plain [Boolean] If ignore color markers into the message. # @param indented_banner [Boolean] If also the banner should be indented. # @param full_colored [Boolean] If the banner should be fully colored. # @param return_code [Fixnum] The code to return to the shell. # @param print [Boolean] If `false`, the result will be returned instead of be printed. # # @see #format def fatal(message, suffix = "\n", indent = true, wrap = false, plain = false, indented_banner = false, full_colored = false, return_code = -1, print = true) error(message, suffix, indent, wrap, plain, indented_banner, full_colored, print) Kernel.exit(return_code.to_integer(-1)) end # Writes a message prepending a cyan banner. # # @param message [String] The message to format. # @param suffix [Object] If not `nil` or `false`, a suffix to add to the message. `true` means to add `\n`. # @param indent [Object] If not `nil` or `false`, the width to use for indentation. `true` means to use the current indentation, a negative value of `-x` will indent of `x` absolute spaces. # @param wrap [Object] If not `nil` or `false`, the maximum length of a line for wrapped text. `true` means the current line width. # @param plain [Boolean] If ignore color markers into the message. # @param indented_banner [Boolean] If also the banner should be indented. # @param full_colored [Boolean] If the banner should be fully colored. # @param print [Boolean] If `false`, the result will be returned instead of be printed. # @param banner [Array] An array with at last letter and style to use for the banner. # # @see #format def info(message, suffix = "\n", indent = true, wrap = false, plain = false, indented_banner = false, full_colored = false, print = true, *banner) banner = banner.ensure_array(nil, true, true, true) banner = ["I", "bright cyan"] if banner.blank? banner = get_banner(banner[0], banner[1], full_colored) message = indent(message, indented_banner ? 0 : indent) write(banner + " " + message, suffix, indented_banner ? indent : 0, wrap, plain, print) end # Writes a message prepending a magenta banner. # # @param message [String] The message to format. # @param suffix [Object] If not `nil` or `false`, a suffix to add to the message. `true` means to add `\n`. # @param indent [Object] If not `nil` or `false`, the width to use for indentation. `true` means to use the current indentation, a negative value of `-x` will indent of `x` absolute spaces. # @param wrap [Object] If not `nil` or `false`, the maximum length of a line for wrapped text. `true` means the current line width. # @param plain [Boolean] If ignore color markers into the message. # @param indented_banner [Boolean] If also the banner should be indented. # @param full_colored [Boolean] If the banner should be fully colored. # @param print [Boolean] If `false`, the result will be returned instead of be printed. # # @see #format def debug(message, suffix = "\n", indent = true, wrap = false, plain = false, indented_banner = false, full_colored = false, print = true) info(message, suffix, indent, wrap, plain, indented_banner, full_colored, print, ["D", "bright magenta"]) end # Writes a message prepending a yellow banner. # # @param message [String] The message to format. # @param suffix [Object] If not `nil` or `false`, a suffix to add to the message. `true` means to add `\n`. # @param indent [Object] If not `nil` or `false`, the width to use for indentation. `true` means to use the current indentation, a negative value of `-x` will indent of `x` absolute spaces. # @param wrap [Object] If not `nil` or `false`, the maximum length of a line for wrapped text. `true` means the current line width. # @param plain [Boolean] If ignore color markers into the message. # @param indented_banner [Boolean] If also the banner should be indented. # @param full_colored [Boolean] If the banner should be fully colored. # @param print [Boolean] If `false`, the result will be returned instead of be printed. # # @see #format def warn(message, suffix = "\n", indent = true, wrap = false, plain = false, indented_banner = false, full_colored = false, print = true) warn_banner = ["W", "bright yellow"] info(message, suffix, indent, wrap, plain, indented_banner, full_colored, print, warn_banner) end # Writes a message prepending a red banner. # # @param message [String] The message to format. # @param suffix [Object] If not `nil` or `false`, a suffix to add to the message. `true` means to add `\n`. # @param indent [Object] If not `nil` or `false`, the width to use for indentation. `true` means to use the current indentation, a negative value of `-x` will indent of `x` absolute spaces. # @param wrap [Object] If not `nil` or `false`, the maximum length of a line for wrapped text. `true` means the current line width. # @param plain [Boolean] If ignore color markers into the message. # @param indented_banner [Boolean] If also the banner should be indented. # @param full_colored [Boolean] If the banner should be fully colored. # @param print [Boolean] If `false`, the result will be returned instead of be printed. # # @see #format def error(message, suffix = "\n", indent = true, wrap = false, plain = false, indented_banner = false, full_colored = false, print = true) info(message, suffix, indent, wrap, plain, indented_banner, full_colored, print, "E", "bright red") end end # Methods to interact with the user and other processes. module Interactions extend ActiveSupport::Concern # Class methods to interact with the user and other processes. module ClassMethods # Executes a command and returns its output. # # @param command [String] The command to execute. # @return [String] The command's output. def execute(command) %x{#{command}} end end # Reads a string from the console. # # @param prompt [String|Boolean] A prompt to show. If `true`, `Please insert a value:` will be used, if `nil` or `false` no prompt will be shown. # @param default_value [String] Default value if user simply pressed the enter key. # @param validator [Array|Regexp] An array of values or a Regexp to match the submitted value against. # @param echo [Boolean] If to show submitted text to the user. **Not supported and thus ignored on Rubinius.** def read(prompt = true, default_value = nil, validator = nil, echo = true) prompt = sanitize_prompt(prompt) validator = sanitize_validator(validator) begin catch(:reply) do while true do reply = validate_input_value(read_input_value(prompt, echo, default_value), validator) handle_reply(reply) end end rescue Interrupt default_value end end # Executes a block of code in a indentation region and then prints out and ending status message. # # @param message [String] The message to format. # @param suffix [Object] If not `nil` or `false`, a suffix to add to the message. `true` means to add `\n`. # @param indent [Object] If not `nil` or `false`, the width to use for indentation. `true` means to use the current indentation, a negative value of `-x` will indent of `x` absolute spaces. # @param wrap [Object] If not `nil` or `false`, the maximum length of a line for wrapped text. `true` means the current line width. # @param plain [Boolean] If ignore color markers into the message. # @param indented_banner [Boolean] If also the banner should be indented. # @param full_colored [Boolean] If the banner should be fully colored. # @param block_indentation [Fixnum] The new width for the indented region. # @param block_indentation_absolute [Boolean] If the new width should not be added to the current one but rather replace it. # @return [Symbol] The exit status for the block. def task(message = nil, suffix = "\n", indent = true, wrap = false, plain = false, indented_banner = false, full_colored = false, block_indentation = 2, block_indentation_absolute = false) status = nil self.begin(message, suffix, indent, wrap, plain, indented_banner, full_colored) if message.present? with_indentation(block_indentation, block_indentation_absolute) do rv = block_given? ? yield.ensure_array : [:ok] # Execute block exit_task(message, rv, plain) # Handle task exit status = rv[0] # Return value end status end private # Handles task exit. # # @param message [String] The message to format. # @param rv [Array] An array with exit status and exit code. # @param plain [Boolean] If ignore color markers into the message. def exit_task(message, rv, plain) if rv[0] == :fatal then status(:fail, plain) exit(rv.length > 1 ? rv[1].to_integer : -1) else status(rv[0], plain) if message.present? end end # Returns a prompt for input prompting. # # @param prompt [String] # @return [String|nil] The prompt to use or `nil`, if no message must be prompted. def sanitize_prompt(prompt) if prompt.present? (prompt.is_a?(TrueClass) ? i18n.console.prompt : prompt).gsub(/:?\s*$/, "") + ": " else nil end end # Make sure that the validators are an array of string if not a regexp. # # @param validator [String|Regexp] The validator to sanitize. # @return [Object] A list of strings, a Regexp or nil. def sanitize_validator(validator) validator.present? && !validator.is_a?(::Regexp) ? validator.ensure_array(nil, true, true, true, :ensure_string) : validator end # Read an input from the terminal. # # @param prompt [String] A message to show to the user. # @param echo [Boolean] If disable echoing. **Not supported and therefore ignored on Rubinius.** # @param default_value [Object] A default value to enter if the user just pressed the enter key. # @return [Object] The read value. def read_input_value(prompt, echo, default_value = nil) if prompt then Kernel.print(format(prompt, false, false)) $stdout.flush end reply = (echo || !$stdin.respond_to?(:noecho) ? $stdin.gets : $stdin.noecho(&:gets)).ensure_string.chop reply.present? ? reply : default_value end # Validates a read value from the terminal. # # @param reply [String] The value to validate. # @param validator [Array|Regexp] An array of values or a Regexp to match the submitted value against. # @return [String|nil] The validated value or `nil`, if the value is invalid. def validate_input_value(reply, validator) # Match against the validator if validator.present? then if validator.is_a?(Array) then reply = nil if validator.length > 0 && !validator.include?(reply) elsif validator.is_a?(Regexp) then reply = nil if !validator.match(reply) end end reply end # Handles a read value from the terminal. # # @param reply [String] The value to handle. def handle_reply(reply) if reply then throw(:reply, reply) else write(i18n.console.unknown_reply, false, false) end end end end # This is a text utility wrapper console I/O. # # @attribute indentation # @return [Fixnum] Current indentation width. # @attribute indentation_string # @return [String] The string used for indentation. class Console attr_accessor :indentation attr_accessor :indentation_string include Lazier::I18n include Bovem::ConsoleMethods::StyleHandling include Bovem::ConsoleMethods::Output include Bovem::ConsoleMethods::Logging include Bovem::ConsoleMethods::Interactions # Returns a unique instance for Console. # # @return [Console] A new instance. def self.instance @instance ||= Bovem::Console.new end # Initializes a new Console. def initialize @indentation = 0 @indentation_string = " " i18n_setup(:bovem, ::File.absolute_path(::Pathname.new(::File.dirname(__FILE__)).to_s + "/../../locales/")) end # Get the width of the terminal. # # @return [Fixnum] The current width of the terminal. If not possible to retrieve the width, it returns `80. def line_width begin require "io/console" if !defined?($stdin.winsize) $stdin.winsize[1] rescue 80 end end end end