Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions src/config/segment.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,25 +311,31 @@ func (segment *Segment) restoreCache() bool {
}

key, store := segment.cacheKeyAndStore()
data, OK := cache.Get[string](store, key)

data, OK := cache.Get[any](store, key)
if !OK {
log.Debugf("no cache found for segment: %s, key: %s", segment.Name(), key)
return false
}

err := json.Unmarshal([]byte(data), &segment.writer)
if err != nil {
log.Error(err)
switch v := data.(type) {
case SegmentWriter:
segment.writer = v
segment.Enabled = true
template.Cache.AddSegmentData(segment.Name(), segment.writer)
log.Debug("restored segment from cache: ", segment.Name())
segment.restored = true
return true
case string:
// legacy JSON cache entry, remove it so it gets re-cached with the new format
log.Debugf("removing legacy cache key: %s", key)
cache.Delete(store, key)
return false
default:
log.Debugf("unexpected cache type for segment: %s, key: %s", segment.Name(), key)
cache.Delete(store, key)
return false
}

segment.Enabled = true
template.Cache.AddSegmentData(segment.Name(), segment.writer)

log.Debug("restored segment from cache: ", segment.Name())

segment.restored = true

return true
}

func (segment *Segment) setCache() {
Expand All @@ -342,17 +348,11 @@ func (segment *Segment) setCache() {
return
}

data, err := json.Marshal(segment.writer)
if err != nil {
log.Error(err)
return
}

// TODO: check if we can make segmentwriter a generic Type indicator
// that way we can actually get the value straight from cache.Get
// and marchalling is obsolete
key, store := segment.cacheKeyAndStore()
cache.Set(store, key, string(data), segment.Cache.Duration)
cache.Set(store, key, segment.writer, segment.Cache.Duration)
}

