Skip to content

Commit 2a29fef

Browse files
committed
feat: add a wsl package
1 parent 56e4f4d commit 2a29fef

4 files changed

Lines changed: 133 additions & 49 deletions

File tree

internal/betterdiscord/install.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"sync"
77

88
"github.com/betterdiscord/cli/internal/models"
9-
"github.com/betterdiscord/cli/internal/utils"
9+
"github.com/betterdiscord/cli/internal/wsl"
1010
)
1111

1212
type BDInstall struct {
@@ -82,8 +82,8 @@ func GetInstallation(base ...string) *BDInstall {
8282
configDir, _ := os.UserConfigDir()
8383

8484
// Handle WSL with Windows home directory
85-
if utils.IsWSL() {
86-
winHome, err := utils.WindowsHome()
85+
if wsl.IsWSL() {
86+
winHome, err := wsl.WindowsHome()
8787
if err == nil && winHome != "" {
8888
configDir = filepath.Join(winHome, "AppData", "Roaming")
8989
}

internal/discord/paths_linux.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"strings"
77

88
"github.com/betterdiscord/cli/internal/models"
9-
"github.com/betterdiscord/cli/internal/utils"
9+
"github.com/betterdiscord/cli/internal/wsl"
1010
)
1111

1212
func init() {
@@ -31,8 +31,8 @@ func init() {
3131
filepath.Join(home, "snap", "{channel-}", "current", ".config", "{channel}"),
3232
}
3333

34-
if utils.IsWSL() {
35-
winHome, err := utils.WindowsHome()
34+
if wsl.IsWSL() {
35+
winHome, err := wsl.WindowsHome()
3636
if err == nil && winHome != "" {
3737
// WSL. Data is stored under the Windows user's AppData folder.
3838
// Example: `/mnt/c/Users/Username/AppData/Local/DiscordCanary`.
@@ -60,7 +60,7 @@ func init() {
6060
// For WSL environments, it uses Windows-style validation.
6161
// For native Linux, it detects Flatpak and Snap installations.
6262
func Validate(proposed string) *DiscordInstall {
63-
if utils.IsWSL() {
63+
if wsl.IsWSL() {
6464
return validateWindowsStyleInstall(proposed)
6565
}
6666

internal/utils/wsl.go

Lines changed: 0 additions & 42 deletions
This file was deleted.

internal/wsl/wsl.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package wsl
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"strings"
8+
"sync"
9+
)
10+
11+
var once sync.Once
12+
var info *WSLInfo
13+
14+
type WSLInfo struct {
15+
IsWSL bool // True if running under WSL1 or WSL2
16+
DistroName string // Value of WSL_DISTRO_NAME
17+
KernelVersion string // Contents of /proc/version
18+
InteropPath string // Value of WSL_INTEROP
19+
WindowsHome string // Windows home directory as a WSL path
20+
}
21+
22+
// loadWSLInfo detects WSL environment details and caches them in the info variable.
23+
// This function is safe to call multiple times but will only execute once.
24+
// It checks for WSL using both environment variables and kernel signatures, and attempts
25+
// to determine the Windows home directory if running under WSL.
26+
//
27+
// This is called lazily by public helper functions to avoid unnecessary overhead
28+
// of init() in non-WSL environments.
29+
func loadWSLInfo() {
30+
once.Do(func() {
31+
i := &WSLInfo{}
32+
33+
// Detect WSL via environment variable
34+
if dn := os.Getenv("WSL_DISTRO_NAME"); dn != "" {
35+
i.IsWSL = true
36+
i.DistroName = dn
37+
}
38+
39+
// Kernel signature fallback
40+
if data, err := os.ReadFile("/proc/version"); err == nil {
41+
ver := strings.ToLower(string(data))
42+
i.KernelVersion = ver
43+
44+
// Check for "microsoft" in kernel version to detect WSL if env var is missing
45+
if strings.Contains(ver, "microsoft") {
46+
i.IsWSL = true
47+
}
48+
}
49+
50+
// Interop path (could be useful someday)
51+
i.InteropPath = os.Getenv("WSL_INTEROP")
52+
53+
// Windows home directory (only if WSL)
54+
if i.IsWSL {
55+
home, err := getWindowsHomePath()
56+
if err == nil {
57+
i.WindowsHome = home
58+
}
59+
}
60+
61+
info = i
62+
})
63+
}
64+
65+
// Info returns cached WSL environment information.
66+
func Info() *WSLInfo {
67+
loadWSLInfo()
68+
return info
69+
}
70+
71+
// IsWSL returns true if running under WSL.
72+
func IsWSL() bool {
73+
loadWSLInfo()
74+
return info.IsWSL
75+
}
76+
77+
// WindowsHome returns the Windows user's home directory as a WSL path.
78+
func WindowsHome() (string, error) {
79+
loadWSLInfo()
80+
if info.WindowsHome == "" {
81+
return "", fmt.Errorf("unable to determine Windows home directory")
82+
}
83+
return info.WindowsHome, nil
84+
}
85+
86+
// ToWSLPath converts a Windows path (C:\Users\Me) → /mnt/c/Users/Me.
87+
func ToWSLPath(winPath string) (string, error) {
88+
out, err := exec.Command("wslpath", "-u", winPath).Output()
89+
if err != nil {
90+
return "", fmt.Errorf("wslpath failed: %w", err)
91+
}
92+
return strings.TrimSpace(string(out)), nil
93+
}
94+
95+
// ToWindowsPath converts a WSL path (/mnt/c/Users/Me) → C:\Users\Me.
96+
func ToWindowsPath(wslPath string) (string, error) {
97+
out, err := exec.Command("wslpath", "-w", wslPath).Output()
98+
if err != nil {
99+
return "", fmt.Errorf("wslpath failed: %w", err)
100+
}
101+
return strings.TrimSpace(string(out)), nil
102+
}
103+
104+
// ExecWindows runs a Windows command and returns stdout.
105+
func ExecWindows(command string) (string, error) {
106+
// Use cmd.exe to run the command
107+
cmd := exec.Command("cmd.exe", "/c", command)
108+
out, err := cmd.Output()
109+
if err != nil {
110+
return "", fmt.Errorf("cmd.exe failed: %w", err)
111+
}
112+
113+
// Clean CRLF from Windows output
114+
return strings.TrimSpace(strings.ReplaceAll(string(out), "\r", "")), nil
115+
}
116+
117+
func getWindowsHomePath() (string, error) {
118+
// Use ExecWindows helper
119+
winPath, err := ExecWindows("echo %USERPROFILE%")
120+
if err != nil {
121+
return "", err
122+
}
123+
124+
// Convert to WSL path
125+
return ToWSLPath(winPath)
126+
}

0 commit comments

Comments
 (0)