commit aeae34365cfc4952bad922a8ed0d2b11a41a6d01 Author: selamanapps Date: Sat May 2 00:05:08 2026 +0300 feat: add zutils v0.2.0 with professional table formatting - Add directory information with Unicode table borders - Add network information with professional formatting - Add version command - Add comprehensive documentation (README.md, DEVELOPMENT.md) - Improve table output with proper borders and alignment - Add project structure (cmd/, pkg/ directories) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01f116e --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +zutils +zutils-* + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Dependency directories +vendor/ + +# Go workspace file +go.work + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..442819b --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,394 @@ +# zutils Development Guide + +## Quick Start + +1. **Build the project:** + ```bash + go build -o zutils + ``` + +2. **Test locally:** + ```bash + ./zutils info main.go + ``` + +3. **Install to system:** + ```bash + ./install.sh + ``` + +## Project Architecture + +### Directory Structure + +``` +zutils/ +├── cmd/ # Command implementations +│ ├── info.go # Main info command router +│ ├── file.go # File info handler +│ ├── dir.go # Directory info handler +│ ├── network.go # Network info handler +│ └── autodetect.go # Auto-detection logic +├── pkg/ # Shared packages +│ ├── colors/ # ANSI color constants +│ ├── formatter/ # Output formatting utilities +│ └── types/ # Command registry and types +├── examples/ # Example files for testing +├── main.go # Application entry point +└── go.mod # Go module definition +``` + +### How It Works + +1. **Entry Point (`main.go`):** + - Creates a command registry + - Registers all available commands + - Parses command-line arguments + - Routes to appropriate command handler + +2. **Command Registry (`pkg/types/registry.go`):** + - Manages all available commands + - Provides registration and lookup functionality + - Makes it easy to add new commands + +3. **Command Handlers (`cmd/`):** + - Each command has its own file + - Implements the handler function + - Uses shared utilities for output formatting + +4. **Shared Utilities (`pkg/`):** + - `colors/`: ANSI color codes for terminal output + - `formatter/`: Utility functions for formatting output + - `types/`: Command registry and type definitions + +## Adding a New Command + +### Step 1: Create the Command File + +Create a new file in `cmd/` directory: + +```go +// cmd/hello.go +package cmd + +import ( + "fmt" + "github.com/zemenawi/zutils/pkg/colors" +) + +func HelloCommand(args []string) error { + fmt.Printf("%s%sHello, World!%s\n", colors.Green, colors.Bold, colors.Reset) + return nil +} +``` + +### Step 2: Register the Command + +Add registration in `main.go`: + +```go +// After existing registrations +registry.Register(&types.Command{ + Name: "hello", + Description: "Print a greeting", + Handler: cmd.HelloCommand, +}) +``` + +### Step 3: Test Your Command + +```bash +go build -o zutils +./zutils hello +``` + +## Best Practices + +### 1. Use Shared Colors + +```go +import "github.com/zemenawi/zutils/pkg/colors" + +fmt.Printf("%sSuccess message%s\n", colors.Green, colors.Reset) +fmt.Printf("%sWarning message%s\n", colors.Yellow, colors.Reset) +fmt.Printf("%sError message%s\n", colors.Red, colors.Reset) +``` + +### 2. Use Formatter Utilities + +```go +import "github.com/zemenawi/zutils/pkg/formatter" + +size := formatter.FormatSize(1234567) // "1.18 MB" +``` + +### 3. Print Box Headers + +```go +cmd.PrintBoxHeader("MY HEADER", colors.Cyan) +``` + +### 4. Print Section Headers + +```go +cmd.PrintSectionHeader("MY SECTION") +``` + +### 5. Return Errors + +Always return errors from command handlers: + +```go +func MyCommand(args []string) error { + if len(args) < 1 { + return fmt.Errorf("missing argument") + } + // Your logic here + return nil +} +``` + +## Color Palette + +| Color | Usage | Constant | +|-------|-------|----------| +| Red | Errors | `colors.Red` | +| Green | Success, positive info | `colors.Green` | +| Yellow | Warnings, secondary info | `colors.Yellow` | +| Blue | Primary labels | `colors.Blue` | +| Purple | Directory info | `colors.Purple` | +| Cyan | Network info, values | `colors.Cyan` | +| Gray | Separators | `colors.Gray` | + +## Command Pattern + +All commands follow this pattern: + +```go +package cmd + +import ( + "fmt" + "github.com/zemenawi/zutils/pkg/colors" + "github.com/zemenawi/zutils/pkg/formatter" +) + +func MyCommand(args []string) error { + // Validate arguments + if len(args) < 1 { + return fmt.Errorf("please specify an argument") + } + + // Print header + PrintBoxHeader("MY COMMAND", colors.Cyan) + + // Your logic here + fmt.Printf("%s%sProcessing: %s%s\n", colors.Blue, colors.Bold, args[0], colors.Reset) + + // Print sections + PrintSectionHeader("DETAILS") + fmt.Printf("Size: %s\n", formatter.FormatSize(1234)) + + fmt.Println() + return nil +} +``` + +## Auto-Detection + +The info command supports auto-detection of files vs directories: + +```go +// AutoDetectInfo in cmd/autodetect.go +func AutoDetectInfo(target string) error { + // Checks if path is file or directory + // Calls appropriate handler +} +``` + +This allows: +```bash +z main.go # Detects as file +z . # Detects as directory +``` + +## Testing Commands + +### Unit Tests + +Create test files alongside your commands: + +```go +// cmd/hello_test.go +package cmd + +import "testing" + +func TestHelloCommand(t *testing.T) { + err := HelloCommand([]string{}) + if err != nil { + t.Errorf("HelloCommand failed: %v", err) + } +} +``` + +### Integration Tests + +Test the full command line: + +```bash +go build -o zutils +./zutils hello +``` + +## Common Tasks + +### Adding a New Color + +Edit `pkg/colors/colors.go`: + +```go +const ( + // Existing colors... + MyColor = "\033[38;5;123m" // RGB color +) +``` + +### Adding a New Formatter + +Edit `pkg/formatter/utils.go`: + +```go +func MyFormatter(value interface{}) string { + // Your logic + return formattedValue +} +``` + +### Adding Command Aliases + +Register multiple commands with the same handler: + +```go +registry.Register(&types.Command{ + Name: "info", + Description: "Get information", + Handler: cmd.InfoCommand, +}) + +registry.Register(&types.Command{ + Name: "i", + Description: "Info (alias)", + Handler: cmd.InfoCommand, +}) +``` + +## Debugging + +### Enable Debug Output + +Add a debug flag to your command: + +```go +import "flag" + +func MyCommand(args []string) error { + debug := flag.Bool("debug", false, "Enable debug output") + flag.Parse() + + if *debug { + fmt.Println("Debug mode enabled") + } + // ... +} +``` + +### Print Structured Data + +Use JSON for debugging complex data: + +```go +import "encoding/json" + +func MyCommand(args []string) error { + data := map[string]interface{}{ + "args": args, + "path": "/some/path", + } + + jsonBytes, _ := json.MarshalIndent(data, "", " ") + fmt.Println(string(jsonBytes)) + return nil +} +``` + +## Performance Tips + +1. **Avoid unnecessary file operations:** + ```go + // Good + info, _ := os.Stat(path) + size := info.Size() + + // Bad + content, _ := os.ReadFile(path) + size := int64(len(content)) + ``` + +2. **Use buffered I/O:** + ```go + scanner := bufio.NewScanner(file) + for scanner.Scan() { + // Process line + } + ``` + +3. **Limit directory scanning:** + ```go + if stats.FileCount > 1000 { + return // Stop scanning + } + ``` + +## Cross-Platform Considerations + +### Path Handling + +Always use `filepath` package: + +```go +import "path/filepath" + +path := filepath.Join("dir", "file.txt") +abs, _ := filepath.Abs(path) +``` + +### OS-Specific Code + +Use build tags or runtime checks: + +```go +import "runtime" + +if runtime.GOOS == "windows" { + // Windows-specific code +} else if runtime.GOOS == "linux" { + // Linux-specific code +} +``` + +## Resources + +- [Go Documentation](https://golang.org/doc/) +- [Go Standard Library](https://golang.org/pkg/) +- [Effective Go](https://golang.org/doc/effective_go.html) + +## Getting Help + +If you're stuck: +1. Check existing commands for examples +2. Read the command registry implementation +3. Look at shared utilities in `pkg/` +4. Test incrementally with small changes + +Happy coding! 🚀 diff --git a/README.md b/README.md new file mode 100644 index 0000000..04cb98a --- /dev/null +++ b/README.md @@ -0,0 +1,306 @@ +# zutils - User-Friendly Terminal Tools + +A collection of user-friendly CLI tools written in Go that make common Linux tasks easier with beautiful, colored, and organized output. + +## Features + +- 🎨 **Beautiful colored terminal output** - Easy to read and visually appealing +- 📊 **Organized information display** - Structured and logical layout +- 🚀 **Fast and lightweight** - Single binary, no external dependencies +- 🔍 **Comprehensive information** - Multiple data points in one view +- 💡 **Token estimation** - For AI/LLM context planning +- ⚡ **Easy to use** - Just one character shortcut: `z` +- 🏗️ **Well-structured** - Modular architecture for easy expansion + +## Installation + +### Build from source + +```bash +cd zutils +go build -o zutils +``` + +### Install to system path + +```bash +# Using the install script (recommended) +./install.sh + +# Or manually +cp zutils ~/.local/bin/ +ln -s ~/.local/bin/zutils ~/.local/bin/z +``` + +The install script creates both `zutils` and `z` (shortcut) commands. + +## Usage + +### Command Syntax + +```bash +z info +``` + +Or without the `info` subcommand (auto-detect): + +```bash +z +``` + +### Examples + +#### File Information + +Get detailed information about a file: + +```bash +z info main.go +# or simply +z main.go +``` + +Output includes: +- File path and name +- File size (formatted) +- File type detection +- Content statistics: + - Line count + - Word count + - Character count + - Estimated token count (for AI/LLM context) +- File details: + - Permissions (human-readable) + - Last modified timestamp + - File extension + +#### Directory Information + +Analyze a directory: + +```bash +z info /path/to/directory +# or simply +z /path/to/directory +``` + +Output includes: +- Total directory size +- File count +- Directory count +- Largest files (top 5) +- Largest directories (top 5) +- File type distribution +- Last modified timestamp + +#### Network Information + +Display network information: + +```bash +z info network +``` + +Output includes: +- Local IP addresses (IPv4 and IPv6) +- Public IP address +- DNS servers +- Active network connections (top 10) + +## Quick Reference + +```bash +# File information +z README.md +z info file main.go + +# Directory information +z . +z info dir /path/to/dir + +# Network information +z info network + +# Auto-detect (file or directory) +z /path/to/anything +``` + +## Project Structure + +The project follows a clean, modular architecture for easy expansion: + +``` +zutils/ +├── cmd/ # Command implementations +│ ├── info.go # Info command router +│ ├── file.go # File information +│ ├── dir.go # Directory information +│ ├── network.go # Network information +│ └── autodetect.go # Auto-detection logic +├── pkg/ # Shared packages +│ ├── colors/ # Color constants +│ ├── formatter/ # Output formatting utilities +│ └── types/ # Command registry and types +├── examples/ # Example files for testing +├── main.go # Entry point +├── go.mod # Go module definition +├── install.sh # Installation script +└── README.md # This file +``` + +### Architecture Highlights + +**Command Registry Pattern:** +- Easy to add new commands +- Centralized command management +- Extensible handler system + +**Modular Design:** +- Separated concerns (commands, colors, formatting, types) +- Reusable components +- Easy to maintain and test + +**No External Dependencies:** +- Uses Go standard library only +- Fast compilation +- Small binary size + +## Adding New Commands + +The project is designed to make adding new commands easy: + +1. Create a new file in `cmd/` directory +2. Implement your command handler function +3. Register it in `main.go` using the command registry + +Example: + +```go +// In cmd/mycommand.go +package cmd + +import ( + "fmt" + "github.com/zemenawi/zutils/pkg/colors" +) + +func MyCommand(args []string) error { + fmt.Printf("%s%sHello from my command!%s\n", colors.Green, colors.Bold, colors.Reset) + return nil +} + +// In main.go +registry.Register(&types.Command{ + Name: "mycmd", + Description: "My custom command", + Handler: cmd.MyCommand, +}) +``` + +## Why zutils? + +Traditional Linux tools suffer from: +- ❌ Confusing command-line options and flags +- ❌ Unformatted or hard-to-read output +- ❌ Multiple tools needed for simple tasks +- ❌ Poor default displays that overwhelm users + +**zutils** solves these issues by: +- ✅ Providing intuitive commands +- ✅ Beautiful, colored, organized output +- ✅ Combining multiple pieces of information in one view +- ✅ Great user experience for terminal users +- ✅ Single-character shortcut (`z`) +- ✅ Auto-detection of file/directory types + +## Token Estimation + +The token count is estimated using a rough approximation (words / 0.75), commonly used for: +- Planning AI/LLM context windows +- Estimating API token costs +- Understanding text complexity + +**Note:** Actual token counts vary by model and tokenizer. This is an estimate for planning purposes. + +## Development + +### Building + +```bash +go build -o zutils +``` + +### Cross-compilation + +```bash +# Linux +GOOS=linux GOARCH=amd64 go build -o zutils-linux-amd64 + +# macOS +GOOS=darwin GOARCH=amd64 go build -o zutils-darwin-amd64 + +# Windows +GOOS=windows GOARCH=amd64 go build -o zutils-windows-amd64.exe +``` + +### Testing + +```bash +# Test file info +z info main.go + +# Test directory info +z info . + +# Test network info +z info network + +# Test auto-detect +z main.go +z . +``` + +## Future Enhancements + +Planned features for future versions: + +- [ ] `stats` command - System overview (CPU, RAM, Disk, processes) +- [ ] `find` command - File searching with filters +- [ ] `diff` command - Side-by-side file comparison +- [ ] `compress` / `extract` commands - Archive handling +- [ ] `procs` command - Process management +- [ ] `port` command - Port usage checking +- [ ] `mem` command - Memory usage details +- [ ] `disk` command - Disk usage analysis +- [ ] `watch` mode - Monitor changes in real-time +- [ ] JSON output mode for scripting +- [ ] Configuration file for custom colors +- [ ] Shell completion (bash, zsh, fish) + +## License + +MIT License - Feel free to use, modify, and distribute. + +## Contributing + +Contributions are welcome! The modular structure makes it easy to add new features: + +1. Add your command to `cmd/` +2. Use shared utilities from `pkg/` +3. Register in `main.go` +4. Test thoroughly +5. Submit pull request + +Feel free to: +- Report bugs +- Suggest new features +- Submit pull requests +- Improve documentation + +## Author + +Created with ❤️ to make terminal tools more user-friendly. + +--- + +**Tip:** Use `z` as your one-character shortcut for all zutils commands! diff --git a/cmd/autodetect.go b/cmd/autodetect.go new file mode 100644 index 0000000..052c556 --- /dev/null +++ b/cmd/autodetect.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" +) + +// AutoDetectInfo automatically determines whether the path is a file or directory +func AutoDetectInfo(target string) error { + absPath, err := filepath.Abs(target) + if err != nil { + return fmt.Errorf("error resolving path: %w", err) + } + + fileInfo, err := os.Stat(absPath) + if err != nil { + return fmt.Errorf("error accessing %s: %w", absPath, err) + } + + if fileInfo.IsDir() { + return DirInfoCommand([]string{absPath}) + } + return FileInfoCommand([]string{absPath}) +} diff --git a/cmd/dir.go b/cmd/dir.go new file mode 100644 index 0000000..be5e841 --- /dev/null +++ b/cmd/dir.go @@ -0,0 +1,203 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "time" + + "github.com/zemenawi/zutils/pkg/colors" + "github.com/zemenawi/zutils/pkg/formatter" +) + +type FileItem struct { + Path string + Size int64 +} + +type DirStats struct { + TotalSize int64 + FileCount int + DirCount int + LargestFiles []FileItem + LargestDirs []FileItem + LastModified time.Time + FileExtensions map[string]int +} + +func DirInfoCommand(args []string) error { + if len(args) < 1 { + return fmt.Errorf("please specify a directory path") + } + + dirPath := args[0] + + stats, err := analyzeDirectory(dirPath) + if err != nil { + return fmt.Errorf("error analyzing directory: %w", err) + } + + size := formatter.FormatSize(stats.TotalSize) + modTime := stats.LastModified.Format(time.RFC1123) + + PrintBoxHeader("DIRECTORY INFORMATION", colors.Purple) + + 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📊 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.Println() + + // Print largest files table + if len(stats.LargestFiles) > 0 { + PrintSectionHeader("LARGEST FILES") + + headers := []string{"#", "File Name", "Size"} + data := make([][]string, 0, len(stats.LargestFiles)) + + for i, file := range stats.LargestFiles { + name := filepath.Base(file.Path) + row := []string{ + fmt.Sprintf("%d", i+1), + name, + formatter.FormatSize(file.Size), + } + data = append(data, row) + } + + printSimpleTable(headers, data) + } + + // Print largest directories table + if len(stats.LargestDirs) > 0 { + PrintSectionHeader("LARGEST DIRS") + + headers := []string{"#", "Directory Name", "Size"} + data := make([][]string, 0, len(stats.LargestDirs)) + + for i, dir := range stats.LargestDirs { + name := filepath.Base(dir.Path) + row := []string{ + fmt.Sprintf("%d", i+1), + name, + formatter.FormatSize(dir.Size), + } + data = append(data, row) + } + + printSimpleTable(headers, data) + } + + // Print file type distribution table + if len(stats.FileExtensions) > 0 { + PrintSectionHeader("FILE TYPE DISTRIBUTION") + + type extCount struct { + ext string + count int + } + var sortedExts []extCount + for ext, count := range stats.FileExtensions { + sortedExts = append(sortedExts, extCount{ext, count}) + } + sort.Slice(sortedExts, func(i, j int) bool { + return sortedExts[i].count > sortedExts[j].count + }) + + maxShow := 10 + if len(sortedExts) < maxShow { + maxShow = len(sortedExts) + } + + headers := []string{"#", "Extension", "File Count", "Percentage"} + data := make([][]string, 0, maxShow) + + for i := 0; i < maxShow; i++ { + ext := sortedExts[i].ext + if ext == "" { + ext = "(no extension)" + } + percentage := float64(sortedExts[i].count) / float64(stats.FileCount) * 100 + row := []string{ + fmt.Sprintf("%d", i+1), + ext, + fmt.Sprintf("%d", sortedExts[i].count), + fmt.Sprintf("%.1f%%", percentage), + } + data = append(data, row) + } + + printSimpleTable(headers, data) + } + + PrintSectionHeader("DIRECTORY DETAILS") + fmt.Printf("%s%s📅 Last Modified:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, modTime) + fmt.Println() + + return nil +} + +func printSimpleTable(headers []string, data [][]string) { + formatter.PrintTable(headers, data, colors.Cyan) + fmt.Println() +} + +func analyzeDirectory(path string) (*DirStats, error) { + stats := &DirStats{ + LargestFiles: make([]FileItem, 0), + LargestDirs: make([]FileItem, 0), + FileExtensions: make(map[string]int), + } + + err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if filePath == path { + return nil + } + + if info.IsDir() { + stats.DirCount++ + stats.LargestDirs = appendSorted(stats.LargestDirs, FileItem{ + Path: filePath, + Size: info.Size(), + }, 5) + } else { + stats.FileCount++ + stats.TotalSize += info.Size() + + ext := filepath.Ext(filePath) + stats.FileExtensions[ext]++ + + stats.LargestFiles = appendSorted(stats.LargestFiles, FileItem{ + Path: filePath, + Size: info.Size(), + }, 5) + + if info.ModTime().After(stats.LastModified) { + stats.LastModified = info.ModTime() + } + } + + return nil + }) + + return stats, err +} + +func appendSorted(items []FileItem, newItem FileItem, maxSize int) []FileItem { + items = append(items, newItem) + + sort.Slice(items, func(i, j int) bool { + return items[i].Size > items[j].Size + }) + + if len(items) > maxSize { + items = items[:maxSize] + } + + return items +} diff --git a/cmd/file.go b/cmd/file.go new file mode 100644 index 0000000..110e257 --- /dev/null +++ b/cmd/file.go @@ -0,0 +1,147 @@ +package cmd + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/zemenawi/zutils/pkg/colors" + "github.com/zemenawi/zutils/pkg/formatter" +) + +func FileInfoCommand(args []string) error { + if len(args) < 1 { + return fmt.Errorf("please specify a file path") + } + + filePath := args[0] + + file, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("error opening file: %w", err) + } + defer file.Close() + + fileInfo, err := file.Stat() + if err != nil { + return fmt.Errorf("error getting file info: %w", err) + } + + if fileInfo.IsDir() { + return fmt.Errorf("'%s' is a directory, use 'z info dir ' instead", filePath) + } + + // Count content + lines, words, chars := countFileContent(file) + + // Reset file pointer + file.Seek(0, 0) + + // Get file info + fileType := getFileType(filePath) + tokens := int(float64(words) / 0.75) + size := formatter.FormatSize(fileInfo.Size()) + perms := fileInfo.Mode().String() + modTime := fileInfo.ModTime().Format(time.RFC1123) + + // Print output + PrintBoxHeader("FILE INFORMATION", colors.Cyan) + + fmt.Printf("%s%s📁 Path:%s %s\n", colors.Blue, colors.Bold, colors.Reset, filePath) + fmt.Printf("%s%s📄 Name:%s %s\n", colors.Blue, colors.Bold, colors.Reset, filepath.Base(filePath)) + fmt.Printf("%s%s📏 Size:%s %s\n", colors.Blue, colors.Bold, colors.Reset, size) + fmt.Printf("%s%s🔖 Type:%s %s\n", colors.Blue, colors.Bold, colors.Reset, fileType) + fmt.Println() + + PrintSectionHeader("CONTENT STATISTICS") + fmt.Printf("%s%s📊 Lines:%s %d\n", colors.Green, colors.Bold, colors.Reset, lines) + fmt.Printf("%s%s📝 Words:%s %d\n", colors.Green, colors.Bold, colors.Reset, words) + fmt.Printf("%s%s🔢 Characters:%s %d\n", colors.Green, colors.Bold, colors.Reset, chars) + fmt.Printf("%s%s🤖 Tokens (est):%s %d\n", colors.Green, colors.Bold, colors.Reset, tokens) + fmt.Println() + + PrintSectionHeader("FILE DETAILS") + fmt.Printf("%s%s🔒 Permissions:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, perms) + 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() + + return nil +} + +func countFileContent(file *os.File) (lines, words, chars int) { + scanner := bufio.NewScanner(file) + lines = 0 + words = 0 + chars = 0 + + for scanner.Scan() { + lines++ + line := scanner.Text() + chars += len(line) + words += len(strings.Fields(line)) + } + + return lines, words, chars +} + +func getFileType(path string) string { + ext := strings.ToLower(filepath.Ext(path)) + switch ext { + case ".go": + return "Go Source" + case ".js": + return "JavaScript" + case ".ts": + return "TypeScript" + case ".py": + return "Python Source" + case ".java": + return "Java Source" + case ".c": + return "C Source" + case ".cpp": + return "C++ Source" + case ".rs": + return "Rust Source" + case ".json": + return "JSON Data" + case ".yaml", ".yml": + return "YAML Data" + case ".xml": + return "XML Data" + case ".html": + return "HTML Document" + case ".css": + return "CSS Stylesheet" + case ".md": + return "Markdown" + case ".txt": + return "Text File" + case ".pdf": + return "PDF Document" + case ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg": + return "Image File" + case ".mp3", ".wav", ".ogg": + return "Audio File" + case ".mp4", ".avi", ".mkv": + return "Video File" + case ".zip", ".tar", ".gz", ".7z", ".rar": + return "Archive File" + case ".exe", ".bin": + return "Binary File" + default: + return "Unknown Type" + } +} + +func getFileExtension(path string) string { + ext := filepath.Ext(path) + if ext == "" { + return "none" + } + return ext +} diff --git a/cmd/info.go b/cmd/info.go new file mode 100644 index 0000000..c04c3d2 --- /dev/null +++ b/cmd/info.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "fmt" + + "github.com/zemenawi/zutils/pkg/types" + "github.com/zemenawi/zutils/pkg/colors" +) + +// InfoCommand handles the info command with subcommands +func InfoCommand(args []string) error { + if len(args) < 1 { + printInfoUsage() + return fmt.Errorf("missing argument") + } + + target := args[0] + + switch target { + case "file": + if len(args) < 2 { + return fmt.Errorf("please specify a file path") + } + return FileInfoCommand(args[1:]) + case "dir": + if len(args) < 2 { + return fmt.Errorf("please specify a directory path") + } + return DirInfoCommand(args[1:]) + case "network": + return NetworkInfoCommand([]string{}) + default: + // Auto-detect: if it's "network", show network info + if target == "network" { + return NetworkInfoCommand([]string{}) + } + // Otherwise, it's a path - auto-detect file or directory + return AutoDetectInfo(target) + } +} + +func printInfoUsage() { + fmt.Printf("%sUsage:%s z info \n\n", colors.Cyan, colors.Reset) + fmt.Printf("%sExamples:%s\n", colors.Cyan, colors.Reset) + fmt.Println(" z info file /path/to/file.txt") + fmt.Println(" z info dir /path/to/directory") + fmt.Println(" z info network") + fmt.Println(" z info /path/to/file.txt # Auto-detect") + fmt.Println(" z info /path/to/directory # Auto-detect") +} + +func RegisterInfoCommands(registry *types.CommandRegistry) { + registry.Register(&types.Command{ + Name: "info", + Description: "Get information about files, directories, or network", + Handler: InfoCommand, + }) +} + +func PrintBoxHeader(title string, colorCode string) { + fmt.Printf("\n%s%s╔══════════════════════════════════════╗%s\n", colorCode, colors.Bold, colors.Reset) + fmt.Printf("%s%s║%s %-34s %s%s║%s\n", colorCode, colors.Bold, colors.Reset, CenterText(title, 34), colorCode, colors.Bold, colors.Reset) + fmt.Printf("%s%s╚══════════════════════════════════════╝%s\n", colorCode, colors.Bold, colors.Reset) + fmt.Println() +} + +func CenterText(text string, width int) string { + padding := width - len(text) + if padding <= 0 { + return text[:width] + } + left := padding / 2 + right := padding - left + + leftPad := "" + for i := 0; i < left; i++ { + leftPad += " " + } + rightPad := "" + for i := 0; i < right; i++ { + rightPad += " " + } + + return leftPad + text + rightPad +} + +func PrintSectionHeader(title string) { + fmt.Printf("%s%s───────── %s ─────────%s\n", colors.Gray, colors.Bold, title, colors.Reset) +} diff --git a/cmd/network.go b/cmd/network.go new file mode 100644 index 0000000..75e0a8d --- /dev/null +++ b/cmd/network.go @@ -0,0 +1,231 @@ +package cmd + +import ( + "fmt" + "net" + "os/exec" + "runtime" + "strings" + + "github.com/zemenawi/zutils/pkg/colors" +) + +func NetworkInfoCommand(args []string) error { + PrintBoxHeader("NETWORK INFORMATION", colors.Cyan) + + PrintSectionHeader("IP ADDRESSES") + + interfaces, err := net.Interfaces() + if err == nil { + for _, iface := range interfaces { + if iface.Flags&net.FlagUp != 0 && iface.Flags&net.FlagLoopback == 0 { + addrs, err := iface.Addrs() + if err == nil { + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + + if ip != nil && !ip.IsLoopback() { + fmt.Printf("%s%s🌐%s %s%-18s%s %s\n", + colors.Green, colors.Bold, colors.Reset, + colors.Cyan, iface.Name, colors.Reset, + ip.String()) + } + } + } + } + } + } else { + fmt.Printf("%sError getting network interfaces: %v%s\n", colors.Red, err, colors.Reset) + } + fmt.Println() + + PrintSectionHeader("PUBLIC IP") + publicIP := getPublicIP() + if publicIP != "" { + fmt.Printf("%s%s🌍%s Public IP: %s%s%s\n", + colors.Green, colors.Bold, colors.Reset, + colors.Cyan, publicIP, colors.Reset) + } else { + fmt.Printf("%sUnable to determine public IP%s\n", colors.Yellow, colors.Reset) + } + fmt.Println() + + PrintSectionHeader("DNS INFO") + dnsServers := getDNSServers() + for i, dns := range dnsServers { + fmt.Printf("%s%s🔗%s DNS Server %d: %s%s%s\n", + colors.Green, colors.Bold, colors.Reset, + i+1, + colors.Cyan, dns, colors.Reset) + } + fmt.Println() + + PrintSectionHeader("ACTIVE CONNECTIONS") + connections := getNetworkConnections() + if len(connections) > 0 { + maxShow := 10 + if len(connections) < maxShow { + maxShow = len(connections) + } + + headers := []string{"#", "Proto", "State", "Local Address", "Remote Address"} + data := make([][]string, 0, maxShow) + + for i := 0; i < maxShow; i++ { + parsed := parseConnection(connections[i]) + row := []string{ + fmt.Sprintf("%d", i+1), + parsed.proto, + parsed.state, + parsed.local, + parsed.remote, + } + data = append(data, row) + } + + printSimpleTable(headers, data) + + if len(connections) > maxShow { + fmt.Printf("%s%s...and %d more connections%s\n", + colors.Gray, colors.Bold, len(connections)-maxShow, colors.Reset) + } + fmt.Println() + } else { + fmt.Printf("%sNo active connections detected%s\n", colors.Yellow, colors.Reset) + fmt.Println() + } + + return nil +} + +func getPublicIP() string { + var cmd *exec.Cmd + + if runtime.GOOS == "windows" { + cmd = exec.Command("curl", "-s", "ifconfig.me") + } else { + cmd = exec.Command("curl", "-s", "ifconfig.me") + } + + output, err := cmd.CombinedOutput() + if err == nil { + return strings.TrimSpace(string(output)) + } + + cmd = exec.Command("wget", "-qO-", "ifconfig.me") + output, err = cmd.CombinedOutput() + if err == nil { + return strings.TrimSpace(string(output)) + } + + return "" +} + +func getDNSServers() []string { + var dnsServers []string + + if runtime.GOOS == "linux" { + cmd := exec.Command("sh", "-c", "grep '^nameserver' /etc/resolv.conf | awk '{print $2}'") + output, err := cmd.CombinedOutput() + if err == nil { + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, line := range lines { + if line != "" { + dnsServers = append(dnsServers, line) + } + } + } + } else if runtime.GOOS == "darwin" { + cmd := exec.Command("scutil", "--dns") + output, err := cmd.CombinedOutput() + if err == nil { + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "nameserver[") { + parts := strings.Fields(line) + if len(parts) >= 3 { + dnsServers = append(dnsServers, parts[2]) + } + } + } + } + } + + if len(dnsServers) == 0 { + dnsServers = append(dnsServers, "8.8.8.8 (Google DNS)") + dnsServers = append(dnsServers, "1.1.1.1 (Cloudflare DNS)") + } + + return dnsServers +} + +func getNetworkConnections() []string { + var connections []string + + if runtime.GOOS == "linux" { + cmd := exec.Command("ss", "-tun") + output, err := cmd.CombinedOutput() + if err == nil { + lines := strings.Split(string(output), "\n") + for i := 1; i < len(lines) && i < 12; i++ { + line := strings.TrimSpace(lines[i]) + if line != "" { + connections = append(connections, line) + } + } + } + } else if runtime.GOOS == "darwin" { + cmd := exec.Command("netstat", "-an") + output, err := cmd.CombinedOutput() + if err == nil { + lines := strings.Split(string(output), "\n") + for i := 0; i < len(lines) && i < 11; i++ { + line := strings.TrimSpace(lines[i]) + if strings.Contains(line, "ESTABLISHED") { + connections = append(connections, line) + } + } + } + } + + return connections +} + +type connectionInfo struct { + proto string + state string + local string + remote string +} + +func parseConnection(conn string) connectionInfo { + parts := strings.Fields(conn) + result := connectionInfo{} + + if len(parts) >= 5 { + result.proto = parts[0] + result.state = parts[1] + + // ss output format: proto state recv-q send-q local remote + // Try to find addresses by position + for i := 4; i < len(parts); i++ { + part := parts[i] + // Look for IP:port patterns + if strings.Contains(part, ":") && (strings.Contains(part, ".") || strings.Contains(part, ":")) { + if result.local == "" { + result.local = part + } else { + result.remote = part + } + } + } + } + + return result +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..e27090f --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "fmt" + "github.com/zemenawi/zutils/pkg/colors" +) + +const Version = "0.1.0" + +func VersionCommand(args []string) error { + fmt.Printf("%s%szutils%s - User-friendly terminal utilities\n\n", colors.Cyan, colors.Bold, colors.Reset) + fmt.Printf("%sVersion:%s %s%s%s\n", colors.Blue, colors.Reset, colors.Green, colors.Bold, Version) + fmt.Printf("%sBuilt with:%s Go\n", colors.Blue, colors.Reset) + fmt.Printf("%sHome:%s https://github.com/zemenawi/zutils\n", colors.Blue, colors.Reset) + fmt.Println() + return nil +} diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 0000000..2943482 --- /dev/null +++ b/examples/package.json @@ -0,0 +1 @@ +{"name": "test", "version": "1.0.0"} diff --git a/examples/script.js b/examples/script.js new file mode 100644 index 0000000..87f570c --- /dev/null +++ b/examples/script.js @@ -0,0 +1 @@ +function hello() { console.log('Hello, World!'); } diff --git a/examples/test.txt b/examples/test.txt new file mode 100644 index 0000000..5d3b099 --- /dev/null +++ b/examples/test.txt @@ -0,0 +1 @@ +This is a test file with some content. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e4ea190 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/zemenawi/zutils + +go 1.22.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..7fc59a2 --- /dev/null +++ b/install.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# zutils Installation Script + +set -e + +echo "Installing zutils..." + +# Build the binary +echo "Building zutils binary..." +go build -o zutils + +# Determine installation directory +if [ -w "/usr/local/bin" ]; then + INSTALL_DIR="/usr/local/bin" +elif [ -w "$HOME/.local/bin" ]; then + INSTALL_DIR="$HOME/.local/bin" + # Create the directory if it doesn't exist + mkdir -p "$INSTALL_DIR" +else + echo "Error: Cannot write to /usr/local/bin or ~/.local/bin" + echo "Please run with sudo or install manually:" + echo " sudo cp zutils /usr/local/bin/" + exit 1 +fi + +# Copy the binary +echo "Installing to $INSTALL_DIR..." +cp zutils "$INSTALL_DIR/zutils" +chmod +x "$INSTALL_DIR/zutils" + +# Create symlink for 'z' shortcut +echo "Creating 'z' shortcut..." +ln -sf "$INSTALL_DIR/zutils" "$INSTALL_DIR/z" + +echo "✅ zutils installed successfully!" +echo "" +echo "You can now use both 'zutils' and 'z' commands from anywhere." +echo "" +echo "Try it out:" +echo " z info README.md # File information" +echo " z info . # Directory information" +echo " z info network # Network information" +echo " z main.go # Auto-detect file info" diff --git a/main.go b/main.go new file mode 100644 index 0000000..9be63c1 --- /dev/null +++ b/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "fmt" + "os" + + "github.com/zemenawi/zutils/cmd" + "github.com/zemenawi/zutils/pkg/types" +) + +func main() { + registry := types.NewCommandRegistry() + + // Register all commands + cmd.RegisterInfoCommands(registry) + registry.Register(&types.Command{ + Name: "version", + Description: "Show version information", + Handler: cmd.VersionCommand, + }) + + // Parse command line arguments + args := os.Args[1:] + + if len(args) == 0 { + printUsage(registry) + os.Exit(1) + } + + // Handle legacy 'info' subcommand pattern for backwards compatibility + if args[0] == "info" { + infoCmd, exists := registry.Get("info") + if !exists { + printUsage(registry) + os.Exit(1) + } + err := infoCmd.Handler(args[1:]) + if err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } + return + } + + // Direct command: z info + commandName := args[0] + cmdArgs := args[1:] + + if command, exists := registry.Get(commandName); exists { + err := command.Handler(cmdArgs) + if err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } + } else { + // If no command matches, try auto-detect as info command + err := cmd.InfoCommand(args) + if err != nil { + fmt.Printf("Error: %v\n", err) + printUsage(registry) + os.Exit(1) + } + } +} + +func printUsage(registry *types.CommandRegistry) { + fmt.Println("z - User-friendly terminal utilities") + fmt.Println() + fmt.Println("Usage: z [arguments]") + fmt.Println() + fmt.Println("Commands:") + for _, command := range registry.GetAll() { + fmt.Printf(" %-12s %s\n", command.Name, command.Description) + } + fmt.Println() + fmt.Println("Quick Examples:") + fmt.Println(" z info main.go # File information") + fmt.Println(" z info . # Directory information") + fmt.Println(" z info network # Network information") + fmt.Println(" z main.go # Auto-detect file info") + fmt.Println() +} diff --git a/pkg/colors/colors.go b/pkg/colors/colors.go new file mode 100644 index 0000000..51046e1 --- /dev/null +++ b/pkg/colors/colors.go @@ -0,0 +1,13 @@ +package colors + +const ( + Reset = "\033[0m" + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + Blue = "\033[34m" + Purple = "\033[35m" + Cyan = "\033[36m" + Gray = "\033[90m" + Bold = "\033[1m" +) diff --git a/pkg/formatter/table.go b/pkg/formatter/table.go new file mode 100644 index 0000000..1f1fcd0 --- /dev/null +++ b/pkg/formatter/table.go @@ -0,0 +1,220 @@ +package formatter + +import ( + "fmt" + "strings" + + "github.com/zemenawi/zutils/pkg/colors" +) + +// Table represents a formatted table with better formatting +type Table struct { + headers []string + rows [][]string + columnWidths []int + showHeader bool + showBorder bool + padding int +} + +// NewTable creates a new table +func NewTable(headers []string) *Table { + return &Table{ + headers: headers, + rows: make([][]string, 0), + columnWidths: calculateColumnWidths(headers, make([][]string, 0)), + showHeader: true, + showBorder: true, + padding: 2, + } +} + +// SetShowBorder sets whether to show borders +func (t *Table) SetShowBorder(show bool) *Table { + t.showBorder = show + return t +} + +// SetShowHeader sets whether to show header +func (t *Table) SetShowHeader(show bool) *Table { + t.showHeader = show + return t +} + +// SetPadding sets the padding +func (t *Table) SetPadding(padding int) *Table { + t.padding = padding + return t +} + +// AddRow adds a row to the table +func (t *Table) AddRow(row []string) *Table { + if len(row) != len(t.headers) { + return t + } + t.rows = append(t.rows, row) + t.columnWidths = calculateColumnWidths(t.headers, t.rows) + return t +} + +// Render renders the table as a string +func (t *Table) Render() string { + var builder strings.Builder + + if t.showBorder { + builder.WriteString(t.renderTopBorder()) + } + + if t.showHeader { + builder.WriteString(t.renderRow(t.headers, true)) + if t.showBorder { + builder.WriteString(t.renderSeparator()) + } + } + + for _, row := range t.rows { + builder.WriteString(t.renderRow(row, false)) + } + + if t.showBorder { + builder.WriteString(t.renderBottomBorder()) + } + + return builder.String() +} + +// PrintTable prints a table to stdout +func PrintTable(headers []string, rows [][]string, borderColor string) { + table := NewTable(headers) + for _, row := range rows { + table.AddRow(row) + } + + output := table.Render() + fmt.Print(borderColor) + fmt.Print(output) + fmt.Println(colors.Reset) +} + +// PrintColoredTable prints a table with colored headers +func PrintColoredTable(headers []string, rows [][]string, headerColor string) { + // Add colors to headers + coloredHeaders := make([]string, len(headers)) + for i, header := range headers { + coloredHeaders[i] = headerColor + header + colors.Reset + } + + table := NewTable(coloredHeaders) + for _, row := range rows { + table.AddRow(row) + } + + table.showBorder = false + fmt.Print(table.Render()) +} + +// renderRow renders a single row +func (t *Table) renderRow(row []string, isHeader bool) string { + var builder strings.Builder + builder.WriteString("│") + + for i, cell := range row { + if i > 0 { + builder.WriteString("│") + } + + builder.WriteString(t.padCell(cell, t.columnWidths[i], isHeader)) + } + + builder.WriteString("│\n") + return builder.String() +} + +// padCell pads a cell to the correct width +func (t *Table) padCell(cell string, width int, isHeader bool) string { + padChar := " " + + // Left padding + builder := strings.Builder{} + for i := 0; i < t.padding; i++ { + builder.WriteString(padChar) + } + + // Cell content + builder.WriteString(cell) + + // Right padding - pad to exact column width + needed := width - len(cell) + for i := 0; i < needed+t.padding; i++ { + builder.WriteString(padChar) + } + + return builder.String() +} + +// renderTopBorder renders the top border +func (t *Table) renderTopBorder() string { + return t.renderBorderLine("╔", "═", "╗", "╦") +} + +// renderBottomBorder renders the bottom border +func (t *Table) renderBottomBorder() string { + return t.renderBorderLine("╚", "═", "╝", "╩") +} + +// renderSeparator renders the separator between header and rows +func (t *Table) renderSeparator() string { + return t.renderBorderLine("╠", "─", "╣", "╪") +} + +// renderBorderLine renders a border line +func (t *Table) renderBorderLine(left, middle, right, junction string) string { + builder := strings.Builder{} + builder.WriteString(left) + + for i, width := range t.columnWidths { + if i > 0 { + builder.WriteString(junction) + } + for j := 0; j < width+2*t.padding; j++ { + builder.WriteString(middle) + } + } + + builder.WriteString(right) + builder.WriteString("\n") + return builder.String() +} + +// calculateColumnWidths calculates the width of each column +func calculateColumnWidths(headers []string, rows [][]string) []int { + widths := make([]int, len(headers)) + + // Start with header widths + for i, header := range headers { + widths[i] = len(header) + } + + // Adjust based on row content + for _, row := range rows { + for i, cell := range row { + if i < len(widths) && len(cell) > widths[i] { + widths[i] = len(cell) + } + } + } + + return widths +} + +// PrintCompactTable prints a compact table without borders +func PrintCompactTable(headers []string, rows [][]string) { + table := NewTable(headers) + for _, row := range rows { + table.AddRow(row) + } + table.showBorder = false + table.showHeader = false + table.padding = 2 + fmt.Print(table.Render()) +} diff --git a/pkg/formatter/utils.go b/pkg/formatter/utils.go new file mode 100644 index 0000000..779e49e --- /dev/null +++ b/pkg/formatter/utils.go @@ -0,0 +1,45 @@ +package formatter + +import ( + "fmt" + "math" +) + +// FormatSize converts bytes to human-readable format +func FormatSize(bytes int64) string { + if bytes == 0 { + return "0 B" + } + + const unit = 1024 + units := []string{"B", "KB", "MB", "GB", "TB", "PB"} + + exp := int(math.Log(float64(bytes)) / math.Log(float64(unit))) + if exp > len(units)-1 { + exp = len(units) - 1 + } + + value := float64(bytes) / math.Pow(float64(unit), float64(exp)) + return fmt.Sprintf("%.2f %s", value, units[exp]) +} + +// CenterText centers text within a given width +func CenterText(text string, width int) string { + if len(text) >= width { + return text[:width] + } + + padding := width - len(text) + left := padding / 2 + right := padding - left + + return fmt.Sprintf("%s%s%s", repeat(" ", left), text, repeat(" ", right)) +} + +func repeat(s string, count int) string { + result := "" + for i := 0; i < count; i++ { + result += s + } + return result +} diff --git a/pkg/types/registry.go b/pkg/types/registry.go new file mode 100644 index 0000000..b395b99 --- /dev/null +++ b/pkg/types/registry.go @@ -0,0 +1,46 @@ +package types + +// Command represents a CLI command +type Command struct { + Name string + Description string + Handler func(args []string) error +} + +// CommandRegistry manages available commands +type CommandRegistry struct { + commands map[string]*Command +} + +// NewCommandRegistry creates a new command registry +func NewCommandRegistry() *CommandRegistry { + return &CommandRegistry{ + commands: make(map[string]*Command), + } +} + +// Register adds a command to the registry +func (r *CommandRegistry) Register(cmd *Command) { + r.commands[cmd.Name] = cmd +} + +// Get retrieves a command by name +func (r *CommandRegistry) Get(name string) (*Command, bool) { + cmd, exists := r.commands[name] + return cmd, exists +} + +// GetAll returns all registered commands +func (r *CommandRegistry) GetAll() []*Command { + cmds := make([]*Command, 0, len(r.commands)) + for _, cmd := range r.commands { + cmds = append(cmds, cmd) + } + return cmds +} + +// Has checks if a command exists +func (r *CommandRegistry) Has(name string) bool { + _, exists := r.commands[name] + return exists +}