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())
TOGGLECACHE = fmt.Sprintf("toggle_cache_%s", pid())
PROMPTCOUNTCACHE = fmt.Sprintf("prompt_count_cache_%s", pid())
PROMPTCACHE = fmt.Sprintf("prompt_cache_%s", pid())
)
type Entry struct {

View file

@ -19,6 +19,7 @@ var (
terminalWidth int
eval bool
cleared bool
cached bool
command string
shellVersion string
@ -64,6 +65,7 @@ var printCmd = &cobra.Command{
Plain: plain,
Primary: args[0] == "primary",
Cleared: cleared,
Cached: cached,
NoExitCode: noStatus,
Column: column,
}
@ -108,6 +110,7 @@ func init() {
printCmd.Flags().StringVar(&command, "command", "", "tooltip command")
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(&cached, "cached", false, "use a cached prompt")
printCmd.Flags().BoolVar(&eval, "eval", false, "output the prompt for eval")
printCmd.Flags().IntVar(&column, "column", 0, "the column position of the cursor")
// Deprecated flags

View file

@ -1,8 +1,10 @@
package prompt
import (
"encoding/json"
"strings"
"github.com/jandedobbeleer/oh-my-posh/src/cache"
"github.com/jandedobbeleer/oh-my-posh/src/color"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/regex"
@ -12,9 +14,14 @@ import (
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
)
var (
cycle *color.Cycle = &color.Cycle{}
)
var cycle *color.Cycle = &color.Cycle{}
type promptCache struct {
Prompt string
CurrentLineLength int
RPrompt string
RPromptLength int
}
type Engine struct {
Config *config.Config
@ -28,6 +35,8 @@ type Engine struct {
activeSegment *config.Segment
previousActiveSegment *config.Segment
promptCache *promptCache
}
func (e *Engine) write(text string) {
@ -500,3 +509,80 @@ func (e *Engine) adjustTrailingDiamondColorOverrides() {
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"
)
type ExtraPromptType int
const (
Transient ExtraPromptType = iota
Valid
Error
Secondary
Debug
)
func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string {
// populate env with latest context
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"
)
type ExtraPromptType int
const (
Transient ExtraPromptType = iota
Valid
Error
Secondary
Debug
)
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()
for i, block := range e.Config.Blocks {
// do not print a leading newline when we're at the first row and the prompt is cleared
if i == 0 {
row, _ := e.Env.CursorPosition()
cancelNewline = e.Env.Flags().Cleared || e.Env.Flags().PromptCount == 1 || row == 1
}
var (
useCache bool
updateCache bool
)
// skip setting a newline when we didn't print anything yet
if i != 0 {
cancelNewline = !didRender
}
if block.Type == config.RPrompt && !needsPrimaryRPrompt {
continue
}
if e.renderBlock(block, cancelNewline) {
didRender = true
if e.Env.Shell() == shell.PWSH || e.Env.Shell() == shell.PWSH5 {
// For PowerShell, use a cached prompt if available, otherwise render a new prompt.
if e.Env.Flags().Cached && e.checkPromptCache() {
useCache = true
} else {
updateCache = true
}
}
if len(e.Config.ConsoleTitleTemplate) > 0 && !e.Env.Flags().Plain {
title := e.getTitleTemplateText()
e.write(terminal.FormatTitle(title))
}
if !useCache {
if e.Config.ShellIntegration {
exitCode, _ := e.Env.StatusCodes()
e.write(terminal.CommandFinished(exitCode, e.Env.Flags().NoExitCode))
e.write(terminal.PromptStart())
}
if e.Config.FinalSpace {
e.write(" ")
e.currentLineLength++
}
// cache a pointer to the color cycle
cycle = &e.Config.Cycle
var cancelNewline, didRender bool
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))
}
for i, block := range e.Config.Blocks {
// do not print a leading newline when we're at the first row and the prompt is cleared
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 {
e.write(terminal.CommandStart())
}
// skip setting a newline when we didn't print anything yet
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() {
case shell.ZSH:
@ -94,6 +100,16 @@ func (e *Engine) Primary() string {
return prompt
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 {
break
}

View file

@ -1,7 +1,6 @@
package prompt
import (
"slices"
"strings"
"github.com/jandedobbeleer/oh-my-posh/src/config"
@ -10,18 +9,6 @@ import (
)
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, " ")
tooltips := make([]*config.Segment, 0, 1)
@ -56,6 +43,15 @@ func (e *Engine) Tooltip(tip string) string {
switch e.Env.Shell() {
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.currentLineLength = e.Env.Flags().Column
space, ok := e.canWriteRightBlock(length, true)

View file

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

View file

@ -17,6 +17,9 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
# Check `ConstrainedLanguage` mode.
$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.
$script:OriginalPromptFunction = $Function:prompt
$script:OriginalContinuationPrompt = (Get-PSReadLineOption).ContinuationPrompt
@ -159,7 +162,22 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
$terminalWidth = Get-TerminalWidth
$cleanPSWD = Get-CleanPSWD
$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) {
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)
$executingCommand = $parseErrors.Count -eq 0
if ($executingCommand) {
$script:NewPrompt = $true
$script:TooltipCommand = ''
if ('::TRANSIENT::' -eq 'true') {
Set-TransientPrompt
@ -211,6 +230,7 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
[Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$start, [ref]$null)
# only render a transient prompt when no text is selected
if ($start -eq -1) {
$script:NewPrompt = $true
$script:TooltipCommand = ''
if ('::TRANSIENT::' -eq 'true') {
Set-TransientPrompt
@ -397,7 +417,7 @@ Example:
}
}
function prompt {
$promptFunction = {
# store the orignal last command execution status
if ($global:NVS_ORIGINAL_LASTEXECUTIONSTATUS -is [bool]) {
# make it compatible with NVS auto-switching, if enabled
@ -410,7 +430,14 @@ Example:
$script:OriginalLastExitCode = $global:LASTEXITCODE
Set-PoshPromptType
# Whether we should use a cached prompt.
$useCache = !$script:NewPrompt
if ($script:PromptType -ne 'transient') {
if ($script:NewPrompt) {
$script:NewPrompt = $false
}
Update-PoshErrorCode
}
$cleanPSWD = Get-CleanPSWD
@ -422,7 +449,21 @@ Example:
$env:POSH_CURSOR_LINE = $Host.UI.RawUI.CursorPosition.Y + 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
Set-PSReadLineOption -ExtraPromptLineCount (($standardOut | Measure-Object -Line).Lines - 1)
@ -436,6 +477,8 @@ Example:
$global:LASTEXITCODE = $script:OriginalLastExitCode
}
$Function:prompt = $promptFunction
# set secondary prompt
Set-PSReadLineOption -ContinuationPrompt ((Start-Utf8Process $script:OMPExecutable @("print", "secondary", "--config=$env:POSH_THEME", "--shell=$script:ShellName")) -join "`n")