diff --git a/cmd/markdown.go b/cmd/markdown.go new file mode 100644 index 0000000..5255487 --- /dev/null +++ b/cmd/markdown.go @@ -0,0 +1,130 @@ +package cmd + +import ( + "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" +) + +func MarkdownCommand(args []string) error { + if len(args) < 1 { + return fmt.Errorf("please specify a markdown 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, 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) + 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() + + lines := strings.Split(content, "\n") + inCodeBlock := false + codeBuffer := &strings.Builder{} + codeLang := "" + + for i, line := range lines { + if strings.HasPrefix(line, "```") { + if !inCodeBlock { + inCodeBlock = true + codeLang = strings.TrimPrefix(line, "```") + codeBuffer.Reset() + } else { + inCodeBlock = false + renderCodeBlock(codeBuffer.String(), codeLang) + codeBuffer.Reset() + codeLang = "" + } + continue + } + + if inCodeBlock { + if codeBuffer.Len() > 0 { + codeBuffer.WriteString("\n") + } + codeBuffer.WriteString(line) + continue + } + + if strings.HasPrefix(line, "# ") { + fmt.Printf("%s%s%s%s\n", colors.Bold, colors.Yellow, line[2:], colors.Reset) + } else if strings.HasPrefix(line, "## ") { + fmt.Printf("%s%s%s%s\n", colors.Bold, colors.Green, line[3:], colors.Reset) + } else if strings.HasPrefix(line, "### ") { + fmt.Printf("%s%s%s%s\n", colors.Bold, colors.Cyan, line[4:], colors.Reset) + } else if strings.HasPrefix(line, "- [ ]") { + fmt.Printf(" %s☐%s %s\n", colors.Gray, colors.Reset, line[5:]) + } else if strings.HasPrefix(line, "- [x]") { + fmt.Printf(" %s☑%s %s\n", colors.Green, colors.Reset, line[5:]) + } else if strings.HasPrefix(line, "- ") && !strings.HasPrefix(line, "- [") { + fmt.Printf(" %s•%s %s\n", colors.Blue, colors.Reset, line[2:]) + } else if strings.HasPrefix(line, "> ") { + fmt.Printf("%s%s%s%s\n", colors.Gray, colors.Italic, line[2:], colors.Reset) + } else if strings.TrimSpace(line) != "" { + fmt.Printf("%s\n", line) + } else { + fmt.Println() + } + _ = i + } + + return nil +} + +func renderCodeBlock(code, lang string) { + if code == "" { + return + } + + lexer := lexers.Match(lang) + if lexer == nil { + lexer = lexers.Fallback + } + + iterator, _ := lexer.Tokenise(nil, code) + formatter := formatters.TTY256 + style := styles.Get("monokai") + if style == nil { + style = styles.Fallback + } + + var buf strings.Builder + formatter.Format(&buf, style, iterator) + + output := buf.String() + for _, line := range strings.Split(output, "\n") { + if line != "" { + fmt.Printf("%s%s│ %s%s\n", colors.Gray, colors.Dim, colors.Reset, line) + } + } + fmt.Println() +} \ No newline at end of file diff --git a/cmd/read.go b/cmd/read.go new file mode 100644 index 0000000..228ed53 --- /dev/null +++ b/cmd/read.go @@ -0,0 +1,77 @@ +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" +) + +func ReadCommand(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, not a readable file", filePath) + } + + content := formatter.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 + } + + 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 +} + diff --git a/go.mod b/go.mod index e4ea190..733884d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module github.com/zemenawi/zutils go 1.22.0 + +require ( + github.com/alecthomas/chroma/v2 v2.24.1 // indirect + github.com/dlclark/regexp2 v1.12.0 // indirect + github.com/yuin/goldmark v1.8.2 // indirect +) diff --git a/go.sum b/go.sum index e69de29..5884727 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,6 @@ +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/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= diff --git a/main.go b/main.go index 9be63c1..1de7dce 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,16 @@ func main() { registry := types.NewCommandRegistry() // Register all commands + registry.Register(&types.Command{ + Name: "read", + Description: "Read and display file with syntax highlighting", + Handler: cmd.ReadCommand, + }) + registry.Register(&types.Command{ + Name: "md", + Description: "Render markdown file to terminal", + Handler: cmd.MarkdownCommand, + }) cmd.RegisterInfoCommands(registry) registry.Register(&types.Command{ Name: "version", @@ -74,9 +84,10 @@ func printUsage(registry *types.CommandRegistry) { } 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(" 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() } diff --git a/pkg/colors/colors.go b/pkg/colors/colors.go index 51046e1..37ac7d4 100644 --- a/pkg/colors/colors.go +++ b/pkg/colors/colors.go @@ -1,13 +1,16 @@ 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" + 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" + Italic = "\033[3m" + Dim = "\033[2m" + Underline = "\033[4m" ) diff --git a/pkg/formatter/utils.go b/pkg/formatter/utils.go index 779e49e..a913fa4 100644 --- a/pkg/formatter/utils.go +++ b/pkg/formatter/utils.go @@ -3,6 +3,7 @@ package formatter import ( "fmt" "math" + "os" ) // FormatSize converts bytes to human-readable format @@ -43,3 +44,18 @@ func repeat(s string, count int) string { } return result } + +func ReadFileContent(file *os.File) string { + content := make([]byte, 0) + buffer := make([]byte, 4096) + for { + n, err := file.Read(buffer) + if n > 0 { + content = append(content, buffer[:n]...) + } + if err != nil { + break + } + } + return string(content) +}