mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2024-11-09 20:44:03 -08:00
feat(strava): new segment
This commit is contained in:
parent
88206ff9c9
commit
d2bf556e94
90
docs/docs/segment-strava.mdx
Normal file
90
docs/docs/segment-strava.mdx
Normal file
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
id: strava
|
||||
title: Strava
|
||||
sidebar_label: Strava
|
||||
---
|
||||
|
||||
import StravaConnect from '/img/strava_connect.svg';
|
||||
|
||||
## What
|
||||
|
||||
[Strava][strava] ia a popular activity tracker for bike, run or any other traning.
|
||||
To keep up with your training goals it is important to be constantly reminded about it.
|
||||
A Strava Oh My Posh segment would show your last activity,
|
||||
and also indicate by a color if it is time to get away from your computer and do some workout.
|
||||
|
||||
## Accessing your Strava data
|
||||
|
||||
To allow Oh My Posh access your Strava data you need to grant access to read your public activities.
|
||||
This will give you an access and a refresh token. Paste the tokens into your strava segment configuration.
|
||||
|
||||
Click the following link to connect with Strava:
|
||||
|
||||
<a href="https://www.strava.com/oauth/authorize?client_id=76033&response_type=code&redirect_uri=https://ohmyposh.dev/api/auth&approval_prompt=force&scope=read,activity:read&state=strava">
|
||||
<StravaConnect/>
|
||||
</a>
|
||||
|
||||
## Sample Configuration
|
||||
|
||||
This configuration sets the background green if you have an activity the last two days,
|
||||
orange if you have one last 5 days, and red else. The `foreground_templates` example below could be set to just a single color,
|
||||
if that color is visible against any of your backgrounds.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "strava",
|
||||
"style": "powerline",
|
||||
"powerline_symbol": "\uE0B0",
|
||||
"foreground": "#ffffff",
|
||||
"background": "#000000",
|
||||
"background_templates": [
|
||||
"{{ if gt .Hours 100 }}#dc3545{{ end }}",
|
||||
"{{ if and (lt .Hours 100) (gt .Hours 50) }}#ffc107{{ end }}",
|
||||
"{{ if lt .Hours 50 }}#28a745{{ end }}"
|
||||
],
|
||||
"foreground_templates": [
|
||||
"{{ if gt .Hours 100 }}#FFFFFF{{ end }}",
|
||||
"{{ if and (lt .Hours 100) (gt .Hours 50) }}#343a40{{ end }}",
|
||||
"{{ if lt .Hours 50 }}#FFFFFF{{ end }}"
|
||||
],
|
||||
"properties": {
|
||||
"access_token":"11111111111111111",
|
||||
"refresh_token":"1111111111111111",
|
||||
"http_timeout": 1500,
|
||||
"template": "{{.Name}} {{.Ago}} {{.ActivityIcon}}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
- access_token: `string` - token from Strava login, see login link in section above. It has the following format: `1111111111111111111111111`
|
||||
- refresh_token: `string` - token from Strava login, see login link in section above. It has the following format: `1111111111111111111111111`
|
||||
- http_timeout: `int` - how long do you want to wait before you want to see your prompt more than your strava data? - defaults to 500ms
|
||||
- template: `string` - a go [text/template][go-text-template] template extended with [sprig][sprig] utilizing the properties below.
|
||||
See the example above. Make sure your NerdFont has the glyph you want or search for one at nerdfonts.com
|
||||
- CacheTimeout: `int` in minutes - How long do you want your numbers cached? - defaults to 5 min
|
||||
- RideIcon - defaults to `\uf5a2`
|
||||
- RunIcon - defaults to `\ufc0c`
|
||||
- SkiingIcon - defaults to `\ue213`
|
||||
- WorkOutIcon - defaults to `\ue213`
|
||||
- UnknownActivityIcon - defaults to `\ue213`
|
||||
|
||||
## Template Properties
|
||||
|
||||
The properties below are availible for use in your template
|
||||
|
||||
- `.DateString`: `time` - The timestamp of the entry
|
||||
- `.Type`: `string` - Activity types as used in strava
|
||||
- `.UtcOffset`: `int` - The UTC offset
|
||||
- `.Hours`: `int` - Number of hours since last activity
|
||||
- `.Name`: `string` - The name of the activity
|
||||
- `.Duration`: `float64` - Total duration in seconds
|
||||
- `.Distance`: `float64` - Toatal distance in meters
|
||||
|
||||
Now, go out and have a fun ride or run!
|
||||
|
||||
[go-text-template]: https://golang.org/pkg/text/template/
|
||||
[sprig]: https://masterminds.github.io/sprig/
|
||||
[strava]: http://www.strava.com/
|
||||
[strava-connect]: https://www.strava.com/oauth/authorize?client_id=76033&response_type=code&redirect_uri=https://ohmyposh.dev/api/auth&approval_prompt=force&scope=read,activity:read
|
|
@ -70,6 +70,7 @@ module.exports = {
|
|||
"session",
|
||||
"shell",
|
||||
"spotify",
|
||||
"strava",
|
||||
"sysinfo",
|
||||
"terraform",
|
||||
"text",
|
||||
|
|
14
docs/static/img/strava_connect.svg
vendored
Normal file
14
docs/static/img/strava_connect.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 11 KiB |
|
@ -35,6 +35,10 @@ const (
|
|||
DisplayError Property = "display_error"
|
||||
// DisplayDefault hides or shows the default
|
||||
DisplayDefault Property = "display_default"
|
||||
// AccessToken is the access token to use for an API
|
||||
AccessToken Property = "access_token"
|
||||
// RefreshToken is the refresh token to use for an API
|
||||
RefreshToken Property = "refresh_token"
|
||||
)
|
||||
|
||||
type properties map[Property]interface{}
|
||||
|
|
|
@ -148,6 +148,8 @@ const (
|
|||
PHP SegmentType = "php"
|
||||
// Nightscout is an open source diabetes system
|
||||
Nightscout SegmentType = "nightscout"
|
||||
// Strava is a sports activity tracker
|
||||
Strava SegmentType = "strava"
|
||||
// Wakatime writes tracked time spend in dev editors
|
||||
Wakatime SegmentType = "wakatime"
|
||||
// WiFi writes details about the current WiFi connection
|
||||
|
@ -287,6 +289,7 @@ func (segment *Segment) mapSegmentWithWriter(env Environment) error {
|
|||
Angular: &angular{},
|
||||
PHP: &php{},
|
||||
Nightscout: &nightscout{},
|
||||
Strava: &strava{},
|
||||
Wakatime: &wakatime{},
|
||||
WiFi: &wifi{},
|
||||
WinReg: &winreg{},
|
||||
|
|
237
src/segment_strava.go
Normal file
237
src/segment_strava.go
Normal file
|
@ -0,0 +1,237 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// segment struct, makes templating easier
|
||||
type strava struct {
|
||||
props Properties
|
||||
env Environment
|
||||
|
||||
StravaData
|
||||
ActivityIcon string
|
||||
Ago string
|
||||
Hours int
|
||||
Authenticate bool
|
||||
Error string
|
||||
}
|
||||
|
||||
const (
|
||||
RideIcon Property = "ride_icon"
|
||||
RunIcon Property = "run_icon"
|
||||
SkiingIcon Property = "skiing_icon"
|
||||
WorkOutIcon Property = "workout_icon"
|
||||
UnknownActivityIcon Property = "unknown_activity_icon"
|
||||
|
||||
StravaAccessToken = "strava_access_token"
|
||||
StravaRefreshToken = "strava_refresh_token"
|
||||
|
||||
Timeout = "timeout"
|
||||
InvalidRefreshToken = "invalid refresh token"
|
||||
TokenRefreshFailed = "token refresh error"
|
||||
)
|
||||
|
||||
// StravaData struct contains the API data
|
||||
type StravaData struct {
|
||||
Type string `json:"type"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
Name string `json:"name"`
|
||||
Distance float64 `json:"distance"`
|
||||
Duration float64 `json:"moving_time"`
|
||||
}
|
||||
|
||||
type TokenExchange struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
type AuthError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (a *AuthError) Error() string {
|
||||
return a.message
|
||||
}
|
||||
|
||||
func (s *strava) enabled() bool {
|
||||
data, err := s.getResult()
|
||||
if err == nil {
|
||||
s.StravaData = *data
|
||||
s.ActivityIcon = s.getActivityIcon()
|
||||
s.Hours = s.getHours()
|
||||
s.Ago = s.getAgo()
|
||||
return true
|
||||
}
|
||||
if _, s.Authenticate = err.(*AuthError); s.Authenticate {
|
||||
s.Error = err.(*AuthError).Error()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *strava) getHours() int {
|
||||
hours := time.Since(s.StartDate).Hours()
|
||||
return int(math.Floor(hours))
|
||||
}
|
||||
|
||||
func (s *strava) getAgo() string {
|
||||
if s.Hours > 24 {
|
||||
days := int32(math.Floor(float64(s.Hours) / float64(24)))
|
||||
return fmt.Sprintf("%d", days) + string('d')
|
||||
}
|
||||
return fmt.Sprintf("%d", s.Hours) + string("h")
|
||||
}
|
||||
|
||||
func (s *strava) getActivityIcon() string {
|
||||
switch s.Type {
|
||||
case "Ride":
|
||||
return s.props.getString(RideIcon, "\uf5a2")
|
||||
case "Run":
|
||||
return s.props.getString(RunIcon, "\ufc0c")
|
||||
case "NordicSki":
|
||||
case "AlpineSki":
|
||||
case "BackcountrySki":
|
||||
return s.props.getString(SkiingIcon, "\ue213")
|
||||
case "WorkOut":
|
||||
return s.props.getString(WorkOutIcon, "\ue213")
|
||||
default:
|
||||
return s.props.getString(UnknownActivityIcon, "\ue213")
|
||||
}
|
||||
return s.props.getString(UnknownActivityIcon, "\ue213")
|
||||
}
|
||||
|
||||
func (s *strava) string() string {
|
||||
if s.Error != "" {
|
||||
return s.Error
|
||||
}
|
||||
segmentTemplate := s.props.getString(SegmentTemplate, "{{ .Ago }}")
|
||||
template := &textTemplate{
|
||||
Template: segmentTemplate,
|
||||
Context: s,
|
||||
Env: s.env,
|
||||
}
|
||||
text, err := template.render()
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func (s *strava) getAccessToken() (string, error) {
|
||||
// get directly from cache
|
||||
if acccessToken, OK := s.env.cache().get(StravaAccessToken); OK {
|
||||
return acccessToken, nil
|
||||
}
|
||||
// use cached refersh token to get new access token
|
||||
if refreshToken, OK := s.env.cache().get(StravaRefreshToken); OK {
|
||||
if acccessToken, err := s.refreshToken(refreshToken); err == nil {
|
||||
return acccessToken, nil
|
||||
}
|
||||
}
|
||||
// use initial refresh token from property
|
||||
refreshToken := s.props.getString(RefreshToken, "")
|
||||
if len(refreshToken) == 0 {
|
||||
return "", &AuthError{
|
||||
message: InvalidRefreshToken,
|
||||
}
|
||||
}
|
||||
// no need to let the user provide access token, we'll always verify the refresh token
|
||||
acccessToken, err := s.refreshToken(refreshToken)
|
||||
return acccessToken, err
|
||||
}
|
||||
|
||||
func (s *strava) refreshToken(refreshToken string) (string, error) {
|
||||
httpTimeout := s.props.getInt(HTTPTimeout, DefaultHTTPTimeout)
|
||||
url := fmt.Sprintf("https://ohmyposh.dev/api/refresh?segment=strava&token=%s", refreshToken)
|
||||
body, err := s.env.HTTPRequest(url, httpTimeout)
|
||||
if err != nil {
|
||||
return "", &AuthError{
|
||||
// This might happen if /api was asleep. Assume the user will just retry
|
||||
message: Timeout,
|
||||
}
|
||||
}
|
||||
tokens := &TokenExchange{}
|
||||
err = json.Unmarshal(body, &tokens)
|
||||
if err != nil {
|
||||
return "", &AuthError{
|
||||
message: TokenRefreshFailed,
|
||||
}
|
||||
}
|
||||
// add tokens to cache
|
||||
s.env.cache().set(StravaAccessToken, tokens.AccessToken, tokens.ExpiresIn/60)
|
||||
s.env.cache().set(StravaRefreshToken, tokens.RefreshToken, 2*525960) // it should never expire unless revoked, default to 2 year
|
||||
return tokens.AccessToken, nil
|
||||
}
|
||||
|
||||
func (s *strava) getResult() (*StravaData, error) {
|
||||
parseSingleElement := func(data []byte) (*StravaData, error) {
|
||||
var result []*StravaData
|
||||
err := json.Unmarshal(data, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil, errors.New("no elements in the array")
|
||||
}
|
||||
return result[0], nil
|
||||
}
|
||||
getCacheValue := func(key string) (*StravaData, error) {
|
||||
val, found := s.env.cache().get(key)
|
||||
// we got something from the cache
|
||||
if found {
|
||||
if data, err := parseSingleElement([]byte(val)); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("no data in cache")
|
||||
}
|
||||
|
||||
// We only want the last activity
|
||||
url := "https://www.strava.com/api/v3/athlete/activities?page=1&per_page=1"
|
||||
httpTimeout := s.props.getInt(HTTPTimeout, DefaultHTTPTimeout)
|
||||
|
||||
// No need to check more the every 30 min
|
||||
cacheTimeout := s.props.getInt(CacheTimeout, 30)
|
||||
if cacheTimeout > 0 {
|
||||
if data, err := getCacheValue(url); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
accessToken, err := s.getAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addAuthHeader := func(request *http.Request) {
|
||||
request.Header.Add("Authorization", "Bearer "+accessToken)
|
||||
}
|
||||
body, err := s.env.HTTPRequest(url, httpTimeout, addAuthHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var arr []*StravaData
|
||||
err = json.Unmarshal(body, &arr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := parseSingleElement(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cacheTimeout > 0 {
|
||||
// persist new sugars in cache
|
||||
s.env.cache().set(url, string(body), cacheTimeout)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *strava) init(props Properties, env Environment) {
|
||||
s.props = props
|
||||
s.env = env
|
||||
}
|
198
src/segment_strava_test.go
Normal file
198
src/segment_strava_test.go
Normal file
|
@ -0,0 +1,198 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStravaSegment(t *testing.T) {
|
||||
h, _ := time.ParseDuration("6h")
|
||||
sixHoursAgo := time.Now().Add(-h).Format(time.RFC3339)
|
||||
h, _ = time.ParseDuration("100h")
|
||||
fourDaysAgo := time.Now().Add(-h).Format(time.RFC3339)
|
||||
|
||||
cases := []struct {
|
||||
Case string
|
||||
JSONResponse string
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
AccessTokenCacheFoundFail bool
|
||||
RefreshTokenCacheFoundFail bool
|
||||
InitialAccessToken string
|
||||
InitialRefreshToken string
|
||||
TokenRefreshToken string
|
||||
TokenResponse string
|
||||
TokenTest bool
|
||||
ExpectedString string
|
||||
ExpectedEnabled bool
|
||||
CacheTimeout int
|
||||
CacheFoundFail bool
|
||||
Template string
|
||||
Error error
|
||||
AuthDebugMsg string
|
||||
}{
|
||||
{
|
||||
Case: "No initial tokens",
|
||||
InitialAccessToken: "",
|
||||
AccessTokenCacheFoundFail: true,
|
||||
RefreshTokenCacheFoundFail: true,
|
||||
TokenTest: true,
|
||||
AuthDebugMsg: "invalid refresh token",
|
||||
},
|
||||
{
|
||||
Case: "Use initial tokens",
|
||||
AccessToken: "NEW_ACCESSTOKEN",
|
||||
InitialAccessToken: "INITIAL ACCESSTOKEN",
|
||||
InitialRefreshToken: "INITIAL REFRESHTOKEN",
|
||||
TokenRefreshToken: "INITIAL REFRESHTOKEN",
|
||||
TokenResponse: `{ "access_token":"NEW_ACCESSTOKEN","refresh_token":"NEW_REFRESHTOKEN", "expires_in":1234 }`,
|
||||
AccessTokenCacheFoundFail: true,
|
||||
RefreshTokenCacheFoundFail: true,
|
||||
TokenTest: true,
|
||||
},
|
||||
{
|
||||
Case: "Access token from cache",
|
||||
AccessToken: "ACCESSTOKEN",
|
||||
TokenTest: true,
|
||||
},
|
||||
{
|
||||
Case: "Refresh token from cache",
|
||||
AccessTokenCacheFoundFail: true,
|
||||
RefreshTokenCacheFoundFail: false,
|
||||
RefreshToken: "REFRESHTOKEN",
|
||||
TokenRefreshToken: "REFRESHTOKEN",
|
||||
TokenTest: true,
|
||||
AuthDebugMsg: "invalid refresh token",
|
||||
},
|
||||
{
|
||||
Case: "Ride 6",
|
||||
JSONResponse: `
|
||||
[{"type":"Ride","start_date":"` + sixHoursAgo + `","name":"Sesongens første på tjukkas","distance":16144.0}]`,
|
||||
Template: "{{.Ago}} {{.ActivityIcon}}",
|
||||
ExpectedString: "6h \uf5a2",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Run 100",
|
||||
JSONResponse: `
|
||||
[{"type":"Run","start_date":"` + fourDaysAgo + `","name":"Sesongens første på tjukkas","distance":16144.0,"moving_time":7665}]`,
|
||||
Template: "{{.Ago}} {{.ActivityIcon}}",
|
||||
ExpectedString: "4d \ufc0c",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Error in retrieving data",
|
||||
JSONResponse: "nonsense",
|
||||
Error: errors.New("Something went wrong"),
|
||||
ExpectedEnabled: false,
|
||||
},
|
||||
{
|
||||
Case: "Empty array",
|
||||
JSONResponse: "[]",
|
||||
ExpectedEnabled: false,
|
||||
},
|
||||
{
|
||||
Case: "Run from cache",
|
||||
JSONResponse: `
|
||||
[{"type":"Run","start_date":"` + fourDaysAgo + `","name":"Sesongens første på tjukkas","distance":16144.0,"moving_time":7665}]`,
|
||||
Template: "{{.Ago}} {{.ActivityIcon}}",
|
||||
ExpectedString: "4d \ufc0c",
|
||||
ExpectedEnabled: true,
|
||||
CacheTimeout: 10,
|
||||
},
|
||||
{
|
||||
Case: "Run from not found cache",
|
||||
JSONResponse: `
|
||||
[{"type":"Run","start_date":"` + fourDaysAgo + `","name":"Morning ride","distance":16144.0,"moving_time":7665}]`,
|
||||
Template: "{{.Ago}} {{.ActivityIcon}} {{.Name}} {{.Hours}}h ago",
|
||||
ExpectedString: "4d \ufc0c Morning ride 100h ago",
|
||||
ExpectedEnabled: true,
|
||||
CacheTimeout: 10,
|
||||
CacheFoundFail: true,
|
||||
},
|
||||
{
|
||||
Case: "Error parsing response",
|
||||
JSONResponse: `
|
||||
4tffgt4e4567`,
|
||||
Template: "{{.Ago}}{{.ActivityIcon}}",
|
||||
ExpectedString: "50",
|
||||
ExpectedEnabled: false,
|
||||
CacheTimeout: 10,
|
||||
},
|
||||
{
|
||||
Case: "Faulty template",
|
||||
JSONResponse: `
|
||||
[{"sgv":50,"direction":"DoubleDown"}]`,
|
||||
Template: "{{.Ago}}{{.Burp}}",
|
||||
ExpectedString: incorrectTemplate,
|
||||
ExpectedEnabled: true,
|
||||
CacheTimeout: 10,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
env := &MockedEnvironment{}
|
||||
url := "https://www.strava.com/api/v3/athlete/activities?page=1&per_page=1"
|
||||
tokenURL := fmt.Sprintf("https://ohmyposh.dev/api/refresh?segment=strava&token=%s", tc.TokenRefreshToken)
|
||||
var props properties = map[Property]interface{}{
|
||||
CacheTimeout: tc.CacheTimeout,
|
||||
}
|
||||
cache := &MockedCache{}
|
||||
cache.On("get", url).Return(tc.JSONResponse, !tc.CacheFoundFail)
|
||||
cache.On("set", url, tc.JSONResponse, tc.CacheTimeout).Return()
|
||||
|
||||
cache.On("get", StravaAccessToken).Return(tc.AccessToken, !tc.AccessTokenCacheFoundFail)
|
||||
cache.On("get", StravaRefreshToken).Return(tc.RefreshToken, !tc.RefreshTokenCacheFoundFail)
|
||||
|
||||
cache.On("set", StravaRefreshToken, "NEW_REFRESHTOKEN", 2*525960)
|
||||
cache.On("set", StravaAccessToken, "NEW_ACCESSTOKEN", 20)
|
||||
|
||||
env.On("HTTPRequest", url).Return([]byte(tc.JSONResponse), tc.Error)
|
||||
env.On("HTTPRequest", tokenURL).Return([]byte(tc.TokenResponse), tc.Error)
|
||||
env.On("cache", nil).Return(cache)
|
||||
|
||||
if tc.Template != "" {
|
||||
props[SegmentTemplate] = tc.Template
|
||||
}
|
||||
if tc.InitialAccessToken != "" {
|
||||
props[AccessToken] = tc.InitialAccessToken
|
||||
}
|
||||
if tc.InitialRefreshToken != "" {
|
||||
props[RefreshToken] = tc.InitialRefreshToken
|
||||
}
|
||||
|
||||
ns := &strava{
|
||||
props: props,
|
||||
env: env,
|
||||
}
|
||||
|
||||
if tc.TokenTest {
|
||||
// continue
|
||||
at, err := ns.getAccessToken()
|
||||
if err != nil {
|
||||
if authErr, ok := err.(*AuthError); ok {
|
||||
assert.Equal(t, tc.AuthDebugMsg, authErr.Error(), tc.Case)
|
||||
} else {
|
||||
assert.Equal(t, tc.Error, err, tc.Case)
|
||||
}
|
||||
} else {
|
||||
assert.Equal(t, tc.AccessToken, at, tc.Case)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
enabled := ns.enabled()
|
||||
assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case)
|
||||
if !enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
var a = ns.string()
|
||||
|
||||
assert.Equal(t, tc.ExpectedString, a, tc.Case)
|
||||
}
|
||||
}
|
112
themes/larserikfinholt.omp.json
Normal file
112
themes/larserikfinholt.omp.json
Normal file
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "prompt",
|
||||
"alignment": "left",
|
||||
"segments": [
|
||||
{
|
||||
"type": "session",
|
||||
"style": "diamond",
|
||||
"foreground": "#ffffff",
|
||||
"background": "#c386f1",
|
||||
"leading_diamond": "",
|
||||
"trailing_diamond": "\uE0B0",
|
||||
"properties": {
|
||||
"template": "{{ .UserName }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "path",
|
||||
"style": "powerline",
|
||||
"powerline_symbol": "\uE0B0",
|
||||
"foreground": "#ffffff",
|
||||
"background": "#ff479c",
|
||||
"properties": {
|
||||
"prefix": " ",
|
||||
"home_icon": "~",
|
||||
"folder_separator_icon": " \uE0b1 ",
|
||||
"style": "folder"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "git",
|
||||
"style": "powerline",
|
||||
"powerline_symbol": "\uE0B0",
|
||||
"foreground": "#193549",
|
||||
"background": "#fffb38",
|
||||
"background_templates": [
|
||||
"{{ if or (.Working.Changed) (.Staging.Changed) }}#FF9248{{ end }}",
|
||||
"{{ if and (gt .Ahead 0) (gt .Behind 0) }}#ff4500{{ end }}",
|
||||
"{{ if gt .Ahead 0 }}#B388FF{{ end }}",
|
||||
"{{ if gt .Behind 0 }}#B388FF{{ end }}"
|
||||
],
|
||||
"leading_diamond": "",
|
||||
"trailing_diamond": "",
|
||||
"properties": {
|
||||
"fetch_status": true,
|
||||
"fetch_stash_count": true,
|
||||
"fetch_upstream_icon": true,
|
||||
"branch_max_length": 25,
|
||||
"template": "{{ .UpstreamIcon }}{{ .HEAD }}{{ .BranchStatus }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}{{ if and (.Working.Changed) (.Staging.Changed) }} |{{ end }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if gt .StashCount 0 }} \uF692 {{ .StashCount }}{{ end }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "executiontime",
|
||||
"style": "plain",
|
||||
"foreground": "#ffffff",
|
||||
"background": "#83769c",
|
||||
"leading_diamond": "",
|
||||
"trailing_diamond": "",
|
||||
"properties": {
|
||||
"always_enabled": true,
|
||||
"prefix": "<transparent>\uE0B0</> \ufbab",
|
||||
"postfix": "\u2800"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "strava",
|
||||
"style": "powerline",
|
||||
"foreground": "#ffffff",
|
||||
"background": "#000000",
|
||||
"background_templates": [
|
||||
"{{ if gt .Hours 100 }}#dc3545{{ end }}",
|
||||
"{{ if and (lt .Hours 100) (gt .Hours 50) }}#ffc107{{ end }}",
|
||||
"{{ if lt .Hours 50 }}#28a745{{ end }}"
|
||||
],
|
||||
"foreground_templates": [
|
||||
"{{ if gt .Hours 100 }}#FFFFFF{{ end }}",
|
||||
"{{ if and (lt .Hours 100) (gt .Hours 50) }}#343a40{{ end }}",
|
||||
"{{ if lt .Hours 50 }}#FFFFFF{{ end }}"
|
||||
],
|
||||
"leading_diamond": "",
|
||||
"trailing_diamond": "",
|
||||
"properties": {
|
||||
"access_token": "0ccbd2ac1e37a5b84101468df3d367177fe02ab3",
|
||||
"refresh_token": "111111111111111111111111111111",
|
||||
"http_timeout": 1500,
|
||||
"template": "{{.Name}} {{.Ago}} {{.ActivityIcon}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "exit",
|
||||
"style": "diamond",
|
||||
"foreground": "#ffffff",
|
||||
"background": "#00897b",
|
||||
"background_templates": ["{{ if gt .Code 0 }}#e91e63{{ end }}"],
|
||||
"leading_diamond": "",
|
||||
"trailing_diamond": "\uE0B4",
|
||||
"properties": {
|
||||
"always_enabled": true,
|
||||
"template": "\uE23A",
|
||||
"prefix": "<parentBackground>\uE0B0</> "
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"final_space": true,
|
||||
"console_title": true,
|
||||
"console_title_style": "template",
|
||||
"console_title_template": "{{ .Shell }} in {{ .Folder }}"
|
||||
}
|
|
@ -178,6 +178,7 @@
|
|||
"rust",
|
||||
"owm",
|
||||
"sysinfo",
|
||||
"strava",
|
||||
"angular",
|
||||
"php",
|
||||
"wakatime",
|
||||
|
@ -1604,6 +1605,71 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": { "const": "strava" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"title": "Display training data from Strava",
|
||||
"description": "https://ohmyposh.dev/docs/strava",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"title": "URL of API with Strava data",
|
||||
"description": "Url of your api provinding a Strava activity",
|
||||
"default": ""
|
||||
},
|
||||
"ride_icon": {
|
||||
"type": "string",
|
||||
"title": "Alternative icon",
|
||||
"description": "Alternative icon for this activity type",
|
||||
"default": "\uf5a2"
|
||||
},
|
||||
"run_icon": {
|
||||
"type": "string",
|
||||
"title": "Alternative icon",
|
||||
"description": "Alternative icon for this activity type",
|
||||
"default": "\ufc0c"
|
||||
},
|
||||
"skiing_icon": {
|
||||
"type": "string",
|
||||
"title": "Alternative icon",
|
||||
"description": "Alternative icon for this activity type",
|
||||
"default": "\ue213"
|
||||
},
|
||||
"workout_icon": {
|
||||
"type": "string",
|
||||
"title": "Alternative icon",
|
||||
"description": "Alternative icon for this activity type",
|
||||
"default": "\ue213"
|
||||
},
|
||||
"unknown_activity_icon": {
|
||||
"type": "string",
|
||||
"title": "Fallback icon",
|
||||
"description": "Fallback icon for other activity types",
|
||||
"default": "\ue213"
|
||||
},
|
||||
"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": {
|
||||
|
|
Loading…
Reference in a new issue