From da66a7bf8ea338e2ac9aa54cad9badbab15eeb4a Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Tue, 2 Feb 2021 23:20:12 +0100 Subject: [PATCH] WakaTime plugin (#90) --- .github/readme/imgs/plugin_wakatime_token.png | Bin 0 -> 35202 bytes source/app/metrics/utils.mjs | 9 ++ source/app/mocks/api/axios/get/wakatime.mjs | 44 +++++++++ source/app/web/statics/app.placeholder.js | 28 ++++++ source/plugins/wakatime/README.md | 36 ++++++++ source/plugins/wakatime/index.mjs | 45 +++++++++ source/plugins/wakatime/metadata.yml | 53 +++++++++++ source/plugins/wakatime/tests.yml | 15 +++ source/templates/classic/partials/_.json | 3 +- .../templates/classic/partials/wakatime.ejs | 87 ++++++++++++++++++ source/templates/classic/style.css | 11 ++- 11 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 .github/readme/imgs/plugin_wakatime_token.png create mode 100644 source/app/mocks/api/axios/get/wakatime.mjs create mode 100644 source/plugins/wakatime/README.md create mode 100644 source/plugins/wakatime/index.mjs create mode 100644 source/plugins/wakatime/metadata.yml create mode 100644 source/plugins/wakatime/tests.yml create mode 100644 source/templates/classic/partials/wakatime.ejs diff --git a/.github/readme/imgs/plugin_wakatime_token.png b/.github/readme/imgs/plugin_wakatime_token.png new file mode 100644 index 0000000000000000000000000000000000000000..2e11305958d6f347b4344e7da6a4b166bf2b116e GIT binary patch literal 35202 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 literal 0 HcmV?d00001 diff --git a/source/app/metrics/utils.mjs b/source/app/metrics/utils.mjs index 9a26ad13..0657d441 100644 --- a/source/app/metrics/utils.mjs +++ b/source/app/metrics/utils.mjs @@ -46,6 +46,15 @@ } format.percentage = percentage +/** Text ellipsis formatter */ + export function ellipsis(text, {length = 20} = {}) { + text = `${text}` + if (text.length < length) + return text + return `${text.substring(0, length)}…` + } + format.ellipsis = ellipsis + /** Array shuffler */ export function shuffle(array) { for (let i = array.length-1; i > 0; i--) { diff --git a/source/app/mocks/api/axios/get/wakatime.mjs b/source/app/mocks/api/axios/get/wakatime.mjs new file mode 100644 index 00000000..0baa0f0f --- /dev/null +++ b/source/app/mocks/api/axios/get/wakatime.mjs @@ -0,0 +1,44 @@ +/** Mocked data */ + export default function ({faker, url, options, login = faker.internet.userName()}) { + //Wakatime api + if (/^https:..wakatime.com.api.v1.users.current.stats.*$/.test(url)) { + //Get user profile + if (/api_key=MOCKED_TOKEN/.test(url)) { + console.debug(`metrics/compute/mocks > mocking wakatime api result > ${url}`) + const stats = (array) => { + const elements = [] + let result = new Array(4+faker.random.number(2)).fill(null).map(_ => ({ + get digital() { return `${this.hours}:${this.minutes}` }, + hours:faker.random.number(1000), minutes:faker.random.number(1000), + name:array ? faker.random.arrayElement(array) : faker.lorem.words(), + percent:faker.random.number(100), total_seconds:faker.random.number(1000000), + })) + return result.filter(({name}) => elements.includes(name) ? false : (elements.push(name), true)) + } + return ({ + status:200, + data:{ + data:{ + best_day:{ + created_at:faker.date.recent(), + date:`${faker.date.recent()}`.substring(0, 10), + total_seconds:faker.random.number(1000000), + }, + categories:stats(), + daily_average:faker.random.number(1000000000), + daily_average_including_other_language:faker.random.number(1000000000), + dependencies:stats(), + editors:stats(["VS Code", "Chrome", "IntelliJ", "PhpStorm", "WebStorm", "Android Studio", "Visual Studio", "Sublime Text", "PyCharm", "Vim", "Atom", "Xcode"]), + languages:stats(["JavaScript", "TypeScript", "PHP", "Java", "Python", "Vue.js", "HTML", "C#", "JSON", "Dart", "SCSS", "Kotlin", "JSX", "Go", "Ruby", "YAML"]), + machines:stats(), + operating_systems:stats(["Mac", "Windows", "Linux"]), + project:null, + projects:stats(), + total_seconds:faker.random.number(1000000000), + total_seconds_including_other_language:faker.random.number(1000000000), + }, + } + }) + } + } + } \ No newline at end of file diff --git a/source/app/web/statics/app.placeholder.js b/source/app/web/statics/app.placeholder.js index 62f0c246..5abf3ae4 100644 --- a/source/app/web/statics/app.placeholder.js +++ b/source/app/web/statics/app.placeholder.js @@ -415,6 +415,28 @@ return result } }) : null), + //Wakatime + ...(set.plugins.enabled.wakatime ? ({ + get wakatime() { + const stats = (array) => { + const elements = [] + let result = new Array(4+faker.random.number(2)).fill(null).map(_ => ({ + name:array ? faker.random.arrayElement(array) : faker.lorem.words(), + percent:faker.random.number(100)/100, total_seconds:faker.random.number(1000000), + })) + return result.filter(({name}) => elements.includes(name) ? false : (elements.push(name), true)).sort((a, b) => b.percent - a.percent) + } + return { + sections:options["wakatime.sections"].split(",").map(x => x.trim()).filter(x => x), + days:Number(options["wakatime.days"])||7, + time:{total:faker.random.number(100000), daily:faker.random.number(24)}, + editors:stats(["VS Code", "Chrome", "IntelliJ", "PhpStorm", "WebStorm", "Android Studio", "Visual Studio", "Sublime Text", "PyCharm", "Vim", "Atom", "Xcode"]), + languages:stats(["JavaScript", "TypeScript", "PHP", "Java", "Python", "Vue.js", "HTML", "C#", "JSON", "Dart", "SCSS", "Kotlin", "JSX", "Go", "Ruby", "YAML"]), + projects:stats(), + os:stats(["Mac", "Windows", "Linux"]), + } + } + }) : null), //Anilist ...(set.plugins.enabled.anilist ? ({ anilist:{ @@ -601,6 +623,12 @@ .replace(/(?<=[.])([1-9]*)(0+)$/, (m, a, b) => a) .replace(/[.]$/, "")}%` } + data.f.ellipsis = function (text, {length = 20} = {}) { + text = `${text}` + if (text.length < length) + return text + return `${text.substring(0, length)}…` + } //Render return await ejs.render(image, data, {async:true, rmWhitespace:true}) } diff --git a/source/plugins/wakatime/README.md b/source/plugins/wakatime/README.md new file mode 100644 index 00000000..be77308c --- /dev/null +++ b/source/plugins/wakatime/README.md @@ -0,0 +1,36 @@ +### ⏰ WakaTime plugin + +The *wakatime* plugin displays statistics from your [WakaTime](https://wakatime.com) account. + + + +
+ + +
+ +
+💬 Obtaining a WakaTime token + +Create a [WakaTime account](https://wakatime.com) and retrieve your API key in your [Account settings](https://wakatime.com/settings/account). + +![WakaTime API token](/.github/readme/imgs/plugin_wakatime_token.png) + +Then setup [WakaTime plugins](https://wakatime.com/plugins) to be ready to go! + +
+ +#### ℹ️ Examples workflows + +[➡️ Available options for this plugin](metadata.yml) + +```yaml +- uses: lowlighter/metrics@latest + with: + # ... other options + plugin_wakatime: yes # (🚧 @master feature) + plugin_wakatime_token: ${{ secrets.WAKATIME_TOKEN }} # Required + plugin_wakatime_days: 7 # Display last week stats + plugin_wakatime_sections: time, projects, projects-graphs # Display time and projects sections, along with projects graphs + plugin_wakatime_limit: 4 # Show 4 entries per graph +``` diff --git a/source/plugins/wakatime/index.mjs b/source/plugins/wakatime/index.mjs new file mode 100644 index 00000000..c29d0abc --- /dev/null +++ b/source/plugins/wakatime/index.mjs @@ -0,0 +1,45 @@ +//Setup + export default async function ({login, q, imports, data, account}, {enabled = false, token} = {}) { + //Plugin execution + try { + //Check if plugin is enabled and requirements are met + if ((!enabled)||(!q.wakatime)) + return null + + //Load inputs + let {sections, days, limit} = imports.metadata.plugins.wakatime.inputs({data, account, q}) + if (!limit) + limit = void(limit) + const range = {"7":"last_7_days", "30":"last_30_days", "180":"last_6_months", "365":"last_year"}[days] ?? "last_7_days" + + //Querying api and format result + //https://wakatime.com/developers#stats + console.debug(`metrics/compute/${login}/plugins > wakatime > querying api`) + const {data:{data:stats}} = await imports.axios.get(`https://wakatime.com/api/v1/users/current/stats/${range}?api_key=${token}`) + const result = { + sections, + days, + time:{ + total:stats.total_seconds/(60*60), + daily:stats.daily_average/(60*60), + }, + projects:stats.projects.map(({name, percent, total_seconds:total}) => ({name, percent:percent/100, total})).sort((a, b) => b.percent - a.percent).slice(0, limit), + languages:stats.languages.map(({name, percent, total_seconds:total}) => ({name, percent:percent/100, total})).sort((a, b) => b.percent - a.percent).slice(0, limit), + os:stats.operating_systems.map(({name, percent, total_seconds:total}) => ({name, percent:percent/100, total})).sort((a, b) => b.percent - a.percent).slice(0, limit), + editors:stats.editors.map(({name, percent, total_seconds:total}) => ({name, percent:percent/100, total})).sort((a, b) => b.percent - a.percent).slice(0, limit), + } + + //Result + return result + } + //Handle errors + catch (error) { + let message = "An error occured" + if (error.isAxiosError) { + const status = error.response?.status + message = `API returned ${status}` + error = error.response?.data ?? null + } + throw {error:{message, instance:error}} + } + } \ No newline at end of file diff --git a/source/plugins/wakatime/metadata.yml b/source/plugins/wakatime/metadata.yml new file mode 100644 index 00000000..c57af522 --- /dev/null +++ b/source/plugins/wakatime/metadata.yml @@ -0,0 +1,53 @@ +name: "⏰ WakaTime plugin" +cost: N/A +supports: + - user +inputs: + + # Enable or disable plugin + plugin_wakatime: + description: Display WakaTime stats + type: boolean + default: no + + # WakaTime API token + # See https://wakatime.com/settings/account get your API key + plugin_wakatime_token: + description: WakaTime API token + type: token + default: "" + + # Time range to use for displayed stats + plugin_wakatime_days: + description: WakaTime time range + type: string + values: + - 7 # Last week + - 30 # Last month + - 180 # Last 6 months + - 365 # Last year + default: 7 + + # Sections to display + plugin_wakatime_sections: + description: Sections to display + type: array + values: + - time # Show total coding time and daily average + - projects # Show most time spent project + - projects-graphs # Show most time spent projects graphs + - languages # Show most language + - languages-graphs # Show languages graphs + - editors # Show most used code editor + - editors-graphs # Show code editors graphs + - os # Show most used operating system + - os-graphs # Show code operating systems graphs + default: time, projects, projects-graphs, languages, languages-graphs, editors, os + + # Number of entries to display per graph + # Set to 0 to disable limitations + plugin_wakatime_limit: + description: Maximum number of entries to display per graph + type: number + default: 5 + min: 0 diff --git a/source/plugins/wakatime/tests.yml b/source/plugins/wakatime/tests.yml new file mode 100644 index 00000000..a8e29ff6 --- /dev/null +++ b/source/plugins/wakatime/tests.yml @@ -0,0 +1,15 @@ +- name: WakaTime plugin (default) + uses: lowlighter/metrics@latest + with: + token: NOT_NEEDED + plugin_wakatime_token: MOCKED_TOKEN + plugin_wakatime: yes + +- name: WakaTime plugin (complete) + uses: lowlighter/metrics@latest + with: + token: NOT_NEEDED + plugin_wakatime_token: MOCKED_TOKEN + plugin_wakatime: yes + plugin_wakatime_limit: 4 + plugin_wakatime_sections: time, projects, projects-graphs, languages, languages-graphs, editors, editors-graphs, os, os-graphs diff --git a/source/templates/classic/partials/_.json b/source/templates/classic/partials/_.json index cd6658ec..8a6bc5f9 100644 --- a/source/templates/classic/partials/_.json +++ b/source/templates/classic/partials/_.json @@ -17,5 +17,6 @@ "stargazers", "people", "activity", - "anilist" + "anilist", + "wakatime" ] \ No newline at end of file diff --git a/source/templates/classic/partials/wakatime.ejs b/source/templates/classic/partials/wakatime.ejs new file mode 100644 index 00000000..4fb9120a --- /dev/null +++ b/source/templates/classic/partials/wakatime.ejs @@ -0,0 +1,87 @@ +<% if (plugins.wakatime) { %> +
+

+ + WakaTime <%= plugins.wakatime?.days ? `(over last ${{7:"week", 30:"month", 180:"6 months", 365:"year"}[plugins.wakatime.days]})` : "" %> +

+ <% if (plugins.wakatime.error) { %> +
+
+
+ <%= plugins.wakatime.error.message %> +
+
+
+ <% } else { %> +
+
+ <% if (plugins.wakatime.sections.includes("time")) { %> +
+ + ~<%= f(Math.ceil(plugins.wakatime.time.total)) %> coding hour<%= s(plugins.wakatime.time.total) %> recorded +
+ <% } %> + <% if ((plugins.wakatime.sections.includes("projects"))&&(plugins.wakatime.projects?.length)) { %> +
+ + Working on <%= f.ellipsis(plugins.wakatime.projects[0]?.name, {length:16}) %> +
+ <% } %> + <% if ((plugins.wakatime.sections.includes("languages"))&&(plugins.wakatime.languages?.length)) { %> +
+ + Mostly coding in <%= plugins.wakatime.languages[0]?.name %> +
+ <% } %> +
+
+ <% if (plugins.wakatime.sections.includes("time")) { %> +
+ + ~<%= f(Math.ceil(plugins.wakatime.time.daily)) %> hour<%= s(plugins.wakatime.time.total) %> of coding per day +
+ <% } %> + <% if ((plugins.wakatime.sections.includes("editors"))&&(plugins.wakatime.editors?.length)) { %> +
+ + Coding with <%= plugins.wakatime.editors[0]?.name %> +
+ <% } %> + <% if ((plugins.wakatime.sections.includes("os"))&&(plugins.wakatime.os?.length)) { %> +
+ + Using <%= plugins.wakatime.os[0]?.name %> +
+ <% } %> +
+
+ + <% { const sections = plugins.wakatime.sections.filter(x => /-graphs$/.test(x)).map(x => x.replace(/-graphs$/, "")) %> + <% for (let i = 0; i < sections.length; i+=2) { %> +
+ <% for (let j = 0; j < 2; j++) { const key = sections[i+j] ; const section = plugins.wakatime[key] ; if (!key) continue %> +
+

<%= {languages:"Language activity", projects:"Projects activity", editors:"Code editors", os:"Operating systems"}[key] %>

+
+ <% if (section?.length) { %> + <% for (const {name, percent, total} of section) { %> +
+ <%= name %> +
+ <%= Math.round(100*percent) %>% +
+ <% } %> + <% } else { %> +
+
No activity
+
+ <% } %> +
+
+ <% } %> +
+ <% }} %> + + <% } %> +
+<% } %> \ No newline at end of file diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index 93d4cb8e..be74c297 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -361,6 +361,11 @@ font-size: 7px; } + .chart-bars .entry .empty { + width: 100%; + text-align: center; + } + .chart-bars .bar { width: 7px; background-color: var(--color-calendar-graph-day-bg); @@ -370,7 +375,6 @@ .chart-bars.horizontal { flex-direction: column; - align-items: space-between; height: 100%; } @@ -383,7 +387,10 @@ .chart-bars.horizontal .entry .name { flex-shrink: 0; text-align: right; - min-width: 30%; + width: 34%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .chart-bars .entry .bottom {