module GHI::Formatting

Constants

CURSOR
THROBBERS

Attributes

paginate[RW]
paging[RW]

Public Instance Methods

columns() click to toggle source
# File lib/ghi/formatting.rb, line 105
def columns
  dimensions[1] || 80
end
dimensions() click to toggle source
# File lib/ghi/formatting.rb, line 109
def dimensions
  `stty size 2>/dev/null`.chomp.split(' ').map { |n| n.to_i }
end
format_comment(c, width = columns) click to toggle source
# File lib/ghi/formatting.rb, line 254
    def format_comment c, width = columns
      <<EOF
@#{c['user']['login']} commented \
#{format_date DateTime.parse(c['created_at'])}:
#{indent c['body'], 4, width}


EOF
    end
format_comment_editor(issue, comment = nil) click to toggle source
# File lib/ghi/formatting.rb, line 413
    def format_comment_editor issue, comment = nil
      message = ERB.new(<<EOF).result binding

Leave a comment. Trailing lines starting with '#' (like these) will be ignored,
and empty messages will not be submitted. Comments are formatted with GitHub
Flavored Markdown (GFM):

  http://github.github.com/github-flavored-markdown

On <%= repo %> issue #<%= issue['number'] %>

<%= no_color { format_issue issue } if verbose %>\
<%= no_color { format_comment comment, columns - 2 } if comment %>
EOF
      message.rstrip!
      message.gsub!(/(?!\A)^.*$/) { |line| "# #{line}".rstrip }
      message.insert 0, comment['body'] if comment
      message
    end
format_comments_and_events(elements) click to toggle source
# File lib/ghi/formatting.rb, line 243
def format_comments_and_events elements
  return 'None.' if elements.empty?
  elements.map do |element|
    if event = element['event']
      format_event(element) unless unimportant_event?(event)
    else
      format_comment(element)
    end
  end.compact
end
format_date(date, suffix = true) click to toggle source
# File lib/ghi/formatting.rb, line 474
def format_date date, suffix = true
  days = (interval = DateTime.now - date).to_i.abs
  string = if days.zero?
    seconds, _ = interval.divmod Rational(1, 86400)
    hours, seconds = seconds.divmod 3600
    minutes, seconds = seconds.divmod 60
    if hours > 0
      "#{hours} hour#{'s' unless hours == 1}"
    elsif minutes > 0
      "#{minutes} minute#{'s' unless minutes == 1}"
    else
      "#{seconds} second#{'s' unless seconds == 1}"
    end
  else
    "#{days} day#{'s' unless days == 1}"
  end
  ago = interval < 0 ? 'from now' : 'ago' if suffix
  [string, ago].compact.join ' '
end
format_editor(issue = nil) click to toggle source
# File lib/ghi/formatting.rb, line 367
    def format_editor issue = nil
      message = ERB.new(<<EOF).result binding

Please explain the issue. The first line will become the title. Trailing
lines starting with '#' (like these) will be ignored, and empty messages will
not be submitted. Issues are formatted with GitHub Flavored Markdown (GFM):

  http://github.github.com/github-flavored-markdown

On <%= repo %>

<%= no_color { format_issue issue, columns - 2 if issue } %>
EOF
      message.rstrip!
      message.gsub!(/(?!\A)^.*$/) { |line|
        "# #{line}".rstrip
      }
      # Adding an extra newline for formatting
      message.insert 0, "\n"
      message.insert 0, [
        issue['title'] || issue[:title], issue['body'] || issue[:body]
      ].compact.join("\n\n") if issue
      message
    end
format_event(e, width = columns) click to toggle source
# File lib/ghi/formatting.rb, line 264
    def format_event e, width = columns
      reference = e['commit_id']
      <<EOF
#{bright { '⁕' }} #{format_event_type(e['event'])} by @#{e['actor']['login']}\
#{" through #{underline { reference[0..6] }}" if reference} \
#{format_date DateTime.parse(e['created_at'])}

EOF
    end
format_event_type(event) click to toggle source
# File lib/ghi/formatting.rb, line 349
def format_event_type(event)
  color_codes = {
    'reopened' => '2cc200',
    'closed' => 'ff0000',
    'merged' => '9677b1',
    'assigned' => 'e1811d',
    'referenced' => 'aaaaaa'
  }
  fg(color_codes[event]) { event }
end
format_issue(i, width = columns) click to toggle source

TODO: Show milestone, number of comments, pull request attached.

# File lib/ghi/formatting.rb, line 211
    def format_issue i, width = columns
      return unless i['created_at']
      ERB.new(<<EOF).result binding
