Technical Stuff
Last updated
Last updated
Keyboard Switch is written using and . Following is the list of technologies used in this app and some other technical aspects.
The Keyboard Switch Settings app is written using , a cross-platform UI framework for .NET. I chose it because unlike other UI frameworks for .NET, it's cross-platform, and I had to do literally nothing for it to work on macOS and Linux as well. The style is provided by .
Previous versions of the app used .NET Framework and WPF. Back then I didn't expect to have it working on different platforms.
Previous versions of the app also included a tray icon so you could see that the app is running. I removed it in version 3.0 as I don't see much value in keeping it and cluttering your tray.
The service app is just an app which runs in the background. I could have made it into a Windows service but decided against it when I read somewhere that Windows services cannot set up keyboard hooks. I didn't actually verify this info, so I'm not sure it's correct, but the background app approach works just fine, so it's likely to stay this way.
The service app calls various native functions from the Windows' user32.dll. It uses to call them.
In the previous versions (up to 3.0) the service and the settings app were not separated. It was just an app running with a hidden window which showed up when you opened it. Having a hidden window always loaded is not that great of an idea, even though it used very little RAM.
On macOS the service app runs as a launchd
service. launchd
provides the ability to start at login. The settings app starts the service app through launchd
as well.
macOS 10.15+ is required since it's the oldest version still supported by .NET.
The service app uses multiple native macOS frameworks:
CoreFoundation - for low-level string, array, and pointer manipulation.
CarbonCore (part of CoreFoundation) - for translating key codes with layout info into Unicode characters.
CoreGraphics - for simulating keyboard events.
HIToolbox - for working with keyboard layouts.
AppKit - for working with the clipboard.
At first, I decided to run the app as a systemd
service, but it proved not to provide much value, so I've since reverted this decision.
The service app uses X11, especially the X Keyboard Extension, and the X Test Extension. Currently it doesn't work on Wayland, even through XWayland. It calls various native Xlib functions using P/Invoke directly.
If the Linux desktop environment is GNOME, then the app switches layouts a little differently than in other desktop environments. GNOME doesn't let apps switch layouts using X11 directly - it will immediately switch it back. Instead, the settings app adds a small GNOME Shell extension (which is simply called Switch Layout) and the app switches layouts through it. GNOME should be made aware of this extension after installation; hence you should restart it after starting the settings app. If you don't then the app will still work but won't be able to switch layouts until you log out or reboot.
Keyboard Switch and Keyboard Switch Settings are published as self-contained single-file applications. The size difference between single-file applications and standard applications with shared libraries is negligible since single-file applications can be compressed.
On macOS the two applications are located in separate directories - this is done because on macOS every application should be contained inside a bundle.
Previously the app used a Windows keyboard hook directly, but that doesn't work on other systems.
Preferences are stored in the user's local app data folder (under the Keyboard Switch directory) on Windows. I didn't put it into the normal app data folder because that one is shared if you use the same Windows account on multiple machines, and that kind of defeats the purpose of this app being configured for each machine individually.
On macOS the settings are stored in the ~/Library/Application Support/Keyboard Switch directory.
On Linux the settings are stored in the ~/.config/keyboard-switch directory.
This app stores settings as plain JSON files.
On Windows and Linux, the log file is stored in the same folder as the preferences, and on macOS it's stored in the ~/Library/Logs/Keyboard Switch directory. The log file is rolled over after reaching 10 MB.
If you really want to (and know how), you can change the logging configuration in the appsettings.json file. On Windows and Linux this file is located in the same folder as the app itself. On macOS this file is located in the bundle's resources folder, and you probably shouldn't change it as it may break the bundle's signature (though I'm not sure about that).
There are very few tests and only for the core functionality. Honestly, I don't even know how to test the rest, because the rest of the app is essentially creating system-wide side effects, so don't think it's really testable. As for the settings app, well, I could test it, but that would take a lot of time, and the app is pretty simple, so I don't plan on doing it in the nearest future.
WiX Toolset 5.0 is used to create the Windows installer. Previously WixSharp was used, but the installer is really simple, so there is no need for it. Also, WixSharp requires .NET Framework, while WiX 4.0+ doesn't require it anymore.
The installer package for macOS is created using the default macOS tools.
The deb and RPM installers for Linux are also created using the default CLI tools.
You can build Keyboard Switch from source if you like. All projects require .NET 8.
The app consists of 10 projects grouped into 3 folders:
src:
KeyboardSwitch: The Keyboard Switch service itself
KeyboardSwitch.Core: Core functionality and interfaces for all projects
KeyboardSwitch.Settings: The Keyboard Switch Settings app
KeyboardSwitch.Settings.Core: The core logic of the settings app
KeyboardSwitch.Windows: The implementation of the core functionality for the Windows platform
KeyboardSwitch.MacOS: The implementation of the core functionality for the macOS platform
KeyboardSwitch.Linux: The implementation of the core functionality for the Linux platform
test:
KeyboardSwitch.Test: The unit test project
build:
KeyboardSwitch.Build: The NUKE project for building Keyboard Switch
KeyboardSwitch.Windows.Installer: The WiX installer project
The following targets are available (the default target is Compile
):
Restore
- runs dotnet restore
for all projects.
Clean
- runs dotnet clean
for all projects.
Compile
- runs dotnet build
for all projects.
Test
- runs dotnet test
for the test project.
Publish
- runs dotnet publish
for the KeyboardSwitch and KeyboardSwitch.Settings projects.
CreateZipArchive
- creates a zip archive which contains a portable distribution of Keyboard Switch.
CreateTarArchive
- creates a tar.gz archive which contains a portable distribution of Keyboard Switch.
CreateWindowsInstaller
- creates a Windows installer.
PrepareMacOSPackage
- prepares all files required for a macOS package.
CreateMacOSPackage
- creates a macOS package.
PrepareMacOSUninstallerPackage
- prepares all files required for a macOS uninstaller package.
CreateMacOSUninstallerPackage
- creates a macOS uninstaller package.
PrepareDebianPackage
- prepares all files required for a deb package.
CreateDebianPackage
- creates a deb package.
PrepareRpmPackage
- prepares all files required for an RPM package.
CreateRpmPackage
- creates an RPM package.
The Windows installer can be created on any OS. The macOS packages can only be created on macOS and require an Apple developer account and certificates. The deb and RPM packages can only be created on Linux.
The following parameters are available:
Configuration
- Debug
or Release
. All targets except Restore
, Clean
, and Compile
and Test
require Release
which is the default.
TargetOS
- Windows
, macOS
, or Linux
. The currently running OS is the default. Windows installer requires Windows
. macOS packages require macOS
. Linux packages require Linux
.
Platform
- x64
or arm64
. The currently running platform is the default.
PublishSingleFile
- true
or false
. All targets after Publish
require true
.
OutputFileSuffix
- the suffix to use on output files before the extension. For example, portable Windows distributions use the win
suffix.
The following parameters are available for building macOS packages:
AppleId
- your Apple developer ID.
AppleTeamId
- your Apple team ID.
AppleApplicationCertificate
- the ID of your Apple application certificate.
AppleInstallerCertificate
- the ID of your Apple installer certificate.
NotaryToolKeychainProfile
- the Keychain profile to be used by notarytool
when notarizing the package.
There are more parameters available for macOS, but they are only used when running in GitHub Actions. Certificates must be stored in Keychain for macOS packages to be built.
Here is the NUKE execution plan which shows the dependencies between targets:
The app interacts directly with X11 for clipboard integration - the code for this integration is based on code from Avalonia. It can also use for clipboard integration instead.
The core logic of the settings app is implemented using as the MVVM framework. Also, is used for service location.
The app uses to create a global keyboard hook. The library is cross-platform and supports almost all popular OSes and architectures. Since this library is native and it's non-trivial to build, I've developed a .NET wrapper for it, , so that I don't have to deal with a native library in this project.
This app uses a plain-text log file to log the stuff it’s doing and errors it encounters along the way. is used as the logging library.
The docs are built and hosted on .
Previously these articles were built using and hosted on , and used the .
Keyboard Switch uses as its build system. It's not required to build Keyboard Switch, but it greatly simplifies creating the target artifacts. You should call the ./build.cmd
script from the root folder to run NUKE (the script is cross-platform).