Skip to content

feat(rules): add one-shot switch action for app/URL rules#22

Merged
BlackHole1 merged 2 commits into
mainfrom
support-switfch
Jun 15, 2026
Merged

feat(rules): add one-shot switch action for app/URL rules#22
BlackHole1 merged 2 commits into
mainfrom
support-switfch

Conversation

@BlackHole1

@BlackHole1 BlackHole1 commented Jun 15, 2026

Copy link
Copy Markdown
Member

Per-app and per-URL rules could only lock a source — continuously re-applied while the rule is in force. Some workflows just want to land on a source when an app or page becomes active, then stay free to change it. This adds a second action, switch, as a fourth app mode (.switched) and a URL RuleAction (.switchOnce); the global default stays lock-only.

The engine fires the one-shot exactly once per genuine entry via an in-memory transition key, with a separate slot for a launcher overlay's own switch so an excursion never re-yanks the underlying app. A switch installs no standing target, so the user is never reverted after moving away.

Import/export carries the new action: app rules through their mode, URL rules through an added action field with lenient decoding so existing v1.x UserDefaults and pre-release .lockime backups still load (a missing action defaults to lock).

Closed: #20

Per-app and per-URL rules could only lock a source — continuously
re-applied while the rule is in force. Some workflows just want to
land on a source when an app or page becomes active, then stay free
to change it. This adds a second action, switch, as a fourth app
mode (.switched) and a URL RuleAction (.switchOnce); the global
default stays lock-only.

The engine fires the one-shot exactly once per genuine entry via an
in-memory transition key, with a separate slot for a launcher
overlay's own switch so an excursion never re-yanks the underlying
app. A switch installs no standing target, so the user is never
reverted after moving away.

Import/export carries the new action: app rules through their mode,
URL rules through an added action field with lenient decoding so
existing v1.x UserDefaults and pre-release .lockime backups still
load (a missing action defaults to lock).

Signed-off-by: Kevin Cui <bh@bugs.cc>
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5f2b10f4-04ac-4426-94b5-0371b4acf8fa

📥 Commits

Reviewing files that changed from the base of the PR and between c027041 and 4fa1dfa.

📒 Files selected for processing (2)
  • Sources/LockIMEKit/LockEngine/LockEngine.swift
  • Tests/LockIMEKitTests/LockEngineTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/LockIMEKit/LockEngine/LockEngine.swift

Summary by CodeRabbit

  • New Features

    • Added "Switch" mode for per-app and per-URL rules: switch once to the specified input source on app/URL activation, then allow manual changes. Lock mode (default) continues to re-apply the source when it drifts.
    • Settings now include "Switch to" pickers alongside "Lock to" options for rule configuration.
  • Documentation

    • Updated design guide and multi-language READMEs to explain lock vs. switch behavior for rules.

Walkthrough

This PR adds a "switch once" action as an alternative to continuous "locking" for per-app and per-URL input source rules, addressing the user request to switch to other input methods rather than remaining permanently locked. A new RuleAction enum (.lock / .switchOnce) and AppRuleMode.pinsSource property are added to the data model. LockResolution gains a switchOnce case; RuleResolver.resolve routes .switched app rules and URL rules with .switchOnce action to it. LockController.switchOnce implements the one-shot switch primitive. LockEngine introduces a SwitchKey deduplication mechanism with separate slots for launcher-overlay excursions. The settings UI adds "Switch to" pickers for both app and URL rule rows. ConfigBackup and ImportPlan are extended to persist and import the action field with backward-compatible decoding. Localization strings, documentation, and comprehensive tests are added throughout.

Possibly Related PRs

  • oomol-lab/LockIME#18: Both PRs modify the backup/import pipeline (ConfigBackup, ImportPlan) to extend format and merge logic for URL-rule data, with this PR adding per-URL "lock vs switch" action persistence alongside the existing locked-source field.

  • oomol-lab/LockIME#11: Both PRs modify LockEngine's rule-resolution and reevaluation paths—this PR adds action-aware .switchOnce handling with URL-matched actions, while the related PR attributes rules to launcher overlays and isolates URL context during overlays—so the underlying locking and activation logic intersects.

  • oomol-lab/LockIME#13: Both PRs modify core rule-resolution plumbing, especially RuleResolver.swift (LockResolution / RuleSource metadata) and related LockEngine activation-event context, so this PR's switch action behavior is tightly coupled to the related PR's rule-source and matched-host propagation infrastructure.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main feature added: a one-shot switch action for app and URL rules, following the required conventional commit format.
Description check ✅ Passed The description comprehensively explains the new switch action feature, implementation approach, and backward compatibility considerations, directly relating to the changeset.
Linked Issues check ✅ Passed The PR implementation fully addresses issue #20 by adding one-shot switch functionality that allows users to set a default source on app activation while retaining freedom to change it manually.
Out of Scope Changes check ✅ Passed All changes are focused on implementing the one-shot switch action feature and its supporting infrastructure (UI, persistence, rules engine, tests, documentation).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch support-switfch

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

98-100: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the XcodeGen/xcbeautify sentence inline.

The leading + at the start of line 100 will render as a list item in Markdown, breaking the paragraph. Please fold xcbeautify back into the same sentence.

