Repository: JanDeDobbeleer/oh-my-posh
Pull request #3551 (2023-03-03 07:36:05)

feat: weekly upgrade notice
feat: weekly upgrade notice

---
Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/JanDeDobbeleer/oh-my-posh/pull/3551).
* __->__ #3551



Patch:
diff --git a/src/cli/cache.go b/src/cli/cache.go
index f8f804c0..7f74e2ae 100644
--- a/src/cli/cache.go
+++ b/src/cli/cache.go
@@ -35,7 +35,9 @@ You can do the following:
 			return
 		}
 		env := &platform.Shell{
-			Version: cliVersion,
+			CmdFlags: &platform.Flags{
+				Version: cliVersion,
+			},
 		}
 		env.Init()
 		defer env.Close()
diff --git a/src/cli/config_export.go b/src/cli/config_export.go
index ae8e0206..6c0bef12 100644
--- a/src/cli/config_export.go
+++ b/src/cli/config_export.go
@@ -35,9 +35,9 @@ Exports the ~/myconfig.omp.json config file to toml and prints the result to std
 	Args: cobra.NoArgs,
 	Run: func(cmd *cobra.Command, args []string) {
 		env := &platform.Shell{
-			Version: cliVersion,
 			CmdFlags: &platform.Flags{
-				Config: config,
+				Config:  config,
+				Version: cliVersion,
 			},
 		}
 		env.Init()
diff --git a/src/cli/config_export_image.go b/src/cli/config_export_image.go
index 14d5abe3..74d69c1d 100644
--- a/src/cli/config_export_image.go
+++ b/src/cli/config_export_image.go
@@ -48,10 +48,10 @@ Exports the config to an image file using customized output options.`,
 	Args: cobra.NoArgs,
 	Run: func(cmd *cobra.Command, args []string) {
 		env := &platform.Shell{
-			Version: cliVersion,
 			CmdFlags: &platform.Flags{
-				Config: config,
-				Shell:  shell.GENERIC,
+				Config:  config,
+				Shell:   shell.GENERIC,
+				Version: cliVersion,
 			},
 		}
 		env.Init()
diff --git a/src/cli/config_migrate.go b/src/cli/config_migrate.go
index 0b6470b9..eeb20052 100644
--- a/src/cli/config_migrate.go
+++ b/src/cli/config_migrate.go
@@ -40,10 +40,10 @@ A backup of the current config can be found at ~/myconfig.omp.json.bak.`,
 	Args: cobra.NoArgs,
 	Run: func(cmd *cobra.Command, args []string) {
 		env := &platform.Shell{
-			Version: cliVersion,
 			CmdFlags: &platform.Flags{
 				Config:  config,
 				Migrate: true,
+				Version: cliVersion,
 			},
 		}
 		env.Init()
diff --git a/src/cli/config_migrate_glyphs.go b/src/cli/config_migrate_glyphs.go
index 3c28e43c..463c613f 100644
--- a/src/cli/config_migrate_glyphs.go
+++ b/src/cli/config_migrate_glyphs.go
@@ -35,9 +35,9 @@ A backup of the current config can be found at ~/myconfig.omp.json.bak.`,
 	Args: cobra.NoArgs,
 	Run: func(cmd *cobra.Command, args []string) {
 		env := &platform.Shell{
-			Version: cliVersion,
 			CmdFlags: &platform.Flags{
-				Config: config,
+				Config:  config,
+				Version: cliVersion,
 			},
 		}
 
diff --git a/src/cli/debug.go b/src/cli/debug.go
index a30fd93a..28d99d8e 100644
--- a/src/cli/debug.go
+++ b/src/cli/debug.go
@@ -21,12 +21,12 @@ var debugCmd = &cobra.Command{
 	Run: func(cmd *cobra.Command, args []string) {
 		startTime := time.Now()
 		env := &platform.Shell{
-			Version: cliVersion,
 			CmdFlags: &platform.Flags{
-				Config: config,
-				Debug:  true,
-				PWD:    pwd,
-				Shell:  shellName,
+				Config:  config,
+				Debug:   true,
+				PWD:     pwd,
+				Shell:   shellName,
+				Version: cliVersion,
 			},
 		}
 		env.Init()
diff --git a/src/cli/get.go b/src/cli/get.go
index bcfdab47..72f6d736 100644
--- a/src/cli/get.go
+++ b/src/cli/get.go
@@ -39,9 +39,9 @@ This command is used to get the value of the following variables:
 			return
 		}
 		env := &platform.Shell{
-			Version: cliVersion,
 			CmdFlags: &platform.Flags{
-				Shell: shellName,
+				Shell:   shellName,
+				Version: cliVersion,
 			},
 		}
 		env.Init()
diff --git a/src/cli/init.go b/src/cli/init.go
index 6f28a6c6..b2da8f94 100644
--- a/src/cli/init.go
+++ b/src/cli/init.go
@@ -54,12 +54,12 @@ func init() { //nolint:gochecknoinits
 
 func runInit(shellName string) {
 	env := &platform.Shell{
-		Version: cliVersion,
 		CmdFlags: &platform.Flags{
-			Shell:  shellName,
-			Config: config,
-			Strict: strict,
-			Manual: manual,
+			Shell:   shellName,
+			Config:  config,
+			Strict:  strict,
+			Manual:  manual,
+			Version: cliVersion,
 		},
 	}
 	env.Init()
diff --git a/src/platform/shell.go b/src/platform/shell.go
index f96d0df1..d2031dad 100644
--- a/src/platform/shell.go
+++ b/src/platform/shell.go
@@ -70,6 +70,7 @@ type Flags struct {
 	Primary       bool
 	PromptCount   int
 	Cleared       bool
+	Version       string
 }
 
 type CommandError struct {
@@ -90,8 +91,12 @@ type FileInfo struct {
 type Cache interface {
 	Init(home string)
 	Close()
+	// Gets the value for a given key.
+	// Returns the value and a boolean indicating if the key was found.
+	// In case the ttl expired, the function returns false.
 	Get(key string) (string, bool)
-	// ttl in minutes
+	// Sets a value for a given key.
+	// The ttl indicates how may minutes to cache the value.
 	Set(key, value string, ttl int)
 }
 
@@ -271,7 +276,6 @@ func (c *commandCache) get(command string) (string, bool) {
 
 type Shell struct {
 	CmdFlags *Flags
-	Version  string
 	Var      map[string]interface{}
 
 	cwd       string
diff --git a/src/shell/init.go b/src/shell/init.go
index 980d5761..016cf32a 100644
--- a/src/shell/init.go
+++ b/src/shell/init.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/jandedobbeleer/oh-my-posh/src/platform"
 	"github.com/jandedobbeleer/oh-my-posh/src/template"
+	"github.com/jandedobbeleer/oh-my-posh/src/upgrade"
 )
 
 //go:embed scripts/omp.ps1
@@ -250,6 +251,7 @@ func PrintInit(env platform.Environment) string {
 	default:
 		return fmt.Sprintf("echo \"No initialization script available for %s\"", shell)
 	}
+	notice, hasNotice := upgrade.Notice(env)
 	return strings.NewReplacer(
 		"::OMP::", executable,
 		"::CONFIG::", configFile,
@@ -259,6 +261,8 @@ func PrintInit(env platform.Environment) string {
 		"::TOOLTIPS::", toggleSetting(Tooltips),
 		"::RPROMPT::", strconv.FormatBool(RPrompt),
 		"::CURSOR::", strconv.FormatBool(Cursor),
+		"::UPGRADE::", strconv.FormatBool(hasNotice),
+		"::UPGRADENOTICE::", notice,
 	).Replace(script)
 }
 
diff --git a/src/shell/scripts/omp.bash b/src/shell/scripts/omp.bash
index 374120ee..2f5458d6 100644
--- a/src/shell/scripts/omp.bash
+++ b/src/shell/scripts/omp.bash
@@ -56,3 +56,7 @@ function _omp_hook() {
 if [ "$TERM" != "linux" ] && [ -x "$(command -v ::OMP::)" ] && ! [[ "$PROMPT_COMMAND" =~ "_omp_hook" ]]; then
     PROMPT_COMMAND="_omp_hook; $PROMPT_COMMAND"
 fi
+
+if [ "::UPGRADE::" == "true" ]; then
+    echo "::UPGRADENOTICE::"
+fi
diff --git a/src/shell/scripts/omp.elv b/src/shell/scripts/omp.elv
index 2c7eb20f..3f9d7d36 100644
--- a/src/shell/scripts/omp.elv
+++ b/src/shell/scripts/omp.elv
@@ -30,3 +30,7 @@ set edit:rprompt = {
     var cmd-duration = (printf "%.0f" (* $edit:command-duration 1000))
     ::OMP:: print right --config=$E:POSH_THEME --shell=elvish --execution-time=$cmd-duration --error=$error-code --pwd=$pwd --shell-version=$E:ELVISH_VERSION
 }
+
+if (eq '::UPGRADE::' 'true') {
+    echo '::UPGRADENOTICE::'
+}
diff --git a/src/shell/scripts/omp.fish b/src/shell/scripts/omp.fish
index b4448cab..73ead916 100644
--- a/src/shell/scripts/omp.fish
+++ b/src/shell/scripts/omp.fish
@@ -126,3 +126,7 @@ end
 function enable_poshtransientprompt
   return
 end
+
+if test "::UPGRADE::" = "true"
+  echo "::UPGRADENOTICE::"
+end
diff --git a/src/shell/scripts/omp.lua b/src/shell/scripts/omp.lua
index a9fdad6a..03b6189b 100644
--- a/src/shell/scripts/omp.lua
+++ b/src/shell/scripts/omp.lua
@@ -1,3 +1,11 @@
+-- Upgrade notice
+
+local notice = [[::UPGRADENOTICE::]]
+
+if '::UPGRADE::' == 'true' then
+    print(notice)
+end
+
 -- Helper functions
 
 local function get_priority_number(name, default)
diff --git a/src/shell/scripts/omp.nu b/src/shell/scripts/omp.nu
index ef9fe835..15ea0d85 100644
--- a/src/shell/scripts/omp.nu
+++ b/src/shell/scripts/omp.nu
@@ -26,3 +26,7 @@ export-env {
         ^::OMP:: print primary $"--config=($env.POSH_THEME)" --shell=nu $"--shell-version=($env.NU_VERSION)" $"--execution-time=($cmd_duration)" $"--error=($env.LAST_EXIT_CODE)" $"--terminal-width=($width)" $"--cleared=($clear)"
     }
 }
+
+if "::UPGRADE::" == "true" {
+    echo "::UPGRADENOTICE::"
+}
diff --git a/src/shell/scripts/omp.ps1 b/src/shell/scripts/omp.ps1
index 199d582c..6a6df2b8 100644
--- a/src/shell/scripts/omp.ps1
+++ b/src/shell/scripts/omp.ps1
@@ -24,10 +24,17 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
     $env:POWERLINE_COMMAND = "oh-my-posh"
     $env:POSH_PID = $PID
     $env:CONDA_PROMPT_MODIFIER = $false
+
+    # set the default theme
     if ((::CONFIG:: -ne '') -and (Test-Path -LiteralPath ::CONFIG::)) {
         $env:POSH_THEME = (Resolve-Path -Path ::CONFIG::).ProviderPath
     }
 
+    # print upgrade notice
+    if ("::UPGRADE::" -eq "true") {
+        Write-Host "::UPGRADENOTICE::"
+    }
+
     # specific module support (disabled by default)
     if (($null -eq $env:POSH_GIT_ENABLED) -or ($null -eq (Get-Module 'posh-git'))) {
         $env:POSH_GIT_ENABLED = $false
diff --git a/src/shell/scripts/omp.py b/src/shell/scripts/omp.py
index d2969eb0..9904d97c 100644
--- a/src/shell/scripts/omp.py
+++ b/src/shell/scripts/omp.py
@@ -21,3 +21,8 @@ def posh_right():
 
 $PROMPT = posh_primary
 $RIGHT_PROMPT = posh_right
+
+notice = """::UPGRADENOTICE::"""
+
+if "::UPGRADE::" == "true":
+  print(notice)
diff --git a/src/shell/scripts/omp.tcsh b/src/shell/scripts/omp.tcsh
index fa3f727f..639c2001 100644
--- a/src/shell/scripts/omp.tcsh
+++ b/src/shell/scripts/omp.tcsh
@@ -9,3 +9,4 @@ alias precmd "$POSH_PRECMD;$USER_PRECMD";
 alias postcmd "$POSH_POSTCMD;$USER_POSTCMD";
 set POSH_START_TIME = `::OMP:: get millis`;
 
+if ("::UPGRADE::" == "true") echo "::UPGRADENOTICE::"
diff --git a/src/shell/scripts/omp.zsh b/src/shell/scripts/omp.zsh
index a2d0ca1a..512db0f4 100644
--- a/src/shell/scripts/omp.zsh
+++ b/src/shell/scripts/omp.zsh
@@ -139,3 +139,7 @@ fi
 if [[ "::TRANSIENT::" = "true" ]]; then
   enable_poshtransientprompt
 fi
+
+if [[ "::UPGRADE::" = "true" ]]; then
+    echo "::UPGRADENOTICE::"
+fi
diff --git a/src/upgrade/notice.go b/src/upgrade/notice.go
new file mode 100644
index 00000000..96e70a24
--- /dev/null
+++ b/src/upgrade/notice.go
@@ -0,0 +1,90 @@
+package upgrade
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/jandedobbeleer/oh-my-posh/src/platform"
+)
+
+type release struct {
+	URL             string    `json:"url"`
+	HTMLURL         string    `json:"html_url"`
+	AssetsURL       string    `json:"assets_url"`
+	UploadURL       string    `json:"upload_url"`
+	TarballURL      string    `json:"tarball_url"`
+	ZipballURL      string    `json:"zipball_url"`
+	DiscussionURL   string    `json:"discussion_url"`
+	ID              int       `json:"id"`
+	NodeID          string    `json:"node_id"`
+	TagName         string    `json:"tag_name"`
+	TargetCommitish string    `json:"target_commitish"`
+	Name            string    `json:"name"`
+	Body            string    `json:"body"`
+	Draft           bool      `json:"draft"`
+	Prerelease      bool      `json:"prerelease"`
+	CreatedAt       time.Time `json:"created_at"`
+	PublishedAt     time.Time `json:"published_at"`
+}
+
+const (
+	releaseURL    = "https://api.github.com/repos/jandedobbeleer/oh-my-posh/releases/latest"
+	upgradeNotice = `
+A new release of Oh My Posh is available: %s → %s
+%s
+`
+	windows = `To upgrade, use the guide for your favorite package manager in the documentation:
+https://ohmyposh.dev/docs/installation/windows#update`
+	unix   = "To upgrade, use your favorite package manager or, if you used Homebrew to install, run: 'brew upgrade oh-my-posh'"
+	darwin = "To upgrade, run: 'brew upgrade oh-my-posh'"
+
+	UPGRADECACHEKEY = "upgrade_check"
+)
+
+func getLatestVersion(env platform.Environment) (string, error) {
+	body, err := env.HTTPRequest(releaseURL, nil, 1000)
+	if err != nil {
+		return "", err
+	}
+	var release release
+	// this can't fail
+	_ = json.Unmarshal(body, &release)
+	return release.TagName, nil
+}
+
+// Returns the upgrade notice if a new version is available
+// that should be displayed to the user.
+//
+// The upgrade check is only performed every other week.
+func Notice(env platform.Environment) (string, bool) {
+	// do not check when last validation was < 1 week ago
+	if _, OK := env.Cache().Get(UPGRADECACHEKEY); OK {
+		return "", false
+	}
+
+	latest, err := getLatestVersion(env)
+	if err != nil {
+		return "", false
+	}
+
+	oneWeek := 10080
+	env.Cache().Set(UPGRADECACHEKEY, latest, oneWeek)
+
+	version := fmt.Sprintf("v%s", env.Flags().Version)
+	if latest == version {
+		return "", false
+	}
+
+	var notice string
+	switch env.GOOS() {
+	case platform.WINDOWS:
+		notice = windows
+	case platform.DARWIN:
+		notice = darwin
+	case platform.LINUX:
+		notice = unix
+	}
+
+	return fmt.Sprintf(upgradeNotice, version, latest, notice), true
+}


Test patch:
diff --git a/src/upgrade/notice_test.go b/src/upgrade/notice_test.go
new file mode 100644
index 00000000..6bb69324
--- /dev/null
+++ b/src/upgrade/notice_test.go
@@ -0,0 +1,49 @@
+package upgrade
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/jandedobbeleer/oh-my-posh/src/mock"
+	"github.com/jandedobbeleer/oh-my-posh/src/platform"
+	"github.com/stretchr/testify/assert"
+
+	mock2 "github.com/stretchr/testify/mock"
+)
+
+func TestCanUpgrade(t *testing.T) {
+	cases := []struct {
+		Case           string
+		Expected       bool
+		CurrentVersion string
+		LatestVersion  string
+		Error          error
+		Cache          bool
+		GOOS           string
+	}{
+		{Case: "Up to date", CurrentVersion: "3.0.0", LatestVersion: "v3.0.0"},
+		{Case: "Oudated Windows", Expected: true, CurrentVersion: "3.0.0", LatestVersion: "v3.0.1", GOOS: platform.WINDOWS},
+		{Case: "Oudated Linux", Expected: true, CurrentVersion: "3.0.0", LatestVersion: "v3.0.1", GOOS: platform.LINUX},
+		{Case: "Oudated Darwin", Expected: true, CurrentVersion: "3.0.0", LatestVersion: "v3.0.1", GOOS: platform.DARWIN},
+		{Case: "Cached", Cache: true},
+		{Case: "Error", Error: fmt.Errorf("error")},
+	}
+
+	for _, tc := range cases {
+		env := new(mock.MockedEnvironment)
+		env.On("Flags").Return(&platform.Flags{
+			Version: tc.CurrentVersion,
+		})
+		cache := &mock.MockedCache{}
+		cache.On("Get", UPGRADECACHEKEY).Return("", tc.Cache)
+		cache.On("Set", mock2.Anything, mock2.Anything, mock2.Anything)
+		env.On("Cache").Return(cache)
+		env.On("GOOS").Return(tc.GOOS)
+
+		json := fmt.Sprintf(`{"tag_name":"%s"}`, tc.LatestVersion)
+		env.On("HTTPRequest", releaseURL).Return([]byte(json), tc.Error)
+		// ignore the notice
+		_, canUpgrade := Notice(env)
+		assert.Equal(t, tc.Expected, canUpgrade, tc.Case)
+	}
+}