feat(upgrade): select source

This commit is contained in:
Jan De Dobbeleer 2024-12-01 17:13:21 +01:00 committed by Jan De Dobbeleer
parent 6068b56e40
commit 06a372424f
17 changed files with 318 additions and 215 deletions

View file

@ -2,9 +2,10 @@ package cli
import ( import (
"fmt" "fmt"
"os"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime"
"github.com/jandedobbeleer/oh-my-posh/src/upgrade"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -23,7 +24,12 @@ var noticeCmd = &cobra.Command{
env.Init(flags) env.Init(flags)
defer env.Close() defer env.Close()
if notice, hasNotice := upgrade.Notice(env, false); hasNotice { sh := os.Getenv("POSH_SHELL")
configFile := config.Path(configFlag)
cfg := config.Load(configFile, sh, false)
cfg.Upgrade.Cache = env.Cache()
if notice, hasNotice := cfg.Upgrade.Notice(); hasNotice {
fmt.Println(notice) fmt.Println(notice)
} }
}, },

View file

@ -7,6 +7,7 @@ import (
"slices" "slices"
"github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/jandedobbeleer/oh-my-posh/src/build"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime"
"github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/jandedobbeleer/oh-my-posh/src/terminal"
"github.com/jandedobbeleer/oh-my-posh/src/upgrade" "github.com/jandedobbeleer/oh-my-posh/src/upgrade"
@ -32,36 +33,42 @@ var upgradeCmd = &cobra.Command{
return return
} }
sh := os.Getenv("POSH_SHELL")
env := &runtime.Terminal{} env := &runtime.Terminal{}
env.Init(nil) env.Init(nil)
defer env.Close() defer env.Close()
terminal.Init(env.Shell()) terminal.Init(sh)
fmt.Print(terminal.StartProgress()) fmt.Print(terminal.StartProgress())
latest, err := upgrade.Latest(env) configFile := config.Path(configFlag)
cfg := config.Load(configFile, sh, false)
cfg.Upgrade.Cache = env.Cache()
latest, err := cfg.Upgrade.Latest()
if err != nil { if err != nil {
fmt.Printf("\n❌ %s\n\n%s", err, terminal.StopProgress()) fmt.Printf("\n❌ %s\n\n%s", err, terminal.StopProgress())
os.Exit(1) os.Exit(1)
return return
} }
cfg.Upgrade.Version = fmt.Sprintf("v%s", latest)
if force { if force {
executeUpgrade(latest) executeUpgrade(cfg.Upgrade)
return return
} }
version := fmt.Sprintf("v%s", build.Version) if upgrade.IsMajorUpgrade(build.Version, latest) {
if upgrade.IsMajorUpgrade(version, latest) {
message := terminal.StopProgress() message := terminal.StopProgress()
message += fmt.Sprintf("\n🚨 major upgrade available: %s -> %s, use oh-my-posh upgrade --force to upgrade\n\n", version, latest) message += fmt.Sprintf("\n🚨 major upgrade available: v%s -> v%s, use oh-my-posh upgrade --force to upgrade\n\n", build.Version, latest)
fmt.Print(message) fmt.Print(message)
return return
} }
if version != latest { if build.Version != latest {
executeUpgrade(latest) executeUpgrade(cfg.Upgrade)
return return
} }
@ -69,8 +76,8 @@ var upgradeCmd = &cobra.Command{
}, },
} }
func executeUpgrade(latest string) { func executeUpgrade(cfg *upgrade.Config) {
err := upgrade.Run(latest) err := upgrade.Run(cfg)
fmt.Print(terminal.StopProgress()) fmt.Print(terminal.StopProgress())
if err == nil { if err == nil {
return return

View file

@ -7,6 +7,7 @@ import (
"github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/template"
"github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/jandedobbeleer/oh-my-posh/src/terminal"
"github.com/jandedobbeleer/oh-my-posh/src/upgrade"
) )
const ( const (
@ -30,26 +31,27 @@ type Config struct {
SecondaryPrompt *Segment `json:"secondary_prompt,omitempty" toml:"secondary_prompt,omitempty"` SecondaryPrompt *Segment `json:"secondary_prompt,omitempty" toml:"secondary_prompt,omitempty"`
TransientPrompt *Segment `json:"transient_prompt,omitempty" toml:"transient_prompt,omitempty"` TransientPrompt *Segment `json:"transient_prompt,omitempty" toml:"transient_prompt,omitempty"`
ErrorLine *Segment `json:"error_line,omitempty" toml:"error_line,omitempty"` ErrorLine *Segment `json:"error_line,omitempty" toml:"error_line,omitempty"`
ConsoleTitleTemplate string `json:"console_title_template,omitempty" toml:"console_title_template,omitempty"` TerminalBackground color.Ansi `json:"terminal_background,omitempty" toml:"terminal_background,omitempty"`
Format string `json:"-" toml:"-"`
origin string origin string
PWD string `json:"pwd,omitempty" toml:"pwd,omitempty"` PWD string `json:"pwd,omitempty" toml:"pwd,omitempty"`
AccentColor color.Ansi `json:"accent_color,omitempty" toml:"accent_color,omitempty"` AccentColor color.Ansi `json:"accent_color,omitempty" toml:"accent_color,omitempty"`
Output string `json:"-" toml:"-"` Output string `json:"-" toml:"-"`
TerminalBackground color.Ansi `json:"terminal_background,omitempty" toml:"terminal_background,omitempty"` ConsoleTitleTemplate string `json:"console_title_template,omitempty" toml:"console_title_template,omitempty"`
Format string `json:"-" toml:"-"`
Upgrade *upgrade.Config `json:"upgrade,omitempty" toml:"upgrade,omitempty"`
Cycle color.Cycle `json:"cycle,omitempty" toml:"cycle,omitempty"` Cycle color.Cycle `json:"cycle,omitempty" toml:"cycle,omitempty"`
ITermFeatures terminal.ITermFeatures `json:"iterm_features,omitempty" toml:"iterm_features,omitempty"` ITermFeatures terminal.ITermFeatures `json:"iterm_features,omitempty" toml:"iterm_features,omitempty"`
Blocks []*Block `json:"blocks,omitempty" toml:"blocks,omitempty"` Blocks []*Block `json:"blocks,omitempty" toml:"blocks,omitempty"`
Tooltips []*Segment `json:"tooltips,omitempty" toml:"tooltips,omitempty"` Tooltips []*Segment `json:"tooltips,omitempty" toml:"tooltips,omitempty"`
Version int `json:"version" toml:"version"` Version int `json:"version" toml:"version"`
UpgradeNotice bool `json:"upgrade_notice,omitempty" toml:"upgrade_notice,omitempty"` AutoUpgrade bool `json:"-" toml:"-"`
AutoUpgrade bool `json:"auto_upgrade,omitempty" toml:"auto_upgrade,omitempty"`
ShellIntegration bool `json:"shell_integration,omitempty" toml:"shell_integration,omitempty"` ShellIntegration bool `json:"shell_integration,omitempty" toml:"shell_integration,omitempty"`
MigrateGlyphs bool `json:"-" toml:"-"` MigrateGlyphs bool `json:"-" toml:"-"`
PatchPwshBleed bool `json:"patch_pwsh_bleed,omitempty" toml:"patch_pwsh_bleed,omitempty"` PatchPwshBleed bool `json:"patch_pwsh_bleed,omitempty" toml:"patch_pwsh_bleed,omitempty"`
EnableCursorPositioning bool `json:"enable_cursor_positioning,omitempty" toml:"enable_cursor_positioning,omitempty"` EnableCursorPositioning bool `json:"enable_cursor_positioning,omitempty" toml:"enable_cursor_positioning,omitempty"`
updated bool updated bool
FinalSpace bool `json:"final_space,omitempty" toml:"final_space,omitempty"` FinalSpace bool `json:"final_space,omitempty" toml:"final_space,omitempty"`
UpgradeNotice bool `json:"-" toml:"-"`
} }
func (cfg *Config) MakeColors(env runtime.Environment) color.String { func (cfg *Config) MakeColors(env runtime.Environment) color.String {
@ -98,12 +100,12 @@ func (cfg *Config) Features(env runtime.Environment) shell.Features {
feats = append(feats, shell.FTCSMarks) feats = append(feats, shell.FTCSMarks)
} }
autoUpgrade := cfg.AutoUpgrade autoUpgrade := cfg.Upgrade.Auto
if _, OK := env.Cache().Get(AUTOUPGRADE); OK { if _, OK := env.Cache().Get(AUTOUPGRADE); OK {
autoUpgrade = true autoUpgrade = true
} }
upgradeNotice := cfg.UpgradeNotice upgradeNotice := cfg.Upgrade.DisplayNotice
if _, OK := env.Cache().Get(UPGRADENOTICE); OK { if _, OK := env.Cache().Get(UPGRADENOTICE); OK {
upgradeNotice = true upgradeNotice = true
} }

View file

@ -15,6 +15,7 @@ import (
"github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/log"
"github.com/jandedobbeleer/oh-my-posh/src/runtime/path" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path"
"github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/upgrade"
json "github.com/goccy/go-json" json "github.com/goccy/go-json"
yaml "github.com/goccy/go-yaml" yaml "github.com/goccy/go-yaml"
@ -32,6 +33,19 @@ func Load(configFile, sh string, migrate bool) *Config {
cfg.BackupAndMigrate() cfg.BackupAndMigrate()
} }
if cfg.Upgrade == nil {
cfg.Upgrade = &upgrade.Config{
Source: upgrade.CDN,
DisplayNotice: cfg.UpgradeNotice,
Auto: cfg.AutoUpgrade,
Interval: cache.ONEWEEK,
}
}
if cfg.Upgrade.Interval.IsEmpty() {
cfg.Upgrade.Interval = cache.ONEWEEK
}
if !cfg.ShellIntegration { if !cfg.ShellIntegration {
return cfg return cfg
} }

View file

@ -68,23 +68,29 @@ func (u *Upgrade) cachedLatest(current string) (*UpgradeCache, error) {
} }
func (u *Upgrade) checkUpdate(current string) (*UpgradeCache, error) { func (u *Upgrade) checkUpdate(current string) (*UpgradeCache, error) {
tag, err := upgrade.Latest(u.env) duration := u.props.GetString(properties.CacheDuration, string(cache.ONEWEEK))
source := u.props.GetString(Source, string(upgrade.CDN))
cfg := &upgrade.Config{
Source: upgrade.Source(source),
Interval: cache.Duration(duration),
}
latest, err := cfg.Latest()
if err != nil { if err != nil {
return nil, err return nil, err
} }
latest := tag[1:]
cacheData := &UpgradeCache{ cacheData := &UpgradeCache{
Latest: latest, Latest: latest,
Current: current, Current: current,
} }
cacheJSON, err := json.Marshal(cacheData) cacheJSON, err := json.Marshal(cacheData)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// update cache
duration := u.props.GetString(properties.CacheDuration, string(cache.ONEWEEK))
u.env.Cache().Set(UPGRADECACHEKEY, string(cacheJSON), cache.Duration(duration)) u.env.Cache().Set(UPGRADECACHEKEY, string(cacheJSON), cache.Duration(duration))
return cacheData, nil return cacheData, nil

View file

@ -1,7 +1,6 @@
package segments package segments
import ( import (
"errors"
"fmt" "fmt"
"testing" "testing"
@ -16,8 +15,10 @@ import (
) )
func TestUpgrade(t *testing.T) { func TestUpgrade(t *testing.T) {
ugc := &upgrade.Config{}
latest, _ := ugc.Latest()
cases := []struct { cases := []struct {
Error error
Case string Case string
CurrentVersion string CurrentVersion string
LatestVersion string LatestVersion string
@ -33,33 +34,28 @@ func TestUpgrade(t *testing.T) {
}, },
{ {
Case: "On latest", Case: "On latest",
CurrentVersion: "1.0.1", CurrentVersion: latest,
LatestVersion: "1.0.1",
},
{
Case: "Error on update check",
Error: errors.New("error"),
}, },
{ {
Case: "On previous, from cache", Case: "On previous, from cache",
HasCache: true, HasCache: true,
CurrentVersion: "1.0.2", CurrentVersion: "1.0.2",
LatestVersion: "1.0.3", LatestVersion: latest,
CachedVersion: "1.0.2", CachedVersion: "1.0.2",
ExpectedEnabled: true, ExpectedEnabled: true,
}, },
{ {
Case: "On latest, version changed", Case: "On latest, version changed",
HasCache: true, HasCache: true,
CurrentVersion: "1.0.2", CurrentVersion: latest,
LatestVersion: "1.0.2", LatestVersion: latest,
CachedVersion: "1.0.1", CachedVersion: "1.0.1",
}, },
{ {
Case: "On previous, version changed", Case: "On previous, version changed",
HasCache: true, HasCache: true,
CurrentVersion: "1.0.2", CurrentVersion: "1.0.2",
LatestVersion: "1.0.3", LatestVersion: latest,
CachedVersion: "1.0.1", CachedVersion: "1.0.1",
ExpectedEnabled: true, ExpectedEnabled: true,
}, },
@ -73,15 +69,13 @@ func TestUpgrade(t *testing.T) {
if len(tc.CachedVersion) == 0 { if len(tc.CachedVersion) == 0 {
tc.CachedVersion = tc.CurrentVersion tc.CachedVersion = tc.CurrentVersion
} }
cacheData := fmt.Sprintf(`{"latest":"%s", "current": "%s"}`, tc.LatestVersion, tc.CachedVersion) cacheData := fmt.Sprintf(`{"latest":"%s", "current": "%s"}`, tc.LatestVersion, tc.CachedVersion)
cache.On("Get", UPGRADECACHEKEY).Return(cacheData, tc.HasCache) cache.On("Get", UPGRADECACHEKEY).Return(cacheData, tc.HasCache)
cache.On("Set", testify_.Anything, testify_.Anything, testify_.Anything) cache.On("Set", testify_.Anything, testify_.Anything, testify_.Anything)
build.Version = tc.CurrentVersion build.Version = tc.CurrentVersion
json := fmt.Sprintf(`{"tag_name":"v%s"}`, tc.LatestVersion)
env.On("HTTPRequest", upgrade.RELEASEURL).Return([]byte(json), tc.Error)
ug := &Upgrade{} ug := &Upgrade{}
ug.Init(properties.Map{}, env) ug.Init(properties.Map{}, env)

View file

@ -39,42 +39,42 @@ type stateMsg state
type model struct { type model struct {
error error error error
config *Config
message string message string
tag string
spinner spinner.Model spinner spinner.Model
state state state state
} }
func initialModel(tag string) *model { func initialModel(cfg *Config) *model {
s := spinner.New() s := spinner.New()
s.Spinner = spinner.Dot s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("170")) s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("170"))
return &model{spinner: s, tag: tag} return &model{spinner: s, config: cfg}
} }
func (m *model) Init() tea.Cmd { func (m *model) Init() tea.Cmd {
defer func() { go m.start()
go func() {
if err := install(m.tag); err != nil {
m.error = err
program.Send(resultMsg(fmt.Sprintf("❌ upgrade failed: %v", err)))
return
}
message := "🚀 Upgrade successful"
current := fmt.Sprintf("v%s", build.Version)
if current != m.tag {
message += ", restart your shell to take full advantage of the new functionality"
}
program.Send(resultMsg(message))
}()
}()
return m.spinner.Tick return m.spinner.Tick
} }
func (m *model) start() {
if err := install(m.config); err != nil {
m.error = err
program.Send(resultMsg(fmt.Sprintf("❌ upgrade failed: %v", err)))
return
}
message := "🚀 Upgrade successful"
current := fmt.Sprintf("v%s", build.Version)
if current != m.config.Version {
message += ", restart your shell to take full advantage of the new functionality"
}
program.Send(resultMsg(message))
}
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
@ -113,7 +113,7 @@ func (m *model) View() string {
message = "Validating current installation" message = "Validating current installation"
case downloading: case downloading:
m.spinner.Spinner = spinner.Globe m.spinner.Spinner = spinner.Globe
message = "Downloading latest version" message = fmt.Sprintf("Downloading latest version from %s", m.config.Source.String())
case verifying: case verifying:
m.spinner.Spinner = spinner.Moon m.spinner.Spinner = spinner.Moon
message = "Verifying download" message = "Verifying download"
@ -125,19 +125,19 @@ func (m *model) View() string {
return title + textStyle.Render(fmt.Sprintf("%s %s", m.spinner.View(), message)) return title + textStyle.Render(fmt.Sprintf("%s %s", m.spinner.View(), message))
} }
func Run(latest string) error { func Run(cfg *Config) error {
titleStyle := lipgloss.NewStyle().Margin(1, 0, 1, 0) titleStyle := lipgloss.NewStyle().Margin(1, 0, 1, 0)
title = "📦 Upgrading Oh My Posh" title = "📦 Upgrading Oh My Posh"
current := build.Version current := fmt.Sprintf("v%s", build.Version)
if len(current) == 0 { if len(current) == 0 {
current = "dev" current = "dev"
} }
title = fmt.Sprintf("%s from %s to %s", title, current, latest) title = fmt.Sprintf("%s from %s to %s", title, current, cfg.Version)
title = titleStyle.Render(title) title = titleStyle.Render(title)
program = tea.NewProgram(initialModel(latest)) program = tea.NewProgram(initialModel(cfg))
resultModel, _ := program.Run() resultModel, _ := program.Run()
programModel, OK := resultModel.(*model) programModel, OK := resultModel.(*model)

100
src/upgrade/config.go Normal file
View file

@ -0,0 +1,100 @@
package upgrade
import (
"context"
"fmt"
"io"
httplib "net/http"
"strings"
"github.com/jandedobbeleer/oh-my-posh/src/cache"
"github.com/jandedobbeleer/oh-my-posh/src/runtime/http"
)
type Config struct {
Cache cache.Cache `json:"-" toml:"-"`
Source Source `json:"source" toml:"source"`
Interval cache.Duration `json:"interval" toml:"interval"`
Version string `json:"-" toml:"-"`
Auto bool `json:"auto" toml:"auto"`
DisplayNotice bool `json:"notice" toml:"notice"`
Force bool `json:"-" toml:"-"`
}
type Source string
const (
GitHub Source = "github"
CDN Source = "cdn"
)
func (s Source) String() string {
switch s {
case GitHub:
return "github.com"
case CDN:
return "cdn.ohmyposh.dev"
default:
return "Unknown"
}
}
func (cfg *Config) Latest() (string, error) {
cfg.Version = "latest"
v, err := cfg.DownloadAsset("version.txt")
version := strings.TrimSpace(string(v))
return strings.TrimPrefix(version, "v"), err
}
func (cfg *Config) DownloadAsset(asset string) ([]byte, error) {
if len(cfg.Source) == 0 {
cfg.Source = GitHub
}
switch cfg.Source {
case GitHub:
var url string
switch cfg.Version {
case "latest":
url = fmt.Sprintf("https://github.com/JanDeDobbeleer/oh-my-posh/releases/latest/download/%s", asset)
default:
url = fmt.Sprintf("https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/%s/%s", cfg.Version, asset)
}
return cfg.Download(url)
case CDN:
fallthrough
default:
url := fmt.Sprintf("https://cdn.ohmyposh.dev/releases/%s/%s", cfg.Version, asset)
return cfg.Download(url)
}
}
func (cfg *Config) Download(url string) ([]byte, error) {
req, err := httplib.NewRequestWithContext(context.Background(), "GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Add("User-Agent", "oh-my-posh")
req.Header.Add("Cache-Control", "max-age=0")
resp, err := http.HTTPClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != httplib.StatusOK {
return nil, fmt.Errorf("failed to download asset: %s", url)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return data, nil
}

View file

@ -1,39 +0,0 @@
package upgrade
import (
"context"
"fmt"
"io"
httplib "net/http"
"github.com/jandedobbeleer/oh-my-posh/src/runtime/http"
)
func downloadReleaseAsset(tag, asset string) ([]byte, error) {
url := fmt.Sprintf("https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/%s/%s", tag, asset)
req, err := httplib.NewRequestWithContext(context.Background(), "GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Add("User-Agent", "oh-my-posh")
resp, err := http.HTTPClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != httplib.StatusOK {
return nil, fmt.Errorf("failed to download asset: %s", url)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return data, nil
}

View file

@ -9,7 +9,7 @@ import (
"path/filepath" "path/filepath"
) )
func install(tag string) error { func install(cfg *Config) error {
setState(validating) setState(validating)
executable, err := os.Executable() executable, err := os.Executable()
@ -28,7 +28,7 @@ func install(tag string) error {
setState(downloading) setState(downloading)
data, err := downloadAndVerify(tag) data, err := downloadAndVerify(cfg)
if err != nil { if err != nil {
return err return err
} }
@ -71,7 +71,7 @@ func install(tag string) error {
_ = hideFile(oldPath) _ = hideFile(oldPath)
} }
updateRegistry(tag, executable) updateRegistry(cfg.Version, executable)
return nil return nil
} }

View file

@ -5,5 +5,3 @@ package upgrade
func hideFile(_ string) error { func hideFile(_ string) error {
return nil return nil
} }
func updateRegistry(_, _ string) {}

View file

@ -1,72 +1,38 @@
package upgrade package upgrade
import ( import (
"encoding/json"
"fmt" "fmt"
"time" "os"
"github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/jandedobbeleer/oh-my-posh/src/build"
"github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/log"
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
"github.com/jandedobbeleer/oh-my-posh/src/runtime/http" "github.com/jandedobbeleer/oh-my-posh/src/runtime/http"
) )
type release struct {
CreatedAt time.Time `json:"created_at"`
PublishedAt time.Time `json:"published_at"`
NodeID string `json:"node_id"`
TagName string `json:"tag_name"`
TarballURL string `json:"tarball_url"`
ZipballURL string `json:"zipball_url"`
DiscussionURL string `json:"discussion_url"`
HTMLURL string `json:"html_url"`
URL string `json:"url"`
UploadURL string `json:"upload_url"`
TargetCommitish string `json:"target_commitish"`
Name string `json:"name"`
Body string `json:"body"`
AssetsURL string `json:"assets_url"`
ID int `json:"id"`
Prerelease bool `json:"prerelease"`
Draft bool `json:"draft"`
}
const ( const (
RELEASEURL = "https://api.github.com/repos/jandedobbeleer/oh-my-posh/releases/latest" CACHEKEY = "upgrade_check"
CACHEKEY = "upgrade_check"
upgradeNotice = ` upgradeNotice = `
A new release of Oh My Posh is available: %s %s A new release of Oh My Posh is available: v%s v%s
To upgrade, run: 'oh-my-posh upgrade%s' To upgrade, run: 'oh-my-posh upgrade%s'
To enable automated upgrades, run: 'oh-my-posh enable upgrade'. To enable automated upgrades, run: 'oh-my-posh enable upgrade'.
` `
) )
func Latest(env runtime.Environment) (string, error) {
body, err := env.HTTPRequest(RELEASEURL, nil, 5000)
if err != nil {
return "", err
}
var release release
// this can't fail
_ = json.Unmarshal(body, &release)
if len(release.TagName) == 0 {
return "", fmt.Errorf("failed to get latest release")
}
return release.TagName, nil
}
// Returns the upgrade notice if a new version is available // Returns the upgrade notice if a new version is available
// that should be displayed to the user. // that should be displayed to the user.
// //
// The upgrade check is only performed every other week. // The upgrade check is only performed every other week.
func Notice(env runtime.Environment, force bool) (string, bool) { func (cfg *Config) Notice() (string, bool) {
// never validate when we install using the Windows Store
if os.Getenv("POSH_INSTALLER") == "ws" {
log.Debug("skipping upgrade check because we are using the Windows Store")
return "", false
}
// do not check when last validation was < 1 week ago // do not check when last validation was < 1 week ago
if _, OK := env.Cache().Get(CACHEKEY); OK && !force { if _, OK := cfg.Cache.Get(CACHEKEY); OK && !cfg.Force {
return "", false return "", false
} }
@ -74,27 +40,21 @@ func Notice(env runtime.Environment, force bool) (string, bool) {
return "", false return "", false
} }
// never validate when we install using the Windows Store latest, err := cfg.Latest()
if env.Getenv("POSH_INSTALLER") == "ws" {
return "", false
}
latest, err := Latest(env)
if err != nil { if err != nil {
return "", false return "", false
} }
env.Cache().Set(CACHEKEY, latest, cache.ONEWEEK) cfg.Cache.Set(CACHEKEY, latest, cfg.Interval)
version := fmt.Sprintf("v%s", build.Version) if latest == build.Version {
if latest == version {
return "", false return "", false
} }
var forceUpdate string var forceUpdate string
if IsMajorUpgrade(version, latest) { if IsMajorUpgrade(build.Version, latest) {
forceUpdate = " --force" forceUpdate = " --force"
} }
return fmt.Sprintf(upgradeNotice, version, latest, forceUpdate), true return fmt.Sprintf(upgradeNotice, build.Version, latest, forceUpdate), true
} }

View file

@ -1,52 +1,47 @@
package upgrade package upgrade
import ( import (
"fmt" "os"
"testing" "testing"
"github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/jandedobbeleer/oh-my-posh/src/build"
cache "github.com/jandedobbeleer/oh-my-posh/src/cache/mock" cache_ "github.com/jandedobbeleer/oh-my-posh/src/cache/mock"
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
"github.com/jandedobbeleer/oh-my-posh/src/runtime/mock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
testify_ "github.com/stretchr/testify/mock"
testify "github.com/stretchr/testify/mock"
) )
func TestCanUpgrade(t *testing.T) { func TestCanUpgrade(t *testing.T) {
ugc := &Config{}
latest, _ := ugc.Latest()
cases := []struct { cases := []struct {
Error error
Case string Case string
CurrentVersion string CurrentVersion string
LatestVersion string
GOOS string
Installer string Installer string
Expected bool Expected bool
Cache bool Cache bool
}{ }{
{Case: "Up to date", CurrentVersion: "3.0.0", LatestVersion: "v3.0.0"}, {Case: "Up to date", CurrentVersion: latest},
{Case: "Outdated Windows", Expected: true, CurrentVersion: "3.0.0", LatestVersion: "v3.0.1", GOOS: runtime.WINDOWS}, {Case: "Outdated Linux", Expected: true, CurrentVersion: "3.0.0"},
{Case: "Outdated Linux", Expected: true, CurrentVersion: "3.0.0", LatestVersion: "v3.0.1", GOOS: runtime.LINUX}, {Case: "Outdated Darwin", Expected: true, CurrentVersion: "3.0.0"},
{Case: "Outdated Darwin", Expected: true, CurrentVersion: "3.0.0", LatestVersion: "v3.0.1", GOOS: runtime.DARWIN},
{Case: "Cached", Cache: true}, {Case: "Cached", Cache: true},
{Case: "Error", Error: fmt.Errorf("error")},
{Case: "Windows Store", Installer: "ws"}, {Case: "Windows Store", Installer: "ws"},
} }
for _, tc := range cases { for _, tc := range cases {
env := new(mock.Environment)
build.Version = tc.CurrentVersion build.Version = tc.CurrentVersion
c := &cache.Cache{} c := &cache_.Cache{}
c.On("Get", CACHEKEY).Return("", tc.Cache) c.On("Get", CACHEKEY).Return("", tc.Cache)
c.On("Set", testify.Anything, testify.Anything, testify.Anything) c.On("Set", testify_.Anything, testify_.Anything, testify_.Anything)
env.On("Cache").Return(c) ugc.Cache = c
env.On("GOOS").Return(tc.GOOS)
env.On("Getenv", "POSH_INSTALLER").Return(tc.Installer)
json := fmt.Sprintf(`{"tag_name":"%s"}`, tc.LatestVersion) if len(tc.Installer) > 0 {
env.On("HTTPRequest", RELEASEURL).Return([]byte(json), tc.Error) os.Setenv("POSH_INSTALLER", tc.Installer)
// ignore the notice }
_, canUpgrade := Notice(env, false)
_, canUpgrade := ugc.Notice()
assert.Equal(t, tc.Expected, canUpgrade, tc.Case) assert.Equal(t, tc.Expected, canUpgrade, tc.Case)
os.Setenv("POSH_INSTALLER", "")
} }
} }

View file

@ -32,7 +32,7 @@ import (
//go:embed public_key.pem //go:embed public_key.pem
var publicKey []byte var publicKey []byte
func downloadAndVerify(tag string) ([]byte, error) { func downloadAndVerify(cfg *Config) ([]byte, error) {
extension := "" extension := ""
if stdruntime.GOOS == runtime.WINDOWS { if stdruntime.GOOS == runtime.WINDOWS {
extension = ".exe" extension = ".exe"
@ -40,14 +40,14 @@ func downloadAndVerify(tag string) ([]byte, error) {
asset := fmt.Sprintf("posh-%s-%s%s", stdruntime.GOOS, stdruntime.GOARCH, extension) asset := fmt.Sprintf("posh-%s-%s%s", stdruntime.GOOS, stdruntime.GOARCH, extension)
data, err := downloadReleaseAsset(tag, asset) data, err := cfg.DownloadAsset(asset)
if err != nil { if err != nil {
return nil, err return nil, err
} }
setState(verifying) setState(verifying)
err = verify(tag, asset, data) err = verify(cfg, asset, data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -55,13 +55,13 @@ func downloadAndVerify(tag string) ([]byte, error) {
return data, nil return data, nil
} }
func verify(tag, asset string, binary []byte) error { func verify(cfg *Config, asset string, binary []byte) error {
checksums, err := downloadReleaseAsset(tag, "checksums.txt") checksums, err := cfg.DownloadAsset("checksums.txt")
if err != nil { if err != nil {
return err return err
} }
signature, err := downloadReleaseAsset(tag, "checksums.txt.sig") signature, err := cfg.DownloadAsset("checksums.txt.sig")
if err != nil { if err != nil {
return err return err
} }

View file

@ -1281,8 +1281,22 @@
"description": "The extensions to look for when determining if a folder is a Fortran workspace", "description": "The extensions to look for when determining if a folder is a Fortran workspace",
"default": [ "default": [
"fpm.toml", "fpm.toml",
"*.f", "*.for", "*.fpp", "*.f77", "*.f90", "*.f95", "*.f03", "*.f08", "*.f",
"*.F", "*.FOR", "*.FPP", "*.F77", "*.F90", "*.F95", "*.F03", "*.F08" "*.for",
"*.fpp",
"*.f77",
"*.f90",
"*.f95",
"*.f03",
"*.f08",
"*.F",
"*.FOR",
"*.FPP",
"*.F77",
"*.F90",
"*.F95",
"*.F03",
"*.F08"
], ],
"items": { "items": {
"type": "string" "type": "string"
@ -5036,17 +5050,36 @@
"description": "https://ohmyposh.dev/docs/configuration/general#general-settings", "description": "https://ohmyposh.dev/docs/configuration/general#general-settings",
"default": "" "default": ""
}, },
"upgrade_notice": { "upgrade": {
"type": "boolean", "type": "object",
"title": "Enable Upgrade Notice", "title": "Enable Upgrade Notice",
"description": "https://ohmyposh.dev/docs/configuration/general#general-settings", "description": "https://ohmyposh.dev/docs/configuration/general#general-settings",
"default": false "default": {
}, "source": "cdn",
"auto_upgrade": { "auto": false,
"type": "boolean", "notice": false
"title": "Enable automatic upgrades for Oh My Posh (supports Windows/macOS only)", },
"description": "https://ohmyposh.dev/docs/configuration/general#general-settings", "properties": {
"default": false "interval": {
"$ref": "#/definitions/cache_duration"
},
"source": {
"type": "string",
"enum": [
"cdn",
"github"
],
"default": "cdn"
},
"auto": {
"type": "boolean",
"default": false
},
"notice": {
"type": "boolean",
"default": false
}
}
}, },
"patch_pwsh_bleed": { "patch_pwsh_bleed": {
"type": "boolean", "type": "boolean",

View file

@ -136,8 +136,7 @@ For example, the following is a valid `--config` flag:
| `shell_integration` | `boolean` | `false` | enable shell integration using FinalTerm's OSC sequences. Works in bash, cmd (Clink v1.14.25+), fish, powershell and zsh | | `shell_integration` | `boolean` | `false` | enable shell integration using FinalTerm's OSC sequences. Works in bash, cmd (Clink v1.14.25+), fish, powershell and zsh |
| `enable_cursor_positioning` | `boolean` | `false` | enable fetching the cursor position in bash and zsh to allow automatic hiding of leading newlines when at the top of the shell | | `enable_cursor_positioning` | `boolean` | `false` | enable fetching the cursor position in bash and zsh to allow automatic hiding of leading newlines when at the top of the shell |
| `patch_pwsh_bleed` | `boolean` | `false` | patch a PowerShell bug where the background colors bleed into the next line at the end of the buffer (can be removed when [this][pwsh-bleed] is merged) | | `patch_pwsh_bleed` | `boolean` | `false` | patch a PowerShell bug where the background colors bleed into the next line at the end of the buffer (can be removed when [this][pwsh-bleed] is merged) |
| `upgrade_notice` | `boolean` | `false` | enable the notice that a new upgrade is available when `auto_upgrade` is disabled | | `upgrade` | `Upgrade` | | enable auto upgrade or the upgrade notice. See [Upgrade] |
| `auto_upgrade` | `boolean` | `false` | enable [automatic upgrades][upgrade] for Oh My Posh (supports Windows, macOS and Linux) |
| `iterm_features` | `[]string` | `false` | enable iTerm2 specific features:<ul><li>`prompt_mark`: add the `iterm2_prompt_mark` [function][iterm2-si] for supported shells</li><li>`current_dir`: expose the current directory for iTerm2</li><li>`remote_host`: expose the current remote and user for iTerm2</li></ul> | | `iterm_features` | `[]string` | `false` | enable iTerm2 specific features:<ul><li>`prompt_mark`: add the `iterm2_prompt_mark` [function][iterm2-si] for supported shells</li><li>`current_dir`: expose the current directory for iTerm2</li><li>`remote_host`: expose the current remote and user for iTerm2</li></ul> |
### JSON Schema Validation ### JSON Schema Validation
@ -194,4 +193,4 @@ Converters won't catch this change, so you will need to adjust manually.
[templates]: /docs/configuration/templates#config-variables [templates]: /docs/configuration/templates#config-variables
[pwsh-bleed]: https://github.com/PowerShell/PowerShell/pull/19019 [pwsh-bleed]: https://github.com/PowerShell/PowerShell/pull/19019
[iterm2-si]: https://iterm2.com/documentation-shell-integration.html [iterm2-si]: https://iterm2.com/documentation-shell-integration.html
[upgrade]: /docs/installation/upgrade#automatic [Upgrade]: /docs/installation/upgrade

View file

@ -6,8 +6,34 @@ sidebar_label: ♻️ Upgrade
import Tabs from "@theme/Tabs"; import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem"; import TabItem from "@theme/TabItem";
import Config from "@site/src/components/Config.js";
## Manual ## Configuration
Oh My Posh can display the availability of an update, or auto update itself when
enabled by adding the following to your configuration.
<Config
data={{
upgrade: {
notice: true,
interval: "168h",
auto: false,
source: "cdn",
},
}}
/>
| Name | Type | Default | Description |
| ---------- | :-------: | :-----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `notice` | `boolean` | `false` | enbale displaying the upgrade notice on shell start, only checks based on `interval` |
| `auto` | `boolean` | `false` | automatically update Oh My Posh when an update is found, only checks based on `interval` |
| `interval` | `string` | `24h` | the duration for which not to check for an update. The duration is a string in the format `1h2m3s` and is parsed using the [time.ParseDuration] function from the Go standard library |
| `source` | `string` | `cdn` | where to fetch the information from. Accepted values are `cdn` (`https://cdn.ohmyposh.dev/releases/latest/version.txt`) and `github` (`https://github.com/JanDeDobbeleer/oh-my-posh/releases/latest/download/version.txt`) |
## Upgrade
### Manual
While you can always follow the upgrade steps listed under the installation section, While you can always follow the upgrade steps listed under the installation section,
you can also use the `upgrade` command to update Oh My Posh to the latest version. you can also use the `upgrade` command to update Oh My Posh to the latest version.
@ -16,7 +42,7 @@ you can also use the `upgrade` command to update Oh My Posh to the latest versio
oh-my-posh upgrade oh-my-posh upgrade
``` ```
## Automated ### Automated
<Tabs <Tabs
defaultValue="cli" defaultValue="cli"
@ -35,11 +61,13 @@ oh-my-posh enable upgrade
</TabItem> </TabItem>
<TabItem value="config"> <TabItem value="config">
import Config from "@site/src/components/Config.js";
<Config <Config
data={{ data={{
"auto_upgrade": true upgrade: {
interval: "168h",
auto: true,
source: "cdn",
},
}} }}
/> />