From f4972861c1d0e8256951f8734b63ed3c680277f6 Mon Sep 17 00:00:00 2001 From: Jason Walton Date: Sun, 10 Apr 2022 14:13:19 -0400 Subject: [PATCH] feat: Add flexible_space module. --- cmd/prompt.go | 6 +- docs/docs/reference/modules.mdx | 4 + internal/kitsch/modules/flexible_space.go | 123 +++++++++++++++++ .../kitsch/modules/flexible_space_test.go | 21 +++ internal/kitsch/modules/moduleWrapper.go | 6 + .../modules/schemas/flexible_space_schema.go | 12 ++ sampleconfig/powerline2.yaml | 128 ++++++++++-------- 7 files changed, 242 insertions(+), 58 deletions(-) create mode 100644 internal/kitsch/modules/flexible_space.go create mode 100644 internal/kitsch/modules/flexible_space_test.go create mode 100644 internal/kitsch/modules/schemas/flexible_space_schema.go diff --git a/cmd/prompt.go b/cmd/prompt.go index 5687b5c..34c4b9a 100644 --- a/cmd/prompt.go +++ b/cmd/prompt.go @@ -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) }, } diff --git a/docs/docs/reference/modules.mdx b/docs/docs/reference/modules.mdx index 337e32a..1c39729 100644 --- a/docs/docs/reference/modules.mdx +++ b/docs/docs/reference/modules.mdx @@ -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. diff --git a/internal/kitsch/modules/flexible_space.go b/internal/kitsch/modules/flexible_space.go new file mode 100644 index 0000000..4380788 --- /dev/null +++ b/internal/kitsch/modules/flexible_space.go @@ -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 +} diff --git a/internal/kitsch/modules/flexible_space_test.go b/internal/kitsch/modules/flexible_space_test.go new file mode 100644 index 0000000..d293895 --- /dev/null +++ b/internal/kitsch/modules/flexible_space_test.go @@ -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) +} diff --git a/internal/kitsch/modules/moduleWrapper.go b/internal/kitsch/modules/moduleWrapper.go index 73196fb..d50621a 100644 --- a/internal/kitsch/modules/moduleWrapper.go +++ b/internal/kitsch/modules/moduleWrapper.go @@ -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 diff --git a/internal/kitsch/modules/schemas/flexible_space_schema.go b/internal/kitsch/modules/schemas/flexible_space_schema.go new file mode 100644 index 0000000..e3204a0 --- /dev/null +++ b/internal/kitsch/modules/schemas/flexible_space_schema.go @@ -0,0 +1,12 @@ +// Code generated by "genSchema --pkg schemas FlexibleSpaceModule"; DO NOT EDIT. + +package schemas + +// FlexibleSpaceModuleJSONSchema is the JSON schema for the FlexibleSpaceModule struct. +var FlexibleSpaceModuleJSONSchema = `{ + "type": "object", + "properties": { + "type": {"type": "string", "description": "Type is the type of this module.", "enum": ["flexible_space"]} + }, + "required": ["type"]}` + diff --git a/sampleconfig/powerline2.yaml b/sampleconfig/powerline2.yaml index e63b766..1d229f8 100644 --- a/sampleconfig/powerline2.yaml +++ b/sampleconfig/powerline2.yaml @@ -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