Repository: JanDeDobbeleer/oh-my-posh
Pull request #1296 (2021-11-24 10:16:49)

wifi 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

- feat(wifi): added wifi segment for windows and wsl
- docs: added wifi docs and schema

closes #1270

<!---

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-wifi.md b/docs/docs/segment-wifi.md
new file mode 100644
index 00000000..5e986f5e
--- /dev/null
+++ b/docs/docs/segment-wifi.md
@@ -0,0 +1,52 @@
+---
+id: wifi
+title: WiFi
+sidebar_label: WiFi
+---
+
+## What
+
+Show details about the currently connected WiFi network.
+
+::info
+Currently only supports Windows and WSL. Pull requests for Darwin and Linux support are welcome :)
+:::
+
+## Sample Configuration
+
+```json
+{
+  "type": "wifi",
+  "style": "powerline",
+  "background": "#8822ee",
+  "foreground": "#222222",
+  "background_templates": [
+    "{{ if (not .Connected) }}#FF1111{{ end }}"
+    "{{ if (lt .Signal 60) }}#DDDD11{{ else if (lt .Signal 90) }}#DD6611{{ else }}#11CC11{{ end }}"
+  ],
+  "powerline_symbol": "\uE0B0",
+  "properties": {
+    "template": "{{ if .Connected }}\uFAA8{{ else }}\uFAA9{{ end }}
+    {{ if .Connected }}{{ .SSID }} {{ .Signal }}% {{ .ReceiveRate }}Mbps{{ else }}{{ .State }}{{ end }}"
+  }
+}
+```
+
+## Properties
+
+- template: `string` - A go [text/template][go-text-template]  extended with [sprig][sprig] using the properties below
+
+## Template Properties
+
+- `.Connected`: `bool` - if WiFi is currently connected
+- `.State`: `string` - WiFi connection status - _e.g. connected or disconnected_
+- `.SSID`: `string` - the SSID of the current wifi network
+- `.RadioType`: `string` - the radio type - _e.g. 802.11ac, 802.11ax, 802.11n, etc._
+- `.Authentication`: `string` - the authentication type - _e.g. WPA2-Personal, WPA2-Enterprise, etc._
+- `.Channel`: `int` - the current channel number
+- `.ReceiveRate`: `int` - the receive rate (Mbps)
+- `.TransmitRate`: `int` - the transmit rate (Mbps)
+- `.Signal`: `int` - the signal strength (%)
+
+[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 17f82305..b7d808ad 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -72,6 +72,7 @@ module.exports = {
         "terraform",
         "text",
         "time",
+        "wifi",
         "ytm",
       ],
     },
diff --git a/src/segment.go b/src/segment.go
index c75196c9..3c2bfe09 100644
--- a/src/segment.go
+++ b/src/segment.go
@@ -132,6 +132,8 @@ const (
 	PHP SegmentType = "php"
 	// Nightscout is an open source diabetes system
 	Nightscout SegmentType = "nightscout"
+	// WiFi writes details about the current WiFi connection
+	WiFi SegmentType = "wifi"
 )
 
 func (segment *Segment) string() string {
@@ -266,6 +268,7 @@ func (segment *Segment) mapSegmentWithWriter(env environmentInfo) error {
 		Angular:       &angular{},
 		PHP:           &php{},
 		Nightscout:    &nightscout{},
+		WiFi:          &wifi{},
 	}
 	if writer, ok := functions[segment.Type]; ok {
 		props := &properties{
diff --git a/src/segment_wifi.go b/src/segment_wifi.go
new file mode 100644
index 00000000..10e10f49
--- /dev/null
+++ b/src/segment_wifi.go
@@ -0,0 +1,114 @@
+package main
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+type wifi struct {
+	props          *properties
+	env            environmentInfo
+	Connected      bool
+	State          string
+	SSID           string
+	RadioType      string
+	Authentication string
+	Channel        int
+	ReceiveRate    int
+	TransmitRate   int
+	Signal         int
+}
+
+const (
+	defaultTemplate = "{{ if .Connected }}\uFAA8{{ else }}\uFAA9{{ end }}{{ if .Connected }}{{ .SSID }} {{ .Signal }}% {{ .ReceiveRate }}Mbps{{ else }}{{ .State }}{{ end }}"
+)
+
+func (w *wifi) enabled() bool {
+	// This segment only supports Windows/WSL for now
+	if w.env.getPlatform() != windowsPlatform && !w.env.isWsl() {
+		return false
+	}
+
+	// Bail out of no netsh command found
+	cmd := "netsh.exe"
+	if !w.env.hasCommand(cmd) {
+		return false
+	}
+
+	// Attempt to retrieve output from netsh command
+	cmdResult, err := w.env.runCommand(cmd, "wlan", "show", "interfaces")
+	displayError := w.props.getBool(DisplayError, false)
+	if err != nil && displayError {
+		w.State = fmt.Sprintf("WIFI ERR: %s", err)
+		return true
+	}
+	if err != nil {
+		return false
+	}
+
+	// Extract data from netsh cmdResult
+	parseNetshCmdResult(cmdResult, w)
+
+	return true
+}
+
+func (w *wifi) string() string {
+	segmentTemplate := w.props.getString(SegmentTemplate, defaultTemplate)
+	template := &textTemplate{
+		Template: segmentTemplate,
+		Context:  w,
+		Env:      w.env,
+	}
+	text, err := template.render()
+	if err != nil {
+		return err.Error()
+	}
+
+	return text
+}
+
+func (w *wifi) init(props *properties, env environmentInfo) {
+	w.props = props
+	w.env = env
+}
+
+func parseNetshCmdResult(netshCmdResult string, w *wifi) {
+	lines := strings.Split(netshCmdResult, "\n")
+	for _, line := range lines {
+		matches := strings.Split(line, " : ")
+		if len(matches) != 2 {
+			continue
+		}
+		name := strings.TrimSpace(matches[0])
+		value := strings.TrimSpace(matches[1])
+
+		switch name {
+		case "State":
+			w.State = value
+			w.Connected = value == "connected"
+		case "SSID":
+			w.SSID = value
+		case "Radio type":
+			w.RadioType = value
+		case "Authentication":
+			w.Authentication = value
+		case "Channel":
+			if intValue, err := strconv.Atoi(value); err == nil {
+				w.Channel = intValue
+			}
+		case "Receive rate (Mbps)":
+			if intValue, err := strconv.Atoi(strings.Split(value, ".")[0]); err == nil {
+				w.ReceiveRate = intValue
+			}
+		case "Transmit rate (Mbps)":
+			if intValue, err := strconv.Atoi(strings.Split(value, ".")[0]); err == nil {
+				w.TransmitRate = intValue
+			}
+		case "Signal":
+			if intValue, err := strconv.Atoi(strings.TrimRight(value, "%")); err == nil {
+				w.Signal = intValue
+			}
+		}
+	}
+}
diff --git a/themes/schema.json b/themes/schema.json
index 9d3e397f..fdb02b58 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -180,7 +180,8 @@
             "owm",
             "sysinfo",
             "angular",
-            "php"
+            "php",
+            "wifi"
           ]
         },
         "style": {
@@ -1697,6 +1698,26 @@
               }
             }
           }
+        },
+        {
+          "if": {
+            "properties": {
+              "type": { "const": "wifi" }
+            }
+          },
+          "then": {
+            "title": "WiFi Segment",
+            "description": "https://ohmyposh.dev/docs/wifi",
+            "properties": {
+              "properties": {
+                "properties": {
+                  "template": {
+                    "$ref": "#/definitions/template"
+                  }
+                }
+              }
+            }
+          }
         }
       ]
     }


