fix(keymap): avoid Luau callback timeouts (#76)
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to Keymap are documented in this file.
|
||||||
|
|
||||||
|
## [1.3.1] - 2026-07-21
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Prevented startup timeouts while parsing larger Niri, Hyprland, and MangoWC configurations.
|
||||||
|
- Prevented intermittent callback timeouts when rapidly switching keyboard modifier layers.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Accelerated stable fingerprints with Luau's native `bit32` operations while retaining a plain-Lua fallback.
|
||||||
|
- Avoided reading Niri's root configuration twice during a refresh.
|
||||||
|
- Made loading snapshots lightweight instead of serializing the previous bind tree again.
|
||||||
|
- Cached panel translations, settings, keyboard indexes, colors, and dynamic key callbacks between renders.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Added 93-bind scale regressions for Niri, Hyprland, and MangoWC.
|
||||||
+15
-24
@@ -442,35 +442,30 @@ end
|
|||||||
|
|
||||||
-- FNV-1a over the exact source line. The editor recalculates this before a
|
-- FNV-1a over the exact source line. The editor recalculates this before a
|
||||||
-- write so a bind is never replaced after the file changed behind its back.
|
-- write so a bind is never replaced after the file changed behind its back.
|
||||||
-- Multiplication is split into 16-bit words to retain exact 32-bit arithmetic
|
-- Splitting the prime into 2^24 + 403 retains exact 32-bit arithmetic in
|
||||||
-- in runtimes where Lua numbers are doubles.
|
-- runtimes where Lua numbers are doubles.
|
||||||
local function xorLowByte(value, byte)
|
local function xorByte(left, right)
|
||||||
local low = value % 256
|
|
||||||
local result = 0
|
local result = 0
|
||||||
local place = 1
|
local place = 1
|
||||||
for _ = 1, 8 do
|
for _ = 1, 8 do
|
||||||
if low % 2 ~= byte % 2 then
|
if left % 2 ~= right % 2 then
|
||||||
result = result + place
|
result = result + place
|
||||||
end
|
end
|
||||||
low = math.floor(low / 2)
|
left = math.floor(left / 2)
|
||||||
byte = math.floor(byte / 2)
|
right = math.floor(right / 2)
|
||||||
place = place * 2
|
place = place * 2
|
||||||
end
|
end
|
||||||
return value - (value % 256) + result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local xorByteFast = type(bit32) == "table" and type(bit32.bxor) == "function" and bit32.bxor or xorByte
|
||||||
|
|
||||||
fingerprint = function(rawSnippet)
|
fingerprint = function(rawSnippet)
|
||||||
local hash = 2166136261
|
local hash = 2166136261
|
||||||
for index = 1, #rawSnippet do
|
for index = 1, #rawSnippet do
|
||||||
hash = xorLowByte(hash, rawSnippet:byte(index))
|
local low = hash % 256
|
||||||
local low = hash % 65536
|
hash = hash - low + xorByteFast(low, rawSnippet:byte(index))
|
||||||
local high = math.floor(hash / 65536)
|
hash = (hash * 403 + (hash % 256) * 16777216) % 4294967296
|
||||||
local lowProduct = low * 403
|
|
||||||
local resultLow = lowProduct % 65536
|
|
||||||
local resultHigh = (
|
|
||||||
math.floor(lowProduct / 65536) + low * 256 + high * 403
|
|
||||||
) % 65536
|
|
||||||
hash = resultHigh * 65536 + resultLow
|
|
||||||
end
|
end
|
||||||
return string.format("%08x", hash)
|
return string.format("%08x", hash)
|
||||||
end
|
end
|
||||||
@@ -918,15 +913,11 @@ function refresh()
|
|||||||
refreshing = true
|
refreshing = true
|
||||||
|
|
||||||
local source = configPath()
|
local source = configPath()
|
||||||
local previous = noctalia.state.get(SNAPSHOT_KEY)
|
-- The panel keeps its last ready snapshot while this status is loading.
|
||||||
local sameCompositor = type(previous) == "table" and previous.compositor == "MangoWC"
|
-- Avoid serializing the complete bind tree a second time on every refresh.
|
||||||
local previousCategories = sameCompositor and previous.categories or {}
|
|
||||||
local previousTotal = sameCompositor and previous.total or 0
|
|
||||||
local previousUpdatedAt = sameCompositor and previous.updated_at or ""
|
|
||||||
local previousHidden = sameCompositor and previous.hidden or {}
|
|
||||||
noctalia.state.set(
|
noctalia.state.set(
|
||||||
SNAPSHOT_KEY,
|
SNAPSHOT_KEY,
|
||||||
snapshot("loading", source, "", previousCategories, previousTotal, {}, previousUpdatedAt, previousHidden)
|
snapshot("loading", source, "", {}, 0, {}, "", {})
|
||||||
)
|
)
|
||||||
|
|
||||||
local parsed = parseConfig(source)
|
local parsed = parseConfig(source)
|
||||||
|
|||||||
+18
-17
@@ -452,14 +452,17 @@ local function xorByte(left, right)
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
-- FNV-1a 32-bit without bit libraries. Splitting the prime (0x01000193) into
|
local xorByteFast = type(bit32) == "table" and type(bit32.bxor) == "function" and bit32.bxor or xorByte
|
||||||
-- 2^24 + 403 keeps every intermediate below the exact integer limit of a
|
|
||||||
-- Lua/Luau number.
|
-- Splitting the FNV prime (0x01000193) into 2^24 + 403 keeps every
|
||||||
|
-- intermediate below the exact integer limit of a Lua/Luau number. Luau's
|
||||||
|
-- native bit32 fast path avoids eight interpreted operations for every byte;
|
||||||
|
-- the arithmetic fallback keeps the parser usable in plain-Lua tests.
|
||||||
local function stableFingerprint(value)
|
local function stableFingerprint(value)
|
||||||
local hash = 2166136261
|
local hash = 2166136261
|
||||||
for index = 1, #value do
|
for index = 1, #value do
|
||||||
local low = hash % 256
|
local low = hash % 256
|
||||||
hash = hash - low + xorByte(low, value:byte(index))
|
hash = hash - low + xorByteFast(low, value:byte(index))
|
||||||
hash = (hash * 403 + (hash % 256) * 16777216) % 4294967296
|
hash = (hash * 403 + (hash % 256) * 16777216) % 4294967296
|
||||||
end
|
end
|
||||||
return string.format("%08x", hash)
|
return string.format("%08x", hash)
|
||||||
@@ -688,7 +691,7 @@ local function parseBind(combo, attributes, action, category, records, activeByC
|
|||||||
activeByCombo[signature] = bind
|
activeByCombo[signature] = bind
|
||||||
end
|
end
|
||||||
|
|
||||||
local function parseConfig(root)
|
local function parseConfig(root, rootSource)
|
||||||
local records = {}
|
local records = {}
|
||||||
local hidden = {}
|
local hidden = {}
|
||||||
local activeByCombo = {}
|
local activeByCombo = {}
|
||||||
@@ -706,7 +709,7 @@ local function parseConfig(root)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local parseFile
|
local parseFile
|
||||||
parseFile = function(path, optional)
|
parseFile = function(path, optional, providedSource)
|
||||||
if visiting[path] then
|
if visiting[path] then
|
||||||
warn("niri_include_cycle:" .. path)
|
warn("niri_include_cycle:" .. path)
|
||||||
return
|
return
|
||||||
@@ -715,7 +718,7 @@ local function parseConfig(root)
|
|||||||
warn("niri_file_limit_reached")
|
warn("niri_file_limit_reached")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local source = noctalia.readFile(path)
|
local source = providedSource or noctalia.readFile(path)
|
||||||
if type(source) ~= "string" then
|
if type(source) ~= "string" then
|
||||||
warn((optional and "niri_optional_include_missing:" or "niri_include_unreadable:") .. path)
|
warn((optional and "niri_optional_include_missing:" or "niri_include_unreadable:") .. path)
|
||||||
if not optional then
|
if not optional then
|
||||||
@@ -858,7 +861,7 @@ local function parseConfig(root)
|
|||||||
visiting[path] = nil
|
visiting[path] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
parseFile(root, false)
|
parseFile(root, false, rootSource)
|
||||||
local categories, total = buildCategories(records)
|
local categories, total = buildCategories(records)
|
||||||
return categories, total, hidden, warnings, fatalError
|
return categories, total, hidden, warnings, fatalError
|
||||||
end
|
end
|
||||||
@@ -897,18 +900,16 @@ function refresh()
|
|||||||
end
|
end
|
||||||
refreshing = true
|
refreshing = true
|
||||||
local source = sourcePath()
|
local source = sourcePath()
|
||||||
local previous = noctalia.state.get(SNAPSHOT_KEY)
|
-- The panel keeps its last ready snapshot while this status is loading.
|
||||||
local keepPrevious = type(previous) == "table" and previous.compositor == "Niri"
|
-- Do not copy the full bind tree through the shared-state serializer merely
|
||||||
local previousCategories = keepPrevious and previous.categories or {}
|
-- to replace it again at the end of this synchronous refresh.
|
||||||
local previousTotal = keepPrevious and previous.total or 0
|
|
||||||
local previousUpdatedAt = keepPrevious and previous.updated_at or ""
|
|
||||||
local previousHidden = keepPrevious and previous.hidden or {}
|
|
||||||
noctalia.state.set(
|
noctalia.state.set(
|
||||||
SNAPSHOT_KEY,
|
SNAPSHOT_KEY,
|
||||||
snapshot("loading", source, "", previousCategories, previousTotal, {}, previousUpdatedAt, previousHidden)
|
snapshot("loading", source, "", {}, 0, {}, "", {})
|
||||||
)
|
)
|
||||||
|
|
||||||
if type(noctalia.readFile(source)) ~= "string" then
|
local rootSource = noctalia.readFile(source)
|
||||||
|
if type(rootSource) ~= "string" then
|
||||||
noctalia.state.set(
|
noctalia.state.set(
|
||||||
SNAPSHOT_KEY,
|
SNAPSHOT_KEY,
|
||||||
snapshot("error", source, "niri_config_unreadable", {}, 0, {}, os.date("%H:%M:%S"))
|
snapshot("error", source, "niri_config_unreadable", {}, 0, {}, os.date("%H:%M:%S"))
|
||||||
@@ -917,7 +918,7 @@ function refresh()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local categories, total, hidden, warnings, fatalError = parseConfig(source)
|
local categories, total, hidden, warnings, fatalError = parseConfig(source, rootSource)
|
||||||
if fatalError ~= nil then
|
if fatalError ~= nil then
|
||||||
noctalia.state.set(
|
noctalia.state.set(
|
||||||
SNAPSHOT_KEY,
|
SNAPSHOT_KEY,
|
||||||
|
|||||||
+64
-16
@@ -112,12 +112,36 @@ do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local NIL_CACHE_VALUE = {}
|
||||||
|
local configCache = {}
|
||||||
|
local translationCache = {}
|
||||||
|
|
||||||
local function tr(key, args)
|
local function tr(key, args)
|
||||||
return noctalia.tr(key, args)
|
if args ~= nil then
|
||||||
|
return noctalia.tr(key, args)
|
||||||
|
end
|
||||||
|
local cached = translationCache[key]
|
||||||
|
if cached ~= nil then
|
||||||
|
return cached
|
||||||
|
end
|
||||||
|
local value = noctalia.tr(key)
|
||||||
|
translationCache[key] = value
|
||||||
|
return value
|
||||||
end
|
end
|
||||||
|
|
||||||
local function cfg(key)
|
local function cfg(key)
|
||||||
return noctalia.getConfig(key)
|
local cached = configCache[key]
|
||||||
|
if cached ~= nil then
|
||||||
|
return cached ~= NIL_CACHE_VALUE and cached or nil
|
||||||
|
end
|
||||||
|
local value = noctalia.getConfig(key)
|
||||||
|
configCache[key] = value ~= nil and value or NIL_CACHE_VALUE
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clearHostValueCaches()
|
||||||
|
configCache = {}
|
||||||
|
translationCache = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local function asString(value, fallback)
|
local function asString(value, fallback)
|
||||||
@@ -297,7 +321,13 @@ local function expandedKeys(value)
|
|||||||
return { canonical }
|
return { canonical }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local keyboardIndexSnapshot = nil
|
||||||
|
local keyboardIndexCache = nil
|
||||||
|
|
||||||
local function keyboardIndex()
|
local function keyboardIndex()
|
||||||
|
if keyboardIndexSnapshot == snapshot and keyboardIndexCache ~= nil then
|
||||||
|
return keyboardIndexCache
|
||||||
|
end
|
||||||
local exact = {}
|
local exact = {}
|
||||||
local any = {}
|
local any = {}
|
||||||
for _, category in ipairs(asArray(snapshot.categories)) do
|
for _, category in ipairs(asArray(snapshot.categories)) do
|
||||||
@@ -318,14 +348,20 @@ local function keyboardIndex()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return { exact = exact, any = any }
|
keyboardIndexSnapshot = snapshot
|
||||||
|
keyboardIndexCache = { exact = exact, any = any }
|
||||||
|
return keyboardIndexCache
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local keyCallbackCache = {}
|
||||||
local function keyCallback(id, code)
|
local function keyCallback(id, code)
|
||||||
local name = "onKeyboardKey_" .. id
|
local name = "onKeyboardKey_" .. id
|
||||||
return registerDynamicCallback(name, function()
|
local callback = keyCallbackCache[name]
|
||||||
selectKeyboardKey(code)
|
if callback == nil then
|
||||||
end)
|
callback = function() selectKeyboardKey(code) end
|
||||||
|
keyCallbackCache[name] = callback
|
||||||
|
end
|
||||||
|
return registerDynamicCallback(name, callback)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function contains(haystack, needle)
|
local function contains(haystack, needle)
|
||||||
@@ -848,21 +884,32 @@ local function keyboardUnitsWidth(units)
|
|||||||
return math.max(1, math.floor(value * KEYBOARD_UNIT + (value - 1) * KEYBOARD_GAP + 0.5))
|
return math.max(1, math.floor(value * KEYBOARD_UNIT + (value - 1) * KEYBOARD_GAP + 0.5))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local colorWithOpacityCache = {}
|
||||||
local function colorWithOpacity(value, fallback, opacity)
|
local function colorWithOpacity(value, fallback, opacity)
|
||||||
|
local cacheKey = asString(value) .. "\0" .. asString(fallback) .. "\0" .. tostring(opacity)
|
||||||
|
local cached = colorWithOpacityCache[cacheKey]
|
||||||
|
if cached ~= nil then return cached end
|
||||||
local color = asString(value, fallback)
|
local color = asString(value, fallback)
|
||||||
|
local result
|
||||||
if string.sub(color, 1, 1) == "#" then
|
if string.sub(color, 1, 1) == "#" then
|
||||||
local alpha = string.format("%02X", math.floor(math.max(0, math.min(1, opacity)) * 255 + 0.5))
|
local alpha = string.format("%02X", math.floor(math.max(0, math.min(1, opacity)) * 255 + 0.5))
|
||||||
if #color == 7 then
|
if #color == 7 then
|
||||||
return color .. alpha
|
result = color .. alpha
|
||||||
elseif #color == 9 then
|
elseif #color == 9 then
|
||||||
return string.sub(color, 1, 7) .. alpha
|
result = string.sub(color, 1, 7) .. alpha
|
||||||
|
else
|
||||||
|
result = color
|
||||||
end
|
end
|
||||||
return color
|
else
|
||||||
|
result = color .. "/" .. string.format("%.2f", opacity)
|
||||||
end
|
end
|
||||||
return color .. "/" .. string.format("%.2f", opacity)
|
colorWithOpacityCache[cacheKey] = result
|
||||||
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
local function keyboardKeyNode(spec, index)
|
local EMPTY_ENTRIES = {}
|
||||||
|
|
||||||
|
local function keyboardKeyNode(spec, index, signature)
|
||||||
if spec.spacer ~= nil then
|
if spec.spacer ~= nil then
|
||||||
-- `ui.spacer` is flexible: inside a fixed-width row it absorbs remaining
|
-- `ui.spacer` is flexible: inside a fixed-width row it absorbs remaining
|
||||||
-- space and moves every key that follows it. Keyboard gaps must be rigid,
|
-- space and moves every key that follows it. Keyboard gaps must be rigid,
|
||||||
@@ -892,9 +939,8 @@ local function keyboardKeyNode(spec, index)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local code = canonicalKey(spec.code)
|
local code = canonicalKey(spec.code)
|
||||||
local signature = activeModifierSignature()
|
local exactEntries = index.exact[signature .. "|" .. code] or EMPTY_ENTRIES
|
||||||
local exactEntries = asArray(index.exact[signature .. "|" .. code])
|
local anyEntries = index.any[code] or EMPTY_ENTRIES
|
||||||
local anyEntries = asArray(index.any[code])
|
|
||||||
local physicalModifier = MODIFIER_ALIASES[normalized(spec.code)]
|
local physicalModifier = MODIFIER_ALIASES[normalized(spec.code)]
|
||||||
local modifierActive = physicalModifier ~= nil and activeModifiers[physicalModifier] == true
|
local modifierActive = physicalModifier ~= nil and activeModifiers[physicalModifier] == true
|
||||||
local selected = creatorOpen and creatorHasKey(code) or selectedKeyboardKey == code
|
local selected = creatorOpen and creatorHasKey(code) or selectedKeyboardKey == code
|
||||||
@@ -1571,7 +1617,7 @@ local function keyboardBody()
|
|||||||
for rowIndex, row in ipairs(layout.rows) do
|
for rowIndex, row in ipairs(layout.rows) do
|
||||||
local keys = {}
|
local keys = {}
|
||||||
for _, spec in ipairs(row) do
|
for _, spec in ipairs(row) do
|
||||||
keys[#keys + 1] = keyboardKeyNode(spec, index)
|
keys[#keys + 1] = keyboardKeyNode(spec, index, signature)
|
||||||
end
|
end
|
||||||
keyboardRows[#keyboardRows + 1] = ui.row({
|
keyboardRows[#keyboardRows + 1] = ui.row({
|
||||||
key = "keyboard-row-" .. layout.id .. "-" .. tostring(rowIndex),
|
key = "keyboard-row-" .. layout.id .. "-" .. tostring(rowIndex),
|
||||||
@@ -2627,7 +2673,7 @@ end
|
|||||||
render = function()
|
render = function()
|
||||||
renderCallbackNames = {}
|
renderCallbackNames = {}
|
||||||
local status = asString(snapshot.status, "idle")
|
local status = asString(snapshot.status, "idle")
|
||||||
local categories = filteredCategories()
|
local categories = viewMode == "list" and filteredCategories() or EMPTY_ENTRIES
|
||||||
local requestedColumns = math.max(1, math.min(4, math.floor(tonumber(cfg("columns")) or 3)))
|
local requestedColumns = math.max(1, math.min(4, math.floor(tonumber(cfg("columns")) or 3)))
|
||||||
local body
|
local body
|
||||||
if status == "error" then
|
if status == "error" then
|
||||||
@@ -2728,6 +2774,7 @@ noctalia.state.watch(UPDATE_RESULT_KEY, function(result)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
function onOpen(_context)
|
function onOpen(_context)
|
||||||
|
clearHostValueCaches()
|
||||||
local configuredLayout = asString(cfg("keyboard_layout"), "100")
|
local configuredLayout = asString(cfg("keyboard_layout"), "100")
|
||||||
keyboardLayoutId = KEYBOARD_LAYOUTS[configuredLayout] ~= nil and configuredLayout or "100"
|
keyboardLayoutId = KEYBOARD_LAYOUTS[configuredLayout] ~= nil and configuredLayout or "100"
|
||||||
local current = noctalia.state.get(SNAPSHOT_KEY)
|
local current = noctalia.state.get(SNAPSHOT_KEY)
|
||||||
@@ -2755,6 +2802,7 @@ function onOpen(_context)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function onConfigChanged()
|
function onConfigChanged()
|
||||||
|
clearHostValueCaches()
|
||||||
render()
|
render()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
id = "blackbartblues/keymap"
|
id = "blackbartblues/keymap"
|
||||||
name = "Keymap"
|
name = "Keymap"
|
||||||
version = "1.3.0"
|
version = "1.3.1"
|
||||||
plugin_api = 5
|
plugin_api = 5
|
||||||
author = "blackbartblues"
|
author = "blackbartblues"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
+6
-7
@@ -314,11 +314,13 @@ local function xorByte(left, right)
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local xorByteFast = type(bit32) == "table" and type(bit32.bxor) == "function" and bit32.bxor or xorByte
|
||||||
|
|
||||||
local function fingerprint(value)
|
local function fingerprint(value)
|
||||||
local hash = 2166136261
|
local hash = 2166136261
|
||||||
for index = 1, #value do
|
for index = 1, #value do
|
||||||
local low = hash % 256
|
local low = hash % 256
|
||||||
hash = hash - low + xorByte(low, value:byte(index))
|
hash = hash - low + xorByteFast(low, value:byte(index))
|
||||||
hash = (hash * 403 + (hash % 256) * 16777216) % 4294967296
|
hash = (hash * 403 + (hash % 256) * 16777216) % 4294967296
|
||||||
end
|
end
|
||||||
return string.format("%08x", hash)
|
return string.format("%08x", hash)
|
||||||
@@ -1003,14 +1005,11 @@ function refresh()
|
|||||||
refreshing = true
|
refreshing = true
|
||||||
|
|
||||||
local source = expandedConfigPath()
|
local source = expandedConfigPath()
|
||||||
local previous = noctalia.state.get(SNAPSHOT_KEY)
|
-- The panel keeps its last ready snapshot while this status is loading.
|
||||||
local previousCategories = type(previous) == "table" and previous.categories or {}
|
-- Avoid serializing the complete bind tree a second time on every refresh.
|
||||||
local previousTotal = type(previous) == "table" and previous.total or 0
|
|
||||||
local previousUpdatedAt = type(previous) == "table" and previous.updated_at or ""
|
|
||||||
local previousHidden = type(previous) == "table" and previous.hidden or {}
|
|
||||||
noctalia.state.set(
|
noctalia.state.set(
|
||||||
SNAPSHOT_KEY,
|
SNAPSHOT_KEY,
|
||||||
snapshot("loading", source, "", previousCategories, previousTotal, {}, previousUpdatedAt, previousHidden)
|
snapshot("loading", source, "", {}, 0, {}, "", {})
|
||||||
)
|
)
|
||||||
|
|
||||||
if not noctalia.commandExists("hyprctl") then
|
if not noctalia.commandExists("hyprctl") then
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
local sourceLines = {}
|
||||||
|
local liveBinds = {}
|
||||||
|
for index = 1, 93 do
|
||||||
|
if index % 20 == 1 then
|
||||||
|
sourceLines[#sourceLines + 1] = "-- " .. tostring(math.floor(index / 20) + 1) .. ". Group "
|
||||||
|
.. tostring(math.floor(index / 20) + 1)
|
||||||
|
end
|
||||||
|
sourceLines[#sourceLines + 1] = string.format(
|
||||||
|
'hl.bind("SUPER+Key%d", action, { description = "Action %d" })', index, index
|
||||||
|
)
|
||||||
|
liveBinds[#liveBinds + 1] = {
|
||||||
|
modmask = 64, key = "Key" .. tostring(index), description = "Action " .. tostring(index),
|
||||||
|
has_description = true, dispatcher = "__lua", arg = tostring(index), submap = "",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
local source = table.concat(sourceLines, "\n")
|
||||||
|
|
||||||
|
local values, watchers = {
|
||||||
|
["keymap.snapshot"] = {
|
||||||
|
status = "ready", compositor = "Hyprland", total = 1,
|
||||||
|
categories = { { name = "Previous", binds = { { id = "previous" } } } },
|
||||||
|
},
|
||||||
|
}, {}
|
||||||
|
local reads, xorCalls, loadingCategoryCount = 0, 0, -1
|
||||||
|
local originalBit32 = bit32
|
||||||
|
bit32 = {
|
||||||
|
bxor = function(left, right)
|
||||||
|
xorCalls = xorCalls + 1
|
||||||
|
local result, place = 0, 1
|
||||||
|
for _ = 1, 8 do
|
||||||
|
if left % 2 ~= right % 2 then result = result + place end
|
||||||
|
left = math.floor(left / 2)
|
||||||
|
right = math.floor(right / 2)
|
||||||
|
place = place * 2
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
noctalia = {
|
||||||
|
state = {
|
||||||
|
get = function(key) return values[key] end,
|
||||||
|
set = function(key, value)
|
||||||
|
if key == "keymap.snapshot" and value.status == "loading" then
|
||||||
|
loadingCategoryCount = #(value.categories or {})
|
||||||
|
end
|
||||||
|
values[key] = value
|
||||||
|
if watchers[key] ~= nil then watchers[key](value) end
|
||||||
|
end,
|
||||||
|
watch = function(key, callback) watchers[key] = callback end,
|
||||||
|
},
|
||||||
|
getConfig = function(key)
|
||||||
|
return ({
|
||||||
|
compositor = "hyprland", hyprland_config = "/fixture/hyprland.lua",
|
||||||
|
merge_sequential = false, show_undescribed = true,
|
||||||
|
})[key]
|
||||||
|
end,
|
||||||
|
getenv = function(key) return key == "HYPRLAND_INSTANCE_SIGNATURE" and "test" or "" end,
|
||||||
|
expandPath = function(path) return path end,
|
||||||
|
fileExists = function(path) return path == "/fixture/hyprland.lua" end,
|
||||||
|
listDir = function() return nil end,
|
||||||
|
readFile = function(path)
|
||||||
|
if path ~= "/fixture/hyprland.lua" then return nil end
|
||||||
|
reads = reads + 1
|
||||||
|
return source
|
||||||
|
end,
|
||||||
|
commandExists = function(command) return command == "hyprctl" end,
|
||||||
|
json = { decode = function() return liveBinds end },
|
||||||
|
runAsync = function(command, callback)
|
||||||
|
callback({ exitCode = 0, timedOut = false, stdout = "fixture" })
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
tr = function(key) return key == "category.other" and "Other" or key end,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(loadfile("service.luau"))()
|
||||||
|
bit32 = originalBit32
|
||||||
|
|
||||||
|
local snapshot = values["keymap.snapshot"]
|
||||||
|
assert(snapshot.status == "ready", "large Hyprland fixture did not parse")
|
||||||
|
assert(snapshot.total == 93, "large Hyprland fixture lost binds")
|
||||||
|
assert(#snapshot.categories == 5, "large Hyprland fixture lost category markers")
|
||||||
|
assert(reads == 1, "Hyprland root config should only be read once per refresh")
|
||||||
|
assert(loadingCategoryCount == 0, "loading snapshot should not reserialize the previous bind tree")
|
||||||
|
assert(xorCalls > 0, "Hyprland parser did not use the native-xor fingerprint path")
|
||||||
|
|
||||||
|
print("hypr scale tests: ok")
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
local sourceLines = {}
|
||||||
|
for index = 1, 93 do
|
||||||
|
if index % 20 == 1 then
|
||||||
|
sourceLines[#sourceLines + 1] = "# Group " .. tostring(math.floor(index / 20) + 1)
|
||||||
|
end
|
||||||
|
sourceLines[#sourceLines + 1] = string.format(
|
||||||
|
'bind=SUPER,Key%d,spawn_shell,true #"Action %d"', index, index
|
||||||
|
)
|
||||||
|
end
|
||||||
|
local source = table.concat(sourceLines, "\n")
|
||||||
|
|
||||||
|
local values, watchers = {
|
||||||
|
["keymap.snapshot"] = {
|
||||||
|
status = "ready", compositor = "MangoWC", total = 1,
|
||||||
|
categories = { { name = "Previous", binds = { { id = "previous" } } } },
|
||||||
|
},
|
||||||
|
}, {}
|
||||||
|
local reads, xorCalls, loadingCategoryCount = 0, 0, -1
|
||||||
|
local originalBit32 = bit32
|
||||||
|
bit32 = {
|
||||||
|
bxor = function(left, right)
|
||||||
|
xorCalls = xorCalls + 1
|
||||||
|
local result, place = 0, 1
|
||||||
|
for _ = 1, 8 do
|
||||||
|
if left % 2 ~= right % 2 then result = result + place end
|
||||||
|
left = math.floor(left / 2)
|
||||||
|
right = math.floor(right / 2)
|
||||||
|
place = place * 2
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
noctalia = {
|
||||||
|
state = {
|
||||||
|
get = function(key) return values[key] end,
|
||||||
|
set = function(key, value)
|
||||||
|
if key == "keymap.snapshot" and value.status == "loading" then
|
||||||
|
loadingCategoryCount = #(value.categories or {})
|
||||||
|
end
|
||||||
|
values[key] = value
|
||||||
|
if watchers[key] ~= nil then watchers[key](value) end
|
||||||
|
end,
|
||||||
|
watch = function(key, callback) watchers[key] = callback end,
|
||||||
|
},
|
||||||
|
getConfig = function(key)
|
||||||
|
return ({ compositor = "mangowc", mangowc_config = "/fixture/config.conf", merge_sequential = false })[key]
|
||||||
|
end,
|
||||||
|
getenv = function(key) return key == "MANGO_INSTANCE_SIGNATURE" and "test" or "" end,
|
||||||
|
expandPath = function(path) return path end,
|
||||||
|
fileExists = function(path) return path == "/fixture/config.conf" end,
|
||||||
|
listDir = function() return nil end,
|
||||||
|
readFile = function(path)
|
||||||
|
if path ~= "/fixture/config.conf" then return nil end
|
||||||
|
reads = reads + 1
|
||||||
|
return source
|
||||||
|
end,
|
||||||
|
tr = function(key, args)
|
||||||
|
if args ~= nil and args.action ~= nil then return args.action end
|
||||||
|
return key == "category.other" and "Other" or key
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(loadfile("mangowc_service.luau"))()
|
||||||
|
bit32 = originalBit32
|
||||||
|
|
||||||
|
local snapshot = values["keymap.snapshot"]
|
||||||
|
assert(snapshot.status == "ready", "large MangoWC fixture did not parse")
|
||||||
|
assert(snapshot.total == 93, "large MangoWC fixture lost binds")
|
||||||
|
assert(#snapshot.categories == 5, "large MangoWC fixture lost category markers")
|
||||||
|
assert(reads == 1, "MangoWC root config should only be read once per refresh")
|
||||||
|
assert(loadingCategoryCount == 0, "loading snapshot should not reserialize the previous bind tree")
|
||||||
|
assert(xorCalls > 0, "MangoWC parser did not use the native-xor fingerprint path")
|
||||||
|
|
||||||
|
print("MangoWC scale tests: ok")
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
local bindLines = { "binds {" }
|
||||||
|
for index = 1, 93 do
|
||||||
|
if index % 20 == 1 then
|
||||||
|
bindLines[#bindLines + 1] = ' // #"Group ' .. tostring(math.floor(index / 20) + 1) .. '"'
|
||||||
|
end
|
||||||
|
bindLines[#bindLines + 1] = string.format(
|
||||||
|
' Mod+Key%d repeat=false cooldown-ms=150 { focus-workspace %d; }', index, index
|
||||||
|
)
|
||||||
|
end
|
||||||
|
bindLines[#bindLines + 1] = "}"
|
||||||
|
local source = table.concat(bindLines, "\n")
|
||||||
|
|
||||||
|
local values, watchers = {
|
||||||
|
["keymap.snapshot"] = {
|
||||||
|
status = "ready", compositor = "Niri", total = 1,
|
||||||
|
categories = { { name = "Previous", binds = { { id = "previous" } } } },
|
||||||
|
},
|
||||||
|
}, {}
|
||||||
|
local reads = 0
|
||||||
|
local xorCalls = 0
|
||||||
|
local loadingCategoryCount = -1
|
||||||
|
local originalBit32 = bit32
|
||||||
|
bit32 = {
|
||||||
|
bxor = function(left, right)
|
||||||
|
xorCalls = xorCalls + 1
|
||||||
|
local result, place = 0, 1
|
||||||
|
for _ = 1, 8 do
|
||||||
|
if left % 2 ~= right % 2 then result = result + place end
|
||||||
|
left = math.floor(left / 2)
|
||||||
|
right = math.floor(right / 2)
|
||||||
|
place = place * 2
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
noctalia = {
|
||||||
|
state = {
|
||||||
|
get = function(key) return values[key] end,
|
||||||
|
set = function(key, value)
|
||||||
|
if key == "keymap.snapshot" and value.status == "loading" then
|
||||||
|
loadingCategoryCount = #(value.categories or {})
|
||||||
|
end
|
||||||
|
values[key] = value
|
||||||
|
if watchers[key] ~= nil then watchers[key](value) end
|
||||||
|
end,
|
||||||
|
watch = function(key, callback) watchers[key] = callback end,
|
||||||
|
},
|
||||||
|
getConfig = function(key)
|
||||||
|
return ({ compositor = "niri", niri_config = "/fixture/config.kdl", merge_sequential = false })[key]
|
||||||
|
end,
|
||||||
|
getenv = function(key) return key == "NIRI_SOCKET" and "test" or "" end,
|
||||||
|
fileExists = function(path) return path == "/fixture/config.kdl" end,
|
||||||
|
listDir = function() return nil end,
|
||||||
|
readFile = function(path)
|
||||||
|
if path ~= "/fixture/config.kdl" then return nil end
|
||||||
|
reads = reads + 1
|
||||||
|
return source
|
||||||
|
end,
|
||||||
|
tr = function(key, args)
|
||||||
|
if args ~= nil and args.action ~= nil then return args.action end
|
||||||
|
return key == "category.other" and "Other" or key
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(loadfile("niri_service.luau"))()
|
||||||
|
bit32 = originalBit32
|
||||||
|
|
||||||
|
local snapshot = values["keymap.snapshot"]
|
||||||
|
assert(snapshot.status == "ready", "large Niri fixture did not parse")
|
||||||
|
assert(snapshot.total == 93, "large Niri fixture lost binds")
|
||||||
|
assert(#snapshot.categories == 5, "large Niri fixture lost category markers")
|
||||||
|
assert(reads == 1, "Niri root config should only be read once per refresh")
|
||||||
|
assert(loadingCategoryCount == 0, "loading snapshot should not reserialize the previous bind tree")
|
||||||
|
assert(xorCalls > 0, "Niri parser did not use the native-xor fingerprint path")
|
||||||
|
|
||||||
|
for _, category in ipairs(snapshot.categories) do
|
||||||
|
for _, bind in ipairs(category.binds) do
|
||||||
|
assert(bind.id:match("^niri:[0-9a-f]+$"), "invalid bind fingerprint")
|
||||||
|
assert(bind.fingerprint:match("^[0-9a-f]+$"), "invalid source fingerprint")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
print("niri scale tests: ok")
|
||||||
+14
-11
@@ -756,21 +756,24 @@ processValidatedRequest = function(request)
|
|||||||
verifyAndReload(request, transaction)
|
verifyAndReload(request, transaction)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function contentFingerprint(value)
|
local function xorByte(left, right)
|
||||||
local function xorByte(left, right)
|
local result, place = 0, 1
|
||||||
local result, place = 0, 1
|
for _ = 1, 8 do
|
||||||
for _ = 1, 8 do
|
if left % 2 ~= right % 2 then result = result + place end
|
||||||
if left % 2 ~= right % 2 then result = result + place end
|
left = math.floor(left / 2)
|
||||||
left = math.floor(left / 2)
|
right = math.floor(right / 2)
|
||||||
right = math.floor(right / 2)
|
place = place * 2
|
||||||
place = place * 2
|
|
||||||
end
|
|
||||||
return result
|
|
||||||
end
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local xorByteFast = type(bit32) == "table" and type(bit32.bxor) == "function" and bit32.bxor or xorByte
|
||||||
|
|
||||||
|
local function contentFingerprint(value)
|
||||||
local hash = 2166136261
|
local hash = 2166136261
|
||||||
for index = 1, #value do
|
for index = 1, #value do
|
||||||
local low = hash % 256
|
local low = hash % 256
|
||||||
hash = hash - low + xorByte(low, value:byte(index))
|
hash = hash - low + xorByteFast(low, value:byte(index))
|
||||||
hash = (hash * 403 + (hash % 256) * 16777216) % 4294967296
|
hash = (hash * 403 + (hash % 256) * 16777216) % 4294967296
|
||||||
end
|
end
|
||||||
return string.format("%08x", hash)
|
return string.format("%08x", hash)
|
||||||
|
|||||||
Reference in New Issue
Block a user