Repository: JanDeDobbeleer/oh-my-posh
Pull request #661 (2021-04-18 17:18:31)

feat: newline as part of block
### Prerequisites

- [x] I have read and understand the `CONTRIBUTING` guide
- [x] The commit message follows the [conventional commits][cc] guidelines
- [x] Tests for the changes have been added (for bug fixes / features)
- [x] Docs have been added / updated (for bug fixes / features)

### Description

[Description of the change]

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index a038b85d..30669ff1 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -7,7 +7,7 @@
       "request": "launch",
       "mode": "debug",
       "program": "${workspaceRoot}/src",
-      "args": ["--config=/Users/jan/.jandedobbeleer.omp.json"]
+      "args": ["--config=${workspaceRoot}/themes/jandedobbeleer.omp.json"]
     },
     {
       "name": "Launch tests",
diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md
index e4ea79d4..b68d754c 100644
--- a/docs/docs/configuration.md
+++ b/docs/docs/configuration.md
@@ -105,7 +105,8 @@ the current working directory is `/usr/home/omp` and the shell is `zsh`.
 
 Let's take a closer look at what defines a block.
 
-- type: `prompt` | `rprompt` | `newline`
+- type: `prompt` | `rprompt`
+- newline: `boolean`
 - alignment: `left` | `right`
 - vertical_offset: `int`
 - horizontal_offset: `int`
@@ -117,9 +118,11 @@ Tells the engine what to do with the block. There are three options:
 
 - `prompt` renders one or more segments
 - `rprompt` renders one or more segments aligned to the right of the cursor. Only one `rprompt` block is permitted.
-Supported on [ZSH][rprompt] and Powershell.
-- `newline` inserts a new line to start the next block on a new line. `newline` blocks require no additional
-configuration other than the `type`.
+Supported on [ZSH][rprompt], Bash and Powershell.
+
+### Newline
+
+Start the block on a new line. Defaults to `false`.
 
 ### Alignment
 
@@ -402,12 +405,10 @@ has to be enabled at the segment level. Hyperlink generation is disabled by defa
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "session",
diff --git a/src/ansi_formats.go b/src/ansi.go
similarity index 85%
rename from src/ansi_formats.go
rename to src/ansi.go
index 85d337d9..eb2a4a82 100644
--- a/src/ansi_formats.go
+++ b/src/ansi.go
@@ -9,7 +9,7 @@ const (
 	ANSIRegex = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
 )
 
-type ansiFormats struct {
+type ansiUtils struct {
 	shell                 string
 	linechange            string
 	left                  string
@@ -32,7 +32,7 @@ type ansiFormats struct {
 	strikethrough         string
 }
 
-func (a *ansiFormats) init(shell string) {
+func (a *ansiUtils) init(shell string) {
 	a.shell = shell
 	switch shell {
 	case zsh:
@@ -98,7 +98,7 @@ func (a *ansiFormats) init(shell string) {
 	}
 }
 
-func (a *ansiFormats) lenWithoutANSI(text string) int {
+func (a *ansiUtils) lenWithoutANSI(text string) int {
 	if len(text) == 0 {
 		return 0
 	}
@@ -119,7 +119,7 @@ func (a *ansiFormats) lenWithoutANSI(text string) int {
 	return len(runeText)
 }
 
-func (a *ansiFormats) generateHyperlink(text string) string {
+func (a *ansiUtils) generateHyperlink(text string) string {
 	// hyperlink matching
 	results := findNamedRegexMatch("(?P<all>(?:\\[(?P<name>.+)\\])(?:\\((?P<url>.*)\\)))", text)
 	if len(results) != 3 {
@@ -131,7 +131,7 @@ func (a *ansiFormats) generateHyperlink(text string) string {
 	return strings.Replace(text, results["all"], hyperlink, 1)
 }
 
-func (a *ansiFormats) formatText(text string) string {
+func (a *ansiUtils) formatText(text string) string {
 	results := findAllNamedRegexMatch("(?P<context><(?P<format>[buis])>(?P<text>[^<]+)</[buis]>)", text)
 	for _, result := range results {
 		var formatted string
@@ -149,3 +149,25 @@ func (a *ansiFormats) formatText(text string) string {
 	}
 	return text
 }
+
+func (a *ansiUtils) carriageForward() string {
+	return fmt.Sprintf(a.left, 1000)
+}
+
+func (a *ansiUtils) getCursorForRightWrite(text string, offset int) string {
+	strippedLen := a.lenWithoutANSI(text) + -offset
+	return fmt.Sprintf(a.right, strippedLen)
+}
+
+func (a *ansiUtils) changeLine(numberOfLines int) string {
+	position := "B"
+	if numberOfLines < 0 {
+		position = "F"
+		numberOfLines = -numberOfLines
+	}
+	return fmt.Sprintf(a.linechange, numberOfLines, position)
+}
+
+func (a *ansiUtils) consolePwd(pwd string) string {
+	return fmt.Sprintf(a.osc99, pwd)
+}
diff --git a/src/ansi_color.go b/src/ansi_color.go
index a35bc1ae..f2468a96 100644
--- a/src/ansi_color.go
+++ b/src/ansi_color.go
@@ -43,10 +43,16 @@ func getColorFromName(colorName string, isBackground bool) (string, error) {
 	return "", errors.New("color name does not exist")
 }
 
+type colorWriter interface {
+	write(background, foreground, text string)
+	string() string
+	reset()
+}
+
 // AnsiColor writes colorized strings
 type AnsiColor struct {
 	builder            strings.Builder
-	formats            *ansiFormats
+	ansi               *ansiUtils
 	terminalBackground string
 }
 
@@ -75,24 +81,24 @@ func (a *AnsiColor) writeColoredText(background, foreground, text string) {
 	if foreground == Transparent && background != "" && a.terminalBackground != "" {
 		bgAnsiColor := a.getAnsiFromColorString(background, true)
 		fgAnsiColor := a.getAnsiFromColorString(a.terminalBackground, false)
-		coloredText := fmt.Sprintf(a.formats.colorFull, bgAnsiColor, fgAnsiColor, text)
+		coloredText := fmt.Sprintf(a.ansi.colorFull, bgAnsiColor, fgAnsiColor, text)
 		a.builder.WriteString(coloredText)
 		return
 	}
 	if foreground == Transparent && background != "" {
 		ansiColor := a.getAnsiFromColorString(background, false)
-		coloredText := fmt.Sprintf(a.formats.colorTransparent, ansiColor, text)
+		coloredText := fmt.Sprintf(a.ansi.colorTransparent, ansiColor, text)
 		a.builder.WriteString(coloredText)
 		return
 	} else if background == "" || background == Transparent {
 		ansiColor := a.getAnsiFromColorString(foreground, false)
-		coloredText := fmt.Sprintf(a.formats.colorSingle, ansiColor, text)
+		coloredText := fmt.Sprintf(a.ansi.colorSingle, ansiColor, text)
 		a.builder.WriteString(coloredText)
 		return
 	}
 	bgAnsiColor := a.getAnsiFromColorString(background, true)
 	fgAnsiColor := a.getAnsiFromColorString(foreground, false)
-	coloredText := fmt.Sprintf(a.formats.colorFull, bgAnsiColor, fgAnsiColor, text)
+	coloredText := fmt.Sprintf(a.ansi.colorFull, bgAnsiColor, fgAnsiColor, text)
 	a.builder.WriteString(coloredText)
 }
 
@@ -102,7 +108,7 @@ func (a *AnsiColor) writeAndRemoveText(background, foreground, text, textToRemov
 }
 
 func (a *AnsiColor) write(background, foreground, text string) {
-	text = a.formats.formatText(text)
+	text = a.ansi.formatText(text)
 	// first we match for any potentially valid colors enclosed in <>
 	match := findAllNamedRegexMatch(`<(?P<foreground>[^,>]+)?,?(?P<background>[^>]+)?>(?P<content>[^<]*)<\/>`, text)
 	for i := range match {
diff --git a/src/ansi_renderer.go b/src/ansi_renderer.go
deleted file mode 100644
index 4f671ba6..00000000
--- a/src/ansi_renderer.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"strings"
-)
-
-// AnsiRenderer exposes functionality using ANSI
-type AnsiRenderer struct {
-	builder strings.Builder
-	formats *ansiFormats
-}
-
-func (r *AnsiRenderer) carriageForward() {
-	r.builder.WriteString(fmt.Sprintf(r.formats.left, 1000))
-}
-
-func (r *AnsiRenderer) setCursorForRightWrite(text string, offset int) {
-	strippedLen := r.formats.lenWithoutANSI(text) + -offset
-	r.builder.WriteString(fmt.Sprintf(r.formats.right, strippedLen))
-}
-
-func (r *AnsiRenderer) changeLine(numberOfLines int) {
-	position := "B"
-	if numberOfLines < 0 {
-		position = "F"
-		numberOfLines = -numberOfLines
-	}
-	r.builder.WriteString(fmt.Sprintf(r.formats.linechange, numberOfLines, position))
-}
-
-func (r *AnsiRenderer) creset() {
-	r.builder.WriteString(r.formats.creset)
-}
-
-func (r *AnsiRenderer) write(text string) {
-	r.builder.WriteString(text)
-	// 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 r.formats.shell == pwsh || r.formats.shell == powershell5 {
-		r.builder.WriteString(r.formats.clearEOL)
-	}
-}
-
-func (r *AnsiRenderer) string() string {
-	return r.builder.String()
-}
-
-func (r *AnsiRenderer) saveCursorPosition() {
-	r.builder.WriteString(r.formats.saveCursorPosition)
-}
-
-func (r *AnsiRenderer) restoreCursorPosition() {
-	r.builder.WriteString(r.formats.restoreCursorPosition)
-}
-
-func (r *AnsiRenderer) osc99(pwd string) {
-	r.builder.WriteString(fmt.Sprintf(r.formats.osc99, pwd))
-}
diff --git a/src/block.go b/src/block.go
new file mode 100644
index 00000000..d6f2f5c7
--- /dev/null
+++ b/src/block.go
@@ -0,0 +1,195 @@
+package main
+
+import (
+	"fmt"
+	"sync"
+	"time"
+)
+
+// BlockType type of block
+type BlockType string
+
+// BlockAlignment aligment of a Block
+type BlockAlignment string
+
+const (
+	// Prompt writes one or more Segments
+	Prompt BlockType = "prompt"
+	// LineBreak creates a line break in the prompt
+	LineBreak BlockType = "newline"
+	// RPrompt a right aligned prompt in ZSH and Powershell
+	RPrompt BlockType = "rprompt"
+	// Left aligns left
+	Left BlockAlignment = "left"
+	// Right aligns right
+	Right BlockAlignment = "right"
+)
+
+// Block defines a part of the prompt with optional segments
+type Block struct {
+	Type             BlockType      `config:"type"`
+	Alignment        BlockAlignment `config:"alignment"`
+	HorizontalOffset int            `config:"horizontal_offset"`
+	VerticalOffset   int            `config:"vertical_offset"`
+	Segments         []*Segment     `config:"segments"`
+	Newline          bool           `config:"newline"`
+
+	env                   environmentInfo
+	writer                colorWriter
+	ansi                  *ansiUtils
+	activeSegment         *Segment
+	previousActiveSegment *Segment
+}
+
+func (b *Block) init(env environmentInfo, writer colorWriter, ansi *ansiUtils) {
+	b.env = env
+	b.writer = writer
+	b.ansi = ansi
+}
+
+func (b *Block) enabled() bool {
+	if b.Type == LineBreak {
+		return true
+	}
+	for _, segment := range b.Segments {
+		if segment.active {
+			return true
+		}
+	}
+	return false
+}
+
+func (b *Block) setStringValues() {
+	wg := sync.WaitGroup{}
+	wg.Add(len(b.Segments))
+	defer wg.Wait()
+	cwd := b.env.getcwd()
+	for _, segment := range b.Segments {
+		go func(s *Segment) {
+			defer wg.Done()
+			s.setStringValue(b.env, cwd)
+		}(segment)
+	}
+}
+
+func (b *Block) renderSegments() string {
+	defer b.writer.reset()
+	for _, segment := range b.Segments {
+		if !segment.active {
+			continue
+		}
+		b.activeSegment = segment
+		b.endPowerline()
+		b.renderSegmentText(segment.stringValue)
+	}
+	if b.previousActiveSegment != nil && b.previousActiveSegment.Style == Powerline {
+		b.writePowerLineSeparator(Transparent, b.previousActiveSegment.background(), true)
+	}
+	return b.writer.string()
+}
+
+func (b *Block) endPowerline() {
+	if b.activeSegment != nil &&
+		b.activeSegment.Style != Powerline &&
+		b.previousActiveSegment != nil &&
+		b.previousActiveSegment.Style == Powerline {
+		b.writePowerLineSeparator(b.getPowerlineColor(false), b.previousActiveSegment.background(), true)
+	}
+}
+
+func (b *Block) writePowerLineSeparator(background, foreground string, end bool) {
+	symbol := b.activeSegment.PowerlineSymbol
+	if end {
+		symbol = b.previousActiveSegment.PowerlineSymbol
+	}
+	if b.activeSegment.InvertPowerline {
+		b.writer.write(foreground, background, symbol)
+		return
+	}
+	b.writer.write(background, foreground, symbol)
+}
+
+func (b *Block) getPowerlineColor(foreground bool) string {
+	if b.previousActiveSegment == nil {
+		return Transparent
+	}
+	if !foreground && b.activeSegment.Style != Powerline {
+		return Transparent
+	}
+	if foreground && b.previousActiveSegment.Style != Powerline {
+		return Transparent
+	}
+	return b.previousActiveSegment.background()
+}
+
+func (b *Block) renderSegmentText(text string) {
+	switch b.activeSegment.Style {
+	case Plain:
+		b.renderPlainSegment(text)
+	case Diamond:
+		b.renderDiamondSegment(text)
+	case Powerline:
+		b.renderPowerLineSegment(text)
+	}
+	b.previousActiveSegment = b.activeSegment
+}
+
+func (b *Block) renderPowerLineSegment(text string) {
+	b.writePowerLineSeparator(b.activeSegment.background(), b.getPowerlineColor(true), false)
+	b.renderText(text)
+}
+
+func (b *Block) renderPlainSegment(text string) {
+	b.renderText(text)
+}
+
+func (b *Block) renderDiamondSegment(text string) {
+	b.writer.write(Transparent, b.activeSegment.background(), b.activeSegment.LeadingDiamond)
+	b.renderText(text)
+	b.writer.write(Transparent, b.activeSegment.background(), b.activeSegment.TrailingDiamond)
+}
+
+func (b *Block) renderText(text string) {
+	text = b.ansi.generateHyperlink(text)
+	defaultValue := " "
+	prefix := b.activeSegment.getValue(Prefix, defaultValue)
+	postfix := b.activeSegment.getValue(Postfix, defaultValue)
+	b.writer.write(b.activeSegment.background(), b.activeSegment.foreground(), fmt.Sprintf("%s%s%s", prefix, text, postfix))
+}
+
+func (b *Block) debug() (int, []*SegmentTiming) {
+	var segmentTimings []*SegmentTiming
+	largestSegmentNameLength := 0
+	for _, segment := range b.Segments {
+		err := segment.mapSegmentWithWriter(b.env)
+		if err != nil || !segment.shouldIncludeFolder(b.env.getcwd()) {
+			continue
+		}
+		var segmentTiming SegmentTiming
+		segmentTiming.name = string(segment.Type)
+		segmentTiming.nameLength = len(segmentTiming.name)
+		if segmentTiming.nameLength > largestSegmentNameLength {
+			largestSegmentNameLength = segmentTiming.nameLength
+		}
+		// enabled() timing
+		start := time.Now()
+		segmentTiming.enabled = segment.enabled()
+		segmentTiming.enabledDuration = time.Since(start)
+		// string() timing
+		if segmentTiming.enabled {
+			start = time.Now()
+			segmentTiming.stringValue = segment.string()
+			segmentTiming.stringDuration = time.Since(start)
+			b.previousActiveSegment = nil
+			b.activeSegment = segment
+			b.renderSegmentText(segmentTiming.stringValue)
+			if b.activeSegment.Style == Powerline {
+				b.writePowerLineSeparator(Transparent, b.activeSegment.background(), true)
+			}
+			segmentTiming.stringValue = b.writer.string()
+			b.writer.reset()
+		}
+		segmentTimings = append(segmentTimings, &segmentTiming)
+	}
+	return largestSegmentNameLength, segmentTimings
+}
diff --git a/src/config.go b/src/config.go
index a559ef30..4fcd6576 100644
--- a/src/config.go
+++ b/src/config.go
@@ -28,36 +28,11 @@ type Config struct {
 	Blocks               []*Block          `config:"blocks"`
 }
 
-// BlockType type of block
-type BlockType string
-
-// BlockAlignment aligment of a Block
-type BlockAlignment string
-
 const (
-	// Prompt writes one or more Segments
-	Prompt BlockType = "prompt"
-	// LineBreak creates a line break in the prompt
-	LineBreak BlockType = "newline"
-	// RPrompt a right aligned prompt in ZSH and Powershell
-	RPrompt BlockType = "rprompt"
-	// Left aligns left
-	Left BlockAlignment = "left"
-	// Right aligns right
-	Right BlockAlignment = "right"
 	// EnableHyperlink enable hyperlink
 	EnableHyperlink Property = "enable_hyperlink"
 )
 
-// Block defines a part of the prompt with optional segments
-type Block struct {
-	Type             BlockType      `config:"type"`
-	Alignment        BlockAlignment `config:"alignment"`
-	HorizontalOffset int            `config:"horizontal_offset"`
-	VerticalOffset   int            `config:"vertical_offset"`
-	Segments         []*Segment     `config:"segments"`
-}
-
 // GetConfig returns the default configuration including possible user overrides
 func GetConfig(env environmentInfo) *Config {
 	cfg, err := loadConfig(env)
diff --git a/src/console_title.go b/src/console_title.go
index 66df35dd..4732c76e 100644
--- a/src/console_title.go
+++ b/src/console_title.go
@@ -6,9 +6,9 @@ import (
 )
 
 type consoleTitle struct {
-	env     environmentInfo
-	config  *Config
-	formats *ansiFormats
+	env    environmentInfo
+	config *Config
+	ansi   *ansiUtils
 }
 
 // ConsoleTitleStyle defines how to show the title in the console window
@@ -37,7 +37,7 @@ func (t *consoleTitle) getConsoleTitle() string {
 	default:
 		title = base(t.getPwd(), t.env)
 	}
-	return fmt.Sprintf(t.formats.title, title)
+	return fmt.Sprintf(t.ansi.title, title)
 }
 
 func (t *consoleTitle) getTemplateText() string {
diff --git a/src/engine.go b/src/engine.go
index ff6f69f8..45d3fc6a 100644
--- a/src/engine.go
+++ b/src/engine.go
@@ -3,161 +3,45 @@ package main
 import (
 	"fmt"
 	"strings"
-	"sync"
 	"time"
 )
 
 type engine struct {
-	config                *Config
-	env                   environmentInfo
-	color                 *AnsiColor
-	renderer              *AnsiRenderer
-	consoleTitle          *consoleTitle
-	activeBlock           *Block
-	activeSegment         *Segment
-	previousActiveSegment *Segment
-	rprompt               string
-}
-
-// SegmentTiming holds the timing context for a segment
-type SegmentTiming struct {
-	name            string
-	nameLength      int
-	enabled         bool
-	stringValue     string
-	enabledDuration time.Duration
-	stringDuration  time.Duration
-}
-
-func (e *engine) getPowerlineColor(foreground bool) string {
-	if e.previousActiveSegment == nil {
-		return Transparent
-	}
-	if !foreground && e.activeSegment.Style != Powerline {
-		return Transparent
-	}
-	if foreground && e.previousActiveSegment.Style != Powerline {
-		return Transparent
-	}
-	return e.previousActiveSegment.background()
-}
+	config       *Config
+	env          environmentInfo
+	colorWriter  colorWriter
+	ansi         *ansiUtils
+	consoleTitle *consoleTitle
 
-func (e *engine) writePowerLineSeparator(background, foreground string, end bool) {
-	symbol := e.activeSegment.PowerlineSymbol
-	if end {
-		symbol = e.previousActiveSegment.PowerlineSymbol
-	}
-	if e.activeSegment.InvertPowerline {
-		e.color.write(foreground, background, symbol)
-		return
-	}
-	e.color.write(background, foreground, symbol)
+	console strings.Builder
+	rprompt string
 }
 
-func (e *engine) endPowerline() {
-	if e.activeSegment != nil &&
-		e.activeSegment.Style != Powerline &&
-		e.previousActiveSegment != nil &&
-		e.previousActiveSegment.Style == Powerline {
-		e.writePowerLineSeparator(e.getPowerlineColor(false), e.previousActiveSegment.background(), true)
+func (e *engine) write(text string) {
+	e.console.WriteString(text)
+	// 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.console.WriteString(e.ansi.clearEOL)
 	}
 }
 
-func (e *engine) renderPowerLineSegment(text string) {
-	e.writePowerLineSeparator(e.activeSegment.background(), e.getPowerlineColor(true), false)
-	e.renderText(text)
-}
-
-func (e *engine) renderPlainSegment(text string) {
-	e.renderText(text)
-}
-
-func (e *engine) renderDiamondSegment(text string) {
-	e.color.write(Transparent, e.activeSegment.background(), e.activeSegment.LeadingDiamond)
-	e.renderText(text)
-	e.color.write(Transparent, e.activeSegment.background(), e.activeSegment.TrailingDiamond)
-}
-
-func (e *engine) renderText(text string) {
-	text = e.color.formats.generateHyperlink(text)
-	defaultValue := " "
-	prefix := e.activeSegment.getValue(Prefix, defaultValue)
-	postfix := e.activeSegment.getValue(Postfix, defaultValue)
-	e.color.write(e.activeSegment.background(), e.activeSegment.foreground(), fmt.Sprintf("%s%s%s", prefix, text, postfix))
-}
-
-func (e *engine) renderSegmentText(text string) {
-	switch e.activeSegment.Style {
-	case Plain:
-		e.renderPlainSegment(text)
-	case Diamond:
-		e.renderDiamondSegment(text)
-	case Powerline:
-		e.renderPowerLineSegment(text)
-	}
-	e.previousActiveSegment = e.activeSegment
-}
-
-func (e *engine) renderBlockSegments(block *Block) string {
-	defer e.resetBlock()
-	e.activeBlock = block
-	e.setStringValues(block.Segments)
-	for _, segment := range block.Segments {
-		if !segment.active {
-			continue
-		}
-		e.activeSegment = segment
-		e.endPowerline()
-		e.renderSegmentText(segment.stringValue)
-	}
-	if e.previousActiveSegment != nil && e.previousActiveSegment.Style == Powerline {
-		e.writePowerLineSeparator(Transparent, e.previousActiveSegment.background(), true)
-	}
-	return e.color.string()
-}
-
-func (e *engine) setStringValues(segments []*Segment) {
-	wg := sync.WaitGroup{}
-	wg.Add(len(segments))
-	defer wg.Wait()
-	cwd := e.env.getcwd()
-	for _, segment := range segments {
-		go func(s *Segment) {
-			defer wg.Done()
-			s.setStringValue(e.env, cwd)
-		}(segment)
-	}
+func (e *engine) string() string {
+	return e.console.String()
 }
 
 func (e *engine) render() string {
 	for _, block := range e.config.Blocks {
-		// if line break, append a line break
-		switch block.Type {
-		case LineBreak:
-			e.renderer.write("\n")
-		case Prompt:
-			if block.VerticalOffset != 0 {
-				e.renderer.changeLine(block.VerticalOffset)
-			}
-			switch block.Alignment {
-			case Right:
-				e.renderer.carriageForward()
-				blockText := e.renderBlockSegments(block)
-				e.renderer.setCursorForRightWrite(blockText, block.HorizontalOffset)
-				e.renderer.write(blockText)
-			case Left:
-				e.renderer.write(e.renderBlockSegments(block))
-			}
-		case RPrompt:
-			e.rprompt = e.renderBlockSegments(block)
-		}
+		e.renderBlock(block)
 	}
 	if e.config.ConsoleTitle {
-		e.renderer.write(e.consoleTitle.getConsoleTitle())
+		e.write(e.consoleTitle.getConsoleTitle())
 	}
-	e.renderer.creset()
+	e.write(e.ansi.creset)
 	if e.config.FinalSpace {
-		e.renderer.write(" ")
+		e.write(" ")
 	}
 
 	if !e.config.OSC99 {
@@ -167,21 +51,54 @@ func (e *engine) render() string {
 	if e.env.isWsl() {
 		cwd, _ = e.env.runCommand("wslpath", "-m", cwd)
 	}
-	e.renderer.osc99(cwd)
+	e.write(e.ansi.consolePwd(cwd))
 	return e.print()
 }
 
+func (e *engine) renderBlock(block *Block) {
+	block.init(e.env, e.colorWriter, 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.write(e.ansi.changeLine(block.VerticalOffset))
+		}
+		switch block.Alignment {
+		case Right:
+			e.write(e.ansi.carriageForward())
+			blockText := block.renderSegments()
+			e.write(e.ansi.getCursorForRightWrite(blockText, block.HorizontalOffset))
+			e.write(blockText)
+		case Left:
+			e.write(block.renderSegments())
+		}
+	case RPrompt:
+		e.rprompt = block.renderSegments()
+	}
+}
+
 // debug will loop through your config file and output the timings for each segments
 func (e *engine) debug() string {
-	var segmentTimings []SegmentTiming
+	var segmentTimings []*SegmentTiming
 	largestSegmentNameLength := 0
-	e.renderer.write("\n\x1b[1mHere are the timings of segments in your prompt:\x1b[0m\n\n")
+	e.write("\n\x1b[1mHere are the timings of segments in your prompt:\x1b[0m\n\n")
 
 	// console title timing
 	start := time.Now()
 	consoleTitle := e.consoleTitle.getTemplateText()
 	duration := time.Since(start)
-	segmentTiming := SegmentTiming{
+	segmentTiming := &SegmentTiming{
 		name:            "ConsoleTitle",
 		nameLength:      12,
 		enabled:         e.config.ConsoleTitle,
@@ -192,36 +109,11 @@ func (e *engine) debug() string {
 	segmentTimings = append(segmentTimings, segmentTiming)
 	// loop each segments of each blocks
 	for _, block := range e.config.Blocks {
-		for _, segment := range block.Segments {
-			err := segment.mapSegmentWithWriter(e.env)
-			if err != nil || !segment.shouldIncludeFolder(e.env.getcwd()) {
-				continue
-			}
-			var segmentTiming SegmentTiming
-			segmentTiming.name = string(segment.Type)
-			segmentTiming.nameLength = len(segmentTiming.name)
-			if segmentTiming.nameLength > largestSegmentNameLength {
-				largestSegmentNameLength = segmentTiming.nameLength
-			}
-			// enabled() timing
-			start := time.Now()
-			segmentTiming.enabled = segment.enabled()
-			segmentTiming.enabledDuration = time.Since(start)
-			// string() timing
-			if segmentTiming.enabled {
-				start = time.Now()
-				segmentTiming.stringValue = segment.string()
-				segmentTiming.stringDuration = time.Since(start)
-				e.previousActiveSegment = nil
-				e.activeSegment = segment
-				e.renderSegmentText(segmentTiming.stringValue)
-				if e.activeSegment.Style == Powerline {
-					e.writePowerLineSeparator(Transparent, e.activeSegment.background(), true)
-				}
-				segmentTiming.stringValue = e.color.string()
-				e.color.builder.Reset()
-			}
-			segmentTimings = append(segmentTimings, segmentTiming)
+		block.init(e.env, e.colorWriter, e.ansi)
+		longestSegmentName, timings := block.debug()
+		segmentTimings = append(segmentTimings, timings...)
+		if longestSegmentName > largestSegmentNameLength {
+			largestSegmentNameLength = longestSegmentName
 		}
 	}
 
@@ -233,9 +125,9 @@ func (e *engine) debug() string {
 			duration += segment.stringDuration.Milliseconds()
 		}
 		segmentName := fmt.Sprintf("%s(%t)", segment.name, segment.enabled)
-		e.renderer.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))
 	}
-	return e.renderer.string()
+	return e.string()
 }
 
 func (e *engine) print() string {
@@ -243,24 +135,18 @@ func (e *engine) print() string {
 	case zsh:
 		if *e.env.getArgs().Eval {
 			// escape double quotes contained in the prompt
-			prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.renderer.string(), "\"", "\"\""))
+			prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), "\"", "\"\""))
 			prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt)
 			return prompt
 		}
 	case pwsh, powershell5, bash, shelly:
 		if e.rprompt != "" {
-			e.renderer.saveCursorPosition()
-			e.renderer.carriageForward()
-			e.renderer.setCursorForRightWrite(e.rprompt, 0)
-			e.renderer.write(e.rprompt)
-			e.renderer.restoreCursorPosition()
+			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.renderer.string()
-}
-
-func (e *engine) resetBlock() {
-	e.color.reset()
-	e.previousActiveSegment = nil
-	e.activeBlock = nil
+	return e.string()
 }
diff --git a/src/image.go b/src/image.go
index bfc44cd1..19a688a8 100644
--- a/src/image.go
+++ b/src/image.go
@@ -96,7 +96,7 @@ func NewRGBColor(ansiColor string) *RGB {
 type ImageRenderer struct {
 	ansiString string
 	author     string
-	formats    *ansiFormats
+	ansi       *ansiUtils
 
 	factor float64
 
@@ -189,7 +189,7 @@ func (ir *ImageRenderer) fontHeight() float64 {
 func (ir *ImageRenderer) calculateWidth() int {
 	longest := 0
 	for _, line := range strings.Split(ir.ansiString, "\n") {
-		length := ir.formats.lenWithoutANSI(line)
+		length := ir.ansi.lenWithoutANSI(line)
 		if length > longest {
 			longest = length
 		}
diff --git a/src/main.go b/src/main.go
index 6d0a00c0..f3af19e5 100644
--- a/src/main.go
+++ b/src/main.go
@@ -173,26 +173,23 @@ func main() {
 		return
 	}
 
-	formats := &ansiFormats{}
-	formats.init(env.getShellName())
-	renderer := &AnsiRenderer{
-		formats: formats,
-	}
+	ansi := &ansiUtils{}
+	ansi.init(env.getShellName())
 	colorer := &AnsiColor{
-		formats:            formats,
+		ansi:               ansi,
 		terminalBackground: getConsoleBackgroundColor(env, cfg.TerminalBackground),
 	}
 	title := &consoleTitle{
-		env:     env,
-		config:  cfg,
-		formats: formats,
+		env:    env,
+		config: cfg,
+		ansi:   ansi,
 	}
 	engine := &engine{
 		config:       cfg,
 		env:          env,
-		color:        colorer,
-		renderer:     renderer,
+		colorWriter:  colorer,
 		consoleTitle: title,
+		ansi:         ansi,
 	}
 
 	if *args.Debug {
@@ -209,7 +206,7 @@ func main() {
 		author:        *args.Author,
 		cursorPadding: *args.CursorPadding,
 		rPromptOffset: *args.RPromptOffset,
-		formats:       formats,
+		ansi:          ansi,
 	}
 	imageCreator.init()
 	match := findNamedRegexMatch(`.*(\/|\\)(?P<STR>.+).omp.(json|yaml|toml)`, *args.Config)
diff --git a/src/segment.go b/src/segment.go
index c50e9018..4e0d5c04 100644
--- a/src/segment.go
+++ b/src/segment.go
@@ -3,6 +3,7 @@ package main
 import (
 	"errors"
 	"fmt"
+	"time"
 )
 
 // Segment represent a single segment and it's configuration
@@ -24,6 +25,16 @@ type Segment struct {
 	active              bool
 }
 
+// SegmentTiming holds the timing context for a segment
+type SegmentTiming struct {
+	name            string
+	nameLength      int
+	enabled         bool
+	stringValue     string
+	enabledDuration time.Duration
+	stringDuration  time.Duration
+}
+
 // SegmentWriter is the interface used to define what and if to write to the prompt
 type SegmentWriter interface {
 	enabled() bool
diff --git a/themes/agnosterplus.omp.json b/themes/agnosterplus.omp.json
index 210f4152..97488a00 100644
--- a/themes/agnosterplus.omp.json
+++ b/themes/agnosterplus.omp.json
@@ -16,12 +16,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "session",
diff --git a/themes/avit.omp.json b/themes/avit.omp.json
index 710a81f0..ad7a60e1 100644
--- a/themes/avit.omp.json
+++ b/themes/avit.omp.json
@@ -34,12 +34,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "text",
diff --git a/themes/blueish.omp.json b/themes/blueish.omp.json
index 5845b881..3adde264 100644
--- a/themes/blueish.omp.json
+++ b/themes/blueish.omp.json
@@ -107,11 +107,9 @@
       ]
     },
     {
-    "type": "newline"
-    },
-    {
     "type": "prompt",
     "alignment": "left",
+    "newline": true,
     "segments": [
         {
         "type": "text",
diff --git a/themes/bubbles.omp.json b/themes/bubbles.omp.json
index 8fcad407..47660bea 100644
--- a/themes/bubbles.omp.json
+++ b/themes/bubbles.omp.json
@@ -146,12 +146,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "executiontime",
diff --git a/themes/bubblesline.omp.json b/themes/bubblesline.omp.json
index 44265d7a..61efb00f 100644
--- a/themes/bubblesline.omp.json
+++ b/themes/bubblesline.omp.json
@@ -133,12 +133,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "session",
diff --git a/themes/cinnamon.omp.json b/themes/cinnamon.omp.json
index 52d1bbf3..56ae76a6 100644
--- a/themes/cinnamon.omp.json
+++ b/themes/cinnamon.omp.json
@@ -38,12 +38,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "text",
diff --git a/themes/darkblood.omp.json b/themes/darkblood.omp.json
index 39b9a2f0..f452ec4e 100644
--- a/themes/darkblood.omp.json
+++ b/themes/darkblood.omp.json
@@ -45,12 +45,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "path",
diff --git a/themes/honukai.omp.json b/themes/honukai.omp.json
index 62cd8496..15ec2cf8 100644
--- a/themes/honukai.omp.json
+++ b/themes/honukai.omp.json
@@ -51,12 +51,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "root",
diff --git a/themes/huvix.omp.json b/themes/huvix.omp.json
index e6c88e6a..e0d1a163 100644
--- a/themes/huvix.omp.json
+++ b/themes/huvix.omp.json
@@ -39,7 +39,7 @@
             "foreground": "#9e7eff",
             "properties": {
                 "prefix": "\u0028\uE235 ",
-                "postfix": "\u0029 " 
+                "postfix": "\u0029 "
               }
             },
             {
@@ -67,12 +67,10 @@
             }
         ]
       },
-      {
-        "type": "newline"
-      },
       {
         "type": "prompt",
         "alignment": "left",
+        "newline": true,
         "segments": [
           {
           "type": "battery",
@@ -97,4 +95,4 @@
     ],
     "final_space": true
   }
-  
+
diff --git a/themes/jonnychipz.omp.json b/themes/jonnychipz.omp.json
index 6ef5f2c8..ec808392 100644
--- a/themes/jonnychipz.omp.json
+++ b/themes/jonnychipz.omp.json
@@ -20,7 +20,7 @@
             		"postfix": "",
             		"prefix": ""
           		}
-        },	
+        },
         {
           	"type": "session",
           	"style": "diamond",
@@ -42,7 +42,7 @@
               		"prefix": " \uFD03 "
             		}
         },
-	
+
 	{
           	"type": "exit",
           	"style": "diamond",
@@ -60,13 +60,11 @@
         }
       ]
       },
-      {
-        "type": "newline"
-      }, 
       {
         "type": "prompt",
         "alignment": "left",
-        "segments": [             
+        "newline": true,
+        "segments": [
       	{
           	"type": "os",
           	"style": "diamond",
@@ -94,7 +92,7 @@
             		"postfix": "<#000000> \ue0b1</>"
           		}
         },
-      
+
         {
           "type": "root",
           "style": "diamond",
@@ -174,4 +172,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/themes/microverse-power.omp.json b/themes/microverse-power.omp.json
index 4aca65f3..748b9241 100644
--- a/themes/microverse-power.omp.json
+++ b/themes/microverse-power.omp.json
@@ -86,12 +86,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "text",
diff --git a/themes/negligible.omp.json b/themes/negligible.omp.json
index c5e192f7..88331b38 100644
--- a/themes/negligible.omp.json
+++ b/themes/negligible.omp.json
@@ -56,12 +56,10 @@
                 }
             ]
         },
-        {
-            "type": "newline"
-        },
         {
             "type": "prompt",
             "alignment": "left",
+            "newline": true,
             "segments": [
                 {
                     "type": "exit",
diff --git a/themes/paradox.omp.json b/themes/paradox.omp.json
index 48c2263a..153e0611 100644
--- a/themes/paradox.omp.json
+++ b/themes/paradox.omp.json
@@ -60,12 +60,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "text",
diff --git a/themes/pararussel.omp.json b/themes/pararussel.omp.json
index 1bf35f4b..e05755ac 100644
--- a/themes/pararussel.omp.json
+++ b/themes/pararussel.omp.json
@@ -60,12 +60,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "text",
diff --git a/themes/pixelrobots.omp.json b/themes/pixelrobots.omp.json
index 823e47a2..6330a7cd 100644
--- a/themes/pixelrobots.omp.json
+++ b/themes/pixelrobots.omp.json
@@ -22,38 +22,36 @@
           }
         },
         {
-            "type": "kubectl",
-            "style": "powerline",
-            "powerline_symbol": "",
-            "foreground": "#000000",
-            "background": "#ffea00",
-            "properties": {
-              "prefix": " \uFD31",
-              "template": " {{.Context}}{{if .Namespace}} :: {{.Namespace}}{{end}}"
-            }
-          },
-          {
-            "type": "az",
-            "style": "powerline",
-            "powerline_symbol": "\uE0B0",
-            "foreground": "#000000",
-            "background": "#008AD7",
-            "properties": {
-              "display_id": false,
-              "display_name": true,
-              "info_separator": " @ ",
-              "prefix": " \uFD03 "
-            }
-          }  
-        ]
+          "type": "kubectl",
+          "style": "powerline",
+          "powerline_symbol": "",
+          "foreground": "#000000",
+          "background": "#ffea00",
+          "properties": {
+            "prefix": " \uFD31",
+            "template": " {{.Context}}{{if .Namespace}} :: {{.Namespace}}{{end}}"
+          }
+        },
+        {
+          "type": "az",
+          "style": "powerline",
+          "powerline_symbol": "\uE0B0",
+          "foreground": "#000000",
+          "background": "#008AD7",
+          "properties": {
+            "display_id": false,
+            "display_name": true,
+            "info_separator": " @ ",
+            "prefix": " \uFD03 "
+          }
+        }
+      ]
     },
-          {
-            "type": "newline"
-          }, 
-          {
-            "type": "prompt",
-            "alignment": "left",
-            "segments": [             
+    {
+      "type": "prompt",
+      "alignment": "left",
+      "newline": true,
+      "segments": [
         {
           "type": "os",
           "style": "diamond",
@@ -167,4 +165,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/themes/powerlevel10k_classic.omp.json b/themes/powerlevel10k_classic.omp.json
index 6348f71c..16e303b3 100644
--- a/themes/powerlevel10k_classic.omp.json
+++ b/themes/powerlevel10k_classic.omp.json
@@ -78,12 +78,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "exit",
diff --git a/themes/powerlevel10k_lean.omp.json b/themes/powerlevel10k_lean.omp.json
index 774f3bae..0c959866 100644
--- a/themes/powerlevel10k_lean.omp.json
+++ b/themes/powerlevel10k_lean.omp.json
@@ -16,12 +16,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "path",
diff --git a/themes/powerlevel10k_modern.opm.json b/themes/powerlevel10k_modern.omp.json
similarity index 98%
rename from themes/powerlevel10k_modern.opm.json
rename to themes/powerlevel10k_modern.omp.json
index f581d78e..9ed1c9ce 100644
--- a/themes/powerlevel10k_modern.opm.json
+++ b/themes/powerlevel10k_modern.omp.json
@@ -85,12 +85,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "exit",
@@ -106,4 +104,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/themes/pure.omp.json b/themes/pure.omp.json
index 7117e63a..dacec272 100644
--- a/themes/pure.omp.json
+++ b/themes/pure.omp.json
@@ -4,12 +4,10 @@
   "console_title_style": "template",
   "console_title_template": "{{if .Root}}(Admin){{end}} {{.Path}}",
   "blocks": [
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "root",
@@ -69,11 +67,9 @@
       ]
     },
     {
-      "type": "newline"
-    },
-    {
-      "alignment": "left",
       "type": "prompt",
+      "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "exit",
diff --git a/themes/schema.json b/themes/schema.json
index 9862ee1e..64bcf1ff 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -50,17 +50,6 @@
       "type": "object",
       "description": "https://ohmyposh.dev/docs/configure#block",
       "allOf": [
-        {
-          "if": {
-            "properties": {
-              "type": { "const": "newline" }
-            }
-          },
-          "then": {
-            "required": ["type"],
-            "title": "Newline, renders a line break"
-          }
-        },
         {
           "if": {
             "properties": {
@@ -89,7 +78,7 @@
           "type": "string",
           "title": "Block type",
           "description": "https://ohmyposh.dev/docs/configure#type",
-          "enum": ["prompt", "rprompt", "newline"],
+          "enum": ["prompt", "rprompt"],
           "default": "prompt"
         },
         "alignment": {
@@ -99,6 +88,12 @@
           "enum": ["left", "right"],
           "default": "left"
         },
+        "newline": {
+          "type": "boolean",
+          "title": "Newline",
+          "description": "https://ohmyposh.dev/docs/configure#newline",
+          "default": false
+        },
         "vertical_offset": {
           "type": "integer",
           "title": "Block vertical offset",
diff --git a/themes/slim.omp.json b/themes/slim.omp.json
index c550ef94..f9ceb6cf 100644
--- a/themes/slim.omp.json
+++ b/themes/slim.omp.json
@@ -165,12 +165,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "text",
diff --git a/themes/slimfat.omp.json b/themes/slimfat.omp.json
index a3fdaf29..0ad30693 100644
--- a/themes/slimfat.omp.json
+++ b/themes/slimfat.omp.json
@@ -163,12 +163,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "text",
diff --git a/themes/star.omp.json b/themes/star.omp.json
index 209a3db3..fb7751a7 100644
--- a/themes/star.omp.json
+++ b/themes/star.omp.json
@@ -53,12 +53,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "text",
diff --git a/themes/stelbent.minimal.omp.json b/themes/stelbent.minimal.omp.json
index b7c2fae8..be3e8bfc 100644
--- a/themes/stelbent.minimal.omp.json
+++ b/themes/stelbent.minimal.omp.json
@@ -37,12 +37,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "session",
@@ -117,12 +115,10 @@
         }
       ]
     },
-    {
-      "type": "newline"
-    },
     {
       "type": "prompt",
       "alignment": "left",
+      "newline": true,
       "segments": [
         {
           "type": "text",
diff --git a/themes/ys.omp.json b/themes/ys.omp.json
index 75267131..d9ae5e07 100644
--- a/themes/ys.omp.json
+++ b/themes/ys.omp.json
@@ -17,12 +17,10 @@
           }
         ]
       },
-      {
-        "type": "newline"
-      },
       {
         "type": "prompt",
         "alignment": "left",
+        "newline": true,
         "segments": [
           {
             "type": "text",
@@ -87,12 +85,10 @@
           }
         ]
       },
-      {
-        "type": "newline"
-      },
       {
         "type": "prompt",
         "alignment": "left",
+        "newline": true,
         "segments": [
           {
             "type": "text",


Test patch:
diff --git a/src/ansi_color_test.go b/src/ansi_color_test.go
index 49330d35..6aa259d1 100644
--- a/src/ansi_color_test.go
+++ b/src/ansi_color_test.go
@@ -12,10 +12,10 @@ const (
 )
 
 func TestWriteAndRemoveText(t *testing.T) {
-	formats := &ansiFormats{}
-	formats.init("pwsh")
+	ansi := &ansiUtils{}
+	ansi.init("pwsh")
 	renderer := &AnsiColor{
-		formats: formats,
+		ansi: ansi,
 	}
 	text := renderer.writeAndRemoveText("#193549", "#fff", "This is white, ", "This is white, ", inputText)
 	assert.Equal(t, "<#ff5733>this is orange</>, white again", text)
@@ -23,10 +23,10 @@ func TestWriteAndRemoveText(t *testing.T) {
 }
 
 func TestWriteAndRemoveTextColored(t *testing.T) {
-	formats := &ansiFormats{}
-	formats.init("pwsh")
+	ansi := &ansiUtils{}
+	ansi.init("pwsh")
 	renderer := &AnsiColor{
-		formats: formats,
+		ansi: ansi,
 	}
 	text := renderer.writeAndRemoveText("#193549", "#ff5733", "this is orange", "<#ff5733>this is orange</>", inputText)
 	assert.Equal(t, "This is white, , white again", text)
@@ -34,20 +34,20 @@ func TestWriteAndRemoveTextColored(t *testing.T) {
 }
 
 func TestWriteColorOverride(t *testing.T) {
-	formats := &ansiFormats{}
-	formats.init("pwsh")
+	ansi := &ansiUtils{}
+	ansi.init("pwsh")
 	renderer := &AnsiColor{
-		formats: formats,
+		ansi: ansi,
 	}
 	renderer.write("#193549", "#ff5733", inputText)
 	assert.NotContains(t, renderer.string(), "<#ff5733>")
 }
 
 func TestWriteColorOverrideBackground(t *testing.T) {
-	formats := &ansiFormats{}
-	formats.init("pwsh")
+	ansi := &ansiUtils{}
+	ansi.init("pwsh")
 	renderer := &AnsiColor{
-		formats: formats,
+		ansi: ansi,
 	}
 	text := "This is white, <,#000000>this is black</>, white again"
 	renderer.write("#193549", "#ff5733", text)
@@ -55,10 +55,10 @@ func TestWriteColorOverrideBackground(t *testing.T) {
 }
 
 func TestWriteColorOverrideBackground16(t *testing.T) {
-	formats := &ansiFormats{}
-	formats.init("pwsh")
+	ansi := &ansiUtils{}
+	ansi.init("pwsh")
 	renderer := &AnsiColor{
-		formats: formats,
+		ansi: ansi,
 	}
 	text := "This is default <,white> this background is changed</> default again"
 	renderer.write("#193549", "#ff5733", text)
@@ -68,10 +68,10 @@ func TestWriteColorOverrideBackground16(t *testing.T) {
 }
 
 func TestWriteColorOverrideBoth(t *testing.T) {
-	formats := &ansiFormats{}
-	formats.init("pwsh")
+	ansi := &ansiUtils{}
+	ansi.init("pwsh")
 	renderer := &AnsiColor{
-		formats: formats,
+		ansi: ansi,
 	}
 	text := "This is white, <#000000,#ffffff>this is black</>, white again"
 	renderer.write("#193549", "#ff5733", text)
@@ -80,10 +80,10 @@ func TestWriteColorOverrideBoth(t *testing.T) {
 }
 
 func TestWriteColorOverrideBoth16(t *testing.T) {
-	formats := &ansiFormats{}
-	formats.init("pwsh")
+	ansi := &ansiUtils{}
+	ansi.init("pwsh")
 	renderer := &AnsiColor{
-		formats: formats,
+		ansi: ansi,
 	}
 	text := "This is white, <black,white>this is black</>, white again"
 	renderer.write("#193549", "#ff5733", text)
@@ -92,10 +92,10 @@ func TestWriteColorOverrideBoth16(t *testing.T) {
 }
 
 func TestWriteColorOverrideDouble(t *testing.T) {
-	formats := &ansiFormats{}
-	formats.init("pwsh")
+	ansi := &ansiUtils{}
+	ansi.init("pwsh")
 	renderer := &AnsiColor{
-		formats: formats,
+		ansi: ansi,
 	}
 	text := "<#ffffff>jan</>@<#ffffff>Jans-MBP</>"
 	renderer.write("#193549", "#ff5733", text)
@@ -104,10 +104,10 @@ func TestWriteColorOverrideDouble(t *testing.T) {
 }
 
 func TestWriteColorTransparent(t *testing.T) {
-	formats := &ansiFormats{}
-	formats.init("pwsh")
+	ansi := &ansiUtils{}
+	ansi.init("pwsh")
 	renderer := &AnsiColor{
-		formats: formats,
+		ansi: ansi,
 	}
 	text := "This is white"
 	renderer.writeColoredText("#193549", Transparent, text)
@@ -115,10 +115,10 @@ func TestWriteColorTransparent(t *testing.T) {
 }
 
 func TestWriteColorName(t *testing.T) {
-	formats := &ansiFormats{}
-	formats.init("pwsh")
+	ansi := &ansiUtils{}
+	ansi.init("pwsh")
 	renderer := &AnsiColor{
-		formats: formats,
+		ansi: ansi,
 	}
 	text := "This is white, <red>this is red</>, white again"
 	renderer.write("#193549", "red", text)
@@ -126,10 +126,10 @@ func TestWriteColorName(t *testing.T) {
 }
 
 func TestWriteColorInvalid(t *testing.T) {
-	formats := &ansiFormats{}
-	formats.init("pwsh")
+	ansi := &ansiUtils{}
+	ansi.init("pwsh")
 	renderer := &AnsiColor{
-		formats: formats,
+		ansi: ansi,
 	}
 	text := "This is white, <invalid>this is orange</>, white again"
 	renderer.write("#193549", "invalid", text)
diff --git a/src/ansi_formats_test.go b/src/ansi_test.go
similarity index 96%
rename from src/ansi_formats_test.go
rename to src/ansi_test.go
index d033004f..7655e946 100644
--- a/src/ansi_formats_test.go
+++ b/src/ansi_test.go
@@ -17,7 +17,7 @@ func TestLenWithoutAnsi(t *testing.T) {
 		{Text: "\\[\x1b[44m\\]hello\\[\x1b[0m\\]", ShellName: bash, Expected: 5},
 	}
 	for _, tc := range cases {
-		a := ansiFormats{}
+		a := ansiUtils{}
 		a.init(tc.ShellName)
 		strippedLength := a.lenWithoutANSI(tc.Text)
 		assert.Equal(t, 5, strippedLength)
@@ -35,7 +35,7 @@ func TestGenerateHyperlinkNoUrl(t *testing.T) {
 		{Text: "sample text with no url", ShellName: bash, Expected: "sample text with no url"},
 	}
 	for _, tc := range cases {
-		a := ansiFormats{}
+		a := ansiUtils{}
 		a.init(tc.ShellName)
 		hyperlinkText := a.generateHyperlink(tc.Text)
 		assert.Equal(t, tc.Expected, hyperlinkText)
@@ -53,7 +53,7 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) {
 		{Text: "[google](http://www.google.be)", ShellName: bash, Expected: "\\[\x1b]8;;http://www.google.be\x1b\\\\\\]google\\[\x1b]8;;\x1b\\\\\\]"},
 	}
 	for _, tc := range cases {
-		a := ansiFormats{}
+		a := ansiUtils{}
 		a.init(tc.ShellName)
 		hyperlinkText := a.generateHyperlink(tc.Text)
 		assert.Equal(t, tc.Expected, hyperlinkText)
@@ -71,7 +71,7 @@ func TestGenerateHyperlinkWithUrlNoName(t *testing.T) {
 		{Text: "[](http://www.google.be)", ShellName: bash, Expected: "[](http://www.google.be)"},
 	}
 	for _, tc := range cases {
-		a := ansiFormats{}
+		a := ansiUtils{}
 		a.init(tc.ShellName)
 		hyperlinkText := a.generateHyperlink(tc.Text)
 		assert.Equal(t, tc.Expected, hyperlinkText)
@@ -91,7 +91,7 @@ func TestFormatText(t *testing.T) {
 		{Case: "strikethrough", Text: "This 
is
 white", Expected: "This \x1b[9mis\x1b[29m white"},
 	}
 	for _, tc := range cases {
-		a := ansiFormats{}
+		a := ansiUtils{}
 		a.init("")
 		formattedText := a.formatText(tc.Text)
 		assert.Equal(t, tc.Expected, formattedText, tc.Case)
diff --git a/src/block_test.go b/src/block_test.go
new file mode 100644
index 00000000..863d8515
--- /dev/null
+++ b/src/block_test.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBlockEnabled(t *testing.T) {
+	cases := []struct {
+		Case     string
+		Expected bool
+		Segments []*Segment
+		Type     BlockType
+	}{
+		{Case: "line break block", Expected: true, Type: LineBreak},
+		{Case: "prompt enabled", Expected: true, Type: Prompt, Segments: []*Segment{{active: true}}},
+		{Case: "prompt disabled", Expected: false, Type: Prompt, Segments: []*Segment{{active: false}}},
+		{Case: "prompt enabled multiple", Expected: true, Type: Prompt, Segments: []*Segment{{active: false}, {active: true}}},
+		{Case: "rprompt enabled multiple", Expected: true, Type: RPrompt, Segments: []*Segment{{active: false}, {active: true}}},
+	}
+	for _, tc := range cases {
+		block := &Block{
+			Type:     tc.Type,
+			Segments: tc.Segments,
+		}
+		assert.Equal(t, tc.Expected, block.enabled(), tc.Case)
+	}
+}
diff --git a/src/console_title_test.go b/src/console_title_test.go
index 8d5f77da..bbc42f42 100644
--- a/src/console_title_test.go
+++ b/src/console_title_test.go
@@ -62,12 +62,12 @@ func TestGetConsoleTitle(t *testing.T) {
 		env.On("getenv", "USERDOMAIN").Return("MyCompany")
 		env.On("getCurrentUser", nil).Return("MyUser")
 		env.On("getHostName", nil).Return("MyHost", nil)
-		formats := &ansiFormats{}
-		formats.init(tc.ShellName)
+		ansi := &ansiUtils{}
+		ansi.init(tc.ShellName)
 		ct := &consoleTitle{
-			env:     env,
-			config:  config,
-			formats: formats,
+			env:    env,
+			config: config,
+			ansi:   ansi,
 		}
 		got := ct.getConsoleTitle()
 		assert.Equal(t, tc.Expected, got)
@@ -117,12 +117,12 @@ func TestGetConsoleTitleIfGethostnameReturnsError(t *testing.T) {
 		env.On("getenv", "USERDOMAIN").Return("MyCompany")
 		env.On("getCurrentUser", nil).Return("MyUser")
 		env.On("getHostName", nil).Return("", fmt.Errorf("I have a bad feeling about this"))
-		formats := &ansiFormats{}
-		formats.init(tc.ShellName)
+		ansi := &ansiUtils{}
+		ansi.init(tc.ShellName)
 		ct := &consoleTitle{
-			env:     env,
-			config:  config,
-			formats: formats,
+			env:    env,
+			config: config,
+			ansi:   ansi,
 		}
 		got := ct.getConsoleTitle()
 		assert.Equal(t, tc.Expected, got)
diff --git a/src/image_test.go b/src/image_test.go
index 538d5df6..1d93fdfb 100644
--- a/src/image_test.go
+++ b/src/image_test.go
@@ -15,11 +15,11 @@ func runImageTest(content string) error {
 		return err
 	}
 	defer os.Remove(file.Name())
-	formats := &ansiFormats{}
-	formats.init(shelly)
+	ansi := &ansiUtils{}
+	ansi.init(shelly)
 	image := &ImageRenderer{
 		ansiString: content,
-		formats:    formats,
+		ansi:       ansi,
 	}
 	image.init()
 	err = image.SavePNG(poshImagePath)