Repository: JanDeDobbeleer/oh-my-posh
Pull request #982 (2021-09-21 18:23:10)

feature cache
- refactor: split concurrent map from cache implementation
- feat: cache capabilities

### Prerequisites

- [x] I have read and understood the `CONTRIBUTING` guide
- [x] The commit message follows the [conventional commits][cc] guidelines
- [x] Tests for the changes have been added (for bug fixes/features)
- [x] Docs have been added / updated (for bug fixes/features)

### Description

[Description of the change]

<!---

Tips:

If you're not comfortable with working with Git, we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary


lnu: I did something similar on my side today… 
With load/save from/to a file
JanDeDobbeleer: > I did something similar on my side today…
> With load/save from/to a file

@lnu I'd appreciate a review, if architecturally OK I can start adding some tests
lnu: All the code I've done is here https://github.com/boarder2/oh-my-posh3/pull/1/files
It's very rough but it's very similar to yours

Patch:
diff --git a/src/concurrent_map.go b/src/concurrent_map.go
new file mode 100644
index 00000000..8e2dc2db
--- /dev/null
+++ b/src/concurrent_map.go
@@ -0,0 +1,40 @@
+package main
+
+import "sync"
+
+type concurrentMap struct {
+	values map[string]interface{}
+	lock   sync.RWMutex
+}
+
+func newConcurrentMap() *concurrentMap {
+	return &concurrentMap{
+		values: make(map[string]interface{}),
+		lock:   sync.RWMutex{},
+	}
+}
+
+func (c *concurrentMap) set(key string, value interface{}) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	c.values[key] = value
+}
+
+func (c *concurrentMap) get(key string) (interface{}, bool) {
+	c.lock.RLock()
+	defer c.lock.RUnlock()
+	if val, ok := c.values[key]; ok {
+		return val, true
+	}
+	return "", false
+}
+
+func (c *concurrentMap) remove(key string) {
+	c.lock.RLock()
+	defer c.lock.RUnlock()
+	delete(c.values, key)
+}
+
+func (c *concurrentMap) list() map[string]interface{} {
+	return c.values
+}
diff --git a/src/environment.go b/src/environment.go
index 3f328307..735678d2 100644
--- a/src/environment.go
+++ b/src/environment.go
@@ -48,6 +48,14 @@ type fileInfo struct {
 	isDir        bool
 }
 
