Skip to content

Commit 7cefa30

Browse files
author
Rajul
committed
Add configurable hotkeys and release automation
Restructure app startup around an app coordinator, improve Input Monitoring permission handling, and make the menu bar shortcut configurable. Also add repo scaffolding for docs, builds, and GitHub Actions release automation.
1 parent d78224d commit 7cefa30

35 files changed

+1104
-339
lines changed

.github/workflows/build.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
build:
15+
name: Build & Verify Universal Binary
16+
runs-on: macos-26
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Show Xcode version
22+
run: xcodebuild -version
23+
24+
- name: Build Universal Binary
25+
run: make build
26+
27+
- name: Verify app bundle
28+
run: make verify-app
29+
30+
- name: Verify Universal Binary (arm64 + x86_64)
31+
run: make verify-universal
32+
33+
- name: Create distribution
34+
run: make dist-app
35+
36+
- name: Upload app artifact
37+
uses: actions/upload-artifact@v4
38+
with:
39+
name: kutuk-app
40+
path: dist/Kutuk.app
41+
retention-days: 14

.github/workflows/release.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
name: Build & Release Universal Binary
14+
runs-on: macos-26
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Show Xcode version
20+
run: xcodebuild -version
21+
22+
- name: Build Universal Binary
23+
run: make build
24+
25+
- name: Verify Universal Binary (arm64 + x86_64)
26+
run: make verify-universal
27+
28+
- name: Create distribution
29+
run: make dist-app
30+
31+
- name: Create DMG
32+
run: make dmg
33+
34+
- name: Create GitHub Release
35+
uses: softprops/action-gh-release@v2
36+
with:
37+
files: |
38+
dist/Kutuk-Release.dmg
39+
generate_release_notes: true
40+
draft: false
41+
prerelease: false

.gitignore

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Xcode
2+
build/
3+
DerivedData/
4+
*.xcuserdata
5+
*.xcscmblueprint
6+
*.xccheckout
7+
8+
# macOS
9+
.DS_Store
10+
.AppleDouble
11+
.LSOverride
12+
Icon?
13+
._*
14+
.Spotlight-V100
15+
.Trashes
16+
17+
# Distribution
18+
dist/
19+
20+
# Archives
21+
*.xcarchive
22+
*.profraw
23+
*.profdata
24+
25+
# CocoaPods
26+
Pods/
27+
28+
# Swift Package Manager
29+
.build/
30+
Package.resolved
31+
32+
# Carthage
33+
Carthage/Build/
34+
35+
# IDE
36+
*.swp
37+
*.swo
38+
*~
39+
.vscode/
40+
.idea/

CONTRIBUTING.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Contributing to Kutuk
2+
3+
Thanks for your interest in contributing to Kutuk! Here's how to get started.
4+
5+
## Development Setup
6+
7+
1. **Requirements**: Xcode 15+ and macOS 14+
8+
2. Clone the repo and open `kutuk.xcodeproj` in Xcode
9+
3. Build and run with `Cmd+R`, or use `make build` from the terminal
10+
11+
## Building from Terminal
12+
13+
```bash
14+
# Build the app
15+
make build
16+
17+
# Create distributable app bundle (ad-hoc signed)
18+
make dist-app
19+
20+
# Create DMG installer
21+
make dmg
22+
23+
# Clean build artifacts
24+
make clean
25+
```
26+
27+
## Project Structure
28+
29+
- `kutuk/` — Main source directory
30+
- `kutukApp.swift` — App entry point
31+
- `Views/` — SwiftUI menu bar UI
32+
- `Services/` — Keyboard monitoring, audio engine, hotkey management
33+
- `Settings/` — UserDefaults persistence
34+
- `Models/` — Data models (sound packs, hotkey shortcuts)
35+
- `Resources/Sounds/` — Sound pack WAV files
36+
- `Assets.xcassets/` — App and menu bar icons
37+
38+
## Adding a Sound Pack
39+
40+
1. Create a new folder under `kutuk/Resources/Sounds/` with your pack ID
41+
2. Add WAV files following the naming convention: `{packId}_{keyType}_{event}_{variation}.wav`
42+
3. Register the pack in `SoundPack.swift`
43+
44+
## Submitting Changes
45+
46+
1. Fork the repo and create a feature branch
47+
2. Make your changes
48+
3. Test the app locally (`make build && make dist-app`)
49+
4. Open a pull request with a clear description
50+
51+
## Code Style
52+
53+
- Follow standard Swift conventions
54+
- Use SwiftUI for all UI components
55+
- Keep services loosely coupled via the coordinator pattern
56+
57+
## Reporting Issues
58+
59+
Open a GitHub issue with steps to reproduce, expected behavior, and your macOS version.

IMPLEMENTATION.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ kutuk/
2626
├── Info.plist # LSUIElement=true (no dock icon)
2727
├── kutuk.entitlements # App sandbox
2828
├── Models/
29-
│ └── SoundPack.swift # KeyType, KeyEvent, SoundPack struct
29+
│ └── SoundPack.swift # KeyType, KeyEvent, single-pack SoundPack struct
3030
├── Services/
3131
│ ├── SoundEngine.swift # AVAudioEngine, polyphony pool
3232
│ ├── KeyboardMonitor.swift # CGEvent tap, key classification
@@ -37,7 +37,7 @@ kutuk/
3737
│ ├── MenuBarView.swift # Main dropdown UI
3838
│ └── OnboardingView.swift # Permission request
3939
└── Resources/
40-
└── Sounds/ # 81 MP3 files
40+
└── Sounds/ # 16 Cherry MX Blue WAV files
4141
```
4242

4343
---
@@ -74,11 +74,11 @@ kutuk/
7474
```
7575

7676
Examples:
77-
- `cherry-mx-blue_regular_press_1.mp3`
78-
- `topre_space_release.mp3`
79-
- `buckling-spring_enter_press.mp3`
77+
- `cherry-mx-blue_regular_press_1.wav`
78+
- `cherry-mx-blue_space_release.wav`
79+
- `cherry-mx-blue_modifier_press.wav`
8080

81-
Packs: `cherry-mx-blue`, `cherry-mx-brown`, `cherry-mx-red`, `topre`, `buckling-spring`
81+
Pack: `cherry-mx-blue`
8282

8383
Key types: `regular`, `space`, `enter`, `backspace`, `modifier`
8484

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) 2026 Rajul Jain
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.

Makefile

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
PROJECT := kutuk.xcodeproj
2+
SCHEME := kutuk
3+
CONFIGURATION ?= Release
4+
DESTINATION ?= generic/platform=macOS
5+
DERIVED_DATA := $(CURDIR)/build/DerivedData
6+
BUILD_PRODUCTS_DIR := $(DERIVED_DATA)/Build/Products/$(CONFIGURATION)
7+
APP_NAME := Kutuk
8+
APP_BUNDLE := $(BUILD_PRODUCTS_DIR)/$(APP_NAME).app
9+
DIST_DIR := $(CURDIR)/dist
10+
DIST_APP_BUNDLE := $(DIST_DIR)/$(APP_NAME).app
11+
DMG_STAGING_DIR := $(DIST_DIR)/dmg
12+
DMG_VOLUME_NAME ?= Kutuk
13+
DMG_NAME ?= Kutuk-$(CONFIGURATION).dmg
14+
DMG_PATH := $(DIST_DIR)/$(DMG_NAME)
15+
BUNDLE_IDENTIFIER ?= io.github.irajul.kutuk
16+
17+
# Build flags: Universal Binary (Intel + Apple Silicon), no code signing for CI
18+
XCODEBUILD_FLAGS ?= \
19+
CODE_SIGNING_ALLOWED=NO \
20+
ONLY_ACTIVE_ARCH=NO \
21+
ARCHS="arm64 x86_64"
22+
23+
.PHONY: build dist-app dmg clean verify-app verify-universal
24+
25+
build:
26+
xcodebuild \
27+
-project "$(PROJECT)" \
28+
-scheme "$(SCHEME)" \
29+
-configuration "$(CONFIGURATION)" \
30+
-destination "$(DESTINATION)" \
31+
-derivedDataPath "$(DERIVED_DATA)" \
32+
build \
33+
$(XCODEBUILD_FLAGS)
34+
35+
verify-app: build
36+
@test -d "$(APP_BUNDLE)" || (echo "Expected app bundle not found at $(APP_BUNDLE)" && exit 1)
37+
38+
verify-universal: verify-app
39+
@echo "Checking Universal Binary architectures..."
40+
@lipo -info "$(APP_BUNDLE)/Contents/MacOS/$(APP_NAME)" | grep -q "arm64" || (echo "Missing arm64 architecture" && exit 1)
41+
@lipo -info "$(APP_BUNDLE)/Contents/MacOS/$(APP_NAME)" | grep -q "x86_64" || (echo "Missing x86_64 architecture" && exit 1)
42+
@echo "Universal Binary verified: arm64 + x86_64"
43+
44+
dist-app: verify-app
45+
rm -rf "$(DIST_APP_BUNDLE)"
46+
mkdir -p "$(DIST_DIR)"
47+
ditto "$(APP_BUNDLE)" "$(DIST_APP_BUNDLE)"
48+
codesign --force --deep --sign - --identifier "$(BUNDLE_IDENTIFIER)" "$(DIST_APP_BUNDLE)"
49+
@echo "App bundle created at $(DIST_APP_BUNDLE)"
50+
51+
dmg: dist-app
52+
rm -rf "$(DMG_STAGING_DIR)" "$(DMG_PATH)"
53+
mkdir -p "$(DMG_STAGING_DIR)"
54+
ditto "$(DIST_APP_BUNDLE)" "$(DMG_STAGING_DIR)/$(APP_NAME).app"
55+
ln -s /Applications "$(DMG_STAGING_DIR)/Applications"
56+
hdiutil create \
57+
-volname "$(DMG_VOLUME_NAME)" \
58+
-srcfolder "$(DMG_STAGING_DIR)" \
59+
-ov \
60+
-format UDZO \
61+
"$(DMG_PATH)"
62+
@if [ -n "$(DMG_CODESIGN_IDENTITY)" ]; then \
63+
codesign --force --sign "$(DMG_CODESIGN_IDENTITY)" --timestamp "$(DMG_PATH)"; \
64+
fi
65+
rm -rf "$(DMG_STAGING_DIR)"
66+
@echo "DMG created at $(DMG_PATH)"
67+
68+
clean:
69+
rm -rf "$(CURDIR)/build" "$(DIST_DIR)"

README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Kutuk
2+
3+
Mechanical keyboard sounds for your Mac. Kutuk plays satisfying Cherry MX Blue click sounds as you type, running quietly in your menu bar.
4+
5+
![macOS](https://img.shields.io/badge/macOS-14.0%2B-blue)
6+
![License](https://img.shields.io/badge/license-MIT-green)
7+
![Build](https://img.shields.io/github/actions/workflow/status/irajul/kutuk/build.yml?branch=main)
8+
9+
## Features
10+
11+
- Cherry MX Blue mechanical keyboard sounds with natural variation
12+
- Universal Binary: runs natively on both Apple Silicon and Intel Macs
13+
- Menu bar app with no dock icon clutter
14+
- Global hotkey toggle (default: Option+Command+K)
15+
- Volume control and sound pack selection
16+
- Launch at Login support
17+
18+
## Install
19+
20+
### Download
21+
22+
Grab the latest DMG from [Releases](https://github.com/irajul/kutuk/releases), open it, and drag **Kutuk.app** into Applications.
23+
24+
### Important: First Launch
25+
26+
Since Kutuk is not notarized (it's a free open-source app), macOS Gatekeeper will block it. You need to remove the quarantine attribute before the first launch:
27+
28+
```bash
29+
xattr -cr /Applications/Kutuk.app
30+
```
31+
32+
Then open the app normally. You'll also need to grant **Input Monitoring** permission when prompted (System Settings > Privacy & Security > Input Monitoring).
33+
34+
### Build from Source
35+
36+
```bash
37+
git clone https://github.com/irajul/kutuk.git
38+
cd kutuk
39+
make build # Build the app
40+
make dist-app # Create ad-hoc signed distribution
41+
make dmg # Create DMG installer
42+
```
43+
44+
Requires Xcode 16+ and macOS 14+.
45+
46+
## Usage
47+
48+
Once running, Kutuk appears as a small icon in your menu bar. Click it to:
49+
50+
- Toggle sounds on/off
51+
- Adjust volume
52+
- Switch sound packs
53+
- Configure the global hotkey
54+
- Set launch at login
55+
56+
## Permissions
57+
58+
Kutuk requires **Input Monitoring** permission to detect keystrokes. It listens in read-only mode and never records, stores, or transmits any keystrokes. You can verify this in the source code — the event tap uses `.listenOnly` mode.
59+
60+
### Granting Permission
61+
62+
1. When prompted, click **Grant Input Monitoring** in the menu
63+
2. Toggle Kutuk **ON** in System Settings > Privacy & Security > Input Monitoring
64+
3. Click **Restart to Apply Permission** in the menu (macOS requires a restart for the permission to take effect)
65+
66+
### Troubleshooting: Multiple Entries in Input Monitoring
67+
68+
If you rebuild from source, macOS may create duplicate Kutuk entries in Input Monitoring. To clean up, open System Settings > Privacy & Security > Input Monitoring, select each old entry, click the **minus (−)** button to remove it, then re-add the current app.
69+
70+
## Contributing
71+
72+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
73+
74+
## License
75+
76+
[MIT](LICENSE)

0 commit comments

Comments
 (0)