Skip to content

Commit

Permalink
all: initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
changkun committed Feb 27, 2021
1 parent 0fdd634 commit 1d76324
Show file tree
Hide file tree
Showing 19 changed files with 1,253 additions and 3 deletions.
12 changes: 12 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# These are supported funding model platforms

github: [changkun] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
48 changes: 48 additions & 0 deletions .github/workflows/hotkey.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright 2021 The golang.design Initiative Authors.
# All rights reserved. Use of this source code is governed
# by a MIT license that can be found in the LICENSE file.
#
# Written by Changkun Ou <changkun.de>

name: hotkey

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
platform_test:

runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]

steps:

- name: install xvfb libx11-dev
run: |
sudo apt update
sudo apt install -y xvfb libx11-dev
if: ${{ runner.os == 'Linux' }}

- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
stable: 'false'
go-version: '1.16.0'

- name: TestLinux
run: |
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
export DISPLAY=:99.0
sleep 5s
go test -v -covermode=atomic ./...
if: ${{ runner.os == 'Linux' }}

- name: TestOthers
run: |
go test -v -covermode=atomic ./...
72 changes: 70 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,70 @@
# hotkey
cross platform hotkey package
# hotkey [![PkgGoDev](https://pkg.go.dev/badge/golang.design/x/hotkey)](https://pkg.go.dev/golang.design/x/hotkey) ![](https://changkun.de/urlstat?mode=github&repo=golang-design/hotkey) ![hotkey](https://github.com/golang-design/hotkey/workflows/hotkey/badge.svg?branch=main)

cross platform hotkey package in Go

```go
import "golang.design/x/hotkey"
```

## Features

- Cross platform supports: macOS, Linux (X11), and Windows
- Global hotkey registration without focus on a window

## API Usage

Package `hotkey` provides the basic facility to register a system-level
hotkey so that the application can be notified if a user triggers the
desired hotkey. By definition, a hotkey is a combination of modifiers
and a single key, and thus register a hotkey that contains multiple
keys is not supported at the moment. Furthermore, because of OS
restriction, hotkey events must be handled on the main thread.

Therefore, in order to use this package properly, here is a complete
example that corporates the [mainthread](https://golang.design/s/mainthread)
package:

```go
package main

import (
"context"

"golang.design/x/hotkey"
"golang.design/x/mainthread"
)

// initialize mainthread facility so that hotkey can be
// properly registered to the system and handled by the
// application.
func main() { mainthread.Init(fn) }
func fn() { // Use fn as the actual main function.
var (
mods = []hotkey.Modifier{hotkey.ModCtrl}
k = hotkey.KeyS
)

// Register a desired hotkey.
hk, err := hotkey.Register(mods, k)
if err != nil {
panic("hotkey registration failed")
}

// Start listen hotkey event whenever you feel it is ready.
triggered := hk.Listen(context.Background())
for range triggered {
println("hotkey ctrl+s is triggered")
}
}
```

## Who is using this package?

The main purpose of building this package is to support the
[midgard](https://changkun.de/s/midgard) project.

To know more projects, check our [wiki](https://github.com/golang-design/clipboard/wiki) page.

## License

MIT | &copy; 2021 The golang.design Initiative Authors, written by [Changkun Ou](https://changkun.de).
30 changes: 30 additions & 0 deletions example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.

package main

import (
"context"

"golang.design/x/hotkey"
"golang.design/x/mainthread"
)

func main() { mainthread.Init(fn) }
func fn() {
var (
mods = []hotkey.Modifier{hotkey.ModCtrl}
k = hotkey.KeyS
)

hk, err := hotkey.Register(mods, k)
if err != nil {
panic("hotkey registration failed")
}

triggered := hk.Listen(context.Background())
for range triggered {
println("hotkey ctrl+s is triggered")
}
}
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
module golang.design/x/hotkey

go 1.16
go 1.16

require (
golang.design/x/mainthread v0.2.1
golang.org/x/sys v0.0.0-20210122093101-04d7465088b8 // indirect
)
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
golang.design/x/mainthread v0.2.1 h1:IUGVW1acDfKoQtFeeS/RD/YYiKK8jxwkJXIQuKuL+ig=
golang.design/x/mainthread v0.2.1/go.mod h1:vYX7cF2b3pTJMGM/hc13NmN6kblKnf4/IyvHeu259L0=
golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210122093101-04d7465088b8 h1:de2yTH1xuxjmGB7i6Z5o2z3RCHVa0XlpSZzjd8Fe6bE=
golang.org/x/sys v0.0.0-20210122093101-04d7465088b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
130 changes: 130 additions & 0 deletions hotkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>

// Package hotkey provides the basic facility to register a system-level
// hotkey so that the application can be notified if a user triggers the
// desired hotkey. By definition, a hotkey is a combination of modifiers
// and a single key, and thus register a hotkey that contains multiple
// keys is not supported at the moment. Furthermore, because of OS
// restriction, hotkey events must be handled on the main thread.
//
// Therefore, in order to use this package properly, here is a complete
// example that corporates the golang.design/x/mainthread package:
//
// package main
//
// import (
// "context"
//
// "golang.design/x/hotkey"
// "golang.design/x/mainthread"
// )
//
// // initialize mainthread facility so that hotkey can be
// // properly registered to the system and handled by the
// // application.
// func main() { mainthread.Init(fn) }
// func fn() { // Use fn as the actual main function.
// var (
// mods = []hotkey.Modifier{hotkey.ModCtrl}
// k = hotkey.KeyS
// )
//
// // Register a desired hotkey.
// hk, err := hotkey.Register(mods, k)
// if err != nil {
// panic("hotkey registration failed")
// }
//
// // Start listen hotkey event whenever you feel it is ready.
// triggered := hk.Listen(context.Background())
// for range triggered {
// println("hotkey ctrl+s is triggered")
// }
// }
package hotkey

import (
"context"
"runtime"

"golang.design/x/mainthread"
)

// Event represents a hotkey event
type Event struct{}

// Hotkey is a combination of modifiers and key to trigger an event
type Hotkey struct {
mods []Modifier
key Key

in chan<- Event
out <-chan Event
}

// Register registers a combination of hotkeys. If the hotkey has
// registered. This function will invalidates the old registration
// and overwrites its callback.
func Register(mods []Modifier, key Key) (*Hotkey, error) {
in, out := newEventChan()
hk := &Hotkey{mods, key, in, out}

var err error
mainthread.Call(func() { err = hk.register() })
if err != nil {
return nil, err
}

runtime.SetFinalizer(hk, func(hk *Hotkey) {
hk.unregister()
})

return hk, nil
}

// Listen handles a hotkey event and triggers a call to fn.
// The hotkey listen hook terminates when the context is canceled.
func (hk *Hotkey) Listen(ctx context.Context) <-chan Event {
mainthread.Go(func() { hk.handle(ctx) })
return hk.out
}

// newEventChan returns a sender and a receiver of a buffered channel
// with infinite capacity.
func newEventChan() (chan<- Event, <-chan Event) {
in, out := make(chan Event), make(chan Event)

go func() {
var q []Event

for {
e, ok := <-in
if !ok {
close(out)
return
}
q = append(q, e)
for len(q) > 0 {
select {
case out <- q[0]:
q = q[1:]
case e, ok := <-in:
if ok {
q = append(q, e)
break
}
for _, e := range q {
out <- e
}
close(out)
return
}
}
}
}()
return in, out
}
Loading

0 comments on commit 1d76324

Please sign in to comment.