<% p = i['pull_request']['html_url'] %>\
<%= bright { no_color { indent '%s%s: %s' % [p ? '↑' : '#', \
*i.values_at('number', 'title')], 0, width } } %>
@<%= i['user']['login'] %> opened this <%= p ? 'pull request' : 'issue' %> \
<%= format_date DateTime.parse(i['created_at']) %>. \
<% if i['merged'] %><%= format_state 'merged', format_tag('merged'), :bg %><% end %> \
<%= format_state i['state'], format_tag(i['state']), :bg %> \
<% unless i['comments'] == 0 %>\
<%= fg('aaaaaa'){
  template = "%d comment"
  template << "s" unless i['comments'] == 1
  '(' << template % i['comments'] << ')'
} %>\
<% end %>\
<% if i['assignee'] || !i['labels'].empty? %>
<% if i['assignee'] %>@<%= i['assignee']['login'] %> is assigned. <% end %>\
<% unless i['labels'].empty? %><%= format_labels(i['labels']) %><% end %>\
<% end %>\
<% if i['milestone'] %>
Milestone #<%= i['milestone']['number'] %>: <%= i['milestone']['title'] %>\
<%= " \#{bright{fg(:yellow){'⚠'}}}" if past_due? i['milestone'] %>\
<% end %>
<% if i['body'] && !i['body'].empty? %>
<%= indent i['body'], 4, width %>
<% end %>

EOF
    end
format_issues(issues, include_repo) click to toggle source
# File lib/ghi/formatting.rb, line 170
def format_issues issues, include_repo
  return 'None.' if issues.empty?

  include_repo and issues.each do |i|
    %r{/repos/[^/]+/([^/]+)} === i['url'] and i['repo'] = $1
  end

  nmax, rmax = %w(number repo).map { |f|
    issues.sort_by { |i| i[f].to_s.size }.last[f].to_s.size
  }

  issues.map { |i|
    n, title, labels = i['number'], i['title'], i['labels']
    l = 9 + nmax + rmax + no_color { format_labels labels }.to_s.length
    a = i['assignee']
    a_is_me = a && a['login'] == Authorization.username
    l += a['login'].to_s.length + 2 if a
    p = i['pull_request']['html_url'] and l += 2 if i['pull_request']
    c = i['comments']
    l += c.to_s.length + 1 unless c == 0
    m = i['milestone']
    [
      " ",
      (i['repo'].to_s.rjust(rmax) if i['repo']),
      format_number(n.to_s.rjust(nmax)),
      truncate(title, l),
      (format_labels(labels) unless assigns[:dont_print_labels]),
      (fg(:green) { m['title'] } if m),
      (fg('aaaaaa') { c } unless c == 0),
      (fg('aaaaaa') { '↑' } if p),
      (fg(a_is_me ? :yellow : :gray) { "@#{a['login']}" } if a),
      (fg('aaaaaa') { '‡' } if m)
    ].compact.join ' '
  }
end
format_issues_header() click to toggle source
# File lib/ghi/formatting.rb, line 121
def format_issues_header
  state = assigns[:state] ||= 'open'
  org = assigns[:org] ||= nil
  header = "# #{repo || org || 'Global,'} #{state} issues"
  if repo
    if milestone = assigns[:milestone]
      case milestone
        when '*'    then header << ' with a milestone'
        when 'none' then header << ' without a milestone'
      else
        header.sub! repo, "#{repo} milestone ##{milestone}"
      end
    end
    if assignee = assigns[:assignee]
      header << case assignee
        when '*'    then ', assigned'
        when 'none' then ', unassigned'
      else
        ", assigned to #{format_username assignee}"
      end
    end
    if mentioned = assigns[:mentioned]
      header << ", mentioning #{format_username mentioned}"
    end
  else
    header << case assigns[:filter]
      when 'created'    then ' you created'
      when 'mentioned'  then ' that mention you'
      when 'subscribed' then " you're subscribed to"
      when 'all'        then ' that you can see'
    else
      ' assigned to you'
    end
  end
  if creator = assigns[:creator]
    header << " #{format_username creator} created"
  end
  if labels = assigns[:labels]
    header << ", labeled #{labels.gsub ',', ', '}"
  end
  if excluded_labels = assigns[:exclude_labels]
    header << ", excluding those labeled #{excluded_labels.gsub ',', ', '}"
  end
  if sort = assigns[:sort]
    header << ", by #{sort} #{reverse ? 'ascending' : 'descending'}"
  end
  format_state assigns[:state], header
end
format_labels(labels) click to toggle source
# File lib/ghi/formatting.rb, line 340
def format_labels labels
  return if labels.empty?
  [*labels].map { |l| bg(l['color']) { format_tag l['name'] } }.join ' '
