feat: newline as part of block

this deprecates the "newline" block and favours using the newline
property on the Block component. For backwards compatibility we'll
keep recognizing the newline block for the time being.

resolves #607
This commit is contained in:
Jan De Dobbeleer 2021-04-18 19:16:06 +02:00 committed by Jan De Dobbeleer
parent 7ad764ceee
commit c24ca82f17
32 changed files with 364 additions and 342 deletions

2
.vscode/launch.json vendored
View file

@ -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",

View file

@ -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",

192
src/block.go Normal file
View file

@ -0,0 +1,192 @@
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
color *AnsiColor
activeSegment *Segment
previousActiveSegment *Segment
}
func (b *Block) init(env environmentInfo, color *AnsiColor) {
b.env = env
b.color = color
}
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 {
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.color.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.color.write(foreground, background, symbol)
return
}
b.color.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.color.write(Transparent, b.activeSegment.background(), b.activeSegment.LeadingDiamond)
b.renderText(text)
b.color.write(Transparent, b.activeSegment.background(), b.activeSegment.TrailingDiamond)
}
func (b *Block) renderText(text string) {
text = b.color.formats.generateHyperlink(text)
defaultValue := " "
prefix := b.activeSegment.getValue(Prefix, defaultValue)
postfix := b.activeSegment.getValue(Postfix, defaultValue)
b.color.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.color.string()
b.color.builder.Reset()
}
segmentTimings = append(segmentTimings, &segmentTiming)
}
return largestSegmentNameLength, segmentTimings
}

29
src/block_test.go Normal file
View file

@ -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)
}
}

View file

@ -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)

View file

@ -3,7 +3,6 @@ package main
import (
"fmt"
"strings"
"sync"
"time"
)
@ -13,144 +12,15 @@ type engine struct {
color *AnsiColor
renderer *AnsiRenderer
consoleTitle *consoleTitle
activeBlock *Block
activeSegment *Segment
previousActiveSegment *Segment
// 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()
}
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)
}
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) 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) 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())
@ -171,9 +41,43 @@ func (e *engine) render() string {
return e.print()
}
func (e *engine) renderBlock(block *Block) {
block.init(e.env, e.color)
block.setStringValues()
defer e.color.reset()
if !block.enabled() {
return
}
if block.Newline {
e.renderer.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.renderer.write("\n")
case Prompt:
if block.VerticalOffset != 0 {
e.renderer.changeLine(block.VerticalOffset)
}
switch block.Alignment {
case Right:
e.renderer.carriageForward()
blockText := block.renderSegments()
e.renderer.setCursorForRightWrite(blockText, block.HorizontalOffset)
e.renderer.write(blockText)
case Left:
e.renderer.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")
@ -181,7 +85,7 @@ func (e *engine) debug() string {
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 +96,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.color)
longestSegmentName, timings := block.debug()
segmentTimings = append(segmentTimings, timings...)
if longestSegmentName > largestSegmentNameLength {
largestSegmentNameLength = longestSegmentName
}
}
@ -258,9 +137,3 @@ func (e *engine) print() string {
}
return e.renderer.string()
}
func (e *engine) resetBlock() {
e.color.reset()
e.previousActiveSegment = nil
e.activeBlock = nil
}

View file

@ -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

View file

@ -16,12 +16,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "session",

View file

@ -34,12 +34,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "text",

View file

@ -107,11 +107,9 @@
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "text",

View file

@ -146,12 +146,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "executiontime",

View file

@ -133,12 +133,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "session",

View file

@ -38,12 +38,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "text",

View file

@ -45,12 +45,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "path",

View file

@ -51,12 +51,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "root",

View file

@ -67,12 +67,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "battery",

View file

@ -60,12 +60,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "os",

View file

@ -86,12 +86,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "text",

View file

@ -56,12 +56,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "exit",

View file

@ -60,12 +60,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "text",

View file

@ -60,12 +60,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "text",

View file

@ -46,13 +46,11 @@
}
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "os",

View file

@ -78,12 +78,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "exit",

View file

@ -16,12 +16,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "path",

View file

@ -85,12 +85,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "exit",

View file

@ -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",

View file

@ -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",

View file

@ -165,12 +165,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "text",

View file

@ -163,12 +163,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "text",

View file

@ -53,12 +53,10 @@
}
]
},
{
"type": "newline"
},
{
"type": "prompt",
"alignment": "left",
"newline": true,
"segments": [
{
"type": "text",

View file

@ -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",

View file

@ -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",