Github Contribution

and geekness (ISSUE #1550)

and sprak3000 (xbar vars)

Image preview of Github Contribution plugin.

github-contribution.10m.rb

Edit
Open on GitHub
#!/usr/bin/env ruby
# frozen_string_literal: true

# <xbar.title>Github Contribution</xbar.title>
# <xbar.version>v0.0.3</xbar.version>
# <xbar.author>mizoR, geekness (ISSUE #1550), sprak3000 (xbar vars)</xbar.author>
# <xbar.author.github>mizoR</xbar.author.github>
# <xbar.image>https://user-images.githubusercontent.com/1257116/34550684-37da7286-f156-11e7-9299-5873b6bb2fd7.png</xbar.image>
# <xbar.dependencies>ruby</xbar.dependencies>
# <xbar.var>string(VAR_USERNAME=mizoR): Your GitHub username</xbar.var>
# <xbar.var>number(VAR_MAX_CONTRIBUTIONS=10): The maximum number of contributions to display</xbar.var>
#
# To setup, create or edit your ~/.bitbarrc file with a new section:
#
# [github_contribution]
# username = mizoR
# max_contributions = 10

require 'erb'
require 'date'
require 'open-uri'

module BitBar
  module GitHubContribution
    ConfigurationError = Class.new(StandardError)

    class Contribution < Struct.new(:username, :contributed_on, :count)
      RE_CONTRIBUTION = %r|<rect .*class="ContributionCalendar-day" .*data-date="(\d\d\d\d-\d\d-\d\d)" .*data-level="(\d+)" .*>.*<\/rect>|

      def self.find_all_by(username:)
        [].tap do |contributions|
          html = URI.parse(url_for(username: username)).open { |f| f.read }
          html.scan(RE_CONTRIBUTION) do |date, count|
            contributions << Contribution.new(username, Date.parse(date), count.to_i)
            break if Date.parse(date) == Date.parse(DateTime.now.to_s)
          end
        end
      end

      def color
        count <= 0 ? 'brown' : 'green'
      end

      def icon
        case count
        when 0    then '👻'
        when 1..3 then '🌱'
        when 4..9 then '🌿'
        else           '🌳'
        end
      end

      def self.url_for(username:)
        "https://github.com/users/#{username}/contributions"
      end
    end

    class View
      TEMPLATE = <<-EOT.gsub(/^ */, '')
        <%= @contribution.icon %><%= @contribution.count %> | color=<%= @contribution.color %>
        ---
        <% @contributions.each do |c| -%>
        <%= @helper.link_to(@helper.contribution_text_for(c), @helper.contribution_activity_for(c)) %>
        <% end -%>
      EOT

      class Helper
        def link_to(text, href)
          if text =~ / | /
            "#{text} href=#{href}"
          else
            "#{text} | href=#{href}"
          end
        end

        def contribution_text_for(contribution)
          "#{contribution.icon} #{contribution.contributed_on.strftime('%Y-%m-%d (%a)')}   \t#{contribution.count} | color=#{contribution.color}"
        end

        def contribution_activity_for(contribution)
          query    = "from=#{contribution.contributed_on}"
          fragment = "year-link-#{contribution.contributed_on.year}"

          "https://github.com/#{contribution.username}?#{query}##{fragment}"
        end
      end

      def initialize(contributions:)
        @contribution  = contributions.fetch(0)
        @contributions = contributions
        @helper        = Helper.new
      end

      def render
        puts ERB.new(TEMPLATE, nil, '-').result(binding)
      end
    end

    class App
      DEFAULT_CONFIG = { max_contributions: 10 }

      def initialize(config = {})
        config = cast_config()

        @username, @max_contributions = config.values_at(:username, :max_contributions)
      end

      def run
        # (DateTime.now-7).to_s
        contributions = Contribution.find_all_by(username: @username)
                                    .sort_by(&:contributed_on)
                                    .select{|l| l.contributed_on < DateTime.now}
                                    .reverse
                                    .slice(0, @max_contributions)
        View.new(contributions: contributions).render
      end

      private

      def cast_config()
        username          = ENV["VAR_USERNAME"]
        max_contributions = ENV["VAR_MAX_CONTRIBUTIONS"].to_i

        if username.empty?
          raise ConfigurationError, 'GitHub username is not given.'
        end

        if !max_contributions.positive?
          raise ConfigurationError,
            "Max contributions should be positive integer, but it was #{max_contributions}"
        end

        { username: username, max_contributions: max_contributions }
      end
    end
  end
end

if __FILE__ == $0
  begin
    BitBar::GitHubContribution::App.new().run
  rescue BitBar::GitHubContribution::ConfigurationError => e
    puts <<-EOM.gsub(/^ */, '')
      ⚠️
      ---
      #{e.message}
    EOM
  end
end