Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
77fbdb1
♻️ refactor(service): make LockEngine.lock idempotent, adopt Logger
LZhenHong Jul 15, 2026
8fece9a
♻️ refactor(core): relocate settings store to Service, SystemSettings…
LZhenHong Jul 15, 2026
e23af8e
✨ feat(xpc): flow settings through the Agent, async stateless client
LZhenHong Jul 15, 2026
9d2a629
✨ feat(app): register Agent via SMAppService, add lock toggle
LZhenHong Jul 15, 2026
ce4ab6e
📝 docs(development): document settings-over-XPC, async client, SMAppS…
LZhenHong Jul 15, 2026
dd38498
🎨 style: apply swiftformat to untouched files
LZhenHong Jul 15, 2026
b4afcf4
✨ feat(app): reconcile lock state when the menu window appears
LZhenHong Jul 15, 2026
67c2cb3
♻️ refactor(client): treat broadcasts as hints, fetch authoritative s…
LZhenHong Jul 15, 2026
6a29f76
📝 docs(architecture): wire state-sync rules into the wrapper checklist
LZhenHong Jul 15, 2026
7298d3f
📝 docs: translate architecture and development guides to Chinese
LZhenHong Jul 15, 2026
7b55a54
📝 docs(architecture): add "why single-core" lessons from the old design
LZhenHong Jul 15, 2026
3002985
🔒 fix(client): make XPCClient a true singleton with private init
LZhenHong Jul 15, 2026
8fe0d57
♻️ refactor(broadcaster): drop the unread isLocked notification payload
LZhenHong Jul 15, 2026
54d37d5
🐛 fix(engine): stop treating flagsChanged as an unlock trigger
LZhenHong Jul 15, 2026
da660ee
🔥 refactor(engine): narrow unused lock-timing accessors to internal
LZhenHong Jul 15, 2026
8515221
🔥 refactor: remove unwired settings-write and accessibility XPC surface
LZhenHong Jul 15, 2026
58bb09c
🔥 refactor(settings): drop model fields with no behavior behind them
LZhenHong Jul 15, 2026
90fedf2
🔥 refactor: remove unwired CLI-install scaffolding
LZhenHong Jul 15, 2026
0261ba8
fix(xpc): derive trusted team from agent
LZhenHong Jul 16, 2026
ec0d88d
fix(build): produce distributable app archive
LZhenHong Jul 16, 2026
929ebc7
fix(input): restrict lock to keyboard events
LZhenHong Jul 16, 2026
f33d481
📝 docs(xpc): add XPC implementation guide and link it from CLAUDE.md
LZhenHong Jul 16, 2026
49e1de7
feat(xpc): add negotiated agent replacement
LZhenHong Jul 16, 2026
5f90457
fix(xpc): enforce mutual peer authentication
LZhenHong Jul 16, 2026
b3fd4d2
feat(app): coordinate agent readiness and updates
LZhenHong Jul 16, 2026
57d0c44
docs(xpc): document agent lifecycle boundaries
LZhenHong Jul 16, 2026
967a7d0
fix(core): stabilize lock lifecycle handling
LZhenHong Jul 16, 2026
17d0133
refactor(app): remove presentation layer
LZhenHong Jul 17, 2026
50719d2
refactor(service): isolate LockEngine to MainActor with transactional…
LZhenHong Jul 17, 2026
8a0761d
feat(common): propagate settings coding errors across XPC boundary
LZhenHong Jul 17, 2026
8c5f125
refactor(agent): adopt MainActor isolation and implement currentSetti…
LZhenHong Jul 17, 2026
6816154
feat(app): add menu-bar presentation with snapshot-driven coordinator
LZhenHong Jul 17, 2026
a1ec383
fix(klock): handle settings fetch failure without fabricating a fallback
LZhenHong Jul 17, 2026
b394b2b
fix(agent): recover missing registration
LZhenHong Jul 17, 2026
070db78
chore(dev): add scoped registration reset
LZhenHong Jul 17, 2026
54ccf94
chore(project): normalize model test groups
LZhenHong Jul 17, 2026
94303c6
feat(klock): add CLI installation workflow
LZhenHong Jul 17, 2026
0a1022b
feat(core): add interactive lock requests
LZhenHong Jul 17, 2026
7e596cd
feat(klock): add Control-C unlock flow
LZhenHong Jul 17, 2026
abc4f35
feat(core): add authoritative lock snapshot
LZhenHong Jul 17, 2026
cddcc0d
feat(app-intents): expose shortcut actions
LZhenHong Jul 17, 2026
80b809e
feat(applescript): add scripting commands
LZhenHong Jul 17, 2026
dbc0658
feat(klock): add noninteractive lock mode
LZhenHong Jul 17, 2026
9682636
feat(xpc): authorize WidgetKit extension
LZhenHong Jul 17, 2026
120e656
feat(widget): add lock status widget
LZhenHong Jul 17, 2026
108e8b2
feat(control): add desired-state lock control
LZhenHong Jul 17, 2026
9738109
feat(klock): add JSON status output
LZhenHong Jul 17, 2026
b2c13ce
feat(core): add focus-owned lock generations
LZhenHong Jul 17, 2026
ba19588
feat(focus): add keyboard lock focus filter
LZhenHong Jul 17, 2026
b97b5b5
feat(services): add native keyboard actions
LZhenHong Jul 17, 2026
5a98272
feat(url): add keyboard automation deep links
LZhenHong Jul 17, 2026
1119795
fix(focus): align activation semantics
LZhenHong Jul 20, 2026
8eb06a6
test(focus): cover lock ownership lifecycle
LZhenHong Jul 20, 2026
86f799b
test(client): cover idempotent mutation retries
LZhenHong Jul 20, 2026
dbe9a50
feat(widget): add interactive lock actions
LZhenHong Jul 20, 2026
32e991d
feat(surfaces): centralize WidgetKit invalidation
LZhenHong Jul 20, 2026
ca18222
feat(automation): refresh system surfaces after mutations
LZhenHong Jul 20, 2026
6463d6b
docs(core): explain focus lock generation identity check
LZhenHong Jul 20, 2026
0b875be
style(core): apply SwiftFormat conventions
LZhenHong Jul 20, 2026
84cddbd
style(app): apply SwiftFormat conventions
LZhenHong Jul 20, 2026
deea144
style(agent,cli,widgets): apply SwiftFormat conventions
LZhenHong Jul 20, 2026
b310c4a
chore: remove .swift-version and .swiftformat
LZhenHong Jul 20, 2026
03f411f
fix(locking): suppress keyboard system controls
LZhenHong Jul 20, 2026
511b39a
chore(core): drop dead lock-timing helpers
LZhenHong Jul 20, 2026
47fdba4
docs: align long-lived docs with verified implementation
LZhenHong Aug 10, 2026
778f4ea
fix(service): log settings-store corruption instead of swallowing it
LZhenHong Aug 11, 2026
9012535
refactor(service): move AgentService into Service module for testability
LZhenHong Aug 11, 2026
698be0a
test(app): cover readiness and replacement coordinators
LZhenHong Aug 11, 2026
299a660
ci: add macOS build-and-test workflow
LZhenHong Aug 11, 2026
cbb9fe1
refactor(service): make LockEngine engine paths testable via injected…
LZhenHong Aug 11, 2026
a47815a
refactor(app): make forced replacement failure structurally impossible
LZhenHong Aug 11, 2026
31bf0fe
refactor(klock): extract argument parser and testable CLI core
LZhenHong Aug 11, 2026
0ce83e5
test(core): pin signing-requirement rejection against real signatures
LZhenHong Aug 11, 2026
aa9c099
feat(core,app): add atomic lock toggle and macOS 26 App Shortcuts
LZhenHong Aug 11, 2026
921fc29
feat(app): add locked notification with Unlock Now action
LZhenHong Aug 11, 2026
bb106f0
chore: ignore LLVM profraw coverage data
LZhenHong Aug 11, 2026
4626aa7
feat(klock): add register-agent command for first-run agent setup
LZhenHong Aug 11, 2026
380a64c
docs: record chronod reload restriction for non-container processes
LZhenHong Aug 11, 2026
5536bd2
fix(common): render hotkeys from the ASCII-capable keyboard layout
LZhenHong Aug 12, 2026
313ad2e
refactor(app): replace soft-deprecated activate(ignoringOtherApps:)
LZhenHong Aug 12, 2026
16c9f14
fix(service): mark LockEngine.eventMasks nonisolated
LZhenHong Aug 12, 2026
acdd19f
feat(service): post the locked notification from the Agent process
LZhenHong Aug 12, 2026
240a0e9
feat(app): confirm before quitting while the keyboard is locked
LZhenHong Aug 12, 2026
03c7974
feat(klock): release a created lock on termination signals
LZhenHong Aug 12, 2026
df2a79e
fix: align cross-surface UX consistency
LZhenHong Aug 12, 2026
f786b1d
fix(app): report Services request failures via the error out-pointer
LZhenHong Aug 12, 2026
8b55511
refactor(app): rename menu lock action to performDisplayedLockAction
LZhenHong Aug 12, 2026
bb699e4
feat(klock): add the toggle command
LZhenHong Aug 12, 2026
a04d55b
feat(klock): add the request-access command
LZhenHong Aug 12, 2026
efe2b8d
feat(klock): add status --snapshot output
LZhenHong Aug 12, 2026
3b76f6d
fix(klock): arbitrate termination during locking
LZhenHong Aug 12, 2026
c4c8312
fix(client): enforce selector protocol versions
LZhenHong Aug 12, 2026
77d2ac4
refactor(service): make AgentService own runtime
LZhenHong Aug 12, 2026
0fe2586
fix(services): publish errors before alerts
LZhenHong Aug 12, 2026
a9c8f1e
refactor(app): centralize shared projections
LZhenHong Aug 12, 2026
8ce933c
fix(security): require live refusal controls
LZhenHong Aug 12, 2026
85a7a5a
docs(xpc): align repaired runtime contracts
LZhenHong Aug 12, 2026
4516082
feat(app): add first-run safety check
LZhenHong Aug 12, 2026
744c56f
test: migrate suites to Swift Testing
LZhenHong Aug 12, 2026
f863813
docs: move editable settings beyond MVP
LZhenHong Aug 12, 2026
b3a09be
fix(core): enforce concurrency boundaries
LZhenHong Aug 12, 2026
7674a87
chore: apply SwiftFormat and normalize async try/await ordering
LZhenHong Aug 12, 2026
3eab2b7
ci: remove Claude Code review workflows
LZhenHong Aug 14, 2026
d672d01
test(services): poll for late failure presentation past the CI timing…
LZhenHong Aug 14, 2026
fa96cd0
test(services): gate late action completion on result publication
LZhenHong Aug 14, 2026
a9ee6c8
fix(client): keep interrupted mutation replies explicitly ambiguous
LZhenHong Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CI

