|
| 1 | +# KeySwift - Agent Guide |
| 2 | + |
| 3 | +## Project Overview |
| 4 | + |
| 5 | +KeySwift is a Linux keyboard remapping tool designed specifically for GNOME desktop environments. It enables application-specific key mappings using JavaScript configuration files, allowing users to customize keyboard shortcuts for different applications. |
| 6 | + |
| 7 | +### Key Features |
| 8 | +- **Application-specific remapping**: Define custom keyboard mappings for specific applications based on window class |
| 9 | +- **JavaScript configuration**: Flexible configuration using QuickJS JavaScript engine |
| 10 | +- **Low-level input handling**: Uses Linux evdev subsystem for direct keyboard input capture |
| 11 | +- **D-Bus integration**: Communicates with GNOME Shell extension for window focus tracking |
| 12 | + |
| 13 | +## Technology Stack |
| 14 | + |
| 15 | +- **Language**: Go 1.22+ |
| 16 | +- **JavaScript Engine**: QuickJS (via buke/quickjs-go) |
| 17 | +- **System Dependencies**: |
| 18 | + - libevdev-dev (Linux input event handling) |
| 19 | + - D-Bus (GNOME integration) |
| 20 | +- **Key Go Dependencies**: |
| 21 | + - `github.com/godbus/dbus/v5` - D-Bus bindings for Go |
| 22 | + - `github.com/buke/quickjs-go` - QuickJS JavaScript engine bindings |
| 23 | + - `github.com/jialeicui/golibevdev` - Linux evdev wrapper |
| 24 | + - `github.com/samber/lo` - Go utilities library |
| 25 | + - `github.com/stretchr/testify` - Testing framework |
| 26 | + |
| 27 | +## Project Structure |
| 28 | + |
| 29 | +``` |
| 30 | +. |
| 31 | +├── cmd/keyswift/main.go # Application entry point and CLI |
| 32 | +├── pkg/ |
| 33 | +│ ├── bus/ # Event processing and coordination |
| 34 | +│ │ ├── impl.go # Main bus implementation |
| 35 | +│ │ ├── mode.go # Event type definitions |
| 36 | +│ │ └── session.go # Per-event processing session |
| 37 | +│ ├── engine/ # JavaScript engine integration |
| 38 | +│ │ ├── interfaces.go # Engine and Bus interfaces |
| 39 | +│ │ └── quickjs.go # QuickJS implementation |
| 40 | +│ ├── evdev/ # Input device management |
| 41 | +│ │ ├── evdev.go # Core types |
| 42 | +│ │ └── overview.go # Device enumeration |
| 43 | +│ ├── handler/ # Input event handling |
| 44 | +│ │ ├── handler.go # Main event processor |
| 45 | +│ │ └── modifier.go # Modifier key state tracking |
| 46 | +│ ├── keys/ # Key code mappings |
| 47 | +│ │ └── keys.go # Key name to code conversion |
| 48 | +│ ├── utils/ # Utilities |
| 49 | +│ │ ├── config.go # Configuration path helpers |
| 50 | +│ │ └── cache/ # Caching utilities |
| 51 | +│ └── wininfo/ # Window information |
| 52 | +│ ├── wininfo.go # Interface definitions |
| 53 | +│ └── dbus/ # D-Bus implementation |
| 54 | +├── examples/ |
| 55 | +│ └── config.js # Example configuration |
| 56 | +├── Makefile # Build automation |
| 57 | +└── go.mod # Go module definition |
| 58 | +``` |
| 59 | + |
| 60 | +## Architecture |
| 61 | + |
| 62 | +### Data Flow |
| 63 | + |
| 64 | +1. **Input Capture** (`pkg/handler/`) |
| 65 | + - Grabs physical keyboard devices via evdev |
| 66 | + - Processes raw key events |
| 67 | + - Tracks modifier key states |
| 68 | + - Manages key press/release sequences |
| 69 | + |
| 70 | +2. **Event Processing** (`pkg/bus/`) |
| 71 | + - Receives key events from handler |
| 72 | + - Creates isolated session per event |
| 73 | + - Executes JavaScript configuration |
| 74 | + - Routes output to virtual keyboard |
| 75 | + |
| 76 | +3. **JavaScript Engine** (`pkg/engine/`) |
| 77 | + - Compiles user configuration to bytecode |
| 78 | + - Exposes `KeySwift` global object with APIs: |
| 79 | + - `getActiveWindowClass()` - Get current application |
| 80 | + - `sendKeys(keys[])` - Send key combination |
| 81 | + - `onKeyPress(keys[], callback)` - Register key handler |
| 82 | + - Fast-path filtering for unregistered key combinations |
| 83 | + |
| 84 | +4. **Window Detection** (`pkg/wininfo/`) |
| 85 | + - D-Bus service receives window info from GNOME extension |
| 86 | + - Falls back to degraded mode if D-Bus unavailable |
| 87 | + - Provides active window class for conditional mappings |
| 88 | + |
| 89 | +5. **Output** (via golibevdev) |
| 90 | + - Creates virtual keyboard device |
| 91 | + - Sends remapped key events |
| 92 | + - Proper modifier handling (press/release ordering) |
| 93 | + |
| 94 | +## Build Commands |
| 95 | + |
| 96 | +```bash |
| 97 | +# Build the binary |
| 98 | +make |
| 99 | + |
| 100 | +# Build with version info |
| 101 | +go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT)" -o keyswift cmd/keyswift/main.go |
| 102 | + |
| 103 | +# Run tests (requires sudo for evdev access) |
| 104 | +sudo go test -v ./... |
| 105 | + |
| 106 | +# Run specific package tests |
| 107 | +sudo go test -v ./pkg/utils/cache/ |
| 108 | +``` |
| 109 | + |
| 110 | +## Testing |
| 111 | + |
| 112 | +### Test Structure |
| 113 | +- Unit tests use `github.com/stretchr/testify` |
| 114 | +- Tests requiring evdev access need `sudo` |
| 115 | +- Key test files: |
| 116 | + - `pkg/utils/cache/cache_test.go` - Cache implementation tests |
| 117 | + - `pkg/evdev/overview_test.go` - Device enumeration tests |
| 118 | + |
| 119 | +### CI/CD |
| 120 | +- GitHub Actions workflow in `.github/workflows/unit-test.yml` |
| 121 | +- Runs on push to main and pull requests |
| 122 | +- Requires `libevdev-dev` package |
| 123 | +- Tests run with Go 1.23 |
| 124 | + |
| 125 | +## Configuration |
| 126 | + |
| 127 | +### JavaScript API |
| 128 | + |
| 129 | +The configuration file (`~/.config/keyswift/config.js`) has access to: |
| 130 | + |
| 131 | +```javascript |
| 132 | +const KeySwift = { |
| 133 | + // Returns the window class of the currently focused application |
| 134 | + getActiveWindowClass: () => string, |
| 135 | + |
| 136 | + // Sends a key combination (modifiers: ctrl, alt, cmd/meta/super, shift) |
| 137 | + sendKeys: (keys: string[]) => void, |
| 138 | + |
| 139 | + // Registers a callback for specific key combination |
| 140 | + // Must be called at top level, not inside callbacks or conditionals |
| 141 | + onKeyPress: (keys: string[], callback: () => void) => void, |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +### Example Configuration |
| 146 | + |
| 147 | +See `examples/config.js` for a comprehensive example including: |
| 148 | +- Terminal-specific mappings (kitty, GNOME Terminal, Ghostty) |
| 149 | +- IDE mappings (JetBrains, Cursor, Sublime Text) |
| 150 | +- macOS-like shortcuts for Linux |
| 151 | +- Chrome tab switching shortcuts |
| 152 | +- Emacs-style navigation |
| 153 | + |
| 154 | +## Runtime Requirements |
| 155 | + |
| 156 | +### System Setup |
| 157 | +1. **GNOME Extension**: Install `keyswift-gnome-ext` from https://github.com/jialeicui/keyswift-gnome-ext |
| 158 | +2. **User Permissions**: Add user to `input` group |
| 159 | +3. **udev Rules**: Create `/etc/udev/rules.d/input.rules`: |
| 160 | + ``` |
| 161 | + KERNEL=="uinput", GROUP="input", TAG+="uaccess" |
| 162 | + ``` |
| 163 | +4. **Restart** system to apply group and udev changes |
| 164 | + |
| 165 | +### Running |
| 166 | + |
| 167 | +```bash |
| 168 | +# List available keyboards and filter by pattern |
| 169 | +./keyswift -keyboards "HHKB" -config ~/.config/keyswift/config.js |
| 170 | + |
| 171 | +# Multiple keyboards (comma-separated) |
| 172 | +./keyswift -keyboards "HHKB,Logitech" -config ~/.config/keyswift/config.js |
| 173 | + |
| 174 | +# Verbose logging |
| 175 | +./keyswift -keyboards "HHKB" -config ~/.config/keyswift/config.js -verbose |
| 176 | + |
| 177 | +# Custom output device name |
| 178 | +./keyswift -keyboards "HHKB" -output-device-name "my-keyboard" -config ~/.config/keyswift/config.js |
| 179 | +``` |
| 180 | + |
| 181 | +**Important**: Do not run with `sudo`. The application requires user-level permissions with `input` group membership. |
| 182 | + |
| 183 | +## Code Style Guidelines |
| 184 | + |
| 185 | +### Go Conventions |
| 186 | +- Standard Go formatting (`gofmt`) |
| 187 | +- Package comments for all public packages |
| 188 | +- Interface definitions in dedicated files (e.g., `interfaces.go`) |
| 189 | +- Error wrapping with context using `fmt.Errorf("...: %w", err)` |
| 190 | +- Structured logging using `log/slog` |
| 191 | + |
| 192 | +### Naming |
| 193 | +- Interfaces with `-er` suffix (e.g., `WinGetter`, `Engine`) |
| 194 | +- Implementation types with descriptive names (e.g., `Impl`, `QuickJS`, `Receiver`) |
| 195 | +- Constants for magic strings (e.g., `FuncSendKeys`, `KeySwiftObj`) |
| 196 | + |
| 197 | +### Error Handling |
| 198 | +- Return errors with context |
| 199 | +- Use `slog.Error()` for operational errors |
| 200 | +- Graceful degradation (e.g., `DegradedReceiver` for D-Bus failures) |
| 201 | + |
| 202 | +## Security Considerations |
| 203 | + |
| 204 | +1. **Input Device Access**: Requires membership in `input` group |
| 205 | +2. **D-Bus Communication**: Exposes service on session bus |
| 206 | +3. **JavaScript Execution**: User-provided scripts run in QuickJS sandbox with memory limits: |
| 207 | + - Memory limit: 1280 KB |
| 208 | + - GC threshold: 2560 KB |
| 209 | + - Max stack size: 65534 |
| 210 | + - Execution timeout: 0 (disabled) |
| 211 | + |
| 212 | +## Key Implementation Details |
| 213 | + |
| 214 | +### Modifier Key Handling |
| 215 | +- Modifier keys (Ctrl, Alt, Meta) are tracked separately |
| 216 | +- Pass-through mode for modifiers enables browser shortcuts (e.g., Ctrl+Click) |
| 217 | +- Modifier release events are synthesized when remapping begins |
| 218 | + |
| 219 | +### Key State Management |
| 220 | +- Debouncing with 5ms threshold |
| 221 | +- Periodic state synchronization (100ms) |
| 222 | +- Emergency key release on shutdown |
| 223 | +- Duplicate event prevention |
| 224 | + |
| 225 | +### Fast Path Optimization |
| 226 | +- JavaScript engine tracks registered key combinations |
| 227 | +- Unregistered combinations bypass script execution |
| 228 | +- Key code caching to avoid repeated lookups |
| 229 | + |
| 230 | +## Related Projects |
| 231 | + |
| 232 | +- **GNOME Extension**: https://github.com/jialeicui/keyswift-gnome-ext |
| 233 | +- Inspired by: xremap, kmonad, autokey, AutoHotkey |
0 commit comments