Skip to content
Merged
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
20 changes: 18 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ zips or other generated artifacts.
AppKit patterns, explicit `NS*` types, and straightforward helpers rather than
heavy abstraction.

## GitHub CLI In Codex

`gh` is authenticated for the `pascalpp` account through the macOS keyring.
Sandboxed agent commands may not be able to read that keyring entry and can
report the token in `~/.config/gh/hosts.yml` as invalid even when
`gh auth status` works in the user's terminal.

Before concluding that GitHub CLI auth is missing, retry authenticated `gh`
commands outside the sandbox with approval/escalation. Do not run `gh auth
login`, `gh auth logout`, or edit `~/.config/gh/hosts.yml` unless the escalated
`gh auth status` check also fails or the user explicitly asks for reauth.

## PR Review Comment Resolution

When asked to address PR review comments, inspect the pull request associated
Expand All @@ -71,14 +83,18 @@ Use Xcode's command-line tools from the repository root:
```sh
xcodebuild -project "Free Ruler.xcodeproj" -scheme "Free Ruler" build
xcodebuild -project "Free Ruler.xcodeproj" -scheme "Free Ruler" test -only-testing:FreeRulerTests
yarn test
yarn test:unit
yarn test:ui
```

For focused test work, prefer the smallest relevant Xcode test invocation first,
then run the core/unit tests if the change affects shared behavior. Do not run
the UI tests unless the user explicitly asks for them.

The `package.json` scripts are for versioning and release automation, not the
normal test suite. `npm test` is intentionally not wired to the Xcode tests.
The `package.json` test scripts are aliases for the Xcode test commands:
`yarn test:unit` runs `FreeRulerTests`; `yarn test:ui` runs
`FreeRulerUITests`; `yarn test` runs both.

## App Behavior Notes

