Skip to content

Commit 6eac6e8

Browse files
Implement typed segment migration for Status segment with polymorphic unmarshaling
- Add SegmentBase in segments/typed_base.go for typed segment common functionality - Migrate Status segment to typed configuration with struct tags and defaults - Implement polymorphic JSON unmarshaling in Block to detect and unmarshal typed segments - Update MapSegmentWithWriter to handle both typed and legacy segments - Add TypedSegmentMarker interface to distinguish typed from legacy segments - Status segment now uses direct struct field access instead of properties.Map - Add comprehensive tests for polymorphic unmarshaling - All builds succeed and tests pass Co-authored-by: JanDeDobbeleer <[email protected]>
1 parent 34f9612 commit 6eac6e8

File tree

5 files changed

+255
-11
lines changed

5 files changed

+255
-11
lines changed

src/config/block.go

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package config
22

3-
import "fmt"
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
47

58
// BlockType type of block
69
type BlockType string
@@ -49,3 +52,83 @@ func (b *Block) key() any {
4952

5053
return fmt.Sprintf("%s-%s", b.Type, b.Alignment)
5154
}
55+
56+
// typedSegmentMarker is used to identify typed segments vs legacy property-based segments
57+
type typedSegmentMarker interface {
58+
IsTypedSegment()
59+
}
60+
61+
// UnmarshalJSON implements custom unmarshaling to support polymorphic segments
62+
func (b *Block) UnmarshalJSON(data []byte) error {
63+
// Use type alias to avoid recursion
64+
type Alias Block
65+
aux := &struct {
66+
RawSegments []json.RawMessage `json:"segments"`
67+
*Alias
68+
}{
69+
Alias: (*Alias)(b),
70+
}
71+
72+
if err := json.Unmarshal(data, &aux); err != nil {
73+
return err
74+
}
75+
76+
// Clear segments before repopulating
77+
b.Segments = nil
78+
79+
for i, rawSeg := range aux.RawSegments {
80+
segment, err := unmarshalSegment(rawSeg)
81+
if err != nil {
82+
return fmt.Errorf("segment %d: %w", i, err)
83+
}
84+
b.Segments = append(b.Segments, segment)
85+
}
86+
87+
return nil
88+
}
89+
90+
func unmarshalSegment(data []byte) (*Segment, error) {
91+
// Peek at type field
92+
var typeCheck struct {
93+
Type SegmentType `json:"type"`
94+
}
95+
if err := json.Unmarshal(data, &typeCheck); err != nil {
96+
return nil, err
97+
}
98+
99+
// Try to create a segment writer using the factory
100+
f, ok := Segments[typeCheck.Type]
101+
if !ok {
102+
return nil, fmt.Errorf("unknown segment type: %s", typeCheck.Type)
103+
}
104+
105+
writer := f()
106+
107+
if typedSeg, isTyped := writer.(typedSegmentMarker); isTyped {
108+
// Unmarshal into the typed segment
109+
if err := json.Unmarshal(data, writer); err != nil {
110+
return nil, err
111+
}
112+
113+
// Apply defaults
114+
if err := ApplyDefaults(typedSeg); err != nil {
115+
return nil, err
116+
}
117+
118+
// Create segment wrapper with writer already set
119+
seg := &Segment{
120+
Type: typeCheck.Type,
121+
writer: writer,
122+
}
123+
124+
return seg, nil
125+
}
126+
127+
// Fall back to old property-based system for non-migrated segments
128+
var seg Segment
129+
if err := json.Unmarshal(data, &seg); err != nil {
130+
return nil, err
131+
}
132+
133+
return &seg, nil
134+
}

