DenoとGraphQLでGitHubの草をターミナルに表示する
GitHubの草が何となく好きです。どれくらい活動しているかが可視化され、自信やモチベーションになる気がします。
しかし普段ターミナルに生息しているので、わざわざWebページを開くのは不便です。
せっかくなのでターミナルに表示させたいと思いました。
作業記録(Zenn Scrap)はこちら
GitHubのトークンを取得する
GraphQLでユーザー情報を取得するため、read:user
権限のトークンが必要になります。
https://github.com/settings/tokens/new からトークンを作成します。
ページを辿っていく場合
メニューのSettingsからサイドバーのDeveloper settingsへ入ります
さらにサイドバーのPersonal access tokensに入り、Generate new tokenへ行ってください。
名前とread:user
権限を設定してGenerate tokenしましょう。
次の画面でトークンが表示されます。
例によってこのタイミングでしか見られないので保存しておきます。
これを使います。もちろんコード内にベタで書いても良いのですが、間違ってpushされてしまうと怖いので、.gitignore
されているファイルに書くのが良いと思います。ここでは.env
に設定します。
GITHUB_READ_USER_TOKEN=github_read_user_token
GITHUB_READ_USER_TOKEN=xxxxxxxxx
これで事前準備は完了です。
DenoからGraphQLを使う
こちらの記事をかなり参考にさせていただきました。
疎通を確認する
まずユーザー名を取得するだけのクエリを作ってみます。先程生成したトークンをBearer token
としてheaders
に入れましょう。
ここではリクエストにfetch
ではなくky
を使っています。使い方は以前書いた記事をご覧ください。
import { ky } from "./deps.ts";
import { GITHUB_READ_USER_TOKEN } from "./env.ts";
const query = `
query {
viewer {
login
}
}
`;
const url = "https://api.github.com/graphql";
const json = { query };
const result = await ky.post(url, {
headers: { Authorization: `Bearer ${GITHUB_READ_USER_TOKEN}` },
json,
}).json();
console.log(result);
実行します。
なお、実行にはVelociraptorを使っています。
設定は以前書いた記事をご覧ください。
❯ vr start
{ data: { viewer: { login: "kawarimidoll" } } }
疎通が確認できました。
しかしkyは導入してから八面六臂の活躍です。べんり。
Contributionsを取得する
本題です。コントリビューションを取得します。
query
を書き換え、variables
も追加します。
import { ky } from "./deps.ts";
import { GITHUB_READ_USER_TOKEN } from "./env.ts";
+ const userName = "kawarimidoll";
const query = `
- query {
- viewer {
- login
+ query($userName:String!) {
+ user(login: $userName){
+ contributionsCollection {
+ contributionCalendar {
+ totalContributions
+ weeks {
+ contributionDays {
+ color
+ contributionCount
+ contributionLevel
+ date
+ }
+ }
+ }
+ }
}
}
`;
+ const variables = `
+ {
+ "userName": "${userName}"
+ }
+ `;
const url = "https://api.github.com/graphql";
- const json = { query };
+ const json = { query, variables };
const result = await ky.post(url, {
headers: { Authorization: `Bearer ${GITHUB_READ_USER_TOKEN}` },
json,
}).json();
console.log(result);
(ほとんど参考元のコピペになってしまった)
実行するとこんな感じです。
❯ vr start
{ data: { user: { contributionsCollection: { contributionCalendar: [Object] } } } }
ちょっと階層が深いので中身を取り出して表示してみます。
(略)
- const result = await ky.post(url, {
+ const { data } = await ky.post(url, {
headers: { Authorization: `Bearer ${GITHUB_READ_USER_TOKEN}` },
json,
}).json();
- console.log(result);
+ console.log(data?.user);
❯ vr start
{
contributionsCollection: {
contributionCalendar: {
totalContributions: 2135,
weeks: [
[Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object],
[Object]
]
}
}
}
参考元でも触れられていますが、Webページ上のcontributions数とはちょっとずれがあるようです。上記実行結果ではtotalContributions
が2135でしたが、ページでは2136と表示されていました。
private repositoryの問題かとも思いましたが、それならもっと差が出ると思うんですよね、よくわかりません。
weeks
の中を表示させてみます。古いものから並んでいるので、最後の週を表示させると最新のものが見られます。
// const { data } の代入まで同じ
interface ContributionDay {
contributionCount: number;
contributionLevel: string;
date: string;
color: string;
}
const { weeks, totalContributions }: {
weeks: { contributionDays: ContributionDay[] }[];
totalContributions: number;
} = data?.user?.contributionsCollection
?.contributionCalendar;
if (!weeks || !totalContributions) {
throw new Error("Could not get contributions data");
}
console.log(totalContributions + " contributions in the last year");
console.log(weeks[weeks.length - 1]);
❯ vr start
2135 contributions in the last year
{
contributionDays: [
{
color: "#9be9a8",
contributionCount: 2,
contributionLevel: "FIRST_QUARTILE",
date: "2021-06-27"
},
{
color: "#9be9a8",
contributionCount: 6,
contributionLevel: "FIRST_QUARTILE",
date: "2021-06-28"
}
]
}
query
で設定した項目が取得できていますね。
ターミナルで表示させる
Contribution数を表示させる
では、まず数値を表示させてみます。
受け取った値は最初の週から日付順に並んでいますが、これを適切に表示させるためには行列を転置して曜日を横断して表示させる必要があります。
// console.log(totalContributions + " contributions in the last year");
// ↑これまで同じ
weeks[0].contributionDays.forEach((_, i) => {
console.log(
weeks.map((row) =>
`${row.contributionDays[i]?.contributionCount ?? ""}`.padStart(3)
).join(""),
);
});
表示処理で??
を使って場合分けしています。||
を使うと、0
がfalsy
のため、contributionCount
が0
のときにうまくいきません。
また、padStart(3)
で桁を調整しています。1日で3桁以上のコントリビューションがない場合はこれで十分でしょう。
実行すると以下のように横に並べることができます。
なお、53週 x 3字ずつ並んでいるので159字以上表示できる幅が必要です。
❯ vr start
2135 contributions in the last year
2 1 5 1 13 5 2 4 5 9 1 2 3 3 3 8 3 10 1 4 1 1 6 2 4 2 10 10 10 9 8 6 13 4 7 7 6 6 3 1 11 1 5 2 6 6 6 21 11 17 13 16 2
0 1 2 1 4 0 1 5 7 1 1 3 9 1 5 8 3 1 4 5 3 1 1 1 1 6 9 12 5 7 5 5 19 2 20 11 8 10 32 4 6 2 5 4 3 3 7 20 5 17 6 5 6
0 0 2 4 0 5 0 7 8 3 3 3 2 2 5 1 1 4 1 3 1 1 2 1 1 7 8 9 4 10 2 14 12 18 11 22 6 7 7 14 1 3 2 5 1 18 5 20 7 4 6 5
0 5 3 4 8 5 1 4 2 5 2 4 1 5 2 3 1 4 1 1 1 1 1 2 2 4 11 5 6 4 2 6 18 16 6 6 2 2 7 9 1 4 1 4 4 9 4 11 5 20 6 14
0 1 0 13 6 1 0 0 5 1 4 3 1 6 3 2 1 1 2 10 1 1 1 2 2 11 4 18 4 15 4 12 24 4 23 15 4 17 8 5 4 2 2 14 2 12 6 9 1 9 4 2
0 5 9 10 2 3 3 2 3 1 3 1 9 5 6 1 1 1 3 1 1 1 1 2 1 9 7 23 6 13 6 1 14 6 3 12 3 4 6 2 1 2 6 14 6 12 6 15 2 10 4 18
0 9 0 5 7 3 3 10 4 5 4 3 2 4 1 6 4 1 4 2 2 3 1 2 1 10 7 28 6 12 8 17 7 6 4 14 13 3 5 15 2 5 4 11 6 15 24 20 8 19 9 9
色をつける
返却値に含まれているcolor
を使って着色します。
Denoでは、出力結果の着色にstd/fmt/colors
が使えます。
GitHubから返却されるcolor
はHEX値ですが、std/fmt/colors
ではHEXに対応したものは提供されていないようでした。
ということで変換処理を作ります。
// コード内の適当な場所に追加
const hexToRgb = (hex: string) => {
if (hex.slice(0, 1) == "#") hex = hex.slice(1);
if (hex.length == 3) {
hex = hex.slice(0, 1) + hex.slice(0, 1) + hex.slice(1, 2) +
hex.slice(1, 2) + hex.slice(2, 3) + hex.slice(2, 3);
}
const r = parseInt("0x" + hex.slice(0, 2));
const g = parseInt("0x" + hex.slice(2, 4));
const b = parseInt("0x" + hex.slice(4, 6));
return { r, g, b };
};
今回は返却値は"#123456"
形式なのでここまでの場合分けは要らないのですが、後々も使えるかと思い、頭に#
のない場合や省略形式("#123"
)にも対応しています。
これを使って■
を着色します。
import { rgb24 } from "https://deno.land/std@0.99.0/fmt/colors.ts"; // import追加
// (略)
const grass = (day?: ContributionDay) =>
day?.color ? rgb24("■", hexToRgb(day.color)) : "";
weeks[0].contributionDays.forEach((_, i) => {
console.log(
weeks.map((row) => grass(row.contributionDays[i])).join(""),
);
});
実行すると草が表示されると思います。
また、color
を直接表示させるのではなくcontributionLevel
を使って場合分け、オリジナルのカラースキームを使うこともできます。
// (略)
+ const colors = [ "#eeeeee", "#f8bbd0", "#f06292", "#e91e63", "#880e4f"];
+ const fillPixel = (day?: ContributionDay) => {
+ switch (day?.contributionLevel) {
+ case "NONE":
+ return hexToRgb(colors[0]);
+ case "FIRST_QUARTILE":
+ return hexToRgb(colors[1]);
+ case "SECOND_QUARTILE":
+ return hexToRgb(colors[2]);
+ case "THIRD_QUARTILE":
+ return hexToRgb(colors[3]);
+ case "FOURTH_QUARTILE":
+ return hexToRgb(colors[4]);
+ }
+ return hexToRgb("#eeeeee");
+ };
const grass = (day?: ContributionDay) =>
- day?.color ? rgb24("■", hexToRgb(day.color)) : "";
+ day ? rgb24("■", fillPixel(day)) : "";
weeks[0].contributionDays.forEach((_, i) => {
console.log(
weeks.map((row) => grass(row.contributionDays[i])).join(""),
);
});
配色はこちらを参考にしました。
補足
以下のように定義するとswitch
文がなくなってきれいになるとは思いますが、複数カラースキームをきりかえて使いたいときなどに面倒な気がしたので今回はswitch
を採用しました。
colors = {
"NONE": "#eeeeee",
"FIRST_QUARTILE": "#f8bbd0",
"SECOND_QUARTILE": "#f06292",
"THIRD_QUARTILE":"#e91e63",
"FOURTH_QUARTILE":"#880e4f"
};
おわりに
今回のキモは true colorに対応した端末を使うこと ですね。
ずっとmacデフォルトのterminal.appを使っていたので悲しいことになっていました。
これまではterminal.appで十分だと思っていましたが、乗り換えどきかもしれません。
Denoでいろいろ遊んでいるリポジトリはこちら。
参考
Discussion