mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2025-03-05 20:49:04 -08:00
feat(wakatime): add segment
This commit is contained in:
parent
2ccaf90cbf
commit
0fb5951375
57
docs/docs/segment-wakatime.md
Normal file
57
docs/docs/segment-wakatime.md
Normal 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/
|
|
@ -73,6 +73,7 @@ module.exports = {
|
||||||
"terraform",
|
"terraform",
|
||||||
"text",
|
"text",
|
||||||
"time",
|
"time",
|
||||||
|
"wakatime",
|
||||||
"wifi",
|
"wifi",
|
||||||
"winreg",
|
"winreg",
|
||||||
"ytm",
|
"ytm",
|
||||||
|
|
|
@ -133,6 +133,8 @@ const (
|
||||||
PHP SegmentType = "php"
|
PHP SegmentType = "php"
|
||||||
// Nightscout is an open source diabetes system
|
// Nightscout is an open source diabetes system
|
||||||
Nightscout SegmentType = "nightscout"
|
Nightscout SegmentType = "nightscout"
|
||||||
|
// Wakatime writes tracked time spend in dev editors
|
||||||
|
Wakatime SegmentType = "wakatime"
|
||||||
// WiFi writes details about the current WiFi connection
|
// WiFi writes details about the current WiFi connection
|
||||||
WiFi SegmentType = "wifi"
|
WiFi SegmentType = "wifi"
|
||||||
// WinReg queries the Windows registry.
|
// WinReg queries the Windows registry.
|
||||||
|
@ -266,6 +268,7 @@ func (segment *Segment) mapSegmentWithWriter(env environmentInfo) error {
|
||||||
Angular: &angular{},
|
Angular: &angular{},
|
||||||
PHP: &php{},
|
PHP: &php{},
|
||||||
Nightscout: &nightscout{},
|
Nightscout: &nightscout{},
|
||||||
|
Wakatime: &wakatime{},
|
||||||
WiFi: &wifi{},
|
WiFi: &wifi{},
|
||||||
WinReg: &winreg{},
|
WinReg: &winreg{},
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NSAPIURL = "FAKE"
|
FAKEAPIURL = "FAKE"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNSSegment(t *testing.T) {
|
func TestNSSegment(t *testing.T) {
|
||||||
|
@ -136,10 +136,10 @@ func TestNSSegment(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
cache := &MockedCache{}
|
cache := &MockedCache{}
|
||||||
cache.On("get", NSAPIURL).Return(tc.JSONResponse, !tc.CacheFoundFail)
|
cache.On("get", FAKEAPIURL).Return(tc.JSONResponse, !tc.CacheFoundFail)
|
||||||
cache.On("set", NSAPIURL, tc.JSONResponse, tc.CacheTimeout).Return()
|
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)
|
env.On("cache", nil).Return(cache)
|
||||||
|
|
||||||
if tc.Template != "" {
|
if tc.Template != "" {
|
||||||
|
|
79
src/segment_wakatime.go
Normal file
79
src/segment_wakatime.go
Normal 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
|
||||||
|
}
|
93
src/segment_wakatime_test.go
Normal file
93
src/segment_wakatime_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,8 +6,6 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/Masterminds/sprig/v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -48,7 +46,7 @@ func (t *textTemplate) renderPlainContextTemplate(context map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *textTemplate) render() (string, error) {
|
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 {
|
if err != nil {
|
||||||
return "", errors.New(invalidTemplate)
|
return "", errors.New(invalidTemplate)
|
||||||
}
|
}
|
||||||
|
|
81
src/template_func.go
Normal file
81
src/template_func.go
Normal 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(), " ")
|
||||||
|
}
|
|
@ -181,6 +181,7 @@
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"angular",
|
"angular",
|
||||||
"php",
|
"php",
|
||||||
|
"wakatime",
|
||||||
"wifi",
|
"wifi",
|
||||||
"winreg",
|
"winreg",
|
||||||
"plastic"
|
"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": {
|
"if": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
Loading…
Reference in a new issue