on:
push:
pull_request:

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build-and-test:
runs-on: macos-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Show toolchain versions
run: |
xcodebuild -version
xcrun swift --version

- name: Core package tests
run: xcrun swift test --package-path Core

# CI runners carry no signing identities, so the app build and the app-level tests run
# with code signing disabled. The mutual XPC code-signing requirement is a runtime
# property exercised by signed local builds; its construction rules are covered by
# XPCCodeSigningRequirementTests in the Core package above.
- name: Build app, agent, CLI, and extensions
run: >-
xcodebuild -scheme KeyboardLocker -configuration Debug build
CODE_SIGNING_ALLOWED=NO

- name: App-level model tests
run: >-
xcodebuild -scheme KeyboardLocker -configuration Debug
-destination 'platform=macOS'
test -only-testing:KeyboardLockerModelTests
CODE_SIGNING_ALLOWED=NO
57 changes: 0 additions & 57 deletions .github/workflows/claude-code-review.yml

This file was deleted.

50 changes: 0 additions & 50 deletions .github/workflows/claude.yml

This file was deleted.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ iOSInjectionProject/
# macOS
.DS_Store

# LLVM coverage data dropped into the working directory by xcodebuild test runs
*.profraw

# xcode-build-server files
buildServer.json
.compile
Expand Down
1 change: 0 additions & 1 deletion .swift-version

