oh-my-posh/src/engine/engine.go

472 lines
12 KiB
Go
Raw Normal View History

2022-01-26 23:38:46 -08:00
package engine
2019-03-13 04:14:30 -07:00
import (
"fmt"
"strings"
"time"
"github.com/jandedobbeleer/oh-my-posh/src/ansi"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/template"
2019-03-13 04:14:30 -07:00
)
2023-01-11 04:36:25 -08:00
var (
cycle *ansi.Cycle
)
2022-01-26 23:38:46 -08:00
type Engine struct {
Config *Config
Env platform.Environment
2023-01-04 11:44:29 -08:00
Writer *ansi.Writer
Plain bool
2021-04-20 12:30:46 -07:00
console strings.Builder
currentLineLength int
rprompt string
rpromptLength int
}
2022-01-26 23:38:46 -08:00
func (e *Engine) write(text string) {
2021-04-20 12:30:46 -07:00
e.console.WriteString(text)
}
2022-01-26 23:38:46 -08:00
func (e *Engine) string() string {
text := e.console.String()
e.console.Reset()
return text
2021-04-20 12:30:46 -07:00
}
func (e *Engine) canWriteRightBlock(rprompt bool) bool {
if rprompt && (e.rprompt == "" || e.Plain) {
return false
}
2022-01-26 23:38:46 -08:00
consoleWidth, err := e.Env.TerminalWidth()
2021-05-25 09:31:23 -07:00
if err != nil || consoleWidth == 0 {
return true
}
promptWidth := e.currentLineLength
availableSpace := consoleWidth - promptWidth
// spanning multiple lines
if availableSpace < 0 {
overflow := promptWidth % consoleWidth
availableSpace = consoleWidth - overflow
}
promptBreathingRoom := 5
if rprompt {
promptBreathingRoom = 30
}
canWrite := (availableSpace - e.rpromptLength) >= promptBreathingRoom
return canWrite
}
2022-03-12 13:04:08 -08:00
func (e *Engine) PrintPrimary() string {
// cache a pointer to the color cycle
2023-01-11 04:36:25 -08:00
cycle = &e.Config.Cycle
2022-01-26 23:38:46 -08:00
for _, block := range e.Config.Blocks {
e.renderBlock(block)
2019-03-13 04:14:30 -07:00
}
2022-03-14 11:14:10 -07:00
if len(e.Config.ConsoleTitleTemplate) > 0 {
title := e.getTitleTemplateText()
e.write(e.Writer.FormatTitle(title))
2020-10-12 00:02:33 -07:00
}
2022-01-26 23:38:46 -08:00
if e.Config.FinalSpace {
2021-04-20 12:30:46 -07:00
e.write(" ")
2019-03-13 04:14:30 -07:00
}
2022-07-13 04:53:55 -07:00
e.printPWD()
return e.print()
}
func (e *Engine) printPWD() {
if len(e.Config.PWD) == 0 && !e.Config.OSC99 {
return
2021-02-15 13:19:19 -08:00
}
2022-01-26 23:38:46 -08:00
cwd := e.Env.Pwd()
2022-07-13 04:53:55 -07:00
// Backwards compatibility for deprecated OSC99
if e.Config.OSC99 {
2023-01-04 11:44:29 -08:00
e.write(e.Writer.ConsolePwd(ansi.OSC99, "", "", cwd))
2022-07-13 04:53:55 -07:00
return
}
// Allow template logic to define when to enable the PWD (when supported)
tmpl := &template.Text{
Template: e.Config.PWD,
Env: e.Env,
}
pwdType, err := tmpl.Render()
if err != nil || len(pwdType) == 0 {
return
}
user := e.Env.User()
host, _ := e.Env.Host()
e.write(e.Writer.ConsolePwd(pwdType, user, host, cwd))
2020-12-17 23:59:45 -08:00
}
func (e *Engine) newline() {
2022-12-08 02:00:02 -08:00
// WARP terminal will remove \n from the prompt, so we hack a newline in
2022-12-08 02:30:21 -08:00
if e.isWarp() {
e.write(e.Writer.LineBreak())
2022-12-08 02:00:02 -08:00
} else {
e.write("\n")
}
e.currentLineLength = 0
}
2022-12-08 02:30:21 -08:00
func (e *Engine) isWarp() bool {
return e.Env.Getenv("TERM_PROGRAM") == "WarpTerminal"
}
2022-02-12 09:40:02 -08:00
func (e *Engine) shouldFill(block *Block, length int) (string, bool) {
if len(block.Filler) == 0 {
return "", false
}
terminalWidth, err := e.Env.TerminalWidth()
if err != nil || terminalWidth == 0 {
2022-02-12 09:40:02 -08:00
return "", false
}
padLength := terminalWidth - e.currentLineLength - length
if padLength <= 0 {
return "", false
}
e.Writer.Write("", "", block.Filler)
filler, lenFiller := e.Writer.String()
if lenFiller == 0 {
return "", false
}
repeat := padLength / lenFiller
return strings.Repeat(filler, repeat), true
}
func (e *Engine) getTitleTemplateText() string {
tmpl := &template.Text{
Template: e.Config.ConsoleTitleTemplate,
Env: e.Env,
}
if text, err := tmpl.Render(); err == nil {
return text
}
return ""
}
2022-01-26 23:38:46 -08:00
func (e *Engine) renderBlock(block *Block) {
defer func() {
2022-10-03 07:46:16 -07:00
// Due to a bug in PowerShell, the end of the line needs to be cleared.
// If this doesn't happen, the portion after the prompt gets colored in the background
// color of the line above the new input line. Clearing the line fixes this,
// but can hopefully one day be removed when this is resolved natively.
if e.Env.Shell() == shell.PWSH || e.Env.Shell() == shell.PWSH5 {
e.write(e.Writer.ClearAfter())
}
}()
// when in bash, for rprompt blocks we need to write plain
// and wrap in escaped mode or the prompt will not render correctly
if e.Env.Shell() == shell.BASH && (block.Type == RPrompt || block.Alignment == Right) {
block.InitPlain(e.Env, e.Config)
} else {
2023-01-11 04:36:25 -08:00
block.Init(e.Env, e.Writer)
}
if !block.Enabled() {
return
}
if block.Newline {
e.newline()
}
switch block.Type {
// This is deprecated but leave if to not break current configs
// It is encouraged to used "newline": true on block level
// rather than the standalone the linebreak block
case LineBreak:
e.newline()
case Prompt:
if block.VerticalOffset != 0 {
e.write(e.Writer.ChangeLine(block.VerticalOffset))
}
if block.Alignment == Left {
text, length := block.RenderSegments()
e.currentLineLength += length
e.write(text)
return
}
if block.Alignment != Right {
return
}
text, length := block.RenderSegments()
e.rpromptLength = length
if !e.canWriteRightBlock(false) {
switch block.Overflow {
case Break:
e.newline()
case Hide:
// make sure to fill if needed
if padText, OK := e.shouldFill(block, 0); OK {
e.write(padText)
}
return
}
}
if padText, OK := e.shouldFill(block, length); OK {
// in this case we can print plain
e.write(padText)
e.write(text)
return
}
// this can contain ANSI escape sequences
writer := e.Writer
2022-03-21 23:41:36 -07:00
if e.Env.Shell() == shell.BASH {
writer.Init(shell.GENERIC)
}
prompt := writer.CarriageForward()
prompt += writer.GetCursorForRightWrite(length, block.HorizontalOffset)
prompt += text
e.currentLineLength = 0
if e.Env.Shell() == shell.BASH {
prompt = e.Writer.FormatText(prompt)
}
e.write(prompt)
case RPrompt:
e.rprompt, e.rpromptLength = block.RenderSegments()
}
}
// debug will loop through your config file and output the timings for each segments
func (e *Engine) PrintDebug(startTime time.Time, version string) string {
var segmentTimings []*SegmentTiming
largestSegmentNameLength := 0
2022-01-26 23:38:46 -08:00
e.write(fmt.Sprintf("\n\x1b[1mVersion:\x1b[0m %s\n", version))
e.write("\n\x1b[1mSegments:\x1b[0m\n\n")
2021-01-14 21:10:36 -08:00
// console title timing
titleStartTime := time.Now()
title := e.getTitleTemplateText()
segmentTiming := &SegmentTiming{
2022-02-02 03:16:39 -08:00
name: "ConsoleTitle",
nameLength: 12,
2022-03-14 11:14:10 -07:00
active: len(e.Config.ConsoleTitleTemplate) > 0,
text: title,
duration: time.Since(titleStartTime),
2021-01-14 21:10:36 -08:00
}
segmentTimings = append(segmentTimings, segmentTiming)
// cache a pointer to the color cycle
2023-01-11 04:36:25 -08:00
cycle = &e.Config.Cycle
// loop each segments of each blocks
2022-01-26 23:38:46 -08:00
for _, block := range e.Config.Blocks {
2023-01-11 04:36:25 -08:00
block.Init(e.Env, e.Writer)
longestSegmentName, timings := block.Debug()
segmentTimings = append(segmentTimings, timings...)
if longestSegmentName > largestSegmentNameLength {
largestSegmentNameLength = longestSegmentName
}
}
2021-01-14 21:10:36 -08:00
// pad the output so the tabs render correctly
largestSegmentNameLength += 7
for _, segment := range segmentTimings {
2022-02-02 03:16:39 -08:00
duration := segment.duration.Milliseconds()
segmentName := fmt.Sprintf("%s(%t)", segment.name, segment.active)
e.write(fmt.Sprintf("%-*s - %3d ms - %s\n", largestSegmentNameLength, segmentName, duration, segment.text))
}
e.write(fmt.Sprintf("\n\x1b[1mRun duration:\x1b[0m %s\n", time.Since(startTime)))
2022-01-26 23:38:46 -08:00
e.write(fmt.Sprintf("\n\x1b[1mCache path:\x1b[0m %s\n", e.Env.CachePath()))
2022-05-07 07:43:24 -07:00
e.write(fmt.Sprintf("\n\x1b[1mConfig path:\x1b[0m %s\n", e.Env.Flags().Config))
e.write("\n\x1b[1mLogs:\x1b[0m\n\n")
2022-01-26 23:38:46 -08:00
e.write(e.Env.Logs())
2021-04-20 12:30:46 -07:00
return e.string()
}
2022-01-26 23:38:46 -08:00
func (e *Engine) print() string {
switch e.Env.Shell() {
2022-03-21 23:41:36 -07:00
case shell.ZSH:
2022-03-12 13:04:08 -08:00
if !e.Env.Flags().Eval {
break
2020-12-23 04:31:21 -08:00
}
2022-12-08 02:30:21 -08:00
// Warp doesn't support RPROMPT so we need to write it manually
if e.isWarp() {
e.write(e.Writer.SaveCursorPosition())
e.write(e.Writer.CarriageForward())
e.write(e.Writer.GetCursorForRightWrite(e.rpromptLength, 0))
2022-12-08 02:30:21 -08:00
e.write(e.rprompt)
e.write(e.Writer.RestoreCursorPosition())
2022-12-08 02:30:21 -08:00
// escape double quotes contained in the prompt
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
return prompt
}
// escape double quotes contained in the prompt
2022-04-14 12:12:09 -07:00
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt)
return prompt
case shell.PWSH, shell.PWSH5, shell.GENERIC, shell.NU:
if !e.canWriteRightBlock(true) {
break
2020-12-17 23:59:45 -08:00
}
e.write(e.Writer.SaveCursorPosition())
e.write(e.Writer.CarriageForward())
e.write(e.Writer.GetCursorForRightWrite(e.rpromptLength, 0))
e.write(e.rprompt)
e.write(e.Writer.RestoreCursorPosition())
case shell.BASH:
if !e.canWriteRightBlock(true) {
break
}
// in bash, the entire rprompt needs to be escaped for the prompt to be interpreted correctly
2022-12-28 08:30:48 -08:00
// see https://github.com/jandedobbeleer/oh-my-posh/pull/2398
2023-01-04 11:44:29 -08:00
writer := &ansi.Writer{}
writer.Init(shell.GENERIC)
prompt := writer.SaveCursorPosition()
prompt += writer.CarriageForward()
prompt += writer.GetCursorForRightWrite(e.rpromptLength, 0)
prompt += e.rprompt
prompt += writer.RestoreCursorPosition()
prompt = e.Writer.FormatText(prompt)
e.write(prompt)
2020-12-15 05:58:15 -08:00
}
2021-04-20 12:30:46 -07:00
return e.string()
2019-03-13 04:14:30 -07:00
}
2021-06-05 07:14:44 -07:00
2022-03-12 13:04:08 -08:00
func (e *Engine) PrintTooltip(tip string) string {
2021-06-05 07:14:44 -07:00
tip = strings.Trim(tip, " ")
var tooltip *Segment
2022-01-26 23:38:46 -08:00
for _, tp := range e.Config.Tooltips {
2021-06-05 07:14:44 -07:00
if !tp.shouldInvokeWithTip(tip) {
continue
}
tooltip = tp
}
if tooltip == nil {
return ""
}
2022-01-26 23:38:46 -08:00
if err := tooltip.mapSegmentWithWriter(e.Env); err != nil {
2021-06-05 07:14:44 -07:00
return ""
}
2022-02-02 03:16:39 -08:00
if !tooltip.writer.Enabled() {
2021-06-05 07:14:44 -07:00
return ""
}
tooltip.Enabled = true
2021-06-05 07:14:44 -07:00
// little hack to reuse the current logic
block := &Block{
Alignment: Right,
Segments: []*Segment{tooltip},
}
2022-01-26 23:38:46 -08:00
switch e.Env.Shell() {
case shell.ZSH, shell.CMD, shell.FISH, shell.GENERIC:
2023-01-11 04:36:25 -08:00
block.Init(e.Env, e.Writer)
if !block.Enabled() {
return ""
}
text, _ := block.RenderSegments()
return text
2022-03-21 23:41:36 -07:00
case shell.PWSH, shell.PWSH5:
block.InitPlain(e.Env, e.Config)
if !block.Enabled() {
return ""
}
text, length := block.RenderSegments()
e.write(e.Writer.ClearAfter())
e.write(e.Writer.CarriageForward())
e.write(e.Writer.GetCursorForRightWrite(length, 0))
e.write(text)
2021-06-05 07:14:44 -07:00
return e.string()
}
return ""
2021-06-05 07:14:44 -07:00
}
2021-06-15 12:23:08 -07:00
2022-02-19 07:49:26 -08:00
type ExtraPromptType int
const (
Transient ExtraPromptType = iota
Valid
Error
2022-02-20 04:56:28 -08:00
Secondary
2022-03-15 12:16:04 -07:00
Debug
2022-02-19 07:49:26 -08:00
)
2022-03-12 13:04:08 -08:00
func (e *Engine) PrintExtraPrompt(promptType ExtraPromptType) string {
// populate env with latest context
e.Env.LoadTemplateCache()
2022-03-27 06:38:12 -07:00
var prompt *Segment
2022-02-19 07:49:26 -08:00
switch promptType {
2022-03-15 12:16:04 -07:00
case Debug:
prompt = e.Config.DebugPrompt
2022-02-19 07:49:26 -08:00
case Transient:
prompt = e.Config.TransientPrompt
case Valid:
prompt = e.Config.ValidLine
case Error:
prompt = e.Config.ErrorLine
2022-02-20 04:56:28 -08:00
case Secondary:
prompt = e.Config.SecondaryPrompt
2022-02-19 07:49:26 -08:00
}
if prompt == nil {
2022-03-27 06:38:12 -07:00
prompt = &Segment{}
}
2022-02-19 07:49:26 -08:00
getTemplate := func(template string) string {
if len(template) != 0 {
return template
}
switch promptType { //nolint: exhaustive
2022-03-15 12:16:04 -07:00
case Debug:
return "[DBG]: "
2022-02-19 07:49:26 -08:00
case Transient:
return "{{ .Shell }}> "
2022-02-20 04:56:28 -08:00
case Secondary:
return "> "
2022-02-19 07:49:26 -08:00
default:
return ""
}
2021-06-15 12:23:08 -07:00
}
2022-01-26 06:54:36 -08:00
tmpl := &template.Text{
2022-02-19 07:49:26 -08:00
Template: getTemplate(prompt.Template),
2022-01-26 23:38:46 -08:00
Env: e.Env,
2021-06-15 12:23:08 -07:00
}
2022-02-19 07:49:26 -08:00
promptText, err := tmpl.Render()
if err != nil {
2022-02-19 07:49:26 -08:00
promptText = err.Error()
}
foreground := prompt.ForegroundTemplates.FirstMatch(nil, e.Env, prompt.Foreground)
background := prompt.BackgroundTemplates.FirstMatch(nil, e.Env, prompt.Background)
e.Writer.SetColors(background, foreground)
e.Writer.Write(background, foreground, promptText)
2022-01-26 23:38:46 -08:00
switch e.Env.Shell() {
2022-03-21 23:41:36 -07:00
case shell.ZSH:
2021-07-03 09:38:23 -07:00
// escape double quotes contained in the prompt
str, _ := e.Writer.String()
2022-02-20 04:56:28 -08:00
if promptType == Transient {
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(str, "\"", "\"\""))
// empty RPROMPT
prompt += "\nRPROMPT=\"\""
return prompt
}
return str
case shell.PWSH, shell.PWSH5, shell.CMD, shell.BASH, shell.FISH, shell.NU, shell.GENERIC:
// Return the string and empty our buffer
str, _ := e.Writer.String()
return str
}
2021-07-03 09:38:23 -07:00
return ""
2021-06-15 12:23:08 -07:00
}
2021-11-13 10:35:15 -08:00
2022-03-12 13:04:08 -08:00
func (e *Engine) PrintRPrompt() string {
2021-11-13 10:35:15 -08:00
filterRPromptBlock := func(blocks []*Block) *Block {
for _, block := range blocks {
if block.Type == RPrompt {
return block
}
}
return nil
}
2022-01-26 23:38:46 -08:00
block := filterRPromptBlock(e.Config.Blocks)
2021-11-13 10:35:15 -08:00
if block == nil {
return ""
}
2023-01-11 04:36:25 -08:00
block.Init(e.Env, e.Writer)
if !block.Enabled() {
2021-11-13 10:35:15 -08:00
return ""
}
text, length := block.RenderSegments()
e.rpromptLength = length
return text
2021-11-13 10:35:15 -08:00
}