New Commands: - z read - Smart file reader with auto-detection (markdown, syntax highlighting) - z sys - System overview (CPU, RAM, Disk, processes) - z find - File search with pattern/size/time filters - z search - User-friendly grep alternative - z usage - Process resource usage monitor Enhancements: - Unified help system with consistent error messages - Help on error shows usage and examples for each command - "Command not found" suggests similar commands - Directory info now detects .gitignore and shows ignored files - File handler registry for extensible file type detection - Added Italic, Dim, Underline color constants
370 lines
9 KiB
Go
370 lines
9 KiB
Go
package cmd
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"os/exec"
|
||
"runtime"
|
||
"strconv"
|
||
"strings"
|
||
"syscall"
|
||
"time"
|
||
|
||
"github.com/zemenawi/zutils/pkg/colors"
|
||
"github.com/zemenawi/zutils/pkg/formatter"
|
||
)
|
||
|
||
type SystemStats struct {
|
||
Hostname string
|
||
OS string
|
||
Platform string
|
||
CPUModel string
|
||
CPUCount int
|
||
Uptime string
|
||
LoadAvg string
|
||
MemTotal int64
|
||
MemUsed int64
|
||
MemFree int64
|
||
DiskTotal int64
|
||
DiskUsed int64
|
||
DiskFree int64
|
||
TopProcs []ProcInfo
|
||
}
|
||
|
||
type ProcInfo struct {
|
||
PID int
|
||
Name string
|
||
CPU float64
|
||
Mem float64
|
||
Command string
|
||
}
|
||
|
||
func SystemCommand(args []string) error {
|
||
stats, err := getSystemStats()
|
||
if err != nil {
|
||
return fmt.Errorf("error getting system stats: %v", err)
|
||
}
|
||
|
||
PrintBoxHeader("SYSTEM OVERVIEW", colors.Cyan)
|
||
|
||
fmt.Printf("%s%s🏠 Hostname:%s %s\n", colors.Blue, colors.Bold, colors.Reset, stats.Hostname)
|
||
fmt.Printf("%s%s💻 OS:%s %s\n", colors.Blue, colors.Bold, colors.Reset, stats.OS)
|
||
fmt.Printf("%s%s🔧 Platform:%s %s\n", colors.Blue, colors.Bold, colors.Reset, stats.Platform)
|
||
fmt.Println()
|
||
|
||
PrintSectionHeader("CPU")
|
||
fmt.Printf("%s%s🖥️ Model:%s %s\n", colors.Green, colors.Bold, colors.Reset, stats.CPUModel)
|
||
fmt.Printf("%s%s🔢 Cores:%s %d\n", colors.Green, colors.Bold, colors.Reset, stats.CPUCount)
|
||
fmt.Printf("%s%s📊 Load Avg:%s %s\n", colors.Green, colors.Bold, colors.Reset, stats.LoadAvg)
|
||
fmt.Printf("%s%s⏱️ Uptime:%s %s\n", colors.Green, colors.Bold, colors.Reset, stats.Uptime)
|
||
fmt.Println()
|
||
|
||
PrintSectionHeader("MEMORY")
|
||
memTotal := formatter.FormatSize(stats.MemTotal)
|
||
memUsed := formatter.FormatSize(stats.MemUsed)
|
||
memFree := formatter.FormatSize(stats.MemFree)
|
||
memPct := float64(stats.MemUsed) / float64(stats.MemTotal) * 100
|
||
|
||
fmt.Printf("%s%s💾 Total:%s %s\n", colors.Purple, colors.Bold, colors.Reset, memTotal)
|
||
fmt.Printf("%s%s📈 Used:%s %s (%.1f%%)\n", colors.Purple, colors.Bold, colors.Reset, memUsed, memPct)
|
||
fmt.Printf("%s%s📉 Free:%s %s\n", colors.Purple, colors.Bold, colors.Reset, memFree)
|
||
printMemoryBar(stats.MemUsed, stats.MemTotal)
|
||
fmt.Println()
|
||
|
||
PrintSectionHeader("DISK")
|
||
diskTotal := formatter.FormatSize(stats.DiskTotal)
|
||
diskUsed := formatter.FormatSize(stats.DiskUsed)
|
||
diskFree := formatter.FormatSize(stats.DiskFree)
|
||
diskPct := float64(stats.DiskUsed) / float64(stats.DiskTotal) * 100
|
||
|
||
fmt.Printf("%s%s💽 Total:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, diskTotal)
|
||
fmt.Printf("%s%s📈 Used:%s %s (%.1f%%)\n", colors.Yellow, colors.Bold, colors.Reset, diskUsed, diskPct)
|
||
fmt.Printf("%s%s📉 Free:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, diskFree)
|
||
printDiskBar(stats.DiskUsed, stats.DiskTotal)
|
||
fmt.Println()
|
||
|
||
if len(stats.TopProcs) > 0 {
|
||
PrintSectionHeader("TOP PROCESSES")
|
||
headers := []string{"PID", "Name", "CPU%", "MEM%"}
|
||
data := make([][]string, 0, len(stats.TopProcs))
|
||
|
||
for _, proc := range stats.TopProcs {
|
||
row := []string{
|
||
fmt.Sprintf("%d", proc.PID),
|
||
truncateString(proc.Name, 20),
|
||
fmt.Sprintf("%.1f", proc.CPU),
|
||
fmt.Sprintf("%.1f", proc.Mem),
|
||
}
|
||
data = append(data, row)
|
||
}
|
||
|
||
formatter.PrintTable(headers, data, colors.Cyan)
|
||
fmt.Println()
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func printMemoryBar(used, total int64) {
|
||
width := 30
|
||
if total == 0 {
|
||
return
|
||
}
|
||
fillLen := int(float64(used) / float64(total) * float64(width))
|
||
|
||
bar := "["
|
||
for i := 0; i < width; i++ {
|
||
if i < fillLen {
|
||
if i < width/3 {
|
||
bar += colors.Green + "█" + colors.Reset
|
||
} else if i < 2*width/3 {
|
||
bar += colors.Yellow + "█" + colors.Reset
|
||
} else {
|
||
bar += colors.Red + "█" + colors.Reset
|
||
}
|
||
} else {
|
||
bar += colors.Gray + "░" + colors.Reset
|
||
}
|
||
}
|
||
bar += "]"
|
||
|
||
fmt.Printf("%s%sMemory Bar:%s %s\n", colors.Gray, colors.Bold, colors.Reset, bar)
|
||
}
|
||
|
||
func printDiskBar(used, total int64) {
|
||
width := 30
|
||
if total == 0 {
|
||
return
|
||
}
|
||
fillLen := int(float64(used) / float64(total) * float64(width))
|
||
|
||
bar := "["
|
||
for i := 0; i < width; i++ {
|
||
if i < fillLen {
|
||
if i < width/3 {
|
||
bar += colors.Green + "█" + colors.Reset
|
||
} else if i < 2*width/3 {
|
||
bar += colors.Yellow + "█" + colors.Reset
|
||
} else {
|
||
bar += colors.Red + "█" + colors.Reset
|
||
}
|
||
} else {
|
||
bar += colors.Gray + "░" + colors.Reset
|
||
}
|
||
}
|
||
bar += "]"
|
||
|
||
fmt.Printf("%s%sDisk Bar:%s %s\n", colors.Gray, colors.Bold, colors.Reset, bar)
|
||
}
|
||
|
||
func getSystemStats() (*SystemStats, error) {
|
||
stats := &SystemStats{}
|
||
|
||
stats.Hostname, _ = os.Hostname()
|
||
stats.OS = runtime.GOOS
|
||
stats.Platform = runtime.GOARCH
|
||
|
||
stats.CPUModel = getCPUModel()
|
||
stats.CPUCount = runtime.NumCPU()
|
||
|
||
stats.Uptime = getUptime()
|
||
stats.LoadAvg = getLoadAvg()
|
||
|
||
getMemoryStats(stats)
|
||
getDiskStats(stats)
|
||
|
||
stats.TopProcs = getTopProcs(5)
|
||
|
||
return stats, nil
|
||
}
|
||
|
||
func getCPUModel() string {
|
||
if runtime.GOOS == "linux" {
|
||
if data, err := os.ReadFile("/proc/cpuinfo"); err == nil {
|
||
lines := strings.Split(string(data), "\n")
|
||
for _, line := range lines {
|
||
if strings.HasPrefix(line, "model name") || strings.HasPrefix(line, "Processor") || strings.HasPrefix(line, "cpu model") {
|
||
parts := strings.SplitN(line, ":", 2)
|
||
if len(parts) == 2 {
|
||
return strings.TrimSpace(parts[1])
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return "Unknown"
|
||
}
|
||
|
||
func getUptime() string {
|
||
if runtime.GOOS == "linux" {
|
||
if data, err := os.ReadFile("/proc/uptime"); err == nil {
|
||
parts := strings.Split(string(data), " ")
|
||
if len(parts) >= 1 {
|
||
if secs, err := strconv.ParseFloat(strings.TrimSpace(parts[0]), 64); err == nil {
|
||
duration := time.Duration(int64(secs)) * time.Second
|
||
return formatDuration(duration)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return "Unknown"
|
||
}
|
||
|
||
func formatDuration(d time.Duration) string {
|
||
days := int(d.Hours() / 24)
|
||
hours := int(d.Hours()) % 24
|
||
minutes := int(d.Minutes()) % 60
|
||
|
||
if days > 0 {
|
||
return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
|
||
}
|
||
if hours > 0 {
|
||
return fmt.Sprintf("%dh %dm", hours, minutes)
|
||
}
|
||
return fmt.Sprintf("%dm", minutes)
|
||
}
|
||
|
||
func getLoadAvg() string {
|
||
if runtime.GOOS == "linux" {
|
||
if data, err := os.ReadFile("/proc/loadavg"); err == nil {
|
||
parts := strings.Split(strings.TrimSpace(string(data)), " ")
|
||
if len(parts) >= 3 {
|
||
return fmt.Sprintf("%.2f %.2f %.2f",
|
||
parseFloatSafe(parts[0]),
|
||
parseFloatSafe(parts[1]),
|
||
parseFloatSafe(parts[2]))
|
||
}
|
||
}
|
||
}
|
||
return "N/A"
|
||
}
|
||
|
||
func parseFloatSafe(s string) float64 {
|
||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||
return f
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func getMemoryStats(stats *SystemStats) {
|
||
if runtime.GOOS == "linux" {
|
||
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
|
||
lines := strings.Split(string(data), "\n")
|
||
for _, line := range lines {
|
||
if strings.HasPrefix(line, "MemTotal") {
|
||
stats.MemTotal = parseMeminfoLine(line)
|
||
} else if strings.HasPrefix(line, "MemAvailable") || strings.HasPrefix(line, "MemFree") {
|
||
if stats.MemFree == 0 {
|
||
stats.MemFree = parseMeminfoLine(line)
|
||
}
|
||
}
|
||
}
|
||
if stats.MemFree > 0 && stats.MemTotal > 0 {
|
||
stats.MemUsed = stats.MemTotal - stats.MemFree
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func parseMeminfoLine(line string) int64 {
|
||
parts := strings.Fields(line)
|
||
if len(parts) >= 2 {
|
||
if val, err := strconv.ParseInt(parts[1], 10, 64); err == nil {
|
||
return val * 1024
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func getDiskStats(stats *SystemStats) {
|
||
if runtime.GOOS == "linux" {
|
||
output, err := exec.Command("df", "-k", "/").Output()
|
||
if err == nil {
|
||
lines := strings.Split(string(output), "\n")
|
||
if len(lines) >= 2 {
|
||
parts := strings.Fields(lines[1])
|
||
if len(parts) >= 4 {
|
||
stats.DiskTotal, _ = strconv.ParseInt(parts[1], 10, 64)
|
||
stats.DiskUsed, _ = strconv.ParseInt(parts[2], 10, 64)
|
||
stats.DiskFree, _ = strconv.ParseInt(parts[3], 10, 64)
|
||
stats.DiskTotal *= 1024
|
||
stats.DiskUsed *= 1024
|
||
stats.DiskFree *= 1024
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func getTopProcs(count int) []ProcInfo {
|
||
var procs []ProcInfo
|
||
|
||
if runtime.GOOS == "linux" {
|
||
dir, err := os.Open("/proc")
|
||
if err != nil {
|
||
return procs
|
||
}
|
||
defer dir.Close()
|
||
|
||
entries, err := dir.Readdirnames(count * 3)
|
||
if err != nil {
|
||
return procs
|
||
}
|
||
|
||
for _, entry := range entries {
|
||
pid, err := strconv.Atoi(entry)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
stat, err := readProcStat(pid)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
procs = append(procs, stat)
|
||
if len(procs) >= count {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
return procs
|
||
}
|
||
|
||
func readProcStat(pid int) (ProcInfo, error) {
|
||
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
|
||
if err != nil {
|
||
return ProcInfo{}, err
|
||
}
|
||
|
||
parts := strings.SplitN(string(data), " ", 4)
|
||
if len(parts) < 4 {
|
||
return ProcInfo{}, fmt.Errorf("invalid format")
|
||
}
|
||
|
||
name := parts[1]
|
||
name = strings.Trim(name, "(")
|
||
name = strings.TrimRight(name, ")")
|
||
|
||
comm, _ := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
|
||
commName := strings.TrimSpace(string(comm))
|
||
|
||
var rusage syscall.Rusage
|
||
syscall.Getrusage(syscall.RUSAGE_SELF, &rusage)
|
||
|
||
return ProcInfo{
|
||
PID: pid,
|
||
Name: commName,
|
||
CPU: 0,
|
||
Mem: 0,
|
||
Command: name,
|
||
}, nil
|
||
}
|
||
|
||
func truncateString(s string, maxLen int) string {
|
||
if len(s) <= maxLen {
|
||
return s
|
||
}
|
||
return s[:maxLen-3] + "..."
|
||
}
|