├── go.mod ├── go.sum ├── README.md ├── main.go └── ansicat └── ansicat.go /go.mod: -------------------------------------------------------------------------------- 1 | module moul.io/ansicat 2 | 3 | go 1.22.1 4 | 5 | require golang.org/x/text v0.16.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 2 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ansicat 2 | 3 | Converts a file from code page 437 to UTF-8 and outputs it at a rate of 7000 4 | bytes per second. Perfect for displaying ANSI files! 5 | 6 | ## Usage 7 | 8 | ```sh 9 | ansicat 10 | ``` 11 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "moul.io/ansicat/ansicat" 9 | ) 10 | 11 | const ( 12 | defaultRateLimit = 7000 // bytes per second 13 | defaultChunkSize = 1024 // bytes 14 | ) 15 | 16 | func main() { 17 | // Define command-line flags 18 | rateLimit := flag.Int("rate-limit", defaultRateLimit, "Rate limit in bytes per second (0 for no limit)") 19 | chunkSize := flag.Int("chunk-size", defaultChunkSize, "Chunk size in bytes") 20 | flag.Parse() 21 | 22 | if flag.NArg() < 1 { 23 | fmt.Println("Usage: go run main.go [--rate-limit=] [--chunk-size=] ") 24 | return 25 | } 26 | 27 | filename := flag.Arg(0) 28 | var file *os.File 29 | var err error 30 | 31 | if filename == "-" { 32 | // Read from stdin 33 | file = os.Stdin 34 | } else { 35 | // Open file 36 | file, err = os.Open(filename) 37 | if err != nil { 38 | fmt.Printf("Error opening file: %v\n", err) 39 | return 40 | } 41 | defer file.Close() 42 | } 43 | 44 | // Process the ANSI file 45 | err = ansicat.ProcessFile(file, os.Stdout, *rateLimit, *chunkSize) 46 | if err != nil { 47 | fmt.Printf("Error processing file: %v\n", err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ansicat/ansicat.go: -------------------------------------------------------------------------------- 1 | package ansicat 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "time" 9 | 10 | "golang.org/x/text/encoding/charmap" 11 | ) 12 | 13 | const sauceSize = 128 // SAUCE metadata size 14 | 15 | // ProcessFile processes the ANSI file, converts it from CP437 to UTF-8, and writes to the output stream. 16 | func ProcessFile(input io.Reader, output io.Writer, rateLimit int, chunkSize int) error { 17 | // Read the entire file into memory 18 | data, err := io.ReadAll(input) 19 | if err != nil { 20 | return fmt.Errorf("error reading input: %w", err) 21 | } 22 | 23 | // Check for SAUCE metadata 24 | sauceBytes := 0 25 | if len(data) > sauceSize { 26 | sauceHeader := data[len(data)-sauceSize : len(data)-sauceSize+5] 27 | if string(sauceHeader) == "SAUCE" { 28 | comments := int(data[len(data)-sauceSize+5+94]) 29 | sauceBytes = 1 + 5 + comments*64 + 128 30 | } 31 | } 32 | 33 | // Adjust data to exclude SAUCE metadata if present 34 | if sauceBytes > 0 { 35 | data = data[:len(data)-sauceBytes] 36 | } 37 | 38 | // Convert CP437 to UTF-8 39 | decoder := charmap.CodePage437.NewDecoder() 40 | utf8Data, err := decoder.Bytes(data) 41 | if err != nil { 42 | return fmt.Errorf("error converting data: %w", err) 43 | } 44 | 45 | // Output the data with optional rate limiting 46 | reader := bufio.NewReader(bytes.NewReader(utf8Data)) 47 | var delay time.Duration 48 | if rateLimit > 0 && rateLimit >= chunkSize { 49 | delay = time.Second / (time.Duration(rateLimit) / time.Duration(chunkSize)) 50 | } else { 51 | rateLimit = 0 // Disable rate limiting if rate limit is smaller than chunk size 52 | } 53 | 54 | for { 55 | buf := make([]byte, chunkSize) 56 | n, err := reader.Read(buf) 57 | if n > 0 { 58 | _, writeErr := output.Write(buf[:n]) 59 | if writeErr != nil { 60 | return fmt.Errorf("error writing to output: %w", writeErr) 61 | } 62 | if rateLimit > 0 { 63 | time.Sleep(delay) 64 | } 65 | } 66 | if err == io.EOF { 67 | break 68 | } 69 | if err != nil { 70 | return fmt.Errorf("error reading data: %w", err) 71 | } 72 | } 73 | 74 | return nil 75 | } 76 | --------------------------------------------------------------------------------