KeyUI

Visualizes and manages keyboard and mouse key configurations.
Back to Files

v4.3.0

File nameKeyUI-v4.3.0.zip
Uploader
BlandrosBlandros
Uploaded
Oct 10, 2026
Downloads
945
Size
3.9 MB
Flavors
MoP ClassicRetailClassic TBCClassic
File ID
9123354
Type
R
Release
Supported game versions
  • 12.1.5
  • 12.1.0
  • 5.5.4
  • 2.5.6
  • 1.15.9

What's new

KeyUI

v4.3.0 (2026-10-10)

Full Changelog Previous Releases

  • fix: name edit-mode mouse keys the way bindings do
    Clicking a key in edit mode with a mouse button other than the left one
    renames it to whatever OnMouseDown reported. Only MiddleButton was
    translated, so mouse button 4 produced a key called "Button4" -- no binding
    has that name, so the key always looked unbound. Translate every ButtonN to
    BUTTONn, the names the binding system and the shipped mouse layouts use.
    Right-click went through the same path, and it also opens the context menu
    on release: the key was renamed to "RightButton" just before the menu for
    it appeared. In edit mode right-click now only opens the menu.
    Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
  • refactor: set the right-click no-op once when a key button is built
    76e1f75 set type2 = ATTRIBUTE_NOOP in SetButtonActionSlot, which runs for
    every button on every refresh_keys -- often twice, once from
    reset_button_state and once with the real slot. The value never changes and
    nothing else touches type2, so set it once in create_*_buttons, next to the
    RegisterForClicks("AnyUp") it exists to answer.
    Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
  • perf: release discarded font strings in one pass per batch
    ReleaseFontStrings walked the whole font registry, and each entry's parent
    chain, once per call -- and clear_key_button_pool called it once per pooled
    key button. A settings reset with about 140 buttons and a registry of a
    similar size cost on the order of 100k GetParent calls in a single frame.
    It now takes a set of owners, and both teardown paths collect theirs first:
    four passes per reset instead of one per button.
    The registry is keyed by object, which only deduplicates the three named
    tab fonts if CreateFont hands back the existing font for a taken name. Reuse
    the global by name first, so a rebuild keeps one entry either way.
    Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
  • fix: draw the layout the selector calls active
    GetCurrentLayout picked the alphabetically first entry of a layout_current_*
    container, but the layout selector text, the open path and delete_layout all
    take next() as the active layout. With a profile that carries two entries,
    the screen could show one layout while the selector named the other, and
    deleting the drawn layout compared against the wrong name, so it kept
    rendering after deletion. Take next()'s entry so they agree, and drop the
    second return value nobody read.
    Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
  • fix: clear a Clique key's spell when the modifier layer changes
    3ebb6b5 gave Clique spell entries a spellid so they get a cooldown overlay.
    Holding a modifier goes through refresh_modifier_layer_for_collection, and a
    Clique key reads as unbound in both layers, so it takes the branch that
    redraws the key without set_key -- and therefore without reset_button_state.
    apply_clique_binding only ever sets spellid, never clears it, so the previous
    layer's spell stayed on the key: with Renew on BUTTON2 and nothing on
    SHIFT-BUTTON2, holding SHIFT left Renew's tooltip and cooldown swipe on an
    empty key.
    Clear spellid before redrawing the key, refresh its cooldown afterwards, and
    count the key as changed when its spell moved, so the tooltip of a hovered
    key is redrawn too.
    Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
  • fix: keep held modifiers in edit-mode key names
    Assigning a key in edit mode stores what was pressed as the key's raw_key,
    held modifiers included, so a layout can carry keys like CTRL-1 -- what many
    MMO mouse side buttons send. d5d404a dropped the modifiers to cure the
    ALT-ALT-X double prefix, on the premise that handle_key_press keeps
    current_modifier_string in step with the held keys. It does not in edit mode:
    MODIFIER_STATE_CHANGED only reaches handle_key_press while every device is
    locked. So CTRL+wheel on a mouse key was stored as plain MOUSEWHEELUP and the
    key showed the unmodified binding.
    Restore the held modifiers in handle_key_down, handle_gamepad_down and
    handle_mouse_wheel, but leave out any that current_modifier_string -- the
    modifier checkboxes -- already supplies, since get_binding prefixes that
    string itself. That is where the double prefix came from. The nil-frame guard
    d5d404a added to handle_mouse_wheel stays.
    Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
  • Merge pull request #71 from stroexd/fix/issue-70-review-items
    Works through the open items from #70.
    Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
  • refactor: drop two unreachable pieces
    addon.class_name was assigned at PLAYER_LOGIN and never read. addon.debug is
    never set anywhere, so the version line in VersionCompat.lua could not print.
    addon.VERSION.string stays -- it is a labelled debug field worth having in a
    bug report, which is not true of either of these.
    Refs #70
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: use the real proc alt-glow atlas off Retail too
    The flat golden rectangle was a fallback for an atlas that is not missing:
    Blizzard_ActionBar/Shared/ActionButtonSpellAlerts.xml declares
    ActionButtonSpellAlertTemplate's own ProcAltGlow texture with
    atlas="UI-HUD-RotationHelper-ProcAltGlow" on the Classic clients as well, and
    has_spell_alert_template() has already established that template exists before
    this code runs.
    Refs #70
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: merge spellbook tabs that share a name instead of losing one
    load_spellbook keyed addon.spells by skill-line name and appended to
    spells_tab_order unconditionally, so two lines with the same name overwrote
    each other while the menu still listed the tab twice -- both entries then
    showing the second line's spells.
    Refs #70
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • feat: give Clique spell entries a cooldown overlay
    apply_clique_binding deliberately sets no action slot, so GetButtonCooldownData
    had nothing to read and the key drew icon and label only. A Clique spell entry
    carries the spell name, which resolves through the same
    C_Spell.GetSpellIDForSpellIdentifier call process_spell already uses, and that
    is all the existing spellid branch needs. The cast itself stays Clique's.
    Refs #70
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: read one layout per device, and anchor mouse keys from the left
    The three generators iterated keyui_settings.layout_current_* with pairs and
    nested a second loop inside, although save_*_layout wipes the container and
    writes exactly one entry. A profile that ever carried two rendered both on top
    of each other in whatever order pairs returned. GetCurrentLayout names that
    contract, and picking the lowest key makes the outcome at least deterministic
    if it is ever violated.
    In the same block: save_mouse_layout stores GetLeft() - mouse_frame:GetLeft(),
    an offset from the frame's left edge, but generate_mouse_key_frames fed it to
    SetPoint("TOPRIGHT", ...). That only round-trips while every button is exactly
    as wide as the 50px frame -- a layout with differently sized buttons drifts by
    50 - width. The keyboard generator already uses TOPLEFT on both sides.
    Refs #70
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • perf: enumerate the interface bindings once, not per right-click
    build_interface_bindings_submenu walked all ~300 GetNumBindings() entries and
    allocated a table per binding every time the context menu opened. The set only
    changes on UPDATE_BINDINGS, which the event handler already sees, so that is
    where the cache is dropped.
    Refs #70
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: let the font registry forget discarded font strings
    RegisterFontString appended and nothing ever removed. ResetAddonSettings nils
    controls_frame, the selectors, the dialogs and the tooltip frame and empties
    the button pools, then everything is built again -- so the registry grew by a
    frame's worth of entries per reset and RefreshAllFonts kept calling SetFont on
    the orphans. The named CreateFont objects were worse: CreateFont returns the
    existing font for a name already taken, so each rebuild added another entry
    pointing at the same object.
    Keying the registry by the font string makes re-registration idempotent, and
    ReleaseFontStrings drops everything parented below a frame that is going away.
    The two teardown paths call it; the frames that survive a reset keep theirs.
    Refs #70
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: correct the Dvorak layouts against the xkeyboard-config reference
    xkeyboard-config ships us(dvorak), us(dvorak-l) and us(dvorak-r), the last two
    derived from console-tools. Comparing all six shipped Dvorak layouts position
    by position against it turned up twelve wrong labels -- four of them the
    duplicate commas from the issue, eight that nobody had spotted:
    DVORAK_100% / _PRIMARY AD01 was '`', the Dvorak home position is "'"
    DVORAK_RIGHT_100% / _PRIMARY AC01/AC02 were '&' and '*', the shifted
    legends; WoW names the keys '7' and '8'
    DVORAK_RIGHT_100% / _PRIMARY AB10 was ',', which is AB04 -- it is "'"
    DVORAK_LEFT_100% / _PRIMARY AC11 was '9', leaving the board with two 9s
    and no 7 at all
    DVORAK_LEFT_100% / _PRIMARY AB01 was ',', which is AB08 -- it is "'"
    Same slip as adcfcb6 fixed in the QWERTY layouts, and the same consequence:
    both buttons resolved to one binding, so one key showed the other's action and
    a real binding on the mistyped key was invisible.
    Every other position in all four rows of all six layouts matches the reference.
    Refs #70
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: stop a right-click on a key from using the action
    The key buttons register for AnyUp and SetButtonActionSlot sets a bare "type"
    attribute. SecureButton_GetButtonSuffix turns a right-click into suffix "2" and
    SecureButton_GetModifiedAttribute falls back to the unsuffixed name when no
    "type2" is set, so "action" answered the right-click too -- while the insecure
    OnMouseUp opened the assignment menu at the same time. In combat that cost a
    GCD on whatever the key happened to hold.
    Setting type2 to ATTRIBUTE_NOOP is the mechanism SecureTemplates.lua documents
    for excluding one button from a shared attribute. It leaves RegisterForClicks,
    useOnKeyDown and the left-click path untouched; the context menu and the drag
    handlers run off OnMouseUp and RegisterForDrag and are unaffected either way.
    Refs #70
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: drop the duplicate modifier prefix from raw_key
    handle_key_down, handle_gamepad_down and handle_mouse_wheel each built an
    "ALT-CTRL-" prefix and stored it in frame.raw_key. Every consumer prefixes
    current_modifier_string itself -- get_binding, apply_clique_binding, the unbind
    action -- and handle_key_press already keeps that string in step with the held
    modifiers, so the prefix was applied twice: ALT-ALT-X, which matches nothing.
    raw_key is also written to saved layouts by save_mouse_layout and used as the
    key_lookup and display_only_keys key, all of which expect the bare name, so the
    modifier corrupted those too.
    handle_mouse_wheel gains the nil-frame guard its two siblings already have; it
    is called with addon.current_hovered_button, which OnLeave clears.
    Refs #70
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: integration registration, Clique modifier layers, Dominos slot fallback
    Four findings from a pass over Core.lua, plus one optimisation.
    initialize_keybind_patterns replaces the pattern table at PLAYER_LOGIN but left
    registered_addons untouched, so register_addon_patterns short-circuited for any
    integration that had already registered from ADDON_LOADED -- that is every addon
    whose folder sorts after "KeyUI" and therefore loads after KeyUI's event frame
    exists. OPie is the one that hits today; it survives only because set_key has a
    separate ORL_RProxy fallback. Wipe the bookkeeping alongside the table.
    The pattern table is now an ordered list. set_key stopped at the first match
    while iterating with pairs, whose order is undefined, so a binding matching two
    patterns could resolve differently between sessions. The MULTIACTIONBAR pattern
    was also unanchored and could match inside a longer binding; it is anchored now.
    refresh_modifier_layer_for_collection is the fast path for holding a modifier.
    A Clique key reads as empty in both layers, so it took the "binding unchanged"
    branch, which hid the label but left the previous layer's icon in place and drew
    the red empty-key highlight over a key Clique has bound. Redraw it from the
    layer being switched to, as set_key does.
    resolve_multibar_click_slot matches Dominos frame names -- Blizzard's own are
    MultiBarRightButton1 etc., without the "Action" -- but derived slots from
    Blizzard's page offsets. Those agree for the first four bars and disagree by
    twelve for MultiBar5/6/7, where Dominos names slot 133 MultiBar5ActionButton1.
    Give the fallback its own table.
    Range polling ran an OnUpdate every frame for the whole session even if KeyUI
    was never opened, and repainted every key's font colour ten times a second
    whether or not it had changed. The frame now hides itself while closed and the
    colour is only set when the range state flips.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: the QWERTY home-row key beside ENTER is the apostrophe, not a comma
    All seven QWERTY layouts labelled the key between ';' and ENTER ',', which is
    the bottom-row key. Two buttons therefore resolved to the same binding: the
    apostrophe showed whatever was bound to comma, a real apostrophe binding was
    invisible, and pressing comma lit up two keys at once.
    The Dvorak layouts in the same file already spell it "'", so that is the
    spelling KeyUI expects. Found by checking every shipped layout for duplicate
    keys and overlapping rectangles; the remaining duplicates are either genuine
    (both ENTER keys report as ENTER -- WoW has no NUMPADENTER) or belong to
    specialty boards that repeat keys on purpose.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: close three small crash and type hazards
    The OnLeave handler on every key button hid the shared tooltip without checking
    it exists. ResetAddonSettings sets addon.keyui_tooltip_frame to nil and then
    hides the buttons, and hiding a frame under the cursor fires OnLeave -- so a
    Full Reset while hovering a key raised. Guard the three call sites.
    addon.bonusbar_offset and addon.current_actionbar_page started life as empty
    tables, but addon:get_action_button_slot compares them against numbers
    (>= 2, == 5), which raises "attempt to compare table with number". Today
    PLAYER_LOGIN happens to overwrite both before anything reads them; start from
    the neutral numbers instead of relying on that.
    NormalizeSettingTypes was missing the six overlay toggles that
    InitializeGeneralSettings sets, so an imported profile could carry a non-boolean
    into show_charge_cooldown and friends and set_if_nil would keep it.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: drive proc glow and pet autocast through APIs the clients still have
    Checked every Blizzard symbol KeyUI calls against the 12.1.5, 5.5.4, 2.5.6 and
    1.15.9 interface dumps. Three of them are defined in none of the four:
    ActionButton_ShowOverlayGlow only a stale call site in Blizzard_PTRFeedback
    ActionButton_HideOverlayGlow no definition and no call site anywhere
    AutoCastShine_AutoCastStart ditto, same for ..._AutoCastStop
    The glow pair sat behind "isRetail or isMoP or isAnniversary", so Classic Era
    fell through to them: every proc on 1.15.9 raised "attempt to call a nil value"
    instead of glowing. The premise of that branch is wrong as well --
    ActionButtonSpellAlertTemplate is defined on all four clients, including Era,
    and its atlases are the same ones KeyUI already sets. Probe the template once
    the way the autocast code does and drop the version split.
    The autocast pair was nil-guarded, so it never raised -- it just never ran. The
    Shine texture inside AutoCastOverlayTemplate starts hidden and only
    AutoCastOverlayMixin:ShowAutoCastEnabled starts it, so a pet ability with
    autocast on has been indistinguishable from one with autocast off.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • ci: harden the release workflow and keep dev files out of the package
    The release workflow fired on '**', so any tag at all - a scratch tag, a
    typo - would have packaged a build and pushed it to CurseForge, Wago and
    WoWInterface. Narrowed to v* so only deliberate version tags publish.
    Other changes:
    • actions/checkout v3 -> v4; v3 runs on Node 16 and is deprecated.
    • Pinned permissions to contents: write. The repository default happens to
      be write today, so nothing was broken, but the release should not depend
      on that staying true.
    • Added workflow_dispatch with a required tag input, so a release that fails
      halfway can be re-run without cutting a new tag. It checks the tag out
      explicitly: without that the packager would see an untagged commit and
      publish it as an alpha (release.sh:849).
    • Added a concurrency group so two runs of the same tag cannot race.
    • Added a step that writes the resolved channel and the TOC interface line
      to the run summary, so it is visible what a tag is about to publish before
      looking at the upload.
    • Documented the channel convention in the workflow header: the packager
      reads it from the tag name, "beta" anywhere in the tag means a beta build.
      .pkgmeta now also ignores .gitignore, .luarc.json, .pkgmeta and
      COMPATIBILITY.md. Gitignored directories are dropped by the packager on its
      own, but these are tracked and were being shipped to users.
      Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: keep Classic on the numeric cooldown path, honour LoC replacement
    Follow-up to #68. The approach there is right - SetCooldown is annotated
    SecretArguments = "AllowedWhenUntainted", so addon code genuinely cannot
    pass secret values, and duration objects are the way around it. Four things
    needed correcting, all checked against the 12.1.5 interface dump.
    The important one: GetActionCooldownDuration, GetActionChargeDuration,
    GetActionLossOfControlCooldownInfo and SetCooldownFromDurationObject are
    documented on all four clients, not just Retail. Testing for their presence
    therefore put MoP, TBC and Era on the new path too - the one place nobody
    had tested. Gate on C_Secrets.HasSecretRestrictions() instead, which is
    false wherever timings are not secret, so Classic keeps the numeric path it
    has always used and the new code only runs where it is actually needed.
    CooldownInfoIsActive pcall'd reading isActive, but isActive carries
    NeverSecret = true (SpellSharedDocumentation.lua) and can never be secret.
    The pcall ran for every button on every refresh, and its "return true on
    failure" fallback erred toward drawing something wrong.
    Blizzard suppresses the normal and charge swipes while a loss-of-control
    effect replaces them (shouldReplaceNormalCooldown, also NeverSecret,
    Blizzard_ActionBar/ActionButton.lua:858-860). Without it KeyUI drew the
    ordinary cooldown on top of the red loss-of-control swipe.
    The LoC branch tested for GetActionLossOfControlCooldownDuration but called
    GetActionLossOfControlCooldownInfo; it now checks both.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
    Co-Authored-By: Babbadger 80891322+Babbadger@users.noreply.github.com
  • feat: show LMB/RMB on the mouse UI, including Clique bindings (#66)
    The mouse layouts had slots for BUTTON3 and the wheel but none for BUTTON1
    or BUTTON2, so the left and right mouse buttons were simply missing from
    the picture. Adds them to all eight layouts, tiled with the wheel as
    LMB | wheel | RMB.
    The reporter uses Clique, which does its click-casting through secure
    attributes on unit frames - those bindings never reach GetBindingAction, so
    a plain slot would have stayed empty for exactly the person who asked. Clique
    does expose them through its AceDB saved variable, and its key strings use
    the same ALT-CTRL-SHIFT ordering KeyUI builds in update_modifier_string, so
    entries match KeyUI keys directly and ship an icon path as well. KeyUI now
    falls back to that lookup whenever WoW's binding table has nothing for a key.
    Read-only: KeyUI never writes to another addon's database.
    These two keys are deliberately not assignable. KeyUI binds through the
    global SetBinding, so an assignment menu on BUTTON1 would let a player
    hijack their own left click. The context menu returns early for them, which
    also puts every SetBinding call site out of reach, and the drag path already
    bails when a button has no action slot. No secure attributes are set either,
    so the slot stays purely visual.
    Layout_2+4x3's two extra thumb slots moved down within their column to make
    room; they also overlapped each other by 3px before, which this fixes.
    Closes #66
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • docs: re-verify the addon integration patterns against current sources
    The integration checklist still said "Dominos (11.2.x)" while Retail is on
    12.1.x, and the frame names and binding formats KeyUI hard-codes had not
    been checked against the addons since.
    Checked Dominos, ElvUI and Bartender4 at their current HEADs: all three
    patterns in the binding_handlers table still hold. Recorded what was
    checked and at which commit, so the next pass has a baseline.
    OPie is not published on GitHub and stays unverified - noted as such rather
    than left to look checked.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • ci: run the Lua syntax check on pull requests
    release.yml triggers only on tags, so pull requests were merged without
    anything having compiled them - PR #68 arrived with zero check runs.
    Reuses the compiler-resolution and luac -p steps from release.yml verbatim,
    including the API/ and Addons/ exclusions, and adds no secrets or packaging
    so it is safe to run on forks. Uses checkout@v4; release.yml is still on v3
    and can be bumped separately.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • chore: bump TOC to live builds and refresh API snapshot docs
    Retail, Anniversary and Classic Era were each a patch behind, so WoW
    flagged KeyUI as out of date on three of the four supported clients.
    Retail 120005 -> 120105, 120100
    MoP Classic 50504 -> 50504 (unchanged)
    Anniversary 20505 -> 20506
    Classic Era 11508 -> 11509
    Two Retail targets on purpose: 120105 is current, but 120100 clients are
    still around and naming only one gets the addon flagged on the other.
    Bartender4 and AdvancedInterfaceOptions do the same. The Classic values are
    cross-checked against ElvUI, Dominos, Bartender4 and AdvancedInterfaceOptions,
    which all declare 50504, 20506 and 11509.
    Re-ran the documented feature audit against the new snapshots. Two of the
    four recorded findings had changed: BINDINGS_LOADED and C_ActionBar.GetSpell
    / IsAssistedCombatAction now exist on all four clients, not just Anniversary
    and Retail. Most of C_ActionBar has converged, so feature detection no
    longer distinguishes Retail from Classic.
    Documented the 12.x secret-value rules, since that is now the real
    difference and the reason existence checks mislead: which struct fields
    carry NeverSecret, why SetCooldown rejects secret values from tainted code,
    and C_Secrets as the way to pick a path.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • Merge pull request #68 from Babbadger/fix/combat-cooldowns-secret-values
    fix: keep cooldowns updating in combat on 12.0 (secret values)
  • fix: withdraw frames from UISpecialFrames during combat
    Blizzard's ESC handler (CloseSpecialWindows) calls Hide() on every frame listed
    in UISpecialFrames. KeyUI's frames parent SecureActionButtonTemplate buttons, so
    that Hide() is blocked during combat and pressing ESC mid-fight produced:
    [ADDON_ACTION_BLOCKED] AddOn 'KeyUI' tried to call the protected function
    'keyui_keyboard_frame:Hide()'.
    Gate ApplyEscClose on combat lockdown and call it from both regen handlers, so
    the frames are withdrawn for the duration of the fight and restored afterwards.
    ESC is simply inert for KeyUI during combat rather than raising; the frames'
    own close paths already defer through SafeHideFrame. Frames are only created out
    of combat, so the registrations at creation time cannot re-add them mid-fight.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: read macro limits from Constants.MacroConsts
    12.0 removed the MAX_ACCOUNT_MACROS and MAX_CHARACTER_MACROS globals, so
    build_macros_submenu ran "for i = 1, nil" and the Macro submenu failed outright:
    Core.lua:4662: 'for' limit must be a number
    Read them from Constants.MacroConsts, falling back to the globals and then to
    the long-standing literals so Classic is unaffected.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: guard key and gamepad handlers against a cleared hover target
    Callers pass addon.current_hovered_button, which OnLeave clears while the
    OnKeyDown script stays installed on the button. A keypress arriving just after
    the cursor leaves a key therefore reached handle_key_down with no frame:
    Core.lua:4438: attempt to perform indexed assignment on local 'frame'
    (a nil value)
    Bail out early in handle_key_down and handle_gamepad_down.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com
  • fix: render cooldowns from 12.0 duration objects so they update in combat
    On 12.0 the action cooldown APIs return secret values during combat. Tainted
    code may neither compare them nor hand them to a widget, so SetCooldown raised:
    bad argument #1 to 'SetCooldown' ... Secret values are only allowed during
    untainted execution for this argument.
    UpdateButtonCooldown assumed the opposite and passed them straight through.
    Because it sits mid-chain in set_key, the throw aborted the whole refresh pass,
    freezing icons, usable state, counts and range as well - the cooldown swipes
    kept animating from their last pre-combat value.
    Use the 12.0 cooldown duration objects instead: opaque handles that tainted code
    cannot read but may pass to a Cooldown widget, driven by the info tables'
    isActive field, which stays readable. The numeric path is kept for pet actions
    and Classic, with its comparisons guarded so a secret value degrades to "nothing
    to draw" rather than erroring.
    Charge and loss-of-control overlays get the same treatment, which also lets them
    drop their InCombatLockdown bail-outs and update during combat.
    Co-Authored-By: Claude Opus 5 noreply@anthropic.com