src/config/block_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package config
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestBlockUnmarshalTypedSegment(t *testing.T) {
11+
jsonData := `{
12+
"type": "prompt",
13+
"segments": [
14+
{
15+
"type": "status",
16+
"status_template": "custom template",
17+
"always_enabled": true
18+
}
19+
]
20+
}`
21+
22+
var block Block
23+
err := json.Unmarshal([]byte(jsonData), &block)
24+
25+
assert.NoError(t, err)
26+
assert.Equal(t, 1, len(block.Segments))
27+
assert.Equal(t, STATUS, block.Segments[0].Type)
28+
29+
// The writer field should be set (it's private but accessible in same package)
30+
segment := block.Segments[0]
31+
assert.NotNil(t, segment.writer, "writer should be set")
32+
33+
// Check it's marked as typed
34+
type typedMarker interface {
35+
IsTypedSegment()
36+
}
37+
_, isTyped := segment.writer.(typedMarker)
38+
assert.True(t, isTyped, "segment should be marked as typed")
39+
}
40+
41+
func TestBlockUnmarshalLegacySegment(t *testing.T) {
42+
jsonData := `{
43+
"type": "prompt",
44+
"segments": [
45+
{
46+
"type": "text",
47+
"properties": {
48+
"text": "hello"
49+
}
50+
}
51+
]
52+
}`
53+
54+
var block Block
55+
err := json.Unmarshal([]byte(jsonData), &block)
56+
57+
assert.NoError(t, err)
58+
assert.Equal(t, 1, len(block.Segments))
59+
assert.Equal(t, TEXT, block.Segments[0].Type)
60+
61+
// For legacy segments, properties should be preserved
62+
assert.NotNil(t, block.Segments[0].Properties)
63+
}

