├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yml
│ └── feature_request.md
└── workflows
│ └── run.yml
├── .gitignore
├── LICENSE
├── README.md
├── example.png
├── package-lock.json
├── package.json
└── src
├── api.js
├── index.js
├── linguist.js
└── text.js
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior.
15 |
16 | **Expected behavior**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Additional context**
20 | Add any other context about the problem here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/run.yml:
--------------------------------------------------------------------------------
1 | name: update gist
2 |
3 | on:
4 | schedule:
5 | - cron: "0 0 * * *"
6 | workflow_dispatch:
7 |
8 | jobs:
9 | run:
10 | runs-on: ubuntu-22.04
11 | steps:
12 | - uses: actions/checkout@v3
13 |
14 | - name: Setup Node
15 | uses: actions/setup-node@v4
16 | with:
17 | node-version: "lts/*"
18 | cache: "npm"
19 |
20 | - run: sudo apt-get update
21 | - run: sudo apt-get install cmake pkg-config libicu-dev zlib1g-dev libcurl4-openssl-dev libssl-dev ruby-dev
22 | - run: sudo gem install github-linguist
23 | - run: npm install
24 | - run: npm start
25 | env:
26 | GH_TOKEN: ${{ secrets.GH_TOKEN }}
27 | GIST_ID: 64dacee1c6c93cdbcf48548f6598f823
28 | USERNAME: ${{ github.actor }}
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 inokawa
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
lang-box
4 | 💻 Update a pinned gist to contain languages of your recent commits in GitHub
5 |
6 |
7 | ---
8 |
9 | > This project is inspired by [waka-box](https://github.com/matchai/waka-box), [productive-box](https://github.com/maxam2017/productive-box) and [metrics](https://github.com/lowlighter/metrics).
10 | > 📌✨ For more pinned-gist projects like this one, check out: https://github.com/matchai/awesome-pinned-gists
11 |
12 | This project gets your recent commits from your activities fetched from GitHub API, and process them with [linguist](https://github.com/github/linguist) to show the percentage of each languages used. This project also calculate how many lines of codes were added/removed per language.
13 |
14 | ## Setup
15 |
16 | ### Prep work
17 |
18 | 1. Create a new public GitHub Gist (https://gist.github.com/)
19 | 1. Create a token with the `gist` scope and copy it. (https://github.com/settings/tokens/new)
20 |
21 | - And if you would like to include commits in private repos, also add `repo` scope.
22 |
23 | > Enable `repo` scope seems **DANGEROUS**, but secrets are not passed to workflows that are triggered by a pull request from a fork (https://docs.github.com/en/actions/reference/encrypted-secrets)
24 |
25 | ### Project setup
26 |
27 | 1. Fork this repo, or [create a repository from template](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template) by clicking [here](https://github.com/inokawa/lang-box/generate) or the **Use this template** button on this project.
28 | - If you added `repo` scope above, it's recommended to create private repository.
29 | 1. Open the "Actions" tab of your fork and click the "enable" button.
30 | 1. Edit the [environment variable](https://github.com/inokawa/lang-box/blob/master/.github/workflows/run.yml#L32-L33) in `.github/workflows/run.yml`:
31 |
32 | - **GIST_ID:** The ID portion from your gist url: `https://gist.github.com/inokawa/`**`64dacee1c6c93cdbcf48548f6598f823`**.
33 |
34 | 1. Go to the repo **Settings > Secrets**
35 | 1. Add the following environment variables:
36 | - **GH_TOKEN:** The GitHub token generated above.
37 | 1. [Pin the newly created Gist](https://help.github.com/en/github/setting-up-and-managing-your-github-profile/pinning-items-to-your-profile)
38 |
--------------------------------------------------------------------------------
/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inokawa/lang-box/4b4c614691e212213e3aa9dd22fe3fbd41ad210d/example.png
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lang-box",
3 | "version": "1.0.0",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "lang-box",
9 | "version": "1.0.0",
10 | "license": "MIT",
11 | "dependencies": {
12 | "node-fetch": "^3.2.10"
13 | }
14 | },
15 | "node_modules/data-uri-to-buffer": {
16 | "version": "4.0.0",
17 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
18 | "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==",
19 | "engines": {
20 | "node": ">= 12"
21 | }
22 | },
23 | "node_modules/fetch-blob": {
24 | "version": "3.2.0",
25 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
26 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
27 | "funding": [
28 | {
29 | "type": "github",
30 | "url": "https://github.com/sponsors/jimmywarting"
31 | },
32 | {
33 | "type": "paypal",
34 | "url": "https://paypal.me/jimmywarting"
35 | }
36 | ],
37 | "dependencies": {
38 | "node-domexception": "^1.0.0",
39 | "web-streams-polyfill": "^3.0.3"
40 | },
41 | "engines": {
42 | "node": "^12.20 || >= 14.13"
43 | }
44 | },
45 | "node_modules/formdata-polyfill": {
46 | "version": "4.0.10",
47 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
48 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
49 | "dependencies": {
50 | "fetch-blob": "^3.1.2"
51 | },
52 | "engines": {
53 | "node": ">=12.20.0"
54 | }
55 | },
56 | "node_modules/node-domexception": {
57 | "version": "1.0.0",
58 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
59 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
60 | "funding": [
61 | {
62 | "type": "github",
63 | "url": "https://github.com/sponsors/jimmywarting"
64 | },
65 | {
66 | "type": "github",
67 | "url": "https://paypal.me/jimmywarting"
68 | }
69 | ],
70 | "engines": {
71 | "node": ">=10.5.0"
72 | }
73 | },
74 | "node_modules/node-fetch": {
75 | "version": "3.2.10",
76 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz",
77 | "integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==",
78 | "dependencies": {
79 | "data-uri-to-buffer": "^4.0.0",
80 | "fetch-blob": "^3.1.4",
81 | "formdata-polyfill": "^4.0.10"
82 | },
83 | "engines": {
84 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
85 | },
86 | "funding": {
87 | "type": "opencollective",
88 | "url": "https://opencollective.com/node-fetch"
89 | }
90 | },
91 | "node_modules/web-streams-polyfill": {
92 | "version": "3.2.1",
93 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
94 | "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
95 | "engines": {
96 | "node": ">= 8"
97 | }
98 | }
99 | },
100 | "dependencies": {
101 | "data-uri-to-buffer": {
102 | "version": "4.0.0",
103 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
104 | "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA=="
105 | },
106 | "fetch-blob": {
107 | "version": "3.2.0",
108 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
109 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
110 | "requires": {
111 | "node-domexception": "^1.0.0",
112 | "web-streams-polyfill": "^3.0.3"
113 | }
114 | },
115 | "formdata-polyfill": {
116 | "version": "4.0.10",
117 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
118 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
119 | "requires": {
120 | "fetch-blob": "^3.1.2"
121 | }
122 | },
123 | "node-domexception": {
124 | "version": "1.0.0",
125 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
126 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
127 | },
128 | "node-fetch": {
129 | "version": "3.2.10",
130 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz",
131 | "integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==",
132 | "requires": {
133 | "data-uri-to-buffer": "^4.0.0",
134 | "fetch-blob": "^3.1.4",
135 | "formdata-polyfill": "^4.0.10"
136 | }
137 | },
138 | "web-streams-polyfill": {
139 | "version": "3.2.1",
140 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
141 | "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q=="
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lang-box",
3 | "version": "1.0.0",
4 | "description": "💻 Update a pinned gist to contain languages of your recent commits in GitHub",
5 | "main": "src/index.js",
6 | "type": "module",
7 | "scripts": {
8 | "start": "node --experimental-modules --es-module-specifier-resolution=node src/index"
9 | },
10 | "dependencies": {
11 | "node-fetch": "^3.2.10"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/inokawa/lang-box.git"
16 | },
17 | "keywords": [],
18 | "author": "inokawa (https://github.com/inokawa/)",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/inokawa/lang-box/issues"
22 | },
23 | "homepage": "https://github.com/inokawa/lang-box#readme"
24 | }
25 |
--------------------------------------------------------------------------------
/src/api.js:
--------------------------------------------------------------------------------
1 | import fetch from 'node-fetch';
2 |
3 | export class ApiClient {
4 | constructor(token) {
5 | this.token = token;
6 | }
7 |
8 | fetch = async (path, method = "GET", body) => {
9 | const res = await fetch(`https://api.github.com${path}`, {
10 | method,
11 | headers: {
12 | Authorization: `bearer ${this.token}`,
13 | "Content-Type": "application/json",
14 | Accept: "application/vnd.github.v3+json",
15 | },
16 | body: JSON.stringify(body),
17 | });
18 | const json = await res.json();
19 | if (!res.ok) {
20 | throw new Error(json.message);
21 | }
22 | return json;
23 | };
24 |
25 | fetchGq = async (query) => {
26 | const res = await fetch("https://api.github.com/graphql", {
27 | method: "POST",
28 | headers: {
29 | Authorization: `bearer ${this.token}`,
30 | "Content-Type": "application/json",
31 | },
32 | body: JSON.stringify({ query }).replace(/\\n/g, ""),
33 | });
34 | const json = await res.json();
35 | if (!res.ok) {
36 | throw new Error(json.message);
37 | }
38 | return json;
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { ApiClient } from "./api.js";
2 | import { createContent } from "./text.js";
3 | import { runLinguist } from "./linguist.js";
4 |
5 | const { GH_TOKEN, GIST_ID, USERNAME, DAYS } = process.env;
6 |
7 | (async () => {
8 | try {
9 | if (!GH_TOKEN) {
10 | throw new Error("GH_TOKEN is not provided.");
11 | }
12 | if (!GIST_ID) {
13 | throw new Error("GIST_ID is not provided.");
14 | }
15 | if (!USERNAME) {
16 | throw new Error("USERNAME is not provided.");
17 | }
18 |
19 | const api = new ApiClient(GH_TOKEN);
20 | const username = USERNAME;
21 | const days = Math.max(1, Math.min(30, Number(DAYS || 14)));
22 |
23 | console.log(`username is ${username}.`);
24 | console.log(`\n`);
25 |
26 | // https://docs.github.com/en/rest/reference/activity
27 | // GitHub API supports 300 events at max and events older than 90 days will not be fetched.
28 | const maxEvents = 300;
29 | const perPage = 100;
30 | const pages = Math.ceil(maxEvents / perPage);
31 | const fromDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
32 |
33 | const commits = [];
34 | try {
35 | for (let page = 0; page < pages; page++) {
36 | // https://docs.github.com/en/developers/webhooks-and-events/github-event-types#pushevent
37 | const pushEvents = (
38 | await api.fetch(
39 | `/users/${username}/events?per_page=${perPage}&page=${page}`
40 | )
41 | ).filter(
42 | ({ type, actor }) => type === "PushEvent" && actor.login === username
43 | );
44 |
45 | const recentPushEvents = pushEvents.filter(
46 | ({ created_at }) => new Date(created_at) > fromDate
47 | );
48 | const isEnd = recentPushEvents.length < pushEvents.length;
49 | console.log(`${recentPushEvents.length} events fetched.`);
50 |
51 | commits.push(
52 | ...(
53 | await Promise.allSettled(
54 | recentPushEvents.flatMap(({ repo, payload }) =>
55 | payload.commits
56 | // Ignore duplicated commits
57 | .filter((c) => c.distinct === true)
58 | .map((c) => api.fetch(`/repos/${repo.name}/commits/${c.sha}`))
59 | )
60 | )
61 | )
62 | .filter(({ status }) => status === "fulfilled")
63 | .map(({ value }) => value)
64 | );
65 |
66 | if (isEnd) {
67 | break;
68 | }
69 | }
70 | } catch (e) {
71 | console.log("no more page to load");
72 | }
73 |
74 | console.log(`${commits.length} commits fetched.`);
75 | console.log(`\n`);
76 |
77 | // https://docs.github.com/en/rest/reference/repos#compare-two-commits
78 | const files = commits
79 | // Ignore merge commits
80 | .filter((c) => c.parents.length <= 1)
81 | .flatMap((c) =>
82 | c.files.map(
83 | ({
84 | filename,
85 | additions,
86 | deletions,
87 | changes,
88 | status, // added, removed, modified, renamed
89 | patch,
90 | }) => ({
91 | path: filename,
92 | additions,
93 | deletions,
94 | changes,
95 | status,
96 | patch,
97 | })
98 | )
99 | );
100 |
101 | const langs = await runLinguist(files);
102 | console.log(`\n`);
103 | langs.forEach((l) =>
104 | console.log(
105 | `${l.name}: ${l.count} files, ${l.additions + l.deletions} changes`
106 | )
107 | );
108 |
109 | const content = createContent(langs);
110 | console.log(`\n`);
111 | console.log(content);
112 | console.log(`\n`);
113 |
114 | const gist = await api.fetch(`/gists/${GIST_ID}`);
115 | const filename = Object.keys(gist.files)[0];
116 | await api.fetch(`/gists/${GIST_ID}`, "PATCH", {
117 | files: {
118 | [filename]: {
119 | filename: `💻 Recent coding in languages`,
120 | content,
121 | },
122 | },
123 | });
124 |
125 | console.log(`Update succeeded.`);
126 | } catch (e) {
127 | console.error(e);
128 | process.exitCode = 1;
129 | }
130 | })();
131 |
--------------------------------------------------------------------------------
/src/linguist.js:
--------------------------------------------------------------------------------
1 | import processes from "child_process";
2 | import { promises as fs } from "fs";
3 | import path from "path";
4 |
5 | const run = (command, options) =>
6 | new Promise((resolve, reject) => {
7 | console.debug(`run > ${command}`);
8 | const child = processes.exec(command, options);
9 | let [stdout, stderr] = ["", ""];
10 | child.stdout.on("data", (data) => (stdout += data));
11 | child.stderr.on("data", (data) => (stderr += data));
12 | child.on("close", (code) => {
13 | console.debug(`exited with code ${code}`);
14 | return code === 0 ? resolve(stdout) : reject(stderr);
15 | });
16 | });
17 |
18 | const createDummyText = (count) => {
19 | let text = "";
20 | for (let i = 0; i < count; i++) {
21 | text += "\n";
22 | }
23 | return text;
24 | };
25 |
26 | export const runLinguist = async (files) => {
27 | await run("git checkout --orphan temp && git rm -rf . && rm -rf *");
28 | const datas = files.map((d, i) => ({
29 | ...d,
30 | path: `${i}${path.extname(d.path)}`,
31 | }));
32 | const pathFileMap = datas.reduce((acc, d) => {
33 | acc[d.path] = d;
34 | return acc;
35 | }, {});
36 | await Promise.all([
37 | ...datas.map((d) =>
38 | fs.writeFile(
39 | d.path,
40 | d.patch
41 | ? d.patch
42 | .split("\n")
43 | .filter((line) => /^[-+]/.test(line))
44 | .map((line) => line.substring(1))
45 | .join("\n")
46 | : d.changes
47 | ? // If the diff is too large, GitHub API do not return patch so calc from changed lines but it's not precise
48 | createDummyText(d.changes)
49 | : ""
50 | )
51 | ),
52 | run(`echo "*.* linguist-detectable" > .gitattributes`),
53 | run(
54 | `git config user.name "dummy" && git config user.email "dummy@github.com"`
55 | ),
56 | ]);
57 | await run(`git add . && git commit -m "dummy"`);
58 |
59 | const stdout = await run("github-linguist --breakdown --json");
60 | const res = JSON.parse(stdout);
61 |
62 | const langs = Object.entries({ ...res })
63 | .reduce((acc, [name, v]) => {
64 | acc.push({
65 | name,
66 | percent: +v.percentage,
67 | additions: v.files.reduce(
68 | (acc, p) => acc + (pathFileMap[p]?.additions ?? 0),
69 | 0
70 | ),
71 | deletions: v.files.reduce(
72 | (acc, p) => acc + (pathFileMap[p]?.deletions ?? 0),
73 | 0
74 | ),
75 | count: v.files.length,
76 | });
77 | return acc;
78 | }, [])
79 | .sort((a, b) => b.percent - a.percent);
80 |
81 | return langs;
82 | };
83 |
--------------------------------------------------------------------------------
/src/text.js:
--------------------------------------------------------------------------------
1 | const trimRightStr = (str, len) => {
2 | return str.length > len ? str.substring(0, len - 1) + "…" : str;
3 | };
4 |
5 | const formatNum = (n) => {
6 | for (const { u, v } of [
7 | { u: "E", v: 10 ** 18 },
8 | { u: "P", v: 10 ** 15 },
9 | { u: "T", v: 10 ** 12 },
10 | { u: "G", v: 10 ** 9 },
11 | { u: "M", v: 10 ** 6 },
12 | { u: "k", v: 10 ** 3 },
13 | ]) {
14 | const top = n / v;
15 | if (top >= 1) {
16 | return `${top.toFixed(1)}${u}`;
17 | }
18 | }
19 | return `${n}`;
20 | };
21 |
22 | const generateBarChart = (percent, size) => {
23 | const syms = "░▏▎▍▌▋▊▉█";
24 |
25 | const frac = Math.floor((size * 8 * percent) / 100);
26 | const barsFull = Math.floor(frac / 8);
27 | if (barsFull >= size) {
28 | return syms.substring(8, 9).repeat(size);
29 | }
30 | const semi = frac % 8;
31 |
32 | return [syms.substring(8, 9).repeat(barsFull), syms.substring(semi, semi + 1)]
33 | .join("")
34 | .padEnd(size, syms.substring(0, 1));
35 | };
36 |
37 | export const createContent = (languages) => {
38 | const lines = [];
39 | for (let i = 0; i < languages.length; i++) {
40 | const data = languages[i];
41 | const { name, percent, additions, deletions } = data;
42 |
43 | lines.push(
44 | [
45 | trimRightStr(name, 10).padEnd(10),
46 | ("+" + formatNum(additions)).padStart(7) +
47 | "/" +
48 | ("-" + formatNum(deletions)).padStart(7),
49 | generateBarChart(percent, 21),
50 | ].join(" ") +
51 | percent.toFixed(1).padStart(5) +
52 | "%"
53 | );
54 | }
55 | return lines.join("\n");
56 | };
57 |
--------------------------------------------------------------------------------