feat(pwsh): cache prompt for repainting

This commit is contained in:
L. Yeung 2024-07-19 23:45:18 +08:00 committed by Jan De Dobbeleer
parent 4a3d283ec4
commit 479e6f551e
9 changed files with 232 additions and 131 deletions

1
src/cache/cache.go vendored
View file

@ -33,6 +33,7 @@ var (
TEMPLATECACHE = fmt.Sprintf("template_cache_%s", pid()) TEMPLATECACHE = fmt.Sprintf("template_cache_%s", pid())
TOGGLECACHE = fmt.Sprintf("toggle_cache_%s", pid()) TOGGLECACHE = fmt.Sprintf("toggle_cache_%s", pid())
PROMPTCOUNTCACHE = fmt.Sprintf("prompt_count_cache_%s", pid()) PROMPTCOUNTCACHE = fmt.Sprintf("prompt_count_cache_%s", pid())
PROMPTCACHE = fmt.Sprintf("prompt_cache_%s", pid())
) )
type Entry struct { type Entry struct {

View file

@ -19,6 +19,7 @@ var (
terminalWidth int terminalWidth int
eval bool eval bool
cleared bool cleared bool
cached bool
command string command string
shellVersion string shellVersion string
@ -64,6 +65,7 @@ var printCmd = &cobra.Command{
Plain: plain, Plain: plain,
Primary: args[0] == "primary", Primary: args[0] == "primary",
Cleared: cleared, Cleared: cleared,
Cached: cached,
NoExitCode: noStatus, NoExitCode: noStatus,
Column: column, Column: column,
} }
@ -108,6 +110,7 @@ func init() {
printCmd.Flags().StringVar(&command, "command", "", "tooltip command") printCmd.Flags().StringVar(&command, "command", "", "tooltip command")
printCmd.Flags().BoolVarP(&plain, "plain", "p", false, "plain text output (no ANSI)") printCmd.Flags().BoolVarP(&plain, "plain", "p", false, "plain text output (no ANSI)")
printCmd.Flags().BoolVar(&cleared, "cleared", false, "do we have a clear terminal or not") printCmd.Flags().BoolVar(&cleared, "cleared", false, "do we have a clear terminal or not")
printCmd.Flags().BoolVar(&cached, "cached", false, "use a cached prompt")
printCmd.Flags().BoolVar(&eval, "eval", false, "output the prompt for eval") printCmd.Flags().BoolVar(&eval, "eval", false, "output the prompt for eval")
printCmd.Flags().IntVar(&column, "column", 0, "the column position of the cursor") printCmd.Flags().IntVar(&column, "column", 0, "the column position of the cursor")
// Deprecated flags // Deprecated flags

View file

@ -1,8 +1,10 @@
package prompt package prompt
import ( import (
"encoding/json"
"strings" "strings"
"github.com/jandedobbeleer/oh-my-posh/src/cache"
"github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/color"
"github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/regex"
@ -12,9 +14,14 @@ import (
"github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/jandedobbeleer/oh-my-posh/src/terminal"
) )
var ( var cycle *color.Cycle = &color.Cycle{}
cycle *color.Cycle = &color.Cycle{}
) type promptCache struct {
Prompt string
CurrentLineLength int
RPrompt string
RPromptLength int
}
type Engine struct { type Engine struct {
Config *config.Config Config *config.Config
@ -28,6 +35,8 @@ type Engine struct {
activeSegment *config.Segment activeSegment *config.Segment
previousActiveSegment *config.Segment previousActiveSegment *config.Segment
promptCache *promptCache
} }
func (e *Engine) write(text string) { func (e *Engine) write(text string) {
@ -500,3 +509,80 @@ func (e *Engine) adjustTrailingDiamondColorOverrides() {
adjustOverride(match[terminal.ANCHOR], color.Ansi(match[terminal.FG])) adjustOverride(match[terminal.ANCHOR], color.Ansi(match[terminal.FG]))
} }
} }
func (e *Engine) checkPromptCache() bool {
data, ok := e.Env.Cache().Get(cache.PROMPTCACHE)
if !ok {
return false
}
e.promptCache = &promptCache{}
err := json.Unmarshal([]byte(data), e.promptCache)
if err != nil {
return false
}
e.write(e.promptCache.Prompt)
e.currentLineLength = e.promptCache.CurrentLineLength
e.rprompt = e.promptCache.RPrompt
e.rpromptLength = e.promptCache.RPromptLength
return true
}
func (e *Engine) updatePromptCache(value *promptCache) {
cacheJSON, err := json.Marshal(value)
if err != nil {
return
}
e.Env.Cache().Set(cache.PROMPTCACHE, string(cacheJSON), 1440)
}
// New returns a prompt engine initialized with the
// given configuration options, and is ready to print any
// of the prompt components.
func New(flags *runtime.Flags) *Engine {
env := &runtime.Terminal{
CmdFlags: flags,
}
env.Init()
cfg := config.Load(env)
if cfg.PatchPwshBleed {
patchPowerShellBleed(env.Shell(), flags)
}
env.Var = cfg.Var
flags.HasTransient = cfg.TransientPrompt != nil
terminal.Init(env.Shell())
terminal.BackgroundColor = shell.ConsoleBackgroundColor(env, cfg.TerminalBackground)
terminal.Colors = cfg.MakeColors()
terminal.Plain = flags.Plain
eng := &Engine{
Config: cfg,
Env: env,
Plain: flags.Plain,
}
return eng
}
func patchPowerShellBleed(sh string, flags *runtime.Flags) {
// when in PowerShell, and force patching the bleed bug
// we need to reduce the terminal width by 1 so the last
// character isn't cut off by the ANSI escape sequences
// See https://github.com/JanDeDobbeleer/oh-my-posh/issues/65
if sh != shell.PWSH && sh != shell.PWSH5 {
return
}
// only do this when relevant
if flags.TerminalWidth <= 0 {
return
}
flags.TerminalWidth--
}

View file

@ -11,6 +11,16 @@ import (
"github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/jandedobbeleer/oh-my-posh/src/terminal"
) )
type ExtraPromptType int
const (
Transient ExtraPromptType = iota
Valid
Error
Secondary
Debug
)
func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string { func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string {
// populate env with latest context // populate env with latest context
e.Env.LoadTemplateCache() e.Env.LoadTemplateCache()

View file

@ -1,57 +0,0 @@
package prompt
import (
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
)
// New returns a prompt engine initialized with the
// given configuration options, and is ready to print any
// of the prompt components.
func New(flags *runtime.Flags) *Engine {
env := &runtime.Terminal{
CmdFlags: flags,
}
env.Init()
cfg := config.Load(env)
if cfg.PatchPwshBleed {
patchPowerShellBleed(env.Shell(), flags)
}
env.Var = cfg.Var
flags.HasTransient = cfg.TransientPrompt != nil
terminal.Init(env.Shell())
terminal.BackgroundColor = shell.ConsoleBackgroundColor(env, cfg.TerminalBackground)
terminal.Colors = cfg.MakeColors()
terminal.Plain = flags.Plain
eng := &Engine{
Config: cfg,
Env: env,
Plain: flags.Plain,
}
return eng
}
func patchPowerShellBleed(sh string, flags *runtime.Flags) {
// when in PowerShell, and force patching the bleed bug
// we need to reduce the terminal width by 1 so the last
// character isn't cut off by the ANSI escape sequences
// See https://github.com/JanDeDobbeleer/oh-my-posh/issues/65
if sh != shell.PWSH && sh != shell.PWSH5 {
return
}
// only do this when relevant
if flags.TerminalWidth <= 0 {
return
}
flags.TerminalWidth--
}

View file

@ -9,70 +9,76 @@ import (
"github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/jandedobbeleer/oh-my-posh/src/terminal"
) )
type ExtraPromptType int
const (
Transient ExtraPromptType = iota
Valid
Error
Secondary
Debug
)
func (e *Engine) Primary() string { func (e *Engine) Primary() string {
if e.Config.ShellIntegration {
exitCode, _ := e.Env.StatusCodes()
e.write(terminal.CommandFinished(exitCode, e.Env.Flags().NoExitCode))
e.write(terminal.PromptStart())
}
// cache a pointer to the color cycle
cycle = &e.Config.Cycle
var cancelNewline, didRender bool
needsPrimaryRPrompt := e.needsPrimaryRPrompt() needsPrimaryRPrompt := e.needsPrimaryRPrompt()
for i, block := range e.Config.Blocks { var (
// do not print a leading newline when we're at the first row and the prompt is cleared useCache bool
if i == 0 { updateCache bool
row, _ := e.Env.CursorPosition() )
cancelNewline = e.Env.Flags().Cleared || e.Env.Flags().PromptCount == 1 || row == 1
}
// skip setting a newline when we didn't print anything yet if e.Env.Shell() == shell.PWSH || e.Env.Shell() == shell.PWSH5 {
if i != 0 { // For PowerShell, use a cached prompt if available, otherwise render a new prompt.
cancelNewline = !didRender if e.Env.Flags().Cached && e.checkPromptCache() {
} useCache = true
} else {
if block.Type == config.RPrompt && !needsPrimaryRPrompt { updateCache = true
continue
}
if e.renderBlock(block, cancelNewline) {
didRender = true
} }
} }
if len(e.Config.ConsoleTitleTemplate) > 0 && !e.Env.Flags().Plain { if !useCache {
title := e.getTitleTemplateText() if e.Config.ShellIntegration {
e.write(terminal.FormatTitle(title)) exitCode, _ := e.Env.StatusCodes()
} e.write(terminal.CommandFinished(exitCode, e.Env.Flags().NoExitCode))
e.write(terminal.PromptStart())
}
if e.Config.FinalSpace { // cache a pointer to the color cycle
e.write(" ") cycle = &e.Config.Cycle
e.currentLineLength++ var cancelNewline, didRender bool
}
if e.Config.ITermFeatures != nil && e.isIterm() { for i, block := range e.Config.Blocks {
host, _ := e.Env.Host() // do not print a leading newline when we're at the first row and the prompt is cleared
e.write(terminal.RenderItermFeatures(e.Config.ITermFeatures, e.Env.Shell(), e.Env.Pwd(), e.Env.User(), host)) if i == 0 {
} row, _ := e.Env.CursorPosition()
cancelNewline = e.Env.Flags().Cleared || e.Env.Flags().PromptCount == 1 || row == 1
}
if e.Config.ShellIntegration && e.Config.TransientPrompt == nil { // skip setting a newline when we didn't print anything yet
e.write(terminal.CommandStart()) if i != 0 {
} cancelNewline = !didRender
}
e.pwd() if block.Type == config.RPrompt && !needsPrimaryRPrompt {
continue
}
if e.renderBlock(block, cancelNewline) {
didRender = true
}
}
if len(e.Config.ConsoleTitleTemplate) > 0 && !e.Env.Flags().Plain {
title := e.getTitleTemplateText()
e.write(terminal.FormatTitle(title))
}
if e.Config.FinalSpace {
e.write(" ")
e.currentLineLength++
}
if e.Config.ITermFeatures != nil && e.isIterm() {
host, _ := e.Env.Host()
e.write(terminal.RenderItermFeatures(e.Config.ITermFeatures, e.Env.Shell(), e.Env.Pwd(), e.Env.User(), host))
}
if e.Config.ShellIntegration && e.Config.TransientPrompt == nil {
e.write(terminal.CommandStart())
}
e.pwd()
}
switch e.Env.Shell() { switch e.Env.Shell() {
case shell.ZSH: case shell.ZSH:
@ -94,6 +100,16 @@ func (e *Engine) Primary() string {
return prompt return prompt
default: default:
if updateCache {
// Cache the new prompt.
e.updatePromptCache(&promptCache{
Prompt: e.prompt.String(),
CurrentLineLength: e.currentLineLength,
RPrompt: e.rprompt,
RPromptLength: e.rpromptLength,
})
}
if !needsPrimaryRPrompt { if !needsPrimaryRPrompt {
break break
} }

View file

@ -1,7 +1,6 @@
package prompt package prompt
import ( import (
"slices"
"strings" "strings"
"github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/config"
@ -10,18 +9,6 @@ import (
) )
func (e *Engine) Tooltip(tip string) string { func (e *Engine) Tooltip(tip string) string {
supportedShells := []string{
shell.ZSH,
shell.CMD,
shell.FISH,
shell.PWSH,
shell.PWSH5,
shell.GENERIC,
}
if !slices.Contains(supportedShells, e.Env.Shell()) {
return ""
}
tip = strings.Trim(tip, " ") tip = strings.Trim(tip, " ")
tooltips := make([]*config.Segment, 0, 1) tooltips := make([]*config.Segment, 0, 1)
@ -56,6 +43,15 @@ func (e *Engine) Tooltip(tip string) string {
switch e.Env.Shell() { switch e.Env.Shell() {
case shell.PWSH, shell.PWSH5: case shell.PWSH, shell.PWSH5:
defer func() {
// If a prompt cache is available, we update the right prompt to the new tooltip for reuse.
if e.checkPromptCache() {
e.promptCache.RPrompt = text
e.promptCache.RPromptLength = length
e.updatePromptCache(e.promptCache)
}
}()
e.rprompt = text e.rprompt = text
e.currentLineLength = e.Env.Flags().Column e.currentLineLength = e.Env.Flags().Column
space, ok := e.canWriteRightBlock(length, true) space, ok := e.canWriteRightBlock(length, true)

View file

@ -60,6 +60,7 @@ type Flags struct {
HasTransient bool HasTransient bool
PromptCount int PromptCount int
Cleared bool Cleared bool
Cached bool
NoExitCode bool NoExitCode bool
Column int Column int
} }
@ -233,7 +234,9 @@ func (term *Terminal) Init() {
term.tmplCache = &cache.Template{} term.tmplCache = &cache.Template{}
term.SetPromptCount() if !term.CmdFlags.Cached {
term.SetPromptCount()
}
} }
func (term *Terminal) resolveConfigPath() { func (term *Terminal) resolveConfigPath() {
@ -1003,7 +1006,7 @@ func returnOrBuildCachePath(path string) string {
if _, err := os.Stat(cachePath); err == nil { if _, err := os.Stat(cachePath); err == nil {
return cachePath return cachePath
} }
if err := os.Mkdir(cachePath, 0755); err != nil { if err := os.Mkdir(cachePath, 0o755); err != nil {
return "" return ""
} }
return cachePath return cachePath

View file

@ -17,6 +17,9 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
# Check `ConstrainedLanguage` mode. # Check `ConstrainedLanguage` mode.
$script:ConstrainedLanguageMode = $ExecutionContext.SessionState.LanguageMode -eq "ConstrainedLanguage" $script:ConstrainedLanguageMode = $ExecutionContext.SessionState.LanguageMode -eq "ConstrainedLanguage"
# This indicates whether a new prompt should be rendered instead of using the cached one.
$script:NewPrompt = $true
# Prompt related backup. # Prompt related backup.
$script:OriginalPromptFunction = $Function:prompt $script:OriginalPromptFunction = $Function:prompt
$script:OriginalContinuationPrompt = (Get-PSReadLineOption).ContinuationPrompt $script:OriginalContinuationPrompt = (Get-PSReadLineOption).ContinuationPrompt
@ -159,7 +162,22 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
$terminalWidth = Get-TerminalWidth $terminalWidth = Get-TerminalWidth
$cleanPSWD = Get-CleanPSWD $cleanPSWD = Get-CleanPSWD
$stackCount = global:Get-PoshStackCount $stackCount = global:Get-PoshStackCount
$standardOut = (Start-Utf8Process $script:OMPExecutable @("print", "tooltip", "--status=$script:ErrorCode", "--shell=$script:ShellName", "--pswd=$cleanPSWD", "--execution-time=$script:ExecutionTime", "--stack-count=$stackCount", "--config=$env:POSH_THEME", "--command=$command", "--shell-version=$script:PSVersion", "--column=$column", "--terminal-width=$terminalWidth", "--no-status=$script:NoExitCode")) -join '' $arguments = @(
"print"
"tooltip"
"--status=$script:ErrorCode"
"--shell=$script:ShellName"
"--pswd=$cleanPSWD"
"--execution-time=$script:ExecutionTime"
"--stack-count=$stackCount"
"--config=$env:POSH_THEME"
"--command=$command"
"--shell-version=$script:PSVersion"
"--column=$column"
"--terminal-width=$terminalWidth"
"--no-status=$script:NoExitCode"
)
$standardOut = (Start-Utf8Process $script:OMPExecutable $arguments) -join ''
if (!$standardOut) { if (!$standardOut) {
return return
} }
@ -191,6 +209,7 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$null, [ref]$null, [ref]$parseErrors, [ref]$null) [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$null, [ref]$null, [ref]$parseErrors, [ref]$null)
$executingCommand = $parseErrors.Count -eq 0 $executingCommand = $parseErrors.Count -eq 0
if ($executingCommand) { if ($executingCommand) {
$script:NewPrompt = $true
$script:TooltipCommand = '' $script:TooltipCommand = ''
if ('::TRANSIENT::' -eq 'true') { if ('::TRANSIENT::' -eq 'true') {
Set-TransientPrompt Set-TransientPrompt
@ -211,6 +230,7 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
[Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$start, [ref]$null) [Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$start, [ref]$null)
# only render a transient prompt when no text is selected # only render a transient prompt when no text is selected
if ($start -eq -1) { if ($start -eq -1) {
$script:NewPrompt = $true
$script:TooltipCommand = '' $script:TooltipCommand = ''
if ('::TRANSIENT::' -eq 'true') { if ('::TRANSIENT::' -eq 'true') {
Set-TransientPrompt Set-TransientPrompt
@ -397,7 +417,7 @@ Example:
} }
} }
function prompt { $promptFunction = {
# store the orignal last command execution status # store the orignal last command execution status
if ($global:NVS_ORIGINAL_LASTEXECUTIONSTATUS -is [bool]) { if ($global:NVS_ORIGINAL_LASTEXECUTIONSTATUS -is [bool]) {
# make it compatible with NVS auto-switching, if enabled # make it compatible with NVS auto-switching, if enabled
@ -410,7 +430,14 @@ Example:
$script:OriginalLastExitCode = $global:LASTEXITCODE $script:OriginalLastExitCode = $global:LASTEXITCODE
Set-PoshPromptType Set-PoshPromptType
# Whether we should use a cached prompt.
$useCache = !$script:NewPrompt
if ($script:PromptType -ne 'transient') { if ($script:PromptType -ne 'transient') {
if ($script:NewPrompt) {
$script:NewPrompt = $false
}
Update-PoshErrorCode Update-PoshErrorCode
} }
$cleanPSWD = Get-CleanPSWD $cleanPSWD = Get-CleanPSWD
@ -422,7 +449,21 @@ Example:
$env:POSH_CURSOR_LINE = $Host.UI.RawUI.CursorPosition.Y + 1 $env:POSH_CURSOR_LINE = $Host.UI.RawUI.CursorPosition.Y + 1
$env:POSH_CURSOR_COLUMN = $Host.UI.RawUI.CursorPosition.X + 1 $env:POSH_CURSOR_COLUMN = $Host.UI.RawUI.CursorPosition.X + 1
$standardOut = Start-Utf8Process $script:OMPExecutable @("print", $script:PromptType, "--status=$script:ErrorCode", "--pswd=$cleanPSWD", "--execution-time=$script:ExecutionTime", "--stack-count=$stackCount", "--config=$env:POSH_THEME", "--shell-version=$script:PSVersion", "--terminal-width=$terminalWidth", "--shell=$script:ShellName", "--no-status=$script:NoExitCode") $arguments = @(
"print"
$script:PromptType
"--status=$script:ErrorCode"
"--pswd=$cleanPSWD"
"--execution-time=$script:ExecutionTime"
"--stack-count=$stackCount"
"--config=$env:POSH_THEME"
"--shell-version=$script:PSVersion"
"--terminal-width=$terminalWidth"
"--shell=$script:ShellName"
"--no-status=$script:NoExitCode"
"--cached=$useCache"
)
$standardOut = Start-Utf8Process $script:OMPExecutable $arguments
# make sure PSReadLine knows if we have a multiline prompt # make sure PSReadLine knows if we have a multiline prompt
Set-PSReadLineOption -ExtraPromptLineCount (($standardOut | Measure-Object -Line).Lines - 1) Set-PSReadLineOption -ExtraPromptLineCount (($standardOut | Measure-Object -Line).Lines - 1)
@ -436,6 +477,8 @@ Example:
$global:LASTEXITCODE = $script:OriginalLastExitCode $global:LASTEXITCODE = $script:OriginalLastExitCode
} }
$Function:prompt = $promptFunction
# set secondary prompt # set secondary prompt
Set-PSReadLineOption -ContinuationPrompt ((Start-Utf8Process $script:OMPExecutable @("print", "secondary", "--config=$env:POSH_THEME", "--shell=$script:ShellName")) -join "`n") Set-PSReadLineOption -ContinuationPrompt ((Start-Utf8Process $script:OMPExecutable @("print", "secondary", "--config=$env:POSH_THEME", "--shell=$script:ShellName")) -join "`n")