end
format_markdown(string, indent = 4) click to toggle source
# File lib/ghi/formatting.rb, line 433
def format_markdown string, indent = 4
  c = '268bd2'

  # Headers.
  string.gsub!(/^( {#{indent}}\#{1,6} .+)$/, bright{'\1'})
  string.gsub!(
    /(^ {#{indent}}.+$\n^ {#{indent}}[-=]+$)/, bright{'\1'}
  )
  # Strong.
  string.gsub!(
    /(^|\s)(\*{2}\w(?:[^*]*\w)?\*{2})(\s|$)/m, '\1' + bright{'\2'} + '\3'
  )
  string.gsub!(
    /(^|\s)(_{2}\w(?:[^_]*\w)?_{2})(\s|$)/m, '\1' + bright {'\2'} + '\3'
  )
  # Emphasis.
  string.gsub!(
    /(^|\s)(\*\w(?:[^*]*\w)?\*)(\s|$)/m, '\1' + underline{'\2'} + '\3'
  )
  string.gsub!(
    /(^|\s)(_\w(?:[^_]*\w)?_)(\s|$)/m, '\1' + underline{'\2'} + '\3'
  )
  # Bullets/Blockquotes.
  string.gsub!(/(^ {#{indent}}(?:[*>-]|\d+\.) )/, fg(c){'\1'})
  # URIs.
  string.gsub!(
    %r{\b(<)?(https?://\S+|[^@\s]+@[^@\s]+)(>)?\b},
    fg(c){'\1' + underline{'\2'} + '\3'}
  )

  # Inline code
  string.gsub!(/`([^`].+?)`(?=[^`])/, inverse { ' \1 ' })

  # Code blocks
  string.gsub!(/(?<indent>^\ {#{indent}})(```)\s*(?<lang>\w*$)(\n)(?<code>.+?)(\n)(^\ {#{indent}}```$)/m) do |m|
    highlight(Regexp.last_match)
  end

  string
end
format_milestone(m, width = columns) click to toggle source
# File lib/ghi/formatting.rb, line 290
    def format_milestone m, width = columns
      ERB.new(<<EOF).result binding
<%= bright { no_color { \
indent '#%s: %s' % m.values_at('number', 'title'), 0, width } } %>
@<%= m['creator']['login'] %> created this milestone \
<%= format_date DateTime.parse(m['created_at']) %>. \
<%= format_state m['state'], format_tag(m['state']), :bg %>
<% if m['due_on'] %>\
<% due_on = DateTime.parse m['due_on'] %>\
<% if past_due? m %>\
<%= bright{fg(:yellow){"⚠"}} %> \
<%= bright{fg(:red){"Past due by \#{format_date due_on, false}."}} %>
<% else %>\
Due in <%= format_date due_on, false %>.
<% end %>\
<% end %>\
<%= percent m %>
<% if m['description'] && !m['description'].empty? %>
<%= indent m['description'], 4, width %>
<% end %>

EOF
    end
format_milestone_editor(milestone = nil) click to toggle source
# File lib/ghi/formatting.rb, line 392
    def format_milestone_editor milestone = nil
      message = ERB.new(<<EOF).result binding

Describe the milestone. The first line will become the title. Trailing lines
starting with '#' (like these) will be ignored, and empty messages will not be
submitted. Milestones are formatted with GitHub Flavored Markdown (GFM):

  http://github.github.com/github-flavored-markdown

On <%= repo %>

<%= no_color { format_milestone milestone, columns - 2 } if milestone %>
EOF
      message.rstrip!
      message.gsub!(/(?!\A)^.*$/) { |line| "# #{line}".rstrip }
      message.insert 0, [
        milestone['title'], milestone['description']
      ].join("\n\n") if milestone
      message
    end
format_milestones(milestones) click to toggle source
# File lib/ghi/formatting.rb, line 274
def format_milestones milestones
  return 'None.' if milestones.empty?

  max = milestones.sort_by { |m|
    m['number'].to_s.size
  }.last['number'].to_s.size

  milestones.map { |m|
    line = ["  #{m['number'].to_s.rjust max }:"]
    space = past_due?(m) ? 6 : 4
    line << truncate(m['title'], max + space)
    line << '⚠' if past_due? m
    percent m, line.join(' ')
  }
end
format_number(n) click to toggle source
# File lib/ghi/formatting.rb, line 206
def format_number n
  colorize? ? "#{bright { n }}:" : "#{n} "
end
format_state(state, string = state, layer = :fg) click to toggle source
# File lib/ghi/formatting.rb, line 331
def format_state state, string = state, layer = :fg
  color_codes = {
    'closed' => 'ff0000',
    'open'   => '2cc200',
    'merged' => '511c7d',
  }
  send(layer, color_codes[state]) { string }
end
format_tag(tag) click to toggle source
# File lib/ghi/formatting.rb, line 345
def format_tag tag
  (colorize? ? ' %s ' : '[%s]') % tag
end
format_username(username) click to toggle source
# File lib/ghi/formatting.rb, line 117
def format_username username
  username == Authorization.username ? 'you' : username
end
indent(string, level = 4, maxwidth = columns) click to toggle source
# File lib/ghi/formatting.rb, line 94
def indent string, level = 4, maxwidth = columns
  string = string.gsub(/\r/, '')
  string.gsub!(/[\t ]+$/, '')
  string.gsub!(/\n{3,}/, "\n\n")
  width = maxwidth - level - 1
  lines = string.scan(
    /.{0,#{width}}(?:\s|\Z)|[\S]{#{width},}/ # TODO: Test long lines.
  ).map { |line| " " * level + line.chomp }
  format_markdown lines.join("\n").rstrip, level
end
page(header = nil, throttle = 0) { || ... } click to toggle source
# File lib/ghi/formatting.rb, line 47
def page header = nil, throttle = 0
  if paginate?
    pager   = GHI.config('ghi.pager') || GHI.config('core.pager')
    pager ||= ENV['PAGER']
    pager ||= 'less'
    pager  += ' -EKRX -b1' if pager =~ /^less( -[EKRX]+)?$/

    if pager && !pager.empty? && pager != 'cat'
      $stdout = IO.popen pager, 'w'
    end

    puts header if header
    self.paging = true
  end

  loop do
    yield
    sleep throttle
  end
rescue Errno::EPIPE
  exit
ensure
  unless $stdout == STDOUT
    $stdout.close_write
    $stdout = STDOUT
    print CURSOR[:show]
    exit
  end
end
paginate?() click to toggle source
# File lib/ghi/formatting.rb, line 77
def paginate?
  ($stdout.tty? && $stdout == STDOUT && Formatting.paginate) || paging?
end
paging?() click to toggle source
# File lib/ghi/formatting.rb, line 81
def paging?
  !!paging
end
past_due?(milestone) click to toggle source
# File lib/ghi/formatting.rb, line 314
def past_due? milestone
  return false unless milestone['due_on']
  DateTime.parse(milestone['due_on']) <= DateTime.now
end
percent(milestone, string = nil) click to toggle source
# File lib/ghi/formatting.rb, line 319
def percent milestone, string = nil
  open, closed = milestone.values_at('open_issues', 'closed_issues')
  complete = closed.to_f / (open + closed)
  complete = 0 if complete.nan?
  i = (columns * complete).round
  if string.nil?
    string = ' %d%% (%d closed, %d open)' % [complete * 100, closed, open]
  end
  string = string.ljust columns
  [bg('2cc200'){string[0, i]}, string[i, columns - i]].join
end
puts(*strings) click to toggle source
Calls superclass method
# File lib/ghi/formatting.rb, line 34
def puts *strings
  strings = strings.flatten.map { |s|
    s.gsub(/(^| *)@(\w+)/) {
      if $2 == Authorization.username
        bright { fg(:yellow) { "#$1@#$2" } }
      else
        bright { "#$1@#$2" }
      end
    }
  }
  super strings
end
throb(position = 0, redraw = CURSOR[:up][1]) { || ... } click to toggle source
# File lib/ghi/formatting.rb, line 494
def throb position = 0, redraw = CURSOR[:up][1]
  return yield unless paginate?

  throb = THROBBERS[rand(THROBBERS.length)]
  throb.reverse! if rand > 0.5
  i = rand throb.length

  thread = Thread.new do
    dot = lambda do
      print "\r#{CURSOR[:column][position]}#{throb[i]}#{CURSOR[:hide]}"
      i = (i + 1) % throb.length
      sleep 0.1 and dot.call
    end
    dot.call
  end
  yield
ensure
  if thread
    thread.kill
    puts "\r#{CURSOR[:column][position]}#{redraw}#{CURSOR[:show]}"
  end
end
truncate(string, reserved) click to toggle source
# File lib/ghi/formatting.rb, line 85
def truncate string, reserved
  return string unless paginate?
  space=columns - reserved
  space=5 if space < 5
  result = string.scan(/.{0,#{space}}(?:\s|\Z)/).first.strip
  result << "..." if result != string
  result
end

Private Instance Methods

unimportant_event?(event) click to toggle source
# File lib/ghi/formatting.rb, line 519
def unimportant_event?(event)
  %w{ subscribed unsubscribed mentioned }.include?(event)
end