From e96d60ae91c04256b86c92e3e80a7818a9782693 Mon Sep 17 00:00:00 2001 From: selamanapps Date: Sat, 2 May 2026 01:53:36 +0300 Subject: [PATCH] feat: add permission interpreter, junks, ports, usages, network commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Commands: - z network: Show IP addresses, public IP, DNS servers, active connections - z ports: List listening ports with process/PID info - z usages: All processes sorted by resource usage with filtering - z junks: Find and clean junk files (caches, temporary files) Enhancements: - Permission interpreter in z info file - shows human-readable permissions e.g. -rw-rw-r-- โ†’ "Owner can read, write; Group can read, write; Other can read" - z usages now supports filtering: z usages chrome -m -n=50 - Summary section shows total CPU/memory when filtering by name - Added help entries for all new commands --- cmd/file.go | 95 ++++++++++- cmd/help.go | 64 +++++++ cmd/junks.go | 383 +++++++++++++++++++++++++++++++++++++++++ cmd/ports.go | 471 +++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 24 +++ 5 files changed, 1036 insertions(+), 1 deletion(-) create mode 100644 cmd/junks.go create mode 100644 cmd/ports.go diff --git a/cmd/file.go b/cmd/file.go index 110e257..8e691c7 100644 --- a/cmd/file.go +++ b/cmd/file.go @@ -44,7 +44,7 @@ func FileInfoCommand(args []string) error { fileType := getFileType(filePath) tokens := int(float64(words) / 0.75) size := formatter.FormatSize(fileInfo.Size()) - perms := fileInfo.Mode().String() + perms := FormatPermissions(fileInfo.Mode()) modTime := fileInfo.ModTime().Format(time.RFC1123) // Print output @@ -65,6 +65,7 @@ func FileInfoCommand(args []string) error { PrintSectionHeader("FILE DETAILS") fmt.Printf("%s%s๐Ÿ”’ Permissions:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, perms) + fmt.Printf("%s โ””โ”€ %s%s\n", colors.Gray, colors.Reset, GetPermissionHuman(fileInfo.Mode())) fmt.Printf("%s%s๐Ÿ“… Modified:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, modTime) fmt.Printf("%s%s๐Ÿ“‚ Extension:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, getFileExtension(filePath)) fmt.Println() @@ -145,3 +146,95 @@ func getFileExtension(path string) string { } return ext } + +type Permission struct { + Symbol string + Human string +} + +func InterpretPermissions(mode os.FileMode) []Permission { + var perms []Permission + + isDir := mode.IsDir() + isLink := mode&os.ModeSymlink != 0 + + bits := []uint32{ + uint32(mode >> 6) & 0x7, + uint32(mode >> 3) & 0x7, + uint32(mode) & 0x7, + } + + who := []string{"Owner", "Group", "Other"} + readable := []string{"read", "write", "execute"} + + for i, whoStr := range who { + for j := 0; j < 3; j++ { + bitVal := (bits[i] >> uint(2-j)) & 1 + symbol := "-" + human := "no " + readable[j] + if bitVal == 1 { + if j == 2 { + if isDir { + symbol = "x" + human = whoStr + " can enter directory" + } else if isLink { + symbol = "x" + human = whoStr + " can follow link" + } else { + symbol = "x" + human = whoStr + " can execute" + } + } else if j == 1 { + symbol = "w" + human = whoStr + " can write" + } else { + symbol = "r" + human = whoStr + " can read" + } + } + perms = append(perms, Permission{Symbol: symbol, Human: human}) + } + } + + if isDir { + perms = append([]Permission{{Symbol: "d", Human: "Directory"}}, perms...) + } else if isLink { + perms = append([]Permission{{Symbol: "l", Human: "Symbolic Link"}}, perms...) + } else { + perms = append([]Permission{{Symbol: "-", Human: "Regular File"}}, perms...) + } + + return perms +} + +func FormatPermissions(mode os.FileMode) string { + perms := InterpretPermissions(mode) + var result string + for _, p := range perms { + result += p.Symbol + } + return result +} + +func GetPermissionHuman(mode os.FileMode) string { + perms := InterpretPermissions(mode) + var parts []string + var currentWho string + + for i, p := range perms { + if i == 0 { + currentWho = p.Human + continue + } + whoIdx := (i - 1) / 3 + who := []string{"Owner", "Group", "Other"}[whoIdx] + if p.Symbol != "-" { + parts = append(parts, who+" "+p.Human[len(who)+1:]) + } + } + + if len(parts) == 0 { + return "No permissions" + } + return currentWho + ", " + strings.Join(parts, ", ") +} diff --git a/cmd/help.go b/cmd/help.go index 2899f26..f5d117e 100644 --- a/cmd/help.go +++ b/cmd/help.go @@ -72,6 +72,54 @@ var usageHelp = CommandHelp{ }, } +var junksHelp = CommandHelp{ + Name: "junks", + Help: "Find and clean junk files (caches, temporary files).\n" + + "Shows storage taken by build outputs, caches, and temporary files.\n" + + "Safe to clean: build outputs, caches, temporary files.\n" + + "Not cleanable: runtime version files (require manual cleanup).", + Examples: []string{ + "z junks # List junk files and space usage", + "z junks clean # Preview what will be cleaned", + "z junks clean --force # Actually clean junk files", + }, +} + +var portsHelp = CommandHelp{ + Name: "ports", + Help: "Show all listening ports and the processes using them.\n" + + "Useful for debugging network services and finding port conflicts.", + Examples: []string{ + "z ports # Show listening ports", + "z ports -a # Show all ports (including established)", + "z ports 8080 # Filter by specific port", + }, +} + +var usagesHelp = CommandHelp{ + Name: "usages", + Help: "Show all running processes sorted by resource usage.\n" + + "By default sorts by CPU usage. Use -m to sort by memory.", + Examples: []string{ + "z usages # Top processes by CPU", + "z usages -m # Top processes by memory", + "z usages -n=50 # Show top 50 processes", + "z usages chrome # Filter processes by name", + }, +} + +var networkHelp = CommandHelp{ + Name: "network", + Help: "Show network information including:\n" + + " - Local IP addresses\n" + + " - Public IP\n" + + " - DNS servers\n" + + " - Active network connections", + Examples: []string{ + "z network # Show network overview", + }, +} + func PrintErrorAndHelp(name string, err error) { help := getHelpForCommand(name) @@ -111,6 +159,14 @@ func getHelpForCommand(name string) *CommandHelp { return &searchHelp case "usage": return &usageHelp + case "junks": + return &junksHelp + case "ports": + return &portsHelp + case "usages": + return &usagesHelp + case "network": + return &networkHelp } return nil } @@ -127,6 +183,14 @@ func getUsageHint(name string) string { return " [path]" case "usage": return "" + case "junks": + return "[clean]" + case "ports": + return "[-a] [port]" + case "usages": + return "[-m] [-n=N]" + case "network": + return "" case "info": return "" } diff --git a/cmd/junks.go b/cmd/junks.go new file mode 100644 index 0000000..7b31711 --- /dev/null +++ b/cmd/junks.go @@ -0,0 +1,383 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + + "github.com/zemenawi/zutils/pkg/colors" + "github.com/zemenawi/zutils/pkg/formatter" +) + +type JunkItem struct { + Path string + Size int64 + Type string + SafeToClean bool +} + +type JunkStats struct { + Items []JunkItem + TotalSize int64 + Cleanable int64 + NonCleanable int64 +} + +var junkPatterns = []struct { + Pattern string + Type string + SafeToClean bool +}{ + {".cache", "Cache Directory", true}, + {"__pycache__", "Python Bytecode", true}, + {".pytest_cache", "Python Test Cache", true}, + {".mypy_cache", "Mypy Cache", true}, + {"node_modules/.cache", "Node Module Cache", true}, + {".npm", "NPM Cache", true}, + {".yarn", "Yarn Cache", true}, + {".pnpm-store", "PNPM Store", true}, + {".next", "Next.js Cache", true}, + {".nuxt", "Nuxt Cache", true}, + {".output", "Nuxt Output", true}, + {".solid", "SolidJS Cache", true}, + {".svelte-kit", "SvelteKit Cache", true}, + {"dist", "Build Output", true}, + {"build", "Build Output", true}, + {".gradle/caches", "Gradle Cache", true}, + {".cargo/registry/cache", "Cargo Registry Cache", true}, + {".composer/cache", "Composer Cache", true}, + {".gem/cache", "Ruby Gem Cache", true}, + {".sass-cache", "Sass Cache", true}, + {".eslintcache", "ESLint Cache", true}, + {".webpack", "Webpack Cache", true}, + {".parcel-cache", "Parcel Cache", true}, + {"thumbs.db", "Windows Thumbnail Cache", true}, + {".ds_store", "macOS metadata", true}, + {".spotlight", "macOS Spotlight", true}, + {".trash", "Trash", true}, + {"trash-1000", "Trash", true}, + {".cache/thumbnails", "Thumbnail Cache", true}, + {".-runtime-version", "Runtime Version", false}, +} + +var userHome string + +func init() { + userHome, _ = os.UserHomeDir() +} + +func JunksCommand(args []string) error { + if len(args) < 1 { + return listJunks() + } + + switch args[0] { + case "clean": + return cleanJunks(args[1:]) + default: + return fmt.Errorf("unknown subcommand: %s\n\nUsage:\n z junks List junk files and space\n z junks clean Clean junk files", args[0]) + } +} + +func listJunks() error { + fmt.Printf("%s%s๐Ÿ” Scanning for junk files...%s\n\n", colors.Yellow, colors.Bold, colors.Reset) + + stats, err := scanForJunks() + if err != nil { + return fmt.Errorf("error scanning: %w", err) + } + + if len(stats.Items) == 0 { + fmt.Printf("%sโœจ No junk files found! Your system is clean.%s\n", colors.Green, colors.Reset) + return nil + } + + PrintBoxHeader("SYSTEM JUNK ANALYSIS", colors.Red) + + fmt.Printf("%s%s๐Ÿ’พ Total Junk Size:%s %s\n", colors.Red, colors.Bold, colors.Reset, formatter.FormatSize(stats.TotalSize)) + fmt.Printf("%s%sโœ… Cleanable:%s %s\n", colors.Green, colors.Bold, colors.Reset, formatter.FormatSize(stats.Cleanable)) + fmt.Printf("%s%sโš ๏ธ Non-cleanable:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, formatter.FormatSize(stats.NonCleanable)) + fmt.Println() + + byType := groupByType(stats.Items) + + type typeGroup struct { + junkType string + items []JunkItem + size int64 + } + var groups []typeGroup + for junkType, items := range byType { + var size int64 + for _, item := range items { + size += item.Size + } + groups = append(groups, typeGroup{junkType: junkType, items: items, size: size}) + } + sort.Slice(groups, func(i, j int) bool { + return groups[i].size > groups[j].size + }) + + PrintSectionHeader("JUNK BY TYPE") + for _, group := range groups { + safe := "" + if len(group.items) > 0 && group.items[0].SafeToClean { + safe = colors.Green + " [cleanable]" + colors.Reset + } else { + safe = colors.Yellow + " [manual cleanup]" + colors.Reset + } + fmt.Printf("%s%s๐Ÿ“ %s%s%s (%d items, %s)\n", colors.Red, colors.Bold, group.junkType, safe, colors.Reset, len(group.items), formatter.FormatSize(group.size)) + for _, item := range group.items { + rel, _ := filepath.Rel(userHome, item.Path) + if strings.HasPrefix(item.Path, "/") && !strings.HasPrefix(item.Path, userHome) { + rel = item.Path + } + fmt.Printf(" %s\n", rel) + } + fmt.Println() + } + + fmt.Printf("%s%s๐Ÿ’ก Tip:%s Run %s z junks clean %s to remove cleanable junk files.\n", colors.Cyan, colors.Bold, colors.Reset, colors.Green, colors.Reset) + + return nil +} + +func cleanJunks(args []string) error { + force := false + for _, arg := range args { + if arg == "--force" || arg == "-f" { + force = true + } + } + + stats, err := scanForJunks() + if err != nil { + return fmt.Errorf("error scanning: %w", err) + } + + cleanableItems := filterCleanable(stats.Items) + + if len(cleanableItems) == 0 { + fmt.Printf("%sโœจ No cleanable junk files found!%s\n", colors.Green, colors.Reset) + return nil + } + + var totalCleanable int64 + for _, item := range cleanableItems { + totalCleanable += item.Size + } + + PrintBoxHeader("CLEAN JUNK FILES", colors.Red) + fmt.Printf("%s%s๐Ÿงน Found %d cleanable items (%s)%s\n\n", colors.Yellow, colors.Bold, len(cleanableItems), formatter.FormatSize(totalCleanable), colors.Reset) + + if !force { + fmt.Printf("%s%sโš ๏ธ This will permanently delete:%s\n", colors.Red, colors.Bold, colors.Reset) + byType := groupByType(cleanableItems) + for junkType, items := range byType { + var size int64 + for _, item := range items { + size += item.Size + } + fmt.Printf(" โ€ข %s (%d items, %s)\n", junkType, len(items), formatter.FormatSize(size)) + } + fmt.Println() + fmt.Printf("%s%sConfirmation required.%s Run with %s--force%s to proceed.\n", colors.Cyan, colors.Bold, colors.Reset, colors.Green, colors.Reset) + fmt.Printf("%s%sExample:%s z junks clean --force\n", colors.Cyan, colors.Bold, colors.Reset) + return nil + } + + fmt.Printf("%s%s๐Ÿ—‘๏ธ Cleaning junk files...%s\n\n", colors.Yellow, colors.Bold, colors.Reset) + + var cleaned int64 + var failed int + for _, item := range cleanableItems { + if err := os.RemoveAll(item.Path); err != nil { + failed++ + fmt.Printf(" %sโœ–%s Failed: %s\n", colors.Red, colors.Reset, item.Path) + } else { + cleaned += item.Size + rel, _ := filepath.Rel(userHome, item.Path) + if !strings.HasPrefix(item.Path, userHome) { + rel = item.Path + } + fmt.Printf(" %sโœ“%s Removed: %s (%s)\n", colors.Green, colors.Reset, rel, formatter.FormatSize(item.Size)) + } + } + + fmt.Println() + PrintSectionHeader("CLEANUP SUMMARY") + fmt.Printf("%s%sโœ… Removed:%s %d items (%s)\n", colors.Green, colors.Bold, colors.Reset, len(cleanableItems)-failed, formatter.FormatSize(cleaned)) + if failed > 0 { + fmt.Printf("%s%sโš ๏ธ Failed:%s %d items\n", colors.Red, colors.Bold, colors.Reset, failed) + } + fmt.Printf("%s%s๐Ÿ’พ Space reclaimed:%s %s\n", colors.Cyan, colors.Bold, colors.Reset, formatter.FormatSize(cleaned)) + + return nil +} + +func scanForJunks() (*JunkStats, error) { + stats := &JunkStats{ + Items: make([]JunkItem, 0), + } + + searchRoots := getJunkSearchRoots() + + typeResult := scanDirs(searchRoots, junkPatterns, stats) + + stats.TotalSize = typeResult.TotalSize + stats.Cleanable = typeResult.Cleanable + stats.NonCleanable = typeResult.NonCleanable + + return stats, nil +} + +func getJunkSearchRoots() []string { + var roots []string + + stdCache := os.Getenv("XDG_CACHE_HOME") + if stdCache != "" { + roots = append(roots, stdCache) + } else { + roots = append(roots, filepath.Join(userHome, ".cache")) + } + + if appCache := filepath.Join(userHome, ".npm"); dirExists(appCache) { + roots = append(roots, appCache) + } + if appCache := filepath.Join(userHome, ".yarn"); dirExists(appCache) { + roots = append(roots, appCache) + } + if appCache := filepath.Join(userHome, ".pnpm-store"); dirExists(appCache) { + roots = append(roots, appCache) + } + if appCache := filepath.Join(userHome, ".gradle"); dirExists(appCache) { + roots = append(roots, appCache) + } + if appCache := filepath.Join(userHome, ".cargo/registry/cache"); dirExists(appCache) { + roots = append(roots, appCache) + } + if appCache := filepath.Join(userHome, ".cargo/registry/index"); dirExists(appCache) { + roots = append(roots, appCache) + } + + tempDir := os.TempDir() + if tempDir != "" && dirExists(tempDir) { + roots = append(roots, tempDir) + } + + return roots +} + +func dirExists(path string) bool { + info, err := os.Stat(path) + return err == nil && info.IsDir() +} + +func scanDirs(roots []string, patterns []struct { + Pattern string + Type string + SafeToClean bool +}, stats *JunkStats) *JunkStats { + for _, root := range roots { + if _, err := os.Stat(root); err != nil { + continue + } + + filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + + baseName := filepath.Base(path) + + for _, jp := range patterns { + matched := false + + if jp.Pattern == baseName || baseName == jp.Pattern { + matched = true + } else if strings.Contains(path, jp.Pattern) { + matched = true + } else if matched, _ = regexp.MatchString("^"+jp.Pattern+"$", baseName); matched { + } + + if matched { + size := getDirSize(path) + stats.Items = append(stats.Items, JunkItem{ + Path: path, + Size: size, + Type: jp.Type, + SafeToClean: jp.SafeToClean, + }) + + if info.IsDir() { + if jp.SafeToClean { + stats.Cleanable += size + } else { + stats.NonCleanable += size + } + stats.TotalSize += size + return filepath.SkipDir + } else { + if jp.SafeToClean { + stats.Cleanable += size + } else { + stats.NonCleanable += size + } + stats.TotalSize += size + } + break + } + } + + return nil + }) + } + + return stats +} + +func getDirSize(path string) int64 { + var size int64 + + info, err := os.Stat(path) + if err != nil { + return 0 + } + + if !info.IsDir() { + return info.Size() + } + + filepath.Walk(path, func(filePath string, fileInfo os.FileInfo, err error) error { + if err != nil { + return nil + } + if fileInfo.Mode().IsRegular() { + size += fileInfo.Size() + } + return nil + }) + + return size +} + +func groupByType(items []JunkItem) map[string][]JunkItem { + result := make(map[string][]JunkItem) + for _, item := range items { + result[item.Type] = append(result[item.Type], item) + } + return result +} + +func filterCleanable(items []JunkItem) []JunkItem { + var cleanable []JunkItem + for _, item := range items { + if item.SafeToClean { + cleanable = append(cleanable, item) + } + } + return cleanable +} \ No newline at end of file diff --git a/cmd/ports.go b/cmd/ports.go new file mode 100644 index 0000000..8b6afdb --- /dev/null +++ b/cmd/ports.go @@ -0,0 +1,471 @@ +package cmd + +import ( + "fmt" + "os" + "os/exec" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/zemenawi/zutils/pkg/colors" + "github.com/zemenawi/zutils/pkg/formatter" +) + +type PortInfo struct { + Protocol string + LocalAddr string + Port int + Process string + PID int + State string +} + +func PortsCommand(args []string) error { + showAll := false + portFilter := 0 + + for _, arg := range args { + if arg == "-a" || arg == "--all" { + showAll = true + } else if strings.HasPrefix(arg, "-") { + continue + } else { + if port, err := strconv.Atoi(arg); err == nil { + portFilter = port + } + } + } + + ports, err := getPortList() + if err != nil { + return fmt.Errorf("error getting ports: %w", err) + } + + if portFilter > 0 { + var filtered []PortInfo + for _, p := range ports { + if p.Port == portFilter { + filtered = append(filtered, p) + } + } + ports = filtered + } + + if len(ports) == 0 { + if portFilter > 0 { + return fmt.Errorf("no process found using port %d", portFilter) + } + fmt.Printf("%sNo listening ports found%s\n", colors.Yellow, colors.Reset) + return nil + } + + sort.Slice(ports, func(i, j int) bool { + if ports[i].Port != ports[j].Port { + return ports[i].Port < ports[j].Port + } + return ports[i].Protocol < ports[j].Protocol + }) + + PrintBoxHeader("PORT LISTENING", colors.Cyan) + + if portFilter > 0 { + fmt.Printf("%s%s๐Ÿ” Filtering by port: %d%s\n", colors.Yellow, colors.Bold, portFilter, colors.Reset) + fmt.Println() + } + + if !showAll && len(ports) > 50 { + fmt.Printf("%s%s๐Ÿ“Š Showing %d of %d ports (use -a for all)%s\n\n", + colors.Yellow, colors.Bold, 50, len(ports), colors.Reset) + ports = ports[:50] + } + + headers := []string{"#", "Protocol", "Port", "Process", "PID", "State"} + data := make([][]string, 0, len(ports)) + + for i, p := range ports { + state := p.State + if state == "LISTEN" { + state = colors.Green + "LISTEN" + colors.Reset + } else if state == "ESTABLISHED" { + state = colors.Cyan + "ESTABLISHED" + colors.Reset + } else { + state = colors.Gray + state + colors.Reset + } + + row := []string{ + fmt.Sprintf("%d", i+1), + strings.ToUpper(p.Protocol), + fmt.Sprintf("%d", p.Port), + p.Process, + fmt.Sprintf("%d", p.PID), + state, + } + data = append(data, row) + } + + formatter.PrintTable(headers, data, colors.Cyan) + fmt.Println() + + fmt.Printf("%s๐Ÿ’ก Tip:%s Use %s z ports %s to filter by specific port\n", + colors.Cyan, colors.Reset, colors.Green, colors.Reset) + + return nil +} + +func getPortList() ([]PortInfo, error) { + var ports []PortInfo + + cmd := exec.Command("ss", "-tulpn") + output, err := cmd.CombinedOutput() + if err != nil { + cmd = exec.Command("netstat", "-tulpn") + output, err = cmd.CombinedOutput() + if err != nil { + return ports, fmt.Errorf("ss and netstat both failed") + } + } + + lines := strings.Split(string(output), "\n") + for _, line := range lines[1:] { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "Netid") || strings.HasPrefix(line, "State") { + continue + } + + port := parseSSLine(line) + if port != nil && port.Port > 0 { + ports = append(ports, *port) + } + } + + return ports, nil +} + +func parseSSLine(line string) *PortInfo { + info := &PortInfo{} + + protoMatch := regexp.MustCompile(`^(tcp6?|udp6?|udplite)\s+`) + protoMatch2 := protoMatch.FindStringSubmatch(strings.TrimSpace(line)) + if len(protoMatch2) > 1 { + info.Protocol = protoMatch2[1] + if info.Protocol == "udp" || info.Protocol == "udplite" { + info.Protocol = "udp" + } + } + + stateMatch := regexp.MustCompile(`(LISTEN|UNCONN|ESTAB|CLOSE-WAIT|CLOSING|SYN-SENT|SYN-RECV|FIN-WAIT-1|FIN-WAIT-2|TIME-WAIT)\s+`) + stateMatch2 := stateMatch.FindStringSubmatch(line) + if len(stateMatch2) > 1 { + info.State = stateMatch2[1] + } else { + info.State = "-" + } + + portStr := "" + for i := len(line) - 1; i >= 0; i-- { + if line[i] == ':' { + start := i + 1 + for start < len(line) && line[start] == ' ' { + start++ + } + end := start + for end < len(line) && line[end] >= '0' && line[end] <= '9' { + end++ + } + if end > start { + portStr = line[start:end] + break + } + } + } + if port, err := strconv.Atoi(portStr); err == nil { + info.Port = port + } + + pidMatch := regexp.MustCompile(`pid=(\d+)`) + pidMatch2 := pidMatch.FindStringSubmatch(line) + if len(pidMatch2) > 1 { + info.PID, _ = strconv.Atoi(pidMatch2[1]) + } + + processMatch := regexp.MustCompile(`process_name="([^"]+)"`) + processMatch2 := processMatch.FindStringSubmatch(line) + if len(processMatch2) > 1 { + info.Process = processMatch2[1] + } + + return info +} + +func getProcessNameByPID(pid int) string { + if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid)); err == nil { + return strings.TrimSpace(string(data)) + } + return "unknown" +} + +type ProcessStats struct { + PID int + Name string + Command string + CPUPercent float64 + MemBytes int64 + MemPercent float64 + Status string + User string +} + +func UsagesCommand(args []string) error { + sortBy := "cpu" + limit := 20 + filter := "" + + for _, arg := range args { + if arg == "-m" || arg == "--memory" { + sortBy = "memory" + } else if arg == "-c" || arg == "--cpu" { + sortBy = "cpu" + } else if strings.HasPrefix(arg, "-n=") { + if n, err := strconv.Atoi(strings.TrimPrefix(arg, "-n=")); err == nil && n > 0 { + limit = n + } + } else if strings.HasPrefix(arg, "--limit=") { + if n, err := strconv.Atoi(strings.TrimPrefix(arg, "--limit=")); err == nil && n > 0 { + limit = n + } + } else if strings.HasPrefix(arg, "-") { + continue + } else { + filter = arg + } + } + + processes, err := getAllProcessStats() + if err != nil { + return fmt.Errorf("error getting process stats: %w", err) + } + + if filter != "" { + var filtered []ProcessStats + for _, p := range processes { + if strings.Contains(strings.ToLower(p.Name), strings.ToLower(filter)) || + strings.Contains(strings.ToLower(p.Command), strings.ToLower(filter)) { + filtered = append(filtered, p) + } + } + processes = filtered + if len(processes) == 0 { + return fmt.Errorf("no processes found matching '%s'", filter) + } + } + + sort.Slice(processes, func(i, j int) bool { + if sortBy == "memory" { + return processes[i].MemBytes > processes[j].MemBytes + } + return processes[i].CPUPercent > processes[j].CPUPercent + }) + + if limit > 0 && len(processes) > limit { + processes = processes[:limit] + } + + PrintBoxHeader(fmt.Sprintf("TOP PROCESSES (by %s)", strings.ToUpper(sortBy)), colors.Cyan) + + if filter != "" { + fmt.Printf("%s%s๐Ÿ” Filtering by: %s%s\n", colors.Yellow, colors.Bold, filter, colors.Reset) + fmt.Println() + } + + headers := []string{"#", "PID", "Process", "CPU%", "Memory", "Mem%", "Status"} + data := make([][]string, 0, len(processes)) + + for i, p := range processes { + status := p.Status + if status == "R" || status == "running" { + status = colors.Green + "running" + colors.Reset + } else if status == "S" || status == "I" || status == "sleeping" { + status = colors.Gray + "sleeping" + colors.Reset + } else { + status = colors.Yellow + status + colors.Reset + } + + cmd := p.Name + if len(p.Command) > 25 { + cmd = p.Command[:25] + "..." + } + + row := []string{ + fmt.Sprintf("%d", i+1), + fmt.Sprintf("%d", p.PID), + cmd, + fmt.Sprintf("%.1f", p.CPUPercent), + formatter.FormatSize(p.MemBytes), + fmt.Sprintf("%.1f%%", p.MemPercent), + status, + } + data = append(data, row) + } + + formatter.PrintTable(headers, data, colors.Cyan) + fmt.Println() + + if filter != "" { + var totalCPU float64 + var totalMem int64 + for _, p := range processes { + totalCPU += p.CPUPercent + totalMem += p.MemBytes + } + + PrintSectionHeader("SUMMARY") + fmt.Printf("%s%s๐Ÿ“Š Total CPU:%s %.1f%%\n", colors.Green, colors.Bold, colors.Reset, totalCPU) + fmt.Printf("%s%s๐Ÿ’พ Total Memory:%s %s (%.1f%% of system)%s\n", + colors.Purple, colors.Bold, colors.Reset, + formatter.FormatSize(totalMem), + float64(totalMem)/float64(getSystemMem())*100, + colors.Reset) + fmt.Println() + } + + fmt.Printf("%s๐Ÿ’ก Tip:%s Use %s z usages -m%s for memory sorting, %s z usages -n=50%s to show 50 processes\n", + colors.Cyan, colors.Reset, colors.Green, colors.Reset, colors.Green, colors.Reset) + + return nil +} + +func getSystemMem() int64 { + if data, err := os.ReadFile("/proc/meminfo"); err == nil { + lines := strings.Split(string(data), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "MemTotal:") { + fields := strings.Fields(line) + if len(fields) >= 2 { + if memKB, err := strconv.ParseInt(fields[1], 10, 64); err == nil { + return memKB * 1024 + } + } + } + } + } + return 1 +} + +func getAllProcessStats() ([]ProcessStats, error) { + var processes []ProcessStats + + dir, err := os.Open("/proc") + if err != nil { + return processes, err + } + defer dir.Close() + + entries, err := dir.Readdirnames(0) + if err != nil { + return processes, err + } + + for _, entry := range entries { + pid, err := strconv.Atoi(entry) + if err != nil { + continue + } + + proc, err := getFullProcessInfo(pid) + if err == nil && proc != nil { + processes = append(processes, *proc) + } + } + + return processes, nil +} + +func getFullProcessInfo(pid int) (*ProcessStats, error) { + comm, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid)) + if err != nil { + return nil, err + } + name := strings.TrimSpace(string(comm)) + + cmdline, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) + cmdStr := strings.Join(strings.Fields(string(cmdline)), " ") + if cmdStr == "" { + cmdStr = name + } + + stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) + if err != nil { + return nil, err + } + + statParts := strings.Split(string(stat), " ") + status := "?" + if len(statParts) > 2 { + status = strings.Trim(statParts[2], "()") + } + + utime := int64(0) + stime := int64(0) + if len(statParts) >= 15 { + utime, _ = strconv.ParseInt(strings.TrimSpace(statParts[13]), 10, 64) + stime, _ = strconv.ParseInt(strings.TrimSpace(statParts[14]), 10, 64) + } + + memBytes := int64(0) + if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)); err == nil { + lines := strings.Split(string(data), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "VmRSS:") { + fields := strings.Fields(line) + if len(fields) >= 2 { + if memKB, err := strconv.ParseInt(fields[1], 10, 64); err == nil { + memBytes = memKB * 1024 + } + } + break + } + } + } + + totalMem := int64(0) + if data, err := os.ReadFile("/proc/meminfo"); err == nil { + lines := strings.Split(string(data), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "MemTotal:") { + fields := strings.Fields(line) + if len(fields) >= 2 { + if memKB, err := strconv.ParseInt(fields[1], 10, 64); err == nil { + totalMem = memKB * 1024 + } + } + break + } + } + } + + cpuPercent := 0.0 + if totalMem > 0 { + cpuPercent = float64(utime+stime) / float64(totalMem) * 100 + } + + memPercent := 0.0 + if totalMem > 0 { + memPercent = float64(memBytes) / float64(totalMem) * 100 + } + + return &ProcessStats{ + PID: pid, + Name: name, + Command: cmdStr, + CPUPercent: cpuPercent, + MemBytes: memBytes, + MemPercent: memPercent, + Status: status, + }, nil +} + +func NetworkCommand(args []string) error { + return NetworkInfoCommand(args) +} \ No newline at end of file diff --git a/main.go b/main.go index dcd553c..89a188f 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,30 @@ func main() { cmd.RegisterInfoCommands(registry) + registry.Register(&types.Command{ + Name: "ports", + Description: "Show listening ports and processes using them", + Handler: cmd.PortsCommand, + }) + + registry.Register(&types.Command{ + Name: "usages", + Description: "Show all processes sorted by resource usage", + Handler: cmd.UsagesCommand, + }) + + registry.Register(&types.Command{ + Name: "network", + Description: "Show network information (IP, connections, DNS)", + Handler: cmd.NetworkCommand, + }) + + registry.Register(&types.Command{ + Name: "junks", + Description: "Find and clean junk files (caches, temporary files)", + Handler: cmd.JunksCommand, + }) + registry.Register(&types.Command{ Name: "version", Description: "Show version information",