yeqown / go-qrcode

To help gophers generate QR Codes with customized styles, such as color, block size, block shape, and icon.
MIT License
567 stars 87 forks source link

[FEATURE] Terminal v2 suggestion #109

Closed alexballas closed 5 months ago

alexballas commented 6 months ago

Hello! Thanks for working on this very helpful package and for keeping the QR code stuff active in the Go ecosystem. I have a proposal and I'd like to hear your thoughts.

The current terminal writer uses Termbox, which is nice but difficult to integrate with for better control over CLI tools. It's especially challenging to display a QR code in the terminal without creating a whole new screen. Additionally, Termbox is no longer maintained.

I'm proposing a terminal v2 writer that uses *os.File for output. I chose *os.File instead of io.Writer because it has a better chance of displaying correctly. I also added a Bitmap() method, which I found useful when working with the skip2 package.

using this writer should be as simple as doing that

func main() {
    QRCodeItem, _ := qrcode.New("Hello!")
    termV2 := New(os.Stdout)
    _ = QRCodeItem.Save(termV2)
}

full example

package main

import (
    "errors"
    "os"

    "github.com/yeqown/go-qrcode/v2"
)

const (
    upRune     = '▀'
    downRune   = '▄'
    upDownRune = '█'
)

var _ qrcode.Writer = (*terminalv2)(nil)

func main() {
    QRCodeItem, _ := qrcode.New("Hello!")
    termV2 := New(os.Stdout)
    _ = QRCodeItem.Save(termV2)
}

type terminalv2 struct {
    out    *os.File
    bitmap [][]bool
}

func (a *terminalv2) Close() error { return nil }
func (a *terminalv2) Write(mat qrcode.Matrix) error {
    if a.out == nil {
        return errors.New("nil file")
    }

    a.bitmap = make([][]bool, mat.Height())
    for i := range a.bitmap {
        a.bitmap[i] = make([]bool, mat.Width())
    }
    mat.Iterate(qrcode.IterDirection_ROW, func(row, col int, s qrcode.QRValue) {
        a.bitmap[col][row] = s.IsSet()
    })

    bm := a.bitmap

    output := make([][]string, len(bm)/2+len(bm)%2)
    for i := range output {
        output[i] = make([]string, len(bm[0]))
    }

    for col := range output {
        for row := range output[col] {
            var selectedRune rune = ' '

            if bm[col*2][row] {
                selectedRune = upRune
            }

            if col*2+1 < len(bm) {
                if bm[col*2+1][row] && !bm[col*2][row] {
                    selectedRune = downRune
                }

                if bm[col*2+1][row] && bm[col*2][row] {
                    selectedRune = upDownRune
                }

                if !bm[col*2+1][row] && !bm[col*2][row] {
                    selectedRune = ' '
                }
            }

            output[col][row] = string(selectedRune)
        }
    }

    for _, col := range output {
        for _, row := range col {
            _, err := a.out.WriteString(row)
            if err != nil {
                return err
            }
        }
        _, err := a.out.WriteString("\n")
        if err != nil {
            return err
        }
    }

    return nil
}

func (a *terminalv2) Bitmap() [][]bool {
    return a.bitmap
}

func New(f *os.File) *terminalv2 {
    return &terminalv2{out: f}
}

Screenshot from 2024-05-20 16-53-24

If you're happy with it, I can raise a PR.

yeqown commented 6 months ago

It looks pretty good, feel free to raise a PR

yeqown commented 6 months ago

Maybe Bitmap can be added to the Matrix struct so that you don't have to iterate to get it?

alexballas commented 6 months ago

Sounds good, I'll proceed with the changes

alexballas commented 6 months ago

raised https://github.com/yeqown/go-qrcode/pull/110