Repository: JanDeDobbeleer/oh-my-posh
Pull request #4279 (2023-09-23 20:27:25)

colors
### Prerequisites

- [x] I have read and understood the [contributing guide][CONTRIBUTING.md].
- [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

<!--
copilot:summary
-->
### <samp>🤖 Generated by Copilot at 44f0d66</samp>

This pull request fixes a bug in the `ansi_writer` package, adds a new feature to the `segments` package, and improves some tests. The bug affected color restoration in nested overrides and was reported in issue #1139. The new feature allows displaying the git remote name and URL in the git segment and was requested in issue #1128. The tests cover the bug fix and the new feature.

### How

<!--
copilot:walkthrough
-->
### <samp>🤖 Generated by Copilot at 44f0d66</samp>

* Fix a bug in the `endColorOverride` function that caused incorrect color restoration when using nested color overrides ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4279/files?diff=unified&w=0#diff-0267d2a06e9c5eab5a04e9625772dd68774a70872a4014630cdb15183ce7dcd9L491-R499),[link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4279/files?diff=unified&w=0#diff-0267d2a06e9c5eab5a04e9625772dd68774a70872a4014630cdb15183ce7dcd9L518-L522))
  * Update the expected output of the test case "Bold with color override" in the `TestWriteANSIColors` function to match the new behavior ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4279/files?diff=unified&w=0#diff-7214a6ffb60e6b167949f626066440b0deabc9698c4e7923aa23478ab773476aL35-R35))
  * Remove an empty line from the `ansi` package in the `ansi_writer_test.go` file ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4279/files?diff=unified&w=0#diff-7214a6ffb60e6b167949f626066440b0deabc9698c4e7923aa23478ab773476aL44))
* Add a new function `Remotes` to the `Git` struct that returns a map of remote names and URLs from the git config file ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4279/files?diff=unified&w=0#diff-5ca08bba45ba772ce1cbd182429db15292b228ac7c5dc0cf370dcbe55611fd92R781-R804))
  * Add a new test function `TestGitRemotes` to the `segments` package in the `git_test.go` file that tests the behavior of the `Remotes` function with different config files and scenarios ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4279/files?diff=unified&w=0#diff-7290b1c5198130d19c091496b4368edbcfd2b10d49813e6f92f01fbe26a56122R1053-R1107))

- fix(ansi): restore previous override correctly
- feat(git): support for remotes

<!---

Tips:

If you're not comfortable with working with Git,
we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 5ee3efd0..030098be 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -59,7 +59,7 @@
       "extensions": [
         "actboy168.tasks",
         "eamodio.gitlens",
-        "bungcip.better-toml",
+        "tamasfe.even-better-toml",
         "davidanson.vscode-markdownlint",
         "editorconfig.editorconfig",
         "esbenp.prettier-vscode",
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 20129c66..7f6664eb 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -6,7 +6,7 @@
     "esbenp.prettier-vscode",
     "davidanson.vscode-markdownlint",
     "yzhang.markdown-all-in-one",
-    "bungcip.better-toml",
+    "tamasfe.even-better-toml",
     "redhat.vscode-yaml"
   ]
 }
diff --git a/src/ansi/ansi_writer.go b/src/ansi/ansi_writer.go
index 820ad1a2..7f1ff9f9 100644
--- a/src/ansi/ansi_writer.go
+++ b/src/ansi/ansi_writer.go
@@ -488,12 +488,15 @@ func (w *Writer) endColorOverride(position int) int {
 	// make sure to reset the colors if needed
 	position += len([]rune(resetStyle.AnchorEnd)) - 1
 
-	// reset colors to previous when we have > 2 in stack
-	// - first is always the starting colors used in Write()
-	// - second is the first override
-	// as soon as we have  more than 2, we can pop the last one
+	// do not restore colors at the end of the string, we print it anyways
+	if position == len(w.runes)-1 {
+		return position
+	}
+
+	// reset colors to previous when we have >= 2 in stack
+	// as soon as we have  more than 1, we can pop the last one
 	// and print the previous override as it wasn't ended yet
-	if w.current.Len() >= 3 {
+	if w.current.Len() >= 2 {
 		fg := w.current.Foreground()
 		bg := w.current.Background()
 
@@ -515,11 +518,6 @@ func (w *Writer) endColorOverride(position int) int {
 		return position
 	}
 
-	// do not restore colors at the end of the string, we print it anyways
-	if position == len(w.runes)-1 {
-		return position
-	}
-
 	if w.transparent {
 		w.writeEscapedAnsiString(transparentEnd)
 	}
diff --git a/src/segments/git.go b/src/segments/git.go
index 7450dc34..cb77876c 100644
--- a/src/segments/git.go
+++ b/src/segments/git.go
@@ -778,6 +778,30 @@ func (g *Git) getRemoteURL() string {
 	return g.getGitCommandOutput("remote", "get-url", upstream)
 }
 
+func (g *Git) Remotes() map[string]string {
+	var remotes = make(map[string]string)
+
+	location := filepath.Join(g.rootDir, "config")
+	config := g.env.FileContent(location)
+	cfg, err := ini.Load([]byte(config))
+	if err != nil {
+		return remotes
+	}
+
+	for _, section := range cfg.Sections() {
+		if !strings.HasPrefix(section.Name(), "remote ") {
+			continue
+		}
+
+		name := strings.TrimPrefix(section.Name(), "remote ")
+		name = strings.Trim(name, "\"")
+		url := section.Key("url").String()
+		url = g.cleanUpstreamURL(url)
+		remotes[name] = url
+	}
+	return remotes
+}
+
 func (g *Git) getUntrackedFilesMode() string {
 	return g.getSwitchMode(UntrackedModes, "-u", "normal")
 }


Test patch:
diff --git a/src/ansi/ansi_writer_test.go b/src/ansi/ansi_writer_test.go
index c23ad58c..d344ba54 100644
--- a/src/ansi/ansi_writer_test.go
+++ b/src/ansi/ansi_writer_test.go
@@ -32,7 +32,7 @@ func TestWriteANSIColors(t *testing.T) {
 		{
 			Case:     "Bold with color override",
 			Input:    "<b><#ffffff>test</></b>",
-			Expected: "\x1b[1m\x1b[30m\x1b[38;2;255;255;255mtest\x1b[49m\x1b[30m\x1b[22m\x1b[0m",
+			Expected: "\x1b[1m\x1b[30m\x1b[38;2;255;255;255mtest\x1b[30m\x1b[22m\x1b[0m",
 			Colors:   &Colors{Foreground: "black", Background: ParentBackground},
 		},
 		{
@@ -41,7 +41,6 @@ func TestWriteANSIColors(t *testing.T) {
 			Expected: "\x1b[38;2;255;255;255m\x1b[1mtest\x1b[22m\x1b[0m",
 			Colors:   &Colors{Foreground: "black", Background: ParentBackground},
 		},
-
 		{
 			Case:     "Double override",
 			Input:    "<#ffffff>jan</>@<#ffffff>Jans-MBP</>",
diff --git a/src/segments/git_test.go b/src/segments/git_test.go
index e6f179dd..80710927 100644
--- a/src/segments/git_test.go
+++ b/src/segments/git_test.go
@@ -1050,3 +1050,58 @@ func TestGitCommit(t *testing.T) {
 		assert.Equal(t, tc.Expected, got, tc.Case)
 	}
 }
+
+func TestGitRemotes(t *testing.T) {
+	cases := []struct {
+		Case     string
+		Expected int
+		Config   string
+	}{
+		{
+			Case:     "Empty config file",
+			Expected: 0,
+		},
+		{
+			Case:     "Two remotes",
+			Expected: 2,
+			Config: `
+[remote "origin"]
+	url = git@github.com:JanDeDobbeleer/test.git
+	fetch = +refs/heads/*:refs/remotes/origin/*
+[remote "upstream"]
+	url = git@github.com:microsoft/test.git
+	fetch = +refs/heads/*:refs/remotes/upstream/*
+`,
+		},
+		{
+			Case:     "One remote",
+			Expected: 1,
+			Config: `
+[remote "origin"]
+	url = git@github.com:JanDeDobbeleer/test.git
+	fetch = +refs/heads/*:refs/remotes/origin/*
+`,
+		},
+		{
+			Case:     "Broken config",
+			Expected: 0,
+			Config:   "{{}}",
+		},
+	}
+
+	for _, tc := range cases {
+		env := new(mock.MockedEnvironment)
+		env.On("FileContent", "config").Return(tc.Config)
+
+		g := &Git{
+			scm: scm{
+				props:   properties.Map{},
+				realDir: "foo",
+				env:     env,
+			},
+		}
+
+		got := g.Remotes()
+		assert.Equal(t, tc.Expected, len(got), tc.Case)
+	}
+}