27

Let's say someone wants to create a cross-platform (Mac, Linux, Windows) global hotkey in Go (golang) - you press a hotkey combination anywhere in OS and let's say something is printed in terminal.

Currently, (July 2016) I haven't found any library to do that, so maybe we can find a way together.

It would involve, of course, calls to some native OS bindings for each OS, but there is very sparse information on how to do it.

Mac

Judging from Googling one should use addGlobalMonitorForEventsMatchingMask

EDIT: useless example removed

Linux

Looks like the suspect is XGrabKey, though, no example code anywhere in near sight https://github.com/search?utf8=%E2%9C%93&q=language%3Ago+XGrabKey&type=Repositories&ref=searchresults

Windows

It seems that we need to use RegisterHotKey, but trying to find some example code leads nowhere: https://github.com/search?utf8=%E2%9C%93&q=language%3Ago+RegisterHotKey

Some interesting cross-platform project to research (in Java) is https://github.com/tulskiy/jkeymaster

Any help would be greatly appreciated!

6
  • 1
    I think it should not belongs to go tag. Commented Jul 29, 2016 at 2:40
  • @JiangYD As per SO rules - one tag must be present. If you have a better suggestion - please share Commented Jul 29, 2016 at 5:41
  • I don't think that this feature can be implemented with Go. Such chross-plattform features, are not the typical Go things. For your task maybe you could use electron: electron.atom.io You could also call a Go app with your electron app. Commented Aug 12, 2016 at 13:13
  • I'm not saying it's easy, that's why I offer a bounty. But it's probably doable :) Electron is nice, but it's 100Mb for a simple app, which is unreasonable in many cases. Commented Aug 12, 2016 at 21:53
  • 1
    Well, that's the point of libraries. They can abstract away all the complexity of running it cross-platform :) Commented Aug 15, 2016 at 12:30

4 Answers 4

32
+200

This is possible on most operating systems with simple system calls, in Go you can use package syscall to do system calls without any extra C code or cgo compiler.

Note that the online official documentation of the syscall package shows only the linux interface. Other operating systems have slightly different interface, e.g. on Windows the syscall package also contains a syscall.DLL type, syscall.LoadDLL() and syscall.MustLoadDLL() functions, among others. To view these, run the godoc tool locally, e.g.

godoc -http=:6060

This starts a webserver that hosts a web page similar to godoc.org, navigate to http://localhost:6060/pkg/syscall/. You can also see platform-specific doc online, for details, see How to access platform specific package documentation?

Here I present a complete, runnable Windows solution in pure Go. The complete example application is available on the Go Playground. It doesn't run on the playground, download it and run it locally (on Windows).


Let's define the type to describe hotkeys we want to use. This is not Windows-specific, just to make our code nicer. The Hotkey.String() method provides a human-friendly display name of the hotkey such as "Hotkey[Id: 1, Alt+Ctrl+O]".

const (
    ModAlt = 1 << iota
    ModCtrl
    ModShift
    ModWin
)

type Hotkey struct {
    Id        int // Unique id
    Modifiers int // Mask of modifiers
    KeyCode   int // Key code, e.g. 'A'
}

// String returns a human-friendly display name of the hotkey
// such as "Hotkey[Id: 1, Alt+Ctrl+O]"
func (h *Hotkey) String() string {
    mod := &bytes.Buffer{}
    if h.Modifiers&ModAlt != 0 {
        mod.WriteString("Alt+")
    }
    if h.Modifiers&ModCtrl != 0 {
        mod.WriteString("Ctrl+")
    }
    if h.Modifiers&ModShift != 0 {
        mod.WriteString("Shift+")
    }
    if h.Modifiers&ModWin != 0 {
        mod.WriteString("Win+")
    }
    return fmt.Sprintf("Hotkey[Id: %d, %s%c]", h.Id, mod, h.KeyCode)
}

The windows user32.dll contains functions for global hotkey management. Let's load it:

user32 := syscall.MustLoadDLL("user32")
defer user32.Release()

It has a RegisterHotkey() function for registering global hotkeys:

reghotkey := user32.MustFindProc("RegisterHotKey")

Using this, let's register some hotkeys, namely ALT+CTRL+O, ALT+SHIFT+M, ALT+CTRL+X (which will be used to exit from the app):

// Hotkeys to listen to:
keys := map[int16]*Hotkey{
    1: &Hotkey{1, ModAlt + ModCtrl, 'O'},  // ALT+CTRL+O
    2: &Hotkey{2, ModAlt + ModShift, 'M'}, // ALT+SHIFT+M
    3: &Hotkey{3, ModAlt + ModCtrl, 'X'},  // ALT+CTRL+X
}

// Register hotkeys:
for _, v := range keys {
    r1, _, err := reghotkey.Call(
        0, uintptr(v.Id), uintptr(v.Modifiers), uintptr(v.KeyCode))
    if r1 == 1 {
        fmt.Println("Registered", v)
    } else {
        fmt.Println("Failed to register", v, ", error:", err)
    }
}

We need a way to "listen" to events of pressing those hotkeys. For this, user32.dll contains the PeekMessage() function:

peekmsg := user32.MustFindProc("PeekMessageW")

PeekMessage() stores the message into an MSG struct, let's define it:

type MSG struct {
    HWND   uintptr
    UINT   uintptr
    WPARAM int16
    LPARAM int64
    DWORD  int32
    POINT  struct{ X, Y int64 }
}

