From 08c98e7ecf55f2d36437ca5d60c6e488910d4a43 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 11 Mar 2026 09:50:55 +0100 Subject: [PATCH 01/30] in function Meta: consider AST correctly for setting redirect --- resources/build/htm-link.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/resources/build/htm-link.lua b/resources/build/htm-link.lua index e85b91fb17..b2ed4fc45b 100644 --- a/resources/build/htm-link.lua +++ b/resources/build/htm-link.lua @@ -21,10 +21,8 @@ end -- Support for redirect meta key, adds a meta refresh to the resulting document function Meta(meta) - for k, v in pairs(meta) do - if k == 'redirect' and type(v) == 'table' and v.t == 'MetaInlines' then - redirect = v[1].c - end + if meta.redirect then + redirect = pandoc.utils.stringify(meta.redirect) end return meta end From bc6b0ea773add827565e8cac5f86037f437bb7b6 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:20:42 +1000 Subject: [PATCH 02/30] fix(windows): change caps on updateable This change also adds a check in aiTIP.cpp to check the thread data stored last key press and scan code. Before if the scan code had been cleared then we would process the synthasized caps lock key presses which was just for the system and not for the current Keyman app. --- windows/src/engine/keyman32/appint/aiTIP.cpp | 7 +++- .../src/engine/keyman32/kmhook_getmessage.cpp | 1 + .../src/engine/keyman32/kmprocessactions.cpp | 36 +++++++++++++------ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index f56bd98620..1332b13e57 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -138,7 +138,12 @@ BOOL TIPProcessKeyInternal( SendDebugMessageFormat("Scan code was zero so using cached scan code %x", scan); } - if(scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { // I4370 + + // We need to check the last key is the wparam or VK_PROCESSKEY (as that is what keyup comes for synthesised keys on the KeyUP). + // Just the lastscan code is not sufficent becasue we have the case where Shift press appears + // here before the low level keyboard hook. + if (((_td->LastKey == wParam || _td->LastKey == VK_PROCESSKEY) && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT) + || scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { if (wParam == VK_CAPITAL && !isUp) { // Must also record toggle state change when Keyman has generated // a Caps Lock event diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index 4fee835254..22eecd46ef 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -174,6 +174,7 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) if (mp->wParam != VK_BACK) { if(scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { mp->lParam = (mp->lParam & 0xFF00FFFFL) | (MapVirtualKey((UINT)mp->wParam, 0) << 16); + SendDebugMessageFormat("wm_keyman clear Keyman scan flag new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x", mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode); } } } diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index bdc8c69e66..d72a7c75c8 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -79,16 +79,27 @@ processPersistOpt(km_core_actions const* actions, LPINTKEYBOARDINFO activeKeyboa static void processCapsLock(const km_core_caps_state caps_lock_state, BOOL isUp, BOOL Updateable, BOOL externalEvent) { + /// For Debuging + // TODO: 15594 - remove this debug message and associated code after testing + SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", + caps_lock_state, isUp, Updateable, externalEvent, IsCapsLockOn()); + + /// // We only want to process the Caps Lock key event once -- - // in the first pass (!Updateable). - if (Updateable){ + // it has to be updateble as TSF does not have updateable=0 events. + if (!Updateable){ return; } if (caps_lock_state == KM_CORE_CAPS_ON) { // This case would occur for the keyboard system store setting `store(&CapsOnOnly) '1'` + if (!isUp && !IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test + SendDebugMessageFormat("TURN CAPS ON key down: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); + } if (isUp && !IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test - SendDebugMessageFormat("TURN CAPS ON: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); + SendDebugMessageFormat("TURN CAPS ON key up: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); } @@ -96,16 +107,21 @@ static void processCapsLock(const km_core_caps_state caps_lock_state, BOOL isUp, // This case would occur for the keyboard system store setting `store(&CapsAlwaysOff) '1'` // A trick is being played here of synthesising a release the CAPSLOCK key event // then a depress CAPSLOCK key event - else if (!isUp && IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test - SendDebugMessageFormat("TURN CAPS OFF: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - } + //else if (!isUp && IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test + // SendDebugMessageFormat("TURN CAPS OFF: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); + // keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); + // keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); + //} } else if (caps_lock_state == KM_CORE_CAPS_OFF) { // This case would occur for the keyboard system store setting `store(&ShiftFreesCaps) '1'` // OR selecting a keyboard with CAPs always off rule if ((!isUp && IsCapsLockOn()) || (externalEvent && IsCapsLockOn())) { - SendDebugMessageFormat("TURN CAPS OFF: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); + SendDebugMessageFormat("TURN CAPS OFF key down: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); + } + if ((isUp && IsCapsLockOn()) || (externalEvent && IsCapsLockOn())) { + SendDebugMessageFormat("TURN CAPS OFF key up: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); } @@ -151,7 +167,7 @@ ProcessActionsNonUpdatableParse(BOOL* emitKeystroke) { km_core_actions const* core_actions = km_core_state_get_actions(_td->lpActiveKeyboard->lpCoreKeyboardState); - processCapsLock(core_actions->new_caps_lock_state, !_td->state.isDown, _td->TIPFUpdateable, FALSE); + if (core_actions->emit_keystroke) { *emitKeystroke = TRUE; SendDebugMessageFormat("EMIT_KEYSTROKE"); From 95a7c6008bfdebaaef081a7d197e79b2cc155820 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:16:48 +1000 Subject: [PATCH 03/30] fix(windows): simplify the processCapsLock if else also update external processsCaps Lock call to handle the Updateable change --- .../src/engine/keyman32/kmprocessactions.cpp | 54 +++++-------------- 1 file changed, 14 insertions(+), 40 deletions(-) diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index d72a7c75c8..d76b16fe6a 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -77,54 +77,28 @@ processPersistOpt(km_core_actions const* actions, LPINTKEYBOARDINFO activeKeyboa } } -static void processCapsLock(const km_core_caps_state caps_lock_state, BOOL isUp, BOOL Updateable, BOOL externalEvent) { - +static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isUp, BOOL Updateable, BOOL externalEvent) { + // Turn three state value into a boolean for whether caps lock should be on or off, we only want to process the key event if the + // state is changing + BOOL required_caps_state = (caps_state_change == KM_CORE_CAPS_ON); + BOOL isCapsOn = IsCapsLockOn(); /// For Debuging // TODO: 15594 - remove this debug message and associated code after testing - SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", - caps_lock_state, isUp, Updateable, externalEvent, IsCapsLockOn()); + SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", caps_state_change, isUp, Updateable, + externalEvent, isCapsOn); - /// // We only want to process the Caps Lock key event once -- // it has to be updateble as TSF does not have updateable=0 events. if (!Updateable){ return; } - - if (caps_lock_state == KM_CORE_CAPS_ON) { - // This case would occur for the keyboard system store setting `store(&CapsOnOnly) '1'` - if (!isUp && !IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test - SendDebugMessageFormat("TURN CAPS ON key down: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - } - if (isUp && !IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test - SendDebugMessageFormat("TURN CAPS ON key up: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - } - - // This case would occur for the keyboard system store setting `store(&CapsAlwaysOff) '1'` - // A trick is being played here of synthesising a release the CAPSLOCK key event - // then a depress CAPSLOCK key event - //else if (!isUp && IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test - // SendDebugMessageFormat("TURN CAPS OFF: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - // keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - // keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - //} - } else if (caps_lock_state == KM_CORE_CAPS_OFF) { - // This case would occur for the keyboard system store setting `store(&ShiftFreesCaps) '1'` - // OR selecting a keyboard with CAPs always off rule - if ((!isUp && IsCapsLockOn()) || (externalEvent && IsCapsLockOn())) { - SendDebugMessageFormat("TURN CAPS OFF key down: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - } - if ((isUp && IsCapsLockOn()) || (externalEvent && IsCapsLockOn())) { - SendDebugMessageFormat("TURN CAPS OFF key up: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - } + + if (isCapsOn != required_caps_state) { + SendDebugMessageFormat( + "Simulate CAPS %s: FIsUp=%d CurrentCapsState=%d ExternalEvent=%d", + required_caps_state ? "ON" : "OFF", isUp, isCapsOn, externalEvent); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); } } From 6277735dbbdd6609a2c898adbd86379c927e75ef Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:02:26 +1000 Subject: [PATCH 04/30] fix(windows): fix typos and grammar in commments --- windows/src/engine/keyman32/appint/aiTIP.cpp | 7 ++++--- windows/src/engine/keyman32/kmhook_getmessage.cpp | 3 ++- windows/src/engine/keyman32/kmprocessactions.cpp | 13 +++++++------ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index 1332b13e57..438fbcc48c 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -139,9 +139,10 @@ BOOL TIPProcessKeyInternal( } - // We need to check the last key is the wparam or VK_PROCESSKEY (as that is what keyup comes for synthesised keys on the KeyUP). - // Just the lastscan code is not sufficent becasue we have the case where Shift press appears - // here before the low level keyboard hook. + // We need to check if the last key is the same as `wparam` or `VK_PROCESSKEY` + // (VK_PROCESSKEY is what synthesised keys on the KeyUP have). + // Just the lastscan code is not sufficient because we have the case where + // Shift press is processed here before the low level keyboard hook. if (((_td->LastKey == wParam || _td->LastKey == VK_PROCESSKEY) && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT) || scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { if (wParam == VK_CAPITAL && !isUp) { diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index 22eecd46ef..f13e3fa6a1 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -174,7 +174,8 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) if (mp->wParam != VK_BACK) { if(scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { mp->lParam = (mp->lParam & 0xFF00FFFFL) | (MapVirtualKey((UINT)mp->wParam, 0) << 16); - SendDebugMessageFormat("wm_keyman clear Keyman scan flag new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x", mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode); + SendDebugMessageFormat("wm_keyman clear `SCAN_FLAG_KEYMAN_KEY_EVENT` new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x", + mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode); } } } diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index d76b16fe6a..e389b820e0 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -78,8 +78,8 @@ processPersistOpt(km_core_actions const* actions, LPINTKEYBOARDINFO activeKeyboa } static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isUp, BOOL Updateable, BOOL externalEvent) { - // Turn three state value into a boolean for whether caps lock should be on or off, we only want to process the key event if the - // state is changing + // Turn three state value into a boolean for whether caps lock should be on or off, + // we only want to process the key event if the state is changing. BOOL required_caps_state = (caps_state_change == KM_CORE_CAPS_ON); BOOL isCapsOn = IsCapsLockOn(); /// For Debuging @@ -87,12 +87,13 @@ static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isU SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", caps_state_change, isUp, Updateable, externalEvent, isCapsOn); - // We only want to process the Caps Lock key event once -- - // it has to be updateble as TSF does not have updateable=0 events. + // We only want to process the Caps Lock key event once; + // it has to be when updateble=1 as TSF does not consistently + // have updateable=0 events. if (!Updateable){ return; } - + if (isCapsOn != required_caps_state) { SendDebugMessageFormat( "Simulate CAPS %s: FIsUp=%d CurrentCapsState=%d ExternalEvent=%d", @@ -157,6 +158,6 @@ ProcessActionsExternalEvent() { return FALSE; } km_core_actions const* core_actions = km_core_state_get_actions(_td->lpActiveKeyboard->lpCoreKeyboardState); - processCapsLock(core_actions->new_caps_lock_state, !_td->state.isDown, FALSE, TRUE); + processCapsLock(core_actions->new_caps_lock_state, !_td->state.isDown, TRUE, TRUE); return TRUE; } From f4c5d9df027b711e4a78e4b6dafaf232739d6acb Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:00:12 +1000 Subject: [PATCH 05/30] fix(windows): fix logic error with caps_changed_state Return early if caps lock state is unchanged. --- windows/src/engine/keyman32/kmprocessactions.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index e389b820e0..93b6cb609a 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -78,21 +78,21 @@ processPersistOpt(km_core_actions const* actions, LPINTKEYBOARDINFO activeKeyboa } static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isUp, BOOL Updateable, BOOL externalEvent) { - // Turn three state value into a boolean for whether caps lock should be on or off, - // we only want to process the key event if the state is changing. - BOOL required_caps_state = (caps_state_change == KM_CORE_CAPS_ON); BOOL isCapsOn = IsCapsLockOn(); /// For Debuging // TODO: 15594 - remove this debug message and associated code after testing SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", caps_state_change, isUp, Updateable, externalEvent, isCapsOn); - // We only want to process the Caps Lock key event once; // it has to be when updateble=1 as TSF does not consistently // have updateable=0 events. - if (!Updateable){ + if (!Updateable || caps_state_change == KM_CORE_CAPS_UNCHANGED) + { return; } + // Turn three state value into a boolean for whether caps lock should be on or off, + // we only want to process the key event if the state is changing. + BOOL required_caps_state = (caps_state_change == KM_CORE_CAPS_ON); if (isCapsOn != required_caps_state) { SendDebugMessageFormat( From c1c3952717206b232e47996e42262b61820c0c90 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:22:35 +1000 Subject: [PATCH 06/30] fix(windows): reduce the number of places km scan flag The SCAN_FLAG_KEYMAN_KEY_EVENT was checked in OnKey pressess as well as an early return option in _KeymanProcessKeystroke. This change removes the extra and allows _KeymanProcessKeystroke, to be the central place for code readablitiy and maintainablity. It also adds a processToggleChange call in kmhook_getmessage as it is sometimes called before the TIP hook. the call is also idempotent --- windows/src/engine/keyman32/appint/aiTIP.cpp | 5 +- .../src/engine/keyman32/kmhook_getmessage.cpp | 7 ++- windows/src/engine/kmtip/keys.cpp | 51 +++++++------------ windows/src/engine/kmtip/kmkey.cpp | 6 +-- 4 files changed, 28 insertions(+), 41 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index 438fbcc48c..d184d18350 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -142,7 +142,7 @@ BOOL TIPProcessKeyInternal( // We need to check if the last key is the same as `wparam` or `VK_PROCESSKEY` // (VK_PROCESSKEY is what synthesised keys on the KeyUP have). // Just the lastscan code is not sufficient because we have the case where - // Shift press is processed here before the low level keyboard hook. + // Shift press is processed here before the kmhook_getmessage hook. if (((_td->LastKey == wParam || _td->LastKey == VK_PROCESSKEY) && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT) || scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { if (wParam == VK_CAPITAL && !isUp) { @@ -150,7 +150,8 @@ BOOL TIPProcessKeyInternal( // a Caps Lock event ProcessToggleChange((UINT)wParam); // I4793 } - SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=0xFF]"); + // TODO: should we clear the scan code flag even though we are now finished? + SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [LastKey=%x LastScanCode=%x]"); return_SendDebugExit(FALSE); } diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index f13e3fa6a1..7903b789f9 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -174,8 +174,11 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) if (mp->wParam != VK_BACK) { if(scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { mp->lParam = (mp->lParam & 0xFF00FFFFL) | (MapVirtualKey((UINT)mp->wParam, 0) << 16); - SendDebugMessageFormat("wm_keyman clear `SCAN_FLAG_KEYMAN_KEY_EVENT` new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x", - mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode); + if (mp->wParam == VK_CAPITAL) { + ProcessToggleChange(VK_CAPITAL); + } + SendDebugMessageFormat("WMKEY=%x clear `SCAN_FLAG_KEYMAN_KEY_EVENT` new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x", + mp->message, mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode); } } } diff --git a/windows/src/engine/kmtip/keys.cpp b/windows/src/engine/kmtip/keys.cpp index 50f2ade3b9..86d6470c73 100644 --- a/windows/src/engine/kmtip/keys.cpp +++ b/windows/src/engine/kmtip/keys.cpp @@ -178,17 +178,10 @@ STDAPI CKMTipTextService::OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPA { SendDebugEntry(); LogKey(0, wParam, lParam); - // If the keystroke is a Keyman-generated key, ignore it - // But we need to pass Caps Lock through, even if we generated it, so we can track Caps Lock state. - // TODO: Fix magic constants - if ((lParam & 0x00FF0000L) == 0xFF0000L && - wParam != VK_CAPITAL) { - *pfEaten = FALSE; - } - else { - *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 -// SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); - } + *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 + + SendDebugMessageFormat(L"pfEaten=%s, wParam=%x, lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); + // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugExit(); return S_OK; } @@ -205,8 +198,9 @@ STDAPI CKMTipTextService::OnKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM { SendDebugEntry(); LogKey(1, wParam, lParam); - fEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 -// SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); + fEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 + SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); + // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugExit(); return S_OK; } @@ -222,17 +216,11 @@ STDAPI CKMTipTextService::OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARA { SendDebugEntry(); LogKey(2, wParam, lParam); - // If the keystroke is a Keyman-generated key, ignore it - // But we need to pass Caps Lock through, even if we generated it, so we can track Caps Lock state. - if ((lParam & 0x00FF0000L) == 0xFF0000L && - wParam != VK_CAPITAL) { // I3566 - *pfEaten = FALSE; - } - else { - _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 - *pfEaten = fEatenBuf[wParam]; - } -// SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); + _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 + *pfEaten = fEatenBuf[wParam]; + + // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); + SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; } @@ -251,16 +239,11 @@ STDAPI CKMTipTextService::OnKeyUp(ITfContext *pContext, WPARAM wParam, LPARAM lP LogKey(3, wParam, lParam); // If the keystroke is a Keyman-generated key, ignore it // But we need to pass Caps Lock through, even if we generated it, so we can track Caps Lock state. - if ((lParam & 0x00FF0000L) == 0xFF0000L && - wParam != VK_CAPITAL) { // I3566 // I3605 - *pfEaten = FALSE; - } - else - { - _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 // I3605 - *pfEaten = fEatenBuf[wParam]; - } -// SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); + + _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 // I3605 + *pfEaten = fEatenBuf[wParam]; + + SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; } diff --git a/windows/src/engine/kmtip/kmkey.cpp b/windows/src/engine/kmtip/kmkey.cpp index 609c98e431..7d9326b335 100644 --- a/windows/src/engine/kmtip/kmkey.cpp +++ b/windows/src/engine/kmtip/kmkey.cpp @@ -102,15 +102,15 @@ BOOL CKMTipTextService::_KeymanProcessKeystroke(ITfContext *pContext, WPARAM wPa // Don't process Unicode characters injected or ProcessKey events which are generated by Windows if (wParam == VK_PACKET || wParam == VK_PROCESSKEY) { + SendDebugMessageFormat(L"wParam=%x early return FALSE", wParam); return FALSE; // I3608 // I4201 } SendDebugEntry(); SendDebugMessageFormat(L"%x %x %s %s ex=%x", wParam, lParam, fUpdate ? L"update" : L"", fPreserved ? L"preserved" : L"", GetMessageExtraInfo()); // I4378 - // Don't process keystrokes generated by Keyman (scan code = 0xFF) - // But we need to pass Caps Lock through, even if we generated it, so we can track Caps Lock state. - // TODO: This is done in multiple places, but we probably only need to do it once + // Don't process keystrokes generated by Keyman (scan code = 0xFF = SCAN_FLAG_KEYMAN_KEY_EVENT) + // But we need to pass Caps Lock through to the keyman engine (but not the core), even if we generated it, so we can track Caps Lock state. if ((lParam & 0xFF0000) == 0xFF0000 && wParam != VK_CAPITAL) { return_SendDebugExit(FALSE); From a1c493665e0208e53d43274c99c9df27bb7963a1 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:16:26 +1000 Subject: [PATCH 07/30] fix(windows): add key transition to better track key press Added a Key transition member to the thread globals. This help so better insure we actually matching the key value that may have been cleared by kmhook_getmessage to the right key. More importantly makes sure we are not matching it to the wrong key press. This was happening when a synthazised capslock key press was followed by and actuall capslock key press. --- windows/src/engine/keyman32/appint/aiTIP.cpp | 8 ++++---- windows/src/engine/keyman32/globals.h | 1 + windows/src/engine/keyman32/keyman64.h | 4 ++++ windows/src/engine/keyman32/kmhook_getmessage.cpp | 6 ++++-- windows/src/engine/kmtip/keys.cpp | 6 +++--- windows/src/engine/kmtip/kmtip.h | 1 + 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index d184d18350..14cd3124dd 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -128,7 +128,7 @@ BOOL TIPProcessKeyInternal( BOOL isUp = keyFlags & KF_UP ? TRUE : FALSE; BOOL extended = keyFlags & KF_EXTENDED ? TRUE : FALSE; BYTE scan = keyFlags & 0xFF; - + BYTE keyTransition = (BYTE)((HIWORD(lParam) & (KF_UP | KF_REPEAT)) >> 14); SendDebugEntry(); SendDebugMessageFormat("VirtualKey=%s lParam=%x IsUp=%d Extended=%d Updateable=%d Preserved=%d", Debug_VirtualKey((WORD) wParam), lParam, isUp, extended, Updateable, Preserved); @@ -143,15 +143,15 @@ BOOL TIPProcessKeyInternal( // (VK_PROCESSKEY is what synthesised keys on the KeyUP have). // Just the lastscan code is not sufficient because we have the case where // Shift press is processed here before the kmhook_getmessage hook. - if (((_td->LastKey == wParam || _td->LastKey == VK_PROCESSKEY) && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT) + if ((_td->LastKey == wParam && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT && _td->LastTransition == keyTransition) || scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { if (wParam == VK_CAPITAL && !isUp) { // Must also record toggle state change when Keyman has generated // a Caps Lock event ProcessToggleChange((UINT)wParam); // I4793 } - // TODO: should we clear the scan code flag even though we are now finished? - SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [LastKey=%x LastScanCode=%x]"); + // TODO: should we clear the scan code flag even though we are now finished? + SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [LastKey=%x LastScanCode=%x LastTransition=%x]", scan, wParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); return_SendDebugExit(FALSE); } diff --git a/windows/src/engine/keyman32/globals.h b/windows/src/engine/keyman32/globals.h index 3c71163446..9db8044394 100644 --- a/windows/src/engine/keyman32/globals.h +++ b/windows/src/engine/keyman32/globals.h @@ -239,6 +239,7 @@ typedef struct tagKEYMAN64THREADDATA WPARAM LastKey; // I4642 BYTE LastScanCode; // I4642 + BYTE LastTransition; /* Serialized key events */ diff --git a/windows/src/engine/keyman32/keyman64.h b/windows/src/engine/keyman32/keyman64.h index e46dad1782..49ce6bab33 100644 --- a/windows/src/engine/keyman32/keyman64.h +++ b/windows/src/engine/keyman32/keyman64.h @@ -118,8 +118,12 @@ #define KEYMSG_FLAG_DLGMODE(lParam) (HIWORD(lParam) & KF_DLGMODE ? 1 : 0) #define KEYMSG_FLAG_MENUMODE(lParam) (HIWORD(lParam) & KF_MENUMODE ? 1 : 0) #define KEYMSG_FLAG_ALTDOWN(lParam) (HIWORD(lParam) & KF_ALTDOWN ? 1 : 0) +// Repeat is actually previous "up" value of the key #define KEYMSG_FLAG_REPEAT(lParam) (HIWORD(lParam) & KF_REPEAT ? 1 : 0) #define KEYMSG_FLAG_UP(lParam) (HIWORD(lParam) & KF_UP ? 1 : 0) +// Combine the transition flags into a single value for higher chance of identification +// 0 = key down, 1 = repeat, 2 = keyup +#define KEYMSG_FLAG_TRANSITION(lParam) ((BYTE)((HIWORD(lParam) & (KF_UP | KF_REPEAT)) >> 14)) // TODO: Deprecate overloading of scancodes and use dwExtraInfo instead #define SCAN_FLAG_KEYMAN_KEY_EVENT 0xFF diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index 7903b789f9..6c0419c5f4 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -156,9 +156,11 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) } BYTE scan = KEYMSG_LPARAM_SCAN(mp->lParam); + BYTE keyTransitionEvent = KEYMSG_FLAG_TRANSITION(mp->lParam); CheckScheduledRefresh(); _td->LastScanCode = scan; _td->LastKey = mp->wParam; + _td->LastTransition = keyTransitionEvent; switch (mp->wParam) { case VK_MENU: @@ -177,8 +179,8 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) if (mp->wParam == VK_CAPITAL) { ProcessToggleChange(VK_CAPITAL); } - SendDebugMessageFormat("WMKEY=%x clear `SCAN_FLAG_KEYMAN_KEY_EVENT` new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x", - mp->message, mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode); + SendDebugMessageFormat("WMKEY=%x clear `SCAN_FLAG_KEYMAN_KEY_EVENT` new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x LastTransition=%x", + mp->message, mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); } } } diff --git a/windows/src/engine/kmtip/keys.cpp b/windows/src/engine/kmtip/keys.cpp index 86d6470c73..382c671bc3 100644 --- a/windows/src/engine/kmtip/keys.cpp +++ b/windows/src/engine/kmtip/keys.cpp @@ -76,6 +76,7 @@ BOOL CKMTipTextService::_InitKeystrokeSink() pKeystrokeMgr->Release(); memset(fEatenBuf, 0, sizeof(fEatenBuf)); + memset(fTestEatenBuf, 0, sizeof(fTestEatenBuf)); return_SendDebugExit(_keystrokeSinkInitialized = (hr == S_OK)); } @@ -178,8 +179,7 @@ STDAPI CKMTipTextService::OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPA { SendDebugEntry(); LogKey(0, wParam, lParam); - *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 - + fTestEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 SendDebugMessageFormat(L"pfEaten=%s, wParam=%x, lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugExit(); @@ -217,7 +217,7 @@ STDAPI CKMTipTextService::OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARA SendDebugEntry(); LogKey(2, wParam, lParam); _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 - *pfEaten = fEatenBuf[wParam]; + *pfEaten = fTestEatenBuf[wParam]; // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); diff --git a/windows/src/engine/kmtip/kmtip.h b/windows/src/engine/kmtip/kmtip.h index b4f307488b..7df21f7428 100644 --- a/windows/src/engine/kmtip/kmtip.h +++ b/windows/src/engine/kmtip/kmtip.h @@ -117,6 +117,7 @@ private: BOOL _keystrokeSinkInitialized; BOOL fEatenBuf[256]; + BOOL fTestEatenBuf[256]; ITfThreadMgr *_pThreadMgr; TfClientId _tfClientId; From 7084d64b7b9f1094a754d2926177c3f971452ce6 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:44:47 +1000 Subject: [PATCH 08/30] fix(windows): clean up debug logs --- windows/src/engine/keyman32/appint/aiTIP.cpp | 13 ++++++------- windows/src/engine/keyman32/appint/aiTIP.h | 4 +++- windows/src/engine/keyman32/kmhook_getmessage.cpp | 2 +- windows/src/engine/keyman32/kmprocessactions.cpp | 14 +++++++------- windows/src/engine/kmtip/keys.cpp | 8 ++------ windows/src/engine/kmtip/kmkey.cpp | 3 ++- 6 files changed, 21 insertions(+), 23 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index 14cd3124dd..33c6e31043 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -138,11 +138,10 @@ BOOL TIPProcessKeyInternal( SendDebugMessageFormat("Scan code was zero so using cached scan code %x", scan); } - - // We need to check if the last key is the same as `wparam` or `VK_PROCESSKEY` - // (VK_PROCESSKEY is what synthesised keys on the KeyUP have). - // Just the lastscan code is not sufficient because we have the case where - // Shift press is processed here before the kmhook_getmessage hook. + // If this key event was generated by Keyman, then we should return FALSE without + // processing it in the core processor. If it is a CapsLock key we update our + // Globals::ShiftState. Since this could run before kmhook_getmessage processes the event, + // we also check for the scan code flag that we set when generating a key event from Keyman. if ((_td->LastKey == wParam && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT && _td->LastTransition == keyTransition) || scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { if (wParam == VK_CAPITAL && !isUp) { @@ -150,8 +149,8 @@ BOOL TIPProcessKeyInternal( // a Caps Lock event ProcessToggleChange((UINT)wParam); // I4793 } - // TODO: should we clear the scan code flag even though we are now finished? - SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [LastKey=%x LastScanCode=%x LastTransition=%x]", scan, wParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); + SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [LastKey=%x LastScanCode=%x LastTransition=%x]", + scan, wParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); return_SendDebugExit(FALSE); } diff --git a/windows/src/engine/keyman32/appint/aiTIP.h b/windows/src/engine/keyman32/appint/aiTIP.h index a5d84f92a8..2e6b105d98 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.h +++ b/windows/src/engine/keyman32/appint/aiTIP.h @@ -80,7 +80,9 @@ public: /** * ProcessToggleChange - * Toggles the state of FLAGS in the Globals::ShiftState bit mask + * Sets or clears the state of FLAGS in the Globals::ShiftState bit mask + * Using the status of the key as determined by GetKeyState, + * ensuring it is consistent with the actual state of the key. * Supports VK_CAPITAL and VK_NUMLOCK * It DOES NOT generate a system event change for these flags * @param key diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index 6c0419c5f4..fc8540d210 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -179,7 +179,7 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) if (mp->wParam == VK_CAPITAL) { ProcessToggleChange(VK_CAPITAL); } - SendDebugMessageFormat("WMKEY=%x clear `SCAN_FLAG_KEYMAN_KEY_EVENT` new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x LastTransition=%x", + SendDebugMessageFormat("WMKEY=%x Clear `SCAN_FLAG_KEYMAN_KEY_EVENT` before LastKey=%x LastScanCode=%x LastTransition=%x after wParam=%x lParam=%x", mp->message, mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); } } diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index 93b6cb609a..64089c0670 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -79,10 +79,11 @@ processPersistOpt(km_core_actions const* actions, LPINTKEYBOARDINFO activeKeyboa static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isUp, BOOL Updateable, BOOL externalEvent) { BOOL isCapsOn = IsCapsLockOn(); - /// For Debuging - // TODO: 15594 - remove this debug message and associated code after testing - SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", caps_state_change, isUp, Updateable, - externalEvent, isCapsOn); + + // This debug message is useful for understanding the sequence of events around caps lock changes + //SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", caps_state_change, isUp, Updateable, + // externalEvent, isCapsOn); + // We only want to process the Caps Lock key event once; // it has to be when updateble=1 as TSF does not consistently // have updateable=0 events. @@ -90,13 +91,13 @@ static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isU { return; } - // Turn three state value into a boolean for whether caps lock should be on or off, + // Turn three state value into a boolean for whether capslock should be on or off, // we only want to process the key event if the state is changing. BOOL required_caps_state = (caps_state_change == KM_CORE_CAPS_ON); if (isCapsOn != required_caps_state) { SendDebugMessageFormat( - "Simulate CAPS %s: FIsUp=%d CurrentCapsState=%d ExternalEvent=%d", + "Simulate CapsLock %s: FIsUp=%d CurrentCapsState=%d ExternalEvent=%d", required_caps_state ? "ON" : "OFF", isUp, isCapsOn, externalEvent); keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); @@ -142,7 +143,6 @@ ProcessActionsNonUpdatableParse(BOOL* emitKeystroke) { km_core_actions const* core_actions = km_core_state_get_actions(_td->lpActiveKeyboard->lpCoreKeyboardState); - if (core_actions->emit_keystroke) { *emitKeystroke = TRUE; SendDebugMessageFormat("EMIT_KEYSTROKE"); diff --git a/windows/src/engine/kmtip/keys.cpp b/windows/src/engine/kmtip/keys.cpp index 382c671bc3..afddecfef0 100644 --- a/windows/src/engine/kmtip/keys.cpp +++ b/windows/src/engine/kmtip/keys.cpp @@ -75,8 +75,8 @@ BOOL CKMTipTextService::_InitKeystrokeSink() pKeystrokeMgr->Release(); - memset(fEatenBuf, 0, sizeof(fEatenBuf)); - memset(fTestEatenBuf, 0, sizeof(fTestEatenBuf)); + memset(fEatenBuf, 0, sizeof(fEatenBuf)); // OnKeyDown/Up + memset(fTestEatenBuf, 0, sizeof(fTestEatenBuf)); // OnTestKeyDown/Up return_SendDebugExit(_keystrokeSinkInitialized = (hr == S_OK)); } @@ -181,7 +181,6 @@ STDAPI CKMTipTextService::OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPA LogKey(0, wParam, lParam); fTestEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 SendDebugMessageFormat(L"pfEaten=%s, wParam=%x, lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); - // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugExit(); return S_OK; } @@ -200,7 +199,6 @@ STDAPI CKMTipTextService::OnKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM LogKey(1, wParam, lParam); fEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); - // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugExit(); return S_OK; } @@ -218,8 +216,6 @@ STDAPI CKMTipTextService::OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARA LogKey(2, wParam, lParam); _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 *pfEaten = fTestEatenBuf[wParam]; - - // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; diff --git a/windows/src/engine/kmtip/kmkey.cpp b/windows/src/engine/kmtip/kmkey.cpp index 7d9326b335..1b767f4946 100644 --- a/windows/src/engine/kmtip/kmkey.cpp +++ b/windows/src/engine/kmtip/kmkey.cpp @@ -110,7 +110,8 @@ BOOL CKMTipTextService::_KeymanProcessKeystroke(ITfContext *pContext, WPARAM wPa SendDebugMessageFormat(L"%x %x %s %s ex=%x", wParam, lParam, fUpdate ? L"update" : L"", fPreserved ? L"preserved" : L"", GetMessageExtraInfo()); // I4378 // Don't process keystrokes generated by Keyman (scan code = 0xFF = SCAN_FLAG_KEYMAN_KEY_EVENT) - // But we need to pass Caps Lock through to the keyman engine (but not the core), even if we generated it, so we can track Caps Lock state. + // But we need to pass Caps Lock through to the keyman engine (but not the core), + // even if we generated it, so we can track Caps Lock state. if ((lParam & 0xFF0000) == 0xFF0000 && wParam != VK_CAPITAL) { return_SendDebugExit(FALSE); From f0cf996976c6325681c6c35a2890b03b2d705cfa Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:43:11 +1000 Subject: [PATCH 09/30] fix(windows): make log code message fix --- windows/src/engine/keyman32/kmhook_getmessage.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index fc8540d210..da01903963 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -179,8 +179,8 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) if (mp->wParam == VK_CAPITAL) { ProcessToggleChange(VK_CAPITAL); } - SendDebugMessageFormat("WMKEY=%x Clear `SCAN_FLAG_KEYMAN_KEY_EVENT` before LastKey=%x LastScanCode=%x LastTransition=%x after wParam=%x lParam=%x", - mp->message, mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); + SendDebugMessageFormat("WMKEY=%x Clear `SCAN_FLAG_KEYMAN_KEY_EVENT` wParam=%x lParam=%x Set LastKey=%x LastScanCode=%x LastTransition=%x", + mp->message, mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); } } } From d7370c472ef64f566f2f0550778cbbe10e5680c7 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:13:45 +1000 Subject: [PATCH 10/30] fix(windows): update lastkey in tip processor Update the lastkey, lastscankey and lasttransition in aaitp. This ensures that when the pfEaten is FALSE and the get_message_hook does not recieve the key press the "last" value is correct. --- windows/src/engine/keyman32/appint/aiTIP.cpp | 25 +++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index 33c6e31043..497c177c92 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -114,6 +114,20 @@ void ProcessToggleChange(UINT key) { // I4793 } } +/** + * Update the cache of the last key event received, this is set here - TIP processing, and by + * the GetMessage hook. + * @param _td Theread data to update + * @param wParam WPARAM of the key event + * @param scan Scan code of the key event + * @param keyTransition Transition state of the key event (0 = key down, 1 = repeat, 3 = key up) + */ +void UpdateLastKeyCache(PKEYMAN64THREADDATA _td, WPARAM wParam, BYTE scan, BYTE keyTransition) { + _td->LastKey = wParam; + _td->LastScanCode = scan; + _td->LastTransition = keyTransition; +} + BOOL TIPProcessKeyInternal( PKEYMAN64THREADDATA _td, WPARAM wParam, @@ -129,6 +143,11 @@ BOOL TIPProcessKeyInternal( BOOL extended = keyFlags & KF_EXTENDED ? TRUE : FALSE; BYTE scan = keyFlags & 0xFF; BYTE keyTransition = (BYTE)((HIWORD(lParam) & (KF_UP | KF_REPEAT)) >> 14); + WPARAM prevKey = _td->LastKey; + BYTE prevScanCode = _td->LastScanCode; + BYTE prevKeyTransition = _td->LastTransition; + + UpdateLastKeyCache(_td, wParam, scan, keyTransition); SendDebugEntry(); SendDebugMessageFormat("VirtualKey=%s lParam=%x IsUp=%d Extended=%d Updateable=%d Preserved=%d", Debug_VirtualKey((WORD) wParam), lParam, isUp, extended, Updateable, Preserved); @@ -142,15 +161,15 @@ BOOL TIPProcessKeyInternal( // processing it in the core processor. If it is a CapsLock key we update our // Globals::ShiftState. Since this could run before kmhook_getmessage processes the event, // we also check for the scan code flag that we set when generating a key event from Keyman. - if ((_td->LastKey == wParam && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT && _td->LastTransition == keyTransition) + if ((prevKey == wParam && prevScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT && prevKeyTransition == keyTransition) || scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { if (wParam == VK_CAPITAL && !isUp) { // Must also record toggle state change when Keyman has generated // a Caps Lock event ProcessToggleChange((UINT)wParam); // I4793 } - SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [LastKey=%x LastScanCode=%x LastTransition=%x]", - scan, wParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); + SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [prevKey=%x prevScanCode=%x prevKeyTransition=%x]", + scan, wParam, prevKey, prevScanCode, prevKeyTransition); return_SendDebugExit(FALSE); } From 605497543864edb0df15377be995a708f6fe6989 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 13 Mar 2026 09:53:17 +0100 Subject: [PATCH 11/30] docs(linux): add clarifying comment about surrounding text --- linux/ibus-keyman/src/engine.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c index 4ed09bc0ea..c78b9e6785 100644 --- a/linux/ibus-keyman/src/engine.c +++ b/linux/ibus-keyman/src/engine.c @@ -246,6 +246,12 @@ debug_utf8_with_codepoints(const gchar *utf8) { static gboolean client_supports_surrounding_text(IBusEngine *engine) { + // This is not always reliable: IBus detects whether the client supports + // surrounding text by emitting the retrieve-surrounding signal. As part + // of that signal handler, the client is expected to call + // gtk_im_context_set_surrounding_with_selection which ends calling + // set_context_if_needed at a time when IBus is still in the middle of + // determining whether the client supports surrounding text. g_assert(engine != NULL); return (engine->client_capabilities & IBUS_CAP_SURROUNDING_TEXT) != 0; } From d2012ed79b336ad10c3690d5af568c31adec640e Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:41:51 +1000 Subject: [PATCH 12/30] fix(windows): provide clarity to log message --- windows/src/engine/keyman32/appint/aiTIP.cpp | 2 +- windows/src/engine/keyman32/kmprocess.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index 497c177c92..a64e96010c 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -152,7 +152,7 @@ BOOL TIPProcessKeyInternal( SendDebugMessageFormat("VirtualKey=%s lParam=%x IsUp=%d Extended=%d Updateable=%d Preserved=%d", Debug_VirtualKey((WORD) wParam), lParam, isUp, extended, Updateable, Preserved); - if(_td->LastKey == wParam && scan == 0) { // I4642 + if (_td->LastKey == wParam && scan == 0) { // I4642 - handle issue with Logos application. scan = _td->LastScanCode; SendDebugMessageFormat("Scan code was zero so using cached scan code %x", scan); } diff --git a/windows/src/engine/keyman32/kmprocess.cpp b/windows/src/engine/keyman32/kmprocess.cpp index 8ba4c22a8b..6e1eff08e3 100644 --- a/windows/src/engine/keyman32/kmprocess.cpp +++ b/windows/src/engine/keyman32/kmprocess.cpp @@ -131,7 +131,7 @@ BOOL ProcessHook() _td->lpActiveKeyboard->lpCoreKeyboardState, KM_CORE_DEBUG_CONTEXT_CACHED ); - SendDebugMessageFormatW(L"Key %s: %hs Context '%s'", + SendDebugMessageFormatW(L"Key %s: %hs Core Cached Context '%s'", _td->state.isDown ? L"pressed" : L"released", Debug_VirtualKey(_td->state.vkey), debug_context); km_core_cu_dispose(debug_context); From c23284ea511e0ab4d028aad65f44bd626f8c1bca Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:19:45 +1000 Subject: [PATCH 13/30] fix(windows): rename fTestEatenBuf to fOnTestEatenBuf --- windows/src/engine/kmtip/keys.cpp | 6 +++--- windows/src/engine/kmtip/kmtip.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/windows/src/engine/kmtip/keys.cpp b/windows/src/engine/kmtip/keys.cpp index afddecfef0..9bf67a5f99 100644 --- a/windows/src/engine/kmtip/keys.cpp +++ b/windows/src/engine/kmtip/keys.cpp @@ -76,7 +76,7 @@ BOOL CKMTipTextService::_InitKeystrokeSink() pKeystrokeMgr->Release(); memset(fEatenBuf, 0, sizeof(fEatenBuf)); // OnKeyDown/Up - memset(fTestEatenBuf, 0, sizeof(fTestEatenBuf)); // OnTestKeyDown/Up + memset(fOnTestEatenBuf, 0, sizeof(fOnTestEatenBuf)); // OnTestKeyDown/Up return_SendDebugExit(_keystrokeSinkInitialized = (hr == S_OK)); } @@ -179,7 +179,7 @@ STDAPI CKMTipTextService::OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPA { SendDebugEntry(); LogKey(0, wParam, lParam); - fTestEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 + fOnTestEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 SendDebugMessageFormat(L"pfEaten=%s, wParam=%x, lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; @@ -215,7 +215,7 @@ STDAPI CKMTipTextService::OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARA SendDebugEntry(); LogKey(2, wParam, lParam); _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 - *pfEaten = fTestEatenBuf[wParam]; + *pfEaten = fOnTestEatenBuf[wParam]; SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; diff --git a/windows/src/engine/kmtip/kmtip.h b/windows/src/engine/kmtip/kmtip.h index 7df21f7428..a067617d44 100644 --- a/windows/src/engine/kmtip/kmtip.h +++ b/windows/src/engine/kmtip/kmtip.h @@ -117,7 +117,7 @@ private: BOOL _keystrokeSinkInitialized; BOOL fEatenBuf[256]; - BOOL fTestEatenBuf[256]; + BOOL fOnTestEatenBuf[256]; ITfThreadMgr *_pThreadMgr; TfClientId _tfClientId; From 40a0c929d9663a836032d7ace934ca7270366ed6 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 27 Feb 2026 17:48:44 +0100 Subject: [PATCH 14/30] fix(linux): fix context after typing Bksp with Wayland This fixes a problem with the context after typing Backspace when using Wayland. Wayland uses double-buffering for the text, so we always have to commit after making changes. Fixes: #15676 --- linux/ibus-keyman/src/engine.c | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c index c78b9e6785..b8d0dd222d 100644 --- a/linux/ibus-keyman/src/engine.c +++ b/linux/ibus-keyman/src/engine.c @@ -83,10 +83,10 @@ struct _IBusKeymanEngine { gboolean rctrl_pressed; gboolean lalt_pressed; gboolean ralt_pressed; - IBusLookupTable *table; IBusProperty *status_prop; IBusPropList *prop_list; void *settings; + gboolean is_dirty; commit_queue_item commit_queue[MAX_QUEUE_SIZE]; commit_queue_item *commit_item; @@ -467,6 +467,7 @@ ibus_keyman_engine_constructor( keyman->lctrl_pressed = FALSE; keyman->ralt_pressed = FALSE; keyman->rctrl_pressed = FALSE; + keyman->is_dirty = FALSE; initialize_queue_items(keyman, 0, MAX_QUEUE_SIZE); keyman->commit_item = &keyman->commit_queue[0]; gchar **split_name = g_strsplit(engine_name, ":", 2); @@ -635,10 +636,25 @@ static void commit_string(IBusKeymanEngine *keyman, const gchar *string) IBusText *text; g_autofree gchar *debug = NULL; g_message("DAR: %s - %s", __FUNCTION__, debug = debug_utf8_with_codepoints(string)); - text = ibus_text_new_from_static_string (string); + text = ibus_text_new_from_string(string); g_object_ref_sink(text); ibus_engine_commit_text ((IBusEngine *)keyman, text); g_object_unref (text); + keyman->is_dirty = FALSE; +} + +// Wayland uses double-buffering for surrounding text and some other +// functionality, so we have to commit to apply the changes. +// See https://wayland.app/protocols/input-method-unstable-v2 +static void +apply_changes(IBusKeymanEngine* keyman) { + IBusText* text; + g_message("%s - committing", __FUNCTION__); + text = ibus_text_new_from_static_string(""); + g_object_ref_sink(text); + ibus_engine_commit_text((IBusEngine*)keyman, text); + g_object_unref(text); + keyman->is_dirty = FALSE; } // @@ -649,7 +665,7 @@ is_core_options_end(km_core_option_item *option) { } static void -process_output_action(IBusEngine *engine, const km_core_usv* output_utf32) { +process_output_action(IBusEngine* engine, const km_core_usv* output_utf32) { if (output_utf32 == NULL || output_utf32[0] == '\0') { return; } @@ -689,12 +705,13 @@ process_backspace_action(IBusEngine *engine, unsigned int code_points_to_delete) return; } + IBusKeymanEngine* keyman = (IBusKeymanEngine*)engine; if (client_supports_surrounding_text(engine)) { g_message("%s: compliant app: deleting surrounding text %d codepoints", __FUNCTION__, code_points_to_delete); ibus_engine_delete_surrounding_text(engine, -code_points_to_delete, code_points_to_delete); + keyman->is_dirty = TRUE; } else { g_message("%s: non-compliant app: queueing %d backspaces", __FUNCTION__, code_points_to_delete); - IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine; keyman->commit_item->code_points_to_delete = code_points_to_delete; } } @@ -767,6 +784,7 @@ commit_current_queue_item(IBusKeymanEngine *keyman) { ibus_engine_forward_key_event(engine, KEYMAN_BACKSPACE_KEYSYM, KEYMAN_BACKSPACE, 0); current_item->code_points_to_delete--; } + keyman->is_dirty = TRUE; // don't remove the item from the queue yet - we need to process it // again for the output and keystrokes. Instead emit the sentinel key // again. @@ -786,6 +804,7 @@ commit_current_queue_item(IBusKeymanEngine *keyman) { g_message("%s: Forwarding key from commit queue: keyval=0x%02x, keycode=0x%02x, state=0x%02x", __FUNCTION__, current_item->keyval, current_item->keycode, current_item->state); ibus_engine_forward_key_event(engine, current_item->keyval, current_item->keycode, current_item->state); + keyman->is_dirty = TRUE; } keyman->commit_item--; memmove(keyman->commit_queue, &keyman->commit_queue[1], sizeof(commit_queue_item) * MAX_QUEUE_SIZE - 1); @@ -796,6 +815,10 @@ static void finish_process_actions(IBusEngine *engine) { g_assert(engine != NULL); IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine; + if (keyman->is_dirty) { + apply_changes(keyman); + } + if (client_supports_surrounding_text(engine)) { // compliant app return; @@ -846,6 +869,8 @@ process_actions( IBusEngine *engine, km_core_actions const *actions ) { + IBusKeymanEngine* keyman = (IBusKeymanEngine*)engine; + keyman->is_dirty = FALSE; process_backspace_action(engine, actions->code_points_to_delete); process_output_action(engine, actions->output); process_persist_action(engine, actions->persist_options); From b71f299d7ecd8e4c41176f96c6a915d9a428f62d Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 8 Apr 2026 18:08:34 +0200 Subject: [PATCH 15/30] Update linux/ibus-keyman/src/engine.c Co-authored-by: Marc Durdin --- linux/ibus-keyman/src/engine.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c index b8d0dd222d..c3ffaa54f5 100644 --- a/linux/ibus-keyman/src/engine.c +++ b/linux/ibus-keyman/src/engine.c @@ -643,9 +643,13 @@ static void commit_string(IBusKeymanEngine *keyman, const gchar *string) keyman->is_dirty = FALSE; } -// Wayland uses double-buffering for surrounding text and some other -// functionality, so we have to commit to apply the changes. -// See https://wayland.app/protocols/input-method-unstable-v2 +/** + * Commit an empty string to flush the surrounding text buffer. + * + * Wayland uses double-buffering for surrounding text and some other + * functionality, so we have to commit to apply the changes. + * See https://wayland.app/protocols/input-method-unstable-v2 + */ static void apply_changes(IBusKeymanEngine* keyman) { IBusText* text; From ad9128eb1da6c18ceb2699ea88cea7c65e24704f Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 8 Apr 2026 18:16:08 +0200 Subject: [PATCH 16/30] chore(linux): add comment on purpose of `is_dirty` flag Addresses code review comment. --- linux/ibus-keyman/src/engine.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c index c3ffaa54f5..ce24ad7953 100644 --- a/linux/ibus-keyman/src/engine.c +++ b/linux/ibus-keyman/src/engine.c @@ -86,6 +86,9 @@ struct _IBusKeymanEngine { IBusProperty *status_prop; IBusPropList *prop_list; void *settings; + // Flag indicating if there are pending changes (forwarded keystrokes, + // deleted surrounding text) that need to be applied by committing an + //empty text to the input engine gboolean is_dirty; commit_queue_item commit_queue[MAX_QUEUE_SIZE]; @@ -645,7 +648,7 @@ static void commit_string(IBusKeymanEngine *keyman, const gchar *string) /** * Commit an empty string to flush the surrounding text buffer. - * + * * Wayland uses double-buffering for surrounding text and some other * functionality, so we have to commit to apply the changes. * See https://wayland.app/protocols/input-method-unstable-v2 From 1714e00901e30d265e21ace38f5a7e4e342e18f0 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 9 Apr 2026 19:45:24 +0200 Subject: [PATCH 17/30] docs(linux): add and update docs on GTK and IBus --- linux/docs/internal/gtk-input-methods.md | 43 ++++++++++++------ linux/docs/internal/ibus-sequence-diagram.md | 48 ++++++++++++++++++++ 2 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 linux/docs/internal/ibus-sequence-diagram.md diff --git a/linux/docs/internal/gtk-input-methods.md b/linux/docs/internal/gtk-input-methods.md index f340d72268..bac007ff7c 100644 --- a/linux/docs/internal/gtk-input-methods.md +++ b/linux/docs/internal/gtk-input-methods.md @@ -18,25 +18,42 @@ support. ```mermaid flowchart LR - subgraph GTK - app(application) - im-module["im-ibus - (IBusIMContext)"] - app --- im-module + subgraph App["Application"] + style App fill:blue + subgraph ClientApp["Client App"] + N1("GTK 3/4 Application") + end + subgraph GtkIMContext["Gtk.IMContext"] + N2["GTK 3:
in /usr/lib/x86_64-linux-gnu/
gtk-3.0/3.0.0/immodules:
im-ibus.so
im-wayland.so
im-xim.so"] + N3["GTK 4:
in /usr/lib/x86_64-linux-gnu/
gtk-4.0/4.0.0/immodules:
libim-ibus.so
(wayland built-in)"] + subgraph S2[" "] + IBusIMContext + N4["Source for IBus clients:
https://github.com/ibus/
ibus/tree/main/client"] + end + end end - subgraph ibus[ibus-daemon] - direction LR - IBusInputContext <--> IBusEngine + subgraph IBus["IBus Daemon"] + style IBus fill:purple + IBusInputContext["IBusInputContext"] + IBusEngine["IBusEngine"] + N5["Source for IBus Daemon in https://github.com/ibus/ibus/tree/main/src"] end - subgraph keyman - ibus-engine-keyman - end + KeymanEngine["Keyman Engine"] - GTK <--> ibus <--> keyman + ClientApp ~~~ GtkIMContext + IBusIMContext <-- IBus--> IBusInputContext + IBusInputContext <--> IBusEngine + IBusEngine <-- IBus --> KeymanEngine - style app color:#FFFFFF, stroke:#00C853, fill:#00C853 +style N1 color:#FFFFFF, stroke:#00C853, fill:#00C853 +style N2 fill:none +style N3 fill:none +style N4 fill:none +style N5 fill:none +style S1 fill:none, stroke-width:0 +style S2 fill:none, stroke-width:0 ``` The im-module implements a [GIOExtensionPoint](https://docs.gtk.org/gio/struct.IOExtensionPoint.html) diff --git a/linux/docs/internal/ibus-sequence-diagram.md b/linux/docs/internal/ibus-sequence-diagram.md new file mode 100644 index 0000000000..8a92a40b4e --- /dev/null +++ b/linux/docs/internal/ibus-sequence-diagram.md @@ -0,0 +1,48 @@ +# Communication between Application, IBus, and Keyman + +The sequence diagram below shows the sequence of events that happen in +response to IBus sending the "Enable" signal to the Keyman Engine. +This illustrates why `client_supports_surrounding_text()` might return +a wrong value initially. + +```mermaid +sequenceDiagram + autonumber + box blue Application + participant A as Client + participant IMC as IBusIMContext + end + box purple IBus + participant InC as IBusInputContext + participant E as IBusEngine + end + participant K as Keyman Engine + + E->>+K: enable signal + K->>+E: IBusEngine.get_surrounding_text(NULL, NULL, NULL) + Note left of K: Keyman wants to use surrounding text + E->>+InC: RequireSurroundingText + InC->>InC: set needs_surrounding_text flag = TRUE + InC->>+IMC: require-surrounding-text signal + IMC->>+A: retrieve_surrounding signal + A->>+IMC: gtk_im_context_set_surrounding() + IMC->>+InC: ibus_input_context_set_surrounding_text() + InC->>+E: SetSurroundingText + E->>+K: set_surrounding_text signal + Note right of K: client_supports_surrounding_text() might
report a wrong value
because the processing of
get_surrounding_text() hasn't finished yet + K->>-E: return from
set_surrounding_text signal + E->>-InC: return from
SetSurroundingText + InC->>-IMC: return from
ibus_input_context_set_surrounding_text() + IMC->>-A: return from
gtk_im_context_set_surrounding() + A->>-IMC: return from
retrieve_surrounding signal + IMC->>-InC: return from
require-surrounding-text signal + InC->>-E: return from
RequireSurroundingText + E->>-K: return from
get_surrounding_text() + K->>-E: return from
enable signal +``` + +## Links + +- +- +- From 8e9e957cc09070c26632bf2e9e68b3507871734b Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 7 Apr 2026 14:46:27 +0200 Subject: [PATCH 18/30] fix(linux): some refactorings and improvements --- linux/ibus-keyman/src/engine.c | 82 ++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c index 9b8c65451b..8231f7ed4d 100644 --- a/linux/ibus-keyman/src/engine.c +++ b/linux/ibus-keyman/src/engine.c @@ -253,47 +253,62 @@ client_supports_surrounding_text(IBusEngine *engine) { // surrounding text by emitting the retrieve-surrounding signal. As part // of that signal handler, the client is expected to call // gtk_im_context_set_surrounding_with_selection which ends calling - // set_context_if_needed at a time when IBus is still in the middle of - // determining whether the client supports surrounding text. + // ibus_keyman_engine_set_surrounding_text at a time when IBus is still + // in the middle of determining whether the client supports surrounding text. g_assert(engine != NULL); + if (!engine->enabled) { + g_warning("%s: engine is not enabled, so return value is likley incorrect.", __FUNCTION__); + } return (engine->client_capabilities & IBUS_CAP_SURROUNDING_TEXT) != 0; } static void -set_context_if_needed(IBusEngine *engine) { - IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine; +set_context_impl( + IBusEngine *engine, + IBusText *text, + guint cursor_pos, + guint anchor_pos +) { + IBusKeymanEngine* keyman = (IBusKeymanEngine*)engine; + g_autofree gchar* application_context_utf8 = NULL; + guint context_start, context_end; + context_end = anchor_pos < cursor_pos ? anchor_pos : cursor_pos; + context_start = context_end > MAXCONTEXT_ITEMS ? context_end - MAXCONTEXT_ITEMS : 0; + application_context_utf8 = g_utf8_substring(ibus_text_get_text(text), context_start, context_end); + g_message( + "%s: new application context: |%s| (len:%u) cursor:%d anchor:%d", __FUNCTION__, application_context_utf8, + context_end - context_start, cursor_pos, anchor_pos); + km_core_cu* application_context_utf16 = g_utf8_to_utf16(application_context_utf8, -1, NULL, NULL, NULL); + km_core_context_status result; + result = km_core_state_context_set_if_needed(keyman->state, application_context_utf16); + g_free(application_context_utf16); + + g_message( + "%s: context %s", __FUNCTION__, + result == KM_CORE_CONTEXT_STATUS_UNCHANGED ? "unchanged" + : result == KM_CORE_CONTEXT_STATUS_UPDATED ? "updated" + : result == KM_CORE_CONTEXT_STATUS_CLEARED ? "cleared" + : result == KM_CORE_CONTEXT_STATUS_ERROR ? "error" + : "invalid argument"); +} + +static void +set_context_if_needed(IBusEngine *engine) { if (!client_supports_surrounding_text(engine)) { g_message("%s: not a compliant client app", __FUNCTION__); return; } IBusText *text; - g_autofree gchar *application_context_utf8 = NULL; - guint cursor_pos, anchor_pos, context_start, context_end; + guint cursor_pos, anchor_pos; g_autofree gchar *debug_context = NULL; g_message("%s: current core context : %s", __FUNCTION__, debug_context = get_context_debug(engine)); ibus_engine_get_surrounding_text(engine, &text, &cursor_pos, &anchor_pos); - context_end = anchor_pos < cursor_pos ? anchor_pos : cursor_pos; - context_start = context_end > MAXCONTEXT_ITEMS ? context_end - MAXCONTEXT_ITEMS : 0; - application_context_utf8 = g_utf8_substring(ibus_text_get_text(text), context_start, context_end); - g_message("%s: new application context: |%s| (len:%u) cursor:%d anchor:%d", __FUNCTION__, - application_context_utf8, context_end - context_start, cursor_pos, anchor_pos); - - km_core_cu *application_context_utf16 = g_utf8_to_utf16(application_context_utf8, -1, NULL, NULL, NULL); - km_core_context_status result; - result = km_core_state_context_set_if_needed(keyman->state, application_context_utf16); - g_free(application_context_utf16); - - g_message("%s: context %s", __FUNCTION__, - result == KM_CORE_CONTEXT_STATUS_UNCHANGED ? "unchanged" - : result == KM_CORE_CONTEXT_STATUS_UPDATED ? "updated" - : result == KM_CORE_CONTEXT_STATUS_CLEARED ? "cleared" - : result == KM_CORE_CONTEXT_STATUS_ERROR ? "error" - : "invalid argument"); + set_context_impl(engine, text, cursor_pos, anchor_pos); } static void @@ -567,8 +582,6 @@ ibus_keyman_engine_constructor( ping_keyman_system_service(); - set_context_if_needed(engine); - return (GObject *) keyman; } @@ -1070,8 +1083,12 @@ ibus_keyman_engine_set_surrounding_text( guint cursor_pos, guint anchor_pos ){ - parent_class->set_surrounding_text(engine, text, cursor_pos, anchor_pos); - set_context_if_needed(engine); + g_message( + "%s: text=%s (len: %u), cursor_pos=%d, anchor_pos=%d", __FUNCTION__, + ibus_text_get_text(text), ibus_text_get_length(text),cursor_pos, anchor_pos); + + parent_class->set_surrounding_text(engine, text, cursor_pos, anchor_pos); + set_context_impl(engine, text, cursor_pos, anchor_pos); } // static void ibus_keyman_engine_set_cursor_location (IBusEngine *engine, @@ -1100,7 +1117,11 @@ ibus_keyman_engine_focus_in (IBusEngine *engine) g_message("%s", __FUNCTION__); ibus_engine_register_properties (engine, keyman->prop_list); - set_context_if_needed(engine); + if (engine->enabled) { + // While the engine is not enabled ibus might not yet know if + // the client app supports surrounding text. + set_context_if_needed(engine); + } parent_class->focus_in (engine); } @@ -1161,7 +1182,8 @@ ibus_keyman_engine_enable (IBusEngine *engine) km_service_set_ldmlfile (service, keyman->ldmlfile); km_service_set_name (service, keyman->kb_name); } - parent_class->enable (engine); + engine->enabled = TRUE; + parent_class->enable(engine); } /** @@ -1186,6 +1208,8 @@ ibus_keyman_engine_disable (IBusEngine *engine) km_service_set_name (service, "None"); // g_clear_object(&service); + engine->enabled = FALSE; + parent_class->disable (engine); } From 139ca64367baf4d0034e066e0cbaa6686172bd1b Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 9 Apr 2026 20:56:20 +0200 Subject: [PATCH 19/30] docs(linux): improve colors --- linux/docs/internal/gtk-input-methods.md | 7 +++---- linux/docs/internal/ibus-sequence-diagram.md | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/linux/docs/internal/gtk-input-methods.md b/linux/docs/internal/gtk-input-methods.md index bac007ff7c..1127c91e96 100644 --- a/linux/docs/internal/gtk-input-methods.md +++ b/linux/docs/internal/gtk-input-methods.md @@ -19,7 +19,7 @@ support. ```mermaid flowchart LR subgraph App["Application"] - style App fill:blue + style App fill:#9999ff subgraph ClientApp["Client App"] N1("GTK 3/4 Application") end @@ -34,7 +34,7 @@ flowchart LR end subgraph IBus["IBus Daemon"] - style IBus fill:purple + style IBus fill:#ff4dff IBusInputContext["IBusInputContext"] IBusEngine["IBusEngine"] N5["Source for IBus Daemon in https://github.com/ibus/ibus/tree/main/src"] @@ -52,7 +52,6 @@ style N2 fill:none style N3 fill:none style N4 fill:none style N5 fill:none -style S1 fill:none, stroke-width:0 style S2 fill:none, stroke-width:0 ``` @@ -146,4 +145,4 @@ After installing a new module the database needs to be updated with ## Links -- [GTK4 Input Handling](https://docs.gtk.org/gtk4/input-handling.html) \ No newline at end of file +- [GTK4 Input Handling](https://docs.gtk.org/gtk4/input-handling.html) diff --git a/linux/docs/internal/ibus-sequence-diagram.md b/linux/docs/internal/ibus-sequence-diagram.md index 8a92a40b4e..467eac43cf 100644 --- a/linux/docs/internal/ibus-sequence-diagram.md +++ b/linux/docs/internal/ibus-sequence-diagram.md @@ -8,11 +8,11 @@ a wrong value initially. ```mermaid sequenceDiagram autonumber - box blue Application + box rgb(153, 153, 255) Application participant A as Client participant IMC as IBusIMContext end - box purple IBus + box rgb(255, 77, 255) IBus participant InC as IBusInputContext participant E as IBusEngine end From e99813b3e3c6810b055a4a0748dfd15366d1b6fa Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 22 Apr 2026 15:33:26 +0200 Subject: [PATCH 20/30] chore(common): cleanup optionals in ldml keyboard reader `boxArrays` had some optional chaining that was either not needed, and/or was used in an inappropriate context, for example within the iterable argument to a `for` statement, which would have caused an "not iterable" error if it ever actually resolved to `undefined`. This should have no material impact on the code, but helps to clarify code readability. Test-bot: skip --- .../ldml-keyboard/ldml-keyboard-xml-reader.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts index f25b03582d..d2ac028e72 100644 --- a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -100,31 +100,31 @@ export class LDMLKeyboardXMLSourceFileReader implements EventResolver { boxXmlArray(source?.keyboard3?.locales, 'locale'); boxXmlArray(source?.keyboard3, 'transforms'); if(source?.keyboard3?.layers) { - for(const layers of source?.keyboard3?.layers) { + for(const layers of source.keyboard3.layers) { boxXmlArray(layers, 'layer'); - if(layers?.layer) { - for(const layer of layers?.layer) { + if(layers.layer) { + for(const layer of layers.layer) { boxXmlArray(layer, 'row'); } } } } if(source?.keyboard3?.forms?.form) { - boxXmlArray(source?.keyboard3?.forms, 'form'); - for(const form of source?.keyboard3?.forms?.form) { + boxXmlArray(source.keyboard3.forms, 'form'); + for(const form of source.keyboard3.forms.form) { boxXmlArray(form, 'scanCodes'); } } if(source?.keyboard3?.flicks) { - boxXmlArray(source?.keyboard3?.flicks, 'flick'); - for(const flick of source?.keyboard3?.flicks?.flick) { + boxXmlArray(source.keyboard3.flicks, 'flick'); + for(const flick of source.keyboard3.flicks.flick) { boxXmlArray(flick, 'flickSegment'); } } if(source?.keyboard3?.variables) { - boxXmlArray(source?.keyboard3?.variables, 'set'); - boxXmlArray(source?.keyboard3?.variables, 'string'); - boxXmlArray(source?.keyboard3?.variables, 'uset'); + boxXmlArray(source.keyboard3.variables, 'set'); + boxXmlArray(source.keyboard3.variables, 'string'); + boxXmlArray(source.keyboard3.variables, 'uset'); } if(source?.keyboard3?.transforms) { for(const transforms of source.keyboard3.transforms) { From 02db51c9b61611d140413ee1c41c91714ea8e571 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:16:17 +1000 Subject: [PATCH 21/30] fix(windows): address review comments --- windows/src/engine/keyman32/appint/aiTIP.cpp | 12 ++---------- windows/src/engine/keyman32/appint/aiTIP.h | 15 +++++++++++++++ windows/src/engine/keyman32/keyman64.h | 7 +++++-- windows/src/engine/keyman32/kmhook_getmessage.cpp | 4 +--- windows/src/engine/keyman32/kmprocessactions.cpp | 7 +++---- 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index a64e96010c..a2baada55b 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -114,14 +114,6 @@ void ProcessToggleChange(UINT key) { // I4793 } } -/** - * Update the cache of the last key event received, this is set here - TIP processing, and by - * the GetMessage hook. - * @param _td Theread data to update - * @param wParam WPARAM of the key event - * @param scan Scan code of the key event - * @param keyTransition Transition state of the key event (0 = key down, 1 = repeat, 3 = key up) - */ void UpdateLastKeyCache(PKEYMAN64THREADDATA _td, WPARAM wParam, BYTE scan, BYTE keyTransition) { _td->LastKey = wParam; _td->LastScanCode = scan; @@ -142,7 +134,7 @@ BOOL TIPProcessKeyInternal( BOOL isUp = keyFlags & KF_UP ? TRUE : FALSE; BOOL extended = keyFlags & KF_EXTENDED ? TRUE : FALSE; BYTE scan = keyFlags & 0xFF; - BYTE keyTransition = (BYTE)((HIWORD(lParam) & (KF_UP | KF_REPEAT)) >> 14); + BYTE keyTransition = KEYMSG_FLAG_TRANSITION(lParam); WPARAM prevKey = _td->LastKey; BYTE prevScanCode = _td->LastScanCode; BYTE prevKeyTransition = _td->LastTransition; @@ -152,7 +144,7 @@ BOOL TIPProcessKeyInternal( SendDebugMessageFormat("VirtualKey=%s lParam=%x IsUp=%d Extended=%d Updateable=%d Preserved=%d", Debug_VirtualKey((WORD) wParam), lParam, isUp, extended, Updateable, Preserved); - if (_td->LastKey == wParam && scan == 0) { // I4642 - handle issue with Logos application. + if (_td->LastKey == wParam && scan == 0) { // I4642 - handle issue with Logos application. scan = _td->LastScanCode; SendDebugMessageFormat("Scan code was zero so using cached scan code %x", scan); } diff --git a/windows/src/engine/keyman32/appint/aiTIP.h b/windows/src/engine/keyman32/appint/aiTIP.h index 2e6b105d98..e379b49941 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.h +++ b/windows/src/engine/keyman32/appint/aiTIP.h @@ -88,4 +88,19 @@ public: * @param key */ void ProcessToggleChange(UINT key); + +// Forward declaration - final in globals.h +struct tagKEYMAN64THREADDATA; +typedef struct tagKEYMAN64THREADDATA *PKEYMAN64THREADDATA; + +/** + * Update the cache of the last key event received, this is set here - TIP processing, and by + * the GetMessage hook. + * @param _td Thread data to update + * @param wParam WPARAM of the key event + * @param scan Scan code of the key event + * @param keyTransition Transition state of the key event (00b = key down, 01b = repeat, 11b = keyup) + */ +void UpdateLastKeyCache(PKEYMAN64THREADDATA _td, WPARAM wParam, BYTE scan, BYTE keyTransition); + #endif diff --git a/windows/src/engine/keyman32/keyman64.h b/windows/src/engine/keyman32/keyman64.h index 49ce6bab33..eb199722c7 100644 --- a/windows/src/engine/keyman32/keyman64.h +++ b/windows/src/engine/keyman32/keyman64.h @@ -118,11 +118,14 @@ #define KEYMSG_FLAG_DLGMODE(lParam) (HIWORD(lParam) & KF_DLGMODE ? 1 : 0) #define KEYMSG_FLAG_MENUMODE(lParam) (HIWORD(lParam) & KF_MENUMODE ? 1 : 0) #define KEYMSG_FLAG_ALTDOWN(lParam) (HIWORD(lParam) & KF_ALTDOWN ? 1 : 0) -// Repeat is actually previous "up" value of the key +// Repeat is actually previous KF_UP value of the key. +// It is always set to 1 for WM_KEYUP and WM_SYSKEYUP messages. +// It is set to 1 for WM_KEYDOWN and WM_SYSKEYDOWN keystroke messages generated by the automatic repeat feature. +// see https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#previous-key-state-flag #define KEYMSG_FLAG_REPEAT(lParam) (HIWORD(lParam) & KF_REPEAT ? 1 : 0) #define KEYMSG_FLAG_UP(lParam) (HIWORD(lParam) & KF_UP ? 1 : 0) // Combine the transition flags into a single value for higher chance of identification -// 0 = key down, 1 = repeat, 2 = keyup +// 00b = key down, 01b = repeat, 11b = keyup #define KEYMSG_FLAG_TRANSITION(lParam) ((BYTE)((HIWORD(lParam) & (KF_UP | KF_REPEAT)) >> 14)) // TODO: Deprecate overloading of scancodes and use dwExtraInfo instead diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index da01903963..4c11dc31b5 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -158,9 +158,7 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) BYTE scan = KEYMSG_LPARAM_SCAN(mp->lParam); BYTE keyTransitionEvent = KEYMSG_FLAG_TRANSITION(mp->lParam); CheckScheduledRefresh(); - _td->LastScanCode = scan; - _td->LastKey = mp->wParam; - _td->LastTransition = keyTransitionEvent; + UpdateLastKeyCache(_td, mp->wParam, scan, keyTransitionEvent); switch (mp->wParam) { case VK_MENU: diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index 64089c0670..2694658762 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -79,16 +79,15 @@ processPersistOpt(km_core_actions const* actions, LPINTKEYBOARDINFO activeKeyboa static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isUp, BOOL Updateable, BOOL externalEvent) { BOOL isCapsOn = IsCapsLockOn(); - + // This debug message is useful for understanding the sequence of events around caps lock changes //SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", caps_state_change, isUp, Updateable, // externalEvent, isCapsOn); - + // We only want to process the Caps Lock key event once; // it has to be when updateble=1 as TSF does not consistently // have updateable=0 events. - if (!Updateable || caps_state_change == KM_CORE_CAPS_UNCHANGED) - { + if (!Updateable || caps_state_change == KM_CORE_CAPS_UNCHANGED) { return; } // Turn three state value into a boolean for whether capslock should be on or off, From 3dad38b4dc862b241f2fb2c756ad925315d5e015 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Thu, 30 Apr 2026 13:01:13 -0500 Subject: [PATCH 22/30] auto: increment master version to 19.0.225 Test-bot: skip Build-bot: skip --- HISTORY.md | 4 ++++ VERSION.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 654ae03b52..ad650b0361 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,9 @@ # Keyman Version History +## 19.0.224 alpha 2026-04-30 + +* fix(linux): fix context after typing Bksp with Wayland (#15820) + ## 19.0.223 alpha 2026-04-23 * fix(android): Reorder display language list (#15863) diff --git a/VERSION.md b/VERSION.md index fe7c6fc70b..8bcb01d62a 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.224 \ No newline at end of file +19.0.225 \ No newline at end of file From 90b1b11d2c4f5ef5b9f4df7c13b1628dab67c82d Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Fri, 1 May 2026 13:01:29 -0500 Subject: [PATCH 23/30] auto: increment master version to 19.0.226 Test-bot: skip Build-bot: skip --- HISTORY.md | 4 ++++ VERSION.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index ad650b0361..24b40bafca 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,9 @@ # Keyman Version History +## 19.0.225 alpha 2026-05-01 + +* fix(windows): contextual help chm file has blank pages (#15724) + ## 19.0.224 alpha 2026-04-30 * fix(linux): fix context after typing Bksp with Wayland (#15820) diff --git a/VERSION.md b/VERSION.md index 8bcb01d62a..1c829c6835 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.225 \ No newline at end of file +19.0.226 \ No newline at end of file From 45773efddfe40ee4e750e63c299c508c6e3ccea1 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Mon, 4 May 2026 13:01:12 -0500 Subject: [PATCH 24/30] auto: increment master version to 19.0.227 Test-bot: skip Build-bot: skip --- HISTORY.md | 4 ++++ VERSION.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 24b40bafca..3f4b87d503 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,9 @@ # Keyman Version History +## 19.0.226 alpha 2026-05-04 + +* chore(common): cleanup optionals in ldml keyboard reader (#15865) + ## 19.0.225 alpha 2026-05-01 * fix(windows): contextual help chm file has blank pages (#15724) diff --git a/VERSION.md b/VERSION.md index 1c829c6835..a3074758b3 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.226 \ No newline at end of file +19.0.227 \ No newline at end of file From 673e044a4b3de384c46ddac6e54f31b9fec16cec Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 5 May 2026 09:34:33 +0200 Subject: [PATCH 25/30] maint(linux): make emscripten setup on build agents idempotent The automatic installation of emscripten on build agents did not work if `EMSCRIPTEN_BASE` was set. This change improves things for a corrupt installation by using `EMSCRIPTEN_BASE` if already set, and by skipping clone if it already exists. Build-bot: skip build:developer Test-bot: skip --- resources/teamcity/includes/tc-linux.inc.sh | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/resources/teamcity/includes/tc-linux.inc.sh b/resources/teamcity/includes/tc-linux.inc.sh index cd02838682..c6a159643c 100644 --- a/resources/teamcity/includes/tc-linux.inc.sh +++ b/resources/teamcity/includes/tc-linux.inc.sh @@ -67,13 +67,25 @@ ba_linux_install_emscripten() { # shellcheck disable=SC2154 . "${KEYMAN_ROOT}/resources/build/minimum-versions.inc.sh" + local EMSDK_DIR + if [[ ! -z "${EMSCRIPTEN_BASE:-}" ]] && [[ -d "${EMSCRIPTEN_BASE}" ]]; then + # EMSCRIPTEN_BASE points to emsdk/upstream/emscripten + EMSDK_DIR="$(realpath "${EMSCRIPTEN_BASE}/../..")" + else + EMSDK_DIR="${HOME}/emsdk" + fi + builder_echo "Installing emscripten version ${KEYMAN_MIN_VERSION_EMSCRIPTEN}" export EMSDK_KEEP_DOWNLOADS=1 + if [[ -d "${EMSDK_DIR}" ]]; then + builder_echo "emsdk directory already exists at ${EMSDK_DIR}, skipping clone" + else + # shellcheck disable=SC2164 + cd "$(dirname "${EMSDK_DIR}")" + git clone https://github.com/emscripten-core/emsdk.git + fi # shellcheck disable=SC2164 - cd "${HOME}" - git clone https://github.com/emscripten-core/emsdk.git - # shellcheck disable=SC2164 - cd emsdk + cd "${EMSDK_DIR}" ./emsdk install "${KEYMAN_MIN_VERSION_EMSCRIPTEN}" ./emsdk activate "${KEYMAN_MIN_VERSION_EMSCRIPTEN}" fi From fd466f338d48b03c6cf08d89b69959fd877b307b Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 5 May 2026 16:31:27 +0200 Subject: [PATCH 26/30] chore: move localization documentation to /docs Test-bot: skip Build-bot: skip --- .../build/l10n => docs/localization}/README.md | 6 +++--- .../localization}/imgs/updating-ios-l10ns-1.png | Bin .../localization}/imgs/updating-ios-l10ns-2.png | Bin 3 files changed, 3 insertions(+), 3 deletions(-) rename {resources/build/l10n => docs/localization}/README.md (96%) rename {resources/build/l10n => docs/localization}/imgs/updating-ios-l10ns-1.png (100%) rename {resources/build/l10n => docs/localization}/imgs/updating-ios-l10ns-2.png (100%) diff --git a/resources/build/l10n/README.md b/docs/localization/README.md similarity index 96% rename from resources/build/l10n/README.md rename to docs/localization/README.md index cb98d820a6..e4a0333b2e 100644 --- a/resources/build/l10n/README.md +++ b/docs/localization/README.md @@ -2,9 +2,9 @@ Localization for Keyman is maintained at https://translate.keyman.com -Downloading and updating files between Keyman and Crowdin happens +Downloading and updating files between Keyman and Crowdin happens on GitHub by way of the Crowdin git integration. The configuration file for all platforms -is a YAML file named [crowdin.yml](../../../crowdin.yml). Currently, the git integration tracks the `master` branch. +is a YAML file named [crowdin.yml](../../crowdin.yml). Currently, the git integration tracks the `master` branch. A daily GitHub action to update the source files in Crowdin, which are organized in a top-level folder "master". @@ -60,7 +60,7 @@ project. https://crowdin.com/project/keyman/settings#languages Only the project owner (sil_ltops) can edit custom language codes (at the bottom of the "Target Languages" list). -For some of these, the language mapping needs to be maintained in the "Language Mapping" button on the project settings menu (instead of the [crowdin.yml](../../../crowdin.yml)) configuration file. +For some of these, the language mapping needs to be maintained in the "Language Mapping" button on the project settings menu (instead of the [crowdin.yml](../../crowdin.yml)) configuration file. Language mappings have been set up for "Greek (Polytonic)" and "Shuwa (Latin)" so downloading with the Crowdin CLI would be: ``` diff --git a/resources/build/l10n/imgs/updating-ios-l10ns-1.png b/docs/localization/imgs/updating-ios-l10ns-1.png similarity index 100% rename from resources/build/l10n/imgs/updating-ios-l10ns-1.png rename to docs/localization/imgs/updating-ios-l10ns-1.png diff --git a/resources/build/l10n/imgs/updating-ios-l10ns-2.png b/docs/localization/imgs/updating-ios-l10ns-2.png similarity index 100% rename from resources/build/l10n/imgs/updating-ios-l10ns-2.png rename to docs/localization/imgs/updating-ios-l10ns-2.png From 3116e70a0e38f2d36c2ca0cb29c931787de42f80 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Tue, 5 May 2026 13:01:19 -0500 Subject: [PATCH 27/30] auto: increment master version to 19.0.228 Test-bot: skip Build-bot: skip --- HISTORY.md | 4 ++++ VERSION.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 3f4b87d503..781cd2763d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,9 @@ # Keyman Version History +## 19.0.227 alpha 2026-05-05 + +* maint(linux): make emscripten setup on build agents idempotent (#15908) + ## 19.0.226 alpha 2026-05-04 * chore(common): cleanup optionals in ldml keyboard reader (#15865) diff --git a/VERSION.md b/VERSION.md index a3074758b3..a8a76a6c9c 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.227 \ No newline at end of file +19.0.228 \ No newline at end of file From 17fa6b348ab39b7c2e437b8cf9d06c96d34aa580 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Wed, 6 May 2026 13:01:23 -0500 Subject: [PATCH 28/30] auto: increment master version to 19.0.229 Test-bot: skip Build-bot: skip --- HISTORY.md | 5 +++++ VERSION.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 781cd2763d..7e37ad5267 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # Keyman Version History +## 19.0.228 alpha 2026-05-06 + +* fix(windows): caps lock stores to work in compliant applications (#15771) +* chore: move localization documentation to /docs (#15909) + ## 19.0.227 alpha 2026-05-05 * maint(linux): make emscripten setup on build agents idempotent (#15908) diff --git a/VERSION.md b/VERSION.md index a8a76a6c9c..a7f9177f19 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.228 \ No newline at end of file +19.0.229 \ No newline at end of file From 14e097ab952d43b6b72d87aed756f302818a2b5c Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 7 May 2026 18:08:44 +0200 Subject: [PATCH 29/30] maint(linux): fix source tarball used for packaging The source tarball we upload to downloads.keyman.com contains all files necessary to run `${KEYMAN_ROOT}/build.sh`, i.e. it contains all projects that can be build on Linux. However, that is not the command used when building Debian packages. There we limit the build to the `linux` subfolder. Therefore we only need a subset of the files. Using the full tarball causes some lintian errors. This change creates and uploads a second tarball on release builds that contains only the files needed for Debian packaging. The file is named `keyman_$VERSION.pkg.tar.xz`. Build-bot: skip Test-bot: skip --- linux/scripts/dist.sh | 1 + linux/scripts/package-build.inc.sh | 2 +- linux/scripts/watch.in | 2 +- resources/teamcity/linux/keyman-linux-release.sh | 14 +++++++++----- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/linux/scripts/dist.sh b/linux/scripts/dist.sh index b9337c21cd..dfeaf26110 100755 --- a/linux/scripts/dist.sh +++ b/linux/scripts/dist.sh @@ -70,6 +70,7 @@ to_exclude=( linux/docs/help \ linux/keyman-config/keyman_config/version.py \ linux/keyman-config/buildtools/build-langtags.py \ + linux/upload \ ) if [[ -z "${create_origdist+x}" ]]; then diff --git a/linux/scripts/package-build.inc.sh b/linux/scripts/package-build.inc.sh index 3e37e16c0d..733659f673 100644 --- a/linux/scripts/package-build.inc.sh +++ b/linux/scripts/package-build.inc.sh @@ -43,7 +43,7 @@ function downloadSource() { cd .. mv "${proj}-${version}" "${BASEDIR}/${packageDir}" mv "${proj}_${version}.orig.tar.xz" "${BASEDIR}/${packageDir}" - mv "${proj}-${version}.tar.xz" "${BASEDIR}/${packageDir}" + mv "${proj}_${version}.pkg.tar.xz" "${BASEDIR}/${packageDir}" mv "${proj}"*.asc "${BASEDIR}/${packageDir}" rm "${proj}"*.debian.tar.xz cd "${BASEDIR}/${packageDir}" || exit diff --git a/linux/scripts/watch.in b/linux/scripts/watch.in index b4c0039a84..82a5ce6cde 100644 --- a/linux/scripts/watch.in +++ b/linux/scripts/watch.in @@ -1,3 +1,3 @@ version=4 # Tier replaced by package-build.inc.sh script -opts=pgpsigurlmangle=s/$/.asc/ https://downloads.keyman.com/linux/$tier/@ANY_VERSION@/@PACKAGE@@ANY_VERSION@@ARCHIVE_EXT@ debian uupdate +opts=pgpsigurlmangle=s/$/.asc/ https://downloads.keyman.com/linux/$tier/@ANY_VERSION@/@PACKAGE@@ANY_VERSION@.pkg@ARCHIVE_EXT@ debian uupdate diff --git a/resources/teamcity/linux/keyman-linux-release.sh b/resources/teamcity/linux/keyman-linux-release.sh index be29f77d97..61c5d93cf4 100755 --- a/resources/teamcity/linux/keyman-linux-release.sh +++ b/resources/teamcity/linux/keyman-linux-release.sh @@ -57,10 +57,14 @@ function _cleanup_before_creating_source_package() { function _make_release_source_tarball() { builder_echo start "make source tarball" "Make source tarball" rm -rf dist + mkdir -p "upload/${KEYMAN_VERSION}" ./scripts/reconf.sh PKG_CONFIG_PATH="${KEYMAN_ROOT}/core/build/arch/release/meson-private" ./scripts/dist.sh - mkdir -p "upload/${KEYMAN_VERSION}" - cp -a dist/*.tar.xz "upload/${KEYMAN_VERSION}" + mv dist/*.tar.xz "upload/${KEYMAN_VERSION}/" + builder_echo heading "Make source for packaging" + PKG_CONFIG_PATH="${KEYMAN_ROOT}/core/build/arch/release/meson-private" ./scripts/dist.sh origdist + mv "dist/keyman_${KEYMAN_VERSION}.orig.tar.xz" "dist/keyman_${KEYMAN_VERSION}.pkg.tar.xz" + mv dist/*.tar.xz "upload/${KEYMAN_VERSION}/" ( cd "upload/${KEYMAN_VERSION}" sha256sum ./*.tar.xz > SHA256SUMS @@ -84,10 +88,9 @@ function _sign_source_tarball() { function _publish_to_downloads() { builder_echo start "publish to downloads" "Publish to downloads.keyman.com" - local UPLOAD_DIR KEYMAN_TXZ + local UPLOAD_DIR UPLOAD_DIR="upload/${KEYMAN_VERSION}" - KEYMAN_TXZ="keyman-${KEYMAN_VERSION}.tar.xz" # Set permissions as required on download site builder_echo "Setting upload file permissions for downloads.keyman.com" @@ -96,7 +99,8 @@ function _publish_to_downloads() { chmod g+w "${UPLOAD_DIR}"/* chmod a+r "${UPLOAD_DIR}"/* - write_download_info "${UPLOAD_DIR}" "${KEYMAN_TXZ}" "Keyman for Linux" tar.xz linux + write_download_info "${UPLOAD_DIR}" "keyman-${KEYMAN_VERSION}.tar.xz" "Keyman for Linux source tarball" tar.xz linux + write_download_info "${UPLOAD_DIR}" "keyman_${KEYMAN_VERSION}.pkg.tar.xz" "Keyman for Linux source for packaging" tar.xz linux tc_rsync_upload "${UPLOAD_DIR}" "linux/${KEYMAN_TIER}" builder_echo end "publish to downloads" success "Publish to downloads.keyman.com" From 9b910cab4af3c26a2394d4078ffd0b5994b1c4eb Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Fri, 8 May 2026 13:01:08 -0500 Subject: [PATCH 30/30] auto: increment master version to 19.0.230 Test-bot: skip Build-bot: skip --- HISTORY.md | 4 ++++ VERSION.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 7e37ad5267..7909ef0351 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,9 @@ # Keyman Version History +## 19.0.229 alpha 2026-05-08 + +* maint(linux): fix source tarball used for packaging (#15926) + ## 19.0.228 alpha 2026-05-06 * fix(windows): caps lock stores to work in compliant applications (#15771) diff --git a/VERSION.md b/VERSION.md index a7f9177f19..ad55e04523 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.229 \ No newline at end of file +19.0.230 \ No newline at end of file