Technical Stuff
Keyboard Switch is written using C# and .NET 8. Following is the list of technologies used in this app and some other technical aspects.
UI
The Keyboard Switch Settings app is written using Avalonia, 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 FluentAvalonia.
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.
Core Logic
Windows
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 Vanara 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.
macOS
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.
Linux
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.
The app interacts directly with X11 for clipboard integration - the code for this integration is based on code from Avalonia. It can also use xsel for clipboard integration instead.
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.
The Settings App
The core logic of the settings app is implemented using ReactiveUI as the MVVM framework. Also, Splat is used for service location.
App Structure
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.
The Global Keyboard Hook
The app uses libuiohook 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, SharpHook, so that I don't have to deal with a native library in this project.
Previously the app used a Windows keyboard hook directly, but that doesn't work on other systems.
Preferences
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.
Logging
This app uses a plain-text log file to log the stuff it’s doing and errors it encounters along the way. Serilog is used as the logging library.
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).
Tests
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.
Docs
The docs are built and hosted on GitBook.
Previously these articles were built using Jekyll and hosted on GitHub Pages, and used the Minimal Mistakes theme.
Installers
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.
Building from Source
You can build Keyboard Switch from source if you like. All projects require .NET 8.
The Projects
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
Building the App Itself
Keyboard Switch uses NUKE 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).
The following targets are available (the default target is Compile
):
Restore
- runsdotnet restore
for all projects.Clean
- runsdotnet clean
for all projects.Compile
- runsdotnet build
for all projects.Test
- runsdotnet test
for the test project.Publish
- runsdotnet 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
orRelease
. All targets exceptRestore
,Clean
, andCompile
andTest
requireRelease
which is the default.TargetOS
-Windows
,macOS
, orLinux
. The currently running OS is the default. Windows installer requiresWindows
. macOS packages requiremacOS
. Linux packages requireLinux
.Platform
-x64
orarm64
. The currently running platform is the default.PublishSingleFile
-true
orfalse
. All targets afterPublish
requiretrue
.OutputFileSuffix
- the suffix to use on output files before the extension. For example, portable Windows distributions use thewin
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 bynotarytool
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:
Last updated