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)
This commit is contained in:
selamanapps 2026-05-02 00:05:08 +03:00
commit aeae34365c
20 changed files with 1899 additions and 0 deletions

31
.gitignore vendored Normal file
View file

@ -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

394
DEVELOPMENT.md Normal file
View file

@ -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! 🚀

306
README.md Normal file
View file

@ -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 <file|dir|network|path>
```
Or without the `info` subcommand (auto-detect):
```bash
z <file|directory>
```
### 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!

25
cmd/autodetect.go Normal file
View file

@ -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})
}

203
cmd/dir.go Normal file
View file

@ -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
}

147
cmd/file.go Normal file
View file

@ -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 <path>' 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
}

89
cmd/info.go Normal file
View file

@ -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 <file|dir|network|path>\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)
}

231
cmd/network.go Normal file
View file

@ -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
}

17
cmd/version.go Normal file
View file

@ -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
}

1
examples/package.json Normal file
View file

@ -0,0 +1 @@
{"name": "test", "version": "1.0.0"}

1
examples/script.js Normal file
View file

@ -0,0 +1 @@
function hello() { console.log('Hello, World!'); }

1
examples/test.txt Normal file
View file

@ -0,0 +1 @@
This is a test file with some content.

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/zemenawi/zutils
go 1.22.0

0
go.sum Normal file
View file

44
install.sh Executable file
View file

@ -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"

82
main.go Normal file
View file

@ -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 <args>
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 <command> [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()
}

13
pkg/colors/colors.go Normal file
View file

@ -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"
)

220
pkg/formatter/table.go Normal file
View file

@ -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())
}

45
pkg/formatter/utils.go Normal file
View file

@ -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
}

46
pkg/types/registry.go Normal file
View file

@ -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
}