Files
groundstation/internal/display/x11.go
T
piotr ffae3560c4
Build Ground Station / build (push) Successful in 26s
fix: resolve reply-bearing X11 liveness cookies with Reply (#2)
2026-10-02 10:42:07 +02:00

798 lines
27 KiB
Go

package display
import (
"context"
"encoding/binary"
"fmt"
"log"
"os/exec"
"strings"
"time"
"unicode/utf16"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/shape"
"github.com/BurntSushi/xgb/xproto"
)
const (
// iso10646FontPattern selects a Unicode-capable fixed font so the exact
// fallback text keeps its diacritics on screen.
iso10646FontPattern = "*fixed*iso10646-1"
// asciiFontName is the degradation path when no Unicode font exists;
// text drawn with it is folded to ASCII.
asciiFontName = "fixed"
// maxImageText16Units bounds one ImageText16 request to the protocol's
// 1-byte string length (2 bytes per unit).
maxImageText16Units = 127
// windowTypePropertyName is the EWMH property naming a window's role.
windowTypePropertyName = "_NET_WM_WINDOW_TYPE"
// layerMarkerPropertyName tags Ground Station layer windows so sibling
// layers — and their window-manager frames — never count as video
// output for each other.
layerMarkerPropertyName = "_GROUNDSTATION_LAYER"
)
// osdEventMask selects only repaint bookkeeping for the OSD overlay: no
// pointer, key or motion events, so the server never routes device input to
// the overlay even before its empty input shape is applied.
const osdEventMask = xproto.EventMaskExposure
// deviceInputEventMask are the pointer and key selection bits a layer above
// video must never select: device input belongs to the windows below.
const deviceInputEventMask = xproto.EventMaskKeyPress | xproto.EventMaskKeyRelease |
xproto.EventMaskButtonPress | xproto.EventMaskButtonRelease |
xproto.EventMaskPointerMotion
// LayerRole orders a Ground Station window against presented video output.
// The fallback/menu canvas stays below video; the OSD overlay stays above.
type LayerRole int
const (
// RoleBelow marks the fullscreen fallback/menu canvas under video.
RoleBelow LayerRole = iota
// RoleAbove marks the transparent, input-transparent OSD overlay above
// video.
RoleAbove
)
// layerMarkerValue is the layer marker payload identifying a role's window
// as our own.
func layerMarkerValue(role LayerRole) string {
if role == RoleAbove {
return "osd"
}
return "kiosk"
}
// markerIsGroundstation reports whether a layer marker payload marks one of
// our own windows: those are Ground Station layers, never video output.
func markerIsGroundstation(marker string) bool {
return marker == layerMarkerValue(RoleBelow) || marker == layerMarkerValue(RoleAbove)
}
// ewmhWindowTypeName is the EWMH window role announced for a layer. The OSD
// overlay announces DOCK, the always-above helper role, so window managers
// never decorate or bury it; the fallback canvas keeps announcing nothing,
// as before.
func ewmhWindowTypeName(role LayerRole) string {
if role == RoleAbove {
return "_NET_WM_WINDOW_TYPE_DOCK"
}
return ""
}
// fullscreenState is the wmctrl stacking state for a layer role.
func fullscreenState(role LayerRole) string {
if role == RoleAbove {
return "above"
}
return "below"
}
// stackModeForVideoAppear is where a layer goes relative to the first
// presented video window: the canvas parks below it, the OSD rises above it.
func stackModeForVideoAppear(role LayerRole) uint32 {
if role == RoleAbove {
return xproto.StackModeAbove
}
return xproto.StackModeBelow
}
// raisesOnVideoEnd reports whether a layer raises itself when video ends:
// only the below canvas needs that to make the fallback visible again; the
// OSD overlay is already above everything.
func raisesOnVideoEnd(role LayerRole) bool {
return role == RoleBelow
}
// transparentShapeKinds are the shape regions cleared to empty so the OSD
// overlay is transparent and passes input through: an empty input shape
// never receives pointer or key events, and an empty bounding shape presents
// nothing over the video below — with or without a compositor.
func transparentShapeKinds() []shape.Kind {
return []shape.Kind{shape.SkInput, shape.SkBounding}
}
// ewmhNonVideoTypes are EWMH auxiliary window roles that never count as
// video output: desktop shells, panels, menus, dialogs and notifications
// must not suppress the fallback. Windows without the property — typical
// for plain video output — count as video.
var ewmhNonVideoTypes = map[string]bool{
"DESKTOP": true,
"DOCK": true,
"TOOLBAR": true,
"MENU": true,
"UTILITY": true,
"SPLASH": true,
"DIALOG": true,
"NOTIFICATION": true,
"TOOLTIP": true,
"COMBO": true,
"DROPDOWN_MENU": true,
"POPUP_MENU": true,
}
// X11Opener connects to an X server (selected by DISPLAY in the environment)
// and maps one fullscreen kiosk window for the layer role it is built with:
// the fallback/menu canvas below video (RoleBelow) or the transparent,
// input-transparent OSD overlay above video (RoleAbove). It is the display
// target for the Viture screen; future targets such as MIPI/touch provide
// their own Opener and templates.
type X11Opener struct {
// PollInterval is how often X events are drained while waiting, so that
// cancellation stays responsive. Zero selects 100 ms.
PollInterval time.Duration
// Fullscreen applies the fullscreen window state. It defaults to
// shelling out to wmctrl; tests may replace it. A failure is logged and
// the window degrades to a plain window.
Fullscreen func(args []string) error
// Video is the shared decoded-video presence signal. The session feeds
// it from the video output window's own lifecycle on the root screen;
// templates consume it to show or hide themselves. When nil the opener
// tracks presence in a private signal.
Video *VideoPresence
// Role orders this layer's window against presented video: RoleBelow
// for the fallback/menu canvas, RoleAbove for the OSD overlay.
Role LayerRole
}
func (o X11Opener) presence() *VideoPresence {
if o.Video != nil {
return o.Video
}
return NewVideoPresence()
}
func (o X11Opener) applyFullscreen(args []string) error {
if o.Fullscreen != nil {
return o.Fullscreen(args)
}
return exec.Command("wmctrl", args...).Run()
}
func (o X11Opener) pollInterval() time.Duration {
if o.PollInterval > 0 {
return o.PollInterval
}
return 100 * time.Millisecond
}
// livenessRequest is one liveness round trip on a connection: it resolves
// its request and reports only the error.
type livenessRequest func() error
// connLivenessProbe returns the liveness round trip used after mapping a
// window and while a session waits for events. GetInputFocus carries a
// reply, so its cookie must be resolved with Reply: calling Check on a
// reply-bearing cookie fails at runtime with the XGB reply-cookie error
// ("Cannot call 'Check' on a cookie that is expecting a *reply*"), which
// killed every layer open and reconnect on the device.
func connLivenessProbe(conn *xgb.Conn) livenessRequest {
return func() error {
return resolveLiveness(xproto.GetInputFocus(conn))
}
}
// resolveLiveness consumes one issued reply-bearing liveness cookie: the
// reply is read and discarded, and only the error propagates. The cookie
// must be resolved with Reply exactly once — Check on a reply-bearing
// cookie returns the XGB reply-cookie error instead of ever reaching the
// server, so a probe resolved with Check fails on every call regardless of
// server health.
func resolveLiveness(cookie xproto.GetInputFocusCookie) error {
_, err := cookie.Reply()
return err
}
// confirmMapped runs the post-map liveness stage of Open: a successful
// reply proves the window is mapped and the server alive; a failure is
// wrapped as the map-kiosk-window error so the caller closes the
// connection and drops the mapped window.
func confirmMapped(probe livenessRequest) error {
if err := probe(); err != nil {
return fmt.Errorf("map kiosk window: %w", err)
}
return nil
}
// Open connects to the X server and maps the kiosk window. Failure is
// expected while the display manager is starting; the caller retries.
func (o X11Opener) Open(ctx context.Context) (Session, error) {
conn, err := xgb.NewConn()
if err != nil {
return nil, fmt.Errorf("connect X11: %w", err)
}
screen := xproto.Setup(conn).DefaultScreen(conn)
window, err := xproto.NewWindowId(conn)
if err != nil {
conn.Close()
return nil, fmt.Errorf("allocate window: %w", err)
}
// The OSD overlay selects no device input even before its empty input
// shape exists; the canvas keeps its legacy exposure-only mask.
eventMask := uint32(xproto.EventMaskExposure)
if o.Role == RoleAbove {
eventMask = osdEventMask
}
values := uint32(xproto.CwBackPixel | xproto.CwEventMask)
xproto.CreateWindow(conn, screen.RootDepth, window, screen.Root,
0, 0, screen.WidthInPixels, screen.HeightInPixels, 0,
xproto.WindowClassInputOutput, screen.RootVisual, values,
[]uint32{screen.BlackPixel, eventMask})
// Tag the window as a Ground Station layer and announce its EWMH role
// before mapping, so no observer or window manager ever sees an
// untagged frame of our own layers.
markerAtom := internAtom(conn, layerMarkerPropertyName)
if markerAtom != 0 {
marker := layerMarkerValue(o.Role)
xproto.ChangeProperty(conn, xproto.PropModeReplace, window, markerAtom,
xproto.AtomString, 8, uint32(len(marker)), []byte(marker))
}
if name := ewmhWindowTypeName(o.Role); name != "" {
typeProp := internAtom(conn, windowTypePropertyName)
roleAtom := internAtom(conn, name)
if typeProp != 0 && roleAtom != 0 {
value := make([]byte, 4)
binary.LittleEndian.PutUint32(value, uint32(roleAtom))
xproto.ChangeProperty(conn, xproto.PropModeReplace, window, typeProp,
xproto.AtomAtom, 32, 1, value)
}
}
// The OSD overlay must be transparent and input-transparent from its
// first mapped frame: the shapes are emptied before mapping, and a
// server without the shape extension never gets the overlay at all, so
// it can never present an unshaped frame over pilot video.
if o.Role == RoleAbove {
if err := applyTransparentShapes(conn, window); err != nil {
conn.Close()
return nil, err
}
}
xproto.MapWindow(conn, window)
// Watch the root screen so video output announces itself: a video
// window mapping is the first presented video frame, its unmap or
// destroy is video end. WFB and launcher processes are never consulted.
xproto.ChangeWindowAttributes(conn, screen.Root,
xproto.CwEventMask, []uint32{xproto.EventMaskSubstructureNotify})
// Round trip confirms the window is mapped and the server is alive;
// the reply-bearing cookie is resolved with Reply (see
// connLivenessProbe).
if err := confirmMapped(connLivenessProbe(conn)); err != nil {
conn.Close()
return nil, err
}
// A layer reconnect must never cover running video: if video windows
// are already presented, record presence and take our side relative to
// the topmost one before anything is drawn.
presence := o.presence()
typeAtom := internWindowTypeAtom(conn)
videoWindows := scanVideoWindows(conn, screen.Root, window, typeAtom, markerAtom)
if len(videoWindows) > 0 {
presence.SetActive(true)
stackWindowRelative(conn, window, videoWindows[len(videoWindows)-1],
stackModeForVideoAppear(o.Role))
} else {
presence.SetActive(false)
}
// Fullscreen on the root screen, on our side of the video window: pilot
// video owns rendering priority whenever it is available. The canvas
// raises itself only after video ends, to bring the fallback back; the
// OSD overlay is already above video. A missing wmctrl only degrades to
// a plain window and must not kill the layer.
if err := o.applyFullscreen(x11FullscreenArgs(uint32(window), o.Role)); err != nil {
log.Printf("kiosk fullscreen state: %v (window stays a plain window)", err)
}
gc, err := xproto.NewGcontextId(conn)
if err != nil {
conn.Close()
return nil, fmt.Errorf("allocate graphics context: %w", err)
}
font, err := xproto.NewFontId(conn)
if err != nil {
conn.Close()
return nil, fmt.Errorf("allocate font: %w", err)
}
// Prefer a Unicode font so the exact fallback text renders with its
// diacritics; without one, drawn text degrades to ASCII.
fontName, unicodeFont := selectFontName(conn)
xproto.OpenFont(conn, font, uint16(len(fontName)), fontName)
xproto.CreateGC(conn, gc, xproto.Drawable(window),
xproto.GcForeground|xproto.GcBackground|xproto.GcFont,
[]uint32{screen.WhitePixel, screen.BlackPixel, uint32(font)})
return &x11Session{
conn: conn,
window: window,
gc: gc,
width: int(screen.WidthInPixels),
height: int(screen.HeightInPixels),
black: screen.BlackPixel,
white: screen.WhitePixel,
poll: o.pollInterval(),
video: presence,
videoWindows: videoWindowSet(videoWindows),
typeAtom: typeAtom,
markerAtom: markerAtom,
role: o.Role,
unicodeFont: unicodeFont,
pollEvent: conn.PollForEvent,
probe: connLivenessProbe(conn),
}, nil
}
func videoWindowSet(windows []xproto.Window) map[xproto.Window]struct{} {
set := make(map[xproto.Window]struct{}, len(windows))
for _, w := range windows {
set[w] = struct{}{}
}
return set
}
func x11FullscreenArgs(window uint32, role LayerRole) []string {
return []string{"-i", "-r", fmt.Sprintf("0x%08x", window),
"-b", "add,fullscreen," + fullscreenState(role)}
}
// applyTransparentShapes empties the overlay's input and bounding shapes
// before it is mapped: it then never intercepts pointer or key input and
// never presents pixels over pilot video, with or without a compositor.
// Future OSD content will punch its output shape per drawn region.
func applyTransparentShapes(conn *xgb.Conn, window xproto.Window) error {
if err := shape.Init(conn); err != nil {
return fmt.Errorf("shape extension: %w", err)
}
for _, kind := range transparentShapeKinds() {
if err := shape.RectanglesChecked(conn, shape.SoSet, kind, 0,
window, 0, 0, nil).Check(); err != nil {
return fmt.Errorf("empty shape region %d: %w", kind, err)
}
}
return nil
}
// scanVideoWindows lists already-presented video windows on the root screen
// in bottom-to-top stacking order, so a layer reconnect can take its side
// relative to the topmost one and never cover running video.
func scanVideoWindows(conn *xgb.Conn, root, own xproto.Window, typeAtom, markerAtom xproto.Atom) []xproto.Window {
tree, err := xproto.QueryTree(conn, root).Reply()
if err != nil {
return nil
}
var windows []xproto.Window
for _, w := range tree.Children {
if w != own && presentedVideoWindow(conn, w, typeAtom, markerAtom) {
windows = append(windows, w)
}
}
return windows
}
// windowFacts is the presentation evidence gathered for one candidate
// window, so the video decision stays a pure function.
type windowFacts struct {
inputOutput bool
overrideRedirect bool
viewable bool
auxiliary bool
groundstation bool
}
// isPresentedVideoWindow decides whether a window is video output. Only a
// real, presented output window counts as video: input-only and
// override-redirect helpers such as menus or notifications never do, and
// neither do windows carrying an EWMH auxiliary window type or a Ground
// Station layer marker.
func isPresentedVideoWindow(f windowFacts) bool {
return f.inputOutput && !f.overrideRedirect && f.viewable &&
!f.auxiliary && !f.groundstation
}
// presentedVideoWindow gathers the presentation facts of w.
func presentedVideoWindow(conn *xgb.Conn, w xproto.Window, typeAtom, markerAtom xproto.Atom) bool {
attrs, err := xproto.GetWindowAttributes(conn, w).Reply()
if err != nil {
return false
}
return isPresentedVideoWindow(windowFacts{
inputOutput: attrs.Class == xproto.WindowClassInputOutput,
overrideRedirect: attrs.OverrideRedirect,
viewable: attrs.MapState == xproto.MapStateViewable,
auxiliary: auxiliaryWindowType(conn, w, typeAtom),
groundstation: groundstationLayerWindow(conn, w, markerAtom),
})
}
// markerWalkBounds bound the descendant search for layer markers: window
// managers reparent client windows into frame windows, so a Ground Station
// layer must be recognized through its frame, and the search must stay
// cheap even on a busy desktop screen. depth must cover our window below
// one window-manager frame; nodes bounds the total windows inspected.
func markerWalkBounds() (depth, nodes int) {
return 4, 64
}
// groundstationLayerWindow reports whether w itself or any window below it
// carries our layer marker. Window managers frame our windows, so the
// marker must be found through frame windows too: otherwise a framed
// Ground Station layer could count as video output and permanently
// suppress the fallback. When the search hits its bounds or the server
// refuses, it reports not-found, degrading to the plain window checks.
func groundstationLayerWindow(conn *xgb.Conn, w xproto.Window, markerAtom xproto.Atom) bool {
if markerAtom == 0 {
return false
}
depth, nodes := markerWalkBounds()
visited := 0
var walk func(xproto.Window, int) bool
walk = func(w xproto.Window, depth int) bool {
if visited >= nodes {
return false
}
visited++
if carriesLayerMarker(conn, w, markerAtom) {
return true
}
if depth <= 0 {
return false
}
tree, err := xproto.QueryTree(conn, w).Reply()
if err != nil {
return false
}
for _, child := range tree.Children {
if walk(child, depth-1) {
return true
}
}
return false
}
return walk(w, depth)
}
// carriesLayerMarker reports whether w itself carries our layer marker.
func carriesLayerMarker(conn *xgb.Conn, w xproto.Window, markerAtom xproto.Atom) bool {
reply, err := xproto.GetProperty(conn, false, w, markerAtom,
xproto.GetPropertyTypeAny, 0, 64).Reply()
if err != nil || reply.ValueLen == 0 {
return false
}
return markerIsGroundstation(string(reply.Value))
}
// internAtom resolves an atom by name, or 0 when the server refuses,
// disabling the checks that would use it.
func internAtom(conn *xgb.Conn, name string) xproto.Atom {
reply, err := xproto.InternAtom(conn, false,
uint16(len(name)), name).Reply()
if err != nil {
return 0
}
return reply.Atom
}
func internWindowTypeAtom(conn *xgb.Conn) xproto.Atom {
return internAtom(conn, windowTypePropertyName)
}
// auxiliaryWindowType reports whether the window carries an EWMH window
// type naming a non-video helper role. A missing property means the window
// counts as video.
func auxiliaryWindowType(conn *xgb.Conn, w xproto.Window, typeAtom xproto.Atom) bool {
if typeAtom == 0 {
return false
}
reply, err := xproto.GetProperty(conn, false, w, typeAtom,
xproto.GetPropertyTypeAny, 0, 32).Reply()
if err != nil || reply.ValueLen < 4 {
return false
}
for i := 0; i+4 <= len(reply.Value); i += 4 {
atom := xproto.Atom(binary.LittleEndian.Uint32(reply.Value[i : i+4]))
name, err := xproto.GetAtomName(conn, atom).Reply()
if err != nil {
continue
}
if ewmhNonVideoTypes[name.Name] {
return true
}
}
return false
}
// stackWindowRelative moves window to the given side of sibling.
func stackWindowRelative(conn *xgb.Conn, window, sibling xproto.Window, mode uint32) {
xproto.ConfigureWindow(conn, window,
xproto.ConfigWindowSibling|xproto.ConfigWindowStackMode,
[]uint32{uint32(sibling), mode})
}
// videoChange is the video-lifecycle meaning of one root structure event.
type videoChange int
const (
videoUnrelated videoChange = iota
videoAppeared
videoDisappeared
)
// classifyVideoChange maps a root structure event onto the video lifecycle.
// own is the kiosk window. The video output windows themselves are the
// presence signal: their maps are the first presented video output and
// their unmaps or destroys are video ends, so neither WFB nor launcher
// processes are ever consulted. Whether an announced window actually is
// video is decided by the session using the presented-window checks.
func classifyVideoChange(event xgb.Event, own xproto.Window) (videoChange, xproto.Window) {
switch e := event.(type) {
case xproto.MapNotifyEvent:
if e.Window == own {
return videoUnrelated, 0
}
return videoAppeared, e.Window
case xproto.UnmapNotifyEvent:
if e.Window == own {
return videoUnrelated, 0
}
return videoDisappeared, e.Window
case xproto.DestroyNotifyEvent:
return videoDisappeared, e.Window
}
return videoUnrelated, 0
}
// addVideoWindow records w as presented video output and reports whether
// presence changed (the set went from empty to non-empty).
func addVideoWindow(windows map[xproto.Window]struct{}, w xproto.Window) bool {
if _, tracked := windows[w]; tracked {
return false
}
wasEmpty := len(windows) == 0
windows[w] = struct{}{}
return wasEmpty
}
// removeVideoWindow drops w from the presented video windows and reports
// whether presence changed (the set became empty).
func removeVideoWindow(windows map[xproto.Window]struct{}, w xproto.Window) bool {
if _, tracked := windows[w]; !tracked {
return false
}
delete(windows, w)
return len(windows) == 0
}
// x11Session implements both Session and Canvas for one X11 connection.
type x11Session struct {
conn *xgb.Conn
window xproto.Window
gc xproto.Gcontext
width int
height int
black uint32
white uint32
poll time.Duration
video *VideoPresence
videoWindows map[xproto.Window]struct{}
typeAtom xproto.Atom
markerAtom xproto.Atom
role LayerRole
unicodeFont bool
// pollEvent drains one pending X event; conn.PollForEvent in
// production. Tests script it.
pollEvent func() (xgb.Event, xgb.Error)
// probe performs one reply-bearing liveness round trip; its error ends
// the session. connLivenessProbe in production. Tests script it.
probe livenessRequest
}
func (s *x11Session) Canvas() Canvas { return s }
func (s *x11Session) Size() (int, int) { return s.width, s.height }
func (s *x11Session) Clear() {
// ClearArea tiles the window background (black, set at creation); it
// involves no graphics context.
xproto.ClearArea(s.conn, false, s.window, 0, 0,
uint16(s.width), uint16(s.height))
}
func (s *x11Session) Text(x, y int, message string) {
xproto.ChangeGC(s.conn, s.gc, xproto.GcForeground, []uint32{s.white})
if s.unicodeFont {
chars := utf16Char2b(message, maxImageText16Units)
xproto.ImageText16(s.conn, byte(len(chars)), xproto.Drawable(s.window), s.gc,
int16(x), int16(y), chars)
return
}
message = foldToASCII(message)
if len(message) > 255 {
message = message[:255]
}
xproto.ImageText8(s.conn, byte(len(message)), xproto.Drawable(s.window), s.gc,
int16(x), int16(y), message)
}
func (s *x11Session) Flush() {
s.conn.Sync()
}
func (s *x11Session) WaitEvent(ctx context.Context) (bool, error) {
ticker := time.NewTicker(s.poll)
defer ticker.Stop()
for {
for {
event, err := s.pollEvent()
if err != nil {
return false, err
}
if event == nil {
break
}
if x11WantsRedraw(event) {
return true, nil
}
if s.applyVideoChange(event) {
return true, nil
}
}
// Round trip keeps the connection liveness visible even without
// incoming events; the reply-bearing cookie is resolved with Reply
// (see connLivenessProbe).
if err := s.probe(); err != nil {
return false, err
}
select {
case <-ctx.Done():
return false, ctx.Err()
case <-ticker.C:
}
}
}
// applyVideoChange folds one root structure event into the shared video
// presence and restacks this layer so video keeps rendering priority: the
// canvas parks below video and the OSD overlay rises above it. It reports
// whether the template must be redrawn: video appearing makes the fallback
// disappear and video ending brings it back. Presence follows the set of
// presented video windows, so an unrelated window appearing or ending can
// never hide or resurrect a template on its own.
func (s *x11Session) applyVideoChange(event xgb.Event) bool {
change, w := classifyVideoChange(event, s.window)
switch change {
case videoAppeared:
// Menus, notifications, other auxiliary windows and our own sibling
// layers never count as video; only a presented output window does.
if !presentedVideoWindow(s.conn, w, s.typeAtom, s.markerAtom) {
return false
}
if !addVideoWindow(s.videoWindows, w) {
return false
}
// The first presented video output takes priority: park on our side
// of it.
stackWindowRelative(s.conn, s.window, w, stackModeForVideoAppear(s.role))
return s.video.SetActive(true)
case videoDisappeared:
if !removeVideoWindow(s.videoWindows, w) {
return false
}
// Only the below canvas raises itself so the fallback returns
// visibly even if some other window was stacked above it; the OSD
// overlay is already above video.
if raisesOnVideoEnd(s.role) {
xproto.ConfigureWindow(s.conn, s.window,
xproto.ConfigWindowStackMode, []uint32{xproto.StackModeAbove})
}
return s.video.SetActive(false)
default:
return false
}
}
func x11WantsRedraw(event xgb.Event) bool {
switch event.(type) {
case xproto.ExposeEvent:
return true
default:
return false
}
}
// selectFontName resolves a Unicode-capable font for the fallback text. It
// reports false when none is installed, in which case drawn text is folded
// to ASCII.
func selectFontName(conn *xgb.Conn) (string, bool) {
reply, err := xproto.ListFontsWithInfo(conn, 1,
uint16(len(iso10646FontPattern)), iso10646FontPattern).Reply()
if err != nil || reply.NameLen == 0 {
return asciiFontName, false
}
return reply.Name, true
}
// utf16Char2b encodes the message as UTF-16 units for ImageText16 so the
// exact fallback text keeps its diacritics, truncating to maxUnits because
// the request carries a 1-byte string length. A cut never splits a
// surrogate pair.
func utf16Char2b(message string, maxUnits int) []xproto.Char2b {
if maxUnits <= 0 {
return nil
}
units := utf16.Encode([]rune(message))
if len(units) > maxUnits {
units = units[:maxUnits]
if last := units[len(units)-1]; last >= 0xD800 && last <= 0xDBFF {
units = units[:len(units)-1]
}
}
chars := make([]xproto.Char2b, len(units))
for i, unit := range units {
chars[i] = xproto.Char2b{Byte1: byte(unit >> 8), Byte2: byte(unit)}
}
return chars
}
// latinFolds maps the Polish diacritics to plain ASCII for fonts without
// Unicode glyphs.
var latinFolds = map[rune]byte{
'ą': 'a', 'ć': 'c', 'ę': 'e', 'ł': 'l', 'ń': 'n', 'ó': 'o', 'ś': 's', 'ź': 'z', 'ż': 'z',
'Ą': 'A', 'Ć': 'C', 'Ę': 'E', 'Ł': 'L', 'Ń': 'N', 'Ó': 'O', 'Ś': 'S', 'Ź': 'Z', 'Ż': 'Z',
}
// foldToASCII degrades text for legacy fonts: Polish diacritics fold to
// their base letters and other non-ASCII runes are dropped.
func foldToASCII(message string) string {
ascii := true
for _, r := range message {
if r >= 0x80 {
ascii = false
break
}
}
if ascii {
return message
}
var b strings.Builder
for _, r := range message {
if r < 0x80 {
b.WriteRune(r)
continue
}
if folded, ok := latinFolds[r]; ok {
b.WriteByte(folded)
}
}
return b.String()
}
func (s *x11Session) Close() error {
// Closing the connection makes the X server destroy the kiosk window.
s.conn.Close()
return nil
}