Skip to content

Commit

Permalink
feat: Add flexible_space module.
Browse files Browse the repository at this point in the history
  • Loading branch information
jwalton committed Apr 10, 2022
1 parent f369fc0 commit f497286
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 58 deletions.
6 changes: 3 additions & 3 deletions cmd/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,15 @@ var promptCmd = &cobra.Command{
performance.End("Context setup")

// Execute the prompt.
result := configuration.Prompt.Execute(&context)
moduleResult, promptTest := modules.RenderPrompt(&context, configuration.Prompt)

performance.EndWithChildren("Prompt", result.Performance)
performance.EndWithChildren("Prompt", moduleResult.Performance)

if perf {
performance.Print()
}

withEscapes := shellprompt.AddZeroWidthCharacterEscapes(context.Globals.Shell, result.Text)
withEscapes := shellprompt.AddZeroWidthCharacterEscapes(context.Globals.Shell, promptTest)
fmt.Print(withEscapes)
},
}
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/reference/modules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ Outputs:
- `Symbol (string)` is the `aheadSymbol`, `behindSymbol`, `divergedSymbol`, `upToDateSymbol`, or `noUpstreamSymbol`.
- `AheadBehind (string)` is the empty string if not in a git repo, or is one of "ahead", "behind", "diverged", or "upToDate" (this will be "upToDate" if there is no upstream).

## flexible_space

The flexible_space module adds a variable-width space to a line. The space will grow to use as many characters as possible without causing the current line to wrap. This can be used to split a prompt into a portion printed on the left side of the terminal and a second portion printed on the right side. You can put multiple flexible_spaces on a single line, in which case the available space will be split evenly between them (you could use two flexible_spaces to center some text, for example).

## git_head

The git_head module returns information about the HEAD of the current git repo. The default output is the `Description` from the Outputs section below.
Expand Down
123 changes: 123 additions & 0 deletions internal/kitsch/modules/flexible_space.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package modules

import (
"strings"

"github.com/jwalton/go-ansiparser"
"github.com/jwalton/kitsch/internal/kitsch/modules/schemas"
"github.com/mattn/go-runewidth"
"gopkg.in/yaml.v3"
)

// We can't work out the width of a flexible space, until after we've rendered
// the complete prompt. We use this marker, which is extremely unlikely to
// occur naturally in the output of any module, as a sentinel for the flexible
// space, and then replace it at the end after the entire prompt is rendered.
//
// An alternative to using a sentinel value here would be to let the block
// module handle a flexible space. We'd have to somehow mark which children were
// flexible spaces, which is a very easy to solve problem. However, if
// a block had a flexible space and a template, the flexible space wouldn't "survive"
// through the template (how would we represent a flexible space in the output
// of a template?) and would end up being ignored. Second, with nested blocks,
// which block should handle the flexible space? Should the nearest ancestor
// fill out the line to the terminal width?
//
// This approach is a lot easier to use, although it has the disadvantage that
// if some other module accidentally or maliciously produces the `flexibleSpaceMarker`,
// we'll end up printing a bunch of spaces where we shouldn't.
const flexibleSpaceMarker = "\t \u00a0/\\;:flex:;\\/\u00a0 \t"

//go:generate go run ../genSchema/main.go --pkg schemas FlexibleSpaceModule

// FlexibleSpaceModule inserts a flexible-width space.
//
type FlexibleSpaceModule struct {
// Type is the type of this module.
Type string `yaml:"type" jsonschema:",required,enum=flexible_space"`
}

// Execute the flexible space module.
func (mod FlexibleSpaceModule) Execute(context *Context) ModuleResult {
return ModuleResult{DefaultText: flexibleSpaceMarker, Data: map[string]interface{}{}}
}

func init() {
registerModule(
"flexible_space",
registeredModule{
jsonSchema: schemas.FlexibleSpaceModuleJSONSchema,
factory: func(node *yaml.Node) (Module, error) {
var module FlexibleSpaceModule = FlexibleSpaceModule{
Type: "flexible_space",
}
err := node.Decode(&module)
return &module, err
},
},
)
}

