From 8aa430c9ee65926e2a9276340c978110e91e5ef2 Mon Sep 17 00:00:00 2001 From: Dmitry Avgustis Date: Fri, 12 Sep 2025 14:20:01 +0200 Subject: [PATCH 1/8] fix build --- README.md | 4 ++++ src/CommandShift.pro | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4e9f0bc..b62e216 100644 --- a/README.md +++ b/README.md @@ -52,3 +52,7 @@ Again, if you have found CommandShift useful please consider supporting my endea * **Patreon (monthly):** https://www.patreon.com/Vasyl_Baran And remember, stay Safe and stay Strong! πŸ‡ΊπŸ‡¦ + +# Build +`/opt/homebrew/opt/qt@5/bin/qmake -config release src/CommandShift.pro +make clean && make -j8` \ No newline at end of file diff --git a/src/CommandShift.pro b/src/CommandShift.pro index dcd43ed..1b6429d 100644 --- a/src/CommandShift.pro +++ b/src/CommandShift.pro @@ -3,12 +3,13 @@ QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets macx { -QMAKE_APPLE_DEVICE_ARCHS = x86_64 arm64 +QMAKE_APPLE_DEVICE_ARCHS = arm64 +QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.15 } -CONFIG += c++11 +CONFIG += c++17 -ICON = icons\icon.icns +ICON = icons/icon.icns # You can make your code fail to compile if it uses deprecated APIs. # In order to do so, uncomment the following line. @@ -36,3 +37,4 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin RESOURCES += \ app_resources.qrc + From fa8dc2eae42995aab6569415d3dbe3f75d70b2c9 Mon Sep 17 00:00:00 2001 From: Dmitry Avgustis Date: Fri, 12 Sep 2025 14:41:25 +0200 Subject: [PATCH 2/8] Make an app independent on default system shortcuts. --- src/CommandShift.pro | 2 +- src/keypresscatcher.cpp | 71 ++++++++++++++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/CommandShift.pro b/src/CommandShift.pro index 1b6429d..c61a6d0 100644 --- a/src/CommandShift.pro +++ b/src/CommandShift.pro @@ -23,7 +23,7 @@ HEADERS += \ constants.h \ keypresscatcher.h -LIBS += -framework ApplicationServices +LIBS += -framework ApplicationServices -framework Carbon # Use custom Info.plist macx { diff --git a/src/keypresscatcher.cpp b/src/keypresscatcher.cpp index 3ca12cc..36593f0 100644 --- a/src/keypresscatcher.cpp +++ b/src/keypresscatcher.cpp @@ -5,6 +5,7 @@ #include #include +#include KeyPressCatcher::KeyPressCatcher(std::function showMessageCallback) : m_showMessageCallback{showMessageCallback} @@ -109,24 +110,63 @@ void KeyPressCatcher::loop() void KeyPressCatcher::sendSystemDefaultChangeLanguageShortcut() { - // Creating a 'Shift + Alt' event - CGEventSourceRef src = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); - CGEventRef spaceDown = CGEventCreateKeyboardEvent(src, 0x31, true); - CGEventRef spaceUp = CGEventCreateKeyboardEvent(src, 0x31, false); + // Current keyboard input source + TISInputSourceRef current = TISCopyCurrentKeyboardInputSource(); + + // Build a list of enabled, selectable keyboard input sources + const void* keys[] = { + kTISPropertyInputSourceCategory, + kTISPropertyInputSourceIsEnabled, + kTISPropertyInputSourceIsSelectCapable + }; + const void* values[] = { + kTISCategoryKeyboardInputSource, + kCFBooleanTrue, + kCFBooleanTrue + }; + + CFDictionaryRef filter = CFDictionaryCreate(kCFAllocatorDefault, + keys, + values, + 3, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + + CFArrayRef sources = TISCreateInputSourceList(filter, false); + if (filter) CFRelease(filter); + + if (!sources) { + if (current) CFRelease(current); + return; + } + + CFIndex count = CFArrayGetCount(sources); + if (count <= 0) { + CFRelease(sources); + if (current) CFRelease(current); + return; + } - CGEventSetFlags(spaceDown, kCGEventFlagMaskAlternate); - CGEventSetFlags(spaceUp, kCGEventFlagMaskAlternate); - CGEventSetFlags(spaceDown, kCGEventFlagMaskControl); - CGEventSetFlags(spaceUp, kCGEventFlagMaskControl); + // Find the current index + CFIndex currentIndex = -1; + for (CFIndex i = 0; i < count; ++i) { + TISInputSourceRef s = (TISInputSourceRef)CFArrayGetValueAtIndex(sources, i); + if (current && CFEqual(s, current)) { + currentIndex = i; + break; + } + } - CGEventTapLocation loc = kCGHIDEventTap; + // Compute next index (wrap-around). If current not found, pick index 0. + CFIndex nextIndex = (currentIndex >= 0) ? ((currentIndex + 1) % count) : 0; + TISInputSourceRef next = (TISInputSourceRef)CFArrayGetValueAtIndex(sources, nextIndex); - CGEventPost(loc, spaceDown); - CGEventPost(loc, spaceUp); + if (next) { + TISSelectInputSource(next); + } - CFRelease(src); - CFRelease(spaceDown); - CFRelease(spaceUp); + CFRelease(sources); + if (current) CFRelease(current); } void KeyPressCatcher::handleModifierKeysStatusChange(bool shift_pressed_down, bool second_key_pressed_down) @@ -158,7 +198,8 @@ bool KeyPressCatcher::init() m_eventTapPtr = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, modifiersPressedMask, [] (CGEventTapProxy, CGEventType type, CGEventRef event, void *keyPressCatcherRawPtr) - { + { + (void)type; // silence unused parameter warning auto catcher = static_cast(keyPressCatcherRawPtr); CGEventFlags flags = CGEventGetFlags(event); auto secondTriggerKey = catcher->getSecondShortcutKey(); From 6fdc0db44ca587d32f94991f2810fd54496c481e Mon Sep 17 00:00:00 2001 From: dmitryavgustis Date: Wed, 19 Aug 2026 12:03:25 +0200 Subject: [PATCH 3/8] Add a reproducible local build, sign and install script Building this from source needed a fair amount of undocumented setup, so capture it: * build.sh runs qmake -> make -> macdeployqt -> codesign, with an optional "install" step that replaces /Applications/CommandShift.app. * README documents the one-time setup, including creating a self-signed code-signing certificate. This matters more than it looks: macOS ties the Accessibility grant to the code signature, and an unsigned or ad-hoc signed build gets a new hash on every compile, so the permission has to be granted again after each rebuild. Signing with a stable certificate makes the designated requirement "identifier + certificate leaf", which survives. * Raise the deployment target from 10.15 to 14.0. Qt 6 cannot target 10.15 at all, so the old value was silently wrong; 14.0 matches what Homebrew's Qt is built against and keeps the link step warning-free. Co-Authored-By: Claude Opus 5 --- .gitignore | 10 +++++++++ README.md | 51 +++++++++++++++++++++++++++++++++++++++++--- build.sh | 51 ++++++++++++++++++++++++++++++++++++++++++++ src/CommandShift.pro | 2 +- src/Info.plist | 2 +- 5 files changed, 111 insertions(+), 5 deletions(-) create mode 100755 build.sh diff --git a/.gitignore b/.gitignore index e43b0f9..1d6c346 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,11 @@ .DS_Store + +# Build output +build/ +*.o +*.app +moc_*.cpp +moc_*.h +qrc_*.cpp +Makefile +.qmake.stash diff --git a/README.md b/README.md index b62e216..9c7b429 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,51 @@ Again, if you have found CommandShift useful please consider supporting my endea And remember, stay Safe and stay Strong! πŸ‡ΊπŸ‡¦ -# Build -`/opt/homebrew/opt/qt@5/bin/qmake -config release src/CommandShift.pro -make clean && make -j8` \ No newline at end of file +# Building from source (macOS) + +### One-time setup + +1. Xcode Command Line Tools: + + ``` + xcode-select --install + ``` + +2. Qt 6: + + ``` + brew install qt + ``` + +3. A code-signing identity. CommandShift needs Accessibility permission, and macOS ties + that permission to the code signature. Without a stable signing identity you have to + re-grant Accessibility after *every* rebuild. Creating a self-signed certificate once + avoids that: + + ``` + openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \ + -keyout key.pem -out cert.pem -subj "/CN=CommandShift Dev" \ + -addext "basicConstraints=critical,CA:false" \ + -addext "keyUsage=critical,digitalSignature" \ + -addext "extendedKeyUsage=critical,codeSigning" + openssl pkcs12 -export -inkey key.pem -in cert.pem -out identity.p12 \ + -passout pass:csdev -name "CommandShift Dev" + security import identity.p12 -k ~/Library/Keychains/login.keychain-db \ + -P csdev -T /usr/bin/codesign + security add-trusted-cert -r trustRoot -p codeSign \ + -k ~/Library/Keychains/login.keychain-db cert.pem + ``` + + Verify with `security find-identity -v -p codesigning`. + +### Build + +``` +./build.sh # build + sign into ./build/CommandShift.app +./build.sh install # ...and replace /Applications/CommandShift.app, then launch +``` + +Override the defaults with `QT_PREFIX` and `CS_SIGN_IDENTITY` if needed. + +Note: a Homebrew Qt build is arm64-only and targets macOS 14+. Use the official Qt +installer if you need a universal binary or a lower deployment target for distribution. \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..e7db580 --- /dev/null +++ b/build.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Build, sign and install CommandShift locally. +# +# ./build.sh build + sign +# ./build.sh install build + sign + replace /Applications/CommandShift.app +# +# Requires: brew install qt and a "CommandShift Dev" code-signing identity +# (see README "Building from source"). + +set -euo pipefail + +QT_PREFIX="${QT_PREFIX:-/opt/homebrew/opt/qt}" +QMAKE="$QT_PREFIX/bin/qmake" +MACDEPLOYQT="$QT_PREFIX/bin/macdeployqt" +IDENTITY="${CS_SIGN_IDENTITY:-CommandShift Dev}" + +ROOT="$(cd "$(dirname "$0")" && pwd)" +BUILD="$ROOT/build" +APP="$BUILD/CommandShift.app" + +[ -x "$QMAKE" ] || { echo "qmake not found at $QMAKE -- run: brew install qt"; exit 1; } + +echo "==> Configuring" +mkdir -p "$BUILD" +cd "$BUILD" +"$QMAKE" "$ROOT/src/CommandShift.pro" CONFIG+=release + +echo "==> Building" +make -j"$(sysctl -n hw.ncpu)" + +echo "==> Bundling Qt frameworks" +"$MACDEPLOYQT" "$APP" + +echo "==> Signing as '$IDENTITY'" +# Sign nested code first, then the bundle, so seals stay valid. +find "$APP/Contents/Frameworks" "$APP/Contents/PlugIns" -name '*.dylib' -o -name '*.framework' 2>/dev/null \ + | while read -r item; do codesign --force --timestamp=none --sign "$IDENTITY" "$item" >/dev/null 2>&1 || true; done +codesign --force --deep --timestamp=none --sign "$IDENTITY" "$APP" +codesign --verify --verbose=2 "$APP" + +echo "==> Built: $APP" + +if [ "${1:-}" = "install" ]; then + echo "==> Installing to /Applications" + pkill -x CommandShift 2>/dev/null || true + sleep 1 + rm -rf /Applications/CommandShift.app + cp -R "$APP" /Applications/CommandShift.app + echo "==> Installed. Launching." + open /Applications/CommandShift.app +fi diff --git a/src/CommandShift.pro b/src/CommandShift.pro index c61a6d0..ab96260 100644 --- a/src/CommandShift.pro +++ b/src/CommandShift.pro @@ -4,7 +4,7 @@ greaterThan(QT_MAJOR_VERSION, 4): QT += widgets macx { QMAKE_APPLE_DEVICE_ARCHS = arm64 -QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.15 +QMAKE_MACOSX_DEPLOYMENT_TARGET = 14.0 } CONFIG += c++17 diff --git a/src/Info.plist b/src/Info.plist index a75523b..5323c20 100644 --- a/src/Info.plist +++ b/src/Info.plist @@ -17,7 +17,7 @@ CFBundleSignature ???? LSMinimumSystemVersion - 11.0 + 14.0 LSUIElement 1 NSPrincipalClass From 43b42b29c9db03bf97c22dacf67c652607001164 Mon Sep 17 00:00:00 2001 From: dmitryavgustis Date: Wed, 19 Aug 2026 12:03:25 +0200 Subject: [PATCH 4/8] Switch input source directly instead of faking a system shortcut CommandShift used to synthesize a Ctrl+Space keystroke and rely on macOS still having "Select the previous input source" bound to it. That forces users to keep a default binding they often want for something else -- Ctrl+Space is the autocomplete shortcut in most IDEs -- and it inherits the latency and occasional dropped events of the system hotkey machinery. Ask Text Input Services to change the source instead, which depends on no binding at all and applies immediately. Switching is "return to the previously used source", tracked by observing kTISNotifySelectedKeyboardInputSourceChanged so that switches made from the menu bar or by another app are remembered too. When there is nothing to go back to, or it has since been disabled, fall back to the next source in the list. Along the way: * Compare sources by kTISPropertyInputSourceID rather than CFEqual. Two TISInputSourceRef handles to the same source are not guaranteed to compare equal, and when they don't the lookup silently returns "not found" and the same source is selected every time. * Filter on input source type, not just category: Press-and-Hold and the character palette are also in the keyboard category but cannot be switched to. * Defer the switch out of the event-tap callback. Taps run on a latency budget and macOS disables them when a callback takes too long. * Handle kCGEventTapDisabledByTimeout and kCGEventTapDisabledByUserInput by re-enabling the tap. These arrive regardless of the event mask, and without handling them a single disable left the app deaf until it was restarted. * Release the CFRunLoopSource created in init(). Co-Authored-By: Claude Opus 5 --- src/keypresscatcher.cpp | 303 +++++++++++++++++++++++++++++++++------- src/keypresscatcher.h | 18 ++- 2 files changed, 266 insertions(+), 55 deletions(-) diff --git a/src/keypresscatcher.cpp b/src/keypresscatcher.cpp index 36593f0..8b8ec31 100644 --- a/src/keypresscatcher.cpp +++ b/src/keypresscatcher.cpp @@ -7,6 +7,155 @@ #include #include +namespace +{ + // An input source's unique identifier, e.g. "com.apple.keylayout.ABC" + QString inputSourceID(TISInputSourceRef source) + { + if (source == nullptr) + { + return QString(); + } + + auto identifier = (CFStringRef)TISGetInputSourceProperty(source, kTISPropertyInputSourceID); + return (identifier != nullptr) ? QString::fromCFString(identifier) : QString(); + } + + QString currentInputSourceID() + { + TISInputSourceRef current = TISCopyCurrentKeyboardInputSource(); + auto identifier = inputSourceID(current); + + if (current != nullptr) + { + CFRelease(current); + } + + return identifier; + } + + // Keyboard layouts (ABC, RussianWin, ...) and input modes (Chinese, Japanese, ...) are the + // only things a user actually switches between. Character palettes, Press-and-Hold and other + // helpers also live in the "keyboard" category, so filtering on category alone is not enough. + bool isSwitchableKeyboardSource(TISInputSourceRef source) + { + auto type = (CFStringRef)TISGetInputSourceProperty(source, kTISPropertyInputSourceType); + if (type == nullptr) + { + return false; + } + + return CFEqual(type, kTISTypeKeyboardLayout) || CFEqual(type, kTISTypeKeyboardInputMode); + } + + // Enabled, selectable keyboard sources in the order macOS reports them. Caller owns the result. + CFArrayRef copySwitchableInputSources() + { + const void* keys[] = { + kTISPropertyInputSourceCategory, + kTISPropertyInputSourceIsEnabled, + kTISPropertyInputSourceIsSelectCapable + }; + const void* values[] = { + kTISCategoryKeyboardInputSource, + kCFBooleanTrue, + kCFBooleanTrue + }; + + CFDictionaryRef filter = CFDictionaryCreate(kCFAllocatorDefault, + keys, + values, + 3, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + if (filter == nullptr) + { + return nullptr; + } + + CFArrayRef candidates = TISCreateInputSourceList(filter, false); + CFRelease(filter); + + if (candidates == nullptr) + { + return nullptr; + } + + CFMutableArrayRef sources = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); + CFIndex candidateCount = CFArrayGetCount(candidates); + for (CFIndex i = 0; i < candidateCount; ++i) + { + auto candidate = (TISInputSourceRef)CFArrayGetValueAtIndex(candidates, i); + if (isSwitchableKeyboardSource(candidate)) + { + CFArrayAppendValue(sources, candidate); + } + } + CFRelease(candidates); + + return sources; + } + + bool selectInputSourceWithID(const QString& wantedID) + { + CFArrayRef sources = copySwitchableInputSources(); + if (sources == nullptr) + { + return false; + } + + bool selected = false; + CFIndex count = CFArrayGetCount(sources); + for (CFIndex i = 0; i < count && !selected; ++i) + { + auto source = (TISInputSourceRef)CFArrayGetValueAtIndex(sources, i); + if (inputSourceID(source) == wantedID) + { + selected = (TISSelectInputSource(source) == noErr); + } + } + + CFRelease(sources); + return selected; + } + + // Used when there is no remembered previous source yet, or it has since been disabled + bool selectNextInputSourceAfter(const QString& currentID) + { + CFArrayRef sources = copySwitchableInputSources(); + if (sources == nullptr) + { + return false; + } + + CFIndex count = CFArrayGetCount(sources); + if (count < 2) + { + CFRelease(sources); + return false; + } + + // Match on the identifier string: two TISInputSourceRef handles to the same + // source are not guaranteed to compare equal, so CFEqual is not reliable here. + CFIndex currentIndex = -1; + for (CFIndex i = 0; i < count; ++i) + { + if (inputSourceID((TISInputSourceRef)CFArrayGetValueAtIndex(sources, i)) == currentID) + { + currentIndex = i; + break; + } + } + + CFIndex nextIndex = (currentIndex >= 0) ? ((currentIndex + 1) % count) : 0; + auto next = (TISInputSourceRef)CFArrayGetValueAtIndex(sources, nextIndex); + bool selected = (next != nullptr) && (TISSelectInputSource(next) == noErr); + + CFRelease(sources); + return selected; + } +} + KeyPressCatcher::KeyPressCatcher(std::function showMessageCallback) : m_showMessageCallback{showMessageCallback} { @@ -21,6 +170,8 @@ KeyPressCatcher::KeyPressCatcher(std::function(observer)->onInputSourceChanged(); + }, + kTISNotifySelectedKeyboardInputSourceChanged, + nullptr, + CFNotificationSuspensionBehaviorDeliverImmediately); + + m_observing_input_source = true; +} + +void KeyPressCatcher::stopObservingInputSource() +{ + if (!m_observing_input_source) + { + return; + } - CFDictionaryRef filter = CFDictionaryCreate(kCFAllocatorDefault, - keys, - values, - 3, - &kCFTypeDictionaryKeyCallBacks, - &kCFTypeDictionaryValueCallBacks); + CFNotificationCenterRemoveObserver(CFNotificationCenterGetDistributedCenter(), + this, + kTISNotifySelectedKeyboardInputSourceChanged, + nullptr); - CFArrayRef sources = TISCreateInputSourceList(filter, false); - if (filter) CFRelease(filter); + m_observing_input_source = false; +} - if (!sources) { - if (current) CFRelease(current); +void KeyPressCatcher::onInputSourceChanged() +{ + auto nowSelected = currentInputSourceID(); + if (nowSelected.isEmpty() || nowSelected == m_currentSourceID) + { return; } - CFIndex count = CFArrayGetCount(sources); - if (count <= 0) { - CFRelease(sources); - if (current) CFRelease(current); + m_previousSourceID = m_currentSourceID; + m_currentSourceID = nowSelected; +} + +void KeyPressCatcher::requestInputSourceSwitch() +{ + // Text Input Services must not be called from the event-tap callback: the tap runs on a + // latency budget and macOS disables it if we take too long. Hand the work to the event loop. + if (m_switch_queued) + { return; } - // Find the current index - CFIndex currentIndex = -1; - for (CFIndex i = 0; i < count; ++i) { - TISInputSourceRef s = (TISInputSourceRef)CFArrayGetValueAtIndex(sources, i); - if (current && CFEqual(s, current)) { - currentIndex = i; - break; - } - } + m_switch_queued = true; + QTimer::singleShot(0, [this] + { + m_switch_queued = false; + toggleInputSource(); + }); +} - // Compute next index (wrap-around). If current not found, pick index 0. - CFIndex nextIndex = (currentIndex >= 0) ? ((currentIndex + 1) % count) : 0; - TISInputSourceRef next = (TISInputSourceRef)CFArrayGetValueAtIndex(sources, nextIndex); +void KeyPressCatcher::toggleInputSource() +{ + auto current = currentInputSourceID(); - if (next) { - TISSelectInputSource(next); + // The notification can be missed (it does not fire for the source we start up on), + // so reconcile what we think is current before deciding where to go. + if (!current.isEmpty() && current != m_currentSourceID) + { + m_previousSourceID = m_currentSourceID; + m_currentSourceID = current; } - CFRelease(sources); - if (current) CFRelease(current); + if (!m_previousSourceID.isEmpty() && + m_previousSourceID != current && + selectInputSourceWithID(m_previousSourceID)) + { + return; + } + + // Nothing worth going back to yet, or it was removed in System Settings + selectNextInputSourceAfter(current); +} + +void KeyPressCatcher::reenableEventTap() +{ + if (m_eventTapPtr != nullptr) + { + CGEventTapEnable(m_eventTapPtr, true); + } } void KeyPressCatcher::handleModifierKeysStatusChange(bool shift_pressed_down, bool second_key_pressed_down) @@ -179,7 +370,7 @@ void KeyPressCatcher::handleModifierKeysStatusChange(bool shift_pressed_down, bo } else if (!shift_pressed_down && m_pending) { - sendSystemDefaultChangeLanguageShortcut(); + requestInputSourceSwitch(); m_pending = false; } } @@ -187,7 +378,7 @@ void KeyPressCatcher::handleModifierKeysStatusChange(bool shift_pressed_down, bo { if (shift_pressed_down && second_key_pressed_down) { - sendSystemDefaultChangeLanguageShortcut(); + requestInputSourceSwitch(); } } } @@ -199,8 +390,16 @@ bool KeyPressCatcher::init() m_eventTapPtr = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, modifiersPressedMask, [] (CGEventTapProxy, CGEventType type, CGEventRef event, void *keyPressCatcherRawPtr) { - (void)type; // silence unused parameter warning auto catcher = static_cast(keyPressCatcherRawPtr); + + // These two arrive regardless of the mask we asked for. Without handling + // them the app goes quietly deaf until it is restarted. + if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) + { + catcher->reenableEventTap(); + return event; + } + CGEventFlags flags = CGEventGetFlags(event); auto secondTriggerKey = catcher->getSecondShortcutKey(); // Checking whether a second key that we expected (depending on configuration) was pressed @@ -220,10 +419,10 @@ bool KeyPressCatcher::init() return false; } - CFRunLoopAddSource(CFRunLoopGetCurrent(), - CFMachPortCreateRunLoopSource(kCFAllocatorDefault, m_eventTapPtr, 0), - kCFRunLoopCommonModes); + CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, m_eventTapPtr, 0); + CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes); + CFRelease(runLoopSource); - CGEventTapEnable(m_eventTapPtr, true); - return true; + CGEventTapEnable(m_eventTapPtr, true); + return true; } diff --git a/src/keypresscatcher.h b/src/keypresscatcher.h index ab3c712..e0b42a7 100644 --- a/src/keypresscatcher.h +++ b/src/keypresscatcher.h @@ -24,14 +24,21 @@ class KeyPressCatcher private: bool init(); - void retryInit(); // Notify user that we've started successfully void notifyAboutSuccessfulStart(); // Notify user that we lost 'Privileges' (i.e. removed from Accessibility) void notifyUserAboutLostPrivileges(); - // Tell the system that we want to change the language - void sendSystemDefaultChangeLanguageShortcut(); + // Ask for a language change; the switch itself runs outside the event-tap callback + void requestInputSourceSwitch(); + // Switch to the previously used input source (or the next one, if there isn't one yet) + void toggleInputSource(); + // Keep track of the most-recently-used pair of input sources + void startObservingInputSource(); + void stopObservingInputSource(); + void onInputSourceChanged(); + // Turn the event tap back on after macOS has disabled it + void reenableEventTap(); // Handle modifiers state change (pressed/released) void handleModifierKeysStatusChange(bool shift_pressed_down, bool second_key_pressed_down); // Perpetual loop checking (every 1 sec) if we still have Accessibility permissions @@ -44,6 +51,11 @@ class KeyPressCatcher bool m_accessibility_granted = false; bool m_change_language_on_release = false; bool m_pending = false; + bool m_switch_queued = false; + bool m_observing_input_source = false; + // Identifiers ("com.apple.keylayout.ABC" and friends) of the two most recently used sources + QString m_currentSourceID; + QString m_previousSourceID; CS::SecondShortcutKeyEnum m_secondShortcutKey = CS::SecondShortcutKeyEnum::Command; }; From 6b9cb246f6fa477ba32f5a21f03e6928521c6cf4 Mon Sep 17 00:00:00 2001 From: dmitryavgustis Date: Wed, 19 Aug 2026 12:04:44 +0200 Subject: [PATCH 5/8] Keep every setting in commandShift.ini KeyPressCatcher held a default-constructed QSettings, so the "Change language with..." choice was written to the application's own domain while every other setting went to ~/.config/commandShift.ini. Two files for one app, only one of which the explicit path in main() was actually pointing at. Pass the settings object in instead, and carry the existing value across on first run so nobody's choice is reset. Also persist the choice when it is made rather than in the destructor. The app normally goes away when the user logs out or force-quits it, in which case the destructor never ran and the selection was silently lost. Co-Authored-By: Claude Opus 5 --- src/keypresscatcher.cpp | 35 ++++++++++++++++++++++++++++++----- src/keypresscatcher.h | 8 ++++++-- src/main.cpp | 2 +- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/keypresscatcher.cpp b/src/keypresscatcher.cpp index 8b8ec31..44f5976 100644 --- a/src/keypresscatcher.cpp +++ b/src/keypresscatcher.cpp @@ -156,9 +156,13 @@ namespace } } -KeyPressCatcher::KeyPressCatcher(std::function showMessageCallback) +KeyPressCatcher::KeyPressCatcher(QSettings& settings, + std::function showMessageCallback) : m_showMessageCallback{showMessageCallback} -{ +, m_settings{settings} +{ + migrateSecondShortcutKeyFromLegacySettings(); + auto secondShortcutKeyQVariant = m_settings.value(CS::secondShortcutKeySettingKeyword); if (!secondShortcutKeyQVariant.isNull()) { @@ -189,9 +193,6 @@ KeyPressCatcher::KeyPressCatcher(std::function showMessageCallback); + KeyPressCatcher(QSettings& settings, + std::function showMessageCallback); ~KeyPressCatcher(); void setSecondShortcutKey(CS::SecondShortcutKeyEnum keyValue); @@ -25,6 +26,9 @@ class KeyPressCatcher private: bool init(); + // Move the shortcut preference out of the location older versions wrote it to + void migrateSecondShortcutKeyFromLegacySettings(); + // Notify user that we've started successfully void notifyAboutSuccessfulStart(); // Notify user that we lost 'Privileges' (i.e. removed from Accessibility) @@ -46,7 +50,7 @@ class KeyPressCatcher std::function m_showMessageCallback; __CFMachPort* m_eventTapPtr = nullptr; - QSettings m_settings; + QSettings& m_settings; bool m_successfully_started = false; bool m_accessibility_granted = false; bool m_change_language_on_release = false; diff --git a/src/main.cpp b/src/main.cpp index 364291f..9c1c44f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -50,7 +50,7 @@ int main(int argc, char *argv[]) 30000); }; - KeyPressCatcher catcher(showMessageCallback); + KeyPressCatcher catcher(*settings, showMessageCallback); auto authorAction = menu.addAction("Visit CommandShift page"); QObject::connect(authorAction, &QAction::triggered, [] { QDesktopServices::openUrl(QUrl("https://github.com/VasylBaran/CommandShift")); }); From 0e43717f22765cede16e881f7027f7bbfa5deeeb Mon Sep 17 00:00:00 2001 From: dmitryavgustis Date: Wed, 19 Aug 2026 12:05:12 +0200 Subject: [PATCH 6/8] Update the FAQ for direct input source switching Two answers described the old behaviour and are now actively misleading: * The troubleshooting entry told users to press "Restore Defaults" in Keyboard -> Shortcuts -> Input Sources, which is precisely the dependency that was removed. Point at the things that can actually still be wrong instead, and add an entry saying Ctrl+Space is now free to rebind. * The 3+ languages entry described holding the second key and tapping Shift to cycle. Switching now returns to the previously used source, so describe that. Its Q and A labels were also the wrong way round. Co-Authored-By: Claude Opus 5 --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9c7b429..cf07e64 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ After a while a friend of mine bought his first Macbook and asked me "How can I CommandShift is a free and open-source app that allows you to change input source using Windows-style shortcut (e.g. Command + Shift, Option + Shift, Control + Shift, Fn + Shift or even just Shift. It's quite customizable). By default MacOS doesn't support shortcuts that consist of modifier keys only. CommandShift solves this problem. +It changes the input source directly rather than by simulating macOS' own shortcut, so it does not require Ctrl+Space (or any other system shortcut) to stay bound, and the switch happens immediately. + # Support I'm a single software engineer from Ukraine. If you found CommandShift useful and would like to say 'thank you' please consider supporting me on [Patreon](https://www.patreon.com/Vasyl_Baran) or [PayPal](https://www.paypal.com/donate/?hosted_button_id=WZAJV3PYPWUHA). It encourages me to further improve, support and update CommandShift as new versions of MacOS come out. @@ -21,15 +23,17 @@ I'm a single software engineer from Ukraine. If you found CommandShift useful an # FAQ: * **Q:** I've added CommandShift to "Security & Privacy -> Privacy -> Accessibility" but it doesn't work =( -* **A:** Please make sure that your settings in "Keyboard -> Shortcuts -> Input Sources" are set to default (you can easily do that by pressing "Restore Defaults" button there) +* **A:** Check that you have at least two input sources enabled in "Keyboard -> Text Input -> Input Sources", and that the switch next to CommandShift is actually turned on (adding the app to the list is not enough). If you have just replaced the app with a newer version, remove the old entry with the "-" button and add it again. +* **Q:** Do I need to keep macOS' own "Select the previous input source" shortcut (Ctrl+Space) bound? +* **A:** No. CommandShift changes the input source directly, so you are free to unbind Ctrl+Space in "Keyboard -> Keyboard Shortcuts -> Input Sources" and use it for something else. Older versions did depend on it. * **Q:** How do I choose which shortcut should switch language? * **A:** Find CommandShift ('CS' icon) in menu bar at the top-right and click on it, there you'll find "Change language with..." drop-down. Select the one you prefer =) * **Q:** I get "CommandShift is damaged and can't be opened" error * **A:** Please run this command in Terminal: _xattr -cr /Applications/CommandShift.app_ * **Q:** How to automatically launch CommandShift when computer starts? * **A:** You can add CommandShift to your list of auto-start items by opening System Settings panel -> Users and Groups OR General (depending on what version of macOS you're using) -> Login Items -> [+] -> [select CommandShift app] -* **A:** I use 3+ languages and CommandShift switches only between the last 2. How to make it cycle between all 3+ languages? -* **Q:** Press and hold second key (the one you chose from the "Change language with..." drop-down) the then press Shift multiple times in order to cycle through your languages. +* **Q:** I use 3+ languages. Which one does CommandShift switch to? +* **A:** The one you used before the current one, the same way Alt+Tab returns you to your last window. To reach a third language, pick it once from the input menu in the menu bar; CommandShift will then switch between that one and whichever you came from. * **Q:** I want to switch language after releasing Shift key instead of when pressing it * **A:** From CommandShift's menu bar at the top-right you can toggle 'Change language after Shift release' * **Q:** I want to switch language by just pressing Shift key alone From 1cbcab32e8a537a5f273056383fb5ba5d7ea0225 Mon Sep 17 00:00:00 2001 From: dmitryavgustis Date: Wed, 19 Aug 2026 12:22:58 +0200 Subject: [PATCH 7/8] Don't change language when the modifiers are part of another shortcut Watching only kCGEventFlagsChanged meant the language changed the moment Cmd and Shift were both down, so every Cmd+Shift+A also switched the layout. In "Shift" mode it was worse still: typing any capital letter switched. Watch key, mouse and scroll events too, and treat the combo as consumed if anything else happens while the modifiers are held. Only a press of the modifiers on their own now changes the language. This necessarily makes the switch happen on release: until the keys are let go there is no way to tell a language switch from the start of another shortcut. That was already available as "Change language after Shift release", which is now the only behaviour, so the menu item and its setting are removed. The tap becomes listen-only. It sees every keystroke now, and since nothing is ever modified or swallowed there is no reason to sit in the input latency path. Co-Authored-By: Claude Opus 5 --- README.md | 8 ++--- src/keypresscatcher.cpp | 70 ++++++++++++++++++++++++++--------------- src/keypresscatcher.h | 11 ++++--- src/main.cpp | 9 ------ 4 files changed, 54 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index cf07e64..040cc92 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ After a while a friend of mine bought his first Macbook and asked me "How can I CommandShift is a free and open-source app that allows you to change input source using Windows-style shortcut (e.g. Command + Shift, Option + Shift, Control + Shift, Fn + Shift or even just Shift. It's quite customizable). By default MacOS doesn't support shortcuts that consist of modifier keys only. CommandShift solves this problem. -It changes the input source directly rather than by simulating macOS' own shortcut, so it does not require Ctrl+Space (or any other system shortcut) to stay bound, and the switch happens immediately. +It changes the input source directly rather than by simulating macOS' own shortcut, so it does not require Ctrl+Space (or any other system shortcut) to stay bound. The language changes only when the shortcut's modifiers are pressed and released on their own, so ordinary shortcuts like Cmd+Shift+A keep working as they should. # Support I'm a single software engineer from Ukraine. If you found CommandShift useful and would like to say 'thank you' please consider supporting me on [Patreon](https://www.patreon.com/Vasyl_Baran) or [PayPal](https://www.paypal.com/donate/?hosted_button_id=WZAJV3PYPWUHA). It encourages me to further improve, support and update CommandShift as new versions of MacOS come out. @@ -34,10 +34,10 @@ I'm a single software engineer from Ukraine. If you found CommandShift useful an * **A:** You can add CommandShift to your list of auto-start items by opening System Settings panel -> Users and Groups OR General (depending on what version of macOS you're using) -> Login Items -> [+] -> [select CommandShift app] * **Q:** I use 3+ languages. Which one does CommandShift switch to? * **A:** The one you used before the current one, the same way Alt+Tab returns you to your last window. To reach a third language, pick it once from the input menu in the menu bar; CommandShift will then switch between that one and whichever you came from. -* **Q:** I want to switch language after releasing Shift key instead of when pressing it -* **A:** From CommandShift's menu bar at the top-right you can toggle 'Change language after Shift release' +* **Q:** Does CommandShift interfere with shortcuts like Cmd+Shift+A? +* **A:** No. If you press any other key, or click or scroll, while holding the modifiers, CommandShift leaves that shortcut alone and does not change the language. This is also why the language changes when you release the keys rather than when you press them: until you let go, there is no way to tell a language switch from the start of another shortcut. * **Q:** I want to switch language by just pressing Shift key alone -* **A:** From CommandShift's menu bar go to "Change language with..." drop-down and select "Shift" +* **A:** From CommandShift's menu bar go to "Change language with..." drop-down and select "Shift". Typing capital letters will not switch anything, since Shift is then being held together with another key. * **Q:** I want to hide CommandShift icon from menu tray * **A:** From CommandShift's menu bar select "Hide icon from tray menu..." and choose whether you want to hide it "Permanently" or "Until restart" * **Q:** I have Arm/Intel based Mac. Is CommandShift going to work on both? diff --git a/src/keypresscatcher.cpp b/src/keypresscatcher.cpp index 44f5976..f0c355c 100644 --- a/src/keypresscatcher.cpp +++ b/src/keypresscatcher.cpp @@ -249,16 +249,6 @@ void KeyPressCatcher::notifyAboutSuccessfulStart() CS::allGoodMessage); } -void KeyPressCatcher::setChangeLanguageOnRelease(bool change_language_on_release) -{ - m_change_language_on_release = change_language_on_release; -} - -bool KeyPressCatcher::changeLanguageOnRelease() const -{ - return m_change_language_on_release; -} - void KeyPressCatcher::loop() { if (!AXIsProcessTrusted()) @@ -385,34 +375,56 @@ void KeyPressCatcher::reenableEventTap() } } +void KeyPressCatcher::noteUnrelatedInput() +{ + if (m_combo_armed) + { + m_combo_used_with_other_input = true; + } +} + void KeyPressCatcher::handleModifierKeysStatusChange(bool shift_pressed_down, bool second_key_pressed_down) { - if (m_change_language_on_release) + if (shift_pressed_down && second_key_pressed_down) { - if (shift_pressed_down && second_key_pressed_down) + if (!m_combo_armed) { - m_pending = true; - } - else if (!shift_pressed_down && m_pending) - { - requestInputSourceSwitch(); - m_pending = false; + m_combo_armed = true; + m_combo_used_with_other_input = false; } + return; } - else + + if (!m_combo_armed) { - if (shift_pressed_down && second_key_pressed_down) - { - requestInputSourceSwitch(); - } + return; + } + + m_combo_armed = false; + + // Whether the combo was a request to change language can only be known once it is let + // go of: Cmd+Shift+A is somebody using a shortcut, and Shift+H is somebody typing a + // capital letter. Only a press of the modifiers on their own means a language change. + if (!m_combo_used_with_other_input) + { + requestInputSourceSwitch(); } } bool KeyPressCatcher::init() { - CGEventMask modifiersPressedMask = CGEventMaskBit(kCGEventFlagsChanged); - - m_eventTapPtr = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, modifiersPressedMask, + // Key and mouse events are watched purely to notice that the modifiers were held as part + // of some other shortcut, so that we can leave that shortcut alone. + CGEventMask eventMask = CGEventMaskBit(kCGEventFlagsChanged) + | CGEventMaskBit(kCGEventKeyDown) + | CGEventMaskBit(kCGEventLeftMouseDown) + | CGEventMaskBit(kCGEventRightMouseDown) + | CGEventMaskBit(kCGEventOtherMouseDown) + | CGEventMaskBit(kCGEventScrollWheel); + + // Listen-only, because we never modify or swallow anything. It also keeps the app out of + // the input latency path, which matters now that every keystroke passes through here. + m_eventTapPtr = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly, eventMask, [] (CGEventTapProxy, CGEventType type, CGEventRef event, void *keyPressCatcherRawPtr) { auto catcher = static_cast(keyPressCatcherRawPtr); @@ -425,6 +437,12 @@ bool KeyPressCatcher::init() return event; } + if (type != kCGEventFlagsChanged) + { + catcher->noteUnrelatedInput(); + return event; + } + CGEventFlags flags = CGEventGetFlags(event); auto secondTriggerKey = catcher->getSecondShortcutKey(); // Checking whether a second key that we expected (depending on configuration) was pressed diff --git a/src/keypresscatcher.h b/src/keypresscatcher.h index 539ac2f..a1cdb12 100644 --- a/src/keypresscatcher.h +++ b/src/keypresscatcher.h @@ -20,9 +20,6 @@ class KeyPressCatcher void setSecondShortcutKey(CS::SecondShortcutKeyEnum keyValue); CS::SecondShortcutKeyEnum getSecondShortcutKey() const; - void setChangeLanguageOnRelease(bool change_language_on_release); - bool changeLanguageOnRelease() const; - private: bool init(); @@ -45,6 +42,8 @@ class KeyPressCatcher void reenableEventTap(); // Handle modifiers state change (pressed/released) void handleModifierKeysStatusChange(bool shift_pressed_down, bool second_key_pressed_down); + // Note that something other than the modifiers themselves was pressed + void noteUnrelatedInput(); // Perpetual loop checking (every 1 sec) if we still have Accessibility permissions void loop(); @@ -53,8 +52,10 @@ class KeyPressCatcher QSettings& m_settings; bool m_successfully_started = false; bool m_accessibility_granted = false; - bool m_change_language_on_release = false; - bool m_pending = false; + // The shortcut's modifiers are currently held down together + bool m_combo_armed = false; + // ...and something else was pressed while they were, so this is a real shortcut + bool m_combo_used_with_other_input = false; bool m_switch_queued = false; bool m_observing_input_source = false; // Identifiers ("com.apple.keylayout.ABC" and friends) of the two most recently used sources diff --git a/src/main.cpp b/src/main.cpp index 9c1c44f..f98c9a7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,7 +17,6 @@ namespace { constexpr static auto s_hide_tray_icon_setting_name = "tray_icon/hide"; - constexpr static auto s_change_language_on_release_setting_name = "language/trigger_on_key_release"; constexpr static auto s_white_tray_icon_preference_setting_name = "tray_icon/white_tray_icon"; } @@ -110,14 +109,6 @@ int main(int argc, char *argv[]) secondShortcutKeyDropDownActionMenu->addAction(commandAction); secondShortcutKeyDropDownActionMenu->addAction(shiftAction); - auto change_language_on_release = settings->value(s_change_language_on_release_setting_name, false).toBool(); - catcher.setChangeLanguageOnRelease(change_language_on_release); - - auto changeLanguageOnKeyReleasedAction = menu.addAction("Change language after Shift release"); - changeLanguageOnKeyReleasedAction->setCheckable(true); - changeLanguageOnKeyReleasedAction->setChecked(change_language_on_release); - QObject::connect(changeLanguageOnKeyReleasedAction, &QAction::triggered, changeLanguageOnKeyReleasedAction, [&catcher, settings] { catcher.setChangeLanguageOnRelease(!catcher.changeLanguageOnRelease()); settings->setValue(s_change_language_on_release_setting_name, catcher.changeLanguageOnRelease()); }); - auto trayIconGroup = new QActionGroup(&menu); trayIconGroup->setExclusive(false); From 5257a98213eb8c7ddc2797d0254b8548e693b1c4 Mon Sep 17 00:00:00 2001 From: dmitryavgustis Date: Wed, 19 Aug 2026 12:27:59 +0200 Subject: [PATCH 8/8] Rewrite README as a fork of Vasyl Baran's CommandShift Make the relationship to the original project explicit, credit Vasyl Baran as its author, and list what this fork actually changes so the difference from his releases is clear. Drop the sections that only make sense coming from the original author: the first-person backstory, the donation links and funding goal, and the banner. Point at his repository and releases instead. The GPL-3.0 license and the attribution in the app itself are unchanged. Also drop the download mirrors and the checksum, which describe his binaries rather than anything built from this tree, and the claim of a universal build, which is not true of the Homebrew Qt build documented here. Co-Authored-By: Claude Opus 5 --- README.md | 113 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 63 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 040cc92..39ee126 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,72 @@ -[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://vshymanskyy.github.io/StandWithUkraine/) +# CommandShift -# Backstory -Initially I wrote this program for myself since I bought my first Macbook and quickly realized that I can't use 'Alt'+Shift to change language on macOS. -After a while a friend of mine bought his first Macbook and asked me "How can I change language using 'Alt'+Shift? I can't seem to change it in system preferences". I shared my app with him and at that point I realized that other people might find it useful as well so decided to share this app with the world. +CommandShift is a free and open-source macOS app that lets you change input source +with a Windows-style shortcut made of modifier keys only β€” Command + Shift, +Option + Shift, Control + Shift, Fn + Shift, or even Shift on its own. macOS does not +support modifier-only shortcuts by itself; CommandShift adds them. -# CommandShift -CommandShift is a free and open-source app that allows you to change input source using Windows-style shortcut (e.g. Command + Shift, Option + Shift, Control + Shift, Fn + Shift or even just Shift. It's quite customizable). -By default MacOS doesn't support shortcuts that consist of modifier keys only. CommandShift solves this problem. +## About this fork -It changes the input source directly rather than by simulating macOS' own shortcut, so it does not require Ctrl+Space (or any other system shortcut) to stay bound. The language changes only when the shortcut's modifiers are pressed and released on their own, so ordinary shortcuts like Cmd+Shift+A keep working as they should. +The original CommandShift was written by **Vasyl Baran** and lives at +[VasylBaran/CommandShift](https://github.com/VasylBaran/CommandShift). All the credit +for the application belongs to him β€” this is a fork that builds on his work, kept for +personal use, and it remains under the same GPL-3.0 license. -# Support -I'm a single software engineer from Ukraine. If you found CommandShift useful and would like to say 'thank you' please consider supporting me on [Patreon](https://www.patreon.com/Vasyl_Baran) or [PayPal](https://www.paypal.com/donate/?hosted_button_id=WZAJV3PYPWUHA). It encourages me to further improve, support and update CommandShift as new versions of MacOS come out. +What this fork changes: -**My current goal is $99 which is a membership fee for Apple Developer program** so that I can propertly sign and notarize CommandShift using Apple developer certificate. -# How-to use -1. Download CommandShift-universal.dmg -2. Drag'n'drop CommandShift application from dmg-file to your Applications folder -3. Add CommandShift to "Security & Privacy -> Privacy -> Accessibility" in order for it to work properly -4. Enjoy! -5. (optionally) Add CommandShift to your startup items ("Users & Groups -> Login Items") +* **No dependency on macOS' own input-source shortcut.** The original simulated a + Ctrl+Space keystroke and relied on that binding still being in place, which meant + giving up Ctrl+Space β€” the autocomplete shortcut in most IDEs. This fork asks Text + Input Services to change the source directly, so no system shortcut needs to stay + bound, and switching is immediate rather than going through the system's hotkey + machinery. +* **Other shortcuts are left alone.** The language only changes when the shortcut's + modifiers are pressed and released on their own. Press another key, click or scroll + while holding them and CommandShift stays out of the way, so Cmd+Shift+A still does + what the app under it expects. In Shift-only mode this also means typing capital + letters no longer switches the layout. +* **Switching returns to the previously used input source**, the way Alt+Tab returns + you to your last window, including when you changed source from the menu bar. +* **One settings file.** Every preference now lives in `~/.config/commandShift.ini`. +* **A build script**, so the app can be built, signed and installed in one command. -# FAQ: -* **Q:** I've added CommandShift to "Security & Privacy -> Privacy -> Accessibility" but it doesn't work =( -* **A:** Check that you have at least two input sources enabled in "Keyboard -> Text Input -> Input Sources", and that the switch next to CommandShift is actually turned on (adding the app to the list is not enough). If you have just replaced the app with a newer version, remove the old entry with the "-" button and add it again. -* **Q:** Do I need to keep macOS' own "Select the previous input source" shortcut (Ctrl+Space) bound? -* **A:** No. CommandShift changes the input source directly, so you are free to unbind Ctrl+Space in "Keyboard -> Keyboard Shortcuts -> Input Sources" and use it for something else. Older versions did depend on it. -* **Q:** How do I choose which shortcut should switch language? -* **A:** Find CommandShift ('CS' icon) in menu bar at the top-right and click on it, there you'll find "Change language with..." drop-down. Select the one you prefer =) -* **Q:** I get "CommandShift is damaged and can't be opened" error -* **A:** Please run this command in Terminal: _xattr -cr /Applications/CommandShift.app_ -* **Q:** How to automatically launch CommandShift when computer starts? -* **A:** You can add CommandShift to your list of auto-start items by opening System Settings panel -> Users and Groups OR General (depending on what version of macOS you're using) -> Login Items -> [+] -> [select CommandShift app] -* **Q:** I use 3+ languages. Which one does CommandShift switch to? -* **A:** The one you used before the current one, the same way Alt+Tab returns you to your last window. To reach a third language, pick it once from the input menu in the menu bar; CommandShift will then switch between that one and whichever you came from. -* **Q:** Does CommandShift interfere with shortcuts like Cmd+Shift+A? -* **A:** No. If you press any other key, or click or scroll, while holding the modifiers, CommandShift leaves that shortcut alone and does not change the language. This is also why the language changes when you release the keys rather than when you press them: until you let go, there is no way to tell a language switch from the start of another shortcut. -* **Q:** I want to switch language by just pressing Shift key alone -* **A:** From CommandShift's menu bar go to "Change language with..." drop-down and select "Shift". Typing capital letters will not switch anything, since Shift is then being held together with another key. -* **Q:** I want to hide CommandShift icon from menu tray -* **A:** From CommandShift's menu bar select "Hide icon from tray menu..." and choose whether you want to hide it "Permanently" or "Until restart" -* **Q:** I have Arm/Intel based Mac. Is CommandShift going to work on both? -* **A:** Yes, CommandShift is a universal app and runs natively on both Arm and Intel Macs. +Because switching now has to distinguish a language change from the start of another +shortcut, it happens when the keys are released rather than when they are pressed. -# Downloads -* Bitbucket: https://bitbucket.org/vasylbaran7/downloads/downloads/ -* Sourceforge: https://sourceforge.net/projects/commandshift/files/ -* GitHub: https://github.com/VasylBaran/CommandShift/releases/ +## Installing -**Latest CommandShift checksum (MD5)** -* CommandShift-universal.dmg: c2fced57a7b755f237a1327941b5049e +This fork has no prebuilt releases β€” build it from source with the instructions below. +Vasyl's original builds are available from +[his releases page](https://github.com/VasylBaran/CommandShift/releases/), but they +predate everything listed above. -Again, if you have found CommandShift useful please consider supporting my endeavors. It encourages me to further improve, support and update CommandShift as new versions of MacOS come out: -* **Buy Me a Coffee (Stripe) (one-time):** https://buymeacoffee.com/vasylbaran -* **Patreon (monthly):** https://www.patreon.com/Vasyl_Baran +Once installed: -And remember, stay Safe and stay Strong! πŸ‡ΊπŸ‡¦ +1. Add CommandShift to "Privacy & Security -> Accessibility" so it can see key presses. +2. (optionally) Add CommandShift to your login items ("General -> Login Items"). -# Building from source (macOS) +## FAQ + +* **Q:** I've added CommandShift to "Privacy & Security -> Accessibility" but it doesn't work =( +* **A:** Check that you have at least two input sources enabled in "Keyboard -> Text Input -> Input Sources", and that the switch next to CommandShift is actually turned on (adding the app to the list is not enough). If you have just replaced the app with a newer version, remove the old entry with the "-" button and add it again. +* **Q:** Do I need to keep macOS' own "Select the previous input source" shortcut (Ctrl+Space) bound? +* **A:** No. CommandShift changes the input source directly, so you are free to unbind Ctrl+Space in "Keyboard -> Keyboard Shortcuts -> Input Sources" and use it for something else. +* **Q:** Does CommandShift interfere with shortcuts like Cmd+Shift+A? +* **A:** No. If you press any other key, or click or scroll, while holding the modifiers, CommandShift leaves that shortcut alone and does not change the language. This is also why the language changes when you release the keys rather than when you press them: until you let go, there is no way to tell a language switch from the start of another shortcut. +* **Q:** How do I choose which shortcut should switch language? +* **A:** Find CommandShift ('CS' icon) in the menu bar at the top-right and click on it, there you'll find a "Change language with..." drop-down. Select the one you prefer. +* **Q:** I use 3+ languages. Which one does CommandShift switch to? +* **A:** The one you used before the current one, the same way Alt+Tab returns you to your last window. To reach a third language, pick it once from the input menu in the menu bar; CommandShift will then switch between that one and whichever you came from. +* **Q:** I want to switch language by just pressing the Shift key alone +* **A:** From CommandShift's menu bar go to the "Change language with..." drop-down and select "Shift". Typing capital letters will not switch anything, since Shift is then being held together with another key. +* **Q:** I want to hide the CommandShift icon from the menu bar +* **A:** From CommandShift's menu select "Tray menu icon..." and choose whether to hide it "permanently" or "until restart". +* **Q:** How do I automatically launch CommandShift when the computer starts? +* **A:** Add it to your login items: System Settings -> General -> Login Items -> [+] -> [select CommandShift]. +* **Q:** I get a "CommandShift is damaged and can't be opened" error +* **A:** Run this in Terminal: `xattr -cr /Applications/CommandShift.app` + +## Building from source (macOS) ### One-time setup @@ -104,4 +113,8 @@ And remember, stay Safe and stay Strong! πŸ‡ΊπŸ‡¦ Override the defaults with `QT_PREFIX` and `CS_SIGN_IDENTITY` if needed. Note: a Homebrew Qt build is arm64-only and targets macOS 14+. Use the official Qt -installer if you need a universal binary or a lower deployment target for distribution. \ No newline at end of file +installer if you need a universal binary or a lower deployment target. + +## License + +GPL-3.0, inherited from the original project. See [LICENSE](LICENSE).