netfetch/internal/collector/terminal.go

384 lines
9.5 KiB
Go

package collector
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
)
func (c *Collector) collectTerminal() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.info.Terminal = detectTerminal()
}
func detectTerminal() string {
if term := detectTerminalFromEnv(); term != "" {
return term
}
if runtime.GOOS == "windows" {
return detectTerminalWindows()
}
if runtime.GOOS == "darwin" {
if term := detectTerminalFromEnv(); term != "" {
return term
}
}
if term := detectTerminalFromProcessTree(); term != "" {
return term
}
return "Unknown"
}
func detectTerminalFromEnv() string {
if term := os.Getenv("TERM_PROGRAM"); term != "" {
version := os.Getenv("TERM_PROGRAM_VERSION")
termName := normalizeTerminalName(term)
if version != "" {
return termName + " " + version
}
return termName
}
terminalEnvVars := map[string]string{
"KITTY_PID": "kitty",
"KITTY_WINDOW_ID": "kitty",
"ALACRITTY_SOCKET": "Alacritty",
"ALACRITTY_LOG": "Alacritty",
"ALACRITTY_WINDOW_ID": "Alacritty",
"WEZTERM_EXECUTABLE": "WezTerm",
"WEZTERM_PANE": "WezTerm",
"WT_SESSION": "Windows Terminal",
"WT_PROFILE_ID": "Windows Terminal",
"KONSOLE_VERSION": "Konsole",
"KONSOLE_DBUS_SESSION": "Konsole",
"GNOME_TERMINAL_SERVICE": "GNOME Terminal",
"GNOME_TERMINAL_SCREEN": "GNOME Terminal",
"TILIX_ID": "Tilix",
"TERMINATOR_UUID": "Terminator",
"TERMINATOR_DBUS_NAME": "Terminator",
"ITERM_SESSION_ID": "iTerm2",
"ITERM_PROFILE": "iTerm2",
"GHOSTTY_RESOURCES_DIR": "Ghostty",
"GHOSTTY_BIN_DIR": "Ghostty",
"TABBY_CONFIG_DIRECTORY": "Tabby",
"HYPER_CLI": "Hyper",
"TERMINUS_PLUGINS": "Terminus",
"VSCODE_INJECTION": "VS Code",
"VSCODE_GIT_IPC_HANDLE": "VS Code",
"TERMINAL_EMULATOR": "",
"COLORTERM": "",
}
for envVar, termName := range terminalEnvVars {
if value := os.Getenv(envVar); value != "" {
if termName != "" {
if envVar == "KONSOLE_VERSION" {
return "Konsole " + formatKonsoleVersion(value)
}
return termName
}
if envVar == "TERMINAL_EMULATOR" {
return normalizeTerminalName(value)
}
}
}
if colorterm := os.Getenv("COLORTERM"); colorterm != "" {
colortermMap := map[string]string{
"truecolor": "",
"24bit": "",
"gnome-terminal": "GNOME Terminal",
"xfce4-terminal": "Xfce Terminal",
"rxvt-xpm": "rxvt",
"rxvt": "rxvt",
}
if term, ok := colortermMap[colorterm]; ok && term != "" {
return term
}
}
return ""
}
func formatKonsoleVersion(version string) string {
if len(version) == 6 {
year := version[0:2]
month := version[2:4]
patch := version[4:6]
return fmt.Sprintf("%s.%s.%s", year, month, patch)
}
return version
}
func normalizeTerminalName(name string) string {
terminalNames := map[string]string{
"apple_terminal": "Terminal.app",
"terminal.app": "Terminal.app",
"iterm.app": "iTerm2",
"iterm2": "iTerm2",
"hyper": "Hyper",
"vscode": "VS Code",
"code": "VS Code",
"visual studio code": "VS Code",
"gnome-terminal": "GNOME Terminal",
"gnome-terminal-server": "GNOME Terminal",
"xfce4-terminal": "Xfce Terminal",
"mate-terminal": "MATE Terminal",
"lxterminal": "LXTerminal",
"qterminal": "QTerminal",
"terminology": "Terminology",
"terminator": "Terminator",
"tilix": "Tilix",
"guake": "Guake",
"tilda": "Tilda",
"yakuake": "Yakuake",
"konsole": "Konsole",
"kitty": "kitty",
"alacritty": "Alacritty",
"wezterm": "WezTerm",
"wezterm-gui": "WezTerm",
"foot": "foot",
"st": "st",
"urxvt": "urxvt",
"rxvt-unicode": "urxvt",
"rxvt": "rxvt",
"xterm": "xterm",
"uxterm": "xterm",
"cool-retro-term": "cool-retro-term",
"termux": "Termux",
"contour": "Contour",
"rio": "Rio",
"warp": "Warp",
"extraterm": "Extraterm",
"black-box": "Black Box",
"ptyxis": "Ptyxis",
"cosmic-term": "COSMIC Terminal",
}
nameLower := strings.ToLower(name)
if normalized, ok := terminalNames[nameLower]; ok {
return normalized
}
return name
}
func detectTerminalFromProcessTree() string {
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" && runtime.GOOS != "freebsd" {
return ""
}
skipProcesses := map[string]bool{
"login": true, "init": true, "systemd": true, "sshd": true, "ssh": true,
"tmux": true, "tmux: server": true, "screen": true, "zellij": true,
"sh": true, "bash": true, "zsh": true, "fish": true, "dash": true,
"ksh": true, "tcsh": true, "csh": true, "ash": true, "nu": true,
"elvish": true, "xonsh": true, "ion": true, "oil": true,
"su": true, "sudo": true, "doas": true, "pkexec": true,
"newgrp": true, "sg": true, "runuser": true,
"python": true, "python3": true, "node": true, "ruby": true, "perl": true,
}
terminalProcesses := map[string]string{
"alacritty": "Alacritty",
"kitty": "kitty",
"wezterm": "WezTerm",
"wezterm-gui": "WezTerm",
"gnome-terminal": "GNOME Terminal",
"gnome-terminal-": "GNOME Terminal",
"konsole": "Konsole",
"xfce4-terminal": "Xfce Terminal",
"xterm": "xterm",
"urxvt": "urxvt",
"rxvt": "rxvt",
"terminator": "Terminator",
"tilix": "Tilix",
"st": "st",
"cool-retro-term": "cool-retro-term",
"lxterminal": "LXTerminal",
"mate-terminal": "MATE Terminal",
"terminology": "Terminology",
"foot": "foot",
"footclient": "foot",
"contour": "Contour",
"qterminal": "QTerminal",
"guake": "Guake",
"tilda": "Tilda",
"yakuake": "Yakuake",
"hyper": "Hyper",
"Hyper": "Hyper",
"terminus": "Terminus",
"tabby": "Tabby",
"extraterm": "Extraterm",
"black-box": "Black Box",
"blackbox": "Black Box",
"ptyxis": "Ptyxis",
"cosmic-term": "COSMIC Terminal",
"rio": "Rio",
"ghostty": "Ghostty",
"code": "VS Code",
"code-oss": "VS Code",
"codium": "VSCodium",
"cursor": "Cursor",
"goland": "GoLand",
"idea": "IntelliJ IDEA",
"pycharm": "PyCharm",
"webstorm": "WebStorm",
"phpstorm": "PhpStorm",
"rubymine": "RubyMine",
"clion": "CLion",
"datagrip": "DataGrip",
"rider": "Rider",
"fleet": "Fleet",
"zed": "Zed",
"sublime_text": "Sublime Text",
"atom": "Atom",
"Terminal": "Terminal.app",
"iTerm2": "iTerm2",
"Apple_Terminal": "Terminal.app",
}
pid := os.Getppid()
visited := make(map[int]bool)
for i := 0; i < 30 && pid > 1; i++ {
if visited[pid] {
break
}
visited[pid] = true
procName := getProcessName(pid)
if procName == "" {
break
}
if skipProcesses[procName] {
pid = getParentPID(pid)
continue
}
if terminal, ok := terminalProcesses[procName]; ok {
return terminal
}
for prefix, terminal := range terminalProcesses {
if strings.HasPrefix(procName, prefix) {
return terminal
}
}
pid = getParentPID(pid)
}
return ""
}
func getProcessName(pid int) string {
if runtime.GOOS == "darwin" {
out, err := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "comm=").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
commPath := fmt.Sprintf("/proc/%d/comm", pid)
data, err := os.ReadFile(commPath)
if err != nil {
return ""
}
return strings.TrimSpace(string(data))
}
func getParentPID(pid int) int {
if runtime.GOOS == "darwin" {
out, err := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "ppid=").Output()
if err != nil {
return 0
}
ppid, err := strconv.Atoi(strings.TrimSpace(string(out)))
if err != nil {
return 0
}
return ppid
}
statPath := fmt.Sprintf("/proc/%d/stat", pid)
data, err := os.ReadFile(statPath)
if err != nil {
return 0
}
statStr := string(data)
closeParen := strings.LastIndex(statStr, ")")
if closeParen == -1 || closeParen+2 >= len(statStr) {
return 0
}
fields := strings.Fields(statStr[closeParen+2:])
if len(fields) < 2 {
return 0
}
ppid, err := strconv.Atoi(fields[1])
if err != nil {
return 0
}
return ppid
}
func detectTerminalWindows() string {
if os.Getenv("WT_SESSION") != "" {
return "Windows Terminal"
}
if os.Getenv("ConEmuPID") != "" {
return "ConEmu"
}
if os.Getenv("CMDER_ROOT") != "" {
return "Cmder"
}
if os.Getenv("ALACRITTY_SOCKET") != "" {
return "Alacritty"
}
if os.Getenv("WEZTERM_EXECUTABLE") != "" {
return "WezTerm"
}
if os.Getenv("TERMINUS_PLUGINS") != "" {
return "Terminus"
}
if os.Getenv("HYPER_CLI") != "" {
return "Hyper"
}
if comspec := os.Getenv("COMSPEC"); comspec != "" {
base := filepath.Base(comspec)
if strings.EqualFold(base, "powershell.exe") {
return "PowerShell"
}
if strings.EqualFold(base, "pwsh.exe") {
return "PowerShell Core"
}
}
return "cmd.exe"
}