mirror of
https://github.com/Alexander-D-Karpov/netfetch.git
synced 2026-03-16 22:07:03 +03:00
619 lines
14 KiB
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 ""
|
|
}
|