// processFlexibleSpaces replaces flexible space markers with spaces.
func processFlexibleSpaces(terminalWidth int, renderedPrompt string) string {
result := ""

// Split into lines.
lines := strings.Split(renderedPrompt, "\n")
for lineIndex, line := range lines {

// For each line, split into segments around the FlexibleSpaceMarker.
if strings.Contains(line, flexibleSpaceMarker) {
segments := strings.Split(line, flexibleSpaceMarker)

segmentsTotalLength := 0
for _, segment := range segments {
segmentsTotalLength += getPrintWidth(segment)
}

extraSpace := terminalWidth - segmentsTotalLength

if extraSpace > 0 {
spacesAdded := 0
spacesPerSegment := extraSpace / (len(segments) - 1)

line = ""
for index := 0; index < len(segments)-2; index++ {
line += segments[index]
line += strings.Repeat(" ", spacesPerSegment)
spacesAdded += spacesPerSegment
}

line += segments[len(segments)-2]
line += strings.Repeat(" ", extraSpace-spacesAdded)
line += segments[len(segments)-1]
}
}

result += line
if lineIndex < len(lines)-1 {
result += "\n"
}
}

return result
}

func getPrintWidth(str string) int {
width := 0
tokenizer := ansiparser.NewStringTokenizer(str)

for tokenizer.Next() {
token := tokenizer.Token()

if token.Type == ansiparser.String {
if token.IsASCII {
width += len(token.Content)
} else {
width += runewidth.StringWidth(token.Content)
}
}
}

return width
}
21 changes: 21 additions & 0 deletions internal/kitsch/modules/flexible_space_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package modules

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestProcessFlexibleSpaces(t *testing.T) {
result := processFlexibleSpaces(10, "a"+flexibleSpaceMarker+"b")
assert.Equal(t, "a b", result)

result = processFlexibleSpaces(10, "a"+flexibleSpaceMarker+"b\n$ ")
assert.Equal(t, "a b\n$ ", result)

result = processFlexibleSpaces(10, "ab")
assert.Equal(t, "ab", result)

result = processFlexibleSpaces(10, "a"+flexibleSpaceMarker+"b"+flexibleSpaceMarker+"c")
assert.Equal(t, "a b c", result)
}
6 changes: 6 additions & 0 deletions internal/kitsch/modules/moduleWrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ func processModuleResult(
}
}

// RenderPrompt renders the top-level module in a prompt.
func RenderPrompt(context *Context, root ModuleWrapper) (ModuleWrapperResult, string) {
result := root.Execute(context)
return result, processFlexibleSpaces(context.Globals.TerminalWidth, result.Text)
}

// func testTemplate(context *Context, prefix string, template string, dataMap map[string]interface{}) {
// if template == "" {
// return
Expand Down
12 changes: 12 additions & 0 deletions internal/kitsch/modules/schemas/flexible_space_schema.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

