feat: add user-friendly help system and new commands
New Commands: - z read - Smart file reader with auto-detection (markdown, syntax highlighting) - z sys - System overview (CPU, RAM, Disk, processes) - z find - File search with pattern/size/time filters - z search - User-friendly grep alternative - z usage - Process resource usage monitor Enhancements: - Unified help system with consistent error messages - Help on error shows usage and examples for each command - "Command not found" suggests similar commands - Directory info now detects .gitignore and shows ignored files - File handler registry for extensible file type detection - Added Italic, Dim, Underline color constants
This commit is contained in:
parent
800ab8464a
commit
5b334071e0
13 changed files with 1426 additions and 129 deletions
74
cmd/dir.go
74
cmd/dir.go
|
|
@ -7,6 +7,7 @@ import (
|
|||
"sort"
|
||||
"time"
|
||||
|
||||
ignore "github.com/sabhiram/go-gitignore"
|
||||
"github.com/zemenawi/zutils/pkg/colors"
|
||||
"github.com/zemenawi/zutils/pkg/formatter"
|
||||
)
|
||||
|
|
@ -14,16 +15,22 @@ import (
|
|||
type FileItem struct {
|
||||
Path string
|
||||
Size int64
|
||||
Ignored bool
|
||||
RelPath string
|
||||
}
|
||||
|
||||
type DirStats struct {
|
||||
TotalSize int64
|
||||
TotalSizeIgnoring int64
|
||||
FileCount int
|
||||
DirCount int
|
||||
IgnoredFileCount int
|
||||
IgnoredSize int64
|
||||
LargestFiles []FileItem
|
||||
LargestDirs []FileItem
|
||||
LastModified time.Time
|
||||
FileExtensions map[string]int
|
||||
HasGitignore bool
|
||||
}
|
||||
|
||||
func DirInfoCommand(args []string) error {
|
||||
|
|
@ -33,51 +40,67 @@ func DirInfoCommand(args []string) error {
|
|||
|
||||
dirPath := args[0]
|
||||
|
||||
stats, err := analyzeDirectory(dirPath)
|
||||
showIgnored := false
|
||||
if len(args) > 1 && args[1] == "--all" {
|
||||
showIgnored = true
|
||||
}
|
||||
|
||||
stats, err := analyzeDirectory(dirPath, showIgnored)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error analyzing directory: %w", err)
|
||||
}
|
||||
|
||||
size := formatter.FormatSize(stats.TotalSize)
|
||||
ignoringSize := formatter.FormatSize(stats.TotalSizeIgnoring)
|
||||
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)
|
||||
if stats.HasGitignore {
|
||||
fmt.Printf("%s%s📏 Size (excl. ignored):%s %s\n", colors.Blue, colors.Bold, colors.Reset, ignoringSize)
|
||||
fmt.Printf("%s%s🚫 Ignored:%s %d files (%s)\n", colors.Gray, colors.Bold, colors.Reset, stats.IgnoredFileCount, formatter.FormatSize(stats.IgnoredSize))
|
||||
}
|
||||
fmt.Printf("%s%s📊 Files:%s %d\n", colors.Blue, colors.Bold, colors.Reset, stats.FileCount)
|
||||
fmt.Printf("%s%s📂 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"}
|
||||
if stats.HasGitignore {
|
||||
headers = []string{"#", "File Name", "Size", "Status"}
|
||||
}
|
||||
data := make([][]string, 0, len(stats.LargestFiles))
|
||||
|
||||
for i, file := range stats.LargestFiles {
|
||||
name := filepath.Base(file.Path)
|
||||
name := file.RelPath
|
||||
row := []string{
|
||||
fmt.Sprintf("%d", i+1),
|
||||
name,
|
||||
formatter.FormatSize(file.Size),
|
||||
}
|
||||
if stats.HasGitignore {
|
||||
if file.Ignored {
|
||||
row = append(row, colors.Gray+"[ignored]"+colors.Reset)
|
||||
} else {
|
||||
row = append(row, "")
|
||||
}
|
||||
}
|
||||
data = append(data, row)
|
||||
}
|
||||
|
||||
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)
|
||||
name := dir.RelPath
|
||||
row := []string{
|
||||
fmt.Sprintf("%d", i+1),
|
||||
name,
|
||||
|
|
@ -89,10 +112,8 @@ func DirInfoCommand(args []string) error {
|
|||
printSimpleTable(headers, data)
|
||||
}
|
||||
|
||||
// Print file type distribution table
|
||||
if len(stats.FileExtensions) > 0 {
|
||||
PrintSectionHeader("FILE TYPE DISTRIBUTION")
|
||||
|
||||
type extCount struct {
|
||||
ext string
|
||||
count int
|
||||
|
|
@ -133,6 +154,9 @@ func DirInfoCommand(args []string) error {
|
|||
|
||||
PrintSectionHeader("DIRECTORY DETAILS")
|
||||
fmt.Printf("%s%s📅 Last Modified:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, modTime)
|
||||
if stats.HasGitignore {
|
||||
fmt.Printf("%s%s📜 Gitignore:%s %s\n", colors.Green, colors.Bold, colors.Reset, "Detected")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
|
|
@ -143,39 +167,67 @@ func printSimpleTable(headers []string, data [][]string) {
|
|||
fmt.Println()
|
||||
}
|
||||
|
||||
func analyzeDirectory(path string) (*DirStats, error) {
|
||||
func analyzeDirectory(path string, showIgnored bool) (*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 {
|
||||
ignoreMatcher, err := ignore.CompileIgnoreFile(filepath.Join(path, ".gitignore"))
|
||||
if err == nil {
|
||||
stats.HasGitignore = true
|
||||
_ = ignoreMatcher
|
||||
}
|
||||
|
||||
err = filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, _ := filepath.Rel(path, filePath)
|
||||
|
||||
if filePath == path {
|
||||
return nil
|
||||
}
|
||||
|
||||
ignored := false
|
||||
if stats.HasGitignore {
|
||||
ignored = ignoreMatcher.MatchesPath(relPath)
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
if !ignored || showIgnored {
|
||||
stats.DirCount++
|
||||
stats.LargestDirs = appendSorted(stats.LargestDirs, FileItem{
|
||||
Path: filePath,
|
||||
Size: info.Size(),
|
||||
Ignored: ignored,
|
||||
RelPath: relPath,
|
||||
}, 5)
|
||||
}
|
||||
} else {
|
||||
stats.FileCount++
|
||||
stats.TotalSize += info.Size()
|
||||
|
||||
if ignored {
|
||||
stats.IgnoredFileCount++
|
||||
stats.IgnoredSize += info.Size()
|
||||
} else {
|
||||
stats.TotalSizeIgnoring += info.Size()
|
||||
}
|
||||
|
||||
ext := filepath.Ext(filePath)
|
||||
stats.FileExtensions[ext]++
|
||||
|
||||
if !ignored || showIgnored {
|
||||
stats.LargestFiles = appendSorted(stats.LargestFiles, FileItem{
|
||||
Path: filePath,
|
||||
Size: info.Size(),
|
||||
Ignored: ignored,
|
||||
RelPath: relPath,
|
||||
}, 5)
|
||||
}
|
||||
|
||||
if info.ModTime().After(stats.LastModified) {
|
||||
stats.LastModified = info.ModTime()
|
||||
|
|
|
|||
192
cmd/find.go
Normal file
192
cmd/find.go
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zemenawi/zutils/pkg/colors"
|
||||
"github.com/zemenawi/zutils/pkg/formatter"
|
||||
)
|
||||
|
||||
type FindOptions struct {
|
||||
Pattern string
|
||||
SizeMin int64
|
||||
SizeMax int64
|
||||
ModifiedDays int
|
||||
FileType string
|
||||
MaxDepth int
|
||||
ShowHidden bool
|
||||
}
|
||||
|
||||
type FindResult struct {
|
||||
Path string
|
||||
Size int64
|
||||
Modified time.Time
|
||||
IsDir bool
|
||||
Permissions string
|
||||
}
|
||||
|
||||
func FindCommand(args []string) error {
|
||||
fs := flag.NewFlagSet("find", flag.ContinueOnError)
|
||||
options := FindOptions{}
|
||||
|
||||
fs.StringVar(&options.Pattern, "name", "*", "File name pattern (glob)")
|
||||
fs.StringVar(&options.FileType, "type", "", "File type: f (file), d (dir)")
|
||||
sizeMin := fs.Int64("size-min", 0, "Minimum size in bytes")
|
||||
sizeMax := fs.Int64("size-max", 0, "Maximum size in bytes")
|
||||
modifiedDays := fs.Int("mtime", 0, "Modified within N days")
|
||||
maxDepth := fs.Int("depth", -1, "Maximum directory depth")
|
||||
showHidden := fs.Bool("hidden", false, "Show hidden files")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid arguments: %v\n\nUsage: z find [path] [options]\n\nOptions:\n%s\n\nExamples:\n z find . -name '*.go' Find all Go files\n z find . -mtime 7 Files modified in last 7 days\n z find . -size +1m Files larger than 1MB\n z find . -type d Directories only", err, getFlagDefaults(fs))
|
||||
}
|
||||
|
||||
options.SizeMin = *sizeMin
|
||||
options.SizeMax = *sizeMax
|
||||
options.ModifiedDays = *modifiedDays
|
||||
options.MaxDepth = *maxDepth
|
||||
options.ShowHidden = *showHidden
|
||||
|
||||
path := "."
|
||||
if fs.NArg() > 0 {
|
||||
path = fs.Arg(0)
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
return fmt.Errorf("missing search path\n\nUsage: z find [path] [options]")
|
||||
}
|
||||
|
||||
results, err := findFiles(path, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding files: %v", err)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
fmt.Printf("%sNo files found matching criteria%s\n", colors.Gray, colors.Reset)
|
||||
return nil
|
||||
}
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Modified.After(results[j].Modified)
|
||||
})
|
||||
|
||||
PrintBoxHeader(fmt.Sprintf("FIND RESULTS (%d files)", len(results)), colors.Green)
|
||||
|
||||
headers := []string{"Path", "Size", "Modified"}
|
||||
data := make([][]string, 0, len(results))
|
||||
|
||||
for _, result := range results {
|
||||
relPath := result.Path
|
||||
if strings.Contains(relPath, "/") {
|
||||
parts := strings.Split(relPath, "/")
|
||||
relPath = strings.Join(parts[len(parts)-2:], "/")
|
||||
if len(parts) > 2 {
|
||||
relPath = "..." + string(filepath.Separator) + strings.Join(parts[len(parts)-2:], "/")
|
||||
}
|
||||
}
|
||||
|
||||
modStr := result.Modified.Format("Jan 02 15:04")
|
||||
if time.Since(result.Modified) > 30*24*time.Hour {
|
||||
modStr = result.Modified.Format("Jan 02 2006")
|
||||
}
|
||||
|
||||
row := []string{
|
||||
relPath,
|
||||
formatter.FormatSize(result.Size),
|
||||
modStr,
|
||||
}
|
||||
data = append(data, row)
|
||||
}
|
||||
|
||||
formatter.PrintTable(headers, data, colors.Cyan)
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFlagDefaults(fs *flag.FlagSet) string {
|
||||
var result []string
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
result = append(result, fmt.Sprintf(" -%s %s (default: %v)", f.Name, f.Usage, f.DefValue))
|
||||
})
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func findFiles(path string, opts FindOptions) ([]FindResult, error) {
|
||||
var results []FindResult
|
||||
|
||||
err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if filePath == path {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !opts.ShowHidden && filepath.Base(filePath)[0] == '.' {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.MaxDepth > 0 {
|
||||
relPath, _ := filepath.Rel(path, filePath)
|
||||
if strings.Count(relPath, string(filepath.Separator)) > opts.MaxDepth {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if opts.FileType == "f" && info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if opts.FileType == "d" && !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
baseName := filepath.Base(filePath)
|
||||
matched, err := filepath.Match(opts.Pattern, baseName)
|
||||
if err != nil || !matched {
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.SizeMin > 0 && info.Size() < opts.SizeMin {
|
||||
return nil
|
||||
}
|
||||
if opts.SizeMax > 0 && info.Size() > opts.SizeMax {
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.ModifiedDays > 0 {
|
||||
cutoff := time.Now().AddDate(0, 0, -opts.ModifiedDays)
|
||||
if info.ModTime().Before(cutoff) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, FindResult{
|
||||
Path: filePath,
|
||||
Size: info.Size(),
|
||||
Modified: info.ModTime(),
|
||||
IsDir: info.IsDir(),
|
||||
Permissions: info.Mode().String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return results, err
|
||||
}
|
||||
193
cmd/help.go
Normal file
193
cmd/help.go
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/zemenawi/zutils/pkg/colors"
|
||||
)
|
||||
|
||||
type CommandHelp struct {
|
||||
Name string
|
||||
Help string
|
||||
Examples []string
|
||||
}
|
||||
|
||||
var readHelp = CommandHelp{
|
||||
Name: "read",
|
||||
Help: "Read and display file with smart detection.\n" +
|
||||
"Automatically detects file type:\n" +
|
||||
" - Markdown (.md) - Renders with colored headings, lists, code blocks\n" +
|
||||
" - Code files - Syntax highlighting with line numbers\n" +
|
||||
" - Other files - Plain text display",
|
||||
Examples: []string{
|
||||
"z read README.md # Read markdown file",
|
||||
"z read main.go # Read code with syntax highlighting",
|
||||
"z read package.json # Read JSON file",
|
||||
},
|
||||
}
|
||||
|
||||
var sysHelp = CommandHelp{
|
||||
Name: "sys",
|
||||
Help: "Show system overview including CPU, memory, disk usage, and top processes.\n" +
|
||||
"Useful for quick system diagnostics.",
|
||||
Examples: []string{
|
||||
"z sys # Show full system overview",
|
||||
},
|
||||
}
|
||||
|
||||
var findHelp = CommandHelp{
|
||||
Name: "find",
|
||||
Help: "Find files matching criteria.\n" +
|
||||
"Supports glob patterns, size filters, modification time, and depth limits.",
|
||||
Examples: []string{
|
||||
"z find . -name '*.go' # Find all Go files",
|
||||
"z find . -mtime 7 # Files modified in last 7 days",
|
||||
"z find . -size +1m # Files larger than 1MB",
|
||||
"z find . -type d # Directories only",
|
||||
},
|
||||
}
|
||||
|
||||
var searchHelp = CommandHelp{
|
||||
Name: "search",
|
||||
Help: "Search for a pattern in files.\n" +
|
||||
"A user-friendly alternative to grep with colored output.",
|
||||
Examples: []string{
|
||||
"z search func cmd/ # Search for 'func' in cmd directory",
|
||||
"z search -i 'error' . # Case-insensitive search",
|
||||
"z search -w 'import' *.go # Whole word match",
|
||||
"z search -C 2 'TODO' . # Show 2 lines of context",
|
||||
},
|
||||
}
|
||||
|
||||
var usageHelp = CommandHelp{
|
||||
Name: "usage",
|
||||
Help: "Show resource usage (CPU, memory) for processes matching a name.\n" +
|
||||
"Useful for monitoring specific applications or services.",
|
||||
Examples: []string{
|
||||
"z usage cron # Show cron process usage",
|
||||
"z usage mongod # Show MongoDB usage",
|
||||
"z usage chrome # Show all Chrome processes",
|
||||
"z usage postgres # Show PostgreSQL usage",
|
||||
},
|
||||
}
|
||||
|
||||
func PrintErrorAndHelp(name string, err error) {
|
||||
help := getHelpForCommand(name)
|
||||
|
||||
fmt.Printf("\n %s✖ Error: %s%s\n\n", colors.Red, err, colors.Reset)
|
||||
|
||||
if help != nil {
|
||||
fmt.Printf(" %sUsage:%s z %s %s\n\n", colors.Bold, colors.Reset, help.Name, getUsageHint(help.Name))
|
||||
|
||||
if help.Help != "" {
|
||||
fmt.Printf(" %sDescription:%s\n", colors.Bold, colors.Reset)
|
||||
lines := splitIntoLines(help.Help, 60)
|
||||
for _, line := range lines {
|
||||
fmt.Printf(" %s%s%s\n", colors.Reset, line, colors.Reset)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
if len(help.Examples) > 0 {
|
||||
fmt.Printf(" %sExamples:%s\n", colors.Bold, colors.Reset)
|
||||
for _, ex := range help.Examples {
|
||||
fmt.Printf(" %s%s%s\n", colors.Green, ex, colors.Reset)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getHelpForCommand(name string) *CommandHelp {
|
||||
switch name {
|
||||
case "read":
|
||||
return &readHelp
|
||||
case "sys":
|
||||
return &sysHelp
|
||||
case "find":
|
||||
return &findHelp
|
||||
case "search":
|
||||
return &searchHelp
|
||||
case "usage":
|
||||
return &usageHelp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUsageHint(name string) string {
|
||||
switch name {
|
||||
case "read":
|
||||
return "<file>"
|
||||
case "sys":
|
||||
return ""
|
||||
case "find":
|
||||
return "[path] [options]"
|
||||
case "search":
|
||||
return "<pattern> [path]"
|
||||
case "usage":
|
||||
return "<process-name>"
|
||||
case "info":
|
||||
return "<file|dir|network>"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func splitIntoLines(text string, maxLen int) []string {
|
||||
var lines []string
|
||||
paragraphs := splitIntoParagraphs(text)
|
||||
|
||||
for _, para := range paragraphs {
|
||||
if para == "" {
|
||||
lines = append(lines, "")
|
||||
continue
|
||||
}
|
||||
|
||||
words := strings.Fields(para)
|
||||
currentLine := ""
|
||||
|
||||
for _, word := range words {
|
||||
if currentLine == "" {
|
||||
currentLine = word
|
||||
} else if len(currentLine)+len(word)+1 <= maxLen {
|
||||
currentLine += " " + word
|
||||
} else {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = word
|
||||
}
|
||||
}
|
||||
|
||||
if currentLine != "" {
|
||||
lines = append(lines, currentLine)
|
||||
}
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func splitIntoParagraphs(text string) []string {
|
||||
var paragraphs []string
|
||||
lines := strings.Split(text, "\n")
|
||||
|
||||
current := ""
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
if current != "" {
|
||||
paragraphs = append(paragraphs, current)
|
||||
current = ""
|
||||
}
|
||||
} else {
|
||||
if current != "" {
|
||||
current += " "
|
||||
}
|
||||
current += trimmed
|
||||
}
|
||||
}
|
||||
|
||||
if current != "" {
|
||||
paragraphs = append(paragraphs, current)
|
||||
}
|
||||
|
||||
return paragraphs
|
||||
}
|
||||
65
cmd/read.go
65
cmd/read.go
|
|
@ -1,77 +1,48 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/formatters"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/zemenawi/zutils/pkg/colors"
|
||||
"github.com/zemenawi/zutils/pkg/formatter"
|
||||
)
|
||||
|
||||
var ReadHelp = CommandHelp{
|
||||
Name: "read",
|
||||
Help: "Read and display file with smart detection.\nAutomatically detects file type:\n - Markdown (.md) - Renders with colored headings, lists, code blocks\n - Code files - Syntax highlighting with line numbers\n - Other files - Plain text display",
|
||||
Examples: []string{
|
||||
"z read README.md # Read markdown file",
|
||||
"z read main.go # Read code with syntax highlighting",
|
||||
"z read package.json # Read JSON file",
|
||||
},
|
||||
}
|
||||
|
||||
func ReadCommand(args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("please specify a file path")
|
||||
return fmt.Errorf("missing file path\n\nUsage: z read <file>")
|
||||
}
|
||||
|
||||
filePath := args[0]
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening file: %w", err)
|
||||
return fmt.Errorf("cannot open file '%s': %s", filePath, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting file info: %w", err)
|
||||
return fmt.Errorf("cannot read file info '%s': %s", filePath, err)
|
||||
}
|
||||
|
||||
if fileInfo.IsDir() {
|
||||
return fmt.Errorf("'%s' is a directory, not a readable file", filePath)
|
||||
return fmt.Errorf("'%s' is a directory\n\nUse: z info dir %s", filePath, filePath)
|
||||
}
|
||||
|
||||
content := formatter.ReadFileContent(file)
|
||||
|
||||
lexer := lexers.Match(filepath.Base(filePath))
|
||||
if lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
handler := formatter.ReadHandlers.GetHandler(filePath)
|
||||
if handler != nil {
|
||||
return handler(args)
|
||||
}
|
||||
|
||||
iterator, err := lexer.Tokenise(nil, content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error tokenizing: %w", err)
|
||||
}
|
||||
|
||||
formatter_ := formatters.TTY256
|
||||
style := styles.Get("monokai")
|
||||
if style == nil {
|
||||
style = styles.Fallback
|
||||
}
|
||||
|
||||
fmt.Printf("%s%s╔══════════════════════════════════════╗%s\n", colors.Cyan, colors.Bold, colors.Reset)
|
||||
fmt.Printf("%s%s║%s %-38s %s%s║%s\n", colors.Cyan, colors.Bold, colors.Reset, filePath, colors.Cyan, colors.Bold, colors.Reset)
|
||||
fmt.Printf("%s%s╚══════════════════════════════════════╝%s\n", colors.Cyan, colors.Bold, colors.Reset)
|
||||
fmt.Println()
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = formatter_.Format(&buf, style, iterator)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error formatting: %w", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(buf.String(), "\n")
|
||||
for i, line := range lines {
|
||||
if line != "" {
|
||||
fmt.Printf("%s%4d: %s%s\n", colors.Gray, i+1, colors.Reset, line)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return formatter.ReadFileWithHighlight(file, filePath)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/formatters"
|
||||
|
|
@ -13,7 +12,12 @@ import (
|
|||
"github.com/zemenawi/zutils/pkg/formatter"
|
||||
)
|
||||
|
||||
func MarkdownCommand(args []string) error {
|
||||
func init() {
|
||||
formatter.ReadHandlers.Register(".md", MarkdownHandler)
|
||||
formatter.ReadHandlers.Register(".markdown", MarkdownHandler)
|
||||
}
|
||||
|
||||
func MarkdownHandler(args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("please specify a markdown file path")
|
||||
}
|
||||
|
|
@ -35,11 +39,6 @@ func MarkdownCommand(args []string) error {
|
|||
return fmt.Errorf("'%s' is a directory, not a readable file", filePath)
|
||||
}
|
||||
|
||||
ext := filepath.Ext(filePath)
|
||||
if ext != ".md" && ext != ".markdown" {
|
||||
return fmt.Errorf("'%s' is not a markdown file", filePath)
|
||||
}
|
||||
|
||||
content := formatter.ReadFileContent(file)
|
||||
|
||||
fmt.Printf("%s%s╔══════════════════════════════════════╗%s\n", colors.Cyan, colors.Bold, colors.Reset)
|
||||
|
|
@ -52,7 +51,7 @@ func MarkdownCommand(args []string) error {
|
|||
codeBuffer := &strings.Builder{}
|
||||
codeLang := ""
|
||||
|
||||
for i, line := range lines {
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "```") {
|
||||
if !inCodeBlock {
|
||||
inCodeBlock = true
|
||||
|
|
@ -94,7 +93,6 @@ func MarkdownCommand(args []string) error {
|
|||
} else {
|
||||
fmt.Println()
|
||||
}
|
||||
_ = i
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -105,20 +103,22 @@ func renderCodeBlock(code, lang string) {
|
|||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s%s╔═══ %s ═══╗%s\n", colors.Gray, colors.Dim, lang, colors.Reset)
|
||||
|
||||
lexer := lexers.Match(lang)
|
||||
if lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
|
||||
iterator, _ := lexer.Tokenise(nil, code)
|
||||
formatter := formatters.TTY256
|
||||
formatter_ := formatters.TTY256
|
||||
style := styles.Get("monokai")
|
||||
if style == nil {
|
||||
style = styles.Fallback
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
formatter.Format(&buf, style, iterator)
|
||||
formatter_.Format(&buf, style, iterator)
|
||||
|
||||
output := buf.String()
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
|
|
@ -126,5 +126,6 @@ func renderCodeBlock(code, lang string) {
|
|||
fmt.Printf("%s%s│ %s%s\n", colors.Gray, colors.Dim, colors.Reset, line)
|
||||
}
|
||||
}
|
||||
fmt.Printf("%s%s╚══════════════╝%s\n", colors.Gray, colors.Dim, colors.Reset)
|
||||
fmt.Println()
|
||||
}
|
||||
180
cmd/search.go
Normal file
180
cmd/search.go
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/zemenawi/zutils/pkg/colors"
|
||||
)
|
||||
|
||||
type SearchOptions struct {
|
||||
Pattern string
|
||||
Path string
|
||||
CaseInsensitive bool
|
||||
WholeWord bool
|
||||
LineNumbers bool
|
||||
ContextLines int
|
||||
FilePattern string
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
File string
|
||||
Line int
|
||||
Content string
|
||||
}
|
||||
|
||||
func SearchCommand(args []string) error {
|
||||
fs := flag.NewFlagSet("search", flag.ContinueOnError)
|
||||
options := SearchOptions{}
|
||||
|
||||
fs.BoolVar(&options.CaseInsensitive, "i", false, "Case insensitive search")
|
||||
fs.BoolVar(&options.WholeWord, "w", false, "Match whole word")
|
||||
fs.BoolVar(&options.LineNumbers, "n", true, "Show line numbers")
|
||||
fs.IntVar(&options.ContextLines, "C", 0, "Show N lines of context")
|
||||
fs.StringVar(&options.FilePattern, "f", "*", "Search only in files matching pattern")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid arguments\n\nUsage: z search [options] pattern [path]\n\nExamples:\n z search func cmd/ Search for 'func' in cmd directory\n z search -i 'error' . Case-insensitive search\n z search -w 'import' *.go Whole word match in Go files")
|
||||
}
|
||||
|
||||
if fs.NArg() < 1 {
|
||||
return fmt.Errorf("missing search pattern\n\nUsage: z search [options] pattern [path]\n\nExamples:\n z search func cmd/ Search for 'func' in cmd directory\n z search -i 'error' . Case-insensitive search\n z search -w 'import' *.go Whole word match in Go files")
|
||||
}
|
||||
|
||||
options.Pattern = fs.Arg(0)
|
||||
if fs.NArg() > 1 {
|
||||
options.Path = fs.Arg(1)
|
||||
} else {
|
||||
options.Path = "."
|
||||
}
|
||||
|
||||
results, err := searchFiles(options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("search error: %v", err)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
fmt.Printf("%sNo matches found for '%s'%s\n", colors.Gray, options.Pattern, colors.Reset)
|
||||
return nil
|
||||
}
|
||||
|
||||
displaySearchResults(results, options)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func searchFiles(opts SearchOptions) ([]SearchResult, error) {
|
||||
var results []SearchResult
|
||||
|
||||
pattern := opts.Pattern
|
||||
if opts.CaseInsensitive {
|
||||
pattern = strings.ToLower(pattern)
|
||||
}
|
||||
|
||||
var regex *regexp.Regexp
|
||||
if opts.WholeWord {
|
||||
regex = regexp.MustCompile(`\b` + regexp.QuoteMeta(pattern) + `\b`)
|
||||
} else {
|
||||
regex = regexp.MustCompile(regexp.QuoteMeta(pattern))
|
||||
}
|
||||
|
||||
err := filepath.Walk(opts.Path, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
baseName := filepath.Base(path)
|
||||
matched, err := filepath.Match(opts.FilePattern, baseName)
|
||||
if err != nil || !matched {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileResults := searchFile(path, regex, opts)
|
||||
results = append(results, fileResults...)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
func searchFile(path string, regex *regexp.Regexp, opts SearchOptions) []SearchResult {
|
||||
var results []SearchResult
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return results
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
lineNum := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
line := scanner.Text()
|
||||
check := line
|
||||
|
||||
if opts.CaseInsensitive {
|
||||
check = strings.ToLower(check)
|
||||
}
|
||||
|
||||
if regex.MatchString(check) {
|
||||
results = append(results, SearchResult{
|
||||
File: path,
|
||||
Line: lineNum,
|
||||
Content: line,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func displaySearchResults(results []SearchResult, opts SearchOptions) {
|
||||
files := make(map[string][]SearchResult)
|
||||
for _, r := range results {
|
||||
files[r.File] = append(files[r.File], r)
|
||||
}
|
||||
|
||||
PrintBoxHeader(fmt.Sprintf("SEARCH RESULTS (%d matches in %d files)", len(results), len(files)), colors.Cyan)
|
||||
|
||||
for file, fileResults := range files {
|
||||
relPath := file
|
||||
if strings.Contains(relPath, "/") {
|
||||
parts := strings.Split(relPath, "/")
|
||||
if len(parts) > 3 {
|
||||
relPath = ".../" + strings.Join(parts[len(parts)-2:], "/")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s%s%s:%s\n", colors.Bold, colors.Yellow, relPath, colors.Reset)
|
||||
|
||||
for _, r := range fileResults {
|
||||
content := r.Content
|
||||
if len(content) > 100 {
|
||||
content = content[:97] + "..."
|
||||
}
|
||||
|
||||
if opts.LineNumbers {
|
||||
fmt.Printf("%s%4d:%s %s\n", colors.Gray, r.Line, colors.Reset, content)
|
||||
} else {
|
||||
fmt.Printf(" %s\n", content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
370
cmd/sys.go
Normal file
370
cmd/sys.go
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/zemenawi/zutils/pkg/colors"
|
||||
"github.com/zemenawi/zutils/pkg/formatter"
|
||||
)
|
||||
|
||||
type SystemStats struct {
|
||||
Hostname string
|
||||
OS string
|
||||
Platform string
|
||||
CPUModel string
|
||||
CPUCount int
|
||||
Uptime string
|
||||
LoadAvg string
|
||||
MemTotal int64
|
||||
MemUsed int64
|
||||
MemFree int64
|
||||
DiskTotal int64
|
||||
DiskUsed int64
|
||||
DiskFree int64
|
||||
TopProcs []ProcInfo
|
||||
}
|
||||
|
||||
type ProcInfo struct {
|
||||
PID int
|
||||
Name string
|
||||
CPU float64
|
||||
Mem float64
|
||||
Command string
|
||||
}
|
||||
|
||||
func SystemCommand(args []string) error {
|
||||
stats, err := getSystemStats()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting system stats: %v", err)
|
||||
}
|
||||
|
||||
PrintBoxHeader("SYSTEM OVERVIEW", colors.Cyan)
|
||||
|
||||
fmt.Printf("%s%s🏠 Hostname:%s %s\n", colors.Blue, colors.Bold, colors.Reset, stats.Hostname)
|
||||
fmt.Printf("%s%s💻 OS:%s %s\n", colors.Blue, colors.Bold, colors.Reset, stats.OS)
|
||||
fmt.Printf("%s%s🔧 Platform:%s %s\n", colors.Blue, colors.Bold, colors.Reset, stats.Platform)
|
||||
fmt.Println()
|
||||
|
||||
PrintSectionHeader("CPU")
|
||||
fmt.Printf("%s%s🖥️ Model:%s %s\n", colors.Green, colors.Bold, colors.Reset, stats.CPUModel)
|
||||
fmt.Printf("%s%s🔢 Cores:%s %d\n", colors.Green, colors.Bold, colors.Reset, stats.CPUCount)
|
||||
fmt.Printf("%s%s📊 Load Avg:%s %s\n", colors.Green, colors.Bold, colors.Reset, stats.LoadAvg)
|
||||
fmt.Printf("%s%s⏱️ Uptime:%s %s\n", colors.Green, colors.Bold, colors.Reset, stats.Uptime)
|
||||
fmt.Println()
|
||||
|
||||
PrintSectionHeader("MEMORY")
|
||||
memTotal := formatter.FormatSize(stats.MemTotal)
|
||||
memUsed := formatter.FormatSize(stats.MemUsed)
|
||||
memFree := formatter.FormatSize(stats.MemFree)
|
||||
memPct := float64(stats.MemUsed) / float64(stats.MemTotal) * 100
|
||||
|
||||
fmt.Printf("%s%s💾 Total:%s %s\n", colors.Purple, colors.Bold, colors.Reset, memTotal)
|
||||
fmt.Printf("%s%s📈 Used:%s %s (%.1f%%)\n", colors.Purple, colors.Bold, colors.Reset, memUsed, memPct)
|
||||
fmt.Printf("%s%s📉 Free:%s %s\n", colors.Purple, colors.Bold, colors.Reset, memFree)
|
||||
printMemoryBar(stats.MemUsed, stats.MemTotal)
|
||||
fmt.Println()
|
||||
|
||||
PrintSectionHeader("DISK")
|
||||
diskTotal := formatter.FormatSize(stats.DiskTotal)
|
||||
diskUsed := formatter.FormatSize(stats.DiskUsed)
|
||||
diskFree := formatter.FormatSize(stats.DiskFree)
|
||||
diskPct := float64(stats.DiskUsed) / float64(stats.DiskTotal) * 100
|
||||
|
||||
fmt.Printf("%s%s💽 Total:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, diskTotal)
|
||||
fmt.Printf("%s%s📈 Used:%s %s (%.1f%%)\n", colors.Yellow, colors.Bold, colors.Reset, diskUsed, diskPct)
|
||||
fmt.Printf("%s%s📉 Free:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, diskFree)
|
||||
printDiskBar(stats.DiskUsed, stats.DiskTotal)
|
||||
fmt.Println()
|
||||
|
||||
if len(stats.TopProcs) > 0 {
|
||||
PrintSectionHeader("TOP PROCESSES")
|
||||
headers := []string{"PID", "Name", "CPU%", "MEM%"}
|
||||
data := make([][]string, 0, len(stats.TopProcs))
|
||||
|
||||
for _, proc := range stats.TopProcs {
|
||||
row := []string{
|
||||
fmt.Sprintf("%d", proc.PID),
|
||||
truncateString(proc.Name, 20),
|
||||
fmt.Sprintf("%.1f", proc.CPU),
|
||||
fmt.Sprintf("%.1f", proc.Mem),
|
||||
}
|
||||
data = append(data, row)
|
||||
}
|
||||
|
||||
formatter.PrintTable(headers, data, colors.Cyan)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printMemoryBar(used, total int64) {
|
||||
width := 30
|
||||
if total == 0 {
|
||||
return
|
||||
}
|
||||
fillLen := int(float64(used) / float64(total) * float64(width))
|
||||
|
||||
bar := "["
|
||||
for i := 0; i < width; i++ {
|
||||
if i < fillLen {
|
||||
if i < width/3 {
|
||||
bar += colors.Green + "█" + colors.Reset
|
||||
} else if i < 2*width/3 {
|
||||
bar += colors.Yellow + "█" + colors.Reset
|
||||
} else {
|
||||
bar += colors.Red + "█" + colors.Reset
|
||||
}
|
||||
} else {
|
||||
bar += colors.Gray + "░" + colors.Reset
|
||||
}
|
||||
}
|
||||
bar += "]"
|
||||
|
||||
fmt.Printf("%s%sMemory Bar:%s %s\n", colors.Gray, colors.Bold, colors.Reset, bar)
|
||||
}
|
||||
|
||||
func printDiskBar(used, total int64) {
|
||||
width := 30
|
||||
if total == 0 {
|
||||
return
|
||||
}
|
||||
fillLen := int(float64(used) / float64(total) * float64(width))
|
||||
|
||||
bar := "["
|
||||
for i := 0; i < width; i++ {
|
||||
if i < fillLen {
|
||||
if i < width/3 {
|
||||
bar += colors.Green + "█" + colors.Reset
|
||||
} else if i < 2*width/3 {
|
||||
bar += colors.Yellow + "█" + colors.Reset
|
||||
} else {
|
||||
bar += colors.Red + "█" + colors.Reset
|
||||
}
|
||||
} else {
|
||||
bar += colors.Gray + "░" + colors.Reset
|
||||
}
|
||||
}
|
||||
bar += "]"
|
||||
|
||||
fmt.Printf("%s%sDisk Bar:%s %s\n", colors.Gray, colors.Bold, colors.Reset, bar)
|
||||
}
|
||||
|
||||
func getSystemStats() (*SystemStats, error) {
|
||||
stats := &SystemStats{}
|
||||
|
||||
stats.Hostname, _ = os.Hostname()
|
||||
stats.OS = runtime.GOOS
|
||||
stats.Platform = runtime.GOARCH
|
||||
|
||||
stats.CPUModel = getCPUModel()
|
||||
stats.CPUCount = runtime.NumCPU()
|
||||
|
||||
stats.Uptime = getUptime()
|
||||
stats.LoadAvg = getLoadAvg()
|
||||
|
||||
getMemoryStats(stats)
|
||||
getDiskStats(stats)
|
||||
|
||||
stats.TopProcs = getTopProcs(5)
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func getCPUModel() string {
|
||||
if runtime.GOOS == "linux" {
|
||||
if data, err := os.ReadFile("/proc/cpuinfo"); err == nil {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "model name") || strings.HasPrefix(line, "Processor") || strings.HasPrefix(line, "cpu model") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
func getUptime() string {
|
||||
if runtime.GOOS == "linux" {
|
||||
if data, err := os.ReadFile("/proc/uptime"); err == nil {
|
||||
parts := strings.Split(string(data), " ")
|
||||
if len(parts) >= 1 {
|
||||
if secs, err := strconv.ParseFloat(strings.TrimSpace(parts[0]), 64); err == nil {
|
||||
duration := time.Duration(int64(secs)) * time.Second
|
||||
return formatDuration(duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
days := int(d.Hours() / 24)
|
||||
hours := int(d.Hours()) % 24
|
||||
minutes := int(d.Minutes()) % 60
|
||||
|
||||
if days > 0 {
|
||||
return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
|
||||
}
|
||||
if hours > 0 {
|
||||
return fmt.Sprintf("%dh %dm", hours, minutes)
|
||||
}
|
||||
return fmt.Sprintf("%dm", minutes)
|
||||
}
|
||||
|
||||
func getLoadAvg() string {
|
||||
if runtime.GOOS == "linux" {
|
||||
if data, err := os.ReadFile("/proc/loadavg"); err == nil {
|
||||
parts := strings.Split(strings.TrimSpace(string(data)), " ")
|
||||
if len(parts) >= 3 {
|
||||
return fmt.Sprintf("%.2f %.2f %.2f",
|
||||
parseFloatSafe(parts[0]),
|
||||
parseFloatSafe(parts[1]),
|
||||
parseFloatSafe(parts[2]))
|
||||
}
|
||||
}
|
||||
}
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
func parseFloatSafe(s string) float64 {
|
||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
return f
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getMemoryStats(stats *SystemStats) {
|
||||
if runtime.GOOS == "linux" {
|
||||
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "MemTotal") {
|
||||
stats.MemTotal = parseMeminfoLine(line)
|
||||
} else if strings.HasPrefix(line, "MemAvailable") || strings.HasPrefix(line, "MemFree") {
|
||||
if stats.MemFree == 0 {
|
||||
stats.MemFree = parseMeminfoLine(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
if stats.MemFree > 0 && stats.MemTotal > 0 {
|
||||
stats.MemUsed = stats.MemTotal - stats.MemFree
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseMeminfoLine(line string) int64 {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 {
|
||||
if val, err := strconv.ParseInt(parts[1], 10, 64); err == nil {
|
||||
return val * 1024
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getDiskStats(stats *SystemStats) {
|
||||
if runtime.GOOS == "linux" {
|
||||
output, err := exec.Command("df", "-k", "/").Output()
|
||||
if err == nil {
|
||||
lines := strings.Split(string(output), "\n")
|
||||
if len(lines) >= 2 {
|
||||
parts := strings.Fields(lines[1])
|
||||
if len(parts) >= 4 {
|
||||
stats.DiskTotal, _ = strconv.ParseInt(parts[1], 10, 64)
|
||||
stats.DiskUsed, _ = strconv.ParseInt(parts[2], 10, 64)
|
||||
stats.DiskFree, _ = strconv.ParseInt(parts[3], 10, 64)
|
||||
stats.DiskTotal *= 1024
|
||||
stats.DiskUsed *= 1024
|
||||
stats.DiskFree *= 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTopProcs(count int) []ProcInfo {
|
||||
var procs []ProcInfo
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
dir, err := os.Open("/proc")
|
||||
if err != nil {
|
||||
return procs
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
entries, err := dir.Readdirnames(count * 3)
|
||||
if err != nil {
|
||||
return procs
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
pid, err := strconv.Atoi(entry)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
stat, err := readProcStat(pid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
procs = append(procs, stat)
|
||||
if len(procs) >= count {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return procs
|
||||
}
|
||||
|
||||
func readProcStat(pid int) (ProcInfo, error) {
|
||||
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
|
||||
if err != nil {
|
||||
return ProcInfo{}, err
|
||||
}
|
||||
|
||||
parts := strings.SplitN(string(data), " ", 4)
|
||||
if len(parts) < 4 {
|
||||
return ProcInfo{}, fmt.Errorf("invalid format")
|
||||
}
|
||||
|
||||
name := parts[1]
|
||||
name = strings.Trim(name, "(")
|
||||
name = strings.TrimRight(name, ")")
|
||||
|
||||
comm, _ := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
|
||||
commName := strings.TrimSpace(string(comm))
|
||||
|
||||
var rusage syscall.Rusage
|
||||
syscall.Getrusage(syscall.RUSAGE_SELF, &rusage)
|
||||
|
||||
return ProcInfo{
|
||||
PID: pid,
|
||||
Name: commName,
|
||||
CPU: 0,
|
||||
Mem: 0,
|
||||
Command: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func truncateString(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen-3] + "..."
|
||||
}
|
||||
228
cmd/usage.go
Normal file
228
cmd/usage.go
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/zemenawi/zutils/pkg/colors"
|
||||
"github.com/zemenawi/zutils/pkg/formatter"
|
||||
)
|
||||
|
||||
type ProcessInfo struct {
|
||||
PID int
|
||||
Name string
|
||||
Command string
|
||||
CPUPercent float64
|
||||
MemBytes int64
|
||||
MemPercent float64
|
||||
Status string
|
||||
User string
|
||||
}
|
||||
|
||||
func UsageCommand(args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("missing process name\n\nUsage: z usage <process-name>\n\nExamples:\n z usage cron # Show cron process usage\n z usage mongod # Show MongoDB usage\n z usage chrome # Show all Chrome processes")
|
||||
}
|
||||
|
||||
processName := args[0]
|
||||
processes := findProcessesByName(processName)
|
||||
|
||||
if len(processes) == 0 {
|
||||
fmt.Printf("\n %sNo processes found matching '%s'%s\n\n", colors.Yellow, processName, colors.Reset)
|
||||
fmt.Printf(" Try a different process name, or use %sz sys%s to see all processes\n\n", colors.Gray, colors.Reset)
|
||||
return nil
|
||||
}
|
||||
|
||||
PrintBoxHeader(fmt.Sprintf("USAGE: %s (%d processes)", processName, len(processes)), colors.Cyan)
|
||||
|
||||
totalCPU := 0.0
|
||||
totalMem := int64(0)
|
||||
|
||||
for _, p := range processes {
|
||||
totalCPU += p.CPUPercent
|
||||
totalMem += p.MemBytes
|
||||
}
|
||||
|
||||
fmt.Printf("%s%s📊 Total CPU:%s %.1f%%\n", colors.Green, colors.Bold, colors.Reset, totalCPU)
|
||||
fmt.Printf("%s%s💾 Total Memory:%s %s (%.1f MB)\n", colors.Purple, colors.Bold, colors.Reset, formatter.FormatSize(totalMem), float64(totalMem)/1024/1024)
|
||||
fmt.Println()
|
||||
|
||||
headers := []string{"PID", "Process", "CPU%", "Memory", "Status"}
|
||||
data := make([][]string, 0, len(processes))
|
||||
|
||||
sort.Slice(processes, func(i, j int) bool {
|
||||
return processes[i].CPUPercent > processes[j].CPUPercent
|
||||
})
|
||||
|
||||
for _, p := range processes {
|
||||
cmd := p.Command
|
||||
if len(cmd) > 30 {
|
||||
parts := strings.Fields(cmd)
|
||||
if len(parts) > 0 {
|
||||
cmd = filepath.Base(parts[0]) + " " + strings.Join(parts[1:], " ")[:25]
|
||||
}
|
||||
}
|
||||
|
||||
status := p.Status
|
||||
if status == "R" {
|
||||
status = colors.Green + "running" + colors.Reset
|
||||
} else if status == "S" || status == "I" {
|
||||
status = colors.Gray + "sleeping" + colors.Reset
|
||||
} else {
|
||||
status = colors.Yellow + status + colors.Reset
|
||||
}
|
||||
|
||||
row := []string{
|
||||
fmt.Sprintf("%d", p.PID),
|
||||
cmd,
|
||||
fmt.Sprintf("%.1f", p.CPUPercent),
|
||||
formatter.FormatSize(p.MemBytes),
|
||||
status,
|
||||
}
|
||||
data = append(data, row)
|
||||
}
|
||||
|
||||
formatter.PrintTable(headers, data, colors.Cyan)
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findProcessesByName(name string) []ProcessInfo {
|
||||
var processes []ProcessInfo
|
||||
|
||||
dir, err := os.Open("/proc")
|
||||
if err != nil {
|
||||
return processes
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
entries, err := dir.Readdirnames(500)
|
||||
if err != nil {
|
||||
return processes
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
pid, err := strconv.Atoi(entry)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
proc, err := getProcessInfo(pid, name)
|
||||
if err == nil && proc != nil {
|
||||
processes = append(processes, *proc)
|
||||
}
|
||||
}
|
||||
|
||||
return processes
|
||||
}
|
||||
|
||||
func getProcessInfo(pid int, targetName string) (*ProcessInfo, error) {
|
||||
comm, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
procName := strings.TrimSpace(string(comm))
|
||||
|
||||
if !strings.Contains(strings.ToLower(procName), strings.ToLower(targetName)) {
|
||||
if !strings.Contains(strings.ToLower(procName), strings.ToLower(targetName)) {
|
||||
cmdline, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
|
||||
if cmdline != nil && !strings.Contains(strings.ToLower(string(cmdline)), strings.ToLower(targetName)) {
|
||||
return nil, fmt.Errorf("name mismatch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parts := strings.Split(string(stat), " ")
|
||||
if len(parts) < 24 {
|
||||
return nil, fmt.Errorf("invalid stat format")
|
||||
}
|
||||
|
||||
status := "?"
|
||||
if len(parts) > 2 {
|
||||
status = strings.Trim(parts[2], "()")
|
||||
}
|
||||
|
||||
utime := parseIntSafe(parts[13])
|
||||
stime := parseIntSafe(parts[14])
|
||||
|
||||
memBytes := int64(0)
|
||||
if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)); err == nil {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "VmRSS:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
memKB := parseIntSafe(fields[1])
|
||||
memBytes = memKB * 1024
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalMem := int64(0)
|
||||
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "MemTotal:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
totalMem = parseIntSafe(fields[1]) * 1024
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cpuPercent := 0.0
|
||||
if totalMem > 0 {
|
||||
cpuPercent = float64(utime+stime) / float64(totalMem) * 100
|
||||
}
|
||||
|
||||
cmdline, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
|
||||
cmdStr := strings.Join(strings.Fields(string(cmdline)), " ")
|
||||
|
||||
return &ProcessInfo{
|
||||
PID: pid,
|
||||
Name: procName,
|
||||
Command: cmdStr,
|
||||
CPUPercent: cpuPercent,
|
||||
MemBytes: memBytes,
|
||||
MemPercent: float64(memBytes) / float64(totalMem) * 100,
|
||||
Status: status,
|
||||
User: getProcessUser(pid),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getProcessUser(pid int) string {
|
||||
if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)); err == nil {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "Uid:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
uid := parseIntSafe(fields[1])
|
||||
return fmt.Sprintf("uid:%d", uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
func parseIntSafe(s string) int64 {
|
||||
if i, err := strconv.ParseInt(strings.TrimSpace(s), 10, 64); err == nil {
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
1
go.mod
1
go.mod
|
|
@ -5,5 +5,6 @@ go 1.22.0
|
|||
require (
|
||||
github.com/alecthomas/chroma/v2 v2.24.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.12.0 // indirect
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
|
||||
github.com/yuin/goldmark v1.8.2 // indirect
|
||||
)
|
||||
|
|
|
|||
8
go.sum
8
go.sum
|
|
@ -1,6 +1,14 @@
|
|||
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
|
||||
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
92
main.go
92
main.go
|
|
@ -3,33 +3,54 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/zemenawi/zutils/cmd"
|
||||
"github.com/zemenawi/zutils/pkg/colors"
|
||||
"github.com/zemenawi/zutils/pkg/types"
|
||||
)
|
||||
|
||||
func main() {
|
||||
registry := types.NewCommandRegistry()
|
||||
|
||||
// Register all commands
|
||||
registry.Register(&types.Command{
|
||||
Name: "read",
|
||||
Description: "Read and display file with syntax highlighting",
|
||||
Description: "Read and display file with smart detection",
|
||||
Handler: cmd.ReadCommand,
|
||||
})
|
||||
|
||||
registry.Register(&types.Command{
|
||||
Name: "md",
|
||||
Description: "Render markdown file to terminal",
|
||||
Handler: cmd.MarkdownCommand,
|
||||
Name: "sys",
|
||||
Description: "Display system information overview",
|
||||
Handler: cmd.SystemCommand,
|
||||
})
|
||||
|
||||
registry.Register(&types.Command{
|
||||
Name: "find",
|
||||
Description: "Find files with pattern and filter support",
|
||||
Handler: cmd.FindCommand,
|
||||
})
|
||||
|
||||
registry.Register(&types.Command{
|
||||
Name: "search",
|
||||
Description: "Search for text pattern in files",
|
||||
Handler: cmd.SearchCommand,
|
||||
})
|
||||
|
||||
registry.Register(&types.Command{
|
||||
Name: "usage",
|
||||
Description: "Show resource usage for a process",
|
||||
Handler: cmd.UsageCommand,
|
||||
})
|
||||
|
||||
cmd.RegisterInfoCommands(registry)
|
||||
|
||||
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 {
|
||||
|
|
@ -37,7 +58,6 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Handle legacy 'info' subcommand pattern for backwards compatibility
|
||||
if args[0] == "info" {
|
||||
infoCmd, exists := registry.Get("info")
|
||||
if !exists {
|
||||
|
|
@ -46,48 +66,62 @@ func main() {
|
|||
}
|
||||
err := infoCmd.Handler(args[1:])
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
cmd.PrintErrorAndHelp("info", 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)
|
||||
cmd.PrintErrorAndHelp(commandName, 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)
|
||||
fmt.Printf("\n %sCommand not found: %s%s\n\n", colors.Red, args[0], colors.Reset)
|
||||
fmt.Printf(" Did you mean one of these?\n\n")
|
||||
suggestions := findSimilarCommands(args[0], registry.GetAll())
|
||||
for _, s := range suggestions {
|
||||
fmt.Printf(" %s%s%s - %s\n", colors.Yellow, s.Name, colors.Reset, s.Description)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
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:")
|
||||
fmt.Printf("\n %s%sz%s - User-friendly terminal utilities\n\n", colors.Bold, colors.Cyan, colors.Reset)
|
||||
fmt.Printf(" %sUsage:%s z <command> [arguments]\n\n", colors.Bold, colors.Reset)
|
||||
fmt.Printf(" %sAvailable Commands:%s\n", colors.Bold, colors.Reset)
|
||||
|
||||
for _, command := range registry.GetAll() {
|
||||
fmt.Printf(" %-12s %s\n", command.Name, command.Description)
|
||||
fmt.Printf(" %s%-10s%s %s\n", colors.Green, command.Name, colors.Reset, command.Description)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("Quick Examples:")
|
||||
fmt.Println(" z read go.mod # Display file with line numbers")
|
||||
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()
|
||||
|
||||
fmt.Printf("\n %sQuick Examples:%s\n", colors.Bold, colors.Reset)
|
||||
fmt.Printf(" %sz read README.md%s # Display file with smart formatting\n", colors.Gray, colors.Reset)
|
||||
fmt.Printf(" %sz sys%s # Show system overview\n", colors.Gray, colors.Reset)
|
||||
fmt.Printf(" %sz find . -name '*.go'%s # Find files\n", colors.Gray, colors.Reset)
|
||||
fmt.Printf(" %sz search pattern path%s # Search in files\n", colors.Gray, colors.Reset)
|
||||
fmt.Printf(" %sz info .%s # Directory info with gitignore support\n", colors.Gray, colors.Reset)
|
||||
fmt.Printf(" %sz info main.go%s # File information\n", colors.Gray, colors.Reset)
|
||||
fmt.Printf("\n Type %sz <command>%s without arguments to see help\n\n", colors.Gray, colors.Reset)
|
||||
}
|
||||
|
||||
func findSimilarCommands(input string, commands []*types.Command) []*types.Command {
|
||||
var suggestions []*types.Command
|
||||
for _, cmd := range commands {
|
||||
if strings.HasPrefix(cmd.Name, input[:1]) {
|
||||
suggestions = append(suggestions, cmd)
|
||||
}
|
||||
}
|
||||
if len(suggestions) > 3 {
|
||||
suggestions = suggestions[:3]
|
||||
}
|
||||
return suggestions
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,85 @@
|
|||
package formatter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/formatters"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
)
|
||||
|
||||
type FileHandler func(args []string) error
|
||||
|
||||
type FileHandlerRegistry struct {
|
||||
handlers map[string]FileHandler
|
||||
}
|
||||
|
||||
var ReadHandlers = &FileHandlerRegistry{
|
||||
handlers: make(map[string]FileHandler),
|
||||
}
|
||||
|
||||
func (r *FileHandlerRegistry) Register(ext string, handler FileHandler) {
|
||||
r.handlers[ext] = handler
|
||||
}
|
||||
|
||||
func (r *FileHandlerRegistry) GetHandler(filename string) FileHandler {
|
||||
ext := filepath.Ext(filename)
|
||||
|
||||
if ext == "" || ext == "." {
|
||||
basename := filepath.Base(filename)
|
||||
if strings.HasPrefix(basename, "Dockerfile") {
|
||||
return r.handlers[".dockerfile"]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if handler, ok := r.handlers[ext]; ok {
|
||||
return handler
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadFileWithHighlight(file *os.File, filePath string) error {
|
||||
content := ReadFileContent(file)
|
||||
|
||||
lexer := lexers.Match(filepath.Base(filePath))
|
||||
if lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
|
||||
iterator, err := lexer.Tokenise(nil, content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error tokenizing: %w", err)
|
||||
}
|
||||
|
||||
formatter_ := formatters.TTY256
|
||||
style := styles.Get("monokai")
|
||||
if style == nil {
|
||||
style = styles.Fallback
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = formatter_.Format(&buf, style, iterator)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error formatting: %w", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(buf.String(), "\n")
|
||||
for i, line := range lines {
|
||||
if line != "" {
|
||||
fmt.Printf("%s%4d: %s%s\n", "\033[90m", i+1, "\033[0m", line)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormatSize converts bytes to human-readable format
|
||||
func FormatSize(bytes int64) string {
|
||||
if bytes == 0 {
|
||||
|
|
|
|||
|
|
@ -1,36 +1,30 @@
|
|||
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 {
|
||||
|
|
@ -39,7 +33,6 @@ func (r *CommandRegistry) GetAll() []*Command {
|
|||
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