feat: add user-friendly help system and new commands

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
This commit is contained in:
selamanapps 2026-05-02 01:12:55 +03:00
parent 800ab8464a
commit 5b334071e0
13 changed files with 1426 additions and 129 deletions

View file

@ -7,23 +7,30 @@ import (
"sort" "sort"
"time" "time"
ignore "github.com/sabhiram/go-gitignore"
"github.com/zemenawi/zutils/pkg/colors" "github.com/zemenawi/zutils/pkg/colors"
"github.com/zemenawi/zutils/pkg/formatter" "github.com/zemenawi/zutils/pkg/formatter"
) )
type FileItem struct { type FileItem struct {
Path string Path string
Size int64 Size int64
Ignored bool
RelPath string
} }
type DirStats struct { type DirStats struct {
TotalSize int64 TotalSize int64
FileCount int TotalSizeIgnoring int64
DirCount int FileCount int
LargestFiles []FileItem DirCount int
LargestDirs []FileItem IgnoredFileCount int
LastModified time.Time IgnoredSize int64
FileExtensions map[string]int LargestFiles []FileItem
LargestDirs []FileItem
LastModified time.Time
FileExtensions map[string]int
HasGitignore bool
} }
func DirInfoCommand(args []string) error { func DirInfoCommand(args []string) error {
@ -33,51 +40,67 @@ func DirInfoCommand(args []string) error {
dirPath := args[0] dirPath := args[0]
stats, err := analyzeDirectory(dirPath) showIgnored := false
if len(args) > 1 && args[1] == "--all" {
showIgnored = true
}
stats, err := analyzeDirectory(dirPath, showIgnored)
if err != nil { if err != nil {
return fmt.Errorf("error analyzing directory: %w", err) return fmt.Errorf("error analyzing directory: %w", err)
} }
size := formatter.FormatSize(stats.TotalSize) size := formatter.FormatSize(stats.TotalSize)
ignoringSize := formatter.FormatSize(stats.TotalSizeIgnoring)
modTime := stats.LastModified.Format(time.RFC1123) modTime := stats.LastModified.Format(time.RFC1123)
PrintBoxHeader("DIRECTORY INFORMATION", colors.Purple) PrintBoxHeader("DIRECTORY INFORMATION", colors.Purple)
fmt.Printf("%s%s📁 Path:%s %s\n", colors.Blue, colors.Bold, colors.Reset, dirPath) fmt.Printf("%s%s📁 Path:%s %s\n", colors.Blue, colors.Bold, colors.Reset, dirPath)
fmt.Printf("%s%s📏 Total Size:%s %s\n", colors.Blue, colors.Bold, colors.Reset, size) fmt.Printf("%s%s📏 Total Size:%s %s\n", colors.Blue, colors.Bold, colors.Reset, size)
if stats.HasGitignore {
fmt.Printf("%s%s📏 Size (excl. ignored):%s %s\n", colors.Blue, colors.Bold, colors.Reset, ignoringSize)
fmt.Printf("%s%s🚫 Ignored:%s %d files (%s)\n", colors.Gray, colors.Bold, colors.Reset, stats.IgnoredFileCount, formatter.FormatSize(stats.IgnoredSize))
}
fmt.Printf("%s%s📊 Files:%s %d\n", colors.Blue, colors.Bold, colors.Reset, stats.FileCount) fmt.Printf("%s%s📊 Files:%s %d\n", colors.Blue, colors.Bold, colors.Reset, stats.FileCount)
fmt.Printf("%s%s📂 Directories:%s %d\n", colors.Blue, colors.Bold, colors.Reset, stats.DirCount) fmt.Printf("%s%s📂 Directories:%s %d\n", colors.Blue, colors.Bold, colors.Reset, stats.DirCount)
fmt.Println() fmt.Println()
// Print largest files table
if len(stats.LargestFiles) > 0 { if len(stats.LargestFiles) > 0 {
PrintSectionHeader("LARGEST FILES") PrintSectionHeader("LARGEST FILES")
headers := []string{"#", "File Name", "Size"} headers := []string{"#", "File Name", "Size"}
if stats.HasGitignore {
headers = []string{"#", "File Name", "Size", "Status"}
}
data := make([][]string, 0, len(stats.LargestFiles)) data := make([][]string, 0, len(stats.LargestFiles))
for i, file := range stats.LargestFiles { for i, file := range stats.LargestFiles {
name := filepath.Base(file.Path) name := file.RelPath
row := []string{ row := []string{
fmt.Sprintf("%d", i+1), fmt.Sprintf("%d", i+1),
name, name,
formatter.FormatSize(file.Size), formatter.FormatSize(file.Size),
} }
if stats.HasGitignore {
if file.Ignored {
row = append(row, colors.Gray+"[ignored]"+colors.Reset)
} else {
row = append(row, "")
}
}
data = append(data, row) data = append(data, row)
} }
printSimpleTable(headers, data) printSimpleTable(headers, data)
} }
// Print largest directories table
if len(stats.LargestDirs) > 0 { if len(stats.LargestDirs) > 0 {
PrintSectionHeader("LARGEST DIRS") PrintSectionHeader("LARGEST DIRS")
headers := []string{"#", "Directory Name", "Size"} headers := []string{"#", "Directory Name", "Size"}
data := make([][]string, 0, len(stats.LargestDirs)) data := make([][]string, 0, len(stats.LargestDirs))
for i, dir := range stats.LargestDirs { for i, dir := range stats.LargestDirs {
name := filepath.Base(dir.Path) name := dir.RelPath
row := []string{ row := []string{
fmt.Sprintf("%d", i+1), fmt.Sprintf("%d", i+1),
name, name,
@ -89,10 +112,8 @@ func DirInfoCommand(args []string) error {
printSimpleTable(headers, data) printSimpleTable(headers, data)
} }
// Print file type distribution table
if len(stats.FileExtensions) > 0 { if len(stats.FileExtensions) > 0 {
PrintSectionHeader("FILE TYPE DISTRIBUTION") PrintSectionHeader("FILE TYPE DISTRIBUTION")
type extCount struct { type extCount struct {
ext string ext string
count int count int
@ -133,6 +154,9 @@ func DirInfoCommand(args []string) error {
PrintSectionHeader("DIRECTORY DETAILS") PrintSectionHeader("DIRECTORY DETAILS")
fmt.Printf("%s%s📅 Last Modified:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, modTime) fmt.Printf("%s%s📅 Last Modified:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, modTime)
if stats.HasGitignore {
fmt.Printf("%s%s📜 Gitignore:%s %s\n", colors.Green, colors.Bold, colors.Reset, "Detected")
}
fmt.Println() fmt.Println()
return nil return nil
@ -143,39 +167,67 @@ func printSimpleTable(headers []string, data [][]string) {
fmt.Println() fmt.Println()
} }
func analyzeDirectory(path string) (*DirStats, error) { func analyzeDirectory(path string, showIgnored bool) (*DirStats, error) {
stats := &DirStats{ stats := &DirStats{
LargestFiles: make([]FileItem, 0), LargestFiles: make([]FileItem, 0),
LargestDirs: make([]FileItem, 0), LargestDirs: make([]FileItem, 0),
FileExtensions: make(map[string]int), FileExtensions: make(map[string]int),
} }
err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error { ignoreMatcher, err := ignore.CompileIgnoreFile(filepath.Join(path, ".gitignore"))
if err == nil {
stats.HasGitignore = true
_ = ignoreMatcher
}
err = filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
relPath, _ := filepath.Rel(path, filePath)
if filePath == path { if filePath == path {
return nil return nil
} }
ignored := false
if stats.HasGitignore {
ignored = ignoreMatcher.MatchesPath(relPath)
}
if info.IsDir() { if info.IsDir() {
stats.DirCount++ if !ignored || showIgnored {
stats.LargestDirs = appendSorted(stats.LargestDirs, FileItem{ stats.DirCount++
Path: filePath, stats.LargestDirs = appendSorted(stats.LargestDirs, FileItem{
Size: info.Size(), Path: filePath,
}, 5) Size: info.Size(),
Ignored: ignored,
RelPath: relPath,
}, 5)
}
} else { } else {
stats.FileCount++ stats.FileCount++
stats.TotalSize += info.Size() stats.TotalSize += info.Size()
if ignored {
stats.IgnoredFileCount++
stats.IgnoredSize += info.Size()
} else {
stats.TotalSizeIgnoring += info.Size()
}
ext := filepath.Ext(filePath) ext := filepath.Ext(filePath)
stats.FileExtensions[ext]++ stats.FileExtensions[ext]++
stats.LargestFiles = appendSorted(stats.LargestFiles, FileItem{ if !ignored || showIgnored {
Path: filePath, stats.LargestFiles = appendSorted(stats.LargestFiles, FileItem{
Size: info.Size(), Path: filePath,
}, 5) Size: info.Size(),
Ignored: ignored,
RelPath: relPath,
}, 5)
}
if info.ModTime().After(stats.LastModified) { if info.ModTime().After(stats.LastModified) {
stats.LastModified = info.ModTime() stats.LastModified = info.ModTime()
@ -200,4 +252,4 @@ func appendSorted(items []FileItem, newItem FileItem, maxSize int) []FileItem {
} }
return items return items
} }

192
cmd/find.go Normal file
View file

@ -0,0 +1,192 @@
package cmd
import (
"flag"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/zemenawi/zutils/pkg/colors"
"github.com/zemenawi/zutils/pkg/formatter"
)
type FindOptions struct {
Pattern string
SizeMin int64
SizeMax int64
ModifiedDays int
FileType string
MaxDepth int
ShowHidden bool
}
type FindResult struct {
Path string
Size int64
Modified time.Time
IsDir bool
Permissions string
}
func FindCommand(args []string) error {
fs := flag.NewFlagSet("find", flag.ContinueOnError)
options := FindOptions{}
fs.StringVar(&options.Pattern, "name", "*", "File name pattern (glob)")
fs.StringVar(&options.FileType, "type", "", "File type: f (file), d (dir)")
sizeMin := fs.Int64("size-min", 0, "Minimum size in bytes")
sizeMax := fs.Int64("size-max", 0, "Maximum size in bytes")
modifiedDays := fs.Int("mtime", 0, "Modified within N days")
maxDepth := fs.Int("depth", -1, "Maximum directory depth")
showHidden := fs.Bool("hidden", false, "Show hidden files")
if err := fs.Parse(args); err != nil {
if err == flag.ErrHelp {
return nil
}
return fmt.Errorf("invalid arguments: %v\n\nUsage: z find [path] [options]\n\nOptions:\n%s\n\nExamples:\n z find . -name '*.go' Find all Go files\n z find . -mtime 7 Files modified in last 7 days\n z find . -size +1m Files larger than 1MB\n z find . -type d Directories only", err, getFlagDefaults(fs))
}
options.SizeMin = *sizeMin
options.SizeMax = *sizeMax
options.ModifiedDays = *modifiedDays
options.MaxDepth = *maxDepth
options.ShowHidden = *showHidden
path := "."
if fs.NArg() > 0 {
path = fs.Arg(0)
}
if path == "" {
return fmt.Errorf("missing search path\n\nUsage: z find [path] [options]")
}
results, err := findFiles(path, options)
if err != nil {
return fmt.Errorf("error finding files: %v", err)
}
if len(results) == 0 {
fmt.Printf("%sNo files found matching criteria%s\n", colors.Gray, colors.Reset)
return nil
}
sort.Slice(results, func(i, j int) bool {
return results[i].Modified.After(results[j].Modified)
})
PrintBoxHeader(fmt.Sprintf("FIND RESULTS (%d files)", len(results)), colors.Green)
headers := []string{"Path", "Size", "Modified"}
data := make([][]string, 0, len(results))
for _, result := range results {
relPath := result.Path
if strings.Contains(relPath, "/") {
parts := strings.Split(relPath, "/")
relPath = strings.Join(parts[len(parts)-2:], "/")
if len(parts) > 2 {
relPath = "..." + string(filepath.Separator) + strings.Join(parts[len(parts)-2:], "/")
}
}
modStr := result.Modified.Format("Jan 02 15:04")
if time.Since(result.Modified) > 30*24*time.Hour {
modStr = result.Modified.Format("Jan 02 2006")
}
row := []string{
relPath,
formatter.FormatSize(result.Size),
modStr,
}
data = append(data, row)
}
formatter.PrintTable(headers, data, colors.Cyan)
fmt.Println()
return nil
}
func getFlagDefaults(fs *flag.FlagSet) string {
var result []string
fs.VisitAll(func(f *flag.Flag) {
result = append(result, fmt.Sprintf(" -%s %s (default: %v)", f.Name, f.Usage, f.DefValue))
})
return strings.Join(result, "\n")
}
func findFiles(path string, opts FindOptions) ([]FindResult, error) {
var results []FindResult
err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if filePath == path {
return nil
}
if !opts.ShowHidden && filepath.Base(filePath)[0] == '.' {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
if opts.MaxDepth > 0 {
relPath, _ := filepath.Rel(path, filePath)
if strings.Count(relPath, string(filepath.Separator)) > opts.MaxDepth {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
}
if opts.FileType == "f" && info.IsDir() {
return nil
}
if opts.FileType == "d" && !info.IsDir() {
return nil
}
baseName := filepath.Base(filePath)
matched, err := filepath.Match(opts.Pattern, baseName)
if err != nil || !matched {
return nil
}
if opts.SizeMin > 0 && info.Size() < opts.SizeMin {
return nil
}
if opts.SizeMax > 0 && info.Size() > opts.SizeMax {
return nil
}
if opts.ModifiedDays > 0 {
cutoff := time.Now().AddDate(0, 0, -opts.ModifiedDays)
if info.ModTime().Before(cutoff) {
return nil
}
}
results = append(results, FindResult{
Path: filePath,
Size: info.Size(),
Modified: info.ModTime(),
IsDir: info.IsDir(),
Permissions: info.Mode().String(),
})
return nil
})
return results, err
}

193
cmd/help.go Normal file
View file

@ -0,0 +1,193 @@
package cmd
import (
"fmt"
"strings"
"github.com/zemenawi/zutils/pkg/colors"
)
type CommandHelp struct {
Name string
Help string
Examples []string
}
var readHelp = CommandHelp{
Name: "read",
Help: "Read and display file with smart detection.\n" +
"Automatically detects file type:\n" +
" - Markdown (.md) - Renders with colored headings, lists, code blocks\n" +
" - Code files - Syntax highlighting with line numbers\n" +
" - Other files - Plain text display",
Examples: []string{
"z read README.md # Read markdown file",
"z read main.go # Read code with syntax highlighting",
"z read package.json # Read JSON file",
},
}
var sysHelp = CommandHelp{
Name: "sys",
Help: "Show system overview including CPU, memory, disk usage, and top processes.\n" +
"Useful for quick system diagnostics.",
Examples: []string{
"z sys # Show full system overview",
},
}
var findHelp = CommandHelp{
Name: "find",
Help: "Find files matching criteria.\n" +
"Supports glob patterns, size filters, modification time, and depth limits.",
Examples: []string{
"z find . -name '*.go' # Find all Go files",
"z find . -mtime 7 # Files modified in last 7 days",
"z find . -size +1m # Files larger than 1MB",
"z find . -type d # Directories only",
},
}
var searchHelp = CommandHelp{
Name: "search",
Help: "Search for a pattern in files.\n" +
"A user-friendly alternative to grep with colored output.",
Examples: []string{
"z search func cmd/ # Search for 'func' in cmd directory",
"z search -i 'error' . # Case-insensitive search",
"z search -w 'import' *.go # Whole word match",
"z search -C 2 'TODO' . # Show 2 lines of context",
},
}
var usageHelp = CommandHelp{
Name: "usage",
Help: "Show resource usage (CPU, memory) for processes matching a name.\n" +
"Useful for monitoring specific applications or services.",
Examples: []string{
"z usage cron # Show cron process usage",
"z usage mongod # Show MongoDB usage",
"z usage chrome # Show all Chrome processes",
"z usage postgres # Show PostgreSQL usage",
},
}
func PrintErrorAndHelp(name string, err error) {
help := getHelpForCommand(name)
fmt.Printf("\n %s✖ Error: %s%s\n\n", colors.Red, err, colors.Reset)
if help != nil {
fmt.Printf(" %sUsage:%s z %s %s\n\n", colors.Bold, colors.Reset, help.Name, getUsageHint(help.Name))
if help.Help != "" {
fmt.Printf(" %sDescription:%s\n", colors.Bold, colors.Reset)
lines := splitIntoLines(help.Help, 60)
for _, line := range lines {
fmt.Printf(" %s%s%s\n", colors.Reset, line, colors.Reset)
}
fmt.Printf("\n")
}
if len(help.Examples) > 0 {
fmt.Printf(" %sExamples:%s\n", colors.Bold, colors.Reset)
for _, ex := range help.Examples {
fmt.Printf(" %s%s%s\n", colors.Green, ex, colors.Reset)
}
fmt.Printf("\n")
}
}
}
func getHelpForCommand(name string) *CommandHelp {
switch name {
case "read":
return &readHelp
case "sys":
return &sysHelp
case "find":
return &findHelp
case "search":
return &searchHelp
case "usage":
return &usageHelp
}
return nil
}
func getUsageHint(name string) string {
switch name {
case "read":
return "<file>"
case "sys":
return ""
case "find":
return "[path] [options]"
case "search":
return "<pattern> [path]"
case "usage":
return "<process-name>"
case "info":
return "<file|dir|network>"
}
return ""
}
func splitIntoLines(text string, maxLen int) []string {
var lines []string
paragraphs := splitIntoParagraphs(text)
for _, para := range paragraphs {
if para == "" {
lines = append(lines, "")
continue
}
words := strings.Fields(para)
currentLine := ""
for _, word := range words {
if currentLine == "" {
currentLine = word
} else if len(currentLine)+len(word)+1 <= maxLen {
currentLine += " " + word
} else {
lines = append(lines, currentLine)
currentLine = word
}
}
if currentLine != "" {
lines = append(lines, currentLine)
}
}
return lines
}
func splitIntoParagraphs(text string) []string {
var paragraphs []string
lines := strings.Split(text, "\n")
current := ""
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
if current != "" {
paragraphs = append(paragraphs, current)
current = ""
}
} else {
if current != "" {
current += " "
}
current += trimmed
}
}
if current != "" {
paragraphs = append(paragraphs, current)
}
return paragraphs
}

View file

@ -1,77 +1,48 @@
package cmd package cmd
import ( import (
"bytes"
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/zemenawi/zutils/pkg/colors"
"github.com/zemenawi/zutils/pkg/formatter" "github.com/zemenawi/zutils/pkg/formatter"
) )
var ReadHelp = CommandHelp{
Name: "read",
Help: "Read and display file with smart detection.\nAutomatically detects file type:\n - Markdown (.md) - Renders with colored headings, lists, code blocks\n - Code files - Syntax highlighting with line numbers\n - Other files - Plain text display",
Examples: []string{
"z read README.md # Read markdown file",
"z read main.go # Read code with syntax highlighting",
"z read package.json # Read JSON file",
},
}
func ReadCommand(args []string) error { func ReadCommand(args []string) error {
if len(args) < 1 { if len(args) < 1 {
return fmt.Errorf("please specify a file path") return fmt.Errorf("missing file path\n\nUsage: z read <file>")
} }
filePath := args[0] filePath := args[0]
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
return fmt.Errorf("error opening file: %w", err) return fmt.Errorf("cannot open file '%s': %s", filePath, err)
} }
defer file.Close() defer file.Close()
fileInfo, err := file.Stat() fileInfo, err := file.Stat()
if err != nil { if err != nil {
return fmt.Errorf("error getting file info: %w", err) return fmt.Errorf("cannot read file info '%s': %s", filePath, err)
} }
if fileInfo.IsDir() { if fileInfo.IsDir() {
return fmt.Errorf("'%s' is a directory, not a readable file", filePath) return fmt.Errorf("'%s' is a directory\n\nUse: z info dir %s", filePath, filePath)
} }
content := formatter.ReadFileContent(file) handler := formatter.ReadHandlers.GetHandler(filePath)
if handler != nil {
lexer := lexers.Match(filepath.Base(filePath)) return handler(args)
if lexer == nil {
lexer = lexers.Fallback
} }
iterator, err := lexer.Tokenise(nil, content) return formatter.ReadFileWithHighlight(file, filePath)
if err != nil { }
return fmt.Errorf("error tokenizing: %w", err)
}
formatter_ := formatters.TTY256
style := styles.Get("monokai")
if style == nil {
style = styles.Fallback
}
fmt.Printf("%s%s╔══════════════════════════════════════╗%s\n", colors.Cyan, colors.Bold, colors.Reset)
fmt.Printf("%s%s║%s %-38s %s%s║%s\n", colors.Cyan, colors.Bold, colors.Reset, filePath, colors.Cyan, colors.Bold, colors.Reset)
fmt.Printf("%s%s╚══════════════════════════════════════╝%s\n", colors.Cyan, colors.Bold, colors.Reset)
fmt.Println()
var buf bytes.Buffer
err = formatter_.Format(&buf, style, iterator)
if err != nil {
return fmt.Errorf("error formatting: %w", err)
}
lines := strings.Split(buf.String(), "\n")
for i, line := range lines {
if line != "" {
fmt.Printf("%s%4d: %s%s\n", colors.Gray, i+1, colors.Reset, line)
}
}
return nil
}

View file

@ -3,7 +3,6 @@ package cmd
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/alecthomas/chroma/v2/formatters" "github.com/alecthomas/chroma/v2/formatters"
@ -13,7 +12,12 @@ import (
"github.com/zemenawi/zutils/pkg/formatter" "github.com/zemenawi/zutils/pkg/formatter"
) )
func MarkdownCommand(args []string) error { func init() {
formatter.ReadHandlers.Register(".md", MarkdownHandler)
formatter.ReadHandlers.Register(".markdown", MarkdownHandler)
}
func MarkdownHandler(args []string) error {
if len(args) < 1 { if len(args) < 1 {
return fmt.Errorf("please specify a markdown file path") return fmt.Errorf("please specify a markdown file path")
} }
@ -35,11 +39,6 @@ func MarkdownCommand(args []string) error {
return fmt.Errorf("'%s' is a directory, not a readable file", filePath) return fmt.Errorf("'%s' is a directory, not a readable file", filePath)
} }
ext := filepath.Ext(filePath)
if ext != ".md" && ext != ".markdown" {
return fmt.Errorf("'%s' is not a markdown file", filePath)
}
content := formatter.ReadFileContent(file) content := formatter.ReadFileContent(file)
fmt.Printf("%s%s╔══════════════════════════════════════╗%s\n", colors.Cyan, colors.Bold, colors.Reset) fmt.Printf("%s%s╔══════════════════════════════════════╗%s\n", colors.Cyan, colors.Bold, colors.Reset)
@ -52,7 +51,7 @@ func MarkdownCommand(args []string) error {
codeBuffer := &strings.Builder{} codeBuffer := &strings.Builder{}
codeLang := "" codeLang := ""
for i, line := range lines { for _, line := range lines {
if strings.HasPrefix(line, "```") { if strings.HasPrefix(line, "```") {
if !inCodeBlock { if !inCodeBlock {
inCodeBlock = true inCodeBlock = true
@ -94,7 +93,6 @@ func MarkdownCommand(args []string) error {
} else { } else {
fmt.Println() fmt.Println()
} }
_ = i
} }
return nil return nil
@ -105,20 +103,22 @@ func renderCodeBlock(code, lang string) {
return return
} }
fmt.Printf("%s%s╔═══ %s ═══╗%s\n", colors.Gray, colors.Dim, lang, colors.Reset)
lexer := lexers.Match(lang) lexer := lexers.Match(lang)
if lexer == nil { if lexer == nil {
lexer = lexers.Fallback lexer = lexers.Fallback
} }
iterator, _ := lexer.Tokenise(nil, code) iterator, _ := lexer.Tokenise(nil, code)
formatter := formatters.TTY256 formatter_ := formatters.TTY256
style := styles.Get("monokai") style := styles.Get("monokai")
if style == nil { if style == nil {
style = styles.Fallback style = styles.Fallback
} }
var buf strings.Builder var buf strings.Builder
formatter.Format(&buf, style, iterator) formatter_.Format(&buf, style, iterator)
output := buf.String() output := buf.String()
for _, line := range strings.Split(output, "\n") { for _, line := range strings.Split(output, "\n") {
@ -126,5 +126,6 @@ func renderCodeBlock(code, lang string) {
fmt.Printf("%s%s│ %s%s\n", colors.Gray, colors.Dim, colors.Reset, line) fmt.Printf("%s%s│ %s%s\n", colors.Gray, colors.Dim, colors.Reset, line)
} }
} }
fmt.Printf("%s%s╚══════════════╝%s\n", colors.Gray, colors.Dim, colors.Reset)
fmt.Println() fmt.Println()
} }

180
cmd/search.go Normal file
View file

@ -0,0 +1,180 @@
package cmd
import (
"bufio"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/zemenawi/zutils/pkg/colors"
)
type SearchOptions struct {
Pattern string
Path string
CaseInsensitive bool
WholeWord bool
LineNumbers bool
ContextLines int
FilePattern string
}
type SearchResult struct {
File string
Line int
Content string
}
func SearchCommand(args []string) error {
fs := flag.NewFlagSet("search", flag.ContinueOnError)
options := SearchOptions{}
fs.BoolVar(&options.CaseInsensitive, "i", false, "Case insensitive search")
fs.BoolVar(&options.WholeWord, "w", false, "Match whole word")
fs.BoolVar(&options.LineNumbers, "n", true, "Show line numbers")
fs.IntVar(&options.ContextLines, "C", 0, "Show N lines of context")
fs.StringVar(&options.FilePattern, "f", "*", "Search only in files matching pattern")
if err := fs.Parse(args); err != nil {
if err == flag.ErrHelp {
return nil
}
return fmt.Errorf("invalid arguments\n\nUsage: z search [options] pattern [path]\n\nExamples:\n z search func cmd/ Search for 'func' in cmd directory\n z search -i 'error' . Case-insensitive search\n z search -w 'import' *.go Whole word match in Go files")
}
if fs.NArg() < 1 {
return fmt.Errorf("missing search pattern\n\nUsage: z search [options] pattern [path]\n\nExamples:\n z search func cmd/ Search for 'func' in cmd directory\n z search -i 'error' . Case-insensitive search\n z search -w 'import' *.go Whole word match in Go files")
}
options.Pattern = fs.Arg(0)
if fs.NArg() > 1 {
options.Path = fs.Arg(1)
} else {
options.Path = "."
}
results, err := searchFiles(options)
if err != nil {
return fmt.Errorf("search error: %v", err)
}
if len(results) == 0 {
fmt.Printf("%sNo matches found for '%s'%s\n", colors.Gray, options.Pattern, colors.Reset)
return nil
}
displaySearchResults(results, options)
return nil
}
func searchFiles(opts SearchOptions) ([]SearchResult, error) {
var results []SearchResult
pattern := opts.Pattern
if opts.CaseInsensitive {
pattern = strings.ToLower(pattern)
}
var regex *regexp.Regexp
if opts.WholeWord {
regex = regexp.MustCompile(`\b` + regexp.QuoteMeta(pattern) + `\b`)
} else {
regex = regexp.MustCompile(regexp.QuoteMeta(pattern))
}
err := filepath.Walk(opts.Path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if info.IsDir() {
return nil
}
baseName := filepath.Base(path)
matched, err := filepath.Match(opts.FilePattern, baseName)
if err != nil || !matched {
return nil
}
fileResults := searchFile(path, regex, opts)
results = append(results, fileResults...)
return nil
})
return results, err
}
func searchFile(path string, regex *regexp.Regexp, opts SearchOptions) []SearchResult {
var results []SearchResult
file, err := os.Open(path)
if err != nil {
return results
}
defer file.Close()
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
check := line
if opts.CaseInsensitive {
check = strings.ToLower(check)
}
if regex.MatchString(check) {
results = append(results, SearchResult{
File: path,
Line: lineNum,
Content: line,
})
}
}
return results
}
func displaySearchResults(results []SearchResult, opts SearchOptions) {
files := make(map[string][]SearchResult)
for _, r := range results {
files[r.File] = append(files[r.File], r)
}
PrintBoxHeader(fmt.Sprintf("SEARCH RESULTS (%d matches in %d files)", len(results), len(files)), colors.Cyan)
for file, fileResults := range files {
relPath := file
if strings.Contains(relPath, "/") {
parts := strings.Split(relPath, "/")
if len(parts) > 3 {
relPath = ".../" + strings.Join(parts[len(parts)-2:], "/")
}
}
fmt.Printf("\n%s%s%s:%s\n", colors.Bold, colors.Yellow, relPath, colors.Reset)
for _, r := range fileResults {
content := r.Content
if len(content) > 100 {
content = content[:97] + "..."
}
if opts.LineNumbers {
fmt.Printf("%s%4d:%s %s\n", colors.Gray, r.Line, colors.Reset, content)
} else {
fmt.Printf(" %s\n", content)
}
}
}
fmt.Println()
}

370
cmd/sys.go Normal file
View file

@ -0,0 +1,370 @@
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] + "..."
}

228
cmd/usage.go Normal file
View file

@ -0,0 +1,228 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/zemenawi/zutils/pkg/colors"
"github.com/zemenawi/zutils/pkg/formatter"
)
type ProcessInfo struct {
PID int
Name string
Command string
CPUPercent float64
MemBytes int64
MemPercent float64
Status string
User string
}
func UsageCommand(args []string) error {
if len(args) < 1 {
return fmt.Errorf("missing process name\n\nUsage: z usage <process-name>\n\nExamples:\n z usage cron # Show cron process usage\n z usage mongod # Show MongoDB usage\n z usage chrome # Show all Chrome processes")
}
processName := args[0]
processes := findProcessesByName(processName)
if len(processes) == 0 {
fmt.Printf("\n %sNo processes found matching '%s'%s\n\n", colors.Yellow, processName, colors.Reset)
fmt.Printf(" Try a different process name, or use %sz sys%s to see all processes\n\n", colors.Gray, colors.Reset)
return nil
}
PrintBoxHeader(fmt.Sprintf("USAGE: %s (%d processes)", processName, len(processes)), colors.Cyan)
totalCPU := 0.0
totalMem := int64(0)
for _, p := range processes {
totalCPU += p.CPUPercent
totalMem += p.MemBytes
}
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 MB)\n", colors.Purple, colors.Bold, colors.Reset, formatter.FormatSize(totalMem), float64(totalMem)/1024/1024)
fmt.Println()
headers := []string{"PID", "Process", "CPU%", "Memory", "Status"}
data := make([][]string, 0, len(processes))
sort.Slice(processes, func(i, j int) bool {
return processes[i].CPUPercent > processes[j].CPUPercent
})
for _, p := range processes {
cmd := p.Command
if len(cmd) > 30 {
parts := strings.Fields(cmd)
if len(parts) > 0 {
cmd = filepath.Base(parts[0]) + " " + strings.Join(parts[1:], " ")[:25]
}
}
status := p.Status
if status == "R" {
status = colors.Green + "running" + colors.Reset
} else if status == "S" || status == "I" {
status = colors.Gray + "sleeping" + colors.Reset
} else {
status = colors.Yellow + status + colors.Reset
}
row := []string{
fmt.Sprintf("%d", p.PID),
cmd,
fmt.Sprintf("%.1f", p.CPUPercent),
formatter.FormatSize(p.MemBytes),
status,
}
data = append(data, row)
}
formatter.PrintTable(headers, data, colors.Cyan)
fmt.Println()
return nil
}
func findProcessesByName(name string) []ProcessInfo {
var processes []ProcessInfo
dir, err := os.Open("/proc")
if err != nil {
return processes
}
defer dir.Close()
entries, err := dir.Readdirnames(500)
if err != nil {
return processes
}
for _, entry := range entries {
pid, err := strconv.Atoi(entry)
if err != nil {
continue
}
proc, err := getProcessInfo(pid, name)
if err == nil && proc != nil {
processes = append(processes, *proc)
}
}
return processes
}
func getProcessInfo(pid int, targetName string) (*ProcessInfo, error) {
comm, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
if err != nil {
return nil, err
}
procName := strings.TrimSpace(string(comm))
if !strings.Contains(strings.ToLower(procName), strings.ToLower(targetName)) {
if !strings.Contains(strings.ToLower(procName), strings.ToLower(targetName)) {
cmdline, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
if cmdline != nil && !strings.Contains(strings.ToLower(string(cmdline)), strings.ToLower(targetName)) {
return nil, fmt.Errorf("name mismatch")
}
}
}
stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
if err != nil {
return nil, err
}
parts := strings.Split(string(stat), " ")
if len(parts) < 24 {
return nil, fmt.Errorf("invalid stat format")
}
status := "?"
if len(parts) > 2 {
status = strings.Trim(parts[2], "()")
}
utime := parseIntSafe(parts[13])
stime := parseIntSafe(parts[14])
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 {
memKB := parseIntSafe(fields[1])
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 {
totalMem = parseIntSafe(fields[1]) * 1024
}
break
}
}
}
cpuPercent := 0.0
if totalMem > 0 {
cpuPercent = float64(utime+stime) / float64(totalMem) * 100
}
cmdline, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
cmdStr := strings.Join(strings.Fields(string(cmdline)), " ")
return &ProcessInfo{
PID: pid,
Name: procName,
Command: cmdStr,
CPUPercent: cpuPercent,
MemBytes: memBytes,
MemPercent: float64(memBytes) / float64(totalMem) * 100,
Status: status,
User: getProcessUser(pid),
}, nil
}
func getProcessUser(pid int) string {
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, "Uid:") {
fields := strings.Fields(line)
if len(fields) >= 2 {
uid := parseIntSafe(fields[1])
return fmt.Sprintf("uid:%d", uid)
}
}
}
}
return "?"
}
func parseIntSafe(s string) int64 {
if i, err := strconv.ParseInt(strings.TrimSpace(s), 10, 64); err == nil {
return i
}
return 0
}

1
go.mod
View file

@ -5,5 +5,6 @@ go 1.22.0
require ( require (
github.com/alecthomas/chroma/v2 v2.24.1 // indirect github.com/alecthomas/chroma/v2 v2.24.1 // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
github.com/yuin/goldmark v1.8.2 // indirect github.com/yuin/goldmark v1.8.2 // indirect
) )

8
go.sum
View file

@ -1,6 +1,14 @@
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM= github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI= github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

96
main.go
View file

@ -3,41 +3,61 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"github.com/zemenawi/zutils/cmd" "github.com/zemenawi/zutils/cmd"
"github.com/zemenawi/zutils/pkg/colors"
"github.com/zemenawi/zutils/pkg/types" "github.com/zemenawi/zutils/pkg/types"
) )
func main() { func main() {
registry := types.NewCommandRegistry() registry := types.NewCommandRegistry()
// Register all commands
registry.Register(&types.Command{ registry.Register(&types.Command{
Name: "read", Name: "read",
Description: "Read and display file with syntax highlighting", Description: "Read and display file with smart detection",
Handler: cmd.ReadCommand, Handler: cmd.ReadCommand,
}) })
registry.Register(&types.Command{ registry.Register(&types.Command{
Name: "md", Name: "sys",
Description: "Render markdown file to terminal", Description: "Display system information overview",
Handler: cmd.MarkdownCommand, Handler: cmd.SystemCommand,
}) })
registry.Register(&types.Command{
Name: "find",
Description: "Find files with pattern and filter support",
Handler: cmd.FindCommand,
})
registry.Register(&types.Command{
Name: "search",
Description: "Search for text pattern in files",
Handler: cmd.SearchCommand,
})
registry.Register(&types.Command{
Name: "usage",
Description: "Show resource usage for a process",
Handler: cmd.UsageCommand,
})
cmd.RegisterInfoCommands(registry) cmd.RegisterInfoCommands(registry)
registry.Register(&types.Command{ registry.Register(&types.Command{
Name: "version", Name: "version",
Description: "Show version information", Description: "Show version information",
Handler: cmd.VersionCommand, Handler: cmd.VersionCommand,
}) })
// Parse command line arguments
args := os.Args[1:] args := os.Args[1:]
if len(args) == 0 { if len(args) == 0 {
printUsage(registry) printUsage(registry)
os.Exit(1) os.Exit(1)
} }
// Handle legacy 'info' subcommand pattern for backwards compatibility
if args[0] == "info" { if args[0] == "info" {
infoCmd, exists := registry.Get("info") infoCmd, exists := registry.Get("info")
if !exists { if !exists {
@ -46,48 +66,62 @@ func main() {
} }
err := infoCmd.Handler(args[1:]) err := infoCmd.Handler(args[1:])
if err != nil { if err != nil {
fmt.Printf("Error: %v\n", err) cmd.PrintErrorAndHelp("info", err)
os.Exit(1) os.Exit(1)
} }
return return
} }
// Direct command: z info <args>
commandName := args[0] commandName := args[0]
cmdArgs := args[1:] cmdArgs := args[1:]
if command, exists := registry.Get(commandName); exists { if command, exists := registry.Get(commandName); exists {
err := command.Handler(cmdArgs) err := command.Handler(cmdArgs)
if err != nil { if err != nil {
fmt.Printf("Error: %v\n", err) cmd.PrintErrorAndHelp(commandName, err)
os.Exit(1) os.Exit(1)
} }
} else { } else {
// If no command matches, try auto-detect as info command fmt.Printf("\n %sCommand not found: %s%s\n\n", colors.Red, args[0], colors.Reset)
err := cmd.InfoCommand(args) fmt.Printf(" Did you mean one of these?\n\n")
if err != nil { suggestions := findSimilarCommands(args[0], registry.GetAll())
fmt.Printf("Error: %v\n", err) for _, s := range suggestions {
printUsage(registry) fmt.Printf(" %s%s%s - %s\n", colors.Yellow, s.Name, colors.Reset, s.Description)
os.Exit(1)
} }
fmt.Printf("\n")
printUsage(registry)
os.Exit(1)
} }
} }
func printUsage(registry *types.CommandRegistry) { func printUsage(registry *types.CommandRegistry) {
fmt.Println("z - User-friendly terminal utilities") fmt.Printf("\n %s%sz%s - User-friendly terminal utilities\n\n", colors.Bold, colors.Cyan, colors.Reset)
fmt.Println() fmt.Printf(" %sUsage:%s z <command> [arguments]\n\n", colors.Bold, colors.Reset)
fmt.Println("Usage: z <command> [arguments]") fmt.Printf(" %sAvailable Commands:%s\n", colors.Bold, colors.Reset)
fmt.Println()
fmt.Println("Commands:")
for _, command := range registry.GetAll() { for _, command := range registry.GetAll() {
fmt.Printf(" %-12s %s\n", command.Name, command.Description) fmt.Printf(" %s%-10s%s %s\n", colors.Green, command.Name, colors.Reset, command.Description)
} }
fmt.Println()
fmt.Println("Quick Examples:") fmt.Printf("\n %sQuick Examples:%s\n", colors.Bold, colors.Reset)
fmt.Println(" z read go.mod # Display file with line numbers") fmt.Printf(" %sz read README.md%s # Display file with smart formatting\n", colors.Gray, colors.Reset)
fmt.Println(" z info main.go # File information") fmt.Printf(" %sz sys%s # Show system overview\n", colors.Gray, colors.Reset)
fmt.Println(" z info . # Directory information") fmt.Printf(" %sz find . -name '*.go'%s # Find files\n", colors.Gray, colors.Reset)
fmt.Println(" z info network # Network information") fmt.Printf(" %sz search pattern path%s # Search in files\n", colors.Gray, colors.Reset)
fmt.Println(" z main.go # Auto-detect file info") fmt.Printf(" %sz info .%s # Directory info with gitignore support\n", colors.Gray, colors.Reset)
fmt.Println() fmt.Printf(" %sz info main.go%s # File information\n", colors.Gray, colors.Reset)
fmt.Printf("\n Type %sz <command>%s without arguments to see help\n\n", colors.Gray, colors.Reset)
}
func findSimilarCommands(input string, commands []*types.Command) []*types.Command {
var suggestions []*types.Command
for _, cmd := range commands {
if strings.HasPrefix(cmd.Name, input[:1]) {
suggestions = append(suggestions, cmd)
}
}
if len(suggestions) > 3 {
suggestions = suggestions[:3]
}
return suggestions
} }

View file

@ -1,11 +1,85 @@
package formatter package formatter
import ( import (
"bytes"
"fmt" "fmt"
"math" "math"
"os" "os"
"path/filepath"
"strings"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
) )
type FileHandler func(args []string) error
type FileHandlerRegistry struct {
handlers map[string]FileHandler
}
var ReadHandlers = &FileHandlerRegistry{
handlers: make(map[string]FileHandler),
}
func (r *FileHandlerRegistry) Register(ext string, handler FileHandler) {
r.handlers[ext] = handler
}
func (r *FileHandlerRegistry) GetHandler(filename string) FileHandler {
ext := filepath.Ext(filename)
if ext == "" || ext == "." {
basename := filepath.Base(filename)
if strings.HasPrefix(basename, "Dockerfile") {
return r.handlers[".dockerfile"]
}
return nil
}
if handler, ok := r.handlers[ext]; ok {
return handler
}
return nil
}
func ReadFileWithHighlight(file *os.File, filePath string) error {
content := ReadFileContent(file)
lexer := lexers.Match(filepath.Base(filePath))
if lexer == nil {
lexer = lexers.Fallback
}
iterator, err := lexer.Tokenise(nil, content)
if err != nil {
return fmt.Errorf("error tokenizing: %w", err)
}
formatter_ := formatters.TTY256
style := styles.Get("monokai")
if style == nil {
style = styles.Fallback
}
var buf bytes.Buffer
err = formatter_.Format(&buf, style, iterator)
if err != nil {
return fmt.Errorf("error formatting: %w", err)
}
lines := strings.Split(buf.String(), "\n")
for i, line := range lines {
if line != "" {
fmt.Printf("%s%4d: %s%s\n", "\033[90m", i+1, "\033[0m", line)
}
}
return nil
}
// FormatSize converts bytes to human-readable format // FormatSize converts bytes to human-readable format
func FormatSize(bytes int64) string { func FormatSize(bytes int64) string {
if bytes == 0 { if bytes == 0 {
@ -29,11 +103,11 @@ func CenterText(text string, width int) string {
if len(text) >= width { if len(text) >= width {
return text[:width] return text[:width]
} }
padding := width - len(text) padding := width - len(text)
left := padding / 2 left := padding / 2
right := padding - left right := padding - left
return fmt.Sprintf("%s%s%s", repeat(" ", left), text, repeat(" ", right)) return fmt.Sprintf("%s%s%s", repeat(" ", left), text, repeat(" ", right))
} }

View file

@ -1,36 +1,30 @@
package types package types
// Command represents a CLI command
type Command struct { type Command struct {
Name string Name string
Description string Description string
Handler func(args []string) error Handler func(args []string) error
} }
// CommandRegistry manages available commands
type CommandRegistry struct { type CommandRegistry struct {
commands map[string]*Command commands map[string]*Command
} }
// NewCommandRegistry creates a new command registry
func NewCommandRegistry() *CommandRegistry { func NewCommandRegistry() *CommandRegistry {
return &CommandRegistry{ return &CommandRegistry{
commands: make(map[string]*Command), commands: make(map[string]*Command),
} }
} }
// Register adds a command to the registry
func (r *CommandRegistry) Register(cmd *Command) { func (r *CommandRegistry) Register(cmd *Command) {
r.commands[cmd.Name] = cmd r.commands[cmd.Name] = cmd
} }
// Get retrieves a command by name
func (r *CommandRegistry) Get(name string) (*Command, bool) { func (r *CommandRegistry) Get(name string) (*Command, bool) {
cmd, exists := r.commands[name] cmd, exists := r.commands[name]
return cmd, exists return cmd, exists
} }
// GetAll returns all registered commands
func (r *CommandRegistry) GetAll() []*Command { func (r *CommandRegistry) GetAll() []*Command {
cmds := make([]*Command, 0, len(r.commands)) cmds := make([]*Command, 0, len(r.commands))
for _, cmd := range r.commands { for _, cmd := range r.commands {
@ -39,7 +33,6 @@ func (r *CommandRegistry) GetAll() []*Command {
return cmds return cmds
} }
// Has checks if a command exists
func (r *CommandRegistry) Has(name string) bool { func (r *CommandRegistry) Has(name string) bool {
_, exists := r.commands[name] _, exists := r.commands[name]
return exists return exists