refactor: move engine to module

This commit is contained in:
Jan De Dobbeleer 2022-01-27 08:38:46 +01:00 committed by Jan De Dobbeleer
parent c0f4b6d6f0
commit fee29f4b2e
23 changed files with 311 additions and 293 deletions

2
.gitignore vendored
View file

@ -9,8 +9,6 @@
*.dll *.dll
*.so *.so
*.dylib *.dylib
# Initialization scripts generated by https://github.com/kevinburke/go-bindata
init.go
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test

View file

@ -1,4 +1,4 @@
package main package engine
import ( import (
"oh-my-posh/color" "oh-my-posh/color"
@ -56,7 +56,7 @@ func (b *Block) initPlain(env environment.Environment, config *Config) {
b.ansi.Init(plain) b.ansi.Init(plain)
b.writer = &color.AnsiWriter{ b.writer = &color.AnsiWriter{
Ansi: b.ansi, Ansi: b.ansi,
TerminalBackground: getConsoleBackgroundColor(env, config.TerminalBackground), TerminalBackground: GetConsoleBackgroundColor(env, config.TerminalBackground),
AnsiColors: config.MakeColors(env), AnsiColors: config.MakeColors(env),
} }
b.env = env b.env = env

View file

@ -1,4 +1,4 @@
package main package engine
import ( import (
"testing" "testing"

View file

@ -1,4 +1,4 @@
package main package engine
import ( import (
// "encoding/json" // "encoding/json"
@ -108,7 +108,7 @@ func loadConfig(env environment.Environment) (*Config, error) {
return &cfg, nil return &cfg, nil
} }
func exportConfig(configFile, format string) string { func ExportConfig(configFile, format string) string {
if len(format) == 0 { if len(format) == 0 {
format = config.JSON format = config.JSON
} }

View file

@ -1,4 +1,4 @@
package main package engine
import ( import (
"oh-my-posh/segments" "oh-my-posh/segments"
@ -11,7 +11,7 @@ import (
func TestSettingsExportJSON(t *testing.T) { func TestSettingsExportJSON(t *testing.T) {
defer testClearDefaultConfig() defer testClearDefaultConfig()
content := exportConfig("../themes/jandedobbeleer.omp.json", "json") content := ExportConfig("../themes/jandedobbeleer.omp.json", "json")
assert.NotContains(t, content, "\\u003ctransparent\\u003e") assert.NotContains(t, content, "\\u003ctransparent\\u003e")
assert.Contains(t, content, "<transparent>") assert.Contains(t, content, "<transparent>")
} }

View file

@ -1,4 +1,4 @@
package main package engine
import ( import (
"fmt" "fmt"
@ -10,40 +10,40 @@ import (
"time" "time"
) )
type engine struct { type Engine struct {
config *Config Config *Config
env environment.Environment Env environment.Environment
writer color.Writer Writer color.Writer
ansi *color.Ansi Ansi *color.Ansi
consoleTitle *console.Title ConsoleTitle *console.Title
plain bool Plain bool
console strings.Builder console strings.Builder
rprompt string rprompt string
} }
func (e *engine) write(text string) { func (e *Engine) write(text string) {
e.console.WriteString(text) e.console.WriteString(text)
} }
func (e *engine) writeANSI(text string) { func (e *Engine) writeANSI(text string) {
if e.plain { if e.Plain {
return return
} }
e.console.WriteString(text) e.console.WriteString(text)
} }
func (e *engine) string() string { func (e *Engine) string() string {
return e.console.String() return e.console.String()
} }
func (e *engine) canWriteRPrompt() bool { func (e *Engine) canWriteRPrompt() bool {
prompt := e.string() prompt := e.string()
consoleWidth, err := e.env.TerminalWidth() consoleWidth, err := e.Env.TerminalWidth()
if err != nil || consoleWidth == 0 { if err != nil || consoleWidth == 0 {
return true return true
} }
promptWidth := e.ansi.LenWithoutANSI(prompt) promptWidth := e.Ansi.LenWithoutANSI(prompt)
availableSpace := consoleWidth - promptWidth availableSpace := consoleWidth - promptWidth
// spanning multiple lines // spanning multiple lines
if availableSpace < 0 { if availableSpace < 0 {
@ -51,37 +51,37 @@ func (e *engine) canWriteRPrompt() bool {
availableSpace = consoleWidth - overflow availableSpace = consoleWidth - overflow
} }
promptBreathingRoom := 30 promptBreathingRoom := 30
canWrite := (availableSpace - e.ansi.LenWithoutANSI(e.rprompt)) >= promptBreathingRoom canWrite := (availableSpace - e.Ansi.LenWithoutANSI(e.rprompt)) >= promptBreathingRoom
return canWrite return canWrite
} }
func (e *engine) render() string { func (e *Engine) Render() string {
for _, block := range e.config.Blocks { for _, block := range e.Config.Blocks {
e.renderBlock(block) e.renderBlock(block)
} }
if e.config.ConsoleTitle { if e.Config.ConsoleTitle {
e.writeANSI(e.consoleTitle.GetTitle()) e.writeANSI(e.ConsoleTitle.GetTitle())
} }
e.writeANSI(e.ansi.ColorReset()) e.writeANSI(e.Ansi.ColorReset())
if e.config.FinalSpace { if e.Config.FinalSpace {
e.write(" ") e.write(" ")
} }
if !e.config.OSC99 { if !e.Config.OSC99 {
return e.print() return e.print()
} }
cwd := e.env.Pwd() cwd := e.Env.Pwd()
e.writeANSI(e.ansi.ConsolePwd(cwd)) e.writeANSI(e.Ansi.ConsolePwd(cwd))
return e.print() return e.print()
} }
func (e *engine) renderBlock(block *Block) { func (e *Engine) renderBlock(block *Block) {
// when in bash, for rprompt blocks we need to write plain // when in bash, for rprompt blocks we need to write plain
// and wrap in escaped mode or the prompt will not render correctly // and wrap in escaped mode or the prompt will not render correctly
if block.Type == RPrompt && e.env.Shell() == bash { if block.Type == RPrompt && e.Env.Shell() == bash {
block.initPlain(e.env, e.config) block.initPlain(e.Env, e.Config)
} else { } else {
block.init(e.env, e.writer, e.ansi) block.init(e.Env, e.Writer, e.Ansi)
} }
block.setStringValues() block.setStringValues()
if !block.enabled() { if !block.enabled() {
@ -98,21 +98,21 @@ func (e *engine) renderBlock(block *Block) {
e.write("\n") e.write("\n")
case Prompt: case Prompt:
if block.VerticalOffset != 0 { if block.VerticalOffset != 0 {
e.writeANSI(e.ansi.ChangeLine(block.VerticalOffset)) e.writeANSI(e.Ansi.ChangeLine(block.VerticalOffset))
} }
switch block.Alignment { switch block.Alignment {
case Right: case Right:
e.writeANSI(e.ansi.CarriageForward()) e.writeANSI(e.Ansi.CarriageForward())
blockText := block.renderSegments() blockText := block.renderSegments()
e.writeANSI(e.ansi.GetCursorForRightWrite(blockText, block.HorizontalOffset)) e.writeANSI(e.Ansi.GetCursorForRightWrite(blockText, block.HorizontalOffset))
e.write(blockText) e.write(blockText)
case Left: case Left:
e.write(block.renderSegments()) e.write(block.renderSegments())
} }
case RPrompt: case RPrompt:
blockText := block.renderSegments() blockText := block.renderSegments()
if e.env.Shell() == bash { if e.Env.Shell() == bash {
blockText = e.ansi.FormatText(blockText) blockText = e.Ansi.FormatText(blockText)
} }
e.rprompt = blockText e.rprompt = blockText
} }
@ -120,33 +120,33 @@ func (e *engine) renderBlock(block *Block) {
// If this doesn't happen, the portion after the prompt gets colored in the background // 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, // 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. // but can hopefully one day be removed when this is resolved natively.
if e.env.Shell() == pwsh || e.env.Shell() == powershell5 { if e.Env.Shell() == pwsh || e.Env.Shell() == powershell5 {
e.writeANSI(e.ansi.ClearAfter()) e.writeANSI(e.Ansi.ClearAfter())
} }
} }
// debug will loop through your config file and output the timings for each segments // debug will loop through your config file and output the timings for each segments
func (e *engine) debug() string { func (e *Engine) Debug(version string) string {
var segmentTimings []*SegmentTiming var segmentTimings []*SegmentTiming
largestSegmentNameLength := 0 largestSegmentNameLength := 0
e.write(fmt.Sprintf("\n\x1b[1mVersion:\x1b[0m %s\n", Version)) e.write(fmt.Sprintf("\n\x1b[1mVersion:\x1b[0m %s\n", version))
e.write("\n\x1b[1mSegments:\x1b[0m\n\n") e.write("\n\x1b[1mSegments:\x1b[0m\n\n")
// console title timing // console title timing
start := time.Now() start := time.Now()
consoleTitle := e.consoleTitle.GetTitle() consoleTitle := e.ConsoleTitle.GetTitle()
duration := time.Since(start) duration := time.Since(start)
segmentTiming := &SegmentTiming{ segmentTiming := &SegmentTiming{
name: "ConsoleTitle", name: "ConsoleTitle",
nameLength: 12, nameLength: 12,
enabled: e.config.ConsoleTitle, enabled: e.Config.ConsoleTitle,
stringValue: consoleTitle, stringValue: consoleTitle,
enabledDuration: 0, enabledDuration: 0,
stringDuration: duration, stringDuration: duration,
} }
segmentTimings = append(segmentTimings, segmentTiming) segmentTimings = append(segmentTimings, segmentTiming)
// loop each segments of each blocks // loop each segments of each blocks
for _, block := range e.config.Blocks { for _, block := range e.Config.Blocks {
block.init(e.env, e.writer, e.ansi) block.init(e.Env, e.Writer, e.Ansi)
longestSegmentName, timings := block.debug() longestSegmentName, timings := block.debug()
segmentTimings = append(segmentTimings, timings...) segmentTimings = append(segmentTimings, timings...)
if longestSegmentName > largestSegmentNameLength { if longestSegmentName > largestSegmentNameLength {
@ -165,16 +165,16 @@ func (e *engine) debug() string {
e.write(fmt.Sprintf("%-*s - %3d ms - %s\n", largestSegmentNameLength, segmentName, duration, segment.stringValue)) 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(fmt.Sprintf("\n\x1b[1mRun duration:\x1b[0m %s\n", time.Since(start)))
e.write(fmt.Sprintf("\n\x1b[1mCache path:\x1b[0m %s\n", e.env.CachePath())) e.write(fmt.Sprintf("\n\x1b[1mCache path:\x1b[0m %s\n", e.Env.CachePath()))
e.write("\n\x1b[1mLogs:\x1b[0m\n\n") e.write("\n\x1b[1mLogs:\x1b[0m\n\n")
e.write(e.env.Logs()) e.write(e.Env.Logs())
return e.string() return e.string()
} }
func (e *engine) print() string { func (e *Engine) print() string {
switch e.env.Shell() { switch e.Env.Shell() {
case zsh: case zsh:
if !*e.env.Args().Eval { if !*e.Env.Args().Eval {
break break
} }
// escape double quotes contained in the prompt // escape double quotes contained in the prompt
@ -182,22 +182,22 @@ func (e *engine) print() string {
prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt) prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt)
return prompt return prompt
case pwsh, powershell5, bash, plain: case pwsh, powershell5, bash, plain:
if e.rprompt == "" || !e.canWriteRPrompt() || e.plain { if e.rprompt == "" || !e.canWriteRPrompt() || e.Plain {
break break
} }
e.write(e.ansi.SaveCursorPosition()) e.write(e.Ansi.SaveCursorPosition())
e.write(e.ansi.CarriageForward()) e.write(e.Ansi.CarriageForward())
e.write(e.ansi.GetCursorForRightWrite(e.rprompt, 0)) e.write(e.Ansi.GetCursorForRightWrite(e.rprompt, 0))
e.write(e.rprompt) e.write(e.rprompt)
e.write(e.ansi.RestoreCursorPosition()) e.write(e.Ansi.RestoreCursorPosition())
} }
return e.string() return e.string()
} }
func (e *engine) renderTooltip(tip string) string { func (e *Engine) RenderTooltip(tip string) string {
tip = strings.Trim(tip, " ") tip = strings.Trim(tip, " ")
var tooltip *Segment var tooltip *Segment
for _, tp := range e.config.Tooltips { for _, tp := range e.Config.Tooltips {
if !tp.shouldInvokeWithTip(tip) { if !tp.shouldInvokeWithTip(tip) {
continue continue
} }
@ -206,7 +206,7 @@ func (e *engine) renderTooltip(tip string) string {
if tooltip == nil { if tooltip == nil {
return "" return ""
} }
if err := tooltip.mapSegmentWithWriter(e.env); err != nil { if err := tooltip.mapSegmentWithWriter(e.Env); err != nil {
return "" return ""
} }
if !tooltip.enabled() { if !tooltip.enabled() {
@ -218,53 +218,53 @@ func (e *engine) renderTooltip(tip string) string {
Alignment: Right, Alignment: Right,
Segments: []*Segment{tooltip}, Segments: []*Segment{tooltip},
} }
switch e.env.Shell() { switch e.Env.Shell() {
case zsh, winCMD: case zsh, winCMD:
block.init(e.env, e.writer, e.ansi) block.init(e.Env, e.Writer, e.Ansi)
return block.renderSegments() return block.renderSegments()
case pwsh, powershell5: case pwsh, powershell5:
block.initPlain(e.env, e.config) block.initPlain(e.Env, e.Config)
tooltipText := block.renderSegments() tooltipText := block.renderSegments()
e.write(e.ansi.ClearAfter()) e.write(e.Ansi.ClearAfter())
e.write(e.ansi.CarriageForward()) e.write(e.Ansi.CarriageForward())
e.write(e.ansi.GetCursorForRightWrite(tooltipText, 0)) e.write(e.Ansi.GetCursorForRightWrite(tooltipText, 0))
e.write(tooltipText) e.write(tooltipText)
return e.string() return e.string()
} }
return "" return ""
} }
func (e *engine) renderTransientPrompt() string { func (e *Engine) RenderTransientPrompt() string {
if e.config.TransientPrompt == nil { if e.Config.TransientPrompt == nil {
return "" return ""
} }
promptTemplate := e.config.TransientPrompt.Template promptTemplate := e.Config.TransientPrompt.Template
if len(promptTemplate) == 0 { if len(promptTemplate) == 0 {
promptTemplate = "{{ .Shell }}> " promptTemplate = "{{ .Shell }}> "
} }
tmpl := &template.Text{ tmpl := &template.Text{
Template: promptTemplate, Template: promptTemplate,
Env: e.env, Env: e.Env,
} }
prompt, err := tmpl.Render() prompt, err := tmpl.Render()
if err != nil { if err != nil {
prompt = err.Error() prompt = err.Error()
} }
e.writer.SetColors(e.config.TransientPrompt.Background, e.config.TransientPrompt.Foreground) e.Writer.SetColors(e.Config.TransientPrompt.Background, e.Config.TransientPrompt.Foreground)
e.writer.Write(e.config.TransientPrompt.Background, e.config.TransientPrompt.Foreground, prompt) e.Writer.Write(e.Config.TransientPrompt.Background, e.Config.TransientPrompt.Foreground, prompt)
switch e.env.Shell() { switch e.Env.Shell() {
case zsh: case zsh:
// escape double quotes contained in the prompt // escape double quotes contained in the prompt
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.writer.String(), "\"", "\"\"")) prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.Writer.String(), "\"", "\"\""))
prompt += "\nRPROMPT=\"\"" prompt += "\nRPROMPT=\"\""
return prompt return prompt
case pwsh, powershell5, winCMD: case pwsh, powershell5, winCMD:
return e.writer.String() return e.Writer.String()
} }
return "" return ""
} }
func (e *engine) renderRPrompt() string { func (e *Engine) RenderRPrompt() string {
filterRPromptBlock := func(blocks []*Block) *Block { filterRPromptBlock := func(blocks []*Block) *Block {
for _, block := range blocks { for _, block := range blocks {
if block.Type == RPrompt { if block.Type == RPrompt {
@ -273,11 +273,11 @@ func (e *engine) renderRPrompt() string {
} }
return nil return nil
} }
block := filterRPromptBlock(e.config.Blocks) block := filterRPromptBlock(e.Config.Blocks)
if block == nil { if block == nil {
return "" return ""
} }
block.init(e.env, e.writer, e.ansi) block.init(e.Env, e.Writer, e.Ansi)
block.setStringValues() block.setStringValues()
if !block.enabled() { if !block.enabled() {
return "" return ""

View file

@ -1,4 +1,4 @@
package main package engine
import ( import (
"errors" "errors"
@ -37,9 +37,9 @@ func TestCanWriteRPrompt(t *testing.T) {
env.On("TerminalWidth").Return(tc.TerminalWidth, tc.TerminalWidthError) env.On("TerminalWidth").Return(tc.TerminalWidth, tc.TerminalWidthError)
ansi := &color.Ansi{} ansi := &color.Ansi{}
ansi.Init(plain) ansi.Init(plain)
engine := &engine{ engine := &Engine{
env: env, Env: env,
ansi: ansi, Ansi: ansi,
} }
engine.rprompt = strings.Repeat("x", tc.RPromptLength) engine.rprompt = strings.Repeat("x", tc.RPromptLength)
engine.console.WriteString(strings.Repeat("x", tc.PromptLength)) engine.console.WriteString(strings.Repeat("x", tc.PromptLength))
@ -101,7 +101,7 @@ func engineRender(configPath string) error {
writerColors := cfg.MakeColors(env) writerColors := cfg.MakeColors(env)
writer := &color.AnsiWriter{ writer := &color.AnsiWriter{
Ansi: ansi, Ansi: ansi,
TerminalBackground: getConsoleBackgroundColor(env, cfg.TerminalBackground), TerminalBackground: GetConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: writerColors, AnsiColors: writerColors,
} }
consoleTitle := &console.Title{ consoleTitle := &console.Title{
@ -110,16 +110,16 @@ func engineRender(configPath string) error {
Style: cfg.ConsoleTitleStyle, Style: cfg.ConsoleTitleStyle,
Template: cfg.ConsoleTitleTemplate, Template: cfg.ConsoleTitleTemplate,
} }
engine := &engine{ engine := &Engine{
config: cfg, Config: cfg,
env: env, Env: env,
writer: writer, Writer: writer,
consoleTitle: consoleTitle, ConsoleTitle: consoleTitle,
ansi: ansi, Ansi: ansi,
plain: *args.Plain, Plain: *args.Plain,
} }
engine.render() engine.Render()
return nil return nil
} }

View file

@ -20,7 +20,7 @@
// https://github.com/homeport/termshot // https://github.com/homeport/termshot
package main package engine
import ( import (
_ "embed" _ "embed"
@ -97,10 +97,14 @@ func NewRGBColor(ansiColor string) *RGB {
} }
type ImageRenderer struct { type ImageRenderer struct {
ansiString string AnsiString string
author string Author string
ansi *color.Ansi CursorPadding int
bgColor string RPromptOffset int
BgColor string
Ansi *color.Ansi
path string
factor float64 factor float64
@ -128,11 +132,12 @@ type ImageRenderer struct {
backgroundColor *RGB backgroundColor *RGB
foregroundColor *RGB foregroundColor *RGB
ansiSequenceRegexMap map[string]string ansiSequenceRegexMap map[string]string
rPromptOffset int
cursorPadding int
} }
func (ir *ImageRenderer) init() { func (ir *ImageRenderer) Init(config string) {
match := regex.FindNamedRegexMatch(`.*(\/|\\)(?P<STR>.+).omp.(json|yaml|toml)`, config)
ir.path = fmt.Sprintf("%s.png", match[str])
f := 2.0 f := 2.0
ir.cleanContent() ir.cleanContent()
@ -244,8 +249,8 @@ func (ir *ImageRenderer) runeAdditionalWidth(r rune) int {
func (ir *ImageRenderer) calculateWidth() int { func (ir *ImageRenderer) calculateWidth() int {
longest := 0 longest := 0
for _, line := range strings.Split(ir.ansiString, "\n") { for _, line := range strings.Split(ir.AnsiString, "\n") {
length := ir.ansi.LenWithoutANSI(line) length := ir.Ansi.LenWithoutANSI(line)
for _, char := range line { for _, char := range line {
length += ir.runeAdditionalWidth(char) length += ir.runeAdditionalWidth(char)
} }
@ -258,27 +263,27 @@ func (ir *ImageRenderer) calculateWidth() int {
func (ir *ImageRenderer) cleanContent() { func (ir *ImageRenderer) cleanContent() {
rPromptAnsi := "\x1b7\x1b[1000C" rPromptAnsi := "\x1b7\x1b[1000C"
hasRPrompt := strings.Contains(ir.ansiString, rPromptAnsi) hasRPrompt := strings.Contains(ir.AnsiString, rPromptAnsi)
// clean abundance of empty lines // clean abundance of empty lines
ir.ansiString = strings.Trim(ir.ansiString, "\n") ir.AnsiString = strings.Trim(ir.AnsiString, "\n")
ir.ansiString = "\n" + ir.ansiString ir.AnsiString = "\n" + ir.AnsiString
// clean string before render // clean string before render
ir.ansiString = strings.ReplaceAll(ir.ansiString, "\x1b[m", "\x1b[0m") ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[m", "\x1b[0m")
ir.ansiString = strings.ReplaceAll(ir.ansiString, "\x1b[K", "") ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[K", "")
ir.ansiString = strings.ReplaceAll(ir.ansiString, "\x1b[1F", "") ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[1F", "")
ir.ansiString = strings.ReplaceAll(ir.ansiString, "\x1b8", "") ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b8", "")
ir.ansiString = strings.ReplaceAll(ir.ansiString, "\u2800", " ") ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\u2800", " ")
// replace rprompt with adding and mark right aligned blocks with a pointer // replace rprompt with adding and mark right aligned blocks with a pointer
ir.ansiString = strings.ReplaceAll(ir.ansiString, rPromptAnsi, fmt.Sprintf("_%s", strings.Repeat(" ", ir.cursorPadding))) ir.AnsiString = strings.ReplaceAll(ir.AnsiString, rPromptAnsi, fmt.Sprintf("_%s", strings.Repeat(" ", ir.CursorPadding)))
ir.ansiString = strings.ReplaceAll(ir.ansiString, "\x1b[1000C", strings.Repeat(" ", ir.rPromptOffset)) ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[1000C", strings.Repeat(" ", ir.RPromptOffset))
if !hasRPrompt { if !hasRPrompt {
ir.ansiString += fmt.Sprintf("_%s", strings.Repeat(" ", ir.cursorPadding)) ir.AnsiString += fmt.Sprintf("_%s", strings.Repeat(" ", ir.CursorPadding))
} }
// add watermarks // add watermarks
ir.ansiString += "\n\n\x1b[1mhttps://ohmyposh.dev\x1b[22m" ir.AnsiString += "\n\n\x1b[1mhttps://ohmyposh.dev\x1b[22m"
if len(ir.author) > 0 { if len(ir.Author) > 0 {
createdBy := fmt.Sprintf(" by \x1b[1m%s\x1b[22m", ir.author) createdBy := fmt.Sprintf(" by \x1b[1m%s\x1b[22m", ir.Author)
ir.ansiString += createdBy ir.AnsiString += createdBy
} }
} }
@ -290,11 +295,11 @@ func (ir *ImageRenderer) measureContent() (width, height float64) {
advance := tmpDrawer.MeasureString(strings.Repeat(" ", linewidth)) advance := tmpDrawer.MeasureString(strings.Repeat(" ", linewidth))
width = float64(advance >> 6) width = float64(advance >> 6)
// height, lines times font height and line spacing // height, lines times font height and line spacing
height = float64(len(strings.Split(ir.ansiString, "\n"))) * ir.fontHeight() * ir.lineSpacing height = float64(len(strings.Split(ir.AnsiString, "\n"))) * ir.fontHeight() * ir.lineSpacing
return width, height return width, height
} }
func (ir *ImageRenderer) SavePNG(path string) error { func (ir *ImageRenderer) SavePNG() error {
var f = func(value float64) float64 { return ir.factor * value } var f = func(value float64) float64 { return ir.factor * value }
var ( var (
@ -345,7 +350,7 @@ func (ir *ImageRenderer) SavePNG(path string) error {
// Draw rounded rectangle with outline and three button to produce the // Draw rounded rectangle with outline and three button to produce the
// impression of a window with controls and a content area // impression of a window with controls and a content area
dc.DrawRoundedRectangle(xOffset, yOffset, width-2*marginX, height-2*marginY, corner) dc.DrawRoundedRectangle(xOffset, yOffset, width-2*marginX, height-2*marginY, corner)
dc.SetHexColor(ir.bgColor) dc.SetHexColor(ir.BgColor)
dc.Fill() dc.Fill()
dc.DrawRoundedRectangle(xOffset, yOffset, width-2*marginX, height-2*marginY, corner) dc.DrawRoundedRectangle(xOffset, yOffset, width-2*marginX, height-2*marginY, corner)
@ -362,16 +367,16 @@ func (ir *ImageRenderer) SavePNG(path string) error {
// Apply the actual text into the prepared content area of the window // Apply the actual text into the prepared content area of the window
var x, y float64 = xOffset + paddingX, yOffset + paddingY + titleOffset + ir.fontHeight() var x, y float64 = xOffset + paddingX, yOffset + paddingY + titleOffset + ir.fontHeight()
for len(ir.ansiString) != 0 { for len(ir.AnsiString) != 0 {
if !ir.shouldPrint() { if !ir.shouldPrint() {
continue continue
} }
runes := []rune(ir.ansiString) runes := []rune(ir.AnsiString)
if len(runes) == 0 { if len(runes) == 0 {
continue continue
} }
str := string(runes[0:1]) str := string(runes[0:1])
ir.ansiString = string(runes[1:]) ir.AnsiString = string(runes[1:])
switch ir.style { switch ir.style {
case bold: case bold:
dc.SetFontFace(ir.bold) dc.SetFontFace(ir.bold)
@ -419,16 +424,16 @@ func (ir *ImageRenderer) SavePNG(path string) error {
x += w x += w
} }
return dc.SavePNG(path) return dc.SavePNG(ir.path)
} }
func (ir *ImageRenderer) shouldPrint() bool { func (ir *ImageRenderer) shouldPrint() bool {
for sequence, re := range ir.ansiSequenceRegexMap { for sequence, re := range ir.ansiSequenceRegexMap {
match := regex.FindNamedRegexMatch(re, ir.ansiString) match := regex.FindNamedRegexMatch(re, ir.AnsiString)
if len(match) == 0 { if len(match) == 0 {
continue continue
} }
ir.ansiString = strings.TrimPrefix(ir.ansiString, match[str]) ir.AnsiString = strings.TrimPrefix(ir.AnsiString, match[str])
switch sequence { switch sequence {
case invertedColor: case invertedColor:
ir.foregroundColor = ir.defaultBackgroundColor ir.foregroundColor = ir.defaultBackgroundColor
@ -463,7 +468,7 @@ func (ir *ImageRenderer) shouldPrint() bool {
ir.setBase16Color(match[fg]) ir.setBase16Color(match[fg])
return false return false
case link: case link:
ir.ansiString = match[url] + ir.ansiString ir.AnsiString = match[url] + ir.AnsiString
} }
} }
return true return true

View file

@ -1,4 +1,4 @@
package main package engine
import ( import (
"io/ioutil" "io/ioutil"
@ -10,7 +10,7 @@ import (
) )
func runImageTest(content string) error { func runImageTest(content string) error {
poshImagePath := "ohmyposh.png" poshImagePath := "jandedobbeleer.png"
file, err := ioutil.TempFile("", poshImagePath) file, err := ioutil.TempFile("", poshImagePath)
if err != nil { if err != nil {
return err return err
@ -19,11 +19,11 @@ func runImageTest(content string) error {
ansi := &color.Ansi{} ansi := &color.Ansi{}
ansi.Init(plain) ansi.Init(plain)
image := &ImageRenderer{ image := &ImageRenderer{
ansiString: content, AnsiString: content,
ansi: ansi, Ansi: ansi,
} }
image.init() image.Init("~/jandedobbeleer.omp.json")
err = image.SavePNG(poshImagePath) err = image.SavePNG()
return err return err
} }

112
src/engine/init.go Normal file
View file

@ -0,0 +1,112 @@
package engine
import (
_ "embed"
"fmt"
"oh-my-posh/environment"
"oh-my-posh/template"
"os"
"strings"
)
//go:embed init/omp.ps1
var pwshInit string
//go:embed init/omp.fish
var fishInit string
//go:embed init/omp.bash
var bashInit string
//go:embed init/omp.zsh
var zshInit string
//go:embed init/omp.lua
var cmdInit string
const (
noExe = "echo \"Unable to find Oh My Posh executable\""
zsh = "zsh"
bash = "bash"
pwsh = "pwsh"
fish = "fish"
powershell5 = "powershell"
winCMD = "cmd"
plain = "shell"
)
func getExecutablePath(shell string) (string, error) {
executable, err := os.Executable()
if err != nil {
return "", err
}
// On Windows, it fails when the excutable is called in MSYS2 for example
// which uses unix style paths to resolve the executable's location.
// PowerShell knows how to resolve both, so we can swap this without any issue.
executable = strings.ReplaceAll(executable, "\\", "/")
switch shell {
case bash, zsh:
return strings.ReplaceAll(executable, " ", "\\ "), nil
}
return executable, nil
}
func InitShell(shell, configFile string) string {
executable, err := getExecutablePath(shell)
if err != nil {
return noExe
}
switch shell {
case pwsh, powershell5:
return fmt.Sprintf("(@(&\"%s\" --print-init --shell=%s --config=\"%s\") -join \"`n\") | Invoke-Expression", executable, shell, configFile)
case zsh, bash, fish, winCMD:
return PrintShellInit(shell, configFile)
default:
return fmt.Sprintf("echo \"No initialization script available for %s\"", shell)
}
}
func PrintShellInit(shell, configFile string) string {
executable, err := getExecutablePath(shell)
if err != nil {
return noExe
}
switch shell {
case pwsh, powershell5:
return getShellInitScript(executable, configFile, pwshInit)
case zsh:
return getShellInitScript(executable, configFile, zshInit)
case bash:
return getShellInitScript(executable, configFile, bashInit)
case fish:
return getShellInitScript(executable, configFile, fishInit)
case winCMD:
return getShellInitScript(executable, configFile, cmdInit)
default:
return fmt.Sprintf("echo \"No initialization script available for %s\"", shell)
}
}
func getShellInitScript(executable, configFile, script string) string {
script = strings.ReplaceAll(script, "::OMP::", executable)
script = strings.ReplaceAll(script, "::CONFIG::", configFile)
return script
}
func GetConsoleBackgroundColor(env environment.Environment, backgroundColorTemplate string) string {
if len(backgroundColorTemplate) == 0 {
return backgroundColorTemplate
}
tmpl := &template.Text{
Template: backgroundColorTemplate,
Context: nil,
Env: env,
}
text, err := tmpl.Render()
if err != nil {
return err.Error()
}
return text
}

31
src/engine/init_test.go Normal file
View file

@ -0,0 +1,31 @@
package engine
import (
"oh-my-posh/environment"
"oh-my-posh/mock"
"testing"
"github.com/stretchr/testify/assert"
)
func TestConsoleBackgroundColorTemplate(t *testing.T) {
cases := []struct {
Case string
Expected string
Term string
}{
{Case: "Inside vscode", Expected: "#123456", Term: "vscode"},
{Case: "Outside vscode", Expected: "", Term: "windowsterminal"},
}
for _, tc := range cases {
env := new(mock.MockedEnvironment)
env.On("TemplateCache").Return(&environment.TemplateCache{
Env: map[string]string{
"TERM_PROGRAM": tc.Term,
},
})
color := GetConsoleBackgroundColor(env, "{{ if eq \"vscode\" .Env.TERM_PROGRAM }}#123456{{end}}")
assert.Equal(t, tc.Expected, color, tc.Case)
}
}

View file

@ -1,4 +1,4 @@
package main package engine
import ( import (
"errors" "errors"

View file

@ -1,4 +1,4 @@
package main package engine
import ( import (
"encoding/json" "encoding/json"

View file

@ -1,16 +1,12 @@
package main package main
import ( import (
_ "embed"
"flag" "flag"
"fmt" "fmt"
"oh-my-posh/color" "oh-my-posh/color"
"oh-my-posh/console" "oh-my-posh/console"
"oh-my-posh/engine"
"oh-my-posh/environment" "oh-my-posh/environment"
"oh-my-posh/regex"
"oh-my-posh/template"
"os"
"strings"
"time" "time"
"github.com/gookit/config/v2" "github.com/gookit/config/v2"
@ -19,33 +15,6 @@ import (
// Version number of oh-my-posh // Version number of oh-my-posh
var Version = "development" var Version = "development"
//go:embed init/omp.ps1
var pwshInit string
//go:embed init/omp.fish
var fishInit string
//go:embed init/omp.bash
var bashInit string
//go:embed init/omp.zsh
var zshInit string
//go:embed init/omp.lua
var cmdInit string
const (
noExe = "echo \"Unable to find Oh My Posh executable\""
zsh = "zsh"
bash = "bash"
pwsh = "pwsh"
fish = "fish"
powershell5 = "powershell"
winCMD = "cmd"
plain = "shell"
)
func main() { func main() {
args := &environment.Args{ args := &environment.Args{
ErrorCode: flag.Int( ErrorCode: flag.Int(
@ -174,20 +143,20 @@ func main() {
return return
} }
if *args.Init { if *args.Init {
init := initShell(*args.Shell, *args.Config) init := engine.InitShell(*args.Shell, *args.Config)
fmt.Print(init) fmt.Print(init)
return return
} }
if *args.PrintInit { if *args.PrintInit {
init := printShellInit(*args.Shell, *args.Config) init := engine.PrintShellInit(*args.Shell, *args.Config)
fmt.Print(init) fmt.Print(init)
return return
} }
if *args.PrintConfig { if *args.PrintConfig {
fmt.Print(exportConfig(*args.Config, *args.ConfigFormat)) fmt.Print(engine.ExportConfig(*args.Config, *args.ConfigFormat))
return return
} }
cfg := GetConfig(env) cfg := engine.GetConfig(env)
ansi := &color.Ansi{} ansi := &color.Ansi{}
ansi.Init(env.Shell()) ansi.Init(env.Shell())
var writer color.Writer var writer color.Writer
@ -197,7 +166,7 @@ func main() {
writerColors := cfg.MakeColors(env) writerColors := cfg.MakeColors(env)
writer = &color.AnsiWriter{ writer = &color.AnsiWriter{
Ansi: ansi, Ansi: ansi,
TerminalBackground: getConsoleBackgroundColor(env, cfg.TerminalBackground), TerminalBackground: engine.GetConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: writerColors, AnsiColors: writerColors,
} }
} }
@ -207,121 +176,46 @@ func main() {
Template: cfg.ConsoleTitleTemplate, Template: cfg.ConsoleTitleTemplate,
Style: cfg.ConsoleTitleStyle, Style: cfg.ConsoleTitleStyle,
} }
engine := &engine{ eng := &engine.Engine{
config: cfg, Config: cfg,
env: env, Env: env,
writer: writer, Writer: writer,
consoleTitle: consoleTitle, ConsoleTitle: consoleTitle,
ansi: ansi, Ansi: ansi,
plain: *args.Plain, Plain: *args.Plain,
} }
if *args.Debug { if *args.Debug {
fmt.Print(engine.debug()) fmt.Print(eng.Debug(Version))
return return
} }
if *args.PrintTransient { if *args.PrintTransient {
fmt.Print(engine.renderTransientPrompt()) fmt.Print(eng.RenderTransientPrompt())
return return
} }
if len(*args.Command) != 0 { if len(*args.Command) != 0 {
fmt.Print(engine.renderTooltip(*args.Command)) fmt.Print(eng.RenderTooltip(*args.Command))
return return
} }
if *args.RPrompt { if *args.RPrompt {
fmt.Print(engine.renderRPrompt()) fmt.Print(eng.RenderRPrompt())
return return
} }
prompt := engine.render() prompt := eng.Render()
if !*args.ExportPNG { if !*args.ExportPNG {
fmt.Print(prompt) fmt.Print(prompt)
return return
} }
imageCreator := &ImageRenderer{ imageCreator := &engine.ImageRenderer{
ansiString: prompt, AnsiString: prompt,
author: *args.Author, Author: *args.Author,
cursorPadding: *args.CursorPadding, CursorPadding: *args.CursorPadding,
rPromptOffset: *args.RPromptOffset, RPromptOffset: *args.RPromptOffset,
bgColor: *args.BGColor, BgColor: *args.BGColor,
ansi: ansi, Ansi: ansi,
} }
imageCreator.init() imageCreator.Init(*args.Config)
match := regex.FindNamedRegexMatch(`.*(\/|\\)(?P<STR>.+).omp.(json|yaml|toml)`, *args.Config) err := imageCreator.SavePNG()
err := imageCreator.SavePNG(fmt.Sprintf("%s.png", match[str]))
if err != nil { if err != nil {
fmt.Print(err.Error()) fmt.Print(err.Error())
} }
} }
func getExecutablePath(shell string) (string, error) {
executable, err := os.Executable()
if err != nil {
return "", err
}
// On Windows, it fails when the excutable is called in MSYS2 for example
// which uses unix style paths to resolve the executable's location.
// PowerShell knows how to resolve both, so we can swap this without any issue.
executable = strings.ReplaceAll(executable, "\\", "/")
switch shell {
case bash, zsh:
return strings.ReplaceAll(executable, " ", "\\ "), nil
}
return executable, nil
}
func initShell(shell, configFile string) string {
executable, err := getExecutablePath(shell)
if err != nil {
return noExe
}
switch shell {
case pwsh, powershell5:
return fmt.Sprintf("(@(&\"%s\" --print-init --shell=%s --config=\"%s\") -join \"`n\") | Invoke-Expression", executable, shell, configFile)
case zsh, bash, fish, winCMD:
return printShellInit(shell, configFile)
default:
return fmt.Sprintf("echo \"No initialization script available for %s\"", shell)
}
}
func printShellInit(shell, configFile string) string {
executable, err := getExecutablePath(shell)
if err != nil {
return noExe
}
switch shell {
case pwsh, powershell5:
return getShellInitScript(executable, configFile, pwshInit)
case zsh:
return getShellInitScript(executable, configFile, zshInit)
case bash:
return getShellInitScript(executable, configFile, bashInit)
case fish:
return getShellInitScript(executable, configFile, fishInit)
case winCMD:
return getShellInitScript(executable, configFile, cmdInit)
default:
return fmt.Sprintf("echo \"No initialization script available for %s\"", shell)
}
}
func getShellInitScript(executable, configFile, script string) string {
script = strings.ReplaceAll(script, "::OMP::", executable)
script = strings.ReplaceAll(script, "::CONFIG::", configFile)
return script
}
func getConsoleBackgroundColor(env environment.Environment, backgroundColorTemplate string) string {
if len(backgroundColorTemplate) == 0 {
return backgroundColorTemplate
}
tmpl := &template.Text{
Template: backgroundColorTemplate,
Context: nil,
Env: env,
}
text, err := tmpl.Render()
if err != nil {
return err.Error()
}
return text
}

View file

@ -8,28 +8,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestConsoleBackgroundColorTemplate(t *testing.T) {
cases := []struct {
Case string
Expected string
Term string
}{
{Case: "Inside vscode", Expected: "#123456", Term: "vscode"},
{Case: "Outside vscode", Expected: "", Term: "windowsterminal"},
}
for _, tc := range cases {
env := new(mock.MockedEnvironment)
env.On("TemplateCache").Return(&environment.TemplateCache{
Env: map[string]string{
"TERM_PROGRAM": tc.Term,
},
})
color := getConsoleBackgroundColor(env, "{{ if eq \"vscode\" .Env.TERM_PROGRAM }}#123456{{end}}")
assert.Equal(t, tc.Expected, color, tc.Case)
}
}
// This can only be tested here due to circular dependencies // This can only be tested here due to circular dependencies
// Which might be an indaction of a fault architecture but // Which might be an indaction of a fault architecture but
// I honestly could not figure out how to do this better // I honestly could not figure out how to do this better