oh-my-posh/src/engine.go
2021-11-19 00:08:17 +01:00

282 lines
7.4 KiB
Go

package main
import (
"fmt"
"strings"
"time"
)
type engine struct {
config *Config
env environmentInfo
writer promptWriter
ansi *ansiUtils
consoleTitle *consoleTitle
plain bool
console strings.Builder
rprompt string
}
func (e *engine) write(text string) {
e.console.WriteString(text)
}
func (e *engine) writeANSI(text string) {
if e.plain {
return
}
e.console.WriteString(text)
}
func (e *engine) string() string {
return e.console.String()
}
func (e *engine) canWriteRPrompt() bool {
prompt := e.string()
consoleWidth, err := e.env.getTerminalWidth()
if err != nil || consoleWidth == 0 {
return true
}
promptWidth := e.ansi.lenWithoutANSI(prompt)
availableSpace := consoleWidth - promptWidth
// spanning multiple lines
if availableSpace < 0 {
overflow := promptWidth % consoleWidth
availableSpace = consoleWidth - overflow
}
promptBreathingRoom := 30
canWrite := (availableSpace - e.ansi.lenWithoutANSI(e.rprompt)) >= promptBreathingRoom
return canWrite
}
func (e *engine) render() string {
for _, block := range e.config.Blocks {
e.renderBlock(block)
}
if e.config.ConsoleTitle {
e.writeANSI(e.consoleTitle.getConsoleTitle())
}
e.writeANSI(e.ansi.creset)
if e.config.FinalSpace {
e.write(" ")
}
if !e.config.OSC99 {
return e.print()
}
cwd := e.env.getcwd()
if e.env.isWsl() {
cwd, _ = e.env.runCommand("wslpath", "-m", cwd)
}
e.writeANSI(e.ansi.consolePwd(cwd))
return e.print()
}
func (e *engine) renderBlock(block *Block) {
// when in bash, for rprompt blocks we need to write plain
// and wrap in escaped mode or the prompt will not render correctly
if block.Type == RPrompt && e.env.getShellName() == bash {
block.initPlain(e.env, e.config)
} else {
block.init(e.env, e.writer, e.ansi)
}
block.setStringValues()
if !block.enabled() {
return
}
if block.Newline {
e.write("\n")
}
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.write("\n")
case Prompt:
if block.VerticalOffset != 0 {
e.writeANSI(e.ansi.changeLine(block.VerticalOffset))
}
switch block.Alignment {
case Right:
e.writeANSI(e.ansi.carriageForward())
blockText := block.renderSegments()
e.writeANSI(e.ansi.getCursorForRightWrite(blockText, block.HorizontalOffset))
e.write(blockText)
case Left:
e.write(block.renderSegments())
}
case RPrompt:
blockText := block.renderSegments()
if e.env.getShellName() == bash {
blockText = fmt.Sprintf(e.ansi.bashFormat, blockText)
}
e.rprompt = blockText
}
// 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.ansi.shell == pwsh || e.ansi.shell == powershell5 {
e.writeANSI(e.ansi.clearAfter())
}
}
// debug will loop through your config file and output the timings for each segments
func (e *engine) debug() string {
var segmentTimings []*SegmentTiming
largestSegmentNameLength := 0
e.write(fmt.Sprintf("\n\x1b[1mVersion:\x1b[0m %s\n", Version))
e.write("\n\x1b[1mSegments:\x1b[0m\n\n")
// console title timing
start := time.Now()
consoleTitle := e.consoleTitle.getTemplateText()
duration := time.Since(start)
segmentTiming := &SegmentTiming{
name: "ConsoleTitle",
nameLength: 12,
enabled: e.config.ConsoleTitle,
stringValue: consoleTitle,
enabledDuration: 0,
stringDuration: duration,
}
segmentTimings = append(segmentTimings, segmentTiming)
// loop each segments of each blocks
for _, block := range e.config.Blocks {
block.init(e.env, e.writer, e.ansi)
longestSegmentName, timings := block.debug()
segmentTimings = append(segmentTimings, timings...)
if longestSegmentName > largestSegmentNameLength {
largestSegmentNameLength = longestSegmentName
}
}
// pad the output so the tabs render correctly
largestSegmentNameLength += 7
for _, segment := range segmentTimings {
duration := segment.enabledDuration.Milliseconds()
if segment.enabled {
duration += segment.stringDuration.Milliseconds()
}
segmentName := fmt.Sprintf("%s(%t)", segment.name, segment.enabled)
e.write(fmt.Sprintf("%-*s - %3d ms - %s\n", largestSegmentNameLength, segmentName, duration, segment.stringValue))
}
e.write(fmt.Sprintf("\n\x1b[1mRun duration:\x1b[0m %s\n", time.Since(start)))
e.write("\n\x1b[1mLogs:\x1b[0m\n\n")
e.write(e.env.logs())
return e.string()
}
func (e *engine) print() string {
switch e.env.getShellName() {
case zsh:
if !*e.env.getArgs().Eval {
break
}
// escape double quotes contained in the prompt
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), "\"", "\"\""))
prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt)
return prompt
case pwsh, powershell5, bash, plain:
if e.rprompt == "" || !e.canWriteRPrompt() || e.plain {
break
}
e.write(e.ansi.saveCursorPosition)
e.write(e.ansi.carriageForward())
e.write(e.ansi.getCursorForRightWrite(e.rprompt, 0))
e.write(e.rprompt)
e.write(e.ansi.restoreCursorPosition)
}
return e.string()
}
func (e *engine) renderTooltip(tip string) string {
tip = strings.Trim(tip, " ")
var tooltip *Segment
for _, tp := range e.config.Tooltips {
if !tp.shouldInvokeWithTip(tip) {
continue
}
tooltip = tp
}
if tooltip == nil {
return ""
}
if err := tooltip.mapSegmentWithWriter(e.env); err != nil {
return ""
}
if !tooltip.enabled() {
return ""
}
tooltip.stringValue = tooltip.string()
// little hack to reuse the current logic
block := &Block{
Alignment: Right,
Segments: []*Segment{tooltip},
}
switch e.env.getShellName() {
case zsh, winCMD:
block.init(e.env, e.writer, e.ansi)
return block.renderSegments()
case pwsh, powershell5:
block.initPlain(e.env, e.config)
tooltipText := block.renderSegments()
e.write(e.ansi.clearAfter())
e.write(e.ansi.carriageForward())
e.write(e.ansi.getCursorForRightWrite(tooltipText, 0))
e.write(tooltipText)
return e.string()
}
return ""
}
func (e *engine) renderTransientPrompt() string {
if e.config.TransientPrompt == nil {
return ""
}
promptTemplate := e.config.TransientPrompt.Template
if len(promptTemplate) == 0 {
promptTemplate = "{{ .Shell }}> "
}
template := &textTemplate{
Template: promptTemplate,
Env: e.env,
}
prompt := template.renderPlainContextTemplate(nil)
e.writer.setColors(e.config.TransientPrompt.Background, e.config.TransientPrompt.Foreground)
e.writer.write(e.config.TransientPrompt.Background, e.config.TransientPrompt.Foreground, prompt)
switch e.env.getShellName() {
case zsh:
// escape double quotes contained in the prompt
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.writer.string(), "\"", "\"\""))
prompt += "\nRPROMPT=\"\""
return prompt
case pwsh, powershell5, winCMD:
return e.writer.string()
}
return ""
}
func (e *engine) renderRPrompt() string {
filterRPromptBlock := func(blocks []*Block) *Block {
for _, block := range blocks {
if block.Type == RPrompt {
return block
}
}
return nil
}
block := filterRPromptBlock(e.config.Blocks)
if block == nil {
return ""
}
block.init(e.env, e.writer, e.ansi)
block.setStringValues()
if !block.enabled() {
return ""
}
return block.renderSegments()
}