diff --git a/README.md b/README.md index 04ecbaf..5ba3881 100644 --- a/README.md +++ b/README.md @@ -221,19 +221,21 @@ The code has no vendored dependencies so no need to worry about that. Execute `gitprompt` as part of `PROMPT`. Add this to your `~/.zshrc`: ``` -export PROMPT='$PROMPT $(gitprompt)' +export PROMPT='$PROMPT %{$(gitprompt -zsh)%}' ``` +> The `-zsh` flag makes `gitprompt` output the correct width of visible +> characters, which fixes counting ansi color codes (breaks wrapping). + Now reload the config (`source ~/.zshrc`) and gitprompt should show up. Feel free to add anything else here too, just execute `gitprompt` where you want the status, for example _(this was used for taking the screenshots in the readme)_: ``` -local ret_status="%(?:%{$fg_bold[green]%}›:%{$fg_bold[red]%}›)" -local dir="%{$fg[cyan]%}%3d%{$reset_color%}" -export PROMPT='${ret_status} ${dir} $(gitprompt)' +export PROMPT='%(?:%{$fg_bold[green]%}›:%{$fg_bold[red]%}›) %{$fg[cyan]%}%3d %{$(gitprompt -zsh)%}%{$reset_color%}' ``` + Alternatively, you can add this to `RPROMPT` instead, which will make the status appear on the right hand side of the screen. `gitprompt` will by default add a trailing space so you you may want to customize the formatting if you diff --git a/cmd/gitprompt/gitprompt.go b/cmd/gitprompt/gitprompt.go index 841a611..332ddfa 100644 --- a/cmd/gitprompt/gitprompt.go +++ b/cmd/gitprompt/gitprompt.go @@ -4,7 +4,6 @@ import ( "flag" "fmt" "os" - "strings" "github.com/akupila/gitprompt" ) @@ -55,8 +54,8 @@ var exampleStatus = &gitprompt.GitStatus{ } var formatHelp = func() string { - return strings.TrimSpace(fmt.Sprintf(` -Define output format. + example, _ := gitprompt.Print(exampleStatus, defaultFormat) + return fmt.Sprintf(`Define output format. Default format is: %q Example result: %s @@ -94,12 +93,12 @@ Text attributes: @f Set faint/dim color @F Clear faint/dim color @i Set italic - @I Clear italic -`, defaultFormat, gitprompt.Print(exampleStatus, defaultFormat))) + @I Clear italic`, defaultFormat, example) } func main() { v := flag.Bool("version", false, "Print version inforformation.") + zsh := flag.Bool("zsh", false, "Print zsh width control characters") flag.Var(&format, "format", formatHelp()) flag.Parse() @@ -111,5 +110,5 @@ func main() { os.Exit(0) } - gitprompt.Exec(format.String()) + gitprompt.Exec(format.String(), *zsh) } diff --git a/git.go b/git.go index 577ec35..9868097 100644 --- a/git.go +++ b/git.go @@ -21,12 +21,15 @@ type GitStatus struct { // data according to the format. // Exits with a non-zero exit code in case git returned an error. Exits with a // blank string if the current directory is not part of a git repository. -func Exec(format string) { +func Exec(format string, printZSH bool) { s, err := Parse() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } - out := Print(s, format) - fmt.Fprintf(os.Stdout, out) + out, num := Print(s, format) + fmt.Fprint(os.Stdout, out) + if printZSH { + fmt.Fprintf(os.Stdout, "%%%dG", num) + } } diff --git a/printer.go b/printer.go index 6b7c580..37d3eff 100644 --- a/printer.go +++ b/printer.go @@ -69,12 +69,15 @@ type group struct { hasData bool hasValue bool + width int } // Print prints the status according to the format. -func Print(s *GitStatus, format string) string { +// +// The integer returned is the print width of the string. +func Print(s *GitStatus, format string) (string, int) { if s == nil { - return "" + return "", 0 } in := make(chan rune) @@ -93,7 +96,7 @@ func Print(s *GitStatus, format string) string { return buildOutput(s, in) } -func buildOutput(s *GitStatus, in chan rune) string { +func buildOutput(s *GitStatus, in chan rune) (string, int) { root := &group{} g := root @@ -148,6 +151,7 @@ func buildOutput(s *GitStatus, in chan rune) string { g.parent.format = g.format g.parent.format.setColor(0) g.parent.format.clearAttributes() + g.parent.width += g.width } g = g.parent default: @@ -170,7 +174,7 @@ func buildOutput(s *GitStatus, in chan rune) string { g.format.clearAttributes() g.format.printANSI(&g.buf) - return root.buf.String() + return root.buf.String(), root.width } func setColor(g *group, ch rune) { @@ -274,15 +278,16 @@ func (g *group) addRune(r rune) { if !unicode.IsSpace(r) { g.format.printANSI(&g.buf) } + g.width++ g.buf.WriteRune(r) } func (g *group) addString(s string) { g.format.printANSI(&g.buf) + g.width += len(s) g.buf.WriteString(s) } func (g *group) addInt(i int) { - g.format.printANSI(&g.buf) - g.buf.WriteString(strconv.Itoa(i)) + g.addString(strconv.Itoa(i)) } diff --git a/printer_test.go b/printer_test.go index ef5901a..0d2539d 100644 --- a/printer_test.go +++ b/printer_test.go @@ -16,23 +16,27 @@ var all = &GitStatus{ } func TestPrinterEmpty(t *testing.T) { - actual := Print(nil, "%h") + actual, w := Print(nil, "%h") assertOutput(t, "", actual) + assertWidth(t, 0, w) } func TestPrinterData(t *testing.T) { - actual := Print(all, "%h %u %m %s %c %a %b") + actual, w := Print(all, "%h %u %m %s %c %a %b") assertOutput(t, "master 0 1 2 3 4 5", actual) + assertWidth(t, 18, w) } func TestPrinterUnicode(t *testing.T) { - actual := Print(all, "%h ✋%u ⚡️%m 🚚%s ❗️%c ⬆%a ⬇%b") + actual, w := Print(all, "%h ✋%u ⚡️%m 🚚%s ❗️%c ⬆%a ⬇%b") assertOutput(t, "master ✋0 ⚡️1 🚚2 ❗️3 ⬆4 ⬇5", actual) + assertWidth(t, 26, w) } func TestShortSHA(t *testing.T) { - actual := Print(&GitStatus{Sha: "858828b5e153f24644bc867598298b50f8223f9b"}, "%h") + actual, w := Print(&GitStatus{Sha: "858828b5e153f24644bc867598298b50f8223f9b"}, "%h") assertOutput(t, "858828b", actual) + assertWidth(t, 7, w) } func TestPrinterColorAttributes(t *testing.T) { @@ -40,68 +44,81 @@ func TestPrinterColorAttributes(t *testing.T) { name string format string expected string + width int }{ { name: "red", format: "#r%h", expected: "\x1b[31mmaster\x1b[0m", + width: 6, }, { name: "bold", format: "@b%h", expected: "\x1b[1mmaster\x1b[0m", + width: 6, }, { name: "color & attribute", format: "#r@bA", expected: "\x1b[1;31mA\x1b[0m", + width: 1, }, { name: "color & attribute reversed", format: "@b#rA", expected: "\x1b[1;31mA\x1b[0m", + width: 1, }, { name: "ignore format until non-whitespace", format: "A#r#g#b B@i\tC", expected: "A \x1b[34mB\t\x1b[3mC\x1b[0m", + width: 9, }, { name: "reset color", format: "#rA#_B", expected: "\x1b[31mA\x1b[0mB", + width: 2, }, { name: "reset attributes", format: "@bA@_B", expected: "\x1b[1mA\x1b[0mB", + width: 2, }, { name: "reset attribute", format: "#ggreen @b@igreen_bold_italic @Bgreen_italic", expected: "\x1b[32mgreen \x1b[1;3mgreen_bold_italic \x1b[0;3;32mgreen_italic\x1b[0m", + width: 36, }, { name: "ending with #", format: "%h#", expected: "master#", + width: 7, }, { name: "ending with !", format: "%h!", expected: "master!", + width: 7, }, { name: "ending with @", format: "%h@", expected: "master@", + width: 7, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual := Print(all, test.format) + actual, w := Print(all, test.format) assertOutput(t, test.expected, actual) + assertWidth(t, test.width, w) }) } } @@ -111,23 +128,27 @@ func TestPrinterGroups(t *testing.T) { name string format string expected string + width int }{ { name: "groups", format: "<[%h][ B%b A%a][ U%u][ C%c]>", expected: "", + width: 17, }, { name: "group color", format: "<[#r%h]-[#g%u]%a[-#b%b]>", expected: "<\x1b[31mmaster\x1b[0m-4-\x1b[34m5\x1b[0m>", + width: 12, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual := Print(all, test.format) + actual, w := Print(all, test.format) assertOutput(t, test.expected, actual) + assertWidth(t, test.width, w) }) } } @@ -137,88 +158,105 @@ func TestPrinterNonMatching(t *testing.T) { name string format string expected string + width int }{ { name: "data valid odd", format: "%%%h", expected: "%%master", + width: 8, }, { name: "data valid even", format: "%%%%h", expected: "%%%%h", + width: 5, }, { name: "data invalid odd", format: "%%%z", expected: "%%%z", + width: 4, }, { name: "data invalid even", format: "%%%%z", expected: "%%%%z", + width: 5, }, { name: "color valid odd", format: "###rA", expected: "##\x1b[31mA\x1b[0m", + width: 3, }, { name: "color valid even", format: "####rA", expected: "####rA", + width: 6, }, { name: "color invalid odd", format: "###zA", expected: "###zA", + width: 5, }, { name: "color invalid even", format: "####zA", expected: "####zA", + width: 6, }, { name: "attribute valid odd", format: "@@@bA", expected: "@@\x1b[1mA\x1b[0m", + width: 3, }, { name: "attribute valid even", format: "@@@@bA", expected: "@@@@bA", + width: 6, }, { name: "attribute invalid odd", format: "@@@zA", expected: "@@@zA", + width: 5, }, { name: "attribute invalid even", format: "@@@@zA", expected: "@@@@zA", + width: 6, }, { name: "trailing %", format: "A%", expected: "A%", + width: 2, }, { name: "trailing #", format: "A#", expected: "A#", + width: 2, }, { name: "trailing @", format: "A@", expected: "A@", + width: 2, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual := Print(all, test.format) + actual, w := Print(all, test.format) assertOutput(t, test.expected, actual) + assertWidth(t, test.width, w) }) } } @@ -228,33 +266,39 @@ func TestPrinterEscape(t *testing.T) { name string format string expected string + width int }{ { name: "data", format: "A\\%h", expected: "A%h", + width: 3, }, { name: "color", format: "A\\#rB", expected: "A#rB", + width: 4, }, { name: "attribute", format: "A\\!bB", expected: "A!bB", + width: 4, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual := Print(all, test.format) + actual, w := Print(all, test.format) assertOutput(t, test.expected, actual) + assertWidth(t, test.width, w) }) } } func assertOutput(t *testing.T, expected, actual string) { + t.Helper() if actual == expected { return } @@ -270,3 +314,10 @@ Actual: %s actual, ) } + +func assertWidth(t *testing.T, expected, actual int) { + t.Helper() + if expected != actual { + t.Errorf("Width does not match; expected %d, actual %d", expected, actual) + } +}