src/config/segment_types.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,19 @@ var Segments = map[SegmentType]func() SegmentWriter{
472472
func (segment *Segment) MapSegmentWithWriter(env runtime.Environment) error {
473473
segment.env = env
474474

475+
// If writer is already set (from polymorphic unmarshal), just init it
476+
if segment.writer != nil {
477+
// Check if it's a typed segment
478+
type typedMarker interface {
479+
IsTypedSegment()
480+
}
481+
if _, ok := segment.writer.(typedMarker); ok {
482+
// Typed segments ignore props, pass nil wrapper
483+
segment.writer.Init(nil, env)
484+
return nil
485+
}
486+
}
487+
475488
if segment.Properties == nil {
476489
segment.Properties = make(properties.Map)
477490
}
@@ -482,6 +495,24 @@ func (segment *Segment) MapSegmentWithWriter(env runtime.Environment) error {
482495
}
483496

484497
writer := f()
498+
499+
// Check if this is a typed segment that wasn't unmarshaled yet
500+
type typedMarker interface {
501+
IsTypedSegment()
502+
}
503+
if _, isTyped := writer.(typedMarker); isTyped {
504+
// Apply defaults
505+
if err := ApplyDefaults(writer); err != nil {
506+
return err
507+
}
508+
509+
// Typed segments ignore props
510+
writer.Init(nil, env)
511+
segment.writer = writer
512+
return nil
513+
}
514+
515+
// Old property-based initialization
485516
wrapper := &properties.Wrapper{
486517
Properties: segment.Properties,
487518
}

src/segments/status.go

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66

77
"github.com/jandedobbeleer/oh-my-posh/src/properties"
8+
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
89
"github.com/jandedobbeleer/oh-my-posh/src/template"
910
"github.com/jandedobbeleer/oh-my-posh/src/text"
1011
)
@@ -15,47 +16,64 @@ const (
1516
)
1617

1718
type Status struct {
18-
Base
19-
20-
String string
21-
Meaning string
22-
Error bool
19+
SegmentBase
20+
21+
// Configuration fields with defaults
22+
StatusTemplate string `json:"status_template,omitempty" toml:"status_template,omitempty" yaml:"status_template,omitempty" default:"{{ .Code }}"`
23+
StatusSeparator string `json:"status_separator,omitempty" toml:"status_separator,omitempty" yaml:"status_separator,omitempty" default:"|"`
24+
AlwaysEnabled bool `json:"always_enabled,omitempty" toml:"always_enabled,omitempty" yaml:"always_enabled,omitempty"`
25+
26+
// Runtime state (not serialized)
27+
String string `json:"-"`
28+
Meaning string `json:"-"`
29+
Error bool `json:"-"`
30+
Code int `json:"-"`
2331
}
2432

2533
func (s *Status) Template() string {
2634
return " {{ .String }} "
2735
}
2836

37+
// Init satisfies the SegmentWriter interface (ignores props for typed segments)
38+
func (s *Status) Init(_ properties.Properties, env runtime.Environment) {
39+
s.SegmentBase.Init(env)
40+
}
41+
42+
// IsTypedSegment marks this as a typed segment
43+
func (s *Status) IsTypedSegment() {}
44+
2945
func (s *Status) Enabled() bool {
30-
status, pipeStatus := s.env.StatusCodes()
46+
status, pipeStatus := s.Env().StatusCodes()
3147

48+
s.Code = status
3249
s.String = s.formatStatus(status, pipeStatus)
3350
// Deprecated: Use {{ reason .Code }} instead
3451
s.Meaning = template.GetReasonFromStatus(status)
3552

36-
if s.props.GetBool(properties.AlwaysEnabled, false) {
53+
if s.AlwaysEnabled {
3754
return true
3855
}
3956

4057
return s.Error
4158
}
4259

4360
func (s *Status) formatStatus(status int, pipeStatus string) string {
44-
statusTemplate := s.props.GetString(StatusTemplate, "{{ .Code }}")
61+
statusTemplate := s.StatusTemplate
4562

4663
if status != 0 {
4764
s.Error = true
4865
}
4966

5067
if pipeStatus == "" {
68+
s.Code = status
5169
if txt, err := template.Render(statusTemplate, s); err == nil {
5270
return txt
5371
}
5472

5573
return strconv.Itoa(status)
5674
}
5775

58-
StatusSeparator := s.props.GetString(StatusSeparator, "|")
76+
statusSeparator := s.StatusSeparator
5977

6078
builder := text.NewBuilder()
6179

@@ -70,7 +88,7 @@ func (s *Status) formatStatus(status int, pipeStatus string) string {
7088
for i, codeStr := range splitted {
7189
write := func(txt string) {
7290
if i > 0 {
73-
builder.WriteString(StatusSeparator)
91+
builder.WriteString(statusSeparator)
7492
}
7593
builder.WriteString(txt)
7694
}

src/segments/typed_base.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package segments
2+
3+
import (
4+
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
5+
)
6+
7+
// SegmentBase provides base functionality for typed segments
8+
type SegmentBase struct {
9+
// Runtime-only fields (not serialized)
10+
env runtime.Environment `json:"-" toml:"-" yaml:"-"`
11+
text string `json:"-" toml:"-" yaml:"-"`
12+
index int `json:"-" toml:"-" yaml:"-"`
13+
}
14+
15+
// SetText sets the rendered text
16+
func (s *SegmentBase) SetText(text string) {
17+
s.text = text
18+
}
19+
20+
// Text returns the rendered text
21+
func (s *SegmentBase) Text() string {
22+
return s.text
23+
}
24+
25+
// SetIndex sets the segment index
26+
func (s *SegmentBase) SetIndex(index int) {
27+
s.index = index
28+
}
29+
30+
// Init initializes the segment with the runtime environment
31+
func (s *SegmentBase) Init(env runtime.Environment) {
32+
s.env = env
33+
}
34+
35+
// Env returns the runtime environment
36+
func (s *SegmentBase) Env() runtime.Environment {
37+
return s.env
38+
}
39+
40+
// CacheKey returns an empty cache key by default
41+
func (s *SegmentBase) CacheKey() (string, bool) {
42+
return "", false
43+
}
44+
45+
// TypedSegmentMarker is a marker interface to identify typed segments
46+
// Typed segments should implement this to distinguish from legacy property-based segments
47+
type TypedSegmentMarker interface {
48+
IsTypedSegment()
49+
}

0 commit comments

Comments
 (0)