Test patch:
diff --git a/src/segment_wifi_test.go b/src/segment_wifi_test.go
new file mode 100644
index 00000000..5f2a1ab6
--- /dev/null
+++ b/src/segment_wifi_test.go
@@ -0,0 +1,138 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+type netshStringArgs struct {
+	state          string
+	ssid           string
+	radioType      string
+	authentication string
+	channel        int
+	receiveRate    int
+	transmitRate   int
+	signal         int
+}
+
+func getNetshString(args *netshStringArgs) string {
+	const netshString string = `
+	There is 1 interface on the system:
+
+	Name                   : Wi-Fi
+	Description            : Intel(R) Wireless-AC 9560 160MHz
+	GUID                   : 6bb8def2-9af2-4bd4-8be2-6bd54e46bdc9
+	Physical address       : d4:3b:04:e6:10:40
+	State                  : %s
+	SSID                   : %s
+	BSSID                  : 5c:7d:7d:82:c5:73
+	Network type           : Infrastructure
+	Radio type             : %s
+	Authentication         : %s
+	Cipher                 : CCMP
+	Connection mode        : Profile
+	Channel                : %d
+	Receive rate (Mbps)    : %d
+	Transmit rate (Mbps)   : %d
+	Signal                 : %d%%
+	Profile                : ohsiggy
+
+	Hosted network status  : Not available`
+
+	return fmt.Sprintf(netshString, args.state, args.ssid, args.radioType, args.authentication, args.channel, args.receiveRate, args.transmitRate, args.signal)
+}
+
+func TestWiFiSegment(t *testing.T) {
+	cases := []struct {
+		Case            string
+		ExpectedString  string
+		ExpectedEnabled bool
+		CommandNotFound bool
+		CommandOutput   string
+		CommandError    error
+		DisplayError    bool
+		Template        string
+		ExpectedState   string
+	}{
+		{
+			Case:            "not enabled on windows when netsh command not found",
+			ExpectedEnabled: false,
+			ExpectedString:  "",
+			CommandNotFound: true,
+		},
+		{
+			Case:            "not enabled on windows when netsh command fails",
+			ExpectedEnabled: false,
+			ExpectedString:  "",
+			CommandError:    errors.New("intentional testing failure"),
+		},
+		{
+			Case:            "enabled on windows with DisplayError=true",
+			ExpectedEnabled: true,
+			ExpectedString:  "WIFI ERR: intentional testing failure",
+			CommandError:    errors.New("intentional testing failure"),
+			DisplayError:    true,
+			Template:        "{{.State}}",
+		},
+		{
+			Case:            "enabled on windows with every property in template",
+			ExpectedEnabled: true,
+			ExpectedString:  "connected testing 802.11ac WPA2-Personal 99 500 400 80",
+			CommandOutput: getNetshString(&netshStringArgs{
+				state:          "connected",
+				ssid:           "testing",
+				radioType:      "802.11ac",
+				authentication: "WPA2-Personal",
+				channel:        99,
+				receiveRate:    500.0,
+				transmitRate:   400.0,
+				signal:         80,
+			}),
+			Template: "{{.State}} {{.SSID}} {{.RadioType}} {{.Authentication}} {{.Channel}} {{.ReceiveRate}} {{.TransmitRate}} {{.Signal}}",
+		},
+		{
+			Case:            "enabled on windows but wifi not connected",
+			ExpectedEnabled: true,
+			ExpectedString:  "disconnected",
+			CommandOutput: getNetshString(&netshStringArgs{
+				state: "disconnected",
+			}),
+			Template: "{{if not .Connected}}{{.State}}{{end}}",
+		},
+		{
+			Case:            "enabled on windows but template is invalid",
+			ExpectedEnabled: true,
+			ExpectedString:  "unable to create text based on template",
+			CommandOutput:   getNetshString(&netshStringArgs{}),
+			Template:        "{{.DoesNotExist}}",
+		},
+	}
+
+	for _, tc := range cases {
+		env := new(MockedEnvironment)
+		env.On("getPlatform", nil).Return(windowsPlatform)
+		env.On("isWsl", nil).Return(false)
+		env.On("hasCommand", "netsh.exe").Return(!tc.CommandNotFound)
+		env.On("runCommand", mock.Anything, mock.Anything).Return(tc.CommandOutput, tc.CommandError)
+
+		props := &properties{
+			values: map[Property]interface{}{
+				DisplayError:    tc.DisplayError,
+				SegmentTemplate: tc.Template,
+			},
+		}
+
+		w := &wifi{
+			env:   env,
+			props: props,
+		}
+
+		assert.Equal(t, tc.ExpectedEnabled, w.enabled(), tc.Case)
+		assert.Equal(t, tc.ExpectedString, w.string(), tc.Case)
+	}
+}