netfetch/internal/collector/de_wm.go

619 lines
14 KiB
Go

package collector
import (
"bufio"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
func (c *Collector) collectDE() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.info.DE = detectDE()
}
func (c *Collector) collectWM() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.info.WM = detectWM()
c.info.WMTheme = detectWMTheme(c.info.WM)
}
func detectDE() string {
if runtime.GOOS == "darwin" {
return "Aqua"
}
if runtime.GOOS == "windows" {
return "Windows Explorer"
}
if de := os.Getenv("XDG_CURRENT_DESKTOP"); de != "" {
parts := strings.Split(de, ":")
de = normalizeDE(parts[len(parts)-1])
if version := getDEVersion(de); version != "" {
return de + " " + version
}
return de
}
if de := os.Getenv("XDG_SESSION_DESKTOP"); de != "" {
de = normalizeDE(de)
if version := getDEVersion(de); version != "" {
return de + " " + version
}
return de
}
if de := os.Getenv("DESKTOP_SESSION"); de != "" {
de = normalizeDE(de)
if version := getDEVersion(de); version != "" {
return de + " " + version
}
return de
}
if os.Getenv("GNOME_DESKTOP_SESSION_ID") != "" {
version := getDEVersion("GNOME")
if version != "" {
return "GNOME " + version
}
return "GNOME"
}
if os.Getenv("MATE_DESKTOP_SESSION_ID") != "" {
return "MATE"
}
if os.Getenv("TDE_FULL_SESSION") != "" {
return "Trinity"
}
return detectDEFromProcesses()
}
func normalizeDE(de string) string {
deLower := strings.ToLower(de)
deMap := map[string]string{
"gnome": "GNOME",
"ubuntu:gnome": "GNOME",
"gnome-xorg": "GNOME",
"gnome-wayland": "GNOME",
"kde": "KDE Plasma",
"kde-plasma": "KDE Plasma",
"plasma": "KDE Plasma",
"plasmashell": "KDE Plasma",
"xfce": "Xfce",
"xfce4": "Xfce",
"cinnamon": "Cinnamon",
"x-cinnamon": "Cinnamon",
"mate": "MATE",
"lxde": "LXDE",
"lxqt": "LXQt",
"budgie": "Budgie",
"budgie-desktop": "Budgie",
"deepin": "Deepin",
"pantheon": "Pantheon",
"enlightenment": "Enlightenment",
"unity": "Unity",
"trinity": "Trinity",
"lumina": "Lumina",
"cosmic": "COSMIC",
}
if normalized, ok := deMap[deLower]; ok {
return normalized
}
return de
}
func getDEVersion(de string) string {
switch strings.ToLower(de) {
case "gnome":
if version := os.Getenv("GNOME_DESKTOP_SESSION_ID"); version != "" && version != "this-is-deprecated" {
return version
}
out, err := exec.Command("gnome-shell", "--version").Output()
if err == nil {
parts := strings.Fields(string(out))
if len(parts) >= 3 {
return parts[2]
}
}
case "kde plasma", "kde", "plasma":
if version := os.Getenv("KDE_SESSION_VERSION"); version != "" {
return version
}
out, err := exec.Command("plasmashell", "--version").Output()
if err == nil {
lines := strings.Split(string(out), "\n")
for _, line := range lines {
if strings.Contains(line, "plasmashell") {
parts := strings.Fields(line)
if len(parts) >= 2 {
return parts[len(parts)-1]
}
}
}
}
case "xfce", "xfce4":
out, err := exec.Command("xfce4-session", "--version").Output()
if err == nil {
lines := strings.Split(string(out), "\n")
if len(lines) > 0 {
parts := strings.Fields(lines[0])
if len(parts) >= 2 {
return parts[len(parts)-1]
}
}
}
case "cinnamon":
if version := os.Getenv("CINNAMON_VERSION"); version != "" {
return version
}
out, err := exec.Command("cinnamon", "--version").Output()
if err == nil {
parts := strings.Fields(string(out))
if len(parts) >= 2 {
return parts[len(parts)-1]
}
}
case "mate":
out, err := exec.Command("mate-session", "--version").Output()
if err == nil {
parts := strings.Fields(string(out))
if len(parts) >= 2 {
return parts[len(parts)-1]
}
}
case "budgie":
out, err := exec.Command("budgie-desktop", "--version").Output()
if err == nil {
parts := strings.Fields(string(out))
for i, part := range parts {
if part == "Budgie" && i+2 < len(parts) {
return parts[i+2]
}
}
}
case "lxqt":
out, err := exec.Command("lxqt-session", "-v").Output()
if err == nil {
parts := strings.Fields(string(out))
if len(parts) >= 2 {
return parts[len(parts)-1]
}
}
}
return ""
}
func detectDEFromProcesses() string {
deProcesses := map[string][]string{
"GNOME": {"gnome-session", "gnome-shell"},
"KDE Plasma": {"plasmashell", "ksmserver", "kded5", "kded6"},
"Xfce": {"xfce4-session", "xfdesktop"},
"Cinnamon": {"cinnamon-session", "cinnamon"},
"MATE": {"mate-session", "mate-panel"},
"Unity": {"unity-panel-service", "unity"},
"LXDE": {"lxsession", "lxpanel"},
"LXQt": {"lxqt-session", "lxqt-panel"},
"Deepin": {"dde-desktop", "dde-session"},
"Pantheon": {"gala", "wingpanel"},
"Budgie": {"budgie-daemon", "budgie-wm"},
"Enlightenment": {"enlightenment"},
"COSMIC": {"cosmic-session", "cosmic-comp"},
}
return detectProcess(deProcesses)
}
func detectWM() string {
if runtime.GOOS == "darwin" {
return detectMacOSWM()
}
if runtime.GOOS == "windows" {
return "Desktop Window Manager"
}
if os.Getenv("WAYLAND_DISPLAY") != "" {
if wm := detectWaylandCompositor(); wm != "" {
return wm
}
}
if os.Getenv("DISPLAY") != "" {
if wm := detectX11WM(); wm != "" {
return wm
}
}
return detectWMFromProcesses()
}
func detectX11WM() string {
out, err := exec.Command("xprop", "-root", "_NET_SUPPORTING_WM_CHECK").Output()
if err != nil {
return ""
}
fields := strings.Fields(string(out))
if len(fields) < 5 {
return ""
}
wmWindowID := fields[len(fields)-1]
out, err = exec.Command("xprop", "-id", wmWindowID, "_NET_WM_NAME").Output()
if err != nil {
out, err = exec.Command("xprop", "-id", wmWindowID, "WM_NAME").Output()
if err != nil {
return ""
}
}
outputStr := string(out)
if idx := strings.Index(outputStr, "="); idx != -1 {
wmName := strings.TrimSpace(outputStr[idx+1:])
wmName = strings.Trim(wmName, `"'`)
return normalizeWMName(wmName)
}
return ""
}
func detectWaylandCompositor() string {
waylandCompositors := map[string]string{
"HYPRLAND_INSTANCE_SIGNATURE": "Hyprland",
"SWAYSOCK": "Sway",
"WAYFIRE_CONFIG_FILE": "Wayfire",
"NIRI_SOCKET": "niri",
"RIVER_RUNTIME_DIR": "River",
}
for envVar, compositor := range waylandCompositors {
if os.Getenv(envVar) != "" {
return compositor
}
}
if os.Getenv("XDG_CURRENT_DESKTOP") == "GNOME" {
return "Mutter"
}
if os.Getenv("KDE_SESSION_VERSION") != "" {
return "KWin"
}
compositorProcesses := map[string][]string{
"Hyprland": {"Hyprland"},
"Sway": {"sway"},
"Wayfire": {"wayfire"},
"River": {"river"},
"labwc": {"labwc"},
"niri": {"niri"},
"KWin": {"kwin_wayland"},
"Mutter": {"mutter"},
"weston": {"weston"},
"cage": {"cage"},
"dwl": {"dwl"},
}
return detectProcess(compositorProcesses)
}
func detectWMFromProcesses() string {
wmProcesses := map[string][]string{
"i3": {"i3"},
"i3-gaps": {"i3-gaps"},
"bspwm": {"bspwm"},
"awesome": {"awesome"},
"dwm": {"dwm"},
"Openbox": {"openbox"},
"Fluxbox": {"fluxbox"},
"IceWM": {"icewm"},
"JWM": {"jwm"},
"herbstluftwm": {"herbstluftwm"},
"qtile": {"qtile"},
"xmonad": {"xmonad-x86_64-linux", "xmonad"},
"spectrwm": {"spectrwm"},
"cwm": {"cwm"},
"2bwm": {"2bwm"},
"sowm": {"sowm"},
"berry": {"berry"},
"leftwm": {"leftwm"},
"Mutter": {"mutter"},
"KWin": {"kwin_x11", "kwin"},
"Xfwm4": {"xfwm4"},
"Marco": {"marco"},
"Metacity": {"metacity"},
"Compiz": {"compiz"},
"Enlightenment": {"enlightenment"},
"fvwm": {"fvwm", "fvwm2", "fvwm3"},
"ctwm": {"ctwm"},
"twm": {"twm"},
"ratpoison": {"ratpoison"},
"stumpwm": {"stumpwm"},
"WindowMaker": {"wmaker"},
"afterstep": {"afterstep"},
"PekWM": {"pekwm"},
"sawfish": {"sawfish"},
}
return detectProcess(wmProcesses)
}
func normalizeWMName(name string) string {
wmNameMap := map[string]string{
"gnome shell": "Mutter",
"gnome-shell": "Mutter",
"mutter": "Mutter",
"kwin": "KWin",
"kwin_x11": "KWin",
"kwin_wayland": "KWin",
"compiz": "Compiz",
"marco": "Marco",
"metacity": "Metacity",
"xfwm4": "Xfwm4",
"openbox": "Openbox",
}
if normalized, ok := wmNameMap[strings.ToLower(name)]; ok {
return normalized
}
return name
}
func detectMacOSWM() string {
wmProcesses := map[string][]string{
"yabai": {"yabai"},
"AeroSpace": {"AeroSpace"},
"Amethyst": {"Amethyst"},
"Rectangle": {"Rectangle"},
"Magnet": {"Magnet"},
"Moom": {"Moom"},
"Spectacle": {"Spectacle"},
"chunkwm": {"chunkwm"},
"kwm": {"kwm"},
}
if wm := detectProcess(wmProcesses); wm != "" {
return wm
}
return "Quartz Compositor"
}
func detectProcess(processMap map[string][]string) string {
if runtime.GOOS == "darwin" {
return detectProcessDarwin(processMap)
}
procDir := "/proc"
entries, err := os.ReadDir(procDir)
if err != nil {
return ""
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
pid := entry.Name()
if len(pid) == 0 || pid[0] < '0' || pid[0] > '9' {
continue
}
commPath := filepath.Join(procDir, pid, "comm")
commData, err := os.ReadFile(commPath)
if err != nil {
continue
}
comm := strings.TrimSpace(string(commData))
for name, executables := range processMap {
for _, executable := range executables {
if comm == executable || strings.HasPrefix(comm, executable) {
return name
}
}
}
}
return ""
}
func detectProcessDarwin(processMap map[string][]string) string {
out, err := exec.Command("ps", "-axco", "comm").Output()
if err != nil {
return ""
}
processes := strings.Split(string(out), "\n")
for name, executables := range processMap {
for _, executable := range executables {
for _, process := range processes {
process = strings.TrimSpace(process)
if process == executable {
return name
}
}
}
}
return ""
}
func detectWMTheme(wm string) string {
wmLower := strings.ToLower(wm)
switch {
case strings.Contains(wmLower, "kwin"):
return getKWinTheme()
case strings.Contains(wmLower, "mutter"):
return getMutterTheme()
case strings.Contains(wmLower, "xfwm"):
return getXfwmTheme()
case strings.Contains(wmLower, "openbox"):
return getOpenboxTheme()
case strings.Contains(wmLower, "marco"):
return getMarcoTheme()
case strings.Contains(wmLower, "i3"):
return "N/A"
case strings.Contains(wmLower, "sway"):
return "N/A"
case strings.Contains(wmLower, "hyprland"):
return "N/A"
}
return ""
}
func getKWinTheme() string {
homeDir, err := os.UserHomeDir()
if err != nil {
return ""
}
kwinrc := filepath.Join(homeDir, ".config", "kwinrc")
return parseINIValue(kwinrc, "org.kde.kdecoration2", "theme")
}
func getMutterTheme() string {
out, err := exec.Command("gsettings", "get", "org.gnome.shell.extensions.user-theme", "name").Output()
if err == nil {
theme := strings.TrimSpace(string(out))
theme = strings.Trim(theme, "'\"")
if theme != "" && theme != "''" {
return theme
}
}
out, err = exec.Command("gsettings", "get", "org.gnome.desktop.wm.preferences", "theme").Output()
if err == nil {
theme := strings.TrimSpace(string(out))
theme = strings.Trim(theme, "'\"")
if theme != "" {
return theme
}
}
return ""
}
func getXfwmTheme() string {
homeDir, err := os.UserHomeDir()
if err != nil {
return ""
}
xfwm4rc := filepath.Join(homeDir, ".config", "xfce4", "xfconf", "xfce-perchannel-xml", "xfwm4.xml")
data, err := os.ReadFile(xfwm4rc)
if err != nil {
return ""
}
content := string(data)
if idx := strings.Index(content, `name="theme"`); idx != -1 {
rest := content[idx:]
if valueIdx := strings.Index(rest, `value="`); valueIdx != -1 {
start := valueIdx + 7
end := strings.Index(rest[start:], `"`)
if end != -1 {
return rest[start : start+end]
}
}
}
return ""
}
func getOpenboxTheme() string {
homeDir, err := os.UserHomeDir()
if err != nil {
return ""
}
rcPaths := []string{
filepath.Join(homeDir, ".config", "openbox", "rc.xml"),
filepath.Join(homeDir, ".config", "openbox", "lxde-rc.xml"),
filepath.Join(homeDir, ".config", "openbox", "lubuntu-rc.xml"),
}
for _, rcPath := range rcPaths {
data, err := os.ReadFile(rcPath)
if err != nil {
continue
}
content := string(data)
if idx := strings.Index(content, "<name>"); idx != -1 {
rest := content[idx+6:]
if end := strings.Index(rest, "</name>"); end != -1 {
return rest[:end]
}
}
}
return ""
}
func getMarcoTheme() string {
out, err := exec.Command("gsettings", "get", "org.mate.Marco.general", "theme").Output()
if err == nil {
theme := strings.TrimSpace(string(out))
theme = strings.Trim(theme, "'\"")
return theme
}
return ""
}
func parseINIValue(filePath, section, key string) string {
file, err := os.Open(filePath)
if err != nil {
return ""
}
defer file.Close()
scanner := bufio.NewScanner(file)
inSection := false
targetSection := "[" + section + "]"
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
inSection = line == targetSection
continue
}
if inSection && strings.Contains(line, "=") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 && strings.TrimSpace(parts[0]) == key {
return strings.TrimSpace(parts[1])
}
}
}
return ""
}