Expand Down
14 changes: 14 additions & 0 deletions Free Ruler.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@
50B1D3672D06100000B1D13A /* ResizeHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3682D06100000B1D13A /* ResizeHandleView.swift */; };
50B1D3692D06100000B1D13A /* ResizeHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3682D06100000B1D13A /* ResizeHandleView.swift */; };
50B1D36A2D06110000B1D13B /* UnitLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D36C2D06110000B1D13B /* UnitLabelView.swift */; };
50B1D3712D06120000B1D140 /* UITestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3702D06120000B1D140 /* UITestSupport.swift */; };
50B1D36B2D06110000B1D13B /* UnitLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D36C2D06110000B1D13B /* UnitLabelView.swift */; };
50B1D3722D06120000B1D140 /* UITestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3702D06120000B1D140 /* UITestSupport.swift */; };
50B1D3732D06120000B1D141 /* UITestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3702D06120000B1D140 /* UITestSupport.swift */; };
50B1D3752D06120000B1D141 /* UITestSupport+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3742D06120000B1D141 /* UITestSupport+App.swift */; };
50B1D3762D06120000B1D141 /* UITestSupport+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3742D06120000B1D141 /* UITestSupport+App.swift */; };
50C6D891228BDBAD0091F19E /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50C6D890228BDBAD0091F19E /* Images.xcassets */; };
50D7BEE7227D42FD0008B95E /* RulerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D7BEE6227D42FD0008B95E /* RulerController.swift */; };
50D7BEE9227D43270008B95E /* Ruler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D7BEE8227D43270008B95E /* Ruler.swift */; };
Expand Down Expand Up @@ -121,6 +126,8 @@
50B1D3642D06010100B1D139 /* AppStoreScreenshotPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreScreenshotPreview.swift; sourceTree = "<group>"; };
50B1D3682D06100000B1D13A /* ResizeHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeHandleView.swift; sourceTree = "<group>"; };
50B1D36C2D06110000B1D13B /* UnitLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitLabelView.swift; sourceTree = "<group>"; };
50B1D3702D06120000B1D140 /* UITestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestSupport.swift; sourceTree = "<group>"; };
50B1D3742D06120000B1D141 /* UITestSupport+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITestSupport+App.swift"; sourceTree = "<group>"; };
50C6D890228BDBAD0091F19E /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
50D7BEE6227D42FD0008B95E /* RulerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RulerController.swift; sourceTree = "<group>"; };
50D7BEE8227D43270008B95E /* Ruler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Ruler.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -245,6 +252,8 @@
507FED5F2280E13200BD77DC /* Prefs.swift */,
50008B6022846FCD001E3EE4 /* Notifications.swift */,
6F4102882260712F00F06A10 /* AppDelegate.swift */,
50B1D3702D06120000B1D140 /* UITestSupport.swift */,
50B1D3742D06120000B1D141 /* UITestSupport+App.swift */,
50B1D3522D05D00100B1D135 /* RulerCursorController.swift */,
50B1D3502D05D00000B1D135 /* MouseTickTimerPolicy.swift */,
50B149002D04A00000149C0D /* HotkeyBezel.swift */,
Expand Down Expand Up @@ -459,6 +468,7 @@
buildActionMask = 2147483647;
files = (
50A147012D03C00000147C0D /* FreeRulerUITests.swift in Sources */,
50B1D3732D06120000B1D141 /* UITestSupport.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -481,6 +491,8 @@
50D7BEE7227D42FD0008B95E /* RulerController.swift in Sources */,
5012CAAD226AB09000BD9565 /* VerticalRule.swift in Sources */,
6F4102892260712F00F06A10 /* AppDelegate.swift in Sources */,
50B1D3712D06120000B1D140 /* UITestSupport.swift in Sources */,
50B1D3752D06120000B1D141 /* UITestSupport+App.swift in Sources */,
50D7BEED227D5C810008B95E /* RuleView.swift in Sources */,
50B1D3672D06100000B1D13A /* ResizeHandleView.swift in Sources */,
50B1D36A2D06110000B1D13B /* UnitLabelView.swift in Sources */,
Expand All @@ -507,6 +519,8 @@
0C56237FE08F4155F0B63FFB /* RulerController.swift in Sources */,
5B52743D84FFD5EBF40B8558 /* VerticalRule.swift in Sources */,
23E2B80975F07733EB6CF5DB /* AppDelegate.swift in Sources */,
50B1D3722D06120000B1D140 /* UITestSupport.swift in Sources */,
50B1D3762D06120000B1D141 /* UITestSupport+App.swift in Sources */,
9A082BBC4A583513B0858C41 /* RuleView.swift in Sources */,
50B1D3692D06100000B1D13A /* ResizeHandleView.swift in Sources */,
50B1D36B2D06110000B1D13B /* UnitLabelView.swift in Sources */,
Expand Down
59 changes: 9 additions & 50 deletions Free Ruler/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,6 @@ import Darwin
import Sparkle
#endif

let env = ProcessInfo.processInfo.environment
let UI_TESTS = env["FREE_RULER_UI_TESTS"] != nil
let UI_TEST_CURSOR_STATE_NAME: String? = {
guard UI_TESTS,
let name = env["FREE_RULER_UI_TEST_CURSOR_STATE_NAME"] else { return nil }

let lastPathComponent = URL(fileURLWithPath: name).lastPathComponent
guard !lastPathComponent.isEmpty, lastPathComponent == name else { return nil }

return name
}()

func writeUITestCursorState(_ value: String) {
guard let name = UI_TEST_CURSOR_STATE_NAME else { return }

let url = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent(name)
try? value.write(to: url, atomically: true, encoding: .utf8)
}

private enum HotkeyBezelLocalizationKey: String {
case rulersFloated = "HotkeyBezel.RulersFloated"
case rulersUnfloated = "HotkeyBezel.RulersUnfloated"
Expand Down Expand Up @@ -117,6 +97,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {

var preferencesController: PreferencesController? = nil
private let hotkeyBezel = HotkeyBezel()
private var uiTestSupport: UITestSupport?
#if SPARKLE
private var updaterController: SPUStandardUpdaterController?
#endif
Expand All @@ -133,13 +114,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
#endif

if UI_TESTS {
resetStateForUITests()
writeUITestCursorState("none")
}
uiTestSupport = UITestSupport.installIfNeeded()
uiTestSupport?.resetApplicationState()
uiTestSupport?.writeCursorState("none")

subscribeToPrefs()
updateDisplay()
uiTestSupport?.writePreferencesState()
#if SPARKLE
configureUpdater()
#endif
Expand Down Expand Up @@ -222,15 +203,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
prefs.observe(\Prefs.unit, options: .new) { prefs, changed in
self.updateUnitMenu()
self.redrawRulers()
self.uiTestSupport?.writePreferencesState()
},
prefs.observe(\Prefs.floatRulers, options: .new) { prefs, changed in
self.updateFloatRulersMenuItem()
self.uiTestSupport?.writePreferencesState()
},
prefs.observe(\Prefs.groupRulers, options: .new) { prefs, changed in
self.updateGroupRulersMenuItem()
self.uiTestSupport?.writePreferencesState()
},
prefs.observe(\Prefs.rulerShadow, options: .new) { prefs, changed in
self.updateRulerShadowMenuItem()
self.uiTestSupport?.writePreferencesState()
},
prefs.observe(\Prefs.rulerColor, options: .new) { prefs, changed in
self.redrawRulers()
Expand Down Expand Up @@ -272,32 +257,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
rulerShadowMenuItem?.state = prefs.rulerShadow ? .on : .off
}

private func resetStateForUITests() {
let defaults = UserDefaults.standard
[
"groupRulers",
"floatRulers",
"rulerShadow",
"foregroundOpacity",
"backgroundOpacity",
"rulerColor",
"unit",
"zeroCorner",
"NSWindow Frame horizontal-ruler",
"NSWindow Frame vertical-ruler",
"NSWindow Frame preferencesWindow",
].forEach(defaults.removeObject(forKey:))

prefs.groupRulers = true
prefs.floatRulers = true
prefs.rulerShadow = false
prefs.foregroundOpacity = 90
prefs.backgroundOpacity = 50
prefs.rulerColor = Prefs.defaultRulerFillColor
prefs.unit = .pixels
prefs.zeroCorner = Prefs.defaultZeroCorner
}

func createRulersIfNeeded() {
guard rulers.isEmpty else { return }

Expand Down
2 changes: 1 addition & 1 deletion Free Ruler/PreferencesController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ func configureOpaqueColorPicking() {
let colorPanel = NSColorPanel.shared
colorPanel.identifier = rulerColorPanelIdentifier
colorPanel.setAccessibilityIdentifier(rulerColorPanelIdentifier.rawValue)
if UI_TESTS {
if UITestSupport.isEnabled {
colorPanel.setAccessibilityValue(rulerColorPanelOpaqueAccessibilityValue)
}
setColorPickingIgnoresAlpha(true)
Expand Down
2 changes: 1 addition & 1 deletion Free Ruler/RulerCursorController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,6 @@ final class RulerCursorController {

private extension RulerCursorController.CursorStyle {
func writeUITestStateIfNeeded() {
writeUITestCursorState(uiTestStateValue)
UITestSupport.current?.writeCursorState(uiTestStateValue)
}
}
57 changes: 57 additions & 0 deletions Free Ruler/UITestSupport+App.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Foundation

extension UITestSupport {
func resetApplicationState() {
let defaults = UserDefaults.standard
[
"groupRulers",
"floatRulers",
"rulerShadow",
"foregroundOpacity",
"backgroundOpacity",
"rulerColor",
"unit",
"zeroCorner",
"NSWindow Frame horizontal-ruler",
"NSWindow Frame vertical-ruler",
"NSWindow Frame preferencesWindow",
].forEach(defaults.removeObject(forKey:))

prefs.groupRulers = true
prefs.floatRulers = true
prefs.rulerShadow = false
prefs.foregroundOpacity = 90
prefs.backgroundOpacity = 50
prefs.rulerColor = Prefs.defaultRulerFillColor
prefs.unit = .pixels
prefs.zeroCorner = Prefs.defaultZeroCorner
}

func writePreferencesState() {
let state = [
"floatRulers": boolStateValue(prefs.floatRulers),
"groupRulers": boolStateValue(prefs.groupRulers),
"rulerShadow": boolStateValue(prefs.rulerShadow),
"unit": unitStateValue(prefs.unit),
]
guard let data = try? JSONSerialization.data(withJSONObject: state, options: [.sortedKeys]),
let value = String(data: data, encoding: .utf8) else { return }

writeState(value, to: preferencesStateURL)
}

private func boolStateValue(_ value: Bool) -> String {
return value ? "true" : "false"
}

private func unitStateValue(_ unit: Unit) -> String {
switch unit {
case .pixels:
return "px"
case .millimeters:
return "mm"
case .inches:
return "in"
}
}
}
Loading