Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@ fastlane/screenshots
Documentation/docs
/Fluor/Sparkle.framework
/Fluor/Sparkle.framework.dSYM
.DS_Store
74 changes: 74 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build Commands

```bash
# Build (requires Xcode; use DEVELOPER_DIR if xcode-select points to CommandLineTools)
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -scheme Fluor -configuration Debug build

# Build without code signing (for development without matching certificates)
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -scheme Fluor -configuration Debug build CODE_SIGN_IDENTITY="-" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO
```

There are no tests in this project. The project uses Xcode (not Swift Package Manager) as its build system.

## Architecture

Fluor is a macOS status bar app (Swift 5 / AppKit) that switches the keyboard's fn-key behavior (media keys vs F1-F12) based on the active application.

### Core Flow

```
AppDelegate → StatusMenuController (status bar item, Main.xib)
├── BehaviorController — monitors active app, switches fn-key mode via FKeyManager
├── MenuItemsController — manages menu UI (embeds 3 child ViewControllers)
└── Window Controllers — lazy-loaded via StoryboardInstantiable protocol
├── PreferencesWindowController (Preferences.storyboard)
├── RulesEditorWindowController (RulesEditor.storyboard)
├── RunningAppWindowController (RunningApps.storyboard)
└── AboutWindowController (About.storyboard)
```

### Key Components

- **FKeyManager** (`Misc/FKeyManager.swift`) — IOKit interface that reads/writes the fn-key mode via `IOHIDSetCFTypeParameter` and `IORegistryEntryCreateCFProperty`. Requires Accessibility permissions.
- **BehaviorController** (`Controllers/BehaviorController.swift`) — Core logic: listens to NSWorkspace active-app changes, determines target FKeyMode from stored rules, and calls FKeyManager. Also handles Fn-key press detection for hybrid/key switch methods.
- **AppManager** (`Models/AppManager.swift`) — Singleton storing all persistent state via `@Defaults` property wrappers (from DefaultsWrapper SPM package). Holds the rule set, default mode, switch method, UI preferences.
- **StatusMenuController** (`Controllers/StatusMenuController.swift`) — Owns the NSStatusItem, delegates to BehaviorController and MenuItemsController, manages window controller lifecycle.

### Communication Pattern

Components communicate via paired Notification observer/poster protocols defined in `Protocols/NotificationHelpers.swift`:
- `BehaviorDidChange` — fn-key behavior changed for an app
- `SwitchMethodDidChange` — user changed switch method (window/hybrid/key)
- `MenuControlObserver/Poster` — menu open/close coordination

### Three Switch Methods (enum `SwitchMethod`)

1. **Window** — auto-switch based on frontmost app's stored rule
2. **Hybrid** — window mode + Fn-key press toggles current app's behavior
3. **Key** — Fn-key press toggles global default mode only

### Objective-C Interop

Bridged via `Fluor-Bridging-Header.h`:
- **LaunchAtLoginController** — manages login item registration
- **PFMoveApplication** — prompts to move app to /Applications (RELEASE builds only)

### SPM Dependencies

- **DefaultsWrapper** — `@Defaults` property wrapper for typed UserDefaults access
- **Sparkle** (v2.x) — auto-update framework (bindings go through `self.updater.*` key paths in Preferences.storyboard)
- **CoreGeometry** / **SmoothOperators** — geometry utilities and operator extensions

## Gotchas

- **KVO + `@objc let` properties**: Storyboard bindings using nested key paths through `@objc let` stored properties (e.g., `objectValue.url.path` where `url` is `let`) crash on modern macOS — KVO can't create an ivar setter for constants. Use a `@objc dynamic var` computed wrapper instead, or avoid nested paths through `let` properties.
- **Window display pattern**: Use `makeKeyAndOrderFront(self)` + `makeMain()` + `NSApp.activate(ignoringOtherApps:)` to show windows. Do not use `orderFrontRegardless()`.
- **Storyboard debugging**: When investigating storyboard-related crashes, always request the crash backtrace — code review alone is insufficient for KVO/binding issues.

## Commit Guidelines

