Skip to content

Commit edbbe7c

Browse files
committed
Initial commit
0 parents  commit edbbe7c

File tree

20 files changed

+743
-0
lines changed

20 files changed

+743
-0
lines changed

.github/workflows/unit-test.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: build and test
2+
3+
on:
4+
push:
5+
tags:
6+
- v*
7+
branches:
8+
- main
9+
pull_request:
10+
11+
jobs:
12+
build-and-test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v3
16+
17+
- name: Set up libevdev
18+
run: sudo apt-get install libevdev-dev
19+
20+
- name: Set up Go
21+
uses: actions/setup-go@v4
22+
with:
23+
go-version: '1.23'
24+
cache-dependency-path: go.sum
25+
26+
- name: Build
27+
run: go build -v ./...
28+
29+
- name: Test
30+
run: sudo go test -v ./...

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.vscode

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Jialei
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

cmd/keyswift/main.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log"
7+
"strings"
8+
9+
"github.com/jialeicui/golibevdev"
10+
"github.com/samber/lo"
11+
12+
"github.com/jialeicui/keyswift/pkg/conf"
13+
"github.com/jialeicui/keyswift/pkg/evdev"
14+
"github.com/jialeicui/keyswift/pkg/keys"
15+
"github.com/jialeicui/keyswift/pkg/wininfo"
16+
"github.com/jialeicui/keyswift/pkg/wininfo/dbus"
17+
)
18+
19+
var (
20+
flagKeyboard = flag.String("keyboard", "Apple", "")
21+
)
22+
23+
func main() {
24+
c, err := conf.LoadConfig(conf.DefaultConfigPath())
25+
if err != nil {
26+
log.Fatal(err)
27+
}
28+
29+
fmt.Printf("Loaded config: %+v\n", c)
30+
31+
r, err := dbus.New()
32+
if err != nil {
33+
log.Fatal(err)
34+
}
35+
defer r.Close()
36+
37+
fmt.Println("Window Monitor service is running...")
38+
39+
r.OnActiveWindowChange(func(info *wininfo.WinInfo) {
40+
fmt.Printf("Active window: %s - %s\n", info.Title, info.Class)
41+
})
42+
43+
devs, err := evdev.NewOverviewImpl().ListInputDevices()
44+
if err != nil {
45+
log.Fatal(err)
46+
}
47+
48+
dev, ok := lo.Find(devs, func(item *evdev.InputDevice) bool {
49+
return strings.Contains(item.Name, *flagKeyboard)
50+
})
51+
if !ok {
52+
log.Fatalf("Keyboard %s not found", *flagKeyboard)
53+
}
54+
55+
out, err := golibevdev.NewVirtualKeyboard("keyswift")
56+
if err != nil {
57+
log.Fatal(err)
58+
}
59+
defer out.Close()
60+
61+
in, err := golibevdev.NewInputDev(dev.Path)
62+
if err != nil {
63+
log.Fatal(err)
64+
}
65+
defer in.Close()
66+
67+
gnome := keys.NewGnomeKeys(out)
68+
69+
for {
70+
ev, err := in.NextEvent(golibevdev.ReadFlagNormal)
71+
if err != nil {
72+
log.Fatal(err)
73+
}
74+
if ev.Type != golibevdev.EvKey {
75+
continue
76+
}
77+
if ev.Code == golibevdev.KeyC {
78+
gnome.Copy()
79+
}
80+
}
81+
}

