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:
commit
aeae34365c
20 changed files with 1899 additions and 0 deletions
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal 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
394
DEVELOPMENT.md
Normal 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
306
README.md
Normal 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
25
cmd/autodetect.go
Normal 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
203
cmd/dir.go
Normal 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
147
cmd/file.go
Normal 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
89
cmd/info.go
Normal 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
231
cmd/network.go
Normal 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
17
cmd/version.go
Normal 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
1
examples/package.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"name": "test", "version": "1.0.0"}
|
||||
1
examples/script.js
Normal file
1
examples/script.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
function hello() { console.log('Hello, World!'); }
|
||||
1
examples/test.txt
Normal file
1
examples/test.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
This is a test file with some content.
|
||||
3
go.mod
Normal file
3
go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/zemenawi/zutils
|
||||
|
||||
go 1.22.0
|
||||
0
go.sum
Normal file
0
go.sum
Normal file
44
install.sh
Executable file
44
install.sh
Executable 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
82
main.go
Normal 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
13
pkg/colors/colors.go
Normal 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
220
pkg/formatter/table.go
Normal 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
45
pkg/formatter/utils.go
Normal 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
46
pkg/types/registry.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue