#!/usr/bin/env ruby
# frozen_string_literal: true
# <xbar.title>Github Contribution</xbar.title>
# <xbar.version>v0.0.2</xbar.version>
# <xbar.author>mizoR</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>
#
# 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
class INIFile
Error = Class.new(StandardError)
INIFileNotFound = Class.new(Error)
SectionNotFound = Class.new(Error)
def self.load(file = "#{ENV['HOME']}/.bitbarrc")
raise INIFileNotFound if !File.exist?(file)
parse(open(file) { |f| f.read })
end
def self.parse(source)
# XXX: This implementation isn't correct, but will work in most cases.
# (Probably `StringScanner` will make code correct and clean.)
sections = {}
section = nil
source.each_line do |line|
if line =~ /^ *;/
# comment
next
end
if line =~ /^\[(.+)\]$/
section = sections[$1.to_sym] = {}
next
end
next unless section
if line =~ /(.+)=(.+)/
name = $1.strip.to_sym
value = $2.strip
section[name] = value[/^"(.*)"$/, 1] || value[/^'(.*)'$/, 1] || value
next
end
end
new(sections: sections)
end
def initialize(sections:)
@sections = sections
end
def fetch(name)
@sections.fetch(name.to_sym)
rescue KeyError
raise SectionNotFound
end
end
module GitHubContribution
ConfigurationError = Class.new(StandardError)
class Contribution < Struct.new(:username, :contributed_on, :count)
RE_CONTRIBUTION = %r|<rect .*class="ContributionCalendar-day" .*data-count="(\d+)" .*data-date="(\d\d\d\d-\d\d-\d\d)".*></rect>|
def self.find_all_by(username:)
[].tap do |contributions|
html = open(url_for(username: username)) { |f| f.read }
html.scan(RE_CONTRIBUTION) do |count, date|
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 ':poop:'
when 1..3 then ':seedling:'
when 4..9 then ':herb:'
else ':deciduous_tree:'
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(DEFAULT_CONFIG.merge(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(config)
username = config[:username].to_s
max_contributions = config[: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
config = BitBar::INIFile.load.fetch(:github_contribution)
BitBar::GitHubContribution::App.new(config).run
rescue BitBar::INIFile::Error
puts <<-EOM.gsub(/^ */, '')
⚠️
---
To setup, create or edit your ~/.bitbarrc file with a new section:
|
;# ~/.bitbarrc
[github_contribution]
username = <GITHUB_USERNAME>
max_contributions = 10
EOM
rescue BitBar::GitHubContribution::ConfigurationError => e
puts <<-EOM.gsub(/^ */, '')
⚠️
---
#{e.message}
EOM
end
end