Reset X11 button state on reconnect, read the Shift+AltGr console table, sync remote Caps Lock with macOS, and restore the FreeBSD stride test

This commit is contained in:
Viktor Liu
2026-09-23 10:18:13 +02:00
parent 395d6521f2
commit 8f158a0827
4 changed files with 77 additions and 11 deletions
@@ -6,6 +6,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// fb_depth is colour depth, not storage: a KMS console is depth 24 stored as
@@ -32,3 +33,33 @@ func TestFreebsdStorageBits(t *testing.T) {
})
}
}
// The row pitch is derived from the mapping, not computed from width and
// depth: a KMS framebuffer pads its rows, and reading at the unpadded width
// shears the image. A mapping too small for the reported geometry would have
// rows read off its end, so it is refused rather than guessed at.
func TestFreebsdFBStride(t *testing.T) {
tests := []struct {
name string
fbt fbType
want int
wantErr bool
}{
{"unpadded 32-bit", fbType{FbWidth: 1024, FbHeight: 768, FbDepth: 32, FbSize: 1024 * 768 * 4}, 4096, false},
{"padded rows", fbType{FbWidth: 1000, FbHeight: 768, FbDepth: 32, FbSize: 4096 * 768}, 4096, false},
{"packed 24-bit", fbType{FbWidth: 1024, FbHeight: 768, FbDepth: 24, FbSize: 1024 * 768 * 3}, 3072, false},
{"16-bit", fbType{FbWidth: 800, FbHeight: 600, FbDepth: 16, FbSize: 800 * 600 * 2}, 1600, false},
{"size cannot hold the geometry", fbType{FbWidth: 1024, FbHeight: 768, FbDepth: 32, FbSize: 1024 * 768 * 2}, 0, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := freebsdFBStride(tc.fbt)
if tc.wantErr {
require.Error(t, err, "a mapping smaller than the geometry must be refused")
return
}
require.NoError(t, err)
assert.Equal(t, tc.want, got, "row pitch in bytes")
})
}
}
+30 -3
View File
@@ -97,6 +97,9 @@ var (
// event, so the receiving app gets those characters whatever the active
// keyboard layout would have produced for the keycode.
cgEventKeyboardSetUnicodeString func(uintptr, uintptr, *uint16)
// cgEventSourceFlagsState reads the modifier flags macOS currently holds
// for an event-source state, which is where the real Caps Lock state is.
cgEventSourceFlagsState func(int32) uint64
// CGEventCreateScrollWheelEvent is variadic, call via SyscallN.
cgEventCreateScrollWheelEventAddr uintptr
@@ -171,6 +174,9 @@ func initDarwinInput() {
if sym, err := purego.Dlsym(cg, "CGEventKeyboardSetUnicodeString"); err == nil {
purego.RegisterFunc(&cgEventKeyboardSetUnicodeString, sym)
}
if sym, err := purego.Dlsym(cg, "CGEventSourceFlagsState"); err == nil {
purego.RegisterFunc(&cgEventSourceFlagsState, sym)
}
sym, err := purego.Dlsym(cg, "CGEventCreateScrollWheelEvent")
if err == nil {
@@ -616,13 +622,22 @@ func (m *MacInputInjector) postModifier(src uintptr, keycode uint16, down bool,
if capsLock && !down {
return
}
// Toggle from the state macOS actually has, not from the one this injector
// has built up: Caps Lock may already be on when the injector starts, or be
// toggled at the local keyboard in the meantime, and toggling a stale copy
// would post the state the system is already in instead of a transition.
systemCaps, haveSystemCaps := systemCapsLock()
var flags uint64
for {
old := m.modifiers.Load()
switch {
case capsLock:
flags = old ^ bit
current := old & bit
if haveSystemCaps {
current = systemCaps
}
flags = (old &^ bit) | (current ^ bit)
case down:
flags = old | bit
default:
@@ -893,8 +908,11 @@ func (m *MacInputInjector) SetClipboard(text string) {
// TypeText synthesizes the given text as keystrokes via Core Graphics.
// Lets a client push host clipboard content to the focused remote app
// even when the app doesn't honor pbpaste-style clipboard sync (e.g.
// login screens, locked-down apps). ASCII printable runes only; others
// are skipped.
// login screens, locked-down apps). Printable runes, non-ASCII included, are
// attached to the events as literal text, so they arrive as written whatever
// the host's keyboard layout. Return and Tab go out as their keys. Where the
// literal-text call is unavailable, ASCII falls back to US-layout keycodes and
// anything else is skipped.
func (m *MacInputInjector) TypeText(text string) {
// Same permission the other injection paths need: without it the posted
// events are swallowed, so asking here is what makes the prompt appear
@@ -1149,3 +1167,12 @@ func (m *MacInputInjector) typeUnicodeRune(src uintptr, r rune) bool {
}
return true
}
// systemCapsLock returns the Caps Lock bit macOS currently holds, and false
// when that cannot be read.
func systemCapsLock() (uint64, bool) {
if cgEventSourceFlagsState == nil {
return 0, false
}
return cgEventSourceFlagsState(kCGEventSourceStateCombinedSessionState) & kCGEventFlagMaskAlphaShift, true
}
+12 -8
View File
@@ -17,12 +17,14 @@ const consoleTTY = "/dev/tty0"
// kdgkbent is KDGKBENT from linux/kd.h, which reads one keymap entry.
const kdgkbent = 0x4B46
// Keymap tables (linux/keyboard.h): the character a key produces unmodified,
// with Shift, and with AltGr.
// Keymap tables (linux/keyboard.h), indexed by the modifier bits held: the
// character a key produces unmodified, with Shift (KG_SHIFT), with AltGr
// (KG_ALTGR), and with both.
const (
kNormTab = 0
kShiftTab = 1
kAltGrTab = 2
kNormTab = 0
kShiftTab = 1
kAltGrTab = 2
kShiftAltGrTab = 3
)
// Key types (linux/keyboard.h). KT_LATIN and KT_LETTER carry a Latin-1
@@ -50,9 +52,10 @@ type consoleKey struct {
}
// readConsoleKeymap reads the active console keymap and returns, for every
// printable character it can type, the key that types it. The unmodified table
// wins over the Shift one, and both over AltGr, so a character reachable more
// than one way gets the simplest.
// printable character it can type, the key that types it. Tables are read from
// fewest modifiers to most (plain, Shift, AltGr, Shift+AltGr), and the first
// one to produce a character wins, so a character reachable more than one way
// gets the simplest.
//
// The kernel decodes uinput key codes with this same keymap, which is what makes
// it the right source. A fixed US table types the wrong characters on any other
@@ -71,6 +74,7 @@ func readConsoleKeymap(tty string) (map[rune]consoleKey, error) {
{kNormTab, consoleKey{}},
{kShiftTab, consoleKey{shift: true}},
{kAltGrTab, consoleKey{altGr: true}},
{kShiftAltGrTab, consoleKey{shift: true, altGr: true}},
}
out := make(map[rune]consoleKey)
+4
View File
@@ -132,6 +132,10 @@ func (x *X11InputInjector) ensureConnLocked() {
x.conn = conn
x.root = screen.Root
x.screen = &screen
// The new server has no buttons down. Keeping the old mask would make a
// button still held across the restart look already pressed, so it would
// never get the ButtonPress it now needs.
x.lastButtons = 0
x.cacheKeyboardMapping()
log.Infof("X11 input injector reconnected (display=%s)", x.display)
}