feat(wakatime): add segment

This commit is contained in:
Khaos66 2021-12-11 17:31:58 +01:00 committed by Jan De Dobbeleer
parent 2ccaf90cbf
commit 0fb5951375
9 changed files with 355 additions and 7 deletions

View file

@ -0,0 +1,57 @@
---
id: wakatime
title: Wakatime
sidebar_label: Wakatime
---
## What
Shows the tracked time on [wakatime][wt] of the current day
:::caution
You **must** request an API key at the [wakatime][wt] website.
The free tier for is sufficient. You'll find the API key in your profile settings page.
:::
## Sample Configuration
```json
{
"type": "wakatime",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "#ffffff",
"background": "#007acc",
"properties": {
"prefix": " \uf7d9 ",
"url": "https://wakatime.com/api/v1/users/current/summaries?start=today&end=today&api_key=API_KEY",
"cache_timeout": 10,
"http_timeout": 500
}
},
```
## Properties
- url: `string` - Your Wakatime [summaries][wk-summaries] URL, including the API key. Example above. You'll know this
works if you can curl it yourself and a result. - defaults to ``
- http_timeout: `int` - The default timeout for http request is 20ms. If no segment is shown, try increasing this timeout.
- cache_timeout: `int` - The default timeout for request caching is 10m. A value of 0 disables the cache.
- template: `string` - A go [text/template][go-text-template] template extended with [sprig][sprig] utilizing the
properties below - defaults to `{{ secondsRound .CummulativeTotal.Seconds }}`
## Template Properties
- `.CummulativeTotal`: `wtTotals` - object holding total tracked time values
### wtTotal Properties
- `.Seconds`: `int` - a number reprecenting the total tracked time in seconds
- `.Text`: `string` - a string with human readable tracked time (eg: "2 hrs 30 mins")
[wt]: https://wakatime.com
[wk-summaries]: https://wakatime.com/developers#summaries
[go-text-template]: https://golang.org/pkg/text/template/
[sprig]: https://masterminds.github.io/sprig/

View file

@ -73,6 +73,7 @@ module.exports = {
"terraform",
"text",
"time",
"wakatime",
"wifi",
"winreg",
"ytm",

View file

@ -133,6 +133,8 @@ const (
PHP SegmentType = "php"
// Nightscout is an open source diabetes system
Nightscout SegmentType = "nightscout"
// Wakatime writes tracked time spend in dev editors
Wakatime SegmentType = "wakatime"
// WiFi writes details about the current WiFi connection
WiFi SegmentType = "wifi"
// WinReg queries the Windows registry.
@ -266,6 +268,7 @@ func (segment *Segment) mapSegmentWithWriter(env environmentInfo) error {
Angular: &angular{},
PHP: &php{},
Nightscout: &nightscout{},
Wakatime: &wakatime{},
WiFi: &wifi{},
WinReg: &winreg{},
}

View file

@ -8,7 +8,7 @@ import (
)
const (
NSAPIURL = "FAKE"
FAKEAPIURL = "FAKE"
)
func TestNSSegment(t *testing.T) {
@ -136,10 +136,10 @@ func TestNSSegment(t *testing.T) {
}
cache := &MockedCache{}
cache.On("get", NSAPIURL).Return(tc.JSONResponse, !tc.CacheFoundFail)
cache.On("set", NSAPIURL, tc.JSONResponse, tc.CacheTimeout).Return()
cache.On("get", FAKEAPIURL).Return(tc.JSONResponse, !tc.CacheFoundFail)
cache.On("set", FAKEAPIURL, tc.JSONResponse, tc.CacheTimeout).Return()
env.On("doGet", NSAPIURL).Return([]byte(tc.JSONResponse), tc.Error)
env.On("doGet", FAKEAPIURL).Return([]byte(tc.JSONResponse), tc.Error)
env.On("cache", nil).Return(cache)
if tc.Template != "" {

79
src/segment_wakatime.go Normal file
View file

@ -0,0 +1,79 @@
package main
import (
"encoding/json"
)
type wakatime struct {
props properties
env environmentInfo
wtData
}
type wtTotals struct {
Seconds float64 `json:"seconds"`
Text string `json:"text"`
}
type wtData struct {
CummulativeTotal wtTotals `json:"cummulative_total"`
Start string `json:"start"`
End string `json:"end"`
}
func (w *wakatime) enabled() bool {
err := w.setAPIData()
return err == nil
}
func (w *wakatime) string() string {
segmentTemplate := w.props.getString(SegmentTemplate, "{{ secondsRound .CummulativeTotal.Seconds }}")
template := &textTemplate{
Template: segmentTemplate,
Context: w,
Env: w.env,
}
text, err := template.render()
if err != nil {
return err.Error()
}
return text
}
func (w *wakatime) setAPIData() error {
url := w.props.getString(URL, "")
cacheTimeout := w.props.getInt(CacheTimeout, DefaultCacheTimeout)
if cacheTimeout > 0 {
// check if data stored in cache
if val, found := w.env.cache().get(url); found {
err := json.Unmarshal([]byte(val), &w.wtData)
if err != nil {
return err
}
return nil
}
}
httpTimeout := w.props.getInt(HTTPTimeout, DefaultHTTPTimeout)
body, err := w.env.doGet(url, httpTimeout)
if err != nil {
return err
}
err = json.Unmarshal(body, &w.wtData)
if err != nil {
return err
}
if cacheTimeout > 0 {
w.env.cache().set(url, string(body), cacheTimeout)
}
return nil
}
func (w *wakatime) init(props properties, env environmentInfo) {
w.props = props
w.env = env
}

View file

@ -0,0 +1,93 @@
package main
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWTTrackedTime(t *testing.T) {
cases := []struct {
Case string
Seconds int
Expected string
Template string
CacheTimeout int
CacheFoundFail bool
Error error
}{
{
Case: "nothing tracked",
Seconds: 0,
Expected: "0s",
},
{
Case: "25 minutes",
Seconds: 1500,
Expected: "25m",
},
{
Case: "2 hours",
Seconds: 7200,
Expected: "2h",
},
{
Case: "2h 45m",
Seconds: 9900,
Expected: "2h 45m",
},
{
Case: "negative number",
Seconds: -9900,
Expected: "2h 45m",
},
{
Case: "cache 2h 45m",
Seconds: 9900,
Expected: "2h 45m",
CacheTimeout: 20,
},
{
Case: "no cache 2h 45m",
Seconds: 9900,
Expected: "2h 45m",
CacheTimeout: 20,
CacheFoundFail: true,
},
{
Case: "api error",
Seconds: 2,
Expected: "0s",
CacheTimeout: 20,
CacheFoundFail: true,
Error: errors.New("api error"),
},
}
for _, tc := range cases {
env := &MockedEnvironment{}
response := fmt.Sprintf(`{"cummulative_total": {"seconds": %.2f, "text": "x"}}`, float64(tc.Seconds))
env.On("doGet", FAKEAPIURL).Return([]byte(response), tc.Error)
cache := &MockedCache{}
cache.On("get", FAKEAPIURL).Return(response, !tc.CacheFoundFail)
cache.On("set", FAKEAPIURL, response, tc.CacheTimeout).Return()
env.On("cache", nil).Return(cache)
w := &wakatime{
props: map[Property]interface{}{
APIKey: "key",
CacheTimeout: tc.CacheTimeout,
URL: FAKEAPIURL,
},
env: env,
}
assert.ErrorIs(t, tc.Error, w.setAPIData(), tc.Case+" - Error")
assert.Equal(t, tc.Expected, w.string(), tc.Case+" - String")
}
}

View file

@ -6,8 +6,6 @@ import (
"reflect"
"strings"
"text/template"
"github.com/Masterminds/sprig/v3"
)
const (
@ -48,7 +46,7 @@ func (t *textTemplate) renderPlainContextTemplate(context map[string]interface{}
}
func (t *textTemplate) render() (string, error) {
tmpl, err := template.New("title").Funcs(sprig.TxtFuncMap()).Parse(t.Template)
tmpl, err := template.New("title").Funcs(funcMap()).Parse(t.Template)
if err != nil {
return "", errors.New(invalidTemplate)
}

81
src/template_func.go Normal file
View file

@ -0,0 +1,81 @@
package main
import (
"errors"
"strconv"
"strings"
"text/template"
"github.com/Masterminds/sprig/v3"
)
var funcMapCache map[string]interface{}
func funcMap() template.FuncMap {
if funcMapCache != nil {
return template.FuncMap(funcMapCache)
}
funcMapCache = map[string]interface{}{
"secondsRound": secondsRound,
}
for key, fun := range sprig.TxtFuncMap() {
if _, ok := funcMapCache[key]; !ok {
funcMapCache[key] = fun
}
}
return template.FuncMap(funcMapCache)
}
func parseSeconds(seconds interface{}) (int, error) {
switch seconds := seconds.(type) {
default:
return 0, errors.New("invalid seconds type")
case string:
return strconv.Atoi(seconds)
case int:
return seconds, nil
case int64:
return int(seconds), nil
case float64:
return int(seconds), nil
}
}
func secondsRound(seconds interface{}) string {
s, err := parseSeconds(seconds)
if err != nil {
return err.Error()
}
if s == 0 {
return "0s"
}
neg := s < 0
if neg {
s = -s
}
var (
second = 1
minute = 60
hour = minute * 60
day = hour * 24
month = day * 30
year = day * 365
)
var builder strings.Builder
writePart := func(unit int, name string) {
if s >= unit {
builder.WriteString(" ")
builder.WriteString(strconv.Itoa(s / unit))
builder.WriteString(name)
s %= unit
}
}
writePart(year, "y")
writePart(month, "mo")
writePart(day, "d")
writePart(hour, "h")
writePart(minute, "m")
writePart(second, "s")
return strings.Trim(builder.String(), " ")
}

View file

@ -181,6 +181,7 @@
"sysinfo",
"angular",
"php",
"wakatime",
"wifi",
"winreg",
"plastic"
@ -1649,6 +1650,41 @@
}
}
},
{
"if": {
"properties": {
"type": { "const": "wakatime" }
}
},
"then": {
"title": "Wakatime",
"description": "Displays the tracked time on wakatime.com",
"properties": {
"properties": {
"properties": {
"apikey": {
"type": "string",
"title": "apikey",
"description": "The apikey used for the api call (Required)",
"default": "."
},
"http_timeout": {
"$ref": "#/definitions/http_timeout"
},
"cache_timeout": {
"type": "integer",
"title": "cache timeout",
"description": "The number of minutes the response is cached. A value of 0 disables the cache.",
"default": 10
},
"template": {
"$ref": "#/definitions/template"
}
}
}
}
}
},
{
"if": {
"properties": {