func (segment *Segment) cacheKeyAndStore() (string, cache.Store) {
Expand Down
93 changes: 93 additions & 0 deletions src/config/segment_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package config

import (
"testing"

"github.com/jandedobbeleer/oh-my-posh/src/cache"
"github.com/jandedobbeleer/oh-my-posh/src/maps"
"github.com/jandedobbeleer/oh-my-posh/src/runtime/mock"
"github.com/jandedobbeleer/oh-my-posh/src/segments"
"github.com/jandedobbeleer/oh-my-posh/src/template"
"github.com/stretchr/testify/assert"
)

func TestSegmentCache(t *testing.T) {
template.Cache = &cache.Template{
Segments: maps.NewConcurrent[any](),
}
defer cache.DeleteAll(cache.Device)

env := new(mock.Environment)
env.On("Pwd").Return("/tmp")

segment := &Segment{
Type: TEXT,
Cache: &Cache{
Strategy: Folder,
Duration: cache.Duration("10m"),
},
Alias: "my_text_segment",
env: env,
}

textWriter := &segments.Text{}
textWriter.Init(nil, nil)
textWriter.SetText("Hello, Cache!")
segment.writer = textWriter
segment.name = "my_text_segment"

segment.setCache()

newSegment := &Segment{
Type: TEXT,
Cache: &Cache{
Strategy: Folder,
Duration: cache.Duration("10m"),
},
Alias: "my_text_segment",
env: env,
}
newSegment.name = "my_text_segment"

newTextWriter := &segments.Text{}
newTextWriter.Init(nil, nil)
newSegment.writer = newTextWriter

restored := newSegment.restoreCache()

assert.True(t, restored, "Cache should be restored")
assert.NotNil(t, newSegment.writer, "Writer should be restored")
assert.IsType(t, &segments.Text{}, newSegment.writer, "Writer should be of type *segments.Text")
if newSegment.writer != nil {
assert.Equal(t, "Hello, Cache!", newSegment.writer.Text(), "Restored text should match")
}

// Test legacy cache (string value)
legacySegment := &Segment{
Type: TEXT,
Cache: &Cache{
Strategy: Device,
Duration: cache.Duration("10m"),
},
Alias: "legacy_segment",
env: env,
}
legacySegment.name = "legacy_segment"

// Initialize writer so cacheKeyAndStore can compute the key
legacyWriter := &segments.Text{}
legacyWriter.Init(nil, nil)
legacySegment.writer = legacyWriter

// Derive the key using the same logic as production code
legacyKey, legacyStore := legacySegment.cacheKeyAndStore()
cache.Set(legacyStore, legacyKey, "legacy_json_string", cache.Duration("10m"))

// Should return false and delete the key
restoredLegacy := legacySegment.restoreCache()
assert.False(t, restoredLegacy, "Legacy cache should not be restored")

// Verify key is gone
_, found := cache.Get[string](legacyStore, legacyKey)
assert.False(t, found, "Legacy key should be removed")
}
4 changes: 4 additions & 0 deletions src/config/segment_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func init() {
gob.Register(&segments.Swift{})
gob.Register(&segments.SystemInfo{})
gob.Register(&segments.TalosCTL{})
gob.Register(&segments.Taskwarrior{})
gob.Register(&segments.Tauri{})
gob.Register(&segments.Terraform{})
gob.Register(&segments.Text{})
Expand Down Expand Up @@ -338,6 +339,8 @@ const (
SYSTEMINFO SegmentType = "sysinfo"
// TALOSCTL writes the talosctl context
TALOSCTL SegmentType = "talosctl"
// TASKWARRIOR writes Taskwarrior task counts and context
TASKWARRIOR SegmentType = "taskwarrior"
// Tauri Segment
TAURI SegmentType = "tauri"
// TERRAFORM writes the terraform workspace we're currently in
Expand Down Expand Up @@ -470,6 +473,7 @@ var Segments = map[SegmentType]func() SegmentWriter{
SWIFT: func() SegmentWriter { return &segments.Swift{} },
SYSTEMINFO: func() SegmentWriter { return &segments.SystemInfo{} },
TALOSCTL: func() SegmentWriter { return &segments.TalosCTL{} },
TASKWARRIOR: func() SegmentWriter { return &segments.Taskwarrior{} },
TAURI: func() SegmentWriter { return &segments.Tauri{} },
TERRAFORM: func() SegmentWriter { return &segments.Terraform{} },
TEXT: func() SegmentWriter { return &segments.Text{} },
Expand Down
68 changes: 68 additions & 0 deletions src/segments/executiontime.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ const (
Round DurationStyle = "round"
// Always 7 character width
Lucky7 = "lucky7"
// ISO8601 ISO 8601 duration format (seconds)
ISO8601 DurationStyle = "iso8601"
// ISO8601Ms ISO 8601 duration format with milliseconds
ISO8601Ms DurationStyle = "iso8601ms"

second = 1000
minute = 60000
Expand Down Expand Up @@ -87,6 +91,10 @@ func (t *Executiontime) formatDuration(style DurationStyle) string {
return t.formatDurationRound()
case Lucky7:
return t.formatDurationLucky7()
case ISO8601:
return t.formatDurationISO8601()
case ISO8601Ms:
return t.formatDurationISO8601Ms()
default:
return fmt.Sprintf("Style: %s is not available", style)
}
Expand Down Expand Up @@ -273,3 +281,63 @@ func (t *Executiontime) formatDurationLucky7() string {
d := t.Ms / day
return fmt.Sprintf("%6dd", d)
}

func (t *Executiontime) formatDurationISO8601() string {
// ISO 8601 duration format: PT[n]H[n]M[n]S
// Examples: PT13M12S, PT1H30M45S
result := "PT"

hours := t.Ms / hour
minutes := (t.Ms % hour) / minute
seconds := float64(t.Ms%minute) / second

roundedSeconds := int64(seconds)
if t.Ms%second >= second/2 {
roundedSeconds++
}

// Handle potential overflow from rounding
if roundedSeconds >= secondsPerMinute {
roundedSeconds = 0
minutes++
if minutes >= minutesPerHour {
minutes = 0
hours++
}
}

if hours > 0 {
result += fmt.Sprintf("%dH", hours)
}
if minutes > 0 {
result += fmt.Sprintf("%dM", minutes)
}
if roundedSeconds > 0 || (hours == 0 && minutes == 0) {
result += fmt.Sprintf("%dS", roundedSeconds)
}

return result
}

func (t *Executiontime) formatDurationISO8601Ms() string {
// ISO 8601 duration format with milliseconds: PT[n]H[n]M[n]S
// Examples: PT13M12.1S, PT1H30M45.123S
result := "PT"

hours := t.Ms / hour
minutes := (t.Ms % hour) / minute
seconds := float64(t.Ms%minute) / second

if hours > 0 {
result += fmt.Sprintf("%dH", hours)
}
if minutes > 0 {
result += fmt.Sprintf("%dM", minutes)
}
if seconds > 0 || (hours == 0 && minutes == 0) {
secondsStr := strconv.FormatFloat(seconds, 'f', -1, 64)
result += fmt.Sprintf("%sS", secondsStr)
}

return result
}
56 changes: 56 additions & 0 deletions src/segments/executiontime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,59 @@ func TestExecutionTimeFormatDurationLucky7(t *testing.T) {
assert.Equal(t, len(executionTime), 7)
}
}

func TestExecutionTimeFormatISO8601(t *testing.T) {
cases := []struct {
Input string
Expected string
}{
{Input: "0.001s", Expected: "PT0S"},
{Input: "0.1s", Expected: "PT0S"},
{Input: "0.5s", Expected: "PT1S"},
{Input: "1s", Expected: "PT1S"},
{Input: "2.1s", Expected: "PT2S"},
{Input: "2.6s", Expected: "PT3S"},
{Input: "1m", Expected: "PT1M"},
{Input: "3m2.1s", Expected: "PT3M2S"},
{Input: "3m2.6s", Expected: "PT3M3S"},
{Input: "1h", Expected: "PT1H"},
{Input: "4h3m2.1s", Expected: "PT4H3M2S"},
{Input: "124h3m2.1s", Expected: "PT124H3M2S"},
{Input: "124h3m2.0s", Expected: "PT124H3M2S"},
}

for _, tc := range cases {
duration, _ := time.ParseDuration(tc.Input)
executionTime := &Executiontime{}
executionTime.Ms = duration.Milliseconds()
output := executionTime.formatDurationISO8601()
assert.Equal(t, tc.Expected, output, "Input: %s", tc.Input)
}
}

func TestExecutionTimeFormatISO8601Ms(t *testing.T) {
cases := []struct {
Input string
Expected string
}{
{Input: "0.001s", Expected: "PT0.001S"},
{Input: "0.1s", Expected: "PT0.1S"},
{Input: "1s", Expected: "PT1S"},
{Input: "2.1s", Expected: "PT2.1S"},
{Input: "2.123s", Expected: "PT2.123S"},
{Input: "1m", Expected: "PT1M"},
{Input: "3m2.1s", Expected: "PT3M2.1S"},
{Input: "3m2.123s", Expected: "PT3M2.123S"},
{Input: "1h", Expected: "PT1H"},
{Input: "4h3m2.1s", Expected: "PT4H3M2.1S"},
{Input: "124h3m2.123s", Expected: "PT124H3M2.123S"},
}

for _, tc := range cases {
duration, _ := time.ParseDuration(tc.Input)
executionTime := &Executiontime{}
executionTime.Ms = duration.Milliseconds()
output := executionTime.formatDurationISO8601Ms()
assert.Equal(t, tc.Expected, output, "Input: %s", tc.Input)
}
}
69 changes: 69 additions & 0 deletions src/segments/taskwarrior.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package segments

import (
"strings"

c "golang.org/x/text/cases"
"golang.org/x/text/language"

"github.com/jandedobbeleer/oh-my-posh/src/log"
"github.com/jandedobbeleer/oh-my-posh/src/segments/options"
)

// Taskwarrior option constants
const (
TaskwarriorCommand options.Option = "command"
TaskwarriorCommands options.Option = "commands"
)

// Taskwarrior displays task counts and context from Taskwarrior.
// The Commands field is a map from capitalized command name to the raw output
// of the corresponding Taskwarrior invocation. Each entry in the config map
// has the command name as key and a full Taskwarrior argument string as value.
type Taskwarrior struct {
Base

// Commands holds the raw output of each configured command, keyed by name
// with the first letter uppercased.
Commands map[string]string
}

func (t *Taskwarrior) Template() string {
return " \uf4a0 {{ range $k, $v := .Commands }}{{ $k }}:{{ $v }} {{ end }}"
}

func (t *Taskwarrior) Enabled() bool {
cmd := t.options.String(TaskwarriorCommand, "task")

if !t.env.HasCommand(cmd) {
return false
}

defaultCommands := map[string]string{
"due": "+PENDING due.before:tomorrow count",
"scheduled": "+PENDING scheduled.before:tomorrow count",
"waiting": "+WAITING count",
"context": "_get rc.context",
}

configuredCommands := t.options.KeyValueMap(TaskwarriorCommands, defaultCommands)

t.Commands = make(map[string]string, len(configuredCommands))

for name, args := range configuredCommands {
key := c.Title(language.English).String(name)
t.Commands[key] = t.runCommand(cmd, args)
}

return true
}

func (t *Taskwarrior) runCommand(cmd, args string) string {
output, err := t.env.RunCommand(cmd, strings.Fields(args)...)
if err != nil {
log.Error(err)
return ""
}

return strings.TrimSpace(output)
}
Loading