feat: withings segment

This commit is contained in:
Jan De Dobbeleer 2022-07-17 21:11:23 +02:00 committed by Jan De Dobbeleer
parent e5bf5db9c2
commit 2ec6b085fd
24 changed files with 563 additions and 76 deletions

View file

@ -53,7 +53,6 @@ if you do `Codespaces: Rebuild Container` again, you'll be back to the latest st
[docs]: https://ohmyposh.dev/docs
[guide]: https://ohmyposh.dev/docs/contributing/started
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary
[conduct]: mailto:conduct@ohmyposh.dev
[codespaces]: https://github.com/features/codespaces
[devcontainer-ext]: https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers
[timezones]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

View file

@ -190,6 +190,8 @@ const (
WIFI SegmentType = "wifi"
// WINREG queries the Windows registry.
WINREG SegmentType = "winreg"
// WITHINGS queries the Withings API.
WITHINGS SegmentType = "withings"
// YTM writes YouTube Music information and status
YTM SegmentType = "ytm"
)
@ -318,6 +320,7 @@ func (segment *Segment) mapSegmentWithWriter(env environment.Environment) error
WAKATIME: &segments.Wakatime{},
WIFI: &segments.Wifi{},
WINREG: &segments.WindowsRegistry{},
WITHINGS: &segments.Withings{},
YTM: &segments.Ytm{},
}
if segment.Properties == nil {

View file

@ -92,7 +92,7 @@ func uint32ToFloat64(num uint32) (float64, error) {
}
func setupDiSetup(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) (uintptr, error) {
r1, _, errno := syscall.Syscall6(proc.Addr(), nargs, a1, a2, a3, a4, a5, a6)
r1, _, errno := syscall.SyscallN(proc.Addr(), nargs, a1, a2, a3, a4, a5, a6)
if windows.Handle(r1) == windows.InvalidHandle {
if errno != 0 {
return 0, error(errno)
@ -103,7 +103,7 @@ func setupDiSetup(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr)
}
func setupDiCall(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) syscall.Errno {
r1, _, errno := syscall.Syscall6(proc.Addr(), nargs, a1, a2, a3, a4, a5, a6)
r1, _, errno := syscall.SyscallN(proc.Addr(), nargs, a1, a2, a3, a4, a5, a6)
if r1 == 0 {
if errno != 0 {
return errno
@ -148,7 +148,7 @@ func systemGet(idx int) (*battery, error) {
return nil, err
}
defer func() {
_, _, _ = syscall.Syscall(setupDiDestroyDeviceInfoList.Addr(), 1, hdev, 0, 0)
_, _, _ = syscall.SyscallN(setupDiDestroyDeviceInfoList.Addr(), 1, hdev, 0, 0)
}()
var did spDeviceInterfaceData

View file

@ -9,6 +9,7 @@ import (
"io/fs"
"log"
"net/http"
"net/http/httputil"
"oh-my-posh/environment/battery"
"oh-my-posh/environment/cmd"
"oh-my-posh/regex"
@ -166,7 +167,7 @@ type Environment interface {
BatteryState() (*battery.Info, error)
QueryWindowTitles(processName, windowTitleRegex string) (string, error)
WindowsRegistryKeyValue(path string) (*WindowsRegistryValue, error)
HTTPRequest(url string, timeout int, requestModifiers ...HTTPRequestModifier) ([]byte, error)
HTTPRequest(url string, body io.Reader, timeout int, requestModifiers ...HTTPRequestModifier) ([]byte, error)
IsWsl() bool
IsWsl2() bool
StackCount() int
@ -271,7 +272,7 @@ func (env *ShellEnvironment) resolveConfigPath() {
func (env *ShellEnvironment) downloadConfig(location string) error {
defer env.Trace(time.Now(), "downloadConfig", location)
configPath := filepath.Join(env.CachePath(), "config.omp.json")
cfg, err := env.HTTPRequest(location, 5000)
cfg, err := env.HTTPRequest(location, nil, 5000)
if err != nil {
return err
}
@ -572,17 +573,21 @@ func (env *ShellEnvironment) Shell() string {
return env.CmdFlags.Shell
}
func (env *ShellEnvironment) HTTPRequest(targetURL string, timeout int, requestModifiers ...HTTPRequestModifier) ([]byte, error) {
func (env *ShellEnvironment) HTTPRequest(targetURL string, body io.Reader, timeout int, requestModifiers ...HTTPRequestModifier) ([]byte, error) {
defer env.Trace(time.Now(), "HTTPRequest", targetURL)
ctx, cncl := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
defer cncl()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil)
request, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, body)
if err != nil {
return nil, err
}
for _, modifier := range requestModifiers {
modifier(request)
}
if env.CmdFlags.Debug {
dump, _ := httputil.DumpRequestOut(request, true)
env.Log(Debug, "HTTPRequest", string(dump))
}
response, err := client.Do(request)
if err != nil {
env.Log(Error, "HTTPRequest", err.Error())
@ -596,13 +601,13 @@ func (env *ShellEnvironment) HTTPRequest(targetURL string, timeout int, requestM
return nil, err
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
responseBody, err := io.ReadAll(response.Body)
if err != nil {
env.Log(Error, "HTTPRequest", err.Error())
return nil, err
}
env.Log(Debug, "HTTPRequest", string(body))
return body, nil
env.Log(Debug, "HTTPRequest", string(responseBody))
return responseBody, nil
}
func (env *ShellEnvironment) HasParentFilePath(path string) (*FileInfo, error) {

View file

@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"oh-my-posh/environment"
"oh-my-posh/properties"
@ -45,11 +46,11 @@ func (o *OAuth) error(err error) {
func (o *OAuth) getAccessToken() (string, error) {
// get directly from cache
if acccessToken, OK := o.Env.Cache().Get(o.AccessTokenKey); OK {
if acccessToken, OK := o.Env.Cache().Get(o.AccessTokenKey); OK && len(acccessToken) != 0 {
return acccessToken, nil
}
// use cached refresh token to get new access token
if refreshToken, OK := o.Env.Cache().Get(o.RefreshTokenKey); OK {
if refreshToken, OK := o.Env.Cache().Get(o.RefreshTokenKey); OK && len(refreshToken) != 0 {
if acccessToken, err := o.refreshToken(refreshToken); err == nil {
return acccessToken, nil
}
@ -70,7 +71,7 @@ func (o *OAuth) getAccessToken() (string, error) {
func (o *OAuth) refreshToken(refreshToken string) (string, error) {
httpTimeout := o.Props.GetInt(properties.HTTPTimeout, properties.DefaultHTTPTimeout)
url := fmt.Sprintf("https://ohmyposh.dev/api/refresh?segment=%s&token=%s", o.SegmentName, refreshToken)
body, err := o.Env.HTTPRequest(url, httpTimeout)
body, err := o.Env.HTTPRequest(url, nil, httpTimeout)
if err != nil {
return "", &OAuthError{
// This might happen if /api was asleep. Assume the user will just retry
@ -90,7 +91,7 @@ func (o *OAuth) refreshToken(refreshToken string) (string, error) {
return tokens.AccessToken, nil
}
func OauthResult[a any](o *OAuth, url string) (a, error) {
func OauthResult[a any](o *OAuth, url string, body io.Reader, requestModifiers ...environment.HTTPRequestModifier) (a, error) {
var data a
getCacheValue := func(key string) (a, error) {
@ -126,20 +127,26 @@ func OauthResult[a any](o *OAuth, url string) (a, error) {
request.Header.Add("Authorization", "Bearer "+accessToken)
}
body, err := o.Env.HTTPRequest(url, httpTimeout, addAuthHeader)
if requestModifiers == nil {
requestModifiers = []environment.HTTPRequestModifier{}
}
requestModifiers = append(requestModifiers, addAuthHeader)
responseBody, err := o.Env.HTTPRequest(url, body, httpTimeout, requestModifiers...)
if err != nil {
o.error(err)
return data, err
}
err = json.Unmarshal(body, &data)
err = json.Unmarshal(responseBody, &data)
if err != nil {
o.error(err)
return data, err
}
if cacheTimeout > 0 {
o.Env.Cache().Set(url, string(body), cacheTimeout)
o.Env.Cache().Set(url, string(responseBody), cacheTimeout)
}
return data, nil

View file

@ -171,7 +171,7 @@ func TestOauthResult(t *testing.T) {
SegmentName: "test",
}
got, err := OauthResult[*data](oauth, url)
got, err := OauthResult[*data](oauth, url, nil)
assert.Equal(t, tc.ExpectedData, got, tc.Case)
if len(tc.ExpectedErrorMessage) == 0 {
assert.Nil(t, err, tc.Case)

View file

@ -1,6 +1,7 @@
package mock
import (
"io"
"io/fs"
"oh-my-posh/environment"
"oh-my-posh/environment/battery"
@ -143,7 +144,7 @@ func (env *MockedEnvironment) WindowsRegistryKeyValue(path string) (*environment
return args.Get(0).(*environment.WindowsRegistryValue), args.Error(1)
}
func (env *MockedEnvironment) HTTPRequest(url string, timeout int, requestModifiers ...environment.HTTPRequestModifier) ([]byte, error) {
func (env *MockedEnvironment) HTTPRequest(url string, body io.Reader, timeout int, requestModifiers ...environment.HTTPRequestModifier) ([]byte, error) {
args := env.Called(url)
return args.Get(0).([]byte), args.Error(1)
}

View file

@ -258,7 +258,7 @@ func (bf *Brewfather) getResult() (*Batch, error) {
addAuthHeader := func(request *http.Request) {
request.Header.Add("authorization", authHeader)
}
body, err := bf.env.HTTPRequest(batchURL, httpTimeout, addAuthHeader)
body, err := bf.env.HTTPRequest(batchURL, nil, httpTimeout, addAuthHeader)
if err != nil {
return nil, err
}
@ -270,7 +270,7 @@ func (bf *Brewfather) getResult() (*Batch, error) {
}
// readings
body, err = bf.env.HTTPRequest(batchReadingsURL, httpTimeout, addAuthHeader)
body, err = bf.env.HTTPRequest(batchReadingsURL, nil, httpTimeout, addAuthHeader)
if err != nil {
return nil, err
}

View file

@ -45,7 +45,7 @@ func (i *IPify) getResult() (string, error) {
httpTimeout := i.props.GetInt(properties.HTTPTimeout, properties.DefaultHTTPTimeout)
body, err := i.env.HTTPRequest(url, httpTimeout)
body, err := i.env.HTTPRequest(url, nil, httpTimeout)
if err != nil {
return "", err
}

View file

@ -117,7 +117,7 @@ func (ns *Nightscout) getResult() (*NightscoutData, error) {
}
}
body, err := ns.env.HTTPRequest(url, httpTimeout)
body, err := ns.env.HTTPRequest(url, nil, httpTimeout)
if err != nil {
return nil, err
}

View file

@ -78,7 +78,7 @@ func (d *Owm) getResult() (*owmDataResponse, error) {
httpTimeout := d.props.GetInt(properties.HTTPTimeout, properties.DefaultHTTPTimeout)
d.URL = fmt.Sprintf("http://api.openweathermap.org/data/2.5/weather?q=%s&units=%s&appid=%s", location, units, apikey)
body, err := d.env.HTTPRequest(d.URL, httpTimeout)
body, err := d.env.HTTPRequest(d.URL, nil, httpTimeout)
if err != nil {
return new(owmDataResponse), err
}

View file

@ -20,7 +20,7 @@ type stravaAPI struct {
func (s *stravaAPI) GetActivities() ([]*StravaData, error) {
url := "https://www.strava.com/api/v3/athlete/activities?page=1&per_page=1"
return http.OauthResult[[]*StravaData](&s.OAuth, url)
return http.OauthResult[[]*StravaData](&s.OAuth, url, nil)
}
// segment struct, makes templating easier

View file

@ -49,7 +49,7 @@ func (w *Wakatime) setAPIData() error {
httpTimeout := w.props.GetInt(properties.HTTPTimeout, properties.DefaultHTTPTimeout)
body, err := w.env.HTTPRequest(url, httpTimeout)
body, err := w.env.HTTPRequest(url, nil, httpTimeout)
if err != nil {
return err
}

219
src/segments/withings.go Normal file
View file

@ -0,0 +1,219 @@
package segments
import (
"errors"
"fmt"
"math"
"oh-my-posh/environment"
"oh-my-posh/http"
"oh-my-posh/properties"
"strconv"
"strings"
"sync"
"time"
http2 "net/http"
"net/url"
)
// WithingsData struct contains the API data
type WithingsData struct {
Status int `json:"status"`
Body *Body `json:"body"`
}
type Body struct {
MeasureGroups []*MeasureGroup `json:"measuregrps"`
Activities []*Activity `json:"activities"`
Series []*Series `json:"series"`
}
type MeasureGroup struct {
Measures []*Measure `json:"measures"`
Comment interface{} `json:"comment"`
}
type Measure struct {
Value int `json:"value"`
Type int `json:"type"`
Unit int `json:"unit"`
}
type Series struct {
Startdate int64 `json:"startdate"`
Enddate int64 `json:"enddate"`
}
type Activity struct {
Date string `json:"date"`
Timezone string `json:"timezone"`
Deviceid string `json:"deviceid"`
HashDeviceid string `json:"hash_deviceid"`
Brand int `json:"brand"`
IsTracker bool `json:"is_tracker"`
Steps int `json:"steps"`
Distance int `json:"distance"`
Elevation int `json:"elevation"`
Soft int `json:"soft"`
Moderate int `json:"moderate"`
Intense int `json:"intense"`
Active int `json:"active"`
Calories int `json:"calories"`
Totalcalories int `json:"totalcalories"`
HrAverage int `json:"hr_average"`
HrMin int `json:"hr_min"`
HrMax int `json:"hr_max"`
HrZone0 int `json:"hr_zone_0"`
HrZone1 int `json:"hr_zone_1"`
HrZone2 int `json:"hr_zone_2"`
HrZone3 int `json:"hr_zone_3"`
}
// WithingsAPI is a wrapper around http.Oauth
type WithingsAPI interface {
GetMeasures(meastypes string) (*WithingsData, error)
GetActivities(activities string) (*WithingsData, error)
GetSleep() (*WithingsData, error)
}
type withingsAPI struct {
*http.OAuth
}
func (w *withingsAPI) GetMeasures(meastypes string) (*WithingsData, error) {
twoWeeksAgo := strconv.FormatInt(time.Now().AddDate(0, 0, -14).Unix(), 10)
formData := url.Values{
"meastypes": {meastypes},
"action": {"getmeas"},
"lastupdate": {twoWeeksAgo},
"category": {"1"},
}
return w.getWithingsData("https://wbsapi.withings.net/measure", formData)
}
func (w *withingsAPI) GetActivities(activities string) (*WithingsData, error) {
yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02")
today := time.Now().Format("2006-01-02")
formData := url.Values{
"data_fields": {activities},
"action": {"getactivity"},
"startdateymd": {yesterday},
"enddateymd": {today},
"category": {"1"},
}
return w.getWithingsData("https://wbsapi.withings.net/v2/measure", formData)
}
func (w *withingsAPI) GetSleep() (*WithingsData, error) {
now := time.Now()
formData := url.Values{
"action": {"get"},
"startdate": {strconv.FormatInt(now.AddDate(0, -1, 0).Unix(), 10)},
"enddate": {strconv.FormatInt(now.Unix(), 10)},
}
return w.getWithingsData("https://wbsapi.withings.net/v2/sleep", formData)
}
func (w *withingsAPI) getWithingsData(endpoint string, formData url.Values) (*WithingsData, error) {
modifiers := func(request *http2.Request) {
request.Method = http2.MethodPost
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
}
body := strings.NewReader(formData.Encode())
data, err := http.OauthResult[*WithingsData](w.OAuth, endpoint, body, modifiers)
if data != nil && data.Status != 0 {
return nil, errors.New("Withings API error: " + strconv.Itoa(data.Status))
}
return data, err
}
type Withings struct {
props properties.Properties
Weight float64
SleepHours string
Steps int
api WithingsAPI
}
const (
WithingsAccessTokenKey = "withings_access_token"
WithingsRefreshTokenKey = "withings_refresh_token"
)
func (w *Withings) Template() string {
return "{{ if gt .Weight 0.0 }} {{ round .Weight 2 }}kg {{ end }}"
}
func (w *Withings) Enabled() bool {
wg := sync.WaitGroup{}
wg.Add(3)
functions := []func() bool{
w.getMeasures,
w.getActivities,
w.getSleep,
}
var enabled bool
for _, function := range functions {
go func(f func() bool) {
defer wg.Done()
success := f()
if success {
enabled = true
}
}(function)
}
wg.Wait()
return enabled
}
func (w *Withings) getMeasures() bool {
data, err := w.api.GetMeasures("1")
if err != nil {
return false
}
// no data
if len(data.Body.MeasureGroups) == 0 || len(data.Body.MeasureGroups[0].Measures) == 0 {
return false
}
measure := data.Body.MeasureGroups[0].Measures[0]
weight := measure.Value
w.Weight = float64(weight) / math.Pow(10, math.Abs(float64(measure.Unit)))
return true
}
func (w *Withings) getActivities() bool {
data, err := w.api.GetActivities("steps")
if err != nil || len(data.Body.Activities) == 0 {
return false
}
w.Steps = data.Body.Activities[0].Steps
return true
}
func (w *Withings) getSleep() bool {
data, err := w.api.GetSleep()
if err != nil || len(data.Body.Series) == 0 {
return false
}
sleepStart := time.Unix(data.Body.Series[0].Startdate, 0)
sleepEnd := time.Unix(data.Body.Series[0].Enddate, 0)
sleepHours := sleepEnd.Sub(sleepStart).Hours()
w.SleepHours = fmt.Sprintf("%0.1f", sleepHours)
return true
}
func (w *Withings) Init(props properties.Properties, env environment.Environment) {
w.props = props
w.api = &withingsAPI{
OAuth: &http.OAuth{
Props: props,
Env: env,
AccessTokenKey: WithingsAccessTokenKey,
RefreshTokenKey: WithingsRefreshTokenKey,
SegmentName: "withings",
},
}
}

View file

@ -0,0 +1,163 @@
package segments
import (
"errors"
"oh-my-posh/mock"
"oh-my-posh/properties"
"testing"
"github.com/stretchr/testify/assert"
mock2 "github.com/stretchr/testify/mock"
)
type mockedWithingsAPI struct {
mock2.Mock
}
func (s *mockedWithingsAPI) GetMeasures(meastypes string) (*WithingsData, error) {
args := s.Called(meastypes)
return args.Get(0).(*WithingsData), args.Error(1)
}
func (s *mockedWithingsAPI) GetActivities(activities string) (*WithingsData, error) {
args := s.Called(activities)
return args.Get(0).(*WithingsData), args.Error(1)
}
func (s *mockedWithingsAPI) GetSleep() (*WithingsData, error) {
args := s.Called()
return args.Get(0).(*WithingsData), args.Error(1)
}
func TestWithingsSegment(t *testing.T) {
cases := []struct {
Case string
ExpectedString string
ExpectedEnabled bool
Template string
MeasuresError error
ActivitiesError error
SleepError error
WithingsData *WithingsData
}{
{
Case: "Error",
MeasuresError: errors.New("error"),
ActivitiesError: errors.New("error"),
SleepError: errors.New("error"),
ExpectedEnabled: false,
},
{
Case: "Only Measures data",
WithingsData: &WithingsData{
Body: &Body{
MeasureGroups: []*MeasureGroup{
{
Measures: []*Measure{
{
Value: 7077,
Unit: -2,
},
},
},
},
},
},
ActivitiesError: errors.New("error"),
SleepError: errors.New("error"),
ExpectedEnabled: true,
ExpectedString: "70.77kg",
},
{
Case: "Measures, no data",
WithingsData: &WithingsData{
Body: &Body{},
},
ActivitiesError: errors.New("error"),
SleepError: errors.New("error"),
ExpectedEnabled: false,
},
{
Case: "Activities",
Template: "{{ .Steps }} steps",
ExpectedString: "7077 steps",
WithingsData: &WithingsData{
Body: &Body{
Activities: []*Activity{
{
Steps: 7077,
},
},
},
},
MeasuresError: errors.New("error"),
SleepError: errors.New("error"),
ExpectedEnabled: true,
},
{
Case: "Sleep",
Template: "{{ .SleepHours }}hr",
ExpectedString: "11.8hr",
WithingsData: &WithingsData{
Body: &Body{
Series: []*Series{
{
Startdate: 1594159200,
Enddate: 1594201500,
},
},
},
},
MeasuresError: errors.New("error"),
ActivitiesError: errors.New("error"),
ExpectedEnabled: true,
},
{
Case: "Sleep and Activity",
Template: "{{ .Steps }} steps with {{ .SleepHours }}hr of sleep",
ExpectedString: "976 steps with 11.8hr of sleep",
WithingsData: &WithingsData{
Body: &Body{
Series: []*Series{
{
Startdate: 1594159200,
Enddate: 1594201500,
},
},
Activities: []*Activity{
{
Steps: 976,
},
},
},
},
MeasuresError: errors.New("error"),
ExpectedEnabled: true,
},
}
for _, tc := range cases {
api := &mockedWithingsAPI{}
api.On("GetMeasures", "1").Return(tc.WithingsData, tc.MeasuresError)
api.On("GetActivities", "steps").Return(tc.WithingsData, tc.ActivitiesError)
api.On("GetSleep").Return(tc.WithingsData, tc.SleepError)
withings := &Withings{
api: api,
props: &properties.Map{},
}
enabled := withings.Enabled()
assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case)
if !enabled {
continue
}
if tc.Template == "" {
tc.Template = withings.Template()
}
var got = renderTemplate(&mock.MockedEnvironment{}, tc.Template, withings)
assert.Equal(t, tc.ExpectedString, got, tc.Case)
}
}

View file

@ -68,7 +68,7 @@ func (y *Ytm) setStatus() error {
// https://github.com/ytmdesktop/ytmdesktop/wiki/Remote-Control-API
url := y.props.GetString(APIURL, "http://127.0.0.1:9863")
httpTimeout := y.props.GetInt(APIURL, properties.DefaultHTTPTimeout)
body, err := y.env.HTTPRequest(url+"/query", httpTimeout)
body, err := y.env.HTTPRequest(url+"/query", nil, httpTimeout)
if err != nil {
return err
}

View file

@ -54,6 +54,30 @@
"description": "Timeout value to use for http request",
"default": 20
},
"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
},
"expires_in": {
"type": "integer",
"title": "Expires in",
"description": "Access token expiration time in seconds",
"default": 0
},
"access_token": {
"type": "string",
"title": "Access token",
"description": "The initial access token",
"default": ""
},
"refresh_token": {
"type": "string",
"title": "Refresh token",
"description": "The initial refresh token",
"default": ""
},
"display_mode": {
"type": "string",
"title": "Display Mode",
@ -251,6 +275,7 @@
"wakatime",
"wifi",
"winreg",
"withings",
"ytm"
]
},
@ -1828,10 +1853,7 @@
"$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
"$ref": "#/definitions/cache_timeout"
}
}
}
@ -1976,10 +1998,16 @@
"$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
"$ref": "#/definitions/cache_timeout"
},
"access_token": {
"$ref": "#/definitions/access_token"
},
"refresh_token": {
"$ref": "#/definitions/refresh_token"
},
"expires_in": {
"$ref": "#/definitions/expires_in"
}
}
}
@ -2097,10 +2125,7 @@
"$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
"$ref": "#/definitions/cache_timeout"
}
}
}
@ -2151,6 +2176,40 @@
}
}
},
{
"if": {
"properties": {
"type": {
"const": "withings"
}
}
},
"then": {
"title": "Display activity data from Withings",
"description": "https://ohmyposh.dev/docs/segments/withings",
"properties": {
"properties": {
"properties": {
"http_timeout": {
"$ref": "#/definitions/http_timeout"
},
"cache_timeout": {
"$ref": "#/definitions/cache_timeout"
},
"access_token": {
"$ref": "#/definitions/access_token"
},
"refresh_token": {
"$ref": "#/definitions/refresh_token"
},
"expires_in": {
"$ref": "#/definitions/expires_in"
}
}
}
}
}
},
{
"if": {
"properties": {
@ -2248,10 +2307,7 @@
"$ref": "#/definitions/http_timeout"
},
"cache_timeout": {
"type": "integer",
"title": "cache timeout in minutes",
"description": "The number of minutes the response is cached. A value of 0 disables the cache.",
"default": 5
"$ref": "#/definitions/cache_timeout"
},
"doubleup_icon": {
"type": "string",
@ -2360,10 +2416,7 @@
"$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
"$ref": "#/definitions/cache_timeout"
}
}
}

View file

@ -47,7 +47,6 @@ can get started even without having to understand the theming. So, let's no long
installation guide to get started right away!
[omp]: https://github.com/JanDeDobbeleer/oh-my-posh2
[omz]: https://github.com/ohmyzsh/ohmyzsh
[block]: /docs/configuration/block
[segment]: /docs/configuration/segment
[themes]: https://github.com/JanDeDobbeleer/oh-my-posh/tree/main/themes

View file

@ -82,4 +82,3 @@ Only winget and scoop add the environment variable `POSH_THEMES_PATH` automatica
[windows]: /docs/installation/windows
[macos]: /docs/installation/macos
[linux]: /docs/installation/linux
[set-prompt]: /docs/installation/prompt

View file

@ -58,11 +58,11 @@ if that color is visible against any of your backgrounds.
## 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`
- access_token: `string` - token from Strava login, see login link in section above.
- refresh_token: `string` - token from Strava login, see login link in section above.
- expires_in: `int` - the default timeout of the token from the Strava login
- 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
- CacheTimeout: `int` in minutes - How long do you want your numbers cached? - defaults to 5 min
- CacheTimeout: `int` in minutes - How long do you want your Strava data cached? - defaults to 5 min
- RideIcon - defaults to `\uf5a2`
- RunIcon - defaults to `\ufc0c`
- SkiingIcon - defaults to `\ue213`

View file

@ -18,9 +18,11 @@ This will give you an access and a refresh token. Paste the tokens into your Wit
Click the following link to connect with Withings:
<a href="https://account.withings.com/oauth2_user/authorize2?client_id=93675962e88ddfe53f83c0c900558f72174e0ac70ccfb57e48053530c7e6e494&response_type=code&redirect_uri=https://ohmyposh.dev/api/auth&scope=user.activity,user.metrics&state=withings">
<WithingsConnect className="withings"/>
</a>
<div className="withings">
<a href="https://account.withings.com/oauth2_user/authorize2?client_id=93675962e88ddfe53f83c0c900558f72174e0ac70ccfb57e48053530c7e6e494&response_type=code&redirect_uri=https://ohmyposh.dev/api/auth&scope=user.activity,user.metrics&state=withings">
<WithingsConnect className="withings"/>
</a>
</div>
## Sample Configuration
@ -31,7 +33,7 @@ Click the following link to connect with Withings:
"powerline_symbol": "\uE0B0",
"foreground": "#ffffff",
"background": "#000000",
"template": "{{.Name}} {{.Ago}} {{.Icon}}",
"template": "{{ if gt .Weight 0.0 }} {{ round .Weight 2 }}kg {{ end }}",
"properties": {
"access_token":"11111111111111111",
"refresh_token":"1111111111111111",
@ -42,18 +44,18 @@ Click the following link to connect with Withings:
## Properties
- access_token: `string` - token from Withings login, see login link in section above. It has the following format: `1111111111111111111111111`
- refresh_token: `string` - token from Withings login, see login link in section above. It has the following format: `1111111111111111111111111`
- access_token: `string` - token from Withings login, see login link in section above.
- refresh_token: `string` - token from Withings login, see login link in section above.
- expires_in: `int` - the default timeout of the token from the Withings login
- http_timeout: `int` - how long do you want to wait before you want to see your prompt more than your Withings data? - defaults to 500ms
- CacheTimeout: `int` in minutes - How long do you want your numbers cached? - defaults to 5 min
- CacheTimeout: `int` in minutes - How long do you want your Withings data cached? - defaults to 5 min
## Template ([info][templates])
:::note default template
``` template
{{ if .Error }}{{ .Error }}{{ else }}{{ .Ago }}{{ end }}
{{ if gt .Weight 0.0 }} {{ round .Weight 2 }}kg {{ end }}
```
:::
@ -62,7 +64,9 @@ Click the following link to connect with Withings:
The properties below are available for use in your template
- `.ID`: `time` - The id of the entry
- `.Weight`: `float` - your last measured weight
- `.SleepHours`: `string` - your last measured sleep SleepHours
- `.Steps`: `int` - your last measured steps
Now, go out and be active!

View file

@ -109,6 +109,7 @@ module.exports = {
"segments/ui5tooling",
"segments/wakatime",
"segments/wifi",
"segments/withings",
"segments/winreg",
"segments/ytm",
],

View file

@ -41,7 +41,7 @@
}
.header-github-link:before {
content: '';
content: "";
width: 24px;
height: 24px;
display: flex;
@ -49,7 +49,7 @@
no-repeat;
}
html[data-theme='dark'] .header-github-link:before {
html[data-theme="dark"] .header-github-link:before {
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")
no-repeat;
}
@ -59,7 +59,7 @@ html[data-theme='dark'] .header-github-link:before {
}
.header-twitter-link:before {
content: '';
content: "";
width: 25px;
height: 25px;
display: flex;
@ -68,7 +68,7 @@ html[data-theme='dark'] .header-github-link:before {
no-repeat;
}
html[data-theme='dark'] .header-twitter-link:before {
html[data-theme="dark"] .header-twitter-link:before {
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 50 50' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M25,2C12.317,2,2,12.317,2,25s10.317,23,23,23s23-10.317,23-23S37.683,2,25,2z M36.237,20.524 c0.01,0.236,0.016,0.476,0.016,0.717C36.253,28.559,30.68,37,20.491,37c-3.128,0-6.041-0.917-8.491-2.489 c0.433,0.052,0.872,0.077,1.321,0.077c2.596,0,4.985-0.884,6.879-2.37c-2.424-0.044-4.468-1.649-5.175-3.847 c0.339,0.065,0.686,0.1,1.044,0.1c0.505,0,0.995-0.067,1.458-0.195c-2.532-0.511-4.441-2.747-4.441-5.432c0-0.024,0-0.047,0-0.07 c0.747,0.415,1.6,0.665,2.509,0.694c-1.488-0.995-2.464-2.689-2.464-4.611c0-1.015,0.272-1.966,0.749-2.786 c2.733,3.351,6.815,5.556,11.418,5.788c-0.095-0.406-0.145-0.828-0.145-1.262c0-3.059,2.48-5.539,5.54-5.539 c1.593,0,3.032,0.672,4.042,1.749c1.261-0.248,2.448-0.709,3.518-1.343c-0.413,1.292-1.292,2.378-2.437,3.064 c1.122-0.136,2.188-0.432,3.183-0.873C38.257,18.766,37.318,19.743,36.237,20.524z'/%3E%3C/svg%3E")
no-repeat;
}
@ -78,7 +78,7 @@ html[data-theme='dark'] .header-twitter-link:before {
}
.header-gk-link:before {
content: '';
content: "";
width: 26px;
height: 26px;
display: flex;
@ -86,7 +86,7 @@ html[data-theme='dark'] .header-twitter-link:before {
no-repeat;
}
html[data-theme='dark'] .header-gk-link:before {
html[data-theme="dark"] .header-gk-link:before {
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 182 182' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='m 176.70092,47.979262 a 3.6,3.6 0 0 0 -7,1.3 2.5,2.5 0 0 0 0.2,1.2 82.6,82.6 0 0 1 5.3,29.5 83.8,83.8 0 0 1 -72.7,82.999998 v -39.1 a 60.2,60.2 0 0 0 7.3,-1.9 v 32.8 a 77,77 0 0 0 19,-142.199998 3.7,3.7 0 0 0 -5.1,1.6 4.5,4.5 0 0 0 -0.3,1.6 3.6,3.6 0 0 0 1.8,3.2 69.7,69.7 0 0 1 -8.1,125.899998 v -29.1 a 11.2,11.2 0 0 0 7.8,-10.6 11,11 0 0 0 -5.5,-9.699998 c 2.6,-25.1 14.1,-18.5 14.1,-26.5 v -4.6 c 0,-12.1 -27.8,-51.1 -40.799999,-52.1 h -2.4 c -13,1 -40.8,40 -40.8,52.1 v 4.6 c 0,8 11.5,1.4 14.1,26.5 a 11.2,11.2 0 0 0 2.4,20.299998 v 29.1 a 69.7,69.7 0 0 1 -8.2,-125.899998 3.6,3.6 0 0 0 1.5,-4.8 3.5,3.5 0 0 0 -3.3,-2 3.3,3.3 0 0 0 -1.7,0.4 77,77 0 0 0 19,142.199998 v -32.8 a 60.2,60.2 0 0 0 7.3,1.9 v 39.1 A 83.8,83.8 0 0 1 7.8009215,79.979262 a 84.8,84.8 0 0 1 5.2999995,-29.5 3.6,3.6 0 0 0 -1.9,-4.6 4,4 0 0 0 -1.4999998,-0.3 3.7,3.7 0 0 0 -3.3999997,2.4 91,91 0 0 0 81.4999995,122.899998 v -46 h 7.4 v 46 a 90.9,90.9 0 0 0 87.299999,-90.899998 89.1,89.1 0 0 0 -5.8,-32 z m -68.4,51.9 a 7.5,7.5 0 1 1 10.6,10.599998 7.5,7.5 0 1 1 -10.6,-10.599998 z M 74.700921,110.47926 a 7.4,7.4 0 0 1 -10.5,0 7.5,7.5 0 0 1 0,-10.599998 7.4,7.4 0 0 1 10.5,0 7.5,7.5 0 0 1 0,10.599998 z'/%3E%3C/svg%3E")
no-repeat;
}
@ -103,19 +103,19 @@ iframe.youtube {
@media screen and (max-width: 350px) {
iframe.youtube {
height: 200px;
height: 200px;
}
}
@media screen and (min-width: 350px) {
iframe.youtube {
height: 300px;
height: 300px;
}
}
@media screen and (min-width: 600px) {
iframe.youtube {
height: 400px;
height: 400px;
}
}
@ -129,10 +129,44 @@ iframe.youtube {
width: 150px;
}
[data-theme='light'] .withings path {
[data-theme="light"] .withings path {
fill: white;
}
[data-theme="dark"] .withings path {
fill: black;
}
[data-theme='dark'] .withings path {
fill: white;
[data-theme="dark"] div.withings {
background: white;
padding: 17px 10px 10px 10px;
display: inline;
border-radius: 10px;
}
[data-theme="light"] div.withings {
background: black;
padding: 17px 10px 10px 10px;
display: inline;
border-radius: 10px;
}
h1.hero__title {
color: white;
}
p.hero__subtitle {
color: white;
}
a.getStarted_src-pages-styles-module {
color: white;
}
[data-theme="light"] .themedImage_node_modules-\@docusaurus-theme-classic-lib-theme-ThemedImage-styles-module {
filter: invert(0%) sepia(8%) saturate(2885%) hue-rotate(67deg) brightness(84%) contrast(87%);
}
[data-theme="dark"] .themedImage_node_modules-\@docusaurus-theme-classic-lib-theme-ThemedImage-styles-module {
filter: invert(100%) sepia(100%) saturate(0%) hue-rotate(226deg) brightness(102%) contrast(101%);
}

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30.851 30.851" style="enable-background:new 0 0 30.851 30.851;" xml:space="preserve">
viewBox="0 0 30.851 30.851" xml:space="preserve">
<g>
<g id="c43_terminal">
<path d="M28.645,2.203H2.204C0.987,2.203,0,3.188,0,4.405v22.038c0,1.216,0.987,2.204,2.204,2.204h26.441
@ -9,7 +9,7 @@
c0,0.607-0.494,1.098-1.102,1.098c-0.611,0-1.104-0.49-1.104-1.098C6.059,4.349,6.551,3.857,7.162,3.857z M3.855,3.857
c0.608,0,1.102,0.491,1.102,1.103c0,0.607-0.493,1.098-1.102,1.098S2.752,5.568,2.752,4.96C2.753,4.349,3.246,3.857,3.855,3.857z
M28.645,26.444H2.204V7.735h26.441V26.444z"/>
<polygon points="12.351,12.264 5.275,9.028 5.275,10.589 10.746,12.929 10.746,12.958 5.275,15.294 5.275,16.854 12.351,13.618
<polygon points="12.351,12.264 5.275,9.028 5.275,10.589 10.746,12.929 10.746,12.958 5.275,15.294 5.275,16.854 12.351,13.618
"/>
<rect x="15.682" y="12.199" width="3.855" height="1.486"/>
</g>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB