Skip to content

Commit 847d089

Browse files
committed
feat: source event and triggers
1 parent edbbe7c commit 847d089

File tree

21 files changed

+1173
-252
lines changed

21 files changed

+1173
-252
lines changed

README.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# KeySwift
2+
3+
KeySwift is a keyboard remapping tool designed for Linux gnome desktop environments. It allows you to customize keyboard mappings for different applications, enhancing your productivity and typing experience.
4+
5+
## Features
6+
7+
- **Application-specific remapping**: Define custom keyboard mappings for specific applications
8+
- **Flexible configuration**: Simple and intuitive configuration format
9+
10+
## Installation
11+
12+
Download the latest release from the [Releases](https://github.com/jialeicui/keyswift/releases) page.
13+
14+
Or install from source:
15+
16+
```bash
17+
git clone https://github.com/jialeicui/keyswift.git
18+
cd keyswift
19+
make install
20+
```
21+
22+
## Configuration
23+
24+
Create a configuration file at `~/.config/keyswift/config.yml`. Here's an example:
25+
26+
```yaml
27+
# yaml-language-server: $schema=schema.json
28+
# KeySwift Configuration File
29+
30+
mode_actions:
31+
default:
32+
if: "true"
33+
triggers:
34+
- source_event:
35+
key_press_event:
36+
key: "esc"
37+
action:
38+
set_value:
39+
normal: true
40+
- source_event:
41+
window_focus_event:
42+
window_class:
43+
- kitty
44+
action:
45+
set_value:
46+
terminal: true
47+
normal:
48+
if: normal
49+
triggers:
50+
- source_event:
51+
key_press_event:
52+
key: "j"
53+
action:
54+
map_to_keys:
55+
keys:
56+
- key: "down"
57+
```
58+
59+
## Acknowledgments
60+
61+
KeySwift was inspired by several excellent projects:
62+
63+
- [xremap](https://github.com/xremap/xremap): A key remapper for X11 and Wayland
64+
- [kmonad](https://github.com/kmonad/kmonad): An advanced keyboard manager with powerful customization features
65+
- [autokey](https://github.com/autokey/autokey): A desktop automation utility for Linux
66+
67+
Thank you to the maintainers of these projects for your contributions to open-source keyboard customization tools!
68+
69+
## Getting Help
70+
71+
If you encounter any issues or have questions, please [open an issue](https://github.com/jialeicui/keyswift/issues) on the GitHub repository.

cmd/keyswift/main.go

Lines changed: 238 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,79 +3,285 @@ package main
33
import (
44
"flag"
55
"fmt"
6-
"log"
6+
"os"
7+
"os/signal"
78
"strings"
9+
"sync"
10+
"syscall"
811

912
"github.com/jialeicui/golibevdev"
1013
"github.com/samber/lo"
14+
log "github.com/sirupsen/logrus"
1115

1216
"github.com/jialeicui/keyswift/pkg/conf"
1317
"github.com/jialeicui/keyswift/pkg/evdev"
1418
"github.com/jialeicui/keyswift/pkg/keys"
15-
"github.com/jialeicui/keyswift/pkg/wininfo"
19+
"github.com/jialeicui/keyswift/pkg/mode"
1620
"github.com/jialeicui/keyswift/pkg/wininfo/dbus"
1721
)
1822

1923
var (
20-
flagKeyboard = flag.String("keyboard", "Apple", "")
24+
flagKeyboards = flag.String("keyboards", "keyboard", "Comma-separated list of keyboard device name substrings")
25+
flagConfig = flag.String("config", "", "Configuration file path (defaults to $XDG_CONFIG_HOME/keyswift/config.yaml)")
26+
// TODO change this to false
27+
flagVerbose = flag.Bool("verbose", true, "Enable verbose logging")
2128
)
2229

23-
func main() {
24-
c, err := conf.LoadConfig(conf.DefaultConfigPath())
25-
if err != nil {
26-
log.Fatal(err)
27-
}
30+
// InputDevice represents a grabbed input device
31+
type InputDevice struct {
32+
Device *golibevdev.InputDev
33+
Name string
34+
Path string
35+
}
2836

29-
fmt.Printf("Loaded config: %+v\n", c)
37+
// InputDeviceManager manages multiple input devices
38+
type InputDeviceManager struct {
39+
devices []*InputDevice
40+
wg sync.WaitGroup
41+
}
42+
43+
// NewInputDeviceManager creates a new input device manager
44+
func NewInputDeviceManager() *InputDeviceManager {
45+
return &InputDeviceManager{
46+
devices: make([]*InputDevice, 0),
47+
}
48+
}
3049

31-
r, err := dbus.New()
50+
// AddDevice adds and grabs a new input device
51+
func (m *InputDeviceManager) AddDevice(name, path string) error {
52+
dev, err := golibevdev.NewInputDev(path)
3253
if err != nil {
33-
log.Fatal(err)
54+
return fmt.Errorf("failed to open input device %s: %w", path, err)
3455
}
35-
defer r.Close()
3656

37-
fmt.Println("Window Monitor service is running...")
57+
if err = dev.Grab(); err != nil {
58+
dev.Close()
59+
return fmt.Errorf("failed to grab input device %s: %w", path, err)
60+
}
3861

39-
r.OnActiveWindowChange(func(info *wininfo.WinInfo) {
40-
fmt.Printf("Active window: %s - %s\n", info.Title, info.Class)
62+
m.devices = append(m.devices, &InputDevice{
63+
Device: dev,
64+
Name: name,
65+
Path: path,
4166
})
67+
return nil
68+
}
4269

43-
devs, err := evdev.NewOverviewImpl().ListInputDevices()
70+
// ProcessEvents starts processing events from all devices
71+
func (m *InputDeviceManager) ProcessEvents(virtualKeyboard *golibevdev.UInputDev, modeManager *mode.Manager) {
72+
for _, dev := range m.devices {
73+
m.wg.Add(1)
74+
go func(d *InputDevice) {
75+
defer m.wg.Done()
76+
m.processDeviceEvents(d, virtualKeyboard, modeManager)
77+
}(dev)
78+
}
79+
}
80+
81+
// processDeviceEvents processes events from a single device
82+
func (m *InputDeviceManager) processDeviceEvents(dev *InputDevice, virtualKeyboard *golibevdev.UInputDev, modeManager *mode.Manager) {
83+
log.Infof("Starting event processing for device: %s", dev.Name)
84+
for {
85+
ev, err := dev.Device.NextEvent(golibevdev.ReadFlagNormal)
86+
if err != nil {
87+
log.Errorf("Error reading from device %s: %v", dev.Name, err)
88+
return
89+
}
90+
91+
log.Debugf("[%s] Received event: %+v", dev.Name, ev)
92+
93+
var handled bool
94+
if ev.Type == golibevdev.EvKey {
95+
// Convert to our event type
96+
event := &mode.Event{
97+
KeyPress: &mode.KeyPressEvent{
98+
Key: keyCodeToString(ev.Code.(golibevdev.KeyEventCode)),
99+
Pressed: ev.Value == 1,
100+
Repeated: ev.Value == 2,
101+
},
102+
}
103+
log.Debugf("[%s] Event: %+v", dev.Name, event.KeyPress)
104+
105+
// Process the event through the mode manager
106+
handled, err = modeManager.ProcessEvent(event)
107+
if err != nil {
108+
log.Errorf("[%s] Error processing event: %v", dev.Name, err)
109+
continue
110+
}
111+
}
112+
// If the event wasn't handled by any mode, pass it through directly
113+
if !handled {
114+
log.Debugf("[%s] Passing through unhandled event: %+v", dev.Name, ev)
115+
// Directly send the original key event to the output device
116+
virtualKeyboard.WriteEvent(ev.Type, ev.Code, ev.Value)
117+
}
118+
log.Debug("--------------------")
119+
}
120+
}
121+
122+
// Wait waits for all event processing to complete
123+
func (m *InputDeviceManager) Wait() {
124+
m.wg.Wait()
125+
}
126+
127+
// Close closes all input devices
128+
func (m *InputDeviceManager) Close() {
129+
for _, dev := range m.devices {
130+
log.Infof("Closing device: %s", dev.Name)
131+
dev.Device.Close()
132+
}
133+
}
134+
135+
func main() {
136+
flag.Parse()
137+
138+
if *flagVerbose {
139+
log.SetLevel(log.DebugLevel)
140+
}
141+
142+
// Load configuration
143+
configPath := *flagConfig
144+
if configPath == "" {
145+
configPath = conf.DefaultConfigPath()
146+
}
147+
148+
c, err := conf.Load(configPath)
44149
if err != nil {
45150
log.Fatal(err)
46151
}
152+
fmt.Printf("Loaded config from %s\n", configPath)
47153

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)
154+
// Initialize window info service
155+
windowMonitor, err := dbus.New()
156+
if err != nil {
157+
log.Fatal(err)
53158
}
159+
defer windowMonitor.Close()
160+
fmt.Println("Window Monitor service is running...")
54161

162+
// Initialize virtual keyboard for output
55163
out, err := golibevdev.NewVirtualKeyboard("keyswift")
56164
if err != nil {
57165
log.Fatal(err)
58166
}
59167
defer out.Close()
168+
gnomeKeys := keys.NewGnomeKeys(out)
169+
170+
// Initialize mode manager
171+
modeManager, err := mode.NewManager(c, gnomeKeys, windowMonitor)
172+
if err != nil {
173+
log.Fatal(err)
174+
}
175+
fmt.Println("Mode manager initialized")
60176

61-
in, err := golibevdev.NewInputDev(dev.Path)
177+
// Find input devices
178+
devs, err := evdev.NewOverviewImpl().ListInputDevices()
62179
if err != nil {
63180
log.Fatal(err)
64181
}
65-
defer in.Close()
66182

67-
gnome := keys.NewGnomeKeys(out)
183+
// Parse keyboard patterns and find matching devices
184+
keyboardPatterns := strings.Split(*flagKeyboards, ",")
185+
var matchedDevices []*evdev.InputDevice
68186

69-
for {
70-
ev, err := in.NextEvent(golibevdev.ReadFlagNormal)
71-
if err != nil {
72-
log.Fatal(err)
73-
}
74-
if ev.Type != golibevdev.EvKey {
187+
for _, pattern := range keyboardPatterns {
188+
pattern = strings.TrimSpace(pattern)
189+
if pattern == "" {
75190
continue
76191
}
77-
if ev.Code == golibevdev.KeyC {
78-
gnome.Copy()
192+
193+
matches := lo.Filter(devs, func(item *evdev.InputDevice, _ int) bool {
194+
return strings.Contains(item.Name, pattern)
195+
})
196+
197+
matchedDevices = append(matchedDevices, matches...)
198+
}
199+
200+
// Remove duplicates
201+
matchedDevices = lo.UniqBy(matchedDevices, func(dev *evdev.InputDevice) string {
202+
return dev.Path
203+
})
204+
205+
if len(matchedDevices) == 0 {
206+
fmt.Println("Available keyboards:")
207+
for _, d := range devs {
208+
fmt.Printf(" - %s\n", d.Name)
79209
}
210+
log.Fatalf("No keyboards matching patterns: %s", *flagKeyboards)
211+
}
212+
213+
// Initialize and set up the device manager
214+
deviceManager := NewInputDeviceManager()
215+
defer deviceManager.Close()
216+
217+
// Add all matched devices to the manager
218+
for _, dev := range matchedDevices {
219+
fmt.Printf("Using keyboard: %s (%s)\n", dev.Name, dev.Path)
220+
if err := deviceManager.AddDevice(dev.Name, dev.Path); err != nil {
221+
log.Warnf("Failed to add device %s: %v", dev.Name, err)
222+
continue
223+
}
224+
}
225+
226+
// Handle signals
227+
sigChan := make(chan os.Signal, 1)
228+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
229+
230+
go func() {
231+
<-sigChan
232+
fmt.Println("Shutting down...")
233+
deviceManager.Close()
234+
out.Close()
235+
windowMonitor.Close()
236+
os.Exit(0)
237+
}()
238+
239+
// Start processing events from all devices
240+
fmt.Printf("Processing events from %d devices... Press Ctrl+C to exit\n", len(deviceManager.devices))
241+
deviceManager.ProcessEvents(out, modeManager)
242+
243+
// Wait for all processing to complete (typically won't reach here except on error)
244+
deviceManager.Wait()
245+
}
246+
247+
// keyCodeToString converts a key code to a string representation
248+
// This is a simplified version - you'd want a complete mapping
249+
func keyCodeToString(code golibevdev.KeyEventCode) string {
250+
// You'll need a complete mapping from golibevdev key codes to strings
251+
// This is just a starting point
252+
keyMap := map[golibevdev.KeyEventCode]string{
253+
golibevdev.KeyEsc: "esc",
254+
golibevdev.KeyA: "a",
255+
golibevdev.KeyB: "b",
256+
golibevdev.KeyC: "c",
257+
golibevdev.KeyD: "d",
258+
golibevdev.KeyE: "e",
259+
golibevdev.KeyF: "f",
260+
golibevdev.KeyG: "g",
261+
golibevdev.KeyH: "h",
262+
golibevdev.KeyI: "i",
263+
golibevdev.KeyJ: "j",
264+
golibevdev.KeyK: "k",
265+
golibevdev.KeyL: "l",
266+
golibevdev.KeyM: "m",
267+
golibevdev.KeyN: "n",
268+
golibevdev.KeyO: "o",
269+
golibevdev.KeyP: "p",
270+
golibevdev.KeyQ: "q",
271+
golibevdev.KeyR: "r",
272+
golibevdev.KeyS: "s",
273+
golibevdev.KeyT: "t",
274+
golibevdev.KeyU: "u",
275+
golibevdev.KeyV: "v",
276+
golibevdev.KeyW: "w",
277+
golibevdev.KeyX: "x",
278+
golibevdev.KeyY: "y",
279+
golibevdev.KeyZ: "z",
280+
// Add more keys as needed
281+
}
282+
283+
if name, ok := keyMap[code]; ok {
284+
return name
80285
}
286+
return code.String()
81287
}

0 commit comments

Comments
 (0)