examples/config.yaml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# KeySwift Configuration File
2+
3+
# Application Groups define categories of applications
4+
appGroups:
5+
terminal:
6+
name: Terminal Emulators
7+
description: Command line terminal applications
8+
appPatterns:
9+
- "gnome-terminal-server"
10+
- "konsole"
11+
- "alacritty"
12+
- "kitty"
13+
- "com.mitchellh.ghostty"
14+
15+
jetbrains:
16+
name: JetBrains IDEs
17+
description: JetBrains integrated development environments
18+
appPatterns:
19+
- "jetbrains-idea"
20+
- "jetbrains-clion"
21+
- "jetbrains-pycharm"
22+
- "jetbrains-webstorm"
23+
- "jetbrains-goland"
24+
25+
browsers:
26+
name: Web Browsers
27+
description: Web browser applications
28+
appPatterns:
29+
- "*firefox*"
30+
- "*chrome*"
31+
- "*chromium*"
32+
- "brave"
33+
34+
# Keymap Groups define sets of keymappings that apply to specific app groups
35+
keymapGroups:
36+
vim_navigation:
37+
name: Vim-style Navigation
38+
description: Vim-like navigation keys (hjkl)
39+
enabled: true
40+
appGroups:
41+
- terminal
42+
- jetbrains
43+
keymaps:
44+
- key: "Alt+h"
45+
command: "Left"
46+
description: "Move cursor left"
47+
enabled: true
48+
49+
- key: "Alt+j"
50+
command: "Down"
51+
description: "Move cursor down"
52+
enabled: true
53+
54+
- key: "Alt+k"
55+
command: "Up"
56+
description: "Move cursor up"
57+
enabled: true
58+
59+
- key: "Alt+l"
60+
command: "Right"
61+
description: "Move cursor right"
62+
enabled: true
63+
64+
media_controls:
65+
name: Media Control Shortcuts
66+
description: Global media playback controls
67+
enabled: true
68+
appGroups: [ ] # Empty means global (all applications)
69+
keymaps:
70+
- key: "Super+p"
71+
command: "XF86AudioPlay"
72+
description: "Play/Pause media"
73+
enabled: true
74+
75+
- key: "Super+["
76+
command: "XF86AudioPrev"
77+
description: "Previous track"
78+
enabled: true
79+
80+
- key: "Super+]"
81+
command: "XF86AudioNext"
82+
description: "Next track"
83+
enabled: true

go.mod

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module github.com/jialeicui/keyswift
2+
3+
go 1.23.0
4+
5+
toolchain go1.23.4
6+
7+
require (
8+
github.com/godbus/dbus/v5 v5.1.0
9+
github.com/stretchr/testify v1.10.0
10+
gopkg.in/yaml.v3 v3.0.1
11+
)
12+
13+
require (
14+
github.com/samber/lo v1.49.1 // indirect
15+
golang.org/x/text v0.23.0 // indirect
16+
)
17+
18+
require (
19+
github.com/davecgh/go-spew v1.1.1 // indirect
20+
github.com/jialeicui/golibevdev v0.0.1
21+
github.com/pmezard/go-difflib v1.0.0 // indirect
22+
)

go.sum

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
4+
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
5+
github.com/jialeicui/golibevdev v0.0.1 h1:iwR1+KnJ97fCeWz5Ili4tVLJ21LtZVNFy0uMS7yetFU=
6+
github.com/jialeicui/golibevdev v0.0.1/go.mod h1:BIGi9TtzkWS9wHy3n+uGXeSfhiPWJUOVCvgg/Qh8tX8=
7+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
9+
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
10+
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
11+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
12+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
13+
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
14+
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
15+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
16+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
17+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
18+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/emacs/emacs.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package emacs
2+
3+
// ctrl+b -> backward
4+
// ctrl+f -> forward
5+
// ctrl+n -> next
6+
// ctrl+p -> previous
7+
// ctrl+a -> beginning
8+
// ctrl+e -> end

pkg/conf/appgroup.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package conf
2+
3+
import "path/filepath"
4+
5+
// AppGroup represents a group of applications with similar characteristics
6+
type AppGroup struct {
7+
Name string `yaml:"name"`
8+
Description string `yaml:"description"`
9+
AppPatterns []string `yaml:"appPatterns"` // Window class patterns to match applications
10+
}
11+
12+
// MatchesApp checks if the given window class/application matches this app group
13+
func (a *AppGroup) MatchesApp(windowClass string) bool {
14+
for _, pattern := range a.AppPatterns {
15+
matched, _ := filepath.Match(pattern, windowClass)
16+
if matched {
17+
return true
18+
}
19+
}
20+
return false
21+
}

pkg/conf/config.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package conf
2+
3+
import "fmt"
4+
5+
// Config represents the root configuration for KeySwift
6+
type Config struct {
7+
AppGroups map[string]AppGroup `yaml:"appGroups"`
8+
KeymapGroups map[string]KeymapGroup `yaml:"keymapGroups"`
9+
}
10+
11+
// Validate ensures the configuration is valid
12+
func (c *Config) Validate() error {
13+
// Validate app groups references in keymap groups
14+
for name, kmGroup := range c.KeymapGroups {
15+
for _, appGroupName := range kmGroup.AppGroups {
16+
if _, exists := c.AppGroups[appGroupName]; !exists {
17+
return fmt.Errorf("keymap group '%s' references non-existent app group '%s'", name, appGroupName)
18+
}
19+
}
20+
}
21+
return nil
22+
}

0 commit comments

Comments
 (0)