tailscale/util/qrcodes/qrcodes.go
Simon Law dbaf0b0472
cmd/tailscale/cli, util/qrcodes: format QR codes on Linux consoles
Raw Linux consoles support UTF-8, but we cannot assume that all UTF-8
characters are available. The default Fixed and Terminus fonts don’t
contain half-block characters (`▀` and `▄`), but do contain the
full-block character (`█`).

Sometimes, Linux doesn’t have a framebuffer, so it falls back to VGA.
When this happens, the full-block character could be anywhere in
extended ASCII block, because we don’t know which code page is active.

This PR introduces `--qr-format=auto` which tries to heuristically
detect when Tailscale is printing to a raw Linux console, whether
UTF-8 is enabled, and which block characters have been mapped in the
console font.

If Unicode characters are unavailable, the new `--qr-format=ascii`
formatter uses `#` characters instead of full-block characters.

Fixes #12935

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2025-12-10 18:52:00 -08:00

68 lines
1.5 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package qrcodes formats QR codes according to the active terminal.
package qrcodes
import (
"fmt"
"io"
"strings"
qrcode "github.com/skip2/go-qrcode"
)
// --qr-format values
type Format string
const (
FormatAuto Format = "auto"
FormatASCII Format = "ascii"
FormatLarge Format = "large"
FormatSmall Format = "small"
)
// Fprintln formats s according to [Format] and writes to w and a newline is
// appended. It returns the number of bytes written and any write error
// encountered.
func Fprintln(w io.Writer, format Format, s string) (n int, err error) {
const inverse = false // Modern scanners can read QR codes of any colour.
q, err := qrcode.New(s, qrcode.Medium)
if err != nil {
return 0, fmt.Errorf("QR code error: %w", err)
}
if format == FormatAuto {
format, err = detectFormat(w, inverse)
if err != nil {
return 0, fmt.Errorf("QR code error: %w", err)
}
}
var out string
switch format {
case FormatASCII:
out = q.ToString(inverse)
out = strings.ReplaceAll(out, "█", "#")
case FormatLarge:
out = q.ToString(inverse)
case FormatSmall:
out = q.ToSmallString(inverse)
default:
return 0, fmt.Errorf("unknown QR code format: %q", format)
}
return fmt.Fprintln(w, out)
}
// EncodePNG renders a QR code for s as a PNG, with a width and height of size
// pixels.
func EncodePNG(s string, size int) ([]byte, error) {
q, err := qrcode.New(s, qrcode.Medium)
if err != nil {
return nil, err
}
return q.PNG(size)
}