Now here's our listening loop which listens and acts on global key presses (here just simply print the pressed hotkey to the console, and if CTRL+ALT+X is pressed, exit from the app):

for {
    var msg = &MSG{}
    peekmsg.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0, 1)

    // Registered id is in the WPARAM field:
    if id := msg.WPARAM; id != 0 {
        fmt.Println("Hotkey pressed:", keys[id])
        if id == 3 { // CTRL+ALT+X = Exit
            fmt.Println("CTRL+ALT+X pressed, goodbye...")
            return
        }
    }

    time.Sleep(time.Millisecond * 50)
}

And we're done!

Starting the above application prints:

Registered Hotkey[Id: 1, Alt+Ctrl+O]
Registered Hotkey[Id: 2, Alt+Shift+M]
Registered Hotkey[Id: 3, Alt+Ctrl+X]

Now let's press some of the registered hotkeys (with any app being in focus, not necessarily our app), we'll see on the console:

Hotkey pressed: Hotkey[Id: 1, Alt+Ctrl+O]
Hotkey pressed: Hotkey[Id: 1, Alt+Ctrl+O]
Hotkey pressed: Hotkey[Id: 2, Alt+Shift+M]
Hotkey pressed: Hotkey[Id: 3, Alt+Ctrl+X]
CTRL+ALT+X pressed, goodbye...
Sign up to request clarification or add additional context in comments.

8 Comments

Perfect answer! Thank you!
There is a memory leak somewhere in the for loop. If you watch the memory in task manager it'll slowly grow, never going down.
Is it possible to use something like a for channel in place of just time.Sleep? I'm not too familiar with the library or the user32 dll but great explanations and answer.
@Datsik No, I don't think so. I would've used channels if I could have.
Would you say this is a "hacky" way of accomplishing this, and would be better left to be done in a different language or is this fine? Last question ;)
|
7

icza's answer is awesome and was of great help to me, but I found that using the PeekMessageW API call to be more considerably expensive than the GetMessageW API call and less reliable.

As per the Microsoft documentation:

Unlike GetMessage, the PeekMessage function does not wait for a message to be posted before returning.

From my limited understanding of Windows internals, that means that PeekMessageW is continually checking the message queue and returning "0" until an event is found, resulting in unnecessary compute.

Simply changing the code to:

getmsg := user32.MustFindProc("GetMessageW")

for {
    var msg = &MSG{}
    getmsg.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)

Fixed this for me. That means you can also get rid of the timeout without a notable performance hit:

time.Sleep(time.Millisecond * 50)

I do wonder if the reliability issues comes from the fact that I'm running the hotkey loop in a goroutine. I'm not experienced enough with Go or the Windows API to know but hope this helps someone! I also haven't experienced any sort of memory leak with this change, though I cannot verify if a memory leak existed with the original code as Programming4life suggests.

1 Comment

Great answer, not only does this prevent busy waiting but also gets rid of the polling lag!
5
+400

It can be done. For example gaming engine Azul3d can do that https://github.com/azul3d/engine/tree/master/keyboard. Your OSX snippet doesn't compile because AppDelegate.h file is missing. You can either write it by hand or first compile project with XCode which do bookkeeping for you and then look at AppDelegate.h and AppDelegate.m. On linux to log keystroke stream you can just read /dev/input/event$ID file which is done here https://github.com/MarinX/keylogger. On windows you will need some syscalls like in this code https://github.com/eiannone/keyboard. Both on nix and win you even need not cgo and can be happy with syscalls and pure Go.

2 Comments

Excellent pointers! I'll award a bounty in 23 hours as per SO rules :)
I've used github.com/MarinX/keylogger. Works well enough on Linux.
0

An option for cross platform is to use github.com/robotn/gohook, there is a useful example at https://blog.petehouston.com/implement-global-event-hook-for-keyboard-and-mouse-in-go/.

Note that this works well if you are looking to respond to a specific set of keys being pressed/released, e.g. the example shows gohook registered for []string{"ctrl", "shift", "x"} as being called back when Ctrl-Shift-X is pressed down.

I have had success getting non control key codes mapped to character/descriptive string, the code below works for standard keys and some others, including 'left arrow', but not 'backspace', 'escape', 'tab', etc., which are reported as a single character string with rawcode 8, 27, 9 (on windows), i.e. the standard ASCII character.

import (
    "fmt"

    gohook "github.com/robotn/gohook"
)

func CheckChanged() { // in main you need to run with : go CheckChanged()
    ch := gohook.Start()
    defer gohook.End()

    for e := range ch {
        if e.Kind == gohook.KeyDown || e.Kind == gohook.KeyUp {
            keyc := gohook.RawcodetoKeychar(e.Rawcode)
            if e.Kind == gohook.KeyDown {
                if keyc >= " " && keyc <= "~" {
                    fmt.Println("v", keyc)
                } else {
                    fmt.Println("v", e, keyc, len(keyc))
                }

            }
        }
    }
}

Example output for a1(shift)1(unshift)(left arrow)(control) below

v a
v 1
v ^
v !
v left arrow
v 2025-11-17 10:40:12.0635064 +0000 GMT m=+390.489723701 - Event: {Kind: KeyDown, Rawcode: 162, Keychar: 65535} ؛ 2

Note that the auto repeat will fire multiple presses...

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.