- Do not include "Co-Authored-By" lines or any Claude/AI attribution in commit messages.
61 changes: 26 additions & 35 deletions Fluor.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -42,8 +42,7 @@
3F8F93BB1EEAC9B900FCE91F /* RuleCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8F93BA1EEAC9B900FCE91F /* RuleCellView.swift */; };
3F8F93BD1EEAF1EB00FCE91F /* RuleValueTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8F93BC1EEAF1EB00FCE91F /* RuleValueTransformer.swift */; };
3F9EDD2A245C7BAF0047D1AC /* MenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F9EDD29245C7BAF0047D1AC /* MenuItemView.swift */; };
3FBE4C262222A3C600782647 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3FBE4C202222A22200782647 /* Sparkle.framework */; };
3FBE4C272222A3C600782647 /* Sparkle.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3FBE4C202222A22200782647 /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3FBE4C262222A3C600782647 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 3FBE4C302222A3C600782647 /* Sparkle */; };
3FBE4C292222AB0200782647 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = 3FBE4C282222AB0200782647 /* dsa_pub.pem */; };
3FC44EFA1D7F169A0065D433 /* Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC44EF91D7F169A0065D433 /* Enums.swift */; };
3FC44EFC1D7F16CB0065D433 /* Items.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC44EFB1D7F16CB0065D433 /* Items.swift */; };
Expand All @@ -68,20 +67,6 @@
3FF655CA20751D1600C8D2FC /* PFMoveApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 3FF655C820751D1600C8D2FC /* PFMoveApplication.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
3FBE4C252222A25200782647 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
3FBE4C272222A3C600782647 /* Sparkle.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
3DF3612324C206AD00231BF5 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/MainMenu.strings"; sourceTree = "<group>"; };
3DF3612424C206AD00231BF5 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/RunningApps.strings"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -126,7 +111,6 @@
3F8F93BA1EEAC9B900FCE91F /* RuleCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuleCellView.swift; sourceTree = "<group>"; };
3F8F93BC1EEAF1EB00FCE91F /* RuleValueTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuleValueTransformer.swift; sourceTree = "<group>"; };
3F9EDD29245C7BAF0047D1AC /* MenuItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemView.swift; sourceTree = "<group>"; };
3FBE4C202222A22200782647 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = ../../../../pyroh/Dev/Projects/Fluor/Fluor/Sparkle.framework; sourceTree = "<group>"; };
3FBE4C282222AB0200782647 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dsa_pub.pem; sourceTree = "<group>"; };
3FC44EF91D7F169A0065D433 /* Enums.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Enums.swift; sourceTree = "<group>"; };
3FC44EFB1D7F16CB0065D433 /* Items.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Items.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -165,7 +149,7 @@
files = (
3FE7B6DB245DF8450027DB39 /* SmoothOperators in Frameworks */,
3F16ECDD23E9D1AC008BC89A /* DefaultsWrapper in Frameworks */,
3FBE4C262222A3C600782647 /* Sparkle.framework in Frameworks */,
3FBE4C262222A3C600782647 /* Sparkle in Frameworks */,
3F15629F23FEDD0000CD0773 /* CoreGeometry in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -279,7 +263,6 @@
3F7291EC2025BD1E005C9B70 /* Frameworks */ = {
isa = PBXGroup;
children = (
3FBE4C202222A22200782647 /* Sparkle.framework */,
);
name = Frameworks;
sourceTree = "<group>";
Expand Down Expand Up @@ -347,7 +330,6 @@
3FCD169E1D79793600C57B22 /* Frameworks */,
3FCD169F1D79793600C57B22 /* Resources */,
3FD11B2F207A320800742415 /* Run Script */,
3FBE4C252222A25200782647 /* Embed Frameworks */,
);
buildRules = (
);
Expand All @@ -358,6 +340,7 @@
3F16ECDC23E9D1AC008BC89A /* DefaultsWrapper */,
3F15629E23FEDD0000CD0773 /* CoreGeometry */,
3FE7B6DA245DF8450027DB39 /* SmoothOperators */,
3FBE4C302222A3C600782647 /* Sparkle */,
);
productName = Fluor;
productReference = 3FCD16A11D79793600C57B22 /* Fluor.app */;
Expand Down Expand Up @@ -403,6 +386,7 @@
3F16ECDB23E9D1AC008BC89A /* XCRemoteSwiftPackageReference "DefaultsWrapper" */,
3F15629D23FEDD0000CD0773 /* XCRemoteSwiftPackageReference "CoreGeometry" */,
3FE7B6D9245DF8450027DB39 /* XCRemoteSwiftPackageReference "SmoothOperators" */,
3FBE4C2F2222A3C600782647 /* XCRemoteSwiftPackageReference "Sparkle" */,
);
productRefGroup = 3FCD16A21D79793600C57B22 /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -626,7 +610,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.12;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
Expand Down Expand Up @@ -680,7 +664,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.12;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand All @@ -699,18 +683,15 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = Q2E884V952;
DEVELOPMENT_TEAM = 87NLB3L2H3;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Fluor",
);
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = Fluor/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.12;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MARKETING_VERSION = 2.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.pyrolyse.Fluor;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -733,18 +714,15 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = Q2E884V952;
DEVELOPMENT_TEAM = 87NLB3L2H3;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Fluor",
);
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = Fluor/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.12;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MARKETING_VERSION = 2.5.0;
OTHER_SWIFT_FLAGS = "-DRELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.pyrolyse.Fluor;
Expand Down Expand Up @@ -798,6 +776,14 @@
minimumVersion = 1.1.0;
};
};
3FBE4C2F2222A3C600782647 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sparkle-project/Sparkle.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.0.0;
};
};
3FE7B6D9245DF8450027DB39 /* XCRemoteSwiftPackageReference "SmoothOperators" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Pyroh/SmoothOperators.git";
Expand All @@ -819,6 +805,11 @@
package = 3F16ECDB23E9D1AC008BC89A /* XCRemoteSwiftPackageReference "DefaultsWrapper" */;
productName = DefaultsWrapper;
};
3FBE4C302222A3C600782647 /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
package = 3FBE4C2F2222A3C600782647 /* XCRemoteSwiftPackageReference "Sparkle" */;
productName = Sparkle;
};
3FE7B6DA245DF8450027DB39 /* SmoothOperators */ = {
isa = XCSwiftPackageProductDependency;
package = 3FE7B6D9245DF8450027DB39 /* XCRemoteSwiftPackageReference "SmoothOperators" */;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 1 addition & 37 deletions Fluor.xcodeproj/xcshareddata/xcschemes/Fluor.xcscheme
Original file line number Diff line number Diff line change
@@ -1,28 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0930"
version = "1.7">
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3FCD16A01D79793600C57B22"
BuildableName = "Fluor.app"
BlueprintName = "Fluor"
ReferencedContainer = "container:Fluor.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
Expand Down Expand Up @@ -109,23 +91,5 @@
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
<PostActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3FCD16A01D79793600C57B22"
BuildableName = "Fluor.app"
BlueprintName = "Fluor"
ReferencedContainer = "container:Fluor.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PostActions>
</ArchiveAction>
</Scheme>
Loading