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
|
||||
-- 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
|
||||
-- in runtimes where Lua numbers are doubles.
|
||||
local function xorLowByte(value, byte)
|
||||
local low = value % 256
|
||||
-- Splitting the prime into 2^24 + 403 retains exact 32-bit arithmetic in
|
||||
-- runtimes where Lua numbers are doubles.
|
||||
local function xorByte(left, right)
|
||||
local result = 0
|
||||
local place = 1
|
||||
for _ = 1, 8 do
|
||||
if low % 2 ~= byte % 2 then
|
||||
if left % 2 ~= right % 2 then
|
||||
result = result + place
|
||||
end
|
||||
low = math.floor(low / 2)
|
||||
byte = math.floor(byte / 2)
|
||||
left = math.floor(left / 2)
|
||||
right = math.floor(right / 2)
|
||||
place = place * 2
|
||||
end
|
||||
return value - (value % 256) + result
|
||||
return result
|
||||
end
|
||||
|
||||
local xorByteFast = type(bit32) == "table" and type(bit32.bxor) == "function" and bit32.bxor or xorByte
|
||||
|
||||
fingerprint = function(rawSnippet)
|
||||
local hash = 2166136261
|
||||
for index = 1, #rawSnippet do
|
||||
hash = xorLowByte(hash, rawSnippet:byte(index))
|
||||
local low = hash % 65536
|
||||
local high = math.floor(hash / 65536)
|
||||
local lowProduct = low * 403
|
||||
local resultLow = lowProduct % 65536
|
||||
local resultHigh = (
|
||||
math.floor(lowProduct / 65536) + low * 256 + high * 403
|
||||
) % 65536
|
||||
hash = resultHigh * 65536 + resultLow
|
||||
local low = hash % 256
|
||||
hash = hash - low + xorByteFast(low, rawSnippet:byte(index))
|
||||
hash = (hash * 403 + (hash % 256) * 16777216) % 4294967296
|
||||
end
|
||||
return string.format("%08x", hash)
|
||||
end
|
||||
@@ -918,15 +913,11 @@ function refresh()
|
||||
refreshing = true
|
||||
|
||||
local source = configPath()
|
||||
local previous = noctalia.state.get(SNAPSHOT_KEY)
|
||||
local sameCompositor = type(previous) == "table" and previous.compositor == "MangoWC"
|
||||
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 {}
|
||||
-- The panel keeps its last ready snapshot while this status is loading.
|
||||
-- Avoid serializing the complete bind tree a second time on every refresh.
|
||||
noctalia.state.set(
|
||||
SNAPSHOT_KEY,
|
||||
snapshot("loading", source, "", previousCategories, previousTotal, {}, previousUpdatedAt, previousHidden)
|
||||
snapshot("loading", source, "", {}, 0, {}, "", {})
|
||||
)
|
||||
|
||||
local parsed = parseConfig(source)
|
||||
|
||||
+18
-17
@@ -452,14 +452,17 @@ local function xorByte(left, right)
|
||||
return result
|
||||
end
|
||||
|
||||
-- FNV-1a 32-bit without bit libraries. Splitting the prime (0x01000193) into
|
||||
-- 2^24 + 403 keeps every intermediate below the exact integer limit of a
|
||||
-- Lua/Luau number.
|
||||
local xorByteFast = type(bit32) == "table" and type(bit32.bxor) == "function" and bit32.bxor or xorByte
|
||||
|
||||
-- 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 hash = 2166136261
|
||||
for index = 1, #value do
|
||||
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
|
||||
end
|
||||
return string.format("%08x", hash)
|
||||
@@ -688,7 +691,7 @@ local function parseBind(combo, attributes, action, category, records, activeByC
|
||||
activeByCombo[signature] = bind
|
||||
end
|
||||
|
||||
local function parseConfig(root)
|
||||
local function parseConfig(root, rootSource)
|
||||
local records = {}
|
||||
local hidden = {}
|
||||
local activeByCombo = {}
|
||||
@@ -706,7 +709,7 @@ local function parseConfig(root)
|
||||
end
|
||||
|
||||
local parseFile
|
||||
parseFile = function(path, optional)
|
||||
parseFile = function(path, optional, providedSource)
|
||||
if visiting[path] then
|
||||
warn("niri_include_cycle:" .. path)
|
||||
return
|
||||
@@ -715,7 +718,7 @@ local function parseConfig(root)
|
||||
warn("niri_file_limit_reached")
|
||||
return
|
||||
end
|
||||
local source = noctalia.readFile(path)
|
||||
local source = providedSource or noctalia.readFile(path)
|
||||
if type(source) ~= "string" then
|
||||
warn((optional and "niri_optional_include_missing:" or "niri_include_unreadable:") .. path)
|
||||
if not optional then
|
||||
@@ -858,7 +861,7 @@ local function parseConfig(root)
|
||||
visiting[path] = nil
|
||||
end
|
||||
|
||||
parseFile(root, false)
|
||||
parseFile(root, false, rootSource)
|
||||
local categories, total = buildCategories(records)
|
||||
return categories, total, hidden, warnings, fatalError
|
||||
end
|
||||
@@ -897,18 +900,16 @@ function refresh()
|
||||
end
|
||||
refreshing = true
|
||||
local source = sourcePath()
|
||||
local previous = noctalia.state.get(SNAPSHOT_KEY)
|
||||
local keepPrevious = type(previous) == "table" and previous.compositor == "Niri"
|
||||
local previousCategories = keepPrevious and previous.categories or {}
|
||||
local previousTotal = keepPrevious and previous.total or 0
|
||||
local previousUpdatedAt = keepPrevious and previous.updated_at or ""
|
||||
local previousHidden = keepPrevious and previous.hidden or {}
|
||||
-- The panel keeps its last ready snapshot while this status is loading.
|
||||
-- Do not copy the full bind tree through the shared-state serializer merely
|
||||
-- to replace it again at the end of this synchronous refresh.
|
||||
noctalia.state.set(
|
||||
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(
|
||||
SNAPSHOT_KEY,
|
||||
snapshot("error", source, "niri_config_unreadable", {}, 0, {}, os.date("%H:%M:%S"))
|
||||
@@ -917,7 +918,7 @@ function refresh()
|
||||
return
|
||||
end
|
||||
|
||||
local categories, total, hidden, warnings, fatalError = parseConfig(source)
|
||||
local categories, total, hidden, warnings, fatalError = parseConfig(source, rootSource)
|
||||
if fatalError ~= nil then
|
||||
noctalia.state.set(
|
||||
SNAPSHOT_KEY,
|
||||
|
||||
+64
-16
@@ -112,12 +112,36 @@ do
|
||||
end
|
||||
end
|
||||
|
||||
local NIL_CACHE_VALUE = {}
|
||||
local configCache = {}
|
||||
local translationCache = {}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
local function asString(value, fallback)
|
||||
@@ -297,7 +321,13 @@ local function expandedKeys(value)
|
||||
return { canonical }
|
||||
end
|
||||
|
||||
local keyboardIndexSnapshot = nil
|
||||
local keyboardIndexCache = nil
|
||||
|
||||
local function keyboardIndex()
|
||||
if keyboardIndexSnapshot == snapshot and keyboardIndexCache ~= nil then
|
||||
return keyboardIndexCache
|
||||
end
|
||||
local exact = {}
|
||||
local any = {}
|
||||
for _, category in ipairs(asArray(snapshot.categories)) do
|
||||
@@ -318,14 +348,20 @@ local function keyboardIndex()
|
||||
end
|
||||
end
|
||||
end
|
||||
return { exact = exact, any = any }
|
||||
keyboardIndexSnapshot = snapshot
|
||||
keyboardIndexCache = { exact = exact, any = any }
|
||||
return keyboardIndexCache
|
||||
end
|
||||
|
||||
local keyCallbackCache = {}
|
||||
local function keyCallback(id, code)
|
||||
local name = "onKeyboardKey_" .. id
|
||||
return registerDynamicCallback(name, function()
|
||||
selectKeyboardKey(code)
|
||||
end)
|
||||
local callback = keyCallbackCache[name]
|
||||
if callback == nil then
|
||||
callback = function() selectKeyboardKey(code) end
|
||||
keyCallbackCache[name] = callback
|
||||
end
|
||||
return registerDynamicCallback(name, callback)
|
||||
end
|
||||
|
||||
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))
|
||||
end
|
||||
|
||||
local colorWithOpacityCache = {}
|
||||
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 result
|
||||
if string.sub(color, 1, 1) == "#" then
|
||||
local alpha = string.format("%02X", math.floor(math.max(0, math.min(1, opacity)) * 255 + 0.5))
|
||||
if #color == 7 then
|
||||
return color .. alpha
|
||||
result = color .. alpha
|
||||
elseif #color == 9 then
|
||||
return string.sub(color, 1, 7) .. alpha
|
||||
result = string.sub(color, 1, 7) .. alpha
|
||||
else
|
||||
result = color
|
||||
end
|
||||
return color
|
||||
else
|
||||
result = color .. "/" .. string.format("%.2f", opacity)
|
||||
end
|
||||
return color .. "/" .. string.format("%.2f", opacity)
|
||||
colorWithOpacityCache[cacheKey] = result
|
||||
return result
|
||||
end
|
||||
|
||||
local function keyboardKeyNode(spec, index)
|
||||
local EMPTY_ENTRIES = {}
|
||||
|
||||
local function keyboardKeyNode(spec, index, signature)
|
||||
if spec.spacer ~= nil then
|
||||
-- `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,
|
||||
@@ -892,9 +939,8 @@ local function keyboardKeyNode(spec, index)
|
||||
end
|
||||
|
||||
local code = canonicalKey(spec.code)
|
||||
local signature = activeModifierSignature()
|
||||
local exactEntries = asArray(index.exact[signature .. "|" .. code])
|
||||
local anyEntries = asArray(index.any[code])
|
||||
local exactEntries = index.exact[signature .. "|" .. code] or EMPTY_ENTRIES
|
||||
local anyEntries = index.any[code] or EMPTY_ENTRIES
|
||||
local physicalModifier = MODIFIER_ALIASES[normalized(spec.code)]
|
||||
local modifierActive = physicalModifier ~= nil and activeModifiers[physicalModifier] == true
|
||||
local selected = creatorOpen and creatorHasKey(code) or selectedKeyboardKey == code
|
||||
@@ -1571,7 +1617,7 @@ local function keyboardBody()
|
||||
for rowIndex, row in ipairs(layout.rows) do
|
||||
local keys = {}
|
||||
for _, spec in ipairs(row) do
|
||||
keys[#keys + 1] = keyboardKeyNode(spec, index)
|
||||
keys[#keys + 1] = keyboardKeyNode(spec, index, signature)
|
||||
end
|
||||
keyboardRows[#keyboardRows + 1] = ui.row({
|
||||
key = "keyboard-row-" .. layout.id .. "-" .. tostring(rowIndex),
|
||||
@@ -2627,7 +2673,7 @@ end
|
||||
render = function()
|
||||
renderCallbackNames = {}
|
||||
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 body
|
||||
if status == "error" then
|
||||
@@ -2728,6 +2774,7 @@ noctalia.state.watch(UPDATE_RESULT_KEY, function(result)
|
||||
end)
|
||||
|
||||
function onOpen(_context)
|
||||
clearHostValueCaches()
|
||||
local configuredLayout = asString(cfg("keyboard_layout"), "100")
|
||||
keyboardLayoutId = KEYBOARD_LAYOUTS[configuredLayout] ~= nil and configuredLayout or "100"
|
||||
local current = noctalia.state.get(SNAPSHOT_KEY)
|
||||
@@ -2755,6 +2802,7 @@ function onOpen(_context)
|
||||
end
|
||||
|
||||
function onConfigChanged()
|
||||
clearHostValueCaches()
|
||||
render()
|
||||
end
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
id = "blackbartblues/keymap"
|
||||
name = "Keymap"
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
plugin_api = 5
|
||||
author = "blackbartblues"
|
||||
license = "MIT"
|
||||
|
||||
+6
-7
@@ -314,11 +314,13 @@ local function xorByte(left, right)
|
||||
return result
|
||||
end
|
||||
|
||||
local xorByteFast = type(bit32) == "table" and type(bit32.bxor) == "function" and bit32.bxor or xorByte
|
||||
|
||||
local function fingerprint(value)
|
||||
local hash = 2166136261
|
||||
for index = 1, #value do
|
||||
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
|
||||
end
|
||||
return string.format("%08x", hash)
|
||||
@@ -1003,14 +1005,11 @@ function refresh()
|
||||
refreshing = true
|
||||
|
||||
local source = expandedConfigPath()
|
||||
local previous = noctalia.state.get(SNAPSHOT_KEY)
|
||||
local previousCategories = type(previous) == "table" and previous.categories or {}
|
||||
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 {}
|
||||
-- The panel keeps its last ready snapshot while this status is loading.
|
||||
-- Avoid serializing the complete bind tree a second time on every refresh.
|
||||
noctalia.state.set(
|
||||
SNAPSHOT_KEY,
|
||||
snapshot("loading", source, "", previousCategories, previousTotal, {}, previousUpdatedAt, previousHidden)
|
||||
snapshot("loading", source, "", {}, 0, {}, "", {})
|
||||
)
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
local function contentFingerprint(value)
|
||||
local function xorByte(left, right)
|
||||
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
|
||||
local function xorByte(left, right)
|
||||
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
|
||||
|
||||
local xorByteFast = type(bit32) == "table" and type(bit32.bxor) == "function" and bit32.bxor or xorByte
|
||||
|
||||
local function contentFingerprint(value)
|
||||
local hash = 2166136261
|
||||
for index = 1, #value do
|
||||
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
|
||||
end
|
||||
return string.format("%08x", hash)
|
||||
|
||||
Reference in New Issue
Block a user