Make a global hotkey on Windows to insert the current date via Syscalls

Alexey Stern
6 min readSep 18, 2020

--

One very common task for any sort of automation is Keyboard in- and output. On Windows, great scripting tools like AutoHotkey already exist for this.

But what if you want to do something which either needs very complex logic or feels too simple to use a general-purpose tool with its own domain-specific language? Why not try to program something on your own to get to know how these apps do it?

Enter this tutorial. Here we are going to use Go as our implementation language. Mainly for two reasons:

  • Go can be easily compiled into an .exe file
  • Go has an outstanding time package included

However, you can easily transfer the ideas used to any language like Python, NodeJs, or C++/C#.

The example task

Our app is going to:

  1. register a global hotkey (I chose CTRL+ALT+D)
  2. write the current date in ISO8601 format when you press this global hotkey, e.g. 2020–01–20

This is useful when you want to keep a daily journal or have to frequently insert the date

We are going to accomplish this using three Windows Syscalls

all three are available via the user32.dll / winuser interface

About Syscalls

A system call (syscall) is a mechanism that provides the interface between a process and the operating system. By default, the operating system is isolated from the user processes in order to ensure both security and stability. Syscalls are the exception to that rule.

A high-level view of syscalls independent of OS. Applications use syscalls to enter kernel space

Using syscalls applications can do things like reading files or keyboard input. Any print command does a syscall.

However, most syscalls are abstracted away from the programmer by the programming language. This abstraction is very important to ensure cross-system compatibility. For example, in Go, the os package offers a way to open a file for reading via

os.Open("file.go")

Under the hood, this will use the appropriate syscall depending on the operating system.

Yet, some things are hard to abstract away. For example, the way that the clipboard is managed is vastly different from OSX to Linux to Windows. In these cases, no abstraction exists, and the programmer has to do the syscalls on their own. This sacrifices cross-platform compatibility so much that it usually should be made evident during compilation via adding a system-dependent compile flag.

Registering Hotkeys

In our main function, we want to load user32.dll and retain a pointer to the API. There are generally two ways to load a .dll file: lazy or immediate. Lazy will not actually load the .dll until we use a syscall. I prefer immediate as it will instantly throw an error if there are any problems.

Never mind the imports for now, some of them we are going to user later. Now let’s take a loot at registerHotkeys:

registerHotkeys will try to find the RegisterHotKey procedure inside user32. Afterward, it will loop through all defined HOTKEYS in the HOTKEYS map, for each hotkey we will pass a pointer to a struct to a call of the RegisterHotKey procedure. Specifically for go, we are making use of its interoperability with C.

The important bit here is the ID variable that we pass to reghotkey (we chose 4 in this case). We will use this id to identify our hotkey later. Also note that we are passing a bitmask for the modifier keys so ALT+CTRL will be 1+2 = 3 = 11 in binary.

Listening to Hotkeys

Let’s extend our main function to listen to hotkeys via the GetMessageW procedure.

Inside the for loop, the call to getmsg will block until there is a new message received. This message is written into an MSG struct. If the newly received messages’ WPARAM is equal to the id that we had set earlier in reghotkey, we know that it is the hotkey that we had registered.

printCharacters will receive a pointer to the win32.dll (so we do not have to load it at each invocation) and the current date as an ISO8601 formatted string. The Format() function in Go accepts a wide array of format strings so feel free to customize this to your heart’s content.

Next, let’s take a look at printCharacters which will handle outputting the current date.

Outputting the current date

First, outputting the date will have us use virtual key codes for keyboard output in Windows. Virtual Key codes represent hexadecimal values representative of a certain keypress. Note that lower and uppercase letters do not have a separate keycode as they are achieved via pressing the SHIFT modifier key.

The following is an incomplete (but for our use-case sufficient) mapping of virtual codes to their hexadecimal values:

We are now ready to define the printCharacters function. In this function, we will be going through each character that we sent to it and invoking a call of keyBoardInput for example “2020–04–23” would start with sending 2, then 0, and so on.

There are two structs here: input and keyboard input. The naming is taken over from the Windows docs whereas input represents a generic input which could be a mouse or a touch device and keyBoardInput is specific to keypresses.

Note that in lines 31- 42 we are definingkeyup command for CTRL and ALT and adding them to the slice that will be passed to sendInput. This is necessary because when you are pressing CTRL+ALT+D on your keyboard to invoke the shortcut, any input that you would try to send out would be considering CTRL+ALT as pressed.

There are two additional things to note: in line 52, we are passing the beginning of our slice to the procedure and in line 53 we are passing the size of each individual input element. Both are necessary, since sendInput will operate directly on the underlying memory.

That’s basically it, feel free to go all the way down to jump to the GitHub repo or to download the .exe to test out your code 🎄.

Autostart after logging in

Since we now have the tool operational, we probably want it to run at startup. Open your startup folder via pressingWIN+R and type shell:startup, and press enter. This will open your autostart folder. Drag and drop the .exe there. see the Windows docs for more info

You may additionally have to enable autostart in the Windows task manager

Bonus: adding a tray icon while the app is running

Now it would be helpful if we could know whether the application is running or not. The common way to do this on Windows is to use a little tray icon.

a tray icon for our app

Now adding the tray icon would actually require additional syscalls and quite a bit of plumbing, so we are going to use the

https://github.com/getlantern/systray package in this case. Adding systray requires us to do a little change to our main function: instead of directly running our main event loop, we invoke systray and provide an onReady function:

func main() { 
systray.Run(onReady, func() {})
}
func onReady() {
setupSystray()
run()
}

run will simply contain our previous main function whereas setupSystray will contain the icon and menu definitions for our app:

In order to have a flat .exe file, we serialized the icon file from the fileimages/bitmap.icointo a resource file with the go-bindata tool.

go get -u github.com/go-bindata/go-bindata/...

Thanks

thanks to this Stackoverflow post from icza which contained an example on how to listen to hotkeys.

GitHub repo and .exe file

You can find the Github repo for this tutorial which implements a global hotkey and tray icon here. Download the .exe under Releases to the right.

--

--

Alexey Stern

Software Engineer at Google in AMS. Previously at Uber, Spotify and in research at FIR@RWTH Aachen. Opinions are my own.