Repository: JanDeDobbeleer/oh-my-posh
Pull request #1297 (2021-11-24 12:47:51)

feat(windows): registry query segment
### Prerequisites

- [x] I have read and understood the `CONTRIBUTING` guide
- [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

closes #1260
<!---

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.

-->

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/docs/docs/segment-winreg.md b/docs/docs/segment-winreg.md
new file mode 100644
index 00000000..64d6630f
--- /dev/null
+++ b/docs/docs/segment-winreg.md
@@ -0,0 +1,46 @@
+---
+id: winreg
+title: Windows Registry Key Query
+sidebar_label: Windows Registry Key Query
+---
+
+## What
+
+Display the content of the requested Windows registry key.
+
+Supported registry key types:
+
+- String
+- DWORD (displayed in upper-case 0x hex)
+
+## Sample Configuration
+
+```json
+  {
+    "type": "winreg",
+    "style": "powerline",
+    "powerline_symbol": "\uE0B0",
+    "foreground": "#ffffff",
+    "background": "#444444",
+    "properties": {
+      "path": "HKLM\\software\\microsoft\\windows nt\\currentversion",
+      "key":"buildlab",
+      "template":"{{ if .Value }}{{ .Value }}{{ else }}unknown{{ end }}",
+      "prefix": " \uE62A "
+    }
+  },
+```
+
+## Properties
+
+- path: `string` - registry path to the desired key using backslashes and with a valid root HKEY name.
+- key: `string` - the key to read from the `path location.  If `""`, will read the default value.
+- template: `string` - a go [text/template][go-text-template] template extended
+  with [sprig][sprig] utilizing the properties below.
+
+## Template Properties
+
+- .Value: `string` - The result of your query
+
+[go-text-template]: https://golang.org/pkg/text/template/
+[sprig]: https://masterminds.github.io/sprig/
diff --git a/docs/sidebars.js b/docs/sidebars.js
index b7d808ad..a5b247fa 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -73,6 +73,7 @@ module.exports = {
         "text",
         "time",
         "wifi",
+        "winreg",
         "ytm",
       ],
     },
diff --git a/src/environment.go b/src/environment.go
index fd975461..81480cee 100644
--- a/src/environment.go
+++ b/src/environment.go
@@ -80,6 +80,7 @@ type environmentInfo interface {
 	getBatteryInfo() ([]*battery.Battery, error)
 	getShellName() string
 	getWindowTitle(imageName, windowTitleRegex string) (string, error)
+	getWindowsRegistryKeyValue(regPath, regKey string) (string, error)
 	doGet(url string, timeout int) ([]byte, error)
 	hasParentFilePath(path string) (fileInfo *fileInfo, err error)
 	isWsl() bool
diff --git a/src/environment_unix.go b/src/environment_unix.go
index 9f82c05f..507d3825 100644
--- a/src/environment_unix.go
+++ b/src/environment_unix.go
@@ -59,3 +59,7 @@ func (env *environment) getCachePath() string {
 	}
 	return env.homeDir()
 }
+
+func (env *environment) getWindowsRegistryKeyValue(regPath, regKey string) (string, error) {
+	return "", errors.New("not implemented")
+}
diff --git a/src/environment_windows.go b/src/environment_windows.go
index a22e7766..47c5445c 100644
--- a/src/environment_windows.go
+++ b/src/environment_windows.go
@@ -3,9 +3,13 @@
 package main
 
 import (
+	"errors"
+	"fmt"
 	"os"
+	"strings"
 	"syscall"
 	"time"
+	"unsafe"
 
 	"github.com/Azure/go-ansiterm/winterm"
 	"golang.org/x/sys/windows"
@@ -99,3 +103,98 @@ func (env *environment) getCachePath() string {
 	}
 	return env.homeDir()
 }
+
+//
+// Takes a registry path like "HKLM\Software\Microsoft\Windows NT\CurrentVersion" and a key under that path like "CurrentVersion" (or "" if the (Default) key is required).
+// Returns a bool and string:
+//
+//   true and the retrieved value formatted into a string if successful.
+//   false and the string will be the error
+//
+func (env *environment) getWindowsRegistryKeyValue(regPath, regKey string) (string, error) {
+	env.trace(time.Now(), "getWindowsRegistryKeyValue", regPath, regKey)
+
+	// Extract root HK value and turn it into a windows.Handle to open the key.
+	regPathParts := strings.SplitN(regPath, "\\", 2)
+
+	regRootHKeyHandle := getHKEYHandleFromAbbrString(regPathParts[0])
+	if regRootHKeyHandle == 0 {
+		errorLogMsg := fmt.Sprintf("Error, Supplied root HKEY value not valid: '%s'", regPathParts[0])
+		env.log(Error, "getWindowsRegistryKeyValue", errorLogMsg)
+		return "", errors.New(errorLogMsg)
+	}
+
+	// Second part of split is registry path after HK part - needs to be UTF16 to pass to the windows. API
+	regPathUTF16, regPathUTF16ConversionErr := windows.UTF16FromString(regPathParts[1])
+	if regPathUTF16ConversionErr != nil {
+		errorLogMsg := fmt.Sprintf("Error, Could not convert supplied path '%s' to UTF16, error: '%s'", regPathParts[1], regPathUTF16ConversionErr)
+		env.log(Error, "getWindowsRegistryKeyValue", errorLogMsg)
+		return "", errors.New(errorLogMsg)
+	}
+
+	// Ok - open it..
+	var hKeyHandle windows.Handle
+	regOpenErr := windows.RegOpenKeyEx(regRootHKeyHandle, &regPathUTF16[0], 0, windows.KEY_READ, &hKeyHandle)
+	if regOpenErr != nil {
+		errorLogMsg := fmt.Sprintf("Error RegOpenKeyEx opening registry path to '%s', error: '%s'", regPath, regOpenErr)
+		env.log(Error, "getWindowsRegistryKeyValue", errorLogMsg)
+		return "", errors.New(errorLogMsg)
+	}
+	// Success - from here on out, when returning make sure to close that reg key with a deferred call to close:
+	defer func() {
+		err := windows.RegCloseKey(hKeyHandle)
+		if err != nil {
+			env.log(Error, "getWindowsRegistryKeyValue", fmt.Sprintf("Error closing registry key: %s", err))
+		}
+	}()
+
+	// Again - need UTF16 of the key for the API:
+	regKeyUTF16, regKeyUTF16ConversionErr := windows.UTF16FromString(regKey)
+	if regKeyUTF16ConversionErr != nil {
+		errorLogMsg := fmt.Sprintf("Error, could not convert supplied key '%s' to UTF16, error: '%s'", regKey, regKeyUTF16ConversionErr)
+		env.log(Error, "getWindowsRegistryKeyValue", errorLogMsg)
+		return "", errors.New(errorLogMsg)
+	}
+
+	// Two stage way to get the key value - query once to get size - then allocate and query again to fill it.
+	var keyBufType uint32
+	var keyBufSize uint32
+
+	regQueryErr := windows.RegQueryValueEx(hKeyHandle, &regKeyUTF16[0], nil, &keyBufType, nil, &keyBufSize)
+	if regQueryErr != nil {
+		errorLogMsg := fmt.Sprintf("Error calling RegQueryValueEx to retrieve key data size with error '%s'", regQueryErr)
+		env.log(Error, "getWindowsRegistryKeyValue", errorLogMsg)
+		return "", errors.New(errorLogMsg)
+	}
+
+	// Alloc and fill...
+	var keyBuf = make([]byte, keyBufSize)
+
+	regQueryErr = windows.RegQueryValueEx(hKeyHandle, &regKeyUTF16[0], nil, &keyBufType, &keyBuf[0], &keyBufSize)
+	if regQueryErr != nil {
+		errorLogMsg := fmt.Sprintf("Error calling RegQueryValueEx to retrieve key data with error '%s'", regQueryErr)
+		env.log(Error, "getWindowsRegistryKeyValue", errorLogMsg)
+		return "", errors.New(errorLogMsg)
+	}
+
+	// Format result into a string, depending on type.  (future refactor - move this out into it's own function)
+	switch keyBufType {
+	case windows.REG_SZ:
+		var uint16p *uint16
+		uint16p = (*uint16)(unsafe.Pointer(&keyBuf[0])) // nasty casty
+
+		valueString := windows.UTF16PtrToString(uint16p)
+		env.log(Debug, "getWindowsRegistryKeyValue", fmt.Sprintf("success, string: %s", valueString))
+		return valueString, nil
+	case windows.REG_DWORD:
+		var uint32p *uint32
+		uint32p = (*uint32)(unsafe.Pointer(&keyBuf[0])) // more casting goodness
+
+		valueString := fmt.Sprintf("0x%08X", *uint32p)
+		env.log(Debug, "getWindowsRegistryKeyValue", fmt.Sprintf("success, DWORD, formatted as string: %s", valueString))
+		return valueString, nil
+	default:
+		errorLogMsg := fmt.Sprintf("Error, no formatter for REG_? type:%d, data size:%d bytes", keyBufType, keyBufSize)
+		return "", errors.New(errorLogMsg)
+	}
+}
diff --git a/src/environment_windows_win32.go b/src/environment_windows_win32.go
index 714e147b..09dc92c8 100644
--- a/src/environment_windows_win32.go
+++ b/src/environment_windows_win32.go
@@ -179,3 +179,22 @@ func GetWindowTitle(pid int, windowTitleRegex string) (syscall.Handle, string) {
 	_ = EnumWindows(cb, 0)
 	return hwnd, title
 }
+
+// Return the windows handles corresponding to the names of the root registry keys.
+// A returned value of 0 means there was no match.
+func getHKEYHandleFromAbbrString(abbr string) windows.Handle {
+	switch abbr {
+	case "HKCR", "HKEY_CLASSES_ROOT":
+		return windows.HKEY_CLASSES_ROOT
+	case "HKCC", "HKEY_CURRENT_CONFIG":
+		return windows.HKEY_CURRENT_CONFIG
+	case "HKCU", "HKEY_CURRENT_USER":
+		return windows.HKEY_CURRENT_USER
+	case "HKLM", "HKEY_LOCAL_MACHINE":
+		return windows.HKEY_LOCAL_MACHINE
+	case "HKU", "HKEY_USERS":
+		return windows.HKEY_USERS
+	}
+
+	return 0
+}
diff --git a/src/segment.go b/src/segment.go
index 3c2bfe09..667d26ca 100644
--- a/src/segment.go
+++ b/src/segment.go
@@ -134,6 +134,8 @@ const (
 	Nightscout SegmentType = "nightscout"
 	// WiFi writes details about the current WiFi connection
 	WiFi SegmentType = "wifi"
+	// WinReg queries the Windows registry.
+	WinReg SegmentType = "winreg"
 )
 
 func (segment *Segment) string() string {
@@ -269,6 +271,7 @@ func (segment *Segment) mapSegmentWithWriter(env environmentInfo) error {
 		PHP:           &php{},
 		Nightscout:    &nightscout{},
 		WiFi:          &wifi{},
+		WinReg:        &winreg{},
 	}
 	if writer, ok := functions[segment.Type]; ok {
 		props := &properties{
diff --git a/src/segment_winreg.go b/src/segment_winreg.go
new file mode 100644
index 00000000..adafcdea
--- /dev/null
+++ b/src/segment_winreg.go
@@ -0,0 +1,51 @@
+package main
+
+type winreg struct {
+	props *properties
+	env   environmentInfo
+
+	Value string
+}
+
+const (
+	// path from the supplied root under which the key exists
+	RegistryPath Property = "path"
+	// key within full reg path formed from two above
+	RegistryKey Property = "key"
+)
+
+func (wr *winreg) init(props *properties, env environmentInfo) {
+	wr.props = props
+	wr.env = env
+}
+
+func (wr *winreg) enabled() bool {
+	if wr.env.getRuntimeGOOS() != windowsPlatform {
+		return false
+	}
+
+	registryPath := wr.props.getString(RegistryPath, "")
+	registryKey := wr.props.getString(RegistryKey, "")
+
+	var err error
+	wr.Value, err = wr.env.getWindowsRegistryKeyValue(registryPath, registryKey)
+	return err == nil
+}
+
+func (wr *winreg) string() string {
+	segmentTemplate := wr.props.getString(SegmentTemplate, "{{ .Value }}")
+	return wr.templateString(segmentTemplate)
+}
+
+func (wr *winreg) templateString(segmentTemplate string) string {
+	template := &textTemplate{
+		Template: segmentTemplate,
+		Context:  wr,
+		Env:      wr.env,
+	}
+	text, err := template.render()
+	if err != nil {
+		return err.Error()
+	}
+	return text
+}
diff --git a/themes/schema.json b/themes/schema.json
index fdb02b58..948afaac 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -181,7 +181,8 @@
             "sysinfo",
             "angular",
             "php",
-            "wifi"
+            "wifi",
+            "regquery"
           ]
         },
         "style": {
@@ -1718,6 +1719,38 @@
               }
             }
           }
+        },
+        {
+          "if": {
+            "properties": {
+              "type": { "const": "winreg" }
+            }
+          },
+          "then": {
+            "title": "Windows Registry Query",
+            "description": "https://ohmyposh.dev/docs/winreg",
+            "properties": {
+              "properties": {
+                "properties": {
+                  "template": {
+                    "$ref": "#/definitions/template"
+                  },
+                  "path": {
+                    "type": "string",
+                    "title": "Registry Path",
+                    "description": "The path under which the registy key lives (case insensitive, must use backslashes), e.g. HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion",
+                    "default": ""
+                  },
+                  "key": {
+                    "type": "string",
+                    "title": "Registry Key",
+                    "description": "The key under he registry path to get (case insensitive).  If left blank, will get the value of the (Default) key in the registry_path",
+                    "default": ""
+                  }
+                }
+              }
+            }
+          }
         }
       ]
     }


Test patch:
diff --git a/src/segment_path_test.go b/src/segment_path_test.go
index a76a43f3..91035dbc 100644
--- a/src/segment_path_test.go
+++ b/src/segment_path_test.go
@@ -129,6 +129,11 @@ func (env *MockedEnvironment) getWindowTitle(imageName, windowTitleRegex string)
 	return args.String(0), args.Error(1)
 }
 
+func (env *MockedEnvironment) getWindowsRegistryKeyValue(regPath, regKey string) (string, error) {
+	args := env.Called(regPath, regKey)
+	return args.String(0), args.Error(1)
+}
+
 func (env *MockedEnvironment) doGet(url string, timeout int) ([]byte, error) {
 	args := env.Called(url)
 	return args.Get(0).([]byte), args.Error(1)
diff --git a/src/segment_winreg_test.go b/src/segment_winreg_test.go
new file mode 100644
index 00000000..85219d99
--- /dev/null
+++ b/src/segment_winreg_test.go
@@ -0,0 +1,52 @@
+package main
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRegQueryEnabled(t *testing.T) {
+	cases := []struct {
+		CaseDescription string
+		Path            string
+		Key             string
+		ExpectedSuccess bool
+		Output          string
+		Err             error
+	}{
+		{
+			CaseDescription: "Error",
+			Path:            "HKLLM\\Software\\Microsoft\\Windows NT\\CurrentVersion",
+			Key:             "ProductName",
+			ExpectedSuccess: false,
+			Err:             errors.New("No match"),
+		},
+		{
+			CaseDescription: "Value",
+			Path:            "HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion",
+			Key:             "InstallTime",
+			ExpectedSuccess: true,
+			Output:          "no formatter",
+		},
+	}
+
+	for _, tc := range cases {
+		env := new(MockedEnvironment)
+		env.On("getRuntimeGOOS", nil).Return(windowsPlatform)
+		env.On("getWindowsRegistryKeyValue", tc.Path, tc.Key).Return(tc.Output, tc.Err)
+		props := &properties{
+			values: map[Property]interface{}{
+				RegistryPath: tc.Path,
+				RegistryKey:  tc.Key,
+			},
+		}
+		r := &winreg{
+			env:   env,
+			props: props,
+		}
+
+		assert.Equal(t, r.enabled(), tc.ExpectedSuccess, tc.CaseDescription)
+	}
+}