diff --git a/.github/readme/imgs/plugin_wakatime_token.png b/.github/readme/imgs/plugin_wakatime_token.png
new file mode 100644
index 00000000..2e113059
Binary files /dev/null and b/.github/readme/imgs/plugin_wakatime_token.png differ
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).
+
+
+
+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 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 {