128 changes: 73 additions & 55 deletions sampleconfig/powerline2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,64 +13,82 @@ colors:
$gitUnstagedBg: "#600"
prompt:
type: block
join: ""
modules:
- type: time
- type: directory
- type: git_head
- type: git_diverged
- type: git_state
- type: git_status
indexStyle: "#3f3"
unstagedStyle: "#f33"
unmergedStyle: "#f8f bold"
stashStyle: "#f90 bold"
- type: jobs
- type: command_duration
style: $commandDurationFg
template: |
{{- $pl := newPowerline " " "\ue0b0" " " -}}
{{- $globals := .Globals -}}
{{- with .Data.Modules -}}
{{- printf " %s" .time.Text | style "$timeFg" | $pl.Segment "$timeBg" -}}
{{- .directory.Text | style "$directoryFg" | $pl.Segment "$directoryBg" -}}
- type: block
id: left
modules:
- type: directory
- type: git_head
- type: git_diverged
- type: git_state
- type: git_status
indexStyle: "#3f3"
unstagedStyle: "#f33"
unmergedStyle: "#f8f bold"
stashStyle: "#f90 bold"
- type: jobs
- type: command_duration
style: $commandDurationFg
template: |
{{- $pl := newPowerline " " "\ue0b0" " " -}}
{{- $globals := .Globals -}}
{{- with .Data.Modules -}}
{{- printf " %s" .directory.Text | style "$directoryFg" | $pl.Segment "$directoryBg" -}}
{{- /* Git */ -}}
{{- if .git_head -}}
{{- $gitStyles := dict
"upToDate" "$gitBg"
"ahead" "$gitBgAhead"
"behind" "$gitBgBehind"
"diverged" "^gitBgBehind"
-}}
{{- $gitBg := (get $gitStyles .git_diverged.Data.AheadBehind) -}}
{{- $gitInfo := printf "%s %s%s" .git_head.Text .git_diverged.Text .git_state.Text -}}
{{- $branchSymbol := "\ue0a0" -}}
{{- printf "%s %s" $branchSymbol $gitInfo | style "$gitFg" | $pl.Segment $gitBg -}}
{{- end -}}
{{- with .git_status -}}
{{- $gitStatusBg := "#444" -}}
{{- if and (gt .Data.Index.Total 0) (gt .Data.Unstaged.Total 0) -}}
{{- $gitStatusBg = "linear-gradient($gitIndexBg, $gitUnstagedBg)" -}}
{{- else if gt .Data.Index.Total 0 -}}
{{- $gitStatusBg = "$gitIndexBg" -}}
{{- else if gt .Data.Unstaged.Total 0 -}}
{{- $gitStatusBg = "$gitUnstagedBg" -}}
{{- /* Git */ -}}
{{- if .git_head -}}
{{- $gitStyles := dict
"upToDate" "$gitBg"
"ahead" "$gitBgAhead"
"behind" "$gitBgBehind"
"diverged" "^gitBgBehind"
-}}
{{- $gitBg := (get $gitStyles .git_diverged.Data.AheadBehind) -}}
{{- $gitInfo := printf "%s %s%s" .git_head.Text .git_diverged.Text .git_state.Text -}}
{{- $branchSymbol := "\ue0a0" -}}
{{- printf "%s %s" $branchSymbol $gitInfo | style "$gitFg" | $pl.Segment $gitBg -}}
{{- end -}}
{{- .Text | style "#fff" | $pl.Segment $gitStatusBg -}}
{{- end -}}
{{- .command_duration.Text | $pl.Segment "$commandDurationBg" -}}
{{- with .git_status -}}
{{- $gitStatusBg := "#444" -}}
{{- if and (gt .Data.Index.Total 0) (gt .Data.Unstaged.Total 0) -}}
{{- $gitStatusBg = "linear-gradient($gitIndexBg, $gitUnstagedBg)" -}}
{{- else if gt .Data.Index.Total 0 -}}
{{- $gitStatusBg = "$gitIndexBg" -}}
{{- else if gt .Data.Unstaged.Total 0 -}}
{{- $gitStatusBg = "$gitUnstagedBg" -}}
{{- end -}}
{{- .Text | style "#fff" | $pl.Segment $gitStatusBg -}}
{{- end -}}
{{- .command_duration.Text | $pl.Segment "$commandDurationBg" -}}
{{- /* Jobs and Status */ -}}
{{- if or (ne $globals.Status 0) .jobs.Text (eq $globals.Keymap "vicmd") -}}
{{- $color := (ne $globals.Status 0) | ternary "#b00" "#00b" -}}
{{- $out := list .jobs.Text -}}
{{- if (ne $globals.Status 0) -}}
{{- $out = mustAppend $out $globals.Status -}}
{{- /* Jobs and Status */ -}}
{{- if or (ne $globals.Status 0) .jobs.Text (eq $globals.Keymap "vicmd") -}}
{{- $color := (ne $globals.Status 0) | ternary "#b00" "#00b" -}}
{{- $out := list .jobs.Text -}}
{{- if (ne $globals.Status 0) -}}
{{- $out = mustAppend $out $globals.Status -}}
{{- end -}}
{{- if (eq $globals.Keymap "vicmd") -}}
{{- $out = mustAppend $out ":" -}}
{{- end -}}
{{- $out | join " " | style "#fff" | $pl.Segment $color -}}
{{- end -}}
{{- end -}}
{{- if (eq $globals.Keymap "vicmd") -}}
{{- $out = mustAppend $out ":" -}}
{{- $pl.Finish -}}{{- " " -}}
- type: flexible_space
- type: block
id: right
modules:
- type: time
template: |
{{- $pl := newReversePowerline " " "\ue0b2" " " -}}
{{- with .Data.Modules -}}
{{- printf "%s " .time.Text | style "$timeFg" | $pl.Segment "$timeBg" -}}
{{- end -}}
{{- $out | join " " | style "#fff" | $pl.Segment $color -}}
{{- end -}}
{{- end -}}
{{- $pl.Finish -}}{{- " " -}}
{{- $pl.Finish -}}{{- "\n" -}}
- type: block
id: prompt
modules:
- type: prompt
errorStyle: brightRed

0 comments on commit f497286

Please sign in to comment.