🛠 Proposed fix
-Requires Xcode 26+ (the app itself targets macOS 14+), and
-[XcodeGen](https://github.com/yonaskolb/XcodeGen)
-+ [xcbeautify](https://github.com/cpisciotta/xcbeautify) (`brew install xcodegen xcbeautify`).
+Requires Xcode 26+ (the app itself targets macOS 14+), and [XcodeGen](https://github.com/yonaskolb/XcodeGen) + [xcbeautify](https://github.com/cpisciotta/xcbeautify) (`brew install xcodegen xcbeautify`).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 98 - 100, The `+` character at the beginning of the
line containing `[xcbeautify]` is being interpreted as a Markdown list marker,
which breaks the paragraph flow. Remove the line break before the `+` and fold
the `xcbeautify` installation instruction back into the same sentence as the
XcodeGen reference, keeping the entire installation instruction as a continuous
paragraph without list markers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/LockIMEKit/LockEngine/LockEngine.swift`:
- Around line 241-252: The `fireSwitchOnceIfNeeded` function unconditionally
assigns `slot = key` after calling `controller.switchOnce(...)`, but since
`switchOnce` can early-return without actually performing the switch when the
current source is unknown, this marks the one-shot as already fired even though
no switch occurred. Only assign `slot = key` after `controller.switchOnce(...)`
completes successfully by checking the return value of `switchOnce` and only
updating `slot` when the switch actually executed, rather than updating it
unconditionally.

In `@Tests/LockIMEKitTests/LockEngineTests.swift`:
- Around line 801-834: The test switchActivationReasons has a mismatch between
its intent and assertions in the URL switch section. The test name indicates URL
switches should be logged as .urlMatched, but the URL rule branch only asserts
.startupApplied after calling apply on engine2. To fix this, add a re-activation
step (such as calling monitor2.activate() with the Safari bundle ID) after
applying the configuration to trigger the URL matching behavior, then update the
assertion from events2.last?.reason == .startupApplied to events2.last?.reason
== .urlMatched to properly validate that URL switches are logged with the
correct activation reason.

---

Outside diff comments:
In `@README.md`:
- Around line 98-100: The `+` character at the beginning of the line containing
`[xcbeautify]` is being interpreted as a Markdown list marker, which breaks the
paragraph flow. Remove the line break before the `+` and fold the `xcbeautify`
installation instruction back into the same sentence as the XcodeGen reference,
keeping the entire installation instruction as a continuous paragraph without
list markers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0c9f1310-8e20-464c-8e1f-6622628c6fed

📥 Commits

Reviewing files that changed from the base of the PR and between f513fd3 and c027041.

📒 Files selected for processing (28)
  • README.md
  • Sources/LockIME/AppState.swift
  • Sources/LockIME/Localizable.xcstrings
  • Sources/LockIME/UI/Settings/AppRulesSettingsPane.swift
  • Sources/LockIME/UI/Settings/ImportReviewSheet.swift
  • Sources/LockIME/UI/Settings/URLRulesSettingsPane.swift
  • Sources/LockIMEKit/Backup/ConfigBackup.swift
  • Sources/LockIMEKit/Backup/ImportPlan.swift
  • Sources/LockIMEKit/LockEngine/LockController.swift
  • Sources/LockIMEKit/LockEngine/LockEngine.swift
  • Sources/LockIMEKit/Rules/LockConfiguration.swift
  • Sources/LockIMEKit/Rules/RuleResolver.swift
  • Tests/LockIMEKitTests/ConfigBackupTests.swift
  • Tests/LockIMEKitTests/ImportPlanTests.swift
  • Tests/LockIMEKitTests/LockConfigurationTests.swift
  • Tests/LockIMEKitTests/LockControllerTests.swift
  • Tests/LockIMEKitTests/LockEngineTests.swift
  • Tests/LockIMEKitTests/RuleResolverTests.swift
  • Tests/LockIMEKitTests/RuleStoreTests.swift
  • docs/DESIGN.md
  • docs/README/README.de.md
  • docs/README/README.es.md
  • docs/README/README.fr.md
  • docs/README/README.ja.md
  • docs/README/README.pt.md
  • docs/README/README.ru.md
  • docs/README/README.zh-CN.md
  • docs/README/README.zh-TW.md

Comment thread Sources/LockIMEKit/LockEngine/LockEngine.swift
Comment thread Tests/LockIMEKitTests/LockEngineTests.swift
fireSwitchOnceIfNeeded marked the one-shot as fired even when
LockController.switchOnce early-returned because the current input
source could not be read (a transient TIS failure), permanently
suppressing the intended switch for that entry. Guard on a readable
source before consuming the key, so the one-shot stays eligible for
the next reevaluation. When the source is known but already equals the
target, the switch still no-ops and the key is consumed, so a user who
later moves away from an app entered already on target is not re-yanked.

Also fix a test whose name promised a URL .urlMatched assertion but
only checked the apply-driven reason: add a navigation step that
re-resolves to a different URL switch rule and asserts .urlMatched.

Signed-off-by: Kevin Cui <bh@bugs.cc>
@BlackHole1 BlackHole1 merged commit 6a74a92 into main Jun 15, 2026
3 checks passed
@BlackHole1 BlackHole1 deleted the support-switfch branch June 15, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Not able to switch to other IME

1 participant