+type cache interface {
+	init(home string)
+	close()
+	get(key string) (string, bool)
+	// ttl in seconds
+	set(key, value string, ttl int64)
+}
+
 type environmentInfo interface {
 	getenv(key string) string
 	getcwd() string
@@ -77,27 +85,25 @@ type environmentInfo interface {
 	isWsl() bool
 	stackCount() int
 	getTerminalWidth() (int, error)
+	cache() cache
+	close()
 }
 
 type commandCache struct {
-	commands map[string]string
-	lock     sync.RWMutex
+	commands *concurrentMap
 }
 
 func (c *commandCache) set(command, path string) {
-	c.lock.Lock()
-	defer c.lock.Unlock()
-	c.commands[command] = path
+	c.commands.set(command, path)
 }
 
 func (c *commandCache) get(command string) (string, bool) {
-	c.lock.RLock()
-	defer c.lock.RUnlock()
-	if cmd, ok := c.commands[command]; ok {
-		command = cmd
-		return command, true
+	cmd, found := c.commands.get(command)
+	if !found {
+		return "", false
 	}
-	return "", false
+	command, ok := cmd.(string)
+	return command, ok
 }
 
 type tracer struct {
@@ -137,19 +143,20 @@ func (t *tracer) trace(start time.Time, function string, args ...string) {
 }
 
 type environment struct {
-	args     *args
-	cwd      string
-	cmdCache *commandCache
-	tracer   *tracer
+	args      *args
+	cwd       string
+	cmdCache  *commandCache
+	fileCache *fileCache
+	tracer    *tracer
 }
 
 func (env *environment) init(args *args) {
 	env.args = args
-	cmdCache := &commandCache{
-		commands: make(map[string]string),
-		lock:     sync.RWMutex{},
+	env.cmdCache = &commandCache{
+		commands: newConcurrentMap(),
 	}
-	env.cmdCache = cmdCache
+	env.fileCache = &fileCache{}
+	env.fileCache.init(env.homeDir())
 	tracer := &tracer{
 		debug: *args.Debug,
 	}
@@ -472,6 +479,15 @@ func (env *environment) stackCount() int {
 	return *env.args.StackCount
 }
 
+func (env *environment) cache() cache {
+	return env.fileCache
+}
+
+func (env *environment) close() {
+	env.fileCache.close()
+	env.tracer.close()
+}
+
 func cleanHostName(hostName string) string {
 	garbage := []string{
 		".lan",
diff --git a/src/environment_cache.go b/src/environment_cache.go
new file mode 100644
index 00000000..94e9ffc8
--- /dev/null
+++ b/src/environment_cache.go
@@ -0,0 +1,71 @@
+package main
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"time"
+)
+
+const (
+	cachePath = "/.omp.cache"
+)
+
+type cacheObject struct {
+	Value     string `json:"value"`
+	Timestamp int64  `json:"timestamp"`
+	TTL       int64  `json:"ttl"`
+}
+
+type fileCache struct {
+	cache *concurrentMap
+	home  string
+}
+
+func (fc *fileCache) init(home string) {
+	fc.cache = newConcurrentMap()
+	fc.home = home
+	content, err := ioutil.ReadFile(fc.home + cachePath)
+	if err != nil {
+		return
+	}
+
+	var list map[string]*cacheObject
+	if err := json.Unmarshal(content, &list); err != nil {
+		return
+	}
+
+	for key, co := range list {
+		fc.cache.set(key, co)
+	}
+}
+
+func (fc *fileCache) close() {
+	if dump, err := json.MarshalIndent(fc.cache.list(), "", "    "); err == nil {
+		_ = ioutil.WriteFile(fc.home+cachePath, dump, 0644)
+	}
+}
+
+func (fc *fileCache) get(key string) (string, bool) {
+	val, found := fc.cache.get(key)
+	if !found {
+		return "", false
+	}
+	co, ok := val.(*cacheObject)
+	if !ok {
+		return "", false
+	}
+	expired := time.Now().Unix() >= (co.Timestamp + co.TTL*60)
+	if expired {
+		fc.cache.remove(key)
+		return "", false
+	}
+	return co.Value, true
+}
+
+func (fc *fileCache) set(key, value string, ttl int64) {
+	fc.cache.set(key, &cacheObject{
+		Value:     value,
+		Timestamp: time.Now().Unix(),
+		TTL:       ttl,
+	})
+}
diff --git a/src/main.go b/src/main.go
index 2528ff13..47b57232 100644
--- a/src/main.go
+++ b/src/main.go
@@ -160,7 +160,7 @@ func main() {
 	flag.Parse()
 	env := &environment{}
 	env.init(args)
-	defer env.tracer.close()
+	defer env.close()
 	if *args.Millis {
 		fmt.Print(time.Now().UnixNano() / 1000000)
 		return


Test patch:
diff --git a/src/environment_cache_mock_test.go b/src/environment_cache_mock_test.go
new file mode 100644
index 00000000..b6bf1e0c
--- /dev/null
+++ b/src/environment_cache_mock_test.go
@@ -0,0 +1,44 @@
+package main
+
+import mock "github.com/stretchr/testify/mock"
+
+// MockedCache is an autogenerated mock type for the cache type
+type MockedCache struct {
+	mock.Mock
+}
+
+// close provides a mock function with given fields:
+func (_m *MockedCache) close() {
+	_m.Called()
+}
+
+// get provides a mock function with given fields: key
+func (_m *MockedCache) get(key string) (string, bool) {
+	ret := _m.Called(key)
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func(string) string); ok {
+		r0 = rf(key)
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	var r1 bool
+	if rf, ok := ret.Get(1).(func(string) bool); ok {
+		r1 = rf(key)
+	} else {
+		r1 = ret.Get(1).(bool)
+	}
+
+	return r0, r1
+}
+
+// init provides a mock function with given fields: home
+func (_m *MockedCache) init(home string) {
+	_m.Called(home)
+}
+
+// set provides a mock function with given fields: key, value, ttl
+func (_m *MockedCache) set(key, value string, ttl int64) {
+	_m.Called(key, value, ttl)
+}
diff --git a/src/segment_path_test.go b/src/segment_path_test.go
index a4016c93..c626a7c6 100644
--- a/src/segment_path_test.go
+++ b/src/segment_path_test.go
@@ -153,6 +153,15 @@ func (env *MockedEnvironment) getTerminalWidth() (int, error) {
 	return args.Int(0), args.Error(1)
 }
 
+func (env *MockedEnvironment) cache() cache {
+	args := env.Called(nil)
+	return args.Get(0).(cache)
+}
+
+func (env *MockedEnvironment) close() {
+	_ = env.Called(nil)
+}
+
 const (
 	homeBill        = "/home/bill"
 	homeJan         = "/usr/home/jan"