refactor(qr): draw the code centered with the image widget in a white margin, encoded once
This commit is contained in:
parent
b860df0a25
commit
23b45b9c16
3 changed files with 40 additions and 61 deletions
|
|
@ -2,27 +2,37 @@ package qr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"strings"
|
stdimage "image"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ayn2op/tview"
|
"github.com/ayn2op/tview"
|
||||||
|
"github.com/ayn2op/tview/box"
|
||||||
"github.com/ayn2op/tview/center"
|
"github.com/ayn2op/tview/center"
|
||||||
|
"github.com/ayn2op/tview/column"
|
||||||
|
"github.com/ayn2op/tview/image"
|
||||||
"github.com/ayn2op/tview/layout"
|
"github.com/ayn2op/tview/layout"
|
||||||
"github.com/ayn2op/tview/text"
|
"github.com/ayn2op/tview/text"
|
||||||
"github.com/ayn2op/tview/viewport"
|
|
||||||
"github.com/gdamore/tcell/v3"
|
"github.com/gdamore/tcell/v3"
|
||||||
"github.com/skip2/go-qrcode"
|
"github.com/gdamore/tcell/v3/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// quietZoneColor is the white of the margin around the code, as go-qrcode draws its light modules.
|
||||||
|
quietZoneColor = color.IsRGB | color.IsValid | 0xffffff
|
||||||
|
// quietZoneRows and quietZoneColumns make a margin of two modules on every side, as a row holds two.
|
||||||
|
quietZoneRows, quietZoneColumns = 1, 2
|
||||||
|
// statusSpacing is the blank rows between the code and the status.
|
||||||
|
statusSpacing = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
scrollState viewport.ScrollState
|
|
||||||
|
|
||||||
conn *gatewayConn
|
conn *gatewayConn
|
||||||
heartbeatInterval time.Duration
|
heartbeatInterval time.Duration
|
||||||
privateKey *rsa.PrivateKey
|
privateKey *rsa.PrivateKey
|
||||||
fingerprint string
|
fingerprint string
|
||||||
|
|
||||||
qrCode *qrcode.QRCode
|
// qrCode is the code at one pixel per module, made once as it arrives.
|
||||||
|
qrCode stdimage.Image
|
||||||
status string
|
status string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,9 +64,6 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
|
||||||
return m, closeConn(m.conn)
|
return m, closeConn(m.conn)
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case scrollMsg:
|
|
||||||
m.scrollState = viewport.ScrollState(msg)
|
|
||||||
return m, nil
|
|
||||||
|
|
||||||
case connCreateMsg:
|
case connCreateMsg:
|
||||||
m.conn = msg.conn
|
m.conn = msg.conn
|
||||||
|
|
@ -113,53 +120,21 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// halfBlock packs a vertical pair of QR pixels into one glyph, fitting two bitmap rows into a single terminal row.
|
// View centers the code above the status.
|
||||||
func halfBlock(top, bottom bool) rune {
|
|
||||||
switch [2]bool{top, bottom} {
|
|
||||||
case [2]bool{true, true}:
|
|
||||||
return '█'
|
|
||||||
case [2]bool{true, false}:
|
|
||||||
return '▀'
|
|
||||||
case [2]bool{false, true}:
|
|
||||||
return '▄'
|
|
||||||
default:
|
|
||||||
return ' '
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// View centers the code, which scrolls when it is taller than the tab.
|
|
||||||
func (m Model) View() tview.Widget {
|
func (m Model) View() tview.Widget {
|
||||||
code := m.code()
|
var qrCode tview.Widget
|
||||||
return center.New(
|
|
||||||
viewport.New(text.New(code).Wrapping(text.WrappingNone).AlignX(tview.HorizontalAlignmentCenter), m.scrollState).
|
|
||||||
Height(layout.Shrink).
|
|
||||||
Focused(true).
|
|
||||||
OnChange(func(s *viewport.ScrollState) tview.Msg { return scrollMsg(*s) }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// code returns the QR code above the status.
|
|
||||||
func (m Model) code() string {
|
|
||||||
var out strings.Builder
|
|
||||||
if m.qrCode != nil {
|
if m.qrCode != nil {
|
||||||
bitmap := m.qrCode.Bitmap()
|
// The white margin around the code, two modules wide, is what scanners find it by against a dark terminal.
|
||||||
for y := 0; y < len(bitmap); y += 2 {
|
qrCode = box.New(image.New(m.qrCode)).
|
||||||
if y > 0 {
|
Background(quietZoneColor).
|
||||||
out.WriteByte('\n')
|
Padding(quietZoneRows, quietZoneRows, quietZoneColumns, quietZoneColumns).
|
||||||
}
|
Width(layout.Shrink).
|
||||||
for x := range bitmap[y] {
|
Height(layout.Shrink)
|
||||||
top := bitmap[y][x]
|
|
||||||
bottom := y+1 < len(bitmap) && bitmap[y+1][x]
|
|
||||||
out.WriteRune(halfBlock(top, bottom))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if m.status != "" {
|
|
||||||
if out.Len() > 0 {
|
|
||||||
out.WriteString("\n\n")
|
|
||||||
}
|
|
||||||
out.WriteString(m.status)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return out.String()
|
return center.New(column.New(qrCode, text.New(m.status)).
|
||||||
|
Spacing(statusSpacing).
|
||||||
|
AlignX(tview.HorizontalAlignmentCenter).
|
||||||
|
Width(layout.Shrink).
|
||||||
|
Height(layout.Shrink))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ func TestModelView(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
m.qrCode = code
|
m.qrCode = code.Image(-1)
|
||||||
m.status = "status"
|
m.status = "status"
|
||||||
m.View().Draw(screen, tview.Rectangle{Width: width, Height: height})
|
m.View().Draw(screen, tview.Rectangle{Width: width, Height: height})
|
||||||
|
|
||||||
|
|
@ -42,5 +42,12 @@ func TestModelView(t *testing.T) {
|
||||||
if !strings.Contains(drawn.String(), "status") || !strings.ContainsAny(drawn.String(), "█▀▄") {
|
if !strings.Contains(drawn.String(), "status") || !strings.ContainsAny(drawn.String(), "█▀▄") {
|
||||||
t.Fatalf("missing content:\n%s", drawn.String())
|
t.Fatalf("missing content:\n%s", drawn.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The status is centered across the width, under the centered code.
|
||||||
|
for line := range strings.Lines(drawn.String()) {
|
||||||
|
if start := strings.Index(line, "status"); start >= 0 && (start < width/2-4 || start > width/2-2) {
|
||||||
|
t.Fatalf("status starts at %d, want it centered:\n%s", start, drawn.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
json "encoding/json/v2"
|
json "encoding/json/v2"
|
||||||
"errors"
|
"errors"
|
||||||
|
"image"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -18,7 +19,6 @@ import (
|
||||||
"github.com/ayn2op/discordo/internal/gateway"
|
"github.com/ayn2op/discordo/internal/gateway"
|
||||||
"github.com/ayn2op/discordo/internal/http"
|
"github.com/ayn2op/discordo/internal/http"
|
||||||
"github.com/ayn2op/tview"
|
"github.com/ayn2op/tview"
|
||||||
"github.com/ayn2op/tview/viewport"
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/skip2/go-qrcode"
|
"github.com/skip2/go-qrcode"
|
||||||
)
|
)
|
||||||
|
|
@ -231,7 +231,7 @@ func sendNonceProof(conn *gatewayConn, privateKey *rsa.PrivateKey, encryptedNonc
|
||||||
}
|
}
|
||||||
|
|
||||||
type qrCodeMsg struct {
|
type qrCodeMsg struct {
|
||||||
qrCode *qrcode.QRCode
|
qrCode image.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateQRCode(fingerprint string) tview.Cmd {
|
func generateQRCode(fingerprint string) tview.Cmd {
|
||||||
|
|
@ -242,7 +242,7 @@ func generateQRCode(fingerprint string) tview.Cmd {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
qrCode.DisableBorder = true
|
qrCode.DisableBorder = true
|
||||||
return qrCodeMsg{qrCode: qrCode}
|
return qrCodeMsg{qrCode: qrCode.Image(-1)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,8 +301,5 @@ func exchangeTicket(fingerprint string, privateKey *rsa.PrivateKey, ticket strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// scrollMsg scrolls the code.
|
|
||||||
type scrollMsg viewport.ScrollState
|
|
||||||
|
|
||||||
// startMsg starts connecting to the Remote Auth Gateway.
|
// startMsg starts connecting to the Remote Auth Gateway.
|
||||||
type startMsg struct{}
|
type startMsg struct{}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue