fix(data): render a segment the same with or without its writer

Recorded data replayed without a segment writer diverged from the same
data replayed with one, in four ways that only surfaced once both paths
rendered the bundled themes side by side:

- Colour templates resolved against the writer rather than the data, so
  every one of them fell back to the plain colour where no writer
  exists - 49 of 124 themes.
- A value that is not a struct lost its methods. battery.State is an
  int, and `.State.String` is what every battery theme switches on.
  Method results now travel in a tree of their own, which keeps the
  data itself writer-shaped: an entry whose "State" were an object no
  longer unmarshals into the writer.
- A field renamed by its json tag was recorded under its Go name alone,
  so encoding/json never matched it on the way back and terraform's
  version restored as nil. Both names are recorded now.
- `date` fell through to the wall clock for a timestamp arriving as a
  string, which is how a recorded time.Time always arrives.

The wasm build carries no zoneinfo, so its idea of local time was
whatever offset the host happened to be on and a gallery built in July
printed an hour later than the same gallery built in January. It
renders in UTC now, with recorded timestamps written to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Entire-Checkpoint: 707298e5cc26
This commit is contained in:
Jan De Dobbeleer
2026-07-30 19:44:41 +02:00
committed by Jan De Dobbeleer
co-authored by Claude Opus 5
parent e4ac930b0c
commit 47f2228931
10 changed files with 633 additions and 85 deletions
+2 -2
View File
@@ -156,12 +156,12 @@ func buildDataDocument(cfg *config.Config) ([]byte, error) {
continue
}
raw, err := recordSegmentData(writer)
raw, methods, err := recordSegmentData(writer)
if err != nil {
return nil, fmt.Errorf("failed to marshal segment %s: %w", segment.DataKey(), err)
}
recorded := config.RecordedSegment{Data: raw, Enabled: segment.Enabled}
recorded := config.RecordedSegment{Data: raw, Methods: methods, Enabled: segment.Enabled}
recordedRaw, err := json.Marshal(recorded)
if err != nil {
+121 -22
View File
@@ -4,6 +4,8 @@ import (
"encoding/json"
"reflect"
"strings"
"github.com/jandedobbeleer/oh-my-posh/src/config"
)
// interfaceMethods are the SegmentWriter methods every writer has to satisfy. They describe how a
@@ -36,18 +38,48 @@ const maxMethodDepth = 3
//
// Fields win over methods on a name collision: the field is what json.Marshal chose to call the
// value, and a method shadowing it would change what replay sees.
func recordSegmentData(writer any) ([]byte, error) {
data, err := asDataMap(reflect.ValueOf(writer), 0)
//
// The methods a struct field carries in its own right are the exception, and they are why this
// returns two trees rather than one. battery.State is an int, and every battery theme switches on
// `.State.String`; a map holding the number it marshals to has nothing under that name. Its method
// results have to go somewhere, and they cannot go where the number is - a file whose "State" is
// an object no longer unmarshals into the writer, which is the other half of what a data file is
// for. So the number stays in data, the methods go in a parallel methods tree shaped like it, and
// whoever renders without a writer merges the second over the first (config.MergeRecordedMethods).
func recordSegmentData(writer any) (data, methods []byte, err error) {
recorded, err := asDataMap(reflect.ValueOf(writer), 0)
if err != nil {
return nil, err
return nil, nil, err
}
return json.Marshal(data)
data, err = json.Marshal(recorded.data)
if err != nil {
return nil, nil, err
}
if recorded.methods == nil {
return data, nil, nil
}
methods, err = json.Marshal(recorded.methods)
if err != nil {
return nil, nil, err
}
return data, methods, nil
}
func asDataMap(value reflect.Value, depth int) (any, error) {
// recorded is one value's two trees: what it marshals to, and the method results that have no
// place in that marshalling. methods is nil for everything that needs no overlay, which is most of
// what a writer holds.
type recorded struct {
data any
methods any
}
func asDataMap(value reflect.Value, depth int) (recorded, error) {
if !value.IsValid() {
return nil, nil
return recorded{}, nil
}
structValue := reflect.Indirect(value)
@@ -58,20 +90,32 @@ func asDataMap(value reflect.Value, depth int) (any, error) {
if structValue.Kind() != reflect.Struct || marshalsItself(value) {
raw, err := json.Marshal(value.Interface())
if err != nil {
return nil, err
return recorded{}, err
}
return json.RawMessage(raw), nil
// A type with its own MarshalJSON gets no overlay: that representation is the one templates
// consume, and time.Time's methods would bury the timestamp `date` parses.
var methods any
if overlay := methodResults(value, depth); len(overlay) != 0 {
methods = overlay
}
return recorded{data: json.RawMessage(raw), methods: methods}, nil
}
fields := make(map[string]any)
overlay := make(map[string]any)
if depth < maxMethodDepth {
addStructFields(structValue, fields, depth)
addMethodResults(value, fields, depth)
addStructFields(structValue, fields, overlay, depth)
addMethodResults(value, fields, overlay, depth)
}
return fields, nil
if len(overlay) == 0 {
return recorded{data: fields}, nil
}
return recorded{data: fields, methods: overlay}, nil
}
// marshalsItself reports whether a type defines its own JSON representation, in which case taking
@@ -87,16 +131,20 @@ func marshalsItself(value reflect.Value) bool {
return ok
}
// addStructFields records a struct's exported fields keyed by their Go names rather than their
// json tags.
// addStructFields records a struct's exported fields under their Go names, and again under a json
// tag that renames them.
//
// This is what a template reads. `{{ .Name }}` resolves against a field called Name; the az
// The Go name is what a template reads. `{{ .Name }}` resolves against a field called Name; the az
// segment's json tag for it is "name", and wakatime's CumulativeTotal is tagged
// "cumulative_total". A struct bridges the two because encoding/json knows the tag - a map has no
// tags, so a recorded file keyed by tags leaves every such template unable to find anything. Since
// the whole point of recording is to be replayed where no struct exists, the keys have to be the
// names the templates use.
func addStructFields(structValue reflect.Value, fields map[string]any, depth int) {
// tags, so a recorded file keyed by tags leaves every such template unable to find anything.
//
// The tag is what a writer reads. encoding/json matches a tagged field by its tag and by nothing
// else, so a file keyed only by Go names restores every plainly-named field and silently skips the
// renamed ones - terraform's Version, tagged "terraform_version", came back nil and took the
// version out of the prompt. Both keys carry the same value, so whichever side reads the file
// finds what it is looking for under the name it knows.
func addStructFields(structValue reflect.Value, fields, overlay map[string]any, depth int) {
for i := range structValue.NumField() {
field := structValue.Type().Field(i)
@@ -115,7 +163,7 @@ func addStructFields(structValue reflect.Value, fields map[string]any, depth int
// concerned, so they are flattened rather than nested under the type name.
embedded := reflect.Indirect(structValue.Field(i))
if embedded.Kind() == reflect.Struct {
addStructFields(embedded, fields, depth)
addStructFields(embedded, fields, overlay, depth)
continue
}
}
@@ -125,11 +173,58 @@ func addStructFields(structValue reflect.Value, fields map[string]any, depth int
continue
}
fields[field.Name] = nested
fields[field.Name] = nested.data
if name := taggedName(&field); name != "" {
fields[name] = nested.data
}
// The overlay is read only where there is no writer, so it needs the template's name and
// not the writer's.
if nested.methods != nil {
overlay[field.Name] = nested.methods
}
}
}
func addMethodResults(value reflect.Value, fields map[string]any, depth int) {
// taggedName is the name encoding/json will look for when it differs from the field's own, and ""
// when the two agree or the field carries no tag at all.
func taggedName(field *reflect.StructField) string {
tag, tagged := field.Tag.Lookup("json")
if !tagged {
return ""
}
name, _, _ := strings.Cut(tag, ",")
if name == "" || name == field.Name {
return ""
}
return name
}
// methodResults records a value's methods on their own, for the non-struct case that has no
// fields to merge them with. What comes back is the whole of what replaces the value, so each
// method's own overlay is folded in here rather than left for the merge at restore to find - the
// merge stops descending the moment one side is not a map, and a scalar's data is never one.
func methodResults(value reflect.Value, depth int) map[string]any {
if depth >= maxMethodDepth || marshalsItself(value) {
return nil
}
fields := make(map[string]any)
overlay := make(map[string]any)
addMethodResults(value, fields, overlay, depth)
for name, methods := range overlay {
fields[name] = config.MergeRecordedMethods(fields[name], methods)
}
return fields
}
func addMethodResults(value reflect.Value, fields, overlay map[string]any, depth int) {
for i := range value.NumMethod() {
name := value.Type().Method(i).Name
@@ -158,7 +253,11 @@ func addMethodResults(value reflect.Value, fields map[string]any, depth int) {
continue
}
fields[name] = nested
fields[name] = nested.data
if nested.methods != nil {
overlay[name] = nested.methods
}
}
}
+30 -1
View File
@@ -76,10 +76,39 @@ func ThemeFiles(dir string) ([]string, error) {
// version marker. A hand-written file skips this wrapper and stores a segment's
// raw writer fields directly, as it always has.
type RecordedSegment struct {
Data json.RawMessage `json:"data"`
Data json.RawMessage `json:"data"`
// Methods holds the method results that have no room in Data, shaped like it. A recorded
// value's methods normally sit alongside its fields, but a value that is not a struct has no
// fields to sit alongside: battery.State is an int, and an entry whose "State" were the object
// its String() belongs in would no longer unmarshal into the writer. Kept apart, Data stays
// writer-shaped and a render with no writer merges this over it - see MergeRecordedMethods.
Methods json.RawMessage `json:"methods,omitempty"`
Enabled bool `json:"enabled"`
}
// MergeRecordedMethods overlays a recorded segment's Methods tree on its Data tree. Two maps are
// merged key by key; anything else is replaced outright, which is what puts a scalar's method
// results in the place of the scalar.
func MergeRecordedMethods(data, methods any) any {
dataMap, isDataMap := data.(map[string]any)
methodsMap, isMethodsMap := methods.(map[string]any)
if !isDataMap || !isMethodsMap {
return methods
}
for key, value := range methodsMap {
if existing, ok := dataMap[key]; ok {
dataMap[key] = MergeRecordedMethods(existing, value)
continue
}
dataMap[key] = value
}
return dataMap
}
// Data holds template data supplied via the --data flag, used to render a
// prompt deterministically without querying the real runtime.
type Data struct {
+28 -9
View File
@@ -417,7 +417,7 @@ func (segment *Segment) ResolveForeground() color.Ansi {
}
if len(segment.ForegroundTemplates) != 0 {
match := segment.ForegroundTemplates.FirstMatch(segment.writer, segment.Foreground.String())
match := segment.ForegroundTemplates.FirstMatch(segment.templateContext(), segment.Foreground.String())
segment.Foreground = color.Ansi(match)
}
@@ -433,7 +433,7 @@ func (segment *Segment) ResolveBackground() color.Ansi {
}
if len(segment.BackgroundTemplates) != 0 {
match := segment.BackgroundTemplates.FirstMatch(segment.writer, segment.Background.String())
match := segment.BackgroundTemplates.FirstMatch(segment.templateContext(), segment.Background.String())
segment.Background = color.Ansi(match)
}
@@ -611,7 +611,7 @@ func (segment *Segment) restoreData() bool {
return true
}
if err := segment.restoreInto(recorded.Data); err != nil {
if err := segment.restoreInto(recorded.Data, recorded.Methods); err != nil {
log.Error(err)
return false
}
@@ -631,7 +631,11 @@ func (segment *Segment) restoreData() bool {
// degraded form - a template resolves a name against a map key exactly as it resolves it against
// a struct field, so both carry the same recorded values to the same templates. What a map cannot
// carry is a method result, which is why the recorder writes those out as data too.
func (segment *Segment) restoreInto(raw json.RawMessage) error {
//
// methods is the overlay for the values whose method results could not be written as data without
// changing what the writer sees (RecordedSegment.Methods explains which). It is read here and
// nowhere else: a writer brings its own methods, so only the map ever needs it.
func (segment *Segment) restoreInto(raw, methods json.RawMessage) error {
if segment.writer != nil {
return json.Unmarshal(raw, &segment.writer)
}
@@ -641,6 +645,15 @@ func (segment *Segment) restoreInto(raw json.RawMessage) error {
return err
}
if len(methods) != 0 {
overlay := make(map[string]any)
if err := json.Unmarshal(methods, &overlay); err != nil {
return err
}
MergeRecordedMethods(data, overlay)
}
segment.data = normalizeNumbers(data).(map[string]any)
return nil
@@ -688,12 +701,12 @@ func normalizeNumbers(value any) any {
}
// decodeRecordedSegment reports whether raw is exactly a RecordedSegment
// envelope: a JSON object with only "enabled" and "data" keys, nothing else.
// Anything short of that - a flat hand-written entry, or malformed JSON - is
// left for the caller to treat as unmarked data.
// envelope: a JSON object holding "enabled" and "data", and nothing beyond an
// optional "methods". Anything short of that - a flat hand-written entry, or
// malformed JSON - is left for the caller to treat as unmarked data.
func decodeRecordedSegment(raw json.RawMessage) (RecordedSegment, bool) {
var fields map[string]json.RawMessage
if err := json.Unmarshal(raw, &fields); err != nil || len(fields) != 2 {
if err := json.Unmarshal(raw, &fields); err != nil {
return RecordedSegment{}, false
}
@@ -703,12 +716,18 @@ func decodeRecordedSegment(raw json.RawMessage) (RecordedSegment, bool) {
return RecordedSegment{}, false
}
methodsRaw, hasMethods := fields["methods"]
if len(fields) != 2 && (len(fields) != 3 || !hasMethods) {
return RecordedSegment{}, false
}
var enabled bool
if err := json.Unmarshal(enabledRaw, &enabled); err != nil {
return RecordedSegment{}, false
}
return RecordedSegment{Data: dataRaw, Enabled: enabled}, true
return RecordedSegment{Data: dataRaw, Methods: methodsRaw, Enabled: enabled}, true
}
func (segment *Segment) setCache() {
+9
View File
@@ -213,6 +213,15 @@ func reachableNames(value reflect.Value) (map[string]bool, map[string]reflect.Va
names[field.Name] = true
fields[field.Name] = sv.Field(i)
// The recorder writes a renamed field under both names, so a tag is just as legitimate
// a key as the Go name - it is the one the writer itself reads back.
if tag, tagged := field.Tag.Lookup("json"); tagged {
if name, _, _ := strings.Cut(tag, ","); name != "" && name != "-" {
names[name] = true
fields[name] = sv.Field(i)
}
}
}
}
+9
View File
@@ -44,6 +44,15 @@ func (n Number) String() string {
type Index string
// String is what a template compares an index against. `{{ eq "moderate" .Index }}` reads the
// value itself and works the same, but only where a segment renders from its writer: a segment
// rendered from recorded data has no Go type to carry Icon, so the recorder stores an index as
// its method results, and a template reaching past them for the raw string finds nothing. Naming
// the string as a method keeps both readings available wherever a segment renders from.
func (i Index) String() string {
return string(i)
}
func (i Index) Icon() string {
switch i {
case "very low":
+18 -5
View File
@@ -24,11 +24,7 @@ func dateInZone(fmt string, date any, zone string) string {
case int32:
t = time.Unix(int64(v), 0)
case string:
if epoch, err := strconv.ParseInt(v, 10, 64); err == nil {
t = time.Unix(epoch, 0)
} else {
t = time.Now()
}
t = parseDateString(v)
default:
t = time.Now()
}
@@ -41,6 +37,23 @@ func dateInZone(fmt string, date any, zone string) string {
return t.In(loc).Format(fmt)
}
// parseDateString reads the two shapes a date reaches a template as text in: a Unix epoch, which
// is what sprig's own unixEpoch hands on, and an RFC 3339 timestamp, which is how a time.Time
// marshals. The second matters for a segment rendered from recorded data rather than from its
// writer: JSON has no time type, so a recorded date arrives as the string it marshalled to, and
// falling through to time.Now() would quietly replace it with the wall clock.
func parseDateString(value string) time.Time {
if epoch, err := strconv.ParseInt(value, 10, 64); err == nil {
return time.Unix(epoch, 0)
}
if parsed, err := time.Parse(time.RFC3339Nano, value); err == nil {
return parsed
}
return time.Now()
}
func ompDate(fmt string, date any) string {
return dateInZone(fmt, date, "Local")
}
+10
View File
@@ -23,6 +23,7 @@ import (
"fmt"
"strings"
"syscall/js"
"time"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/render"
@@ -31,6 +32,15 @@ import (
)
func main() {
// Every render here is meant to be reproducible: the same config and the same recorded data
// give the same SVG, whoever builds the site and whenever. A recorded date is the one thing
// that would not have been - `{{ .CurrentDate | date .Format }}` converts to the local zone,
// and this build carries no zoneinfo, so "local" is whatever fixed offset the host happened to
// be on. A gallery built in July would print an hour later than the same gallery built in
// January. Pinning the zone takes the host back out of it; recorded timestamps are written in
// UTC to match (website/segment_data.json).
time.Local = time.UTC
js.Global().Set("render", js.FuncOf(renderJS))
// A wasm_exec.js-hosted module's main must never return: the host glue
@@ -28,11 +28,11 @@ import Config from "@site/src/components/Config.js";
foreground: "#000000",
background: "#ffffff",
background_templates: [
'{{if eq "very low" .Index}}#a3e635{{end}}',
'{{if eq "low" .Index}}#bef264{{end}}',
'{{if eq "moderate" .Index}}#fbbf24{{end}}',
'{{if eq "high" .Index}}#ef4444{{end}}',
'{{if eq "very high" .Index}}#dc2626{{end}}',
'{{if eq "very low" .Index.String}}#a3e635{{end}}',
'{{if eq "low" .Index.String}}#bef264{{end}}',
'{{if eq "moderate" .Index.String}}#fbbf24{{end}}',
'{{if eq "high" .Index.String}}#ef4444{{end}}',
'{{if eq "very high" .Index.String}}#dc2626{{end}}',
],
template:
" CO₂ {{ .Index.Icon }}{{ .Actual.String }} {{ .TrendIcon }} {{ .Forecast.String }} ",
+401 -41
View File
@@ -126,7 +126,17 @@
"TenantDisplayName": "",
"TenantID": "00000000-0000-0000-0000-000000000000",
"Text": " x ",
"User": null
"User": null,
"environmentName": "AzureCloud",
"homeTenantId": "00000000-0000-0000-0000-000000000000",
"id": "00000000-0000-0000-0000-000000000000",
"isDefault": false,
"managedByTenants": null,
"name": "Contoso Production",
"state": "",
"tenantDisplayName": "",
"tenantId": "00000000-0000-0000-0000-000000000000",
"user": null
},
"enabled": true
},
@@ -138,7 +148,9 @@
"Text": " x "
},
"Text": " x ",
"Version": 1
"Version": 1,
"defaultEnvironment": "zava-prod",
"version": 1
},
"enabled": true
},
@@ -181,7 +193,17 @@
"TenantDisplayName": "",
"TenantID": "00000000-0000-0000-0000-000000000000",
"Text": " x ",
"User": null
"User": null,
"environmentName": "AzureCloud",
"homeTenantId": "00000000-0000-0000-0000-000000000000",
"id": "00000000-0000-0000-0000-000000000000",
"isDefault": false,
"managedByTenants": null,
"name": "Contoso Production",
"state": "",
"tenantDisplayName": "",
"tenantId": "00000000-0000-0000-0000-000000000000",
"user": null
},
"enabled": true
},
@@ -197,7 +219,12 @@
"State": 3,
"Text": " x "
},
"enabled": true
"enabled": true,
"methods": {
"State": {
"String": "Charging"
}
}
},
"bazel": {
"data": {
@@ -239,7 +266,8 @@
"Reading": null,
"ReadingAge": 3,
"Recipe": {
"Name": "Contoso Pale Ale"
"Name": "Contoso Pale Ale",
"name": "Contoso Pale Ale"
},
"Segment": {
"Index": 10,
@@ -250,7 +278,20 @@
"TemperatureTrend": 0,
"TemperatureTrendIcon": "→",
"Text": " x ",
"URL": ""
"URL": "",
"batchNo": 42,
"bottlingDate": 0,
"brewDate": 0,
"fermentationStartDate": 0,
"measuredAbv": 5.4,
"measuredFg": 0,
"measuredOg": 0,
"name": "Batch 42",
"recipe": {
"Name": "Contoso Pale Ale",
"name": "Contoso Pale Ale"
},
"status": "Fermenting"
},
"enabled": true
},
@@ -308,9 +349,24 @@
"Text": " x "
},
"Text": " x ",
"TrendIcon": "↘"
"TrendIcon": "↘",
"actual": 132,
"forecast": 148,
"index": "moderate"
},
"enabled": true
"enabled": true,
"methods": {
"Actual": {
"String": "132"
},
"Forecast": {
"String": "148"
},
"Index": {
"Icon": "•",
"String": "moderate"
}
}
},
"cds": {
"data": {
@@ -381,14 +437,25 @@
"RemainingPercentage": null,
"TotalInputTokens": 68000,
"TotalOutputTokens": 4200,
"UsedPercentage": 42
"UsedPercentage": 42,
"context_window_size": 200000,
"current_usage": null,
"remaining_percentage": null,
"total_input_tokens": 68000,
"total_output_tokens": 4200,
"used_percentage": 42
},
"Cost": {
"TotalAPIDurationMS": 0,
"TotalCostUSD": 1.82,
"TotalDurationMS": 734000,
"TotalLinesAdded": 118,
"TotalLinesRemoved": 26
"TotalLinesRemoved": 26,
"total_api_duration_ms": 0,
"total_cost_usd": 1.82,
"total_duration_ms": 734000,
"total_lines_added": 118,
"total_lines_removed": 26
},
"Effort": null,
"Exceeds200KTokens": false,
@@ -403,7 +470,9 @@
"FormattedTokens": "72.2K",
"Model": {
"DisplayName": "Opus 4.5",
"ID": "claude-opus-4-5"
"ID": "claude-opus-4-5",
"display_name": "Opus 4.5",
"id": "claude-opus-4-5"
},
"OutputStyle": null,
"PR": null,
@@ -432,9 +501,74 @@
"CurrentDir": "~/dev/oh-my-posh",
"GitWorktree": "",
"ProjectDir": "~/dev/oh-my-posh",
"Repo": null
"Repo": null,
"added_dirs": null,
"current_dir": "~/dev/oh-my-posh",
"git_worktree": "",
"project_dir": "~/dev/oh-my-posh",
"repo": null
},
"Worktree": null
"Worktree": null,
"agent": null,
"context_window": {
"ContextWindowSize": 200000,
"CurrentUsage": null,
"RemainingPercentage": null,
"TotalInputTokens": 68000,
"TotalOutputTokens": 4200,
"UsedPercentage": 42,
"context_window_size": 200000,
"current_usage": null,
"remaining_percentage": null,
"total_input_tokens": 68000,
"total_output_tokens": 4200,
"used_percentage": 42
},
"cost": {
"TotalAPIDurationMS": 0,
"TotalCostUSD": 1.82,
"TotalDurationMS": 734000,
"TotalLinesAdded": 118,
"TotalLinesRemoved": 26,
"total_api_duration_ms": 0,
"total_cost_usd": 1.82,
"total_duration_ms": 734000,
"total_lines_added": 118,
"total_lines_removed": 26
},
"cwd": "~/dev/oh-my-posh",
"effort": null,
"exceeds_200k_tokens": false,
"fast_mode": false,
"model": {
"DisplayName": "Opus 4.5",
"ID": "claude-opus-4-5",
"display_name": "Opus 4.5",
"id": "claude-opus-4-5"
},
"output_style": null,
"pr": null,
"prompt_id": "",
"rate_limits": null,
"session_id": "a1b2c3d4-e5f6-4789-b012-3456789abcde",
"session_name": "oh-my-posh",
"thinking": null,
"transcript_path": "",
"version": "2.1.0",
"vim": null,
"workspace": {
"AddedDirs": null,
"CurrentDir": "~/dev/oh-my-posh",
"GitWorktree": "",
"ProjectDir": "~/dev/oh-my-posh",
"Repo": null,
"added_dirs": null,
"current_dir": "~/dev/oh-my-posh",
"git_worktree": "",
"project_dir": "~/dev/oh-my-posh",
"repo": null
},
"worktree": null
},
"enabled": true
},
@@ -505,29 +639,119 @@
"Percent": 34,
"Remaining": 66,
"Unlimited": false,
"Used": 34
"Used": 34,
"limit": 100,
"percent": 34,
"remaining": 66,
"unlimited": false,
"used": 34
},
"Inline": {
"Limit": 0,
"Percent": 0,
"Remaining": 100,
"Unlimited": true,
"Used": 0
"Used": 0,
"limit": 0,
"percent": 0,
"remaining": 100,
"unlimited": true,
"used": 0
},
"Premium": {
"Limit": 300,
"Percent": 40,
"Remaining": 60,
"Unlimited": false,
"Used": 120
"Used": 120,
"limit": 300,
"percent": 40,
"remaining": 60,
"unlimited": false,
"used": 120
},
"Segment": {
"Index": 21,
"Text": " x "
},
"Text": " x "
"Text": " x ",
"billing_cycle_end": "2026-08-01",
"chat": {
"Limit": 100,
"Percent": 34,
"Remaining": 66,
"Unlimited": false,
"Used": 34,
"limit": 100,
"percent": 34,
"remaining": 66,
"unlimited": false,
"used": 34
},
"inline": {
"Limit": 0,
"Percent": 0,
"Remaining": 100,
"Unlimited": true,
"Used": 0,
"limit": 0,
"percent": 0,
"remaining": 100,
"unlimited": true,
"used": 0
},
"premium": {
"Limit": 300,
"Percent": 40,
"Remaining": 60,
"Unlimited": false,
"Used": 120,
"limit": 300,
"percent": 40,
"remaining": 60,
"unlimited": false,
"used": 120
}
},
"enabled": true
"enabled": true,
"methods": {
"Chat": {
"Percent": {
"Gauge": "▰▰▰▱▱",
"GaugeUsed": "▰▱▱▱▱",
"String": "34"
},
"Remaining": {
"Gauge": "▰▱▱▱▱",
"GaugeUsed": "▰▰▰▱▱",
"String": "66"
}
},
"Inline": {
"Percent": {
"Gauge": "▰▰▰▰▰",
"GaugeUsed": "▱▱▱▱▱",
"String": "0"
},
"Remaining": {
"Gauge": "▱▱▱▱▱",
"GaugeUsed": "▰▰▰▰▰",
"String": "100"
}
},
"Premium": {
"Percent": {
"Gauge": "▰▰▰▱▱",
"GaugeUsed": "▰▰▱▱▱",
"String": "40"
},
"Remaining": {
"Gauge": "▰▰▱▱▱",
"GaugeUsed": "▰▰▰▱▱",
"String": "60"
}
}
}
},
"copilot_cli": {
"data": {
@@ -545,26 +769,47 @@
"TotalOutputTokens": 0,
"TotalReasoningTokens": 0,
"TotalTokens": 36000,
"UsedPercentage": 18
"UsedPercentage": 18,
"context_window_size": 200000,
"current_context_tokens": 36000,
"last_call_input_tokens": 0,
"last_call_output_tokens": 0,
"remaining_percentage": null,
"remaining_tokens": null,
"total_cache_read_tokens": 0,
"total_cache_write_tokens": 0,
"total_input_tokens": 0,
"total_output_tokens": 0,
"total_reasoning_tokens": 0,
"total_tokens": 36000,
"used_percentage": 18
},
"Cost": {
"TotalAPIDurationMS": 0,
"TotalDurationMS": 214000,
"TotalLinesAdded": 9,
"TotalLinesRemoved": 2,
"TotalPremiumRequests": 3
"TotalPremiumRequests": 3,
"total_api_duration_ms": 0,
"total_duration_ms": 214000,
"total_lines_added": 9,
"total_lines_removed": 2,
"total_premium_requests": 3
},
"FormattedAPIDuration": "0m 0s",
"FormattedDuration": "3m 34s",
"FormattedTokens": "36.0K",
"Model": {
"DisplayName": "Sonnet 4.5",
"ID": "claude-sonnet-4-5"
"ID": "claude-sonnet-4-5",
"display_name": "Sonnet 4.5",
"id": "claude-sonnet-4-5"
},
"RemainingPercent": 82,
"RemainingTokensCount": 164000,
"Remote": {
"Connected": false
"Connected": false,
"connected": false
},
"Segment": {
"Index": 22,
@@ -580,7 +825,68 @@
"Username": "alice",
"Version": "0.0.339",
"Workspace": {
"CurrentDir": "~/dev/oh-my-posh"
"CurrentDir": "~/dev/oh-my-posh",
"current_dir": "~/dev/oh-my-posh"
},
"context_window": {
"ContextWindowSize": 200000,
"CurrentContextTokens": 36000,
"LastCallInputTokens": 0,
"LastCallOutputTokens": 0,
"RemainingPercentage": null,
"RemainingTokens": null,
"TotalCacheReadTokens": 0,
"TotalCacheWriteTokens": 0,
"TotalInputTokens": 0,
"TotalOutputTokens": 0,
"TotalReasoningTokens": 0,
"TotalTokens": 36000,
"UsedPercentage": 18,
"context_window_size": 200000,
"current_context_tokens": 36000,
"last_call_input_tokens": 0,
"last_call_output_tokens": 0,
"remaining_percentage": null,
"remaining_tokens": null,
"total_cache_read_tokens": 0,
"total_cache_write_tokens": 0,
"total_input_tokens": 0,
"total_output_tokens": 0,
"total_reasoning_tokens": 0,
"total_tokens": 36000,
"used_percentage": 18
},
"cost": {
"TotalAPIDurationMS": 0,
"TotalDurationMS": 214000,
"TotalLinesAdded": 9,
"TotalLinesRemoved": 2,
"TotalPremiumRequests": 3,
"total_api_duration_ms": 0,
"total_duration_ms": 214000,
"total_lines_added": 9,
"total_lines_removed": 2,
"total_premium_requests": 3
},
"cwd": "~/dev/oh-my-posh",
"model": {
"DisplayName": "Sonnet 4.5",
"ID": "claude-sonnet-4-5",
"display_name": "Sonnet 4.5",
"id": "claude-sonnet-4-5"
},
"remote": {
"Connected": false,
"connected": false
},
"session_id": "f6e5d4c3-b2a1-4098-9876-543210fedcba",
"session_name": "oh-my-posh",
"transcript_path": "",
"username": "alice",
"version": "0.0.339",
"workspace": {
"CurrentDir": "~/dev/oh-my-posh",
"current_dir": "~/dev/oh-my-posh"
}
},
"enabled": true
@@ -1375,7 +1681,14 @@
"Text": " x ",
"Time": "Q3 6:42"
},
"enabled": true
"enabled": true,
"methods": {
"GameStatus": {
"Int": 2,
"String": "In Progress",
"Valid": true
}
}
},
"nbgv": {
"data": {
@@ -1413,7 +1726,18 @@
"Trend": 4,
"TrendIcon": "",
"Type": "sgv",
"UtcOffset": 60
"UtcOffset": 60,
"_id": "",
"date": 1782500000000,
"dateString": "2026-07-28T09:33:20Z",
"device": "xDrip-DexcomG6",
"direction": "Flat",
"mills": 1782500000000,
"sgv": 118,
"sysTime": "2026-07-28T09:33:20Z",
"trend": 4,
"type": "sgv",
"utcOffset": 60
},
"enabled": true
},
@@ -1595,7 +1919,12 @@
"Text": " x ",
"Writable": true
},
"enabled": true
"enabled": true,
"methods": {
"Folders": {
"List": null
}
}
},
"perl": {
"data": {
@@ -1722,7 +2051,9 @@
"Stack": "contoso-prod",
"Text": " x ",
"URL": "app.pulumi.com/contoso",
"User": "alice"
"User": "alice",
"url": "app.pulumi.com/contoso",
"user": "alice"
},
"enabled": true
},
@@ -1753,7 +2084,9 @@
"data": {
"AppVite": {
"Dev": true,
"Version": "1.9.3"
"Version": "1.9.3",
"dev": true,
"version": "1.9.3"
},
"BuildMetadata": "",
"Error": "",
@@ -1775,7 +2108,9 @@
"URL": "",
"Vite": {
"Dev": true,
"Version": "5.4.11"
"Version": "5.4.11",
"dev": true,
"version": "5.4.11"
}
},
"enabled": true
@@ -2019,7 +2354,19 @@
"Text": " x ",
"Type": "Ride",
"URL": "",
"WeightedAverageWatts": 0
"WeightedAverageWatts": 0,
"average_heartrate": 0,
"average_watts": 0,
"device_watts": false,
"distance": 0,
"id": 0,
"kudos_count": 0,
"max_heartrate": 0,
"moving_time": 0,
"name": "Morning Ride",
"start_date": "0001-01-01T00:00:00Z",
"type": "Ride",
"weighted_average_watts": 0
},
"enabled": true
},
@@ -2181,7 +2528,8 @@
},
"Text": " x ",
"Version": "1.9.0",
"WorkspaceName": "example_corp-prod"
"WorkspaceName": "example_corp-prod",
"terraform_version": "1.9.0"
},
"enabled": true
},
@@ -2197,7 +2545,7 @@
},
"time": {
"data": {
"CurrentDate": "2026-03-17T09:41:00+01:00",
"CurrentDate": "2026-03-17T09:41:00Z",
"Format": "",
"Segment": {
"Index": 98,
@@ -2286,7 +2634,9 @@
"Text": " x "
},
"Text": " x ",
"Version": ""
"Version": "",
"current": "26.7.1",
"latest": "26.8.0"
},
"enabled": true
},
@@ -2350,7 +2700,9 @@
"data": {
"CumulativeTotal": {
"Seconds": 16200,
"Text": "4 hrs 30 mins"
"Text": "4 hrs 30 mins",
"seconds": 16200,
"text": "4 hrs 30 mins"
},
"End": "2026-07-09T23:59:59Z",
"Segment": {
@@ -2358,7 +2710,15 @@
"Text": " x "
},
"Start": "2026-07-09T00:00:00Z",
"Text": " x "
"Text": " x ",
"cumulative_total": {
"Seconds": 16200,
"Text": "4 hrs 30 mins",
"seconds": 16200,
"text": "4 hrs 30 mins"
},
"end": "2026-07-09T23:59:59Z",
"start": "2026-07-09T00:00:00Z"
},
"enabled": true
},
@@ -2372,16 +2732,16 @@
"UpdateCount": 2,
"Updates": [
{
"Name": "Microsoft Edge",
"ID": "Microsoft.Edge",
"Available": "131.0.2903.51",
"Current": "130.0.2849.68",
"Available": "131.0.2903.51"
"ID": "Microsoft.Edge",
"Name": "Microsoft Edge"
},
{
"Name": "Git",
"ID": "Git.Git",
"Available": "2.47.1",
"Current": "2.47.0",
"Available": "2.47.1"
"ID": "Git.Git",
"Name": "Git"
}
]
},