diff --git a/.vscode/launch.json b/.vscode/launch.json index e669166c..3db61234 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -145,6 +145,16 @@ "git" ] }, + { + "name": "Upgrade", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceRoot}/src", + "args": [ + "upgrade" + ] + }, { "type": "node", "request": "launch", diff --git a/src/cli/notice.go b/src/cli/notice.go index 6b03828c..4f663e38 100644 --- a/src/cli/notice.go +++ b/src/cli/notice.go @@ -21,7 +21,7 @@ var noticeCmd = &cobra.Command{ env.Init() defer env.Close() - if notice, hasNotice := upgrade.Notice(env); hasNotice { + if notice, hasNotice := upgrade.Notice(env, false); hasNotice { fmt.Println(notice) } }, diff --git a/src/cli/upgrade.go b/src/cli/upgrade.go new file mode 100644 index 00000000..4170328d --- /dev/null +++ b/src/cli/upgrade.go @@ -0,0 +1,43 @@ +package cli + +import ( + "fmt" + + "github.com/jandedobbeleer/oh-my-posh/src/platform" + "github.com/jandedobbeleer/oh-my-posh/src/upgrade" + "github.com/spf13/cobra" +) + +var force bool + +// noticeCmd represents the get command +var upgradeCmd = &cobra.Command{ + Use: "upgrade", + Short: "Upgrade when a new version is available.", + Long: "Upgrade when a new version is available.", + Args: cobra.NoArgs, + Run: func(_ *cobra.Command, _ []string) { + if force { + upgrade.Run() + return + } + + env := &platform.Shell{ + CmdFlags: &platform.Flags{}, + } + env.Init() + defer env.Close() + + if _, hasNotice := upgrade.Notice(env, true); !hasNotice { + fmt.Print("\n ✅ no new version available\n\n") + return + } + + upgrade.Run() + }, +} + +func init() { + upgradeCmd.Flags().BoolVarP(&force, "force", "f", false, "force the upgrade even if the version is up to date") + RootCmd.AddCommand(upgradeCmd) +} diff --git a/src/engine/prompt.go b/src/engine/prompt.go index 30e098dd..dd6e37d8 100644 --- a/src/engine/prompt.go +++ b/src/engine/prompt.go @@ -160,6 +160,7 @@ func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string { Template: getTemplate(prompt.Template), Env: e.Env, } + promptText, err := tmpl.Render() if err != nil { promptText = err.Error() diff --git a/src/shell/init.go b/src/shell/init.go index df74910e..e480aad5 100644 --- a/src/shell/init.go +++ b/src/shell/init.go @@ -268,7 +268,7 @@ func PrintInit(env platform.Environment) string { // only run this for shells that support // injecting the notice directly if shell != PWSH && shell != PWSH5 { - notice, hasNotice = upgrade.Notice(env) + notice, hasNotice = upgrade.Notice(env, false) } return strings.NewReplacer( diff --git a/src/upgrade/cli.go b/src/upgrade/cli.go new file mode 100644 index 00000000..cd515b82 --- /dev/null +++ b/src/upgrade/cli.go @@ -0,0 +1,133 @@ +package upgrade + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/jandedobbeleer/oh-my-posh/src/platform" +) + +var ( + program *tea.Program + textStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) +) + +type resultMsg string + +type state int + +const ( + validating state = iota + downloading + installing +) + +type stateMsg state + +type model struct { + spinner spinner.Model + message string + state state +} + +func initialModel() *model { + s := spinner.New() + s.Spinner = spinner.Dot + s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("170")) + return &model{spinner: s} +} + +func (m *model) Init() tea.Cmd { + defer func() { + go func() { + if err := install(); err != nil { + message := fmt.Sprintf("⚠️ %s", err) + program.Send(resultMsg(message)) + return + } + + program.Send(resultMsg(successMsg)) + }() + }() + + return m.spinner.Tick +} + +func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "esc", "ctrl+c": + return m, tea.Quit + default: + return m, nil + } + + case resultMsg: + m.message = string(msg) + return m, tea.Quit + + case stateMsg: + m.state = state(msg) + return m, nil + + default: + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd + } +} + +func (m *model) View() string { + if len(m.message) > 0 { + return textStyle.Render(m.message) + } + + var message string + m.spinner.Spinner = spinner.Dot + switch m.state { + case validating: + message = "Validating current installation" + case downloading: + m.spinner.Spinner = spinner.Globe + message = "Downloading latest version" + case installing: + message = "Installing" + } + + return textStyle.Render(fmt.Sprintf("%s %s", m.spinner.View(), message)) +} + +func Run() { + program = tea.NewProgram(initialModel()) + if _, err := program.Run(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func downloadAsset(asset string) (io.ReadCloser, error) { + url := fmt.Sprintf("https://github.com/JanDeDobbeleer/oh-my-posh/releases/latest/download/%s", asset) + + req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil) + if err != nil { + return nil, err + } + + resp, err := platform.Client.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Failed to download installer: %s", url) + } + + return resp.Body, nil +} diff --git a/src/upgrade/cli_unix.go b/src/upgrade/cli_unix.go new file mode 100644 index 00000000..661e1c8f --- /dev/null +++ b/src/upgrade/cli_unix.go @@ -0,0 +1,48 @@ +//go:build !windows + +package upgrade + +import ( + "errors" + "fmt" + "io" + "os" + "runtime" +) + +var successMsg = "Upgrade successful, restart your shell to take full advantage of the new functionality." + +func install() error { + program.Send(stateMsg(validating)) + executable, err := os.Executable() + if err != nil { + return err + } + + file, err := os.OpenFile(executable, os.O_WRONLY, 0755) + if err != nil { + return errors.New("we don't have permissions to upgrade oh-my-posh, please use elevated permissions to upgrade") + } + + defer file.Close() + + program.Send(stateMsg(downloading)) + + asset := fmt.Sprintf("posh-%s-%s", runtime.GOOS, runtime.GOARCH) + + data, err := downloadAsset(asset) + if err != nil { + return err + } + + defer data.Close() + + program.Send(stateMsg(installing)) + + _, err = io.Copy(file, data) + if err != nil { + return err + } + + return nil +} diff --git a/src/upgrade/cli_windows.go b/src/upgrade/cli_windows.go new file mode 100644 index 00000000..c59b579d --- /dev/null +++ b/src/upgrade/cli_windows.go @@ -0,0 +1,59 @@ +package upgrade + +import ( + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" +) + +var successMsg = "Oh My Posh is installing in the background.\nRestart your shell in a minute to take full advantage of the new functionality." + +func install() error { + program.Send(stateMsg(downloading)) + + temp := os.Getenv("TEMP") + if len(temp) == 0 { + return errors.New("failed to get TEMP environment variable") + } + + path := filepath.Join(temp, "install.exe") + + if _, err := os.Stat(path); err == nil { + err := os.Remove(path) + if err != nil { + return errors.New("unable to remove existing installer") + } + } + + file, err := os.Create(path) + if err != nil { + return err + } + + asset := fmt.Sprintf("install-%s.exe", runtime.GOARCH) + + data, err := downloadAsset(asset) + if err != nil { + return err + } + + _, err = io.Copy(file, data) + if err != nil { + return err + } + + data.Close() + file.Close() + + // We need to run the installer in a separate process to avoid being blocked by the current process + go func() { + cmd := exec.Command(path, "/VERYSILENT", "/FORCECLOSEAPPLICATIONS") + _ = cmd.Run() + }() + + return nil +} diff --git a/src/upgrade/notice.go b/src/upgrade/notice.go index 5564e797..02311359 100644 --- a/src/upgrade/notice.go +++ b/src/upgrade/notice.go @@ -58,9 +58,9 @@ func Latest(env platform.Environment) (string, error) { // that should be displayed to the user. // // The upgrade check is only performed every other week. -func Notice(env platform.Environment) (string, bool) { +func Notice(env platform.Environment, force bool) (string, bool) { // do not check when last validation was < 1 week ago - if _, OK := env.Cache().Get(CACHEKEY); OK { + if _, OK := env.Cache().Get(CACHEKEY); OK && !force { return "", false } diff --git a/src/upgrade/notice_test.go b/src/upgrade/notice_test.go index 94c9446f..34bf0035 100644 --- a/src/upgrade/notice_test.go +++ b/src/upgrade/notice_test.go @@ -45,7 +45,7 @@ func TestCanUpgrade(t *testing.T) { json := fmt.Sprintf(`{"tag_name":"%s"}`, tc.LatestVersion) env.On("HTTPRequest", RELEASEURL).Return([]byte(json), tc.Error) // ignore the notice - _, canUpgrade := Notice(env) + _, canUpgrade := Notice(env, false) assert.Equal(t, tc.Expected, canUpgrade, tc.Case) } }