This file was deleted.

96 changes: 0 additions & 96 deletions .swiftformat

This file was deleted.

20 changes: 13 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ Guidance for Claude Code when working in this repository.

## Project Overview

KeyboardLocker locks keyboard/mouse input via CGEventTap on macOS. Three-tier architecture over XPC (Mach service `io.lzhlovesjyq.keyboardlocker.agent`):
KeyboardLocker suppresses keyboard-originated events exposed through CGEventTap on macOS, including standard keys and system-defined controls such as volume, brightness, media playback, eject, and power. Mouse and trackpad input remain available. Hardware or system-reserved input that macOS does not expose to CGEventTap is outside this guarantee. Three-tier architecture over XPC (Mach service `io.lzhlovesjyq.keyboardlocker.agent`):

- **KeyboardLocker** (App, SwiftUI) — imports `Client`
- **KeyboardLocker** (menu-bar App, Agent lifecycle coordinator, and one-shot system actions) — imports `Client` and `SystemSurfaces`
- **KeyboardLockerAgent** (XPC service, runs the lock engine, holds Accessibility permission) — imports `Service`
- **klock** (CLI) — imports `Client`
- **Core** (Swift Package): `Common` (shared protocol/settings), `Client` (XPC client, App/CLI), `Service` (lock engine, Agent). `Client`/`Service` both `@_exported import Common`.
- **KeyboardLockerWidgets** (WidgetKit extension hosting Widget and macOS Control) — imports `Client` and `SystemSurfaces`
- **KeyboardLockerFocusIntents** (Focus Filter App Intents extension) — imports `Client` and `SystemSurfaces`
- **Core** (Swift Package): `Common` (shared protocol/settings), `Client` (XPC client, wrappers), `Service` (lock engine, Agent), `SystemSurfaces` (presentation-only Widget/Control identifiers and invalidation). `Client`/`Service` both `@_exported import Common`; `SystemSurfaces` is linked only by the containing App and its extensions, never by the Agent or CLI. (Verified on macOS 26: `chronod` ignores reload requests from processes that are not a registered extension container — `Ignoring restricted or unknown extension` — so linking `SystemSurfaces` from `klock` cannot actually refresh widgets.)

Bundle IDs: app `io.lzhlovesjyq.keyboardlocker`, agent `…​.agent`, CLI `…​.klock`.
Bundle IDs: app `io.lzhlovesjyq.keyboardlocker`, agent `…​.agent`, CLI `…​.klock`, WidgetKit extension `…​.widgets`, Focus App Intents extension `…​.focus-intents`.

## Architecture Contract — read first

**Before adding or changing any feature (App, CLI, Shortcuts, AppleScript, Widgets, …), read [docs/architecture.md](docs/architecture.md).** It defines the non-negotiable contract: the Agent is the single executor, all other surfaces are thin wrappers, the lock is one global state. When a change conflicts with it, the contract wins.

For task workflows and component details, see [docs/development.md](docs/development.md).
For the process/module boundary and complete XPC call flow, see [docs/xpc.md](docs/xpc.md). For task workflows and component details, see [docs/development.md](docs/development.md). For Shortcuts, AppleScript, CLI, Widget, and Control usage, see [docs/automation.md](docs/automation.md).

## Project-Specific Rules

Expand All @@ -31,6 +33,10 @@ These override the model's defaults — the rest of Swift style is left to stand
## Build & Format

```bash
xcodebuild -scheme KeyboardLocker -configuration Debug build # or KeyboardLockerAgent / klock / Core
swiftformat . # 2-space indent, alpha-sorted imports; run before committing
xcodebuild -scheme KeyboardLocker -configuration Debug build # or KeyboardLockerAgent / klock (Core has no shared Xcode scheme; use the SwiftPM commands below)
xcrun swift test --package-path Core # xcrun pins the Xcode toolchain; a bare `swift` may resolve to swiftly's older default, which cannot consume the current SDK
xcodebuild -scheme KeyboardLocker -configuration Debug -destination 'platform=macOS' test -only-testing:KeyboardLockerModelTests # app-level coordination tests (fake Client/lifecycle; no live SMAppService/XPC)
```

`Core/Package.swift` keeps Swift 5.10 source semantics. Code style is 2-space indent with
alpha-sorted imports.
6 changes: 6 additions & 0 deletions Core/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ let package = Package(
.library(name: "Common", targets: ["Common"]),
.library(name: "Client", targets: ["Client"]),
.library(name: "Service", targets: ["Service"]),
.library(name: "SystemSurfaces", targets: ["SystemSurfaces"]),
],
targets: [
.target(name: "Common"),
.target(name: "Client", dependencies: ["Common"]),
.target(name: "Service", dependencies: ["Common"]),
.target(name: "SystemSurfaces"),
.testTarget(name: "CommonTests", dependencies: ["Common"]),
.testTarget(name: "ClientTests", dependencies: ["Client"]),
.testTarget(name: "ServiceTests", dependencies: ["Service"]),
.testTarget(name: "SystemSurfacesTests", dependencies: ["SystemSurfaces"]),
]
)
20 changes: 20 additions & 0 deletions Core/Sources/Client/IdempotentMutationRetrier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// Retries a mutation only when its reply is lost — timed out, or dropped with a dying peer —
/// and resets the transport boundary before doing so. Callers use this only for operations
/// whose complete semantic effect is idempotent.
enum IdempotentMutationRetrier {
static func perform(
operation: @escaping @Sendable () async throws -> Void,
resetConnection: @escaping @Sendable () -> Void
) async throws {
do {
try await operation()
} catch XPCClientError.timedOut, is LostReplyError {
resetConnection()
do {
try await operation()
} catch XPCClientError.timedOut, is LostReplyError {
throw XPCClientError.operationOutcomeUnknown
}
}
}
}
Loading
Loading