Repository: JanDeDobbeleer/oh-my-posh
Pull request #3411 (2023-01-27 13:41:59)

feat: migrate v3 Nerd Font glyphs
### 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

Allows users to easily migrate their broken v2 Nerd Font glyphs to v3 using the following command:

```bash
oh-my-posh config migrate glyphs --write
```

Relates to https://github.com/ryanoasis/nerd-fonts/issues/365
Relates to https://github.com/ryanoasis/nerd-fonts/issues/1059

<!---

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/.vscode/launch.json b/.vscode/launch.json
index e793e955..736ce94e 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -2,7 +2,7 @@
   "version": "0.2.0",
   "configurations": [
     {
-      "name": "Debug Prompt",
+      "name": "Primary",
       "type": "go",
       "request": "launch",
       "mode": "debug",
@@ -16,7 +16,7 @@
       ]
     },
     {
-      "name": "Debug Tooltip",
+      "name": "Tooltip",
       "type": "go",
       "request": "launch",
       "mode": "debug",
@@ -30,7 +30,7 @@
       ]
     },
     {
-      "name": "Debug Transient",
+      "name": "Transient",
       "type": "go",
       "request": "launch",
       "mode": "debug",
@@ -54,7 +54,7 @@
       ]
     },
     {
-      "name": "Print debug",
+      "name": "Debug",
       "type": "go",
       "request": "launch",
       "mode": "debug",
@@ -65,7 +65,7 @@
       ]
     },
     {
-      "name": "Print init",
+      "name": "Init",
       "type": "go",
       "request": "launch",
       "mode": "debug",
@@ -100,6 +100,18 @@
         "migrate"
       ]
     },
+    {
+      "name": "Migrate glyphs",
+      "type": "go",
+      "request": "launch",
+      "mode": "debug",
+      "program": "${workspaceRoot}/src",
+      "args": [
+        "config",
+        "migrate",
+        "glyphs"
+      ]
+    },
     {
       "name": "Get value",
       "type": "go",
diff --git a/src/cli/config_migrate.go b/src/cli/config_migrate.go
index e0c6be78..ed71cc48 100644
--- a/src/cli/config_migrate.go
+++ b/src/cli/config_migrate.go
@@ -30,11 +30,12 @@ Migrates the ~/myconfig.omp.json config file and prints the result to stdout.
 
 > oh-my-posh config migrate --config ~/myconfig.omp.json --format toml
 
-Migrates the ~/myconfig.omp.json config file to toml and prints the result to stdout.
+Migrates the ~/myconfig.omp.json config file to TOML and prints the result to stdout.
 
 > oh-my-posh config migrate --config ~/myconfig.omp.json --format toml --write
 
-Migrates the ~/myconfig.omp.json config file to toml and writes the result to your config file.
+Migrates the ~/myconfig.omp.json config file to TOML and writes the result to your config file.
+
 A backup of the current config can be found at ~/myconfig.omp.json.bak.`,
 	Args: cobra.NoArgs,
 	Run: func(cmd *cobra.Command, args []string) {
diff --git a/src/cli/config_migrate_glyphs.go b/src/cli/config_migrate_glyphs.go
new file mode 100644
index 00000000..4ea37ec1
--- /dev/null
+++ b/src/cli/config_migrate_glyphs.go
@@ -0,0 +1,60 @@
+package cli
+
+import (
+	"fmt"
+
+	"github.com/jandedobbeleer/oh-my-posh/src/engine"
+	"github.com/jandedobbeleer/oh-my-posh/src/platform"
+
+	"github.com/spf13/cobra"
+)
+
+// migrateCmd represents the migrate command
+var migrateGlyphsCmd = &cobra.Command{
+	Use:   "glyphs",
+	Short: "Migrate the nerd font glyphs in your config",
+	Long: `Migrate the nerd font glyphs in your config.
+
+You can choose to print the output to stdout, or migrate your config in the format of your choice.
+
+Example usage
+
+> oh-my-posh config migrate glyphs --config ~/myconfig.omp.json
+
+Migrates the ~/myconfig.omp.json config file's glyphs and prints the result to stdout.
+
+> oh-my-posh config migrate glyphs --config ~/myconfig.omp.json --format toml
+
+Migrates the ~/myconfig.omp.json config file's glyphs and prints the result to stdout in a TOML format.
+
+> oh-my-posh config migrate glyphs --config ~/myconfig.omp.json --format toml --write
+
+Migrates the ~/myconfig.omp.json config file's glyphs and writes the result to your config file in a TOML format.
+
+A backup of the current config can be found at ~/myconfig.omp.json.bak.`,
+	Args: cobra.NoArgs,
+	Run: func(cmd *cobra.Command, args []string) {
+		env := &platform.Shell{
+			Version: cliVersion,
+			CmdFlags: &platform.Flags{
+				Config: config,
+			},
+		}
+		env.Init()
+		defer env.Close()
+		cfg := engine.LoadConfig(env)
+		cfg.MigrateGlyphs = true
+		if write {
+			cfg.Backup()
+			cfg.Write(format)
+			return
+		}
+		fmt.Print(cfg.Export(format))
+	},
+}
+
+func init() { //nolint:gochecknoinits
+	migrateGlyphsCmd.Flags().BoolVarP(&write, "write", "w", false, "write the migrated config back to the config file")
+	migrateGlyphsCmd.Flags().StringVarP(&format, "format", "f", "json", "the config format to migrate to")
+	migrateCmd.AddCommand(migrateGlyphsCmd)
+}
diff --git a/src/engine/config.go b/src/engine/config.go
index d53ea1f4..fb0cbc4f 100644
--- a/src/engine/config.go
+++ b/src/engine/config.go
@@ -53,7 +53,8 @@ type Config struct {
 	// Deprecated
 	OSC99 bool `json:"osc99,omitempty"`
 
-	Output string `json:"-"`
+	Output        string `json:"-"`
+	MigrateGlyphs bool   `json:"-"`
 
 	format string
 	origin string
@@ -177,7 +178,7 @@ func (cfg *Config) Export(format string) string {
 		_ = jsonEncoder.Encode(cfg)
 		prefix := "{\n  \"$schema\": \"https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json\","
 		data := strings.Replace(result.String(), "{", prefix, 1)
-		return escapeGlyphs(data)
+		return escapeGlyphs(data, cfg.MigrateGlyphs)
 	}
 
 	_, _ = config.DumpTo(&result, cfg.format)
@@ -187,14 +188,14 @@ func (cfg *Config) Export(format string) string {
 		return prefix + result.String()
 	case TOML:
 		prefix := "#:schema https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json\n\n"
-		return prefix + escapeGlyphs(result.String())
+		return prefix + escapeGlyphs(result.String(), cfg.MigrateGlyphs)
 	default:
 		return result.String()
 	}
 }
 
 func (cfg *Config) BackupAndMigrate(env platform.Environment) {
-	cfg.backup()
+	cfg.Backup()
 	cfg.Migrate(env)
 	cfg.Write(cfg.format)
 }
@@ -216,7 +217,7 @@ func (cfg *Config) Write(format string) {
 	_ = f.Close()
 }
 
-func (cfg *Config) backup() {
+func (cfg *Config) Backup() {
 	dst := cfg.origin + ".bak"
 	source, err := os.Open(cfg.origin)
 	if err != nil {
@@ -234,7 +235,7 @@ func (cfg *Config) backup() {
 	}
 }
 
-func escapeGlyphs(s string) string {
+func escapeGlyphs(s string, migrate bool) string {
 	shouldExclude := func(r rune) bool {
 		if r < 0x1000 { // Basic Multilingual Plane
 			return true
@@ -266,6 +267,11 @@ func escapeGlyphs(s string) string {
 		return false
 	}
 
+	var cp codePoints
+	if migrate {
+		cp = getGlyphCodePoints()
+	}
+
 	var builder strings.Builder
 	for _, r := range s {
 		// exclude regular characters and emojis
@@ -274,6 +280,12 @@ func escapeGlyphs(s string) string {
 			continue
 		}
 
+		if migrate {
+			if val, OK := cp[int(r)]; OK {
+				r = rune(val)
+			}
+		}
+
 		if r > 0x10000 {
 			// calculate surrogate pairs
 			one := 0xd800 + (((r - 0x10000) >> 10) & 0x3ff)
diff --git a/src/engine/migrate_glyphs.go b/src/engine/migrate_glyphs.go
new file mode 100644
index 00000000..3ab4dcb5
--- /dev/null
+++ b/src/engine/migrate_glyphs.go
@@ -0,0 +1,52 @@
+package engine
+
+import (
+	"context"
+	"encoding/csv"
+	"net/http"
+	"strconv"
+	"time"
+)
+
+type codePoints map[int]int
+
+func getGlyphCodePoints() codePoints {
+	var codePoints = make(codePoints)
+
+	client := &http.Client{}
+	ctx, cncl := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
+	defer cncl()
+
+	request, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://ohmyposh.dev/codepoints.csv", nil)
+	if err != nil {
+		return codePoints
+	}
+
+	response, err := client.Do(request)
+	if err != nil {
+		return codePoints
+	}
+
+	defer response.Body.Close()
+
+	lines, err := csv.NewReader(response.Body).ReadAll()
+	if err != nil {
+		return codePoints
+	}
+
+	for _, line := range lines {
+		if len(line) < 2 {
+			continue
+		}
+		oldGlyph, err := strconv.ParseUint(line[0], 16, 32)
+		if err != nil {
+			continue
+		}
+		newGlyph, err := strconv.ParseUint(line[1], 16, 32)
+		if err != nil {
+			continue
+		}
+		codePoints[int(oldGlyph)] = int(newGlyph)
+	}
+	return codePoints
+}
diff --git a/website/docs/faq.mdx b/website/docs/faq.mdx
index 2f6efdb8..b740502c 100644
--- a/website/docs/faq.mdx
+++ b/website/docs/faq.mdx
@@ -223,6 +223,20 @@ setopt appendhistory
 
 You should update fish to v3.4.0 or higher. Fish supports `$(...)` and `"$(...)"` since v3.4.0, as described in its [changelog][fish-changelog].
 
+### After updating my Nerd Font to a newer version, the prompt displays unknown characters
+
+Nerd Fonts moved the icons to a different location in the font for v3.
+This can cause the prompt to display unknown characters. There's a built-in migration in Oh My Posh to fix this.
+
+To migrate, run the following command:
+
+```bash
+oh-my-posh config migrate glyphs --write
+```
+
+This will update your configuration file to use the new glyph locations. Do know they might look different, as they also
+updated the icons themselves. A backup of the current config can be found in the same location with a `.bak` extension.
+
 [exclusion]: https://support.microsoft.com/en-us/windows/add-an-exclusion-to-windows-security-811816c0-4dfd-af4a-47e4-c301afe13b26
 [arch-terminfo]: https://wiki.archlinux.org/title/Bash/Prompt_customization#Terminfo_escape_sequences
 [ps-ansi-docs]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_ansi_terminals?view=powershell-7.2


Test patch:
diff --git a/src/engine/config_test.go b/src/engine/config_test.go
index fd69d02f..1e4f1da5 100644
--- a/src/engine/config_test.go
+++ b/src/engine/config_test.go
@@ -57,7 +57,7 @@ func TestEscapeGlyphs(t *testing.T) {
 		{Input: "🏚", Expected: "🏚"},
 	}
 	for _, tc := range cases {
-		assert.Equal(t, tc.Expected, escapeGlyphs(tc.Input), tc.Input)
+		assert.Equal(t, tc.Expected, escapeGlyphs(tc.Input, false), tc.Input)
 	}
 }
 
diff --git a/src/engine/migrate_glyphs_test.go b/src/engine/migrate_glyphs_test.go
new file mode 100644
index 00000000..ef6b6350
--- /dev/null
+++ b/src/engine/migrate_glyphs_test.go
@@ -0,0 +1,12 @@
+package engine
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetCodePoints(t *testing.T) {
+	codepoints := getGlyphCodePoints()
+	assert.Equal(t, 1939, len(codepoints))
+}