From 35c66c33b195b4e5134e74538b66fc6dd1ed4891 Mon Sep 17 00:00:00 2001 From: abue-ammar Date: Sun, 9 Aug 2026 00:38:33 +0600 Subject: [PATCH] fomratter issues fixed --- .swift-format | 28 + .vscode/extensions.json | 2 +- .vscode/settings.json | 9 +- .vscode/tasks.json | 10 +- Scripts/format.sh | 49 ++ Scripts/lint.sh | 2 +- Scripts/run-tests.sh | 12 +- Scripts/sync-lsp.sh | 35 + Tests/calc-test.swift | 2 +- Tests/callout-test.swift | 3 +- Tests/clipboard-test.swift | 2 +- Tests/fuzz-test.swift | 19 +- Tests/hotkey-test.swift | 15 +- Tests/palette-selection-test.swift | 8 +- Tests/ranking-test.swift | 3 +- Tests/raycast-test.swift | 15 +- Tests/scopes-test.swift | 2 +- Tests/snippets-test.swift | 614 ++++++++++++------ Tests/uninstall-test.swift | 16 +- Tests/window-command-test.swift | 21 +- Tinycast/App/AppCore.swift | 31 +- .../Backup/Model/RaycastV1Decoder.swift | 3 +- .../Backup/Model/SettingsBackup.swift | 3 +- .../Backup/Model/SettingsBackupCoverage.swift | 4 +- .../Backup/Service/BackupActions.swift | 7 +- .../Backup/Service/RaycastImportV2.swift | 9 +- .../Backup/Settings/BackupSettingsView.swift | 3 +- .../Calculator/Model/CalcDateTime.swift | 38 +- .../Calculator/Model/CalcEngine.swift | 15 +- .../Calculator/Model/CalcFormatter.swift | 3 +- .../Calculator/Model/CalcParser.swift | 3 +- .../Calculator/Model/CalcQuantity.swift | 6 +- .../Service/CalculatorHistoryStore.swift | 3 +- .../Settings/MiscellaneousSettingsView.swift | 4 +- .../UI/CalculatorHistoryScreen.swift | 7 +- .../Clipboard/Service/ClipboardManager.swift | 3 +- .../Features/Clipboard/Service/Paster.swift | 3 +- .../Clipboard/UI/ClipboardScreen.swift | 3 +- .../Features/Emoji/Service/EmojiIndex.swift | 3 +- .../Emoji/Service/FrequentEmojiStore.swift | 3 +- Tinycast/Features/Emoji/UI/EmojiScreen.swift | 7 +- .../HotKeys/Service/HotKeyManager.swift | 3 +- .../Service/ShortcutCaptureSession.swift | 16 +- .../HotKeys/UI/ShortcutRecorderPopover.swift | 3 +- .../Launcher/Model/LauncherRankingStore.swift | 3 +- .../Features/Launcher/Service/AppIndex.swift | 9 +- .../Launcher/Service/AppLauncher.swift | 3 +- .../Service/SettingsPaneScanner.swift | 3 +- .../Features/Onboarding/OnboardingView.swift | 6 +- .../Features/Quicklinks/Model/Quicklink.swift | 3 +- .../Quicklinks/Model/QuicklinkStore.swift | 11 +- .../Settings/QuicklinksSettingsView.swift | 6 +- .../Quicklinks/UI/QuicklinkListScreen.swift | 11 +- .../Snippets/Model/SnippetKeywordPolicy.swift | 3 +- .../Snippets/Model/SnippetRepository.swift | 24 +- .../Model/SnippetTemplateEngine.swift | 25 +- .../Snippets/Model/SnippetsStore.swift | 24 +- .../Service/SnippetKeywordListener.swift | 24 +- .../Service/SnippetTextInjector.swift | 3 +- .../Settings/SnippetsSettingsView.swift | 3 +- .../UI/SnippetExpansionCoordinator.swift | 7 +- .../Service/SystemActionRunner.swift | 64 +- .../Uninstall/Model/UninstallProtection.swift | 3 +- .../Uninstall/Service/UninstallScanner.swift | 8 +- .../Uninstall/Service/UninstallSession.swift | 4 +- .../Uninstall/UI/UninstallScreen.swift | 3 +- .../WindowManagement/WindowLayout.swift | 21 +- .../WindowManagement/WindowMover.swift | 7 +- Tinycast/Palette/PaletteCoordinator.swift | 1 - Tinycast/Palette/PalettePanel.swift | 6 +- .../Palette/PaletteWindowController.swift | 6 +- Tinycast/Palette/RootPaletteView.swift | 3 +- Tinycast/Platform/Images/IconCache.swift | 6 +- Tinycast/Platform/Signposts.swift | 4 +- Tinycast/Windows/About/AboutView.swift | 9 +- .../Windows/Dialog/DialogController.swift | 7 +- docs/decisions.md | 10 + docs/development.md | 53 +- 78 files changed, 982 insertions(+), 451 deletions(-) create mode 100644 .swift-format create mode 100755 Scripts/format.sh create mode 100755 Scripts/sync-lsp.sh diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000..613f6d8d --- /dev/null +++ b/.swift-format @@ -0,0 +1,28 @@ +{ + "version": 1, + "indentation": { + "spaces": 4 + }, + "lineLength": 110, + "respectsExistingLineBreaks": true, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": false, + "indentSwitchCaseLabels": false, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": false, + "lineBreakBeforeEachGenericRequirement": false, + "lineBreakBetweenDeclarationAttributes": false, + "prioritizeKeepingFunctionOutputTogether": true, + "reflowMultilineStringLiterals": "never", + "rules": { + "GroupNumericLiterals": false, + "OrderedImports": false, + "FileScopedDeclarationPrivacy": false, + "NoAccessLevelOnExtensionDeclaration": false, + "UseShorthandTypeNames": false, + "ReturnVoidInsteadOfEmptyTuple": false, + "AlwaysUseLowerCamelCase": false, + "UseLetInEveryBoundCaseVariable": false, + "NoBlockComments": false + } +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 17310d85..a091a336 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,5 @@ { // The official Swift extension is the only one this setup needs — it drives SourceKit-LSP for code // intelligence. There is no formatter extension on purpose: see docs/decisions.md entry 26. - "recommendations": ["swiftlang.swift"] + "recommendations": ["swiftlang.swift-vscode"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 556a7748..b288173c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,9 +14,12 @@ // assembled .app bundle (see launch.json), not a bare executable. "swift.autoGenerateLaunchConfigurations": false, - // Off on purpose: the only formatter VS Code can reach here is swift-format, via sourcekit-lsp, - // and it restructures code rather than laying it out. See docs/decisions.md entry 26. + // Formatting is swift-format from the Xcode toolchain, reached through sourcekit-lsp — no extra + // extension or brew package. defaultFormatter is required: without it VS Code refuses to pick one + // and ⌘S silently does nothing. `.swift-format` tunes it down to this tree's style; it still + // restructures more than decisions.md entry 26 wanted, so re-read that before trusting it. "[swift]": { - "editor.formatOnSave": false + "editor.formatOnSave": true, + "editor.defaultFormatter": "swiftlang.swift-vscode" } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index c9414110..bddf73d7 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,13 +5,13 @@ // Build the .app with xcodebuild into a fixed path (build/DerivedData) so the app // location is stable — that's what lets F5 find it and keeps the Accessibility grant. // DEVELOPER_DIR forces Xcode's toolchain (the SwiftUI macros aren't in the CLT). - // The build log is piped to `xcode-build-server parse` so SourceKit-LSP's flag - // database (.compile) stays in sync on every build — that's what keeps cross-file - // symbols resolving in VS Code as you add/rename files. pipefail preserves the - // xcodebuild exit code so a failed build still blocks F5 / the run step. + // The build log is handed to Scripts/sync-lsp.sh, which rebuilds SourceKit-LSP's flag + // database from it — that's what keeps cross-file symbols resolving in VS Code as you + // add and rename files. pipefail preserves the xcodebuild exit code so a failed build + // still blocks F5 / the run step. "label": "Build Tinycast.app (debug)", "type": "shell", - "command": "bash -c 'set -o pipefail; xcodebuild -project Tinycast.xcodeproj -scheme Tinycast -configuration Debug -derivedDataPath build/DerivedData build 2>&1 | tee \"${TMPDIR:-/tmp}/tinycast-build.log\"; ec=$?; if command -v xcode-build-server >/dev/null; then xcode-build-server parse -a -o .compile < \"${TMPDIR:-/tmp}/tinycast-build.log\" >/dev/null 2>&1 || true; ./Scripts/run-tests.sh --index >/dev/null 2>&1 || true; fi; exit $ec'", + "command": "bash -c 'set -o pipefail; xcodebuild -project Tinycast.xcodeproj -scheme Tinycast -configuration Debug -derivedDataPath build/DerivedData build 2>&1 | tee \"${TMPDIR:-/tmp}/tinycast-build.log\"; ec=$?; ./Scripts/sync-lsp.sh \"${TMPDIR:-/tmp}/tinycast-build.log\" >/dev/null 2>&1 || true; exit $ec'", "options": { "cwd": "${workspaceFolder}", "env": { "DEVELOPER_DIR": "/Applications/Xcode.app/Contents/Developer" } diff --git a/Scripts/format.sh b/Scripts/format.sh new file mode 100755 index 00000000..3a79cfa2 --- /dev/null +++ b/Scripts/format.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Format the whole project with swift-format. `--check` reports instead of writing, for CI. +# Read docs/decisions.md entry 26 first: this restructures code, it does not merely lay it out. +set -uo pipefail +cd "$(dirname "$0")/.." || exit 1 + +# The Xcode toolchain's swift-format, the same binary sourcekit-lsp formats with — not a brew package, +# so the editor's ⌘S and this script can never disagree. +FORMAT=$(xcrun --find swift-format 2>/dev/null) +[ -x "${FORMAT:-}" ] || { + echo "✗ swift-format not found in the Xcode toolchain. Check 'xcode-select -p'." >&2 + exit 2 +} + +# Generated files are never hand-edited, and formatting one is exactly that — the next +# `node Scripts/gen-emoji.js` would revert it. +# Built with a read loop rather than `mapfile`, which is bash 4 — macOS ships bash 3.2. +files=() +while IFS= read -r f; do files+=("$f"); done < <( + find Tinycast Tests -name '*.swift' ! -name '*.generated.swift' | sort +) + +if [ "${1:-}" = "--check" ]; then + dirty=() + for f in "${files[@]}"; do + "$FORMAT" --configuration .swift-format "$f" 2>/dev/null | diff -q - "$f" >/dev/null 2>&1 || dirty+=("$f") + done + if [ ${#dirty[@]} -gt 0 ]; then + printf '%s\n' "${dirty[@]}" + echo + echo "${#dirty[@]} file(s) need formatting. Run ./Scripts/format.sh" >&2 + exit 1 + fi + echo "✓ format-clean (${#files[@]} files)" + exit 0 +fi + +failed=() +for f in "${files[@]}"; do + "$FORMAT" --configuration .swift-format --in-place "$f" || failed+=("$f") +done + +# swift-format refuses a file that does not parse, so a failure here is a syntax error, not a bug. +if [ ${#failed[@]} -gt 0 ]; then + printf '\n%d file(s) could not be formatted (they do not parse):\n' "${#failed[@]}" >&2 + printf ' %s\n' "${failed[@]}" >&2 + exit 1 +fi +echo "✓ formatted ${#files[@]} files" diff --git a/Scripts/lint.sh b/Scripts/lint.sh index a4a667a8..b45e700f 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -1,6 +1,6 @@ #!/bin/bash # Lint the whole project. `--fix` auto-corrects the mechanical subset first. -# There is no formatter here on purpose — see docs/decisions.md entry 26. +# Formatting is a separate tool with its own caveats — ./Scripts/format.sh, docs/decisions.md entry 26. set -uo pipefail cd "$(dirname "$0")/.." || exit 1 diff --git a/Scripts/run-tests.sh b/Scripts/run-tests.sh index 96531a46..10ae24d5 100755 --- a/Scripts/run-tests.sh +++ b/Scripts/run-tests.sh @@ -42,14 +42,10 @@ run() { printf '{"directory":"%s","command":"swiftc -swift-version 6 -sdk %s' \ "$PWD" "$(xcrun --show-sdk-path --sdk macosx)" >> "$DB" printf ' %s' "${sources[@]}" >> "$DB" - printf '","files":[' >> "$DB" - local first=1 - for source in "${sources[@]}"; do - [ "$first" -eq 0 ] && printf ',' >> "$DB" - printf '"%s"' "$source" >> "$DB" - first=0 - done - printf ']}' >> "$DB" + # Claim only the harness itself. The command still lists every shipped source it compiles, so + # symbols resolve inside the harness — but claiming those sources here would hand them this + # 3-file command instead of the app's, and `.compile` is last-wins. + printf '","files":["%s/Tests/%s.swift"]}' "$PWD" "$name" >> "$DB" return 0 fi diff --git a/Scripts/sync-lsp.sh b/Scripts/sync-lsp.sh new file mode 100755 index 00000000..9f30958b --- /dev/null +++ b/Scripts/sync-lsp.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Rebuild SourceKit-LSP's flag database from a build log. Run by the VS Code build task; also the +# one-time editor setup, since it is what creates buildServer.json. See docs/development.md. +# +# `xcode-build-server parse` with no `-o` is deliberate: that is the only spelling that also writes +# buildServer.json, and it writes it as `kind: manual`. The `kind: xcode` alternative ignores .compile +# entirely and reads a cache scraped from .xcactivitylog instead — which silently freezes the moment +# LogStoreManifest.plist stops updating, pinning the editor to a source list from an old build. + +set -uo pipefail +cd "$(dirname "$0")/.." || exit 1 + +LOG=${1:-} +if [ -z "$LOG" ] || [ ! -f "$LOG" ]; then + echo "usage: $0 " >&2 + exit 2 +fi +command -v xcode-build-server >/dev/null || { echo "xcode-build-server not installed; skipping." >&2; exit 0; } + +# A build that compiled no Swift emits no compile commands, and parsing it would replace .compile with +# an empty database — every file loses its flags. Keep the previous one in that case. +backup="${TMPDIR:-/tmp}/tinycast-compile.bak" +[ -f .compile ] && cp .compile "$backup" + +xcode-build-server parse < "$LOG" >/dev/null 2>&1 + +if [ ! -s .compile ] || ! grep -q '"command"' .compile 2>/dev/null; then + if [ -f "$backup" ]; then + cp "$backup" .compile + echo "no compile commands in log; kept the previous .compile" >&2 + fi +fi +rm -f "$backup" + +./Scripts/run-tests.sh --index diff --git a/Tests/calc-test.swift b/Tests/calc-test.swift index 6ce41248..c74d0d45 100644 --- a/Tests/calc-test.swift +++ b/Tests/calc-test.swift @@ -618,7 +618,7 @@ struct CalcTests { base: "USD", rates: [ "USD": 1, "EUR": 0.92, "GBP": 0.79, "JPY": 157, "INR": 83.5, "CAD": 1.36, - "KRW": 1330, "IDR": 18053, "CHF": 0.81, "AED": 3.6725, + "KRW": 1330, "IDR": 18053, "CHF": 0.81, "AED": 3.6725 ], fetchedAt: Date(timeIntervalSince1970: 1_785_000_000)) diff --git a/Tests/callout-test.swift b/Tests/callout-test.swift index 6c4cad0f..ac92db42 100644 --- a/Tests/callout-test.swift +++ b/Tests/callout-test.swift @@ -76,7 +76,8 @@ struct CalloutPlacementTests { expect( size.width / 2 + inset <= fieldInsetFromPaneEdge, - "the callout is narrow enough to centre on a trailing-edge recorder — widen it and the caret skews") + "the callout is narrow enough to centre on a trailing-edge recorder — widen it and the caret skews" + ) } // MARK: - Above vs below diff --git a/Tests/clipboard-test.swift b/Tests/clipboard-test.swift index 29a1de8d..58511e4d 100644 --- a/Tests/clipboard-test.swift +++ b/Tests/clipboard-test.swift @@ -92,7 +92,7 @@ struct ClipboardTests { _ = store.importEntries([ entry("ancient-pinned", at: old), entry("ancient-loose", at: old.addingTimeInterval(1)), - entry("fresh", at: Date()), + entry("fresh", at: Date()) ]) store.togglePinned(item(store, "ancient-pinned")) diff --git a/Tests/fuzz-test.swift b/Tests/fuzz-test.swift index c352ad7c..dd1a6f6e 100644 --- a/Tests/fuzz-test.swift +++ b/Tests/fuzz-test.swift @@ -53,7 +53,7 @@ struct FuzzTest { // Ships an untranslated localization placeholder — see SearchFields.usableAlternateNames. App(name: "Maps", alternates: ["ALTERNATE_NAME_1", "Maps.app"]), // Alternate that only repeats the display name; contributes nothing. - App(name: "Image Playground", alternates: ["Image Playground", "Image Playground.app"]), + App(name: "Image Playground", alternates: ["Image Playground", "Image Playground.app"]) ] static func app(_ name: String) -> App { apps.first { $0.name == name }! } @@ -69,7 +69,8 @@ struct FuzzTest { return (app.name, s + boosts[app.name, default: 0]) } .sorted { - $0.1 != $1.1 ? $0.1 > $1.1 + $0.1 != $1.1 + ? $0.1 > $1.1 : $0.0.localizedCaseInsensitiveCompare($1.0) == .orderedAscending } .map(\.0) @@ -282,8 +283,11 @@ struct FuzzTest { func identifierHits(_ query: String) -> [String] { rank(query).filter { score(query, $0)! < 2 * SearchRelevance.bandStride } } - check("'com' matches nothing by bundle id", identifierHits("com").isEmpty, "got \(identifierHits("com"))") - check("'co' matches nothing by bundle id", identifierHits("co").isEmpty, "got \(identifierHits("co"))") + check( + "'com' matches nothing by bundle id", identifierHits("com").isEmpty, + "got \(identifierHits("com"))") + check( + "'co' matches nothing by bundle id", identifierHits("co").isEmpty, "got \(identifierHits("co"))") check("'com.' matches nothing by bundle id", identifierHits("com.").isEmpty) check( "a bundle id with no dot still matches", @@ -298,7 +302,8 @@ struct FuzzTest { above(rank("chrome"), "Google Chrome", "Chess") || !rank("chrome").contains("Chess")) let noID = SearchFields(names: ["Solo"]) - check("an entry with no bundle id or executable still matches on its name", + check( + "an entry with no bundle id or executable still matches on its name", SearchRelevance.score(query: "solo", fields: noID) != nil) check("...and matches nothing else", SearchRelevance.score(query: "com", fields: noID) == nil) } @@ -447,6 +452,8 @@ struct FuzzTest { if let bundleID, let alternate, bundleID >= alternate { inversions += 1 } if let executable, let bundleID, executable >= bundleID { inversions += 1 } } - check("the same text always scores lower in a weaker field", inversions == 0, "\(inversions) inversions") + check( + "the same text always scores lower in a weaker field", inversions == 0, "\(inversions) inversions" + ) } } diff --git a/Tests/hotkey-test.swift b/Tests/hotkey-test.swift index 99525af5..5d7851a1 100644 --- a/Tests/hotkey-test.swift +++ b/Tests/hotkey-test.swift @@ -12,7 +12,8 @@ private struct Keyboard { _ modifiers: Set, other: Bool = false, at time: TimeInterval ) { if let modifier = detector.handle( - .modifiers(modifiers, hasOtherModifiers: other), at: time) { + .modifiers(modifiers, hasOtherModifiers: other), at: time) + { fired.append(modifier) } } @@ -130,8 +131,10 @@ struct DoubleTapDetectorTests { "an ordinary combo is untouched, and stays in ⌃⌥⇧⌘ order rather than press order") expect( - KeyShortcut(keyCode: kVK_ANSI_G, modifierFlags: KeyShortcut.hyperChord( - includesShift: true))?.carbonModifiers + KeyShortcut( + keyCode: kVK_ANSI_G, + modifierFlags: KeyShortcut.hyperChord( + includesShift: true))?.carbonModifiers == combo([.control, .option, .shift, .command]).carbonModifiers, "recording while Hyper is held captures exactly the chord") } @@ -162,8 +165,10 @@ struct DoubleTapDetectorTests { "the masking initializer keeps a stray flag out of the retargeted chord") // Anything that isn't the other chord is left exactly as recorded. - for flags in [[.command, .shift], [.option], [.control, .option], []] as [NSEvent - .ModifierFlags] { + for flags in [[.command, .shift], [.option], [.control, .option], []] + as [NSEvent + .ModifierFlags] + { let shortcut = combo(flags) for includesShift in [false, true] { expect( diff --git a/Tests/palette-selection-test.swift b/Tests/palette-selection-test.swift index 9daeaf5d..07bc4bd9 100644 --- a/Tests/palette-selection-test.swift +++ b/Tests/palette-selection-test.swift @@ -16,11 +16,15 @@ struct PaletteRowIndexTests { } static func expect(_ actual: PaletteRow?, _ expected: PaletteRow?, _ message: String) { - expect(actual == expected, "\(message) — got \(String(describing: actual)), want \(String(describing: expected))") + expect( + actual == expected, + "\(message) — got \(String(describing: actual)), want \(String(describing: expected))") } static func expect(_ actual: Int?, _ expected: Int?, _ message: String) { - expect(actual == expected, "\(message) — got \(String(describing: actual)), want \(String(describing: expected))") + expect( + actual == expected, + "\(message) — got \(String(describing: actual)), want \(String(describing: expected))") } /// Where a flat index lands in the rendered grid: its visual row, and its column of that row. diff --git a/Tests/ranking-test.swift b/Tests/ranking-test.swift index b8f996e2..56775bf4 100644 --- a/Tests/ranking-test.swift +++ b/Tests/ranking-test.swift @@ -42,7 +42,8 @@ struct RankingTest { && LauncherRankingStore.normalize("I") != "I".folding( options: [.caseInsensitive, .diacriticInsensitive], - locale: Locale(identifier: "tr_TR"))) + locale: Locale(identifier: "tr_TR")) + ) check("unvisited result has no boost", boost(store, whatsApp, "w") == 0) check("an unlearned query yields an empty table", store.boosts(query: "w").isEmpty) diff --git a/Tests/raycast-test.swift b/Tests/raycast-test.swift index f67738e8..319500b1 100644 --- a/Tests/raycast-test.swift +++ b/Tests/raycast-test.swift @@ -260,7 +260,8 @@ enum RaycastTests { expect(parsed.toggleEmoji?.carbonKeyCode == 49, "emoji command hotkey") // A v1 export has no global palette hotkey and no launch-at-login flag to find. - let empty = payload(""" + let empty = payload( + """ {"builtin_package_raycastPreferences": {"preferencesGeneral": { "raycastAlternativeEscape": false}}} """) @@ -269,7 +270,8 @@ enum RaycastTests { expect(empty?.appHotkeys.isEmpty == true, "no rootSearch means no hotkeys") // Wrong-typed values must be ignored, not crash or coerce. - let wrongTypes = payload(""" + let wrongTypes = payload( + """ {"builtin_package_raycastPreferences": {"preferencesAdvanced": { "popToRootTimeout": "90", "emojiSkinTone": 3, "raycast_hyperKey_state": {"enabled": true}}}} @@ -278,7 +280,8 @@ enum RaycastTests { expect(wrongTypes?.emojiSkinTone == nil, "a numeric skin tone is ignored") expect(wrongTypes?.hyperKey == nil, "a hyper key without a key code is ignored") - let disabledHyper = payload(""" + let disabledHyper = payload( + """ {"builtin_package_raycastPreferences": {"preferencesAdvanced": { "raycast_hyperKey_state": {"enabled": false, "keyCode": 57}}}} """) @@ -362,7 +365,8 @@ enum RaycastTests { // MARK: - Favorites and snippets static func favoritesAndSnippets() { - let favorites = payload(""" + let favorites = payload( + """ {"builtin_package_navigation": {"pinnedMenuItems": [ {"key": "org.alacritty"}, "com.apple.Safari", @@ -377,7 +381,8 @@ enum RaycastTests { "app favorites keep their order; commands and junk are dropped") expect(payload("{}")?.favorites.isEmpty == true, "no navigation provider means no favorites") - let snippets = payload(""" + let snippets = payload( + """ {"builtin_package_snippets": {"snippets": [ {"name": "Sig", "text": "Best,\\nAB", "keyword": ";sig"}, {"name": " Padded ", "text": "x", "keyword": " "}, diff --git a/Tests/scopes-test.swift b/Tests/scopes-test.swift index 5b07a8d1..e0197487 100644 --- a/Tests/scopes-test.swift +++ b/Tests/scopes-test.swift @@ -81,7 +81,7 @@ struct ScopesTest { check( "normalize dedups after abbreviating", SearchScopes.normalize([ - "/Applications", "/Applications/", home + "/Applications", "~/Applications", + "/Applications", "/Applications/", home + "/Applications", "~/Applications" ]) == ["/Applications", "~/Applications"]) check("normalize preserves order", SearchScopes.normalize(["/B", "/A"]) == ["/B", "/A"]) diff --git a/Tests/snippets-test.swift b/Tests/snippets-test.swift index dd9b14b7..82243552 100644 --- a/Tests/snippets-test.swift +++ b/Tests/snippets-test.swift @@ -49,19 +49,23 @@ struct SnippetsTests { ["name": "Multiline 雪", "text": "First\nSecond"], ["name": "Blank Keyword", "text": "Body", "keyword": " "], ["name": " ", "text": "Skipped"], - ["name": "Missing Text"], + ["name": "Missing Text"] ]) - check("Raycast import ignores an unrecognized container", + check( + "Raycast import ignores an unrecognized container", RaycastSnippetImport.parse(["snippets": []]).isEmpty) - check("Raycast import keeps valid entries and source order", + check( + "Raycast import keeps valid entries and source order", imported.map(\.name) == ["Email", "Multiline 雪", "Blank Keyword"]) // The remaining assertions index into the result, so a wrong count has to fail rather than trap. guard imported.count == 3 else { return } check("Raycast import preserves text and Unicode", imported[1].text == "First\nSecond") - check("Raycast import trims keywords and normalizes blanks", + check( + "Raycast import trims keywords and normalizes blanks", imported[0].keyword == "!email" && imported[2].keyword == nil) - check("Raycast import uses safe Tinycast defaults", + check( + "Raycast import uses safe Tinycast defaults", imported.allSatisfy { $0.isEnabled && !$0.showsConfirmation }) } @@ -77,13 +81,20 @@ struct SnippetsTests { let parsed = try SnippetMarkdownSerializer.parse(content: serialized, fileURL: fileURL) check("Markdown codec round-trips escaped quoted scalars", parsed == snippet) - check("serializer emits canonical key order", serialized.hasPrefix( - "---\nname: \"Quote \\\" slash \\\\ line\\nreturn\\rtab\\t雪\"\nkeyword: \"!\\\"\\\\\\n\\t\"\nenabled: false\nshow_confirmation: true\n---\n")) - check("Markdown codec preserves leading, blank, CRLF, and trailing body boundaries", parsed.text == snippet.text) + check( + "serializer emits canonical key order", + serialized.hasPrefix( + "---\nname: \"Quote \\\" slash \\\\ line\\nreturn\\rtab\\t雪\"\nkeyword: \"!\\\"\\\\\\n\\t\"\nenabled: false\nshow_confirmation: true\n---\n" + )) + check( + "Markdown codec preserves leading, blank, CRLF, and trailing body boundaries", + parsed.text == snippet.text) let injection = Snippet(name: "Safe\"\nenabled: false", text: "Body") let injectionSource = SnippetMarkdownSerializer.serialize(injection) - check("quoted scalar encoding prevents frontmatter line injection", !injectionSource.contains("\nenabled: false\nenabled:")) + check( + "quoted scalar encoding prevents frontmatter line injection", + !injectionSource.contains("\nenabled: false\nenabled:")) let parsedInjection = try SnippetMarkdownSerializer.parse( content: injectionSource, fileURL: fileURL) @@ -97,7 +108,8 @@ struct SnippetsTests { let parsedCRLFInjection = try SnippetMarkdownSerializer.parse( content: crlfInjectionSource, fileURL: fileURL) - check("CRLF scalar graphemes are escaped and round-trip literally", + check( + "CRLF scalar graphemes are escaped and round-trip literally", parsedCRLFInjection == crlfInjection && !crlfInjectionSource.contains("Safe\r\nenabled")) expectParseError( @@ -113,13 +125,17 @@ struct SnippetsTests { content: "---\nname: \"No HUD\"\n---\nBody", fileURL: fileURL) check("missing show_confirmation defaults false", !missingHUD.showsConfirmation) - expectParseError("show_confirmation uses strict booleans", content: "---\nshow_confirmation: TRUE\n---\n", fileURL: fileURL) + expectParseError( + "show_confirmation uses strict booleans", content: "---\nshow_confirmation: TRUE\n---\n", + fileURL: fileURL) let delimiterBody = "---\nname: \"Delimiter Body\"\nenabled: true\n---\nFirst\n---\nLast\n" let delimiterParsed = try SnippetMarkdownSerializer.parse( content: delimiterBody, fileURL: fileURL) - check("frontmatter delimiters inside the body remain literal", delimiterParsed.text == "First\n---\nLast\n") + check( + "frontmatter delimiters inside the body remain literal", + delimiterParsed.text == "First\n---\nLast\n") let bodyOnly = "--- not frontmatter\n\nBody" let bodyOnlyParsed = try SnippetMarkdownSerializer.parse( @@ -134,21 +150,37 @@ struct SnippetsTests { let emptyNameParsed = try SnippetMarkdownSerializer.parse( content: "---\nname: \"\"\n---\nBody", fileURL: URL(fileURLWithPath: "/tmp/blank-name-file.md")) - check("a blank frontmatter name falls back to the filename", + check( + "a blank frontmatter name falls back to the filename", blankNameParsed.name == "Blank Name File" && emptyNameParsed.name == "Blank Name File") - expectParseError("missing closing delimiter is rejected", content: "---\nname: \"Broken\"\n", fileURL: fileURL) - expectParseError("non-exact closing delimiter is rejected", content: "---\nname: \"Broken\"\n--- \n", fileURL: fileURL) + expectParseError( + "missing closing delimiter is rejected", content: "---\nname: \"Broken\"\n", fileURL: fileURL) + expectParseError( + "non-exact closing delimiter is rejected", content: "---\nname: \"Broken\"\n--- \n", + fileURL: fileURL) expectParseError("unquoted scalar is rejected", content: "---\nname: Broken\n---\n", fileURL: fileURL) - expectParseError("invalid scalar escape is rejected", content: "---\nname: \"Bad\\q\"\n---\n", fileURL: fileURL) - expectParseError("non-strict boolean is rejected", content: "---\nenabled: FALSE\n---\n", fileURL: fileURL) - expectParseError("duplicate keys are rejected", content: "---\nname: \"A\"\nname: \"B\"\n---\n", fileURL: fileURL) - expectParseError("the removed showInLauncher alias is rejected", content: "---\nshowInLauncher: false\n---\n", fileURL: fileURL) - expectParseError("unknown frontmatter key is rejected", content: "---\nunknown: \"value\"\n---\n", fileURL: fileURL) + expectParseError( + "invalid scalar escape is rejected", content: "---\nname: \"Bad\\q\"\n---\n", fileURL: fileURL) + expectParseError( + "non-strict boolean is rejected", content: "---\nenabled: FALSE\n---\n", fileURL: fileURL) + expectParseError( + "duplicate keys are rejected", content: "---\nname: \"A\"\nname: \"B\"\n---\n", fileURL: fileURL) + expectParseError( + "the removed showInLauncher alias is rejected", content: "---\nshowInLauncher: false\n---\n", + fileURL: fileURL) + expectParseError( + "unknown frontmatter key is rejected", content: "---\nunknown: \"value\"\n---\n", fileURL: fileURL + ) // These keys were removed or renamed; a file still carrying one is reported, not silently half-loaded. - expectParseError("the removed category key is rejected", content: "---\ncategory: \"Work\"\n---\n", fileURL: fileURL) - expectParseError("the removed show_in_launcher key is rejected", content: "---\nshow_in_launcher: true\n---\n", fileURL: fileURL) - expectParseError("the renamed show_hud key is rejected", content: "---\nshow_hud: true\n---\n", fileURL: fileURL) + expectParseError( + "the removed category key is rejected", content: "---\ncategory: \"Work\"\n---\n", + fileURL: fileURL) + expectParseError( + "the removed show_in_launcher key is rejected", content: "---\nshow_in_launcher: true\n---\n", + fileURL: fileURL) + expectParseError( + "the renamed show_hud key is rejected", content: "---\nshow_hud: true\n---\n", fileURL: fileURL) } private static func testRepositoryStorage() throws { @@ -170,14 +202,18 @@ struct SnippetsTests { bundleIdentifier: "com.tinycast.app.dev", applicationSupportRoot: channelRoot) - check("stable, beta, and dev repositories use isolated directories", + check( + "stable, beta, and dev repositories use isolated directories", Set([stable.snippetsDirectory, beta.snippetsDirectory, dev.snippetsDirectory]).count == 3) let firstLoad = try stable.load() - check("a fresh channel starts with an empty library", + check( + "a fresh channel starts with an empty library", firstLoad.records.isEmpty && firstLoad.issues.isEmpty) - check("the first load creates the channel's snippets folder", + check( + "the first load creates the channel's snippets folder", fm.fileExists(atPath: stable.snippetsDirectory.path)) - check("loading one channel does not create another", + check( + "loading one channel does not create another", !fm.fileExists(atPath: dev.snippetsDirectory.path)) let secondLoad = try stable.load() check("a repeated load of an empty library stays empty", secondLoad.records.isEmpty) @@ -196,8 +232,10 @@ struct SnippetsTests { atomically: true, encoding: .utf8) let partial = try corruptRepository.load() - check("a malformed file does not hide valid records", partial.records.map(\.snippet.name) == ["Valid"]) - check("malformed files are returned as per-file issues", + check( + "a malformed file does not hide valid records", partial.records.map(\.snippet.name) == ["Valid"]) + check( + "malformed files are returned as per-file issues", partial.issues.count == 1 && partial.issues[0].fileURL.standardizedFileURL.path == invalidURL.standardizedFileURL.path) @@ -208,14 +246,16 @@ struct SnippetsTests { let linkedEntryURL = corruptRepository.snippetsDirectory.appendingPathComponent("linked.md") try fm.createSymbolicLink(at: linkedEntryURL, withDestinationURL: validURL) let nonRegularEntries = try corruptRepository.load() - check("a directory named like a snippet is neither loaded nor reported as an issue", + check( + "a directory named like a snippet is neither loaded nor reported as an issue", !nonRegularEntries.records.contains { $0.id == directoryEntryURL.standardizedFileURL.path } && !nonRegularEntries.issues.contains { $0.fileURL.standardizedFileURL.path == directoryEntryURL.standardizedFileURL.path }) - check("a snippet file symlinked into the folder still loads", + check( + "a snippet file symlinked into the folder still loads", nonRegularEntries.records.contains { $0.id == linkedEntryURL.standardizedFileURL.path }) @@ -226,24 +266,29 @@ struct SnippetsTests { applicationSupportRoot: crudRoot) let imported = try crudRepository.create([ Snippet(name: "Imported", text: "One"), - Snippet(name: "Imported", text: "Two", keyword: "!two"), + Snippet(name: "Imported", text: "Two", keyword: "!two") ]) - check("batch import creates every snippet without overwriting duplicate names", + check( + "batch import creates every snippet without overwriting duplicate names", imported.map { $0.fileURL.lastPathComponent } == ["imported.md", "imported-2.md"]) let importedReload = try crudRepository.load() - check("batch import round-trips through Markdown storage", + check( + "batch import round-trips through Markdown storage", importedReload.records.filter { $0.snippet.name == "Imported" }.count == 2) let first = try crudRepository.create(Snippet(name: "Same", text: "One")) let second = try crudRepository.create(Snippet(name: "Same", text: "Two")) - check("create never overwrites an existing slug", + check( + "create never overwrites an existing slug", first.fileURL.lastPathComponent == "same.md" && second.fileURL.lastPathComponent == "same-2.md") let oddURL = crudRepository.snippetsDirectory.appendingPathComponent("unrelated-filename.md") try SnippetMarkdownSerializer.serialize(Snippet(name: "Frontmatter Name", text: "Odd")) .write(to: oddURL, atomically: true, encoding: .utf8) let withOddFilename = try crudRepository.load() - check("frontmatter names do not replace path identity", - withOddFilename.records.contains { $0.id == oddURL.path && $0.snippet.name == "Frontmatter Name" }) + check( + "frontmatter names do not replace path identity", + withOddFilename.records.contains { $0.id == oddURL.path && $0.snippet.name == "Frontmatter Name" } + ) var edited = first.snippet edited.name = "Renamed in Frontmatter" @@ -254,14 +299,21 @@ struct SnippetsTests { expectedRevision: first.sourceRevision) let afterSave = try crudRepository.load() check("save keeps the original file identity", saved.id == first.id) - check("save updates in place without creating duplicates", + check( + "save updates in place without creating duplicates", afterSave.records.filter { $0.id == first.id }.count == 1 - && !fm.fileExists(atPath: crudRepository.snippetsDirectory.appendingPathComponent("renamed-in-frontmatter.md").path)) + && !fm.fileExists( + atPath: crudRepository.snippetsDirectory.appendingPathComponent( + "renamed-in-frontmatter.md" + ).path) + ) - let externallyRenamedURL = crudRepository.snippetsDirectory.appendingPathComponent("external-rename.md") + let externallyRenamedURL = crudRepository.snippetsDirectory.appendingPathComponent( + "external-rename.md") try fm.moveItem(at: second.fileURL, to: externallyRenamedURL) let afterRename = try crudRepository.load() - check("an external rename is modeled as delete plus create", + check( + "an external rename is modeled as delete plus create", !afterRename.records.contains { $0.id == second.id } && afterRename.records.contains { $0.id == externallyRenamedURL.path }) @@ -288,7 +340,8 @@ struct SnippetsTests { try crudRepository.delete( fileURL: currentSaved.fileURL, expectedRevision: currentSaved.sourceRevision) - check("delete removes exactly the requested file", + check( + "delete removes exactly the requested file", !fm.fileExists(atPath: currentSaved.fileURL.path) && fm.fileExists(atPath: externallyRenamedURL.path)) do { @@ -338,10 +391,12 @@ struct SnippetsTests { }.value let results = await [first, second] let records = results.compactMap { try? $0.get() } - initializationHeld = records.count == 2 + initializationHeld = + records.count == 2 && records.allSatisfy { fm.fileExists(atPath: $0.fileURL.path) } } - check("concurrent initialization and creates preserve both committed files", + check( + "concurrent initialization and creates preserve both committed files", initializationHeld) let saveRoot = root.appendingPathComponent("save", isDirectory: true) @@ -357,16 +412,20 @@ struct SnippetsTests { var secondEdit = stored.snippet secondEdit.text = "Second" async let firstSave = Task.detached { - Result { try repository.save( - firstEdit, - fileURL: stored.fileURL, - expectedRevision: stored.sourceRevision) } + Result { + try repository.save( + firstEdit, + fileURL: stored.fileURL, + expectedRevision: stored.sourceRevision) + } }.value async let secondSave = Task.detached { - Result { try secondRepositoryOwner.save( - secondEdit, - fileURL: stored.fileURL, - expectedRevision: stored.sourceRevision) } + Result { + try secondRepositoryOwner.save( + secondEdit, + fileURL: stored.fileURL, + expectedRevision: stored.sourceRevision) + } }.value let saveResults = await [firstSave, secondSave] let successCount = saveResults.filter { @@ -379,7 +438,8 @@ struct SnippetsTests { else { return false } return true }.count - check("per-channel repository owners serialize revision validation with commit", + check( + "per-channel repository owners serialize revision validation with commit", successCount == 1 && conflictCount == 1) let physicalSupport = root.appendingPathComponent("physical-support", isDirectory: true) @@ -404,9 +464,11 @@ struct SnippetsTests { mutationHooks: hooks) let symlinkedRecord = try symlinkedRepository.create( Snippet(name: "Alias Race", text: "Original")) - guard let directRecord = try directRepository.load().records.first(where: { - $0.fileURL.lastPathComponent == symlinkedRecord.fileURL.lastPathComponent - }) else { + guard + let directRecord = try directRepository.load().records.first(where: { + $0.fileURL.lastPathComponent == symlinkedRecord.fileURL.lastPathComponent + }) + else { aliasCoordinationHeld = false continue } @@ -416,16 +478,20 @@ struct SnippetsTests { symlinkedEdit.text = "Symlinked \(index)" async let directSave = Task.detached { - Result { try directRepository.save( - directEdit, - fileURL: directRecord.fileURL, - expectedRevision: directRecord.sourceRevision) } + Result { + try directRepository.save( + directEdit, + fileURL: directRecord.fileURL, + expectedRevision: directRecord.sourceRevision) + } }.value async let symlinkedSave = Task.detached { - Result { try symlinkedRepository.save( - symlinkedEdit, - fileURL: symlinkedRecord.fileURL, - expectedRevision: symlinkedRecord.sourceRevision) } + Result { + try symlinkedRepository.save( + symlinkedEdit, + fileURL: symlinkedRecord.fileURL, + expectedRevision: symlinkedRecord.sourceRevision) + } }.value let results = await [directSave, symlinkedSave] let successes = results.filter { @@ -440,7 +506,8 @@ struct SnippetsTests { }.count aliasCoordinationHeld = successes == 1 && conflicts == 1 } - check("direct and symlinked channel aliases share revision coordination", + check( + "direct and symlinked channel aliases share revision coordination", aliasCoordinationHeld) let boundaryRoot = root.appendingPathComponent("mutation-boundary", isDirectory: true) @@ -454,10 +521,11 @@ struct SnippetsTests { bundleIdentifier: boundaryBundle, applicationSupportRoot: boundaryRoot, mutationHooks: .init(beforeRevalidation: { mutation, fileURL in - let text = switch mutation { - case .save: "External before save" - case .delete: "External before delete" - } + let text = + switch mutation { + case .save: "External before save" + case .delete: "External before delete" + } try? Data(text.utf8).write(to: fileURL, options: .atomic) })) var boundaryEdit = boundaryRecord.snippet @@ -470,7 +538,8 @@ struct SnippetsTests { check("save revalidates inside coordinated access at the mutation boundary", false) } catch SnippetRepository.RepositoryError.conflict { let content = try String(contentsOf: boundaryRecord.fileURL, encoding: .utf8) - check("save revalidates inside coordinated access at the mutation boundary", + check( + "save revalidates inside coordinated access at the mutation boundary", content == "External before save") } if let deleteRecord = try boundaryRepository.load().records.first(where: { @@ -483,7 +552,8 @@ struct SnippetsTests { check("delete revalidates inside coordinated access at the mutation boundary", false) } catch SnippetRepository.RepositoryError.conflict { let content = try String(contentsOf: deleteRecord.fileURL, encoding: .utf8) - check("delete revalidates inside coordinated access at the mutation boundary", + check( + "delete revalidates inside coordinated access at the mutation boundary", fm.fileExists(atPath: deleteRecord.fileURL.path) && content == "External before delete") } @@ -504,7 +574,8 @@ struct SnippetsTests { order.append("second") } await queue.drain() - check("interactive deliveries are retained and serialized", + check( + "interactive deliveries are retained and serialized", order == ["first-start", "first-end", "second"] && queue.isIdle) var automaticRan = false @@ -522,19 +593,24 @@ struct SnippetsTests { let completion = SnippetDeliveryCompletion { completionCount += 1 } completion.confirm() completion.confirm() - check("delivery completion invokes its callback exactly once after confirmation", + check( + "delivery completion invokes its callback exactly once after confirmation", completion.isConfirmed && completionCount == 1) - check("unavailable AX text attributes use the event delivery fallback", + check( + "unavailable AX text attributes use the event delivery fallback", SnippetAccessibilityReplacement.unavailable.fallsBackToEvents) - check("a rejected AX keyword replacement fails closed instead of deleting by events", + check( + "a rejected AX keyword replacement fails closed instead of deleting by events", !SnippetAccessibilityReplacement.rejected.fallsBackToEvents) - check("unreadable AX state accepts a posted paste after the conservative delay", + check( + "unreadable AX state accepts a posted paste after the conservative delay", SnippetPasteConfirmationPolicy.acceptsUnconfirmedDelivery( attempt: 15, hadPreviousState: true, readStateAfterPaste: false)) - check("readable unchanged AX state is not treated as a confirmed paste", + check( + "readable unchanged AX state is not treated as a confirmed paste", !SnippetPasteConfirmationPolicy.acceptsUnconfirmedDelivery( attempt: 79, hadPreviousState: true, @@ -551,31 +627,36 @@ struct SnippetsTests { let secondType = NSPasteboard.PasteboardType("com.example.second") let secondItem = NSPasteboardItem() secondItem.setData(Data([4, 5, 6]), forType: secondType) - check("pasteboard fixture writes multiple items and types", + check( + "pasteboard fixture writes multiple items and types", pasteboard.replaceObjects([firstItem, secondItem])) let lease = TemporaryPasteboardLease.begin( text: "Temporary", pasteboard: pasteboard) - check("temporary pasteboard ownership preserves the original item shape", + check( + "temporary pasteboard ownership preserves the original item shape", lease?.isOwned == true && pasteboard.string(forType: .string) == "Temporary" && pasteboard.pasteboardItems?.count == 2 && pasteboard.pasteboardItems?[0].data(forType: customType) == Data([0, 1, 2, 3]) && pasteboard.pasteboardItems?[1].data(forType: secondType) - == Data([4, 5, 6])) + == Data([4, 5, 6]) + ) let clearCountBeforeRestore = pasteboard.clearCount let writeCountBeforeRestore = pasteboard.writeCount let restoreResult = lease?.restoreIfOwned() let restoredItems = pasteboard.pasteboardItems - check("pasteboard restoration preserves every item, type, and payload", + check( + "pasteboard restoration preserves every item, type, and payload", restoreResult != nil && restoredItems?.count == 2 && restoredItems?[0].string(forType: .string) == "Original" && restoredItems?[0].data(forType: customType) == Data([0, 1, 2, 3]) && restoredItems?[1].data(forType: secondType) == Data([4, 5, 6])) - check("pasteboard restoration does not clear before a fallible write", + check( + "pasteboard restoration does not clear before a fallible write", pasteboard.clearCount == clearCountBeforeRestore && pasteboard.writeCount == writeCountBeforeRestore) @@ -585,7 +666,8 @@ struct SnippetsTests { text: "Temporary failure", pasteboard: pasteboard, onMutation: { recoveredMutationCount = $0 }) - check("a failed temporary write restores the original clipboard before falling back", + check( + "a failed temporary write restores the original clipboard before falling back", failedLease == nil && pasteboard.string(forType: .string) == "Original" && pasteboard.pasteboardItems?.count == 2 @@ -597,12 +679,14 @@ struct SnippetsTests { let newerItem = NSPasteboardItem() newerItem.setString("Newer copy", forType: .string) _ = pasteboard.replaceObjects([newerItem]) - check("pasteboard restoration never overwrites a newer copy", + check( + "pasteboard restoration never overwrites a newer copy", supersededLease?.restoreIfOwned() == .superseded && pasteboard.string(forType: .string) == "Newer copy") _ = pasteboard.replaceObjects([]) - check("an empty clipboard declines temporary ownership for the Unicode fallback", + check( + "an empty clipboard declines temporary ownership for the Unicode fallback", TemporaryPasteboardLease.begin( text: "Temporary from empty", pasteboard: pasteboard) == nil @@ -611,7 +695,8 @@ struct SnippetsTests { let imageOnlyItem = NSPasteboardItem() imageOnlyItem.setData(Data([9, 8, 7]), forType: .png) _ = pasteboard.replaceObjects([imageOnlyItem]) - check("a non-text clipboard declines temporary ownership without changing its payload", + check( + "a non-text clipboard declines temporary ownership without changing its payload", TemporaryPasteboardLease.begin( text: "Temporary over image", pasteboard: pasteboard) == nil @@ -633,20 +718,23 @@ struct SnippetsTests { store.onSnapshot = { _ in snapshotCount += 1 } await store.start() - check("store initialization publishes a ready snapshot", + check( + "store initialization publishes a ready snapshot", store.state == .ready && store.snippets.isEmpty && snapshotCount == 1) let externalURL = repository.snippetsDirectory.appendingPathComponent("external.md") try SnippetMarkdownSerializer.serialize(Snippet(name: "External", text: "One")) .write(to: externalURL, atomically: true, encoding: .utf8) try await Task.sleep(for: .milliseconds(500)) - check("watcher reloads an externally created file", + check( + "watcher reloads an externally created file", store.snippets.contains { $0.id == externalURL.path && $0.snippet.text == "One" }) try SnippetMarkdownSerializer.serialize(Snippet(name: "External", text: "Two")) .write(to: externalURL, atomically: true, encoding: .utf8) try await Task.sleep(for: .milliseconds(500)) - check("watcher observes atomic file replacement", + check( + "watcher observes atomic file replacement", store.record(id: externalURL.path)?.snippet.text == "Two") let inPlaceSource = SnippetMarkdownSerializer.serialize( @@ -656,7 +744,8 @@ struct SnippetsTests { try handle.write(contentsOf: Data(inPlaceSource.utf8)) try handle.close() try await Task.sleep(for: .milliseconds(500)) - check("watcher observes same-inode truncate and write", + check( + "watcher observes same-inode truncate and write", store.record(id: externalURL.path)?.snippet.text == "Three") let replacementDirectory = repository.channelDirectory.appendingPathComponent( @@ -673,7 +762,8 @@ struct SnippetsTests { options: []) try await Task.sleep(for: .milliseconds(700)) let installedReplacementURL = repository.snippetsDirectory.appendingPathComponent("replacement.md") - check("watcher rearms after directory replacement", + check( + "watcher rearms after directory replacement", store.snippets.count == 1 && store.snippets.first?.id == installedReplacementURL.path) let renamedDirectory = repository.channelDirectory.appendingPathComponent( @@ -685,32 +775,37 @@ struct SnippetsTests { try SnippetMarkdownSerializer.serialize(Snippet(name: "Recreated", text: "Newest")) .write(to: recreatedURL, atomically: true, encoding: .utf8) try await Task.sleep(for: .milliseconds(700)) - check("watcher rearms after an explicit rename-away and recreation", + check( + "watcher rearms after an explicit rename-away and recreation", store.snippets.count == 1 && store.record(id: recreatedURL.path)?.snippet.text == "Newest") try fm.removeItem(at: renamedDirectory) try fm.removeItem(at: repository.snippetsDirectory) try await Task.sleep(for: .milliseconds(700)) - check("watcher recreates a deleted initialized directory without samples", + check( + "watcher recreates a deleted initialized directory without samples", store.state == .ready && store.snippets.isEmpty && fm.fileExists(atPath: repository.snippetsDirectory.path)) let afterDeleteURL = repository.snippetsDirectory.appendingPathComponent("after-delete.md") try SnippetMarkdownSerializer.serialize(Snippet(name: "After Delete", text: "Rearmed")) .write(to: afterDeleteURL, atomically: true, encoding: .utf8) try await Task.sleep(for: .milliseconds(500)) - check("watcher continues after deleted-directory recovery", + check( + "watcher continues after deleted-directory recovery", store.record(id: afterDeleteURL.path)?.snippet.text == "Rearmed") let beforeBurst = snapshotCount for index in 0..<3 { let fileURL = repository.snippetsDirectory.appendingPathComponent("burst-\(index).md") try SnippetMarkdownSerializer.serialize( - Snippet(name: "Burst \(index)", text: "\(index)")) - .write(to: fileURL, atomically: true, encoding: .utf8) + Snippet(name: "Burst \(index)", text: "\(index)") + ) + .write(to: fileURL, atomically: true, encoding: .utf8) } try await Task.sleep(for: .milliseconds(500)) - check("watcher debounces a burst into one published reload", + check( + "watcher debounces a burst into one published reload", snapshotCount == beforeBurst + 1 && store.snippets.filter { $0.snippet.name.hasPrefix("Burst ") }.count == 3) @@ -720,7 +815,8 @@ struct SnippetsTests { atomically: true, encoding: .utf8) try await Task.sleep(for: .milliseconds(500)) - check("watcher publishes corrupt-file issues without dropping valid files", + check( + "watcher publishes corrupt-file issues without dropping valid files", store.issues.contains { $0.fileURL.lastPathComponent == "corrupt.md" } && store.record(id: afterDeleteURL.path) != nil) @@ -733,7 +829,8 @@ struct SnippetsTests { atomically: true, encoding: .utf8) try await Task.sleep(for: .milliseconds(400)) - check("retry after stop cannot restart loading or watchers", + check( + "retry after stop cannot restart loading or watchers", snapshotCount == stoppedSnapshotCount) store.stop() } @@ -742,8 +839,9 @@ struct SnippetsTests { var calendar = Calendar(identifier: .gregorian) let timeZone = TimeZone(secondsFromGMT: 0)! calendar.timeZone = timeZone - let now = calendar.date(from: DateComponents( - year: 2026, month: 7, day: 24, hour: 13, minute: 5))! + let now = calendar.date( + from: DateComponents( + year: 2026, month: 7, day: 24, hour: 13, minute: 5))! let context = SnippetTemplateEngine.ExpansionContext( clipboard: "{date} 📋", selection: "{cursor} selected", @@ -759,25 +857,37 @@ struct SnippetsTests { dateValues, snippets: [dateValues], context: context) - check("default date and time tokens use the injected locale, calendar, and time zone", + check( + "default date and time tokens use the injected locale, calendar, and time zone", expandedDateValues.text == "Jul 24, 2026|1:05\u{202F}PM") let values = record( "/tmp/values.md", Snippet( name: "Values", - text: "C:{clipboard}|S:{selection}|D:{date format=\"yyyy-MM-dd HH:mm\"}|{argument name=\"First\"}|{argument}|{argument name=\"First\"}")) + text: + "C:{clipboard}|S:{selection}|D:{date format=\"yyyy-MM-dd HH:mm\"}|{argument name=\"First\"}|{argument}|{argument name=\"First\"}" + )) let missing = SnippetTemplateEngine.expand(values, snippets: [values], context: context) - check("missing arguments are unique and ordered by appearance", missing.missingArguments.map(\.name) == ["First", "Argument"]) - check("missing argument tokens stay visible until values are supplied", missing.text.hasSuffix("{argument name=\"First\"}|{argument}|{argument name=\"First\"}")) + check( + "missing arguments are unique and ordered by appearance", + missing.missingArguments.map(\.name) == ["First", "Argument"]) + check( + "missing argument tokens stay visible until values are supplied", + missing.text.hasSuffix("{argument name=\"First\"}|{argument}|{argument name=\"First\"}")) let expandedValues = SnippetTemplateEngine.expand( values, snippets: [values], context: context, userArguments: ["First": "{clipboard}", "Argument": "{cursor}"]) - check("clipboard, selection, and arguments insert token-shaped text literally", expandedValues.text == "C:{date} 📋|S:{cursor} selected|D:2026-07-24 13:05|{clipboard}|{cursor}|{clipboard}") - check("injected cursor-shaped text does not set cursor position", expandedValues.cursorOffsetFromEnd == nil) + check( + "clipboard, selection, and arguments insert token-shaped text literally", + expandedValues.text + == "C:{date} 📋|S:{cursor} selected|D:2026-07-24 13:05|{clipboard}|{cursor}|{clipboard}") + check( + "injected cursor-shaped text does not set cursor position", + expandedValues.cursorOffsetFromEnd == nil) check("all supplied arguments clear the missing list", expandedValues.missingArguments.isEmpty) let literalBraces = record( @@ -787,12 +897,15 @@ struct SnippetsTests { literalBraces, snippets: [literalBraces], context: context) - check("literal JSON and code braces do not mask nested valid tokens", + check( + "literal JSON and code braces do not mask nested valid tokens", literalBraceResult.text == "{\"generated\":\"Jul 24, 2026\"}|struct { value: 1:05\u{202F}PM }") let promptContextSnippet = record( "/tmp/prompt-context.md", - Snippet(name: "Prompt Context", text: "{clipboard}|{selection}|{date format=\"HH:mm\"}|{argument name=\"Value\"}")) + Snippet( + name: "Prompt Context", + text: "{clipboard}|{selection}|{date format=\"HH:mm\"}|{argument name=\"Value\"}")) let beforePrompt = SnippetTemplateEngine.expand( promptContextSnippet, snippets: [promptContextSnippet], @@ -802,7 +915,8 @@ struct SnippetsTests { snippets: [promptContextSnippet], context: context, userArguments: ["Value": "Done"]) - check("argument prompts reuse the captured expansion context", + check( + "argument prompts reuse the captured expansion context", beforePrompt.text.replacingOccurrences( of: "{argument name=\"Value\"}", with: "Done") == afterPrompt.text) @@ -817,7 +931,9 @@ struct SnippetsTests { references, snippets: [duplicateZ, keywordTarget, references, duplicateA], context: context) - check("duplicate name references resolve by stable path identity", referenced.text == "A|K|{snippet:missing}") + check( + "duplicate name references resolve by stable path identity", + referenced.text == "A|K|{snippet:missing}") check("name and keyword references are case-insensitive", referenced.text.hasPrefix("A|K|")) check("missing references remain visible", referenced.text.hasSuffix("{snippet:missing}")) @@ -831,7 +947,8 @@ struct SnippetsTests { disabledReferences, snippets: [disabledChild, disabledReferences], context: context) - check("a disabled snippet cannot be expanded by name or keyword reference", + check( + "a disabled snippet cannot be expanded by name or keyword reference", disabledResult.text == "{snippet:Disabled}|{snippet:!disabled}") let cursorChild = record( @@ -852,17 +969,23 @@ struct SnippetsTests { Snippet(name: "Nested Arguments", text: "{argument name=\"Nested\"}|{argument name=\"Root\"}")) let argumentRoot = record( "/tmp/argument-root.md", - Snippet(name: "Argument Root", text: "{argument name=\"Root\"}|{snippet:Nested Arguments}|{argument name=\"Last\"}")) + Snippet( + name: "Argument Root", + text: "{argument name=\"Root\"}|{snippet:Nested Arguments}|{argument name=\"Last\"}")) let argumentResult = SnippetTemplateEngine.expand( argumentRoot, snippets: [argumentRoot, nestedArguments], context: context) - check("nested arguments follow final appearance order", argumentResult.missingArguments.map(\.name) == ["Root", "Nested", "Last"]) + check( + "nested arguments follow final appearance order", + argumentResult.missingArguments.map(\.name) == ["Root", "Nested", "Last"]) let cycleA = record("/tmp/cycle-a.md", Snippet(name: "A", text: "{snippet:B}")) let cycleB = record("/tmp/cycle-b.md", Snippet(name: "B", text: "{snippet:A}")) let cycleResult = SnippetTemplateEngine.expand(cycleA, snippets: [cycleA, cycleB], context: context) - check("cycles are detected with stable record IDs and remain visible", cycleResult.text == "{snippet:A}") + check( + "cycles are detected with stable record IDs and remain visible", cycleResult.text == "{snippet:A}" + ) let depthRecords = (0...6).map { index in record( @@ -881,8 +1004,9 @@ struct SnippetsTests { var calendar = Calendar(identifier: .gregorian) let timeZone = TimeZone(secondsFromGMT: 0)! calendar.timeZone = timeZone - let now = calendar.date(from: DateComponents( - year: 2026, month: 7, day: 24, hour: 13, minute: 5))! + let now = calendar.date( + from: DateComponents( + year: 2026, month: 7, day: 24, hour: 13, minute: 5))! let uuids = UUIDSequence() let context = SnippetTemplateEngine.ExpansionContext( clipboardHistory: [" newest ", "older", "oldest"], @@ -893,7 +1017,10 @@ struct SnippetsTests { timeZone: timeZone, makeUUID: { uuids.next() }) - func expand(_ text: String, arguments: [String: String] = [:]) -> SnippetTemplateEngine + func expand( + _ text: String, arguments: [String: String] = [:] + ) + -> SnippetTemplateEngine .ExpansionResult { let subject = record("/tmp/placeholders.md", Snippet(name: "Subject", text: text)) @@ -902,79 +1029,104 @@ struct SnippetsTests { } // New date/time tokens. - check("datetime combines the date and time styles", + check( + "datetime combines the date and time styles", expand("{datetime}").text == "Jul 24, 2026 at 1:05\u{202F}PM") check("day renders the weekday name", expand("{day}").text == "Friday") // Offsets: signed, multi-unit, and every documented unit. - check("a single signed offset shifts the date", + check( + "a single signed offset shifts the date", expand("{date offset=\"+1d\"}").text == "Jul 25, 2026") check("offsets accept a bare unquoted value", expand("{day offset=-3d}").text == "Tuesday") - check("multiple offsets apply in order", + check( + "multiple offsets apply in order", expand("{date offset=\"+2y +5M\"}").text == "Dec 24, 2028") - check("minute and hour offsets shift the time", + check( + "minute and hour offsets shift the time", expand("{time offset=\"+3h +30m\"}").text == "4:35\u{202F}PM") - check("an unknown offset unit leaves the token literal", + check( + "an unknown offset unit leaves the token literal", expand("{date offset=\"+1w\"}").text == "{date offset=\"+1w\"}") - check("an offset without an amount leaves the token literal", + check( + "an offset without an amount leaves the token literal", expand("{date offset=\"d\"}").text == "{date offset=\"d\"}") // Locale and format. - check("locale overrides the context locale", + check( + "locale overrides the context locale", expand("{date locale=\"fr-FR\"}").text == "24 juil. 2026") - check("format and locale together are rejected as ambiguous", + check( + "format and locale together are rejected as ambiguous", expand("{date format=\"yyyy\" locale=\"fr-FR\"}").text == "{date format=\"yyyy\" locale=\"fr-FR\"}") - check("format still applies with an offset", + check( + "format still applies with an offset", expand("{date offset=\"-1d\" format=\"yyyy-MM-dd\"}").text == "2026-07-23") // UUID comes from the injected source, once per token. check("each uuid token draws a fresh value", expand("{uuid}|{uuid}").text == "uuid-1|uuid-2") // Clipboard history. - check("clipboard offset zero is the current clipboard", + check( + "clipboard offset zero is the current clipboard", expand("{clipboard}").text == " newest ") - check("clipboard offset reaches back through history", + check( + "clipboard offset reaches back through history", expand("{clipboard offset=1}|{clipboard offset=2}").text == "older|oldest") - check("a clipboard offset past the end expands to nothing", + check( + "a clipboard offset past the end expands to nothing", expand("{clipboard offset=9}").text.isEmpty) - check("a negative clipboard offset leaves the token literal", + check( + "a negative clipboard offset leaves the token literal", expand("{clipboard offset=-1}").text == "{clipboard offset=-1}") // Modifier pipeline. - check("uppercase and lowercase modifiers apply", + check( + "uppercase and lowercase modifiers apply", expand("{selection | uppercase}|{selection | lowercase}").text == "PICKED|picked") check("trim strips surrounding whitespace", expand("{clipboard | trim}").text == "newest") - check("modifiers chain left to right", + check( + "modifiers chain left to right", expand("{clipboard | trim | uppercase}").text == "NEWEST") - check("percent-encode escapes everything outside the unreserved set", + check( + "percent-encode escapes everything outside the unreserved set", expand("{argument name=\"U\" | percent-encode}", arguments: ["U": "a b/c?d&e=f~g-h"]).text == "a%20b%2Fc%3Fd%26e%3Df~g-h") - check("json-stringify escapes without adding quotes", + check( + "json-stringify escapes without adding quotes", expand("{argument name=\"J\" | json-stringify}", arguments: ["J": "a\"b\\c\nd"]).text == "a\\\"b\\\\c\\nd") - check("raw is accepted and changes nothing", + check( + "raw is accepted and changes nothing", expand("{clipboard | raw}").text == " newest ") - check("an unknown modifier leaves the token literal", + check( + "an unknown modifier leaves the token literal", expand("{clipboard | shout}").text == "{clipboard | shout}") - check("a modifier on a structural token leaves it literal", + check( + "a modifier on a structural token leaves it literal", expand("{cursor | uppercase}").text == "{cursor | uppercase}") - check("a pipe inside a quoted value is not a modifier separator", + check( + "a pipe inside a quoted value is not a modifier separator", expand("{date format=\"yyyy|MM\"}").text == "2026|07") // Arguments: defaults and options. let defaulted = expand("{argument name=\"Tone\" default=\"happy\"}") - check("an argument default expands without prompting", + check( + "an argument default expands without prompting", defaulted.text == "happy" && defaulted.missingArguments.isEmpty) - check("a supplied value beats the default", + check( + "a supplied value beats the default", expand("{argument name=\"Tone\" default=\"happy\"}", arguments: ["Tone": "sad"]).text == "sad") let optioned = expand("{argument name=\"Tone\" options=\"happy, sad, professional\"}") - check("options travel with the missing argument", + check( + "options travel with the missing argument", optioned.missingArguments == [ .init(name: "Tone", options: ["happy", "sad", "professional"]) ]) - check("an empty options list leaves the token literal", + check( + "an empty options list leaves the token literal", expand("{argument name=\"Tone\" options=\", \"}").text == "{argument name=\"Tone\" options=\", \"}") @@ -983,15 +1135,18 @@ struct SnippetsTests { let byName = record("/tmp/ph-name.md", Snippet(name: "ByName", text: "{snippet name=\"Child\"}")) let byColon = record("/tmp/ph-colon.md", Snippet(name: "ByColon", text: "{snippet:Child}")) let pool = [child, byName, byColon] - check("snippet name= resolves identically to snippet:", + check( + "snippet name= resolves identically to snippet:", SnippetTemplateEngine.expand(byName, snippets: pool, context: context).text == "nested" && SnippetTemplateEngine.expand(byColon, snippets: pool, context: context).text - == "nested") + == "nested" + ) let disabledChild = record( "/tmp/ph-disabled.md", Snippet(name: "Off", text: "secret", isEnabled: false)) let referencesDisabled = record( "/tmp/ph-ref-off.md", Snippet(name: "Ref", text: "{snippet name=\"Off\"}")) - check("snippet name= cannot reach a disabled snippet", + check( + "snippet name= cannot reach a disabled snippet", SnippetTemplateEngine.expand( referencesDisabled, snippets: [disabledChild, referencesDisabled], @@ -1000,16 +1155,21 @@ struct SnippetsTests { // Malformed tokens stay literal rather than vanishing. check("an unknown placeholder stays literal", expand("{weather}").text == "{weather}") - check("an unknown parameter leaves the token literal", + check( + "an unknown parameter leaves the token literal", expand("{date style=\"long\"}").text == "{date style=\"long\"}") - check("a duplicated parameter leaves the token literal", + check( + "a duplicated parameter leaves the token literal", expand("{date offset=\"+1d\" offset=\"+2d\"}").text == "{date offset=\"+1d\" offset=\"+2d\"}") - check("an unterminated quote leaves the token literal", + check( + "an unterminated quote leaves the token literal", expand("{date format=\"yyyy}").text == "{date format=\"yyyy}") - check("a parameter on a token that takes none leaves it literal", + check( + "a parameter on a token that takes none leaves it literal", expand("{uuid offset=1}").text == "{uuid offset=1}") - check("an empty clipboard history expands the clipboard to nothing", + check( + "an empty clipboard history expands the clipboard to nothing", SnippetTemplateEngine.expand( record("/tmp/ph-empty.md", Snippet(name: "E", text: "[{clipboard}]")), snippets: [], @@ -1047,58 +1207,77 @@ struct SnippetsTests { } // The text entry point. - check("a bare template expands without a snippet record", + check( + "a bare template expands without a snippet record", expand("q={clipboard}").text == "q=a b&c") - check("a snippet reference has nothing to resolve against and stays literal", + check( + "a snippet reference has nothing to resolve against and stays literal", expand("{snippet:Child}").text == "{snippet:Child}") - check("missing arguments are reported from the text entry point too", + check( + "missing arguments are reported from the text entry point too", expand("{argument name=\"Repository\"}").missingArguments == [.init(name: "Repository", options: [])]) // Percent encoding of produced values. - check("percent encoding escapes a value substituted into a URL", + check( + "percent encoding escapes a value substituted into a URL", expand("https://x.com/?q={clipboard}", encoding: .percentEncoding).text == "https://x.com/?q=a%20b%26c") - check("the literal parts of the template are never encoded", + check( + "the literal parts of the template are never encoded", expand("https://x.com/a b?q={selection}", encoding: .percentEncoding).text == "https://x.com/a b?q=a%20b%26c") - check("encoding runs after the pipeline, so uppercase cannot rewrite the hex", + check( + "encoding runs after the pipeline, so uppercase cannot rewrite the hex", expand("{clipboard | uppercase}", encoding: .percentEncoding).text == "A%20B%26C") - check("raw opts a value out of automatic encoding", + check( + "raw opts a value out of automatic encoding", expand("{clipboard | raw}", encoding: .percentEncoding).text == "a b&c") - check("an explicit percent-encode is not applied twice", + check( + "an explicit percent-encode is not applied twice", expand("{clipboard | percent-encode}", encoding: .percentEncoding).text == "a%20b%26c") - check("encoding reaches every value-producing token", + check( + "encoding reaches every value-producing token", expand("{argument name=\"A\"}", encoding: .percentEncoding, arguments: ["A": "x y"]).text == "x%20y") - check("snippets ask for no encoding, so their expansion is unchanged", + check( + "snippets ask for no encoding, so their expansion is unchanged", expand("{clipboard}").text == "a b&c") // {selectedText} is an accepted spelling of {selection}. - check("selectedText resolves to the selection", + check( + "selectedText resolves to the selection", expand("{selectedText}").text == expand("{selection}").text) - check("the alias is case-insensitive like every other token name", + check( + "the alias is case-insensitive like every other token name", expand("{SelectedText}").text == "a b&c") - check("the alias takes the same modifier pipeline", + check( + "the alias takes the same modifier pipeline", expand("{selectedText | trim | uppercase}").text == "A B&C") - check("the alias is encoded like the canonical spelling", + check( + "the alias is encoded like the canonical spelling", expand("{selectedText}", encoding: .percentEncoding).text == "a%20b%26c") - check("the alias rejects parameters, exactly as selection does", + check( + "the alias rejects parameters, exactly as selection does", expand("{selectedText offset=1}").text == "{selectedText offset=1}") let aliasSnippet = record( "/tmp/alias.md", Snippet(name: "Alias", text: "[{selectedText}]")) - check("a snippet may use the alias too — it is not quicklink-only", + check( + "a snippet may use the alias too — it is not quicklink-only", SnippetTemplateEngine.expand(aliasSnippet, snippets: [], context: context).text == "[a b&c]") // usesSelection drives the selection-fallback setting. - check("usesSelection sees both spellings", + check( + "usesSelection sees both spellings", SnippetTemplateEngine.usesSelection("a {selection} b") && SnippetTemplateEngine.usesSelection("a {selectedText} b")) - check("usesSelection is false for a template that reads no selection", + check( + "usesSelection is false for a template that reads no selection", !SnippetTemplateEngine.usesSelection("{clipboard} {date}")) - check("usesSelection parses rather than searches, so a malformed token does not count", + check( + "usesSelection parses rather than searches, so a malformed token does not count", !SnippetTemplateEngine.usesSelection("{selection offset=1}")) } @@ -1112,21 +1291,25 @@ struct SnippetsTests { .init(snippetID: "/tmp/trimmed.md", value: " !trim "), .init( snippetID: "/tmp/too-long.md", - value: String(repeating: "x", count: SnippetKeywordPolicy.maximumBufferLength + 1)), + value: String(repeating: "x", count: SnippetKeywordPolicy.maximumBufferLength + 1)) ]) let longest = policy.process(.text("abc"), at: base) check("keyword matching prefers the longest suffix", longest?.snippetID == "/tmp/long.md") let duplicate = policy.process(.text("!DuP"), at: base.addingTimeInterval(1)) - check("duplicate keywords resolve by stable snippet identity", + check( + "duplicate keywords resolve by stable snippet identity", duplicate?.snippetID == "/tmp/a-duplicate.md") let trimmed = policy.process(.text("!trim"), at: base.addingTimeInterval(1.5)) - check("keyword matching trims surrounding whitespace and deletes only the trigger", - trimmed == .init( - snippetID: "/tmp/trimmed.md", - keyword: "!trim", - deletionCount: 5)) - check("keywords longer than the buffer cap are excluded", + check( + "keyword matching trims surrounding whitespace and deletes only the trigger", + trimmed + == .init( + snippetID: "/tmp/trimmed.md", + keyword: "!trim", + deletionCount: 5)) + check( + "keywords longer than the buffer cap are excluded", !policy.keywords.contains { $0.snippetID == "/tmp/too-long.md" }) let syntheticInput = SnippetKeywordPolicy.classifyInput( @@ -1142,7 +1325,8 @@ struct SnippetsTests { _ = policy.process(.text("!du"), at: base.addingTimeInterval(2)) _ = policy.process(syntheticInput, at: base.addingTimeInterval(2.5)) let afterSynthetic = policy.process(.text("p"), at: base.addingTimeInterval(3)) - check("ignored synthetic events do not alter the keyword buffer", + check( + "ignored synthetic events do not alter the keyword buffer", afterSynthetic?.snippetID == "/tmp/a-duplicate.md") let secureInput = SnippetKeywordPolicy.classifyInput( @@ -1183,18 +1367,21 @@ struct SnippetsTests { let shiftedKeywordMatch = shiftedKeywordPolicy.process( .text("notes"), at: base.addingTimeInterval(10.2)) - check("Shift and Option flag transitions preserve modifier-produced keywords", + check( + "Shift and Option flag transitions preserve modifier-produced keywords", shiftTransition == .ignored && shiftedKeywordMatch?.snippetID == "/tmp/notes.md") _ = policy.process(.text("a"), at: base.addingTimeInterval(40)) let afterTimeout = policy.process(.text("bc"), at: base.addingTimeInterval(56)) - check("keyword buffer resets after the inactivity timeout", afterTimeout?.snippetID == "/tmp/short.md") + check( + "keyword buffer resets after the inactivity timeout", afterTimeout?.snippetID == "/tmp/short.md") _ = policy.process(.text("!dux"), at: base.addingTimeInterval(60)) _ = policy.process(.deleteBackward, at: base.addingTimeInterval(61)) let afterDelete = policy.process(.text("p"), at: base.addingTimeInterval(62)) - check("backspace updates the buffered suffix deterministically", + check( + "backspace updates the buffered suffix deterministically", afterDelete?.snippetID == "/tmp/a-duplicate.md") _ = policy.process( @@ -1214,7 +1401,8 @@ struct SnippetsTests { isSessionActive: true, hasAccessibility: true, tapState: .absent) - check("listener remains off without consent", + check( + "listener remains off without consent", consentOff == .init(status: .off, tapAction: .none)) let stopWithTap = Lifecycle.decide( @@ -1222,7 +1410,8 @@ struct SnippetsTests { isSessionActive: true, hasAccessibility: true, tapState: .active) - check("stop tears down an installed tap synchronously", + check( + "stop tears down an installed tap synchronously", stopWithTap == .init(status: .off, tapAction: .tearDown)) let waiting = Lifecycle.decide( @@ -1230,7 +1419,8 @@ struct SnippetsTests { isSessionActive: true, hasAccessibility: false, tapState: .absent) - check("consent waits without the Accessibility grant and does not install a tap", + check( + "consent waits without the Accessibility grant and does not install a tap", waiting == .init(status: .needsAccessibility, tapAction: .none)) let grantsArrived = Lifecycle.decide( @@ -1238,14 +1428,16 @@ struct SnippetsTests { isSessionActive: true, hasAccessibility: true, tapState: .absent) - check("a later health check installs the tap after the grant arrives", + check( + "a later health check installs the tap after the grant arrives", grantsArrived == .init(status: .needsAccessibility, tapAction: .install)) let retryAfterFailure = Lifecycle.decide( isRequested: true, isSessionActive: true, hasAccessibility: true, tapState: .absent) - check("tap creation failure remains retryable on the next health check", + check( + "tap creation failure remains retryable on the next health check", grantsArrived.tapAction == .install && retryAfterFailure.tapAction == .install) let active = Lifecycle.decide( @@ -1253,9 +1445,11 @@ struct SnippetsTests { isSessionActive: true, hasAccessibility: true, tapState: .active) - check("listener reports active only with the grant and a live tap", + check( + "listener reports active only with the grant and a live tap", active == .init(status: .active, tapAction: .none)) - check("repeated start is idempotent when the tap is already active", + check( + "repeated start is idempotent when the tap is already active", Lifecycle.decide( isRequested: true, isSessionActive: true, @@ -1267,7 +1461,8 @@ struct SnippetsTests { isSessionActive: true, hasAccessibility: false, tapState: .active) - check("permission revocation moves to waiting and tears down the tap", + check( + "permission revocation moves to waiting and tears down the tap", revoked == .init(status: .needsAccessibility, tapAction: .tearDown)) let disabled = Lifecycle.decide( @@ -1275,7 +1470,8 @@ struct SnippetsTests { isSessionActive: true, hasAccessibility: true, tapState: .disabled) - check("a disabled tap is re-enabled before the listener can be active", + check( + "a disabled tap is re-enabled before the listener can be active", disabled == .init(status: .needsAccessibility, tapAction: .reenable)) let inactiveSession = Lifecycle.decide( @@ -1283,7 +1479,8 @@ struct SnippetsTests { isSessionActive: false, hasAccessibility: true, tapState: .active) - check("session resignation tears down the tap and leaves consent waiting", + check( + "session resignation tears down the tap and leaves consent waiting", inactiveSession == .init(status: .needsAccessibility, tapAction: .tearDown)) let rapidOff = Lifecycle.decide( @@ -1296,7 +1493,8 @@ struct SnippetsTests { isSessionActive: true, hasAccessibility: true, tapState: .absent) - check("rapid off then on cannot preserve a stale active tap", + check( + "rapid off then on cannot preserve a stale active tap", rapidOff.tapAction == .tearDown && rapidOff.status == .off && rapidOn.tapAction == .install @@ -1316,28 +1514,33 @@ struct SnippetsTests { logsTapFailures: false) listener.start { _, _, _, _ in } - check("real listener waits without permissions and does not install", + check( + "real listener waits without permissions and does not install", listener.status == .needsAccessibility && tap.installCount == 0) permissions.accessibility = true listener.healthCheck() - check("real listener keeps a failed tap installation retryable", + check( + "real listener keeps a failed tap installation retryable", listener.status == .needsAccessibility && tap.installCount == 1 && tap.state == .absent) listener.healthCheck() - check("real listener applies installation after grants arrive", + check( + "real listener applies installation after grants arrive", listener.status == .active && tap.installCount == 2 && tap.state == .active) listener.start { _, _, _, _ in } - check("real listener repeated start does not install a second tap", + check( + "real listener repeated start does not install a second tap", listener.status == .active && tap.installCount == 2) tap.state = .disabled listener.healthCheck() - check("real listener applies tap re-enable and returns active", + check( + "real listener applies tap re-enable and returns active", listener.status == .active && tap.reenableCount == 1 && tap.state == .active) @@ -1345,7 +1548,8 @@ struct SnippetsTests { tap.reenableSucceeds = false tap.state = .disabled listener.healthCheck() - check("real listener recreates a tap when re-enable fails", + check( + "real listener recreates a tap when re-enable fails", listener.status == .active && tap.tearDownCount >= 1 && tap.installCount == 3) @@ -1353,20 +1557,24 @@ struct SnippetsTests { permissions.accessibility = false listener.healthCheck() - check("real listener tears down synchronously on permission revocation", + check( + "real listener tears down synchronously on permission revocation", listener.status == .needsAccessibility && tap.state == .absent) permissions.accessibility = true listener.healthCheck() - check("real listener reinstalls after permission regrant", + check( + "real listener reinstalls after permission regrant", listener.status == .active && tap.state == .active) listener.stop() - check("real listener stop is authoritative", + check( + "real listener stop is authoritative", listener.status == .off && tap.state == .absent) listener.start { _, _, _, _ in } listener.stop() - check("real listener rapid on and off leaves no tap", + check( + "real listener rapid on and off leaves no tap", listener.status == .off && tap.state == .absent) } diff --git a/Tests/uninstall-test.swift b/Tests/uninstall-test.swift index bd1518e1..9ddd9c3b 100644 --- a/Tests/uninstall-test.swift +++ b/Tests/uninstall-test.swift @@ -49,7 +49,9 @@ struct UninstallTests { return root } - static func evidence(_ name: String, _ relativePath: String, _ identity: UninstallIdentity) + static func evidence( + _ name: String, _ relativePath: String, _ identity: UninstallIdentity + ) -> UninstallEvidence? { UninstallRules.evidence(for: name, in: root(relativePath), identity: identity) @@ -386,7 +388,9 @@ struct UninstallTests { let environment = UninstallEnvironment(home: home, hasFullDiskAccess: false) let withFDA = UninstallEnvironment(home: home, hasFullDiskAccess: true) - func classify(_ facts: PathFacts, _ env: UninstallEnvironment = environment) + func classify( + _ facts: PathFacts, _ env: UninstallEnvironment = environment + ) -> UninstallProtection { UninstallProtectionRules.classify(facts, environment: env) @@ -545,7 +549,7 @@ struct UninstallTests { target: target, candidates: [ candidate("/a", bytes: nil), candidate("/b", protection: .systemProtected, bytes: 20), - candidate("/c", bytes: nil), + candidate("/c", bytes: nil) ], isTargetRunning: false) @@ -607,8 +611,10 @@ struct UninstallTests { for (other, otherIdentity) in identities where other.id != owner.id { for root in UninstallSearchRoot.all where UninstallRules.evidence(for: artifact, in: root, identity: otherIdentity) - != nil { - leaks.append("\(artifact) (\(owner.name)) matched \(other.name) in \(root.relativePath)") + != nil + { + leaks.append( + "\(artifact) (\(owner.name)) matched \(other.name) in \(root.relativePath)") } } } diff --git a/Tests/window-command-test.swift b/Tests/window-command-test.swift index feeff0df..a47529f9 100644 --- a/Tests/window-command-test.swift +++ b/Tests/window-command-test.swift @@ -176,7 +176,7 @@ struct WindowCommandTests { let quarters = [ frame(.topLeftQuarter)!, frame(.topRightQuarter)!, frame(.bottomLeftQuarter)!, - frame(.bottomRightQuarter)!, + frame(.bottomRightQuarter)! ] expect( quarters.reduce(CGRect.null) { $0.union($1) } == mainScreen.visibleFrame, @@ -303,7 +303,8 @@ struct WindowCommandTests { expectRect( frame(.topHalf, on: reserved)!, CGRect(x: 0, y: 25, width: 1440, height: 400), "tiles respect a reserved visible frame") - expectRect(frame(.maximize, on: reserved)!, reserved.visibleFrame, "maximize never covers the menu bar") + expectRect( + frame(.maximize, on: reserved)!, reserved.visibleFrame, "maximize never covers the menu bar") } // MARK: - Gaps @@ -325,7 +326,7 @@ struct WindowCommandTests { // Every outer edge is inset by the full gap, every gutter is exactly one gap. let quarters = [ frame(.topLeftQuarter, gap: 10)!, frame(.topRightQuarter, gap: 10)!, - frame(.bottomLeftQuarter, gap: 10)!, frame(.bottomRightQuarter, gap: 10)!, + frame(.bottomLeftQuarter, gap: 10)!, frame(.bottomRightQuarter, gap: 10)! ] expect(quarters[0].maxX + 10 == quarters[1].minX, "quarters: vertical gutter is the gap") expect(quarters[0].maxY + 10 == quarters[2].minY, "quarters: horizontal gutter is the gap") @@ -347,7 +348,8 @@ struct WindowCommandTests { frame(.leftHalf, gap: 9)!.maxX + 9 == frame(.rightHalf, gap: 9)!.minX, "an odd gap still produces an exact gutter") - expectRect(frame(.maximize, gap: 12)!, CGRect(x: 12, y: 12, width: 1416, height: 876), + expectRect( + frame(.maximize, gap: 12)!, CGRect(x: 12, y: 12, width: 1416, height: 876), "maximize honours the gap") // Degenerate gaps must never produce an unusable window. @@ -565,7 +567,8 @@ struct WindowCommandTests { frame(.leftHalf, on: right, gap: 10)!, "a remembered tile is re-derived exactly on the destination") expectRect( - frame(.nextDisplay, window: frame(.firstThird, on: left)!, lastTile: .firstThird, + frame( + .nextDisplay, window: frame(.firstThird, on: left)!, lastTile: .firstThird, allScreens: both)!, frame(.firstThird, on: right)!, "a remembered third is re-derived exactly on the destination") @@ -799,7 +802,7 @@ struct WindowCommandTests { mainScreen, WindowLayout.Screen( id: 2, frame: CGRect(x: 1440, y: -200, width: 2560, height: 1440), - visibleFrame: CGRect(x: 1440, y: -175, width: 2560, height: 1390)), + visibleFrame: CGRect(x: 1440, y: -175, width: 2560, height: 1390)) ], [ WindowLayout.Screen( @@ -807,15 +810,15 @@ struct WindowCommandTests { visibleFrame: CGRect(x: 0, y: 25, width: 1024, height: 590)), WindowLayout.Screen( id: 6, frame: CGRect(x: -3840, y: 0, width: 3840, height: 2160), - visibleFrame: CGRect(x: -3840, y: 25, width: 3840, height: 2060)), - ], + visibleFrame: CGRect(x: -3840, y: 25, width: 3840, height: 2060)) + ] ] let windows: [CGRect] = [ CGRect(x: 100, y: 100, width: 600, height: 400), CGRect(x: 0, y: 0, width: 0, height: 0), CGRect(x: -900, y: -900, width: 200, height: 150), CGRect(x: 200, y: 200, width: 5000, height: 4000), - CGRect(x: 1439, y: 899, width: 1, height: 1), + CGRect(x: 1439, y: 899, width: 1, height: 1) ] let gaps: [CGFloat] = [0, 1, 8, 25, 200] diff --git a/Tinycast/App/AppCore.swift b/Tinycast/App/AppCore.swift index 448427d0..869211ad 100644 --- a/Tinycast/App/AppCore.swift +++ b/Tinycast/App/AppCore.swift @@ -224,18 +224,21 @@ final class AppCore { // MARK: - Feature switches private func observeFeatureSwitches() { - track({ - _ = $0.windowManagementEnabled - _ = $0.windowManagementShowInLauncher - }, reproject: { $0.applyWindowCommandsPresence() }) - track({ - _ = $0.customCommandsEnabled - _ = $0.customCommandsShowInLauncher - }, reproject: { $0.customCommandCoordinator.applyCustomCommandsPresence() }) - track({ - _ = $0.quicklinksEnabled - _ = $0.quicklinksShowInLauncher - }, reproject: { $0.quicklinkCoordinator.applyQuicklinksPresence() }) + track( + { + _ = $0.windowManagementEnabled + _ = $0.windowManagementShowInLauncher + }, reproject: { $0.applyWindowCommandsPresence() }) + track( + { + _ = $0.customCommandsEnabled + _ = $0.customCommandsShowInLauncher + }, reproject: { $0.customCommandCoordinator.applyCustomCommandsPresence() }) + track( + { + _ = $0.quicklinksEnabled + _ = $0.quicklinksShowInLauncher + }, reproject: { $0.quicklinkCoordinator.applyQuicklinksPresence() }) track({ _ = $0.snippetsEnabled }, reproject: { $0.snippetExpansion.applySnippetsEnabled() }) // Not a feature switch, but the same re-projection: a combo has the chord's ⇧ bit baked in. track({ _ = $0.hyperKeyIncludesShift }, reproject: { $0.applyHyperChord() }) @@ -288,7 +291,9 @@ final class AppCore { } /// A failure with one usable second option; `true` when the user takes it. - func reportFailure(title: String, message: String, symbol: String, recovery: String?) async + func reportFailure( + title: String, message: String, symbol: String, recovery: String? + ) async -> Bool { await dialogs.reportFailure( diff --git a/Tinycast/Features/Backup/Model/RaycastV1Decoder.swift b/Tinycast/Features/Backup/Model/RaycastV1Decoder.swift index dd178aff..0dba0962 100644 --- a/Tinycast/Features/Backup/Model/RaycastV1Decoder.swift +++ b/Tinycast/Features/Backup/Model/RaycastV1Decoder.swift @@ -130,7 +130,8 @@ enum RaycastV1Decoder { payload.popToRootTimeout = advanced?["popToRootTimeout"] as? Int payload.emojiSkinTone = advanced?["emojiSkinTone"] as? String if let state = advanced?["raycast_hyperKey_state"] as? [String: Any], - let keyCode = state["keyCode"] as? Int { + let keyCode = state["keyCode"] as? Int + { payload.hyperKey = .init( enabled: state["enabled"] as? Bool ?? true, keyCode: keyCode, diff --git a/Tinycast/Features/Backup/Model/SettingsBackup.swift b/Tinycast/Features/Backup/Model/SettingsBackup.swift index 5132aa53..7db588bd 100644 --- a/Tinycast/Features/Backup/Model/SettingsBackup.swift +++ b/Tinycast/Features/Backup/Model/SettingsBackup.swift @@ -265,7 +265,8 @@ extension SettingsBackup { count += 1 } if let raw = s.quicklinkSelectionFallback, - let fallback = QuicklinkSelectionFallback(rawValue: raw) { + let fallback = QuicklinkSelectionFallback(rawValue: raw) + { settings.quicklinkSelectionFallback = fallback count += 1 } diff --git a/Tinycast/Features/Backup/Model/SettingsBackupCoverage.swift b/Tinycast/Features/Backup/Model/SettingsBackupCoverage.swift index 86b02224..6b9f2700 100644 --- a/Tinycast/Features/Backup/Model/SettingsBackupCoverage.swift +++ b/Tinycast/Features/Backup/Model/SettingsBackupCoverage.swift @@ -26,13 +26,13 @@ enum SettingsBackupCoverage { "quicklinksShowInLauncher": .quicklinksShowInLauncher, "quicklinkOpensNewWindow": .quicklinkOpensNewWindow, "quicklinkSelectionFallback": .quicklinkSelectionFallback, - "quicklinkConfirmsBeforeDelete": .quicklinkConfirmsBeforeDelete, + "quicklinkConfirmsBeforeDelete": .quicklinkConfirmsBeforeDelete ] /// The `SettingsData` fields no `AppSettings` key stands behind, and what they read instead. static let externallySourced: [String: String] = [ "launchAtLogin": "Read from LaunchAtLogin, which owns the login item, not UserDefaults.", - "showInMenuBar": "SettingsKey.showInMenuBar — shared with MenuBarExtra, not owned here.", + "showInMenuBar": "SettingsKey.showInMenuBar — shared with MenuBarExtra, not owned here." ] /// Keys kept out of a backup on purpose, each with the reason it has to stay out. diff --git a/Tinycast/Features/Backup/Service/BackupActions.swift b/Tinycast/Features/Backup/Service/BackupActions.swift index 72e3c217..71bf17cc 100644 --- a/Tinycast/Features/Backup/Service/BackupActions.swift +++ b/Tinycast/Features/Backup/Service/BackupActions.swift @@ -114,7 +114,8 @@ enum BackupActions { static func quitRaycast() { for app in NSWorkspace.shared.runningApplications where app.bundleIdentifier.map(isRaycastBundleID) == true - && app.activationPolicy != .prohibited { + && app.activationPolicy != .prohibited + { app.terminate() } } @@ -155,7 +156,9 @@ enum BackupActions { return "Applied " + parts.joined(separator: ", ") + "." } - private static func confirmExecutableImport(core: AppCore, commands: Int, shortcuts: Int) async + private static func confirmExecutableImport( + core: AppCore, commands: Int, shortcuts: Int + ) async -> Bool { guard commands > 0 || shortcuts > 0 else { return true } diff --git a/Tinycast/Features/Backup/Service/RaycastImportV2.swift b/Tinycast/Features/Backup/Service/RaycastImportV2.swift index bf19cdb9..5f05a548 100644 --- a/Tinycast/Features/Backup/Service/RaycastImportV2.swift +++ b/Tinycast/Features/Backup/Service/RaycastImportV2.swift @@ -94,7 +94,8 @@ enum RaycastImportV2 { } // Exact-match only: a timeout outside our option set is skipped, not clamped. if let secs = general?["popToRootTimeout"] as? Int, - let timeout = PopToRootTimeout(rawValue: secs) { + let timeout = PopToRootTimeout(rawValue: secs) + { data.popToRootSeconds = timeout.rawValue mapped = true } @@ -118,7 +119,8 @@ enum RaycastImportV2 { var mapped = false if let general = settings?["general"] as? [String: Any], - let binding = binding(from: general["globalHotkey"]) { + let binding = binding(from: general["globalHotkey"]) + { hotkeys.togglePalette = binding mapped = true } @@ -134,7 +136,8 @@ enum RaycastImportV2 { mapped = true case "e:r:applications": if let path = appPath(fromCommandID: command["id"] as? String), - let bundleID = Bundle(url: URL(fileURLWithPath: path))?.bundleIdentifier { + let bundleID = Bundle(url: URL(fileURLWithPath: path))?.bundleIdentifier + { apps[bundleID] = binding mapped = true } diff --git a/Tinycast/Features/Backup/Settings/BackupSettingsView.swift b/Tinycast/Features/Backup/Settings/BackupSettingsView.swift index d1fb3180..a9b1ca58 100644 --- a/Tinycast/Features/Backup/Settings/BackupSettingsView.swift +++ b/Tinycast/Features/Backup/Settings/BackupSettingsView.swift @@ -145,7 +145,8 @@ struct BackupSettingsView: View { if let snippetsError = outcome.snippetsError { parts.append("Couldn’t import snippets: \(snippetsError)") } - var message = parts.isEmpty + var message = + parts.isEmpty ? BackupActions.nothingImportedText : parts.joined(separator: " ") if outcome.missingImages > 0 { message += " \(outcome.missingImages) images were unavailable and skipped." diff --git a/Tinycast/Features/Calculator/Model/CalcDateTime.swift b/Tinycast/Features/Calculator/Model/CalcDateTime.swift index b1e916a7..02ff5fb7 100644 --- a/Tinycast/Features/Calculator/Model/CalcDateTime.swift +++ b/Tinycast/Features/Calculator/Model/CalcDateTime.swift @@ -5,8 +5,11 @@ enum CalcDateTime { /// Which occurrence of a bare, recurring date/time a phrase resolves to. private enum MomentBias { case future, past } - static func evaluate(_ raw: String, now: Date = Date(), calendar: Calendar = .current) - -> CalcResult? { + static func evaluate( + _ raw: String, now: Date = Date(), calendar: Calendar = .current + ) + -> CalcResult? + { let echo = raw.trimmingCharacters(in: .whitespacesAndNewlines) let query = echo.lowercased() guard !query.isEmpty else { return nil } @@ -32,8 +35,11 @@ enum CalcDateTime { // MARK: - Grammar A: duration until a moment - private static func parseUntil(_ query: String, echo: String, now: Date, calendar: Calendar) - -> CalcResult? { + private static func parseUntil( + _ query: String, echo: String, now: Date, calendar: Calendar + ) + -> CalcResult? + { guard let connector = [" until ", " till ", " til "].first(where: query.contains) else { return nil } @@ -78,8 +84,11 @@ enum CalcDateTime { // MARK: - Grammar B: duration since a past moment - private static func parseSince(_ query: String, echo: String, now: Date, calendar: Calendar) - -> CalcResult? { + private static func parseSince( + _ query: String, echo: String, now: Date, calendar: Calendar + ) + -> CalcResult? + { let parts = query.components(separatedBy: " since ") guard parts.count == 2, let unit = durationUnit(parts[0]), @@ -281,7 +290,8 @@ enum CalcDateTime { month: month, day: day, now: now, calendar: calendar, bias: bias) } if parts.count == 3, let month = Int(parts[0]), let day = Int(parts[1]), - let year = Int(parts[2]), let date = makeDate(fullYear(year), month, day, calendar) { + let year = Int(parts[2]), let date = makeDate(fullYear(year), month, day, calendar) + { return Moment(date: date, hasTime: false) } } @@ -401,8 +411,11 @@ enum CalcDateTime { // MARK: - Formatting - private static func momentString(_ date: Date, hasTime: Bool, now: Date, calendar: Calendar) - -> String { + private static func momentString( + _ date: Date, hasTime: Bool, now: Date, calendar: Calendar + ) + -> String + { let day = dateString(date, now: now, calendar: calendar) return hasTime ? "\(day) at \(timeString(date, calendar: calendar))" : day } @@ -477,8 +490,11 @@ enum CalcDateTime { return (hour, 0) } - private static func makeDate(_ year: Int, _ month: Int, _ day: Int, _ calendar: Calendar) - -> Date? { + private static func makeDate( + _ year: Int, _ month: Int, _ day: Int, _ calendar: Calendar + ) + -> Date? + { guard (1...12).contains(month), (1...31).contains(day) else { return nil } var components = DateComponents() components.year = year diff --git a/Tinycast/Features/Calculator/Model/CalcEngine.swift b/Tinycast/Features/Calculator/Model/CalcEngine.swift index 1bf51b32..e478a88a 100644 --- a/Tinycast/Features/Calculator/Model/CalcEngine.swift +++ b/Tinycast/Features/Calculator/Model/CalcEngine.swift @@ -50,7 +50,8 @@ enum CalcEngine { guard let tokens = CalcTokenizer.tokenize(query), !tokens.isEmpty else { return nil } if let partial = partialResult( - tokens, query: query, now: now, calendar: calendar, currency: currency) { + tokens, query: query, now: now, calendar: calendar, currency: currency) + { return partial } @@ -179,14 +180,16 @@ enum CalcEngine { if let quantity = CalcQuantity.evaluate( prefixTokens, query: tokenQuery(prefixTokens), currency: currency, - preserveStandaloneUnit: true) { + preserveStandaloneUnit: true) + { return replacingExpression( quantity, with: "\(quantity.expression) \(operatorText)") } // A conversion's echo drops its target, so echo the typed text; the badges name both. if let complete = evaluate( - tokenQuery(prefixTokens), now: now, calendar: calendar, currency: currency) { + tokenQuery(prefixTokens), now: now, calendar: calendar, currency: currency) + { return replacingExpression(complete, with: prettyExpression(query)) } @@ -255,12 +258,14 @@ enum CalcEngine { sourceBadge = baseName(forRadix: radix) sourceText = literalText } else if valueTokens.count == 1, let value = decimalLiteral(valueTokens[0]), - value >= 0, value.rounded() == value, value <= 9_007_199_254_740_992 { + value >= 0, value.rounded() == value, value <= 9_007_199_254_740_992 + { source = UInt64(value) sourceBadge = "Decimal" sourceText = literalText } else if let value = CalcParser.evaluate(valueTokens), - value >= 0, value.rounded() == value, value <= 9_007_199_254_740_992 { + value >= 0, value.rounded() == value, value <= 9_007_199_254_740_992 + { source = UInt64(value) sourceBadge = "Decimal" sourceText = CalcFormatter.grouped(String(source)) diff --git a/Tinycast/Features/Calculator/Model/CalcFormatter.swift b/Tinycast/Features/Calculator/Model/CalcFormatter.swift index aa12c101..4cef2712 100644 --- a/Tinycast/Features/Calculator/Model/CalcFormatter.swift +++ b/Tinycast/Features/Calculator/Model/CalcFormatter.swift @@ -37,7 +37,8 @@ enum CalcFormatter { let magnitude = abs(feet) let wholeFeet = magnitude.rounded(.towardZero) let inches = (magnitude - wholeFeet) * 12 - let feetPart = wholeFeet == 0 ? "" : "\(sign)\(display(wholeFeet)) \(wholeFeet == 1 ? "foot" : "feet")" + let feetPart = + wholeFeet == 0 ? "" : "\(sign)\(display(wholeFeet)) \(wholeFeet == 1 ? "foot" : "feet")" let inchText = display(inches) let inchPart = "\(inchText) \(inchText == "1" ? "inch" : "inches")" if feetPart.isEmpty { return "\(sign)\(inchPart)" } diff --git a/Tinycast/Features/Calculator/Model/CalcParser.swift b/Tinycast/Features/Calculator/Model/CalcParser.swift index af4e76ac..cf3f080c 100644 --- a/Tinycast/Features/Calculator/Model/CalcParser.swift +++ b/Tinycast/Features/Calculator/Model/CalcParser.swift @@ -30,7 +30,8 @@ enum CalcTokenizer { // Radix literals need ≥1 digit after the prefix, else "0" is a plain number. if ch == "0", i + 2 < chars.count, - let radix = ["x": 16, "b": 2, "o": 8][String(chars[i + 1]).lowercased()] { + let radix = ["x": 16, "b": 2, "o": 8][String(chars[i + 1]).lowercased()] + { let start = i + 2 var end = start while end < chars.count, chars[end].isHexDigit { end += 1 } diff --git a/Tinycast/Features/Calculator/Model/CalcQuantity.swift b/Tinycast/Features/Calculator/Model/CalcQuantity.swift index b88e7283..578b9fb7 100644 --- a/Tinycast/Features/Calculator/Model/CalcQuantity.swift +++ b/Tinycast/Features/Calculator/Model/CalcQuantity.swift @@ -55,7 +55,8 @@ enum CalcQuantity { // A bare `50cm` auto-converts below; with an operator the typed units are kept. if !preserveStandaloneUnit, parser.operationCount == 0, parser.dimensionCount == 1, case .ident(let finalName)? = split.expressionTokens.last, - CalcUnits.byName[finalName] != nil { + CalcUnits.byName[finalName] != nil + { return nil } guard parser.operationCount > 0 || preserveStandaloneUnit else { return nil } @@ -211,7 +212,8 @@ enum CalcQuantity { // Money is written sign-first (`$10`), so echo the amount ahead of its code. if case .ident(let name) = tokens[index], CalcUnits.byName[name] == nil, let definition = CalcCurrency.byName[name], index + 1 < tokens.count, - let amount = numberValue(tokens[index + 1]) { + let amount = numberValue(tokens[index + 1]) + { add(CalcFormatter.copyText(amount)) add(definition.code) index += 2 diff --git a/Tinycast/Features/Calculator/Service/CalculatorHistoryStore.swift b/Tinycast/Features/Calculator/Service/CalculatorHistoryStore.swift index 54a53b43..79bcd61c 100644 --- a/Tinycast/Features/Calculator/Service/CalculatorHistoryStore.swift +++ b/Tinycast/Features/Calculator/Service/CalculatorHistoryStore.swift @@ -32,7 +32,8 @@ final class CalculatorHistoryStore { fileURL = AppPaths.caches().appendingPathComponent("calculator-history.json") if let data = try? Data(contentsOf: fileURL), - let decoded = try? JSONDecoder().decode([CalcHistoryEntry].self, from: data) { + let decoded = try? JSONDecoder().decode([CalcHistoryEntry].self, from: data) + { entries = decoded } else { entries = [] diff --git a/Tinycast/Features/Calculator/Settings/MiscellaneousSettingsView.swift b/Tinycast/Features/Calculator/Settings/MiscellaneousSettingsView.swift index b25fd01f..82647f26 100644 --- a/Tinycast/Features/Calculator/Settings/MiscellaneousSettingsView.swift +++ b/Tinycast/Features/Calculator/Settings/MiscellaneousSettingsView.swift @@ -92,8 +92,8 @@ private struct CurrencyConsentSheet: View { Text( "Tinycast downloads exchange rates from \(CurrencyRateStore.provider) once a day and " - + "keeps a copy on your Mac. No account, no identifiers, nothing you type. " - + "Turning it off deletes the cached rates." + + "keeps a copy on your Mac. No account, no identifiers, nothing you type. " + + "Turning it off deletes the cached rates." ) .font(.callout) .foregroundStyle(.secondary) diff --git a/Tinycast/Features/Calculator/UI/CalculatorHistoryScreen.swift b/Tinycast/Features/Calculator/UI/CalculatorHistoryScreen.swift index fed43036..a5e6456c 100644 --- a/Tinycast/Features/Calculator/UI/CalculatorHistoryScreen.swift +++ b/Tinycast/Features/Calculator/UI/CalculatorHistoryScreen.swift @@ -129,8 +129,11 @@ struct CalculatorHistoryScreen: PaletteScreen { /// Actions menu content for a calculator-history entry, shown bottom-right like the other modes. @MainActor enum CalcHistoryActionsMenu { - static func content(entry: CalcHistoryEntry, core: AppCore, calcHistory: CalculatorHistoryStore) - -> PopoverMenuContent { + static func content( + entry: CalcHistoryEntry, core: AppCore, calcHistory: CalculatorHistoryStore + ) + -> PopoverMenuContent + { PopoverMenuContent( header: entry.expression, items: [ diff --git a/Tinycast/Features/Clipboard/Service/ClipboardManager.swift b/Tinycast/Features/Clipboard/Service/ClipboardManager.swift index 042c4220..bb111f17 100644 --- a/Tinycast/Features/Clipboard/Service/ClipboardManager.swift +++ b/Tinycast/Features/Clipboard/Service/ClipboardManager.swift @@ -101,7 +101,8 @@ final class ClipboardManager { if let sourceBundleID, settings.clipboardDisabledApps.contains(sourceBundleID) { return } if let text = pb.string(forType: .string), - !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + { guard text.count <= Self.maxTextLength else { return } store.addText(text, sourceBundleID: sourceBundleID) return diff --git a/Tinycast/Features/Clipboard/Service/Paster.swift b/Tinycast/Features/Clipboard/Service/Paster.swift index bd83fcea..48968871 100644 --- a/Tinycast/Features/Clipboard/Service/Paster.swift +++ b/Tinycast/Features/Clipboard/Service/Paster.swift @@ -120,7 +120,8 @@ enum Paster { let v = CGKeyCode(kVK_ANSI_V) guard let down = CGEvent(keyboardEventSource: source, virtualKey: v, keyDown: true), - let up = CGEvent(keyboardEventSource: source, virtualKey: v, keyDown: false) else { return } + let up = CGEvent(keyboardEventSource: source, virtualKey: v, keyDown: false) + else { return } down.flags = .maskCommand up.flags = .maskCommand diff --git a/Tinycast/Features/Clipboard/UI/ClipboardScreen.swift b/Tinycast/Features/Clipboard/UI/ClipboardScreen.swift index befd1d02..f72b7819 100644 --- a/Tinycast/Features/Clipboard/UI/ClipboardScreen.swift +++ b/Tinycast/Features/Clipboard/UI/ClipboardScreen.swift @@ -54,7 +54,8 @@ struct ClipboardScreen: PaletteScreen { guard old.id != nil else { return } let rows = rows if vm.query.trimmingCharacters(in: .whitespaces).isEmpty, old.id != new.id, let id = new.id, - let index = rows.firstIndex(where: { $0.id == id }) { + let index = rows.firstIndex(where: { $0.id == id }) + { vm.selection = index } scrollToFollow() diff --git a/Tinycast/Features/Emoji/Service/EmojiIndex.swift b/Tinycast/Features/Emoji/Service/EmojiIndex.swift index 9cc51b38..20804800 100644 --- a/Tinycast/Features/Emoji/Service/EmojiIndex.swift +++ b/Tinycast/Features/Emoji/Service/EmojiIndex.swift @@ -51,7 +51,8 @@ final class EmojiIndex { let nameScore = FuzzyMatch.score(query: q, candidate: entry.name) var best = nameScore if !entry.keywords.isEmpty, - let keywordScore = FuzzyMatch.score(query: q, candidate: entry.keywords) { + let keywordScore = FuzzyMatch.score(query: q, candidate: entry.keywords) + { best = max(best ?? Int.min, keywordScore - 500) } if let best { scored.append(ScoredEntry(entry: entry, score: best, order: order)) } diff --git a/Tinycast/Features/Emoji/Service/FrequentEmojiStore.swift b/Tinycast/Features/Emoji/Service/FrequentEmojiStore.swift index 5b72a59f..83039b10 100644 --- a/Tinycast/Features/Emoji/Service/FrequentEmojiStore.swift +++ b/Tinycast/Features/Emoji/Service/FrequentEmojiStore.swift @@ -25,7 +25,8 @@ final class FrequentEmojiStore { fileURL = AppPaths.caches().appendingPathComponent("emoji-frequency.json") if let data = try? Data(contentsOf: fileURL), - let decoded = try? JSONDecoder().decode([FrequentEmoji].self, from: data) { + let decoded = try? JSONDecoder().decode([FrequentEmoji].self, from: data) + { records = decoded } else { records = [] diff --git a/Tinycast/Features/Emoji/UI/EmojiScreen.swift b/Tinycast/Features/Emoji/UI/EmojiScreen.swift index bb77da60..f9446dee 100644 --- a/Tinycast/Features/Emoji/UI/EmojiScreen.swift +++ b/Tinycast/Features/Emoji/UI/EmojiScreen.swift @@ -92,8 +92,11 @@ struct EmojiScreen: PaletteScreen { /// Actions menu for a cell, shown bottom-right on right-click like `ClipboardActionsMenu`. @MainActor enum EmojiActionsMenu { - static func content(entry: EmojiEntry, core: AppCore, target: PasteTarget?) - -> PopoverMenuContent { + static func content( + entry: EmojiEntry, core: AppCore, target: PasteTarget? + ) + -> PopoverMenuContent + { PopoverMenuContent( header: entry.displayName, items: [ diff --git a/Tinycast/Features/HotKeys/Service/HotKeyManager.swift b/Tinycast/Features/HotKeys/Service/HotKeyManager.swift index 1500c381..878e1166 100644 --- a/Tinycast/Features/HotKeys/Service/HotKeyManager.swift +++ b/Tinycast/Features/HotKeys/Service/HotKeyManager.swift @@ -96,7 +96,8 @@ final class HotKeyManager { let previous = bindings[action] if let binding, let data = try? encoder.encode(binding), - let json = String(data: data, encoding: .utf8) { + let json = String(data: data, encoding: .utf8) + { bindings[action] = binding UserDefaults.standard.set(json, forKey: action.defaultsKey) } else { diff --git a/Tinycast/Features/HotKeys/Service/ShortcutCaptureSession.swift b/Tinycast/Features/HotKeys/Service/ShortcutCaptureSession.swift index 46b51d25..f4ac297c 100644 --- a/Tinycast/Features/HotKeys/Service/ShortcutCaptureSession.swift +++ b/Tinycast/Features/HotKeys/Service/ShortcutCaptureSession.swift @@ -40,7 +40,8 @@ final class ShortcutCaptureSession { hotKeys: hotKeys) } return nil // always consume: no beeps, no leaking keys to the window - }) { + }) + { monitors.append(monitor) } @@ -61,7 +62,8 @@ final class ShortcutCaptureSession { hotKeys: hotKeys) } return event - }) { + }) + { monitors.append(monitor) } @@ -71,7 +73,8 @@ final class ShortcutCaptureSession { handler: { [weak hotKeys] event in MainActor.assumeIsolated { hotKeys?.recordingAction = nil } return event - }) { + }) + { monitors.append(monitor) } @@ -134,8 +137,11 @@ final class ShortcutCaptureSession { commit(.doubleTap(modifier), action: action, hotKeys: hotKeys) } - private static func doubleTapModifiers(in flags: NSEvent.ModifierFlags) - -> Set { + private static func doubleTapModifiers( + in flags: NSEvent.ModifierFlags + ) + -> Set + { var held: Set = [] if flags.contains(.control) { held.insert(.control) } if flags.contains(.option) { held.insert(.option) } diff --git a/Tinycast/Features/HotKeys/UI/ShortcutRecorderPopover.swift b/Tinycast/Features/HotKeys/UI/ShortcutRecorderPopover.swift index 1de102b1..adfdafd7 100644 --- a/Tinycast/Features/HotKeys/UI/ShortcutRecorderPopover.swift +++ b/Tinycast/Features/HotKeys/UI/ShortcutRecorderPopover.swift @@ -49,7 +49,8 @@ struct ShortcutRecorderPopover: View { .padding(.vertical, Theme.Spacing.sm) .padding(placement.caretEdge == .top ? .top : .bottom, Theme.Size.calloutCaretHeight) .frame( - width: Theme.Size.shortcutPopover.width, height: Theme.Size.shortcutPopover.height) + width: Theme.Size.shortcutPopover.width, height: Theme.Size.shortcutPopover.height + ) // Stock glass owns its elevation, as in `PopoverMenu` — no hand-tuned shadow. .glassEffect( .regular, in: CalloutShape(caretEdge: placement.caretEdge, caretX: placement.caretX)) diff --git a/Tinycast/Features/Launcher/Model/LauncherRankingStore.swift b/Tinycast/Features/Launcher/Model/LauncherRankingStore.swift index 0d94a63e..505b6c35 100644 --- a/Tinycast/Features/Launcher/Model/LauncherRankingStore.swift +++ b/Tinycast/Features/Launcher/Model/LauncherRankingStore.swift @@ -33,7 +33,8 @@ final class LauncherRankingStore { self.now = now if let data = try? Data(contentsOf: self.fileURL), - let decoded = try? JSONDecoder().decode([LauncherRankingRecord].self, from: data) { + let decoded = try? JSONDecoder().decode([LauncherRankingRecord].self, from: data) + { records = decoded.filter { !$0.itemKey.isEmpty && !$0.query.isEmpty && $0.count > 0 } diff --git a/Tinycast/Features/Launcher/Service/AppIndex.swift b/Tinycast/Features/Launcher/Service/AppIndex.swift index 77f71a5a..b5c9a6ec 100644 --- a/Tinycast/Features/Launcher/Service/AppIndex.swift +++ b/Tinycast/Features/Launcher/Service/AppIndex.swift @@ -208,7 +208,8 @@ final class AppIndex { /// Replaces the quicklink slice and its built-ins together, so a toggle can't split them. func setQuicklinks(_ quicklinks: [Quicklink], commandsVisible: Bool) { - let entries = quicklinks + let entries = + quicklinks .filter(\.showsInRootSearch) .sorted(by: Quicklink.precedes) .map { quicklink in @@ -219,7 +220,8 @@ final class AppIndex { symbolName: quicklink.iconSymbol ?? QuicklinkDestination.detect(quicklink.link)?.defaultSymbol) } - let commands = commandsVisible + let commands = + commandsVisible ? CommandCatalog.all : CommandCatalog.all.filter { entry in CommandCatalog.command(for: entry).map { !$0.isQuicklinkCommand } ?? true @@ -239,7 +241,8 @@ final class AppIndex { } func updateSnippets(_ records: [StoredSnippet]) { - let entries = records + let entries = + records .filter { $0.snippet.isEnabled } .map { record in AppEntry( diff --git a/Tinycast/Features/Launcher/Service/AppLauncher.swift b/Tinycast/Features/Launcher/Service/AppLauncher.swift index a7be6bb6..9e726dad 100644 --- a/Tinycast/Features/Launcher/Service/AppLauncher.swift +++ b/Tinycast/Features/Launcher/Service/AppLauncher.swift @@ -45,7 +45,8 @@ enum AppLauncher { return } if let url = running?.bundleURL - ?? NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) { + ?? NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) + { // Dock-click semantics; a bare `activate()` does none of it reliably. NSWorkspace.shared.openApplication( at: url, configuration: NSWorkspace.OpenConfiguration()) diff --git a/Tinycast/Features/Launcher/Service/SettingsPaneScanner.swift b/Tinycast/Features/Launcher/Service/SettingsPaneScanner.swift index e5ca084d..71653106 100644 --- a/Tinycast/Features/Launcher/Service/SettingsPaneScanner.swift +++ b/Tinycast/Features/Launcher/Service/SettingsPaneScanner.swift @@ -81,7 +81,8 @@ enum SettingsPaneScanner { codes.append("en") for code in codes { if let entry = table[code] as? [String: Any], - let name = entry["CFBundleDisplayName"] as? String { + let name = entry["CFBundleDisplayName"] as? String + { return name } } diff --git a/Tinycast/Features/Onboarding/OnboardingView.swift b/Tinycast/Features/Onboarding/OnboardingView.swift index e5324490..dcd3f559 100644 --- a/Tinycast/Features/Onboarding/OnboardingView.swift +++ b/Tinycast/Features/Onboarding/OnboardingView.swift @@ -235,7 +235,8 @@ struct OnboardingView: View { .foregroundStyle(.secondary) } if step == 2 && model.importing { - Button {} label: { + Button { + } label: { HStack(spacing: Theme.Spacing.sm) { ProgressView().controlSize(.small) Text("Importing…") @@ -343,7 +344,8 @@ struct OnboardingView: View { private static let appIcon: NSImage = { if let name = Bundle.main.infoDictionary?["CFBundleIconFile"] as? String, let url = Bundle.main.url(forResource: name, withExtension: "icns"), - let image = NSImage(contentsOf: url) { + let image = NSImage(contentsOf: url) + { return image } return NSApp.applicationIconImage diff --git a/Tinycast/Features/Quicklinks/Model/Quicklink.swift b/Tinycast/Features/Quicklinks/Model/Quicklink.swift index 319aa13d..c30a67cd 100644 --- a/Tinycast/Features/Quicklinks/Model/Quicklink.swift +++ b/Tinycast/Features/Quicklinks/Model/Quicklink.swift @@ -52,7 +52,8 @@ struct Quicklink: Codable, Hashable, Identifiable, Sendable { case (.none, .some): return false case (.none, .none): let order = lhs.name.localizedCaseInsensitiveCompare(rhs.name) - return order != .orderedSame ? order == .orderedAscending + return order != .orderedSame + ? order == .orderedAscending : lhs.id.uuidString < rhs.id.uuidString } } diff --git a/Tinycast/Features/Quicklinks/Model/QuicklinkStore.swift b/Tinycast/Features/Quicklinks/Model/QuicklinkStore.swift index c45a49ac..8b63b5b2 100644 --- a/Tinycast/Features/Quicklinks/Model/QuicklinkStore.swift +++ b/Tinycast/Features/Quicklinks/Model/QuicklinkStore.swift @@ -182,9 +182,11 @@ final class QuicklinkStore { var value = draft value.name = draft.name.trimmingCharacters(in: .whitespacesAndNewlines) value.link = draft.link.trimmingCharacters(in: .whitespacesAndNewlines) - value.iconSymbol = draft.iconSymbol?.trimmingCharacters(in: .whitespacesAndNewlines) + value.iconSymbol = + draft.iconSymbol?.trimmingCharacters(in: .whitespacesAndNewlines) .nilIfEmpty - value.openWithBundleID = draft.openWithBundleID? + value.openWithBundleID = + draft.openWithBundleID? .trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty guard !value.name.isEmpty else { throw .emptyName } guard !value.link.isEmpty else { throw .emptyLink } @@ -192,8 +194,9 @@ final class QuicklinkStore { throw .invalidCharacter } // A templated link is only knowable once filled, so it is reported at open time. - guard QuicklinkDestination.containsPlaceholder(value.link) - || QuicklinkDestination.detect(value.link) != nil + guard + QuicklinkDestination.containsPlaceholder(value.link) + || QuicklinkDestination.detect(value.link) != nil else { throw .unresolvableLink } guard !quicklinks.contains(where: { diff --git a/Tinycast/Features/Quicklinks/Settings/QuicklinksSettingsView.swift b/Tinycast/Features/Quicklinks/Settings/QuicklinksSettingsView.swift index bb8156ba..100a7707 100644 --- a/Tinycast/Features/Quicklinks/Settings/QuicklinksSettingsView.swift +++ b/Tinycast/Features/Quicklinks/Settings/QuicklinksSettingsView.swift @@ -39,7 +39,9 @@ struct QuicklinksSettingsView: View { title: Text("Delete “\(quicklink.name)”?"), message: Text("Its global shortcut and launcher references will also be removed."), primaryButton: .destructive(Text("Delete")) { - Task { await core.quicklinkCoordinator.deleteQuicklink(id: quicklink.id, confirming: false) } + Task { + await core.quicklinkCoordinator.deleteQuicklink(id: quicklink.id, confirming: false) + } }, secondaryButton: .cancel()) } @@ -94,7 +96,7 @@ struct QuicklinksSettingsView: View { Text("Open in a new window") Text( "Ask the handler for a new window instead of reusing its frontmost tab. " - + "Only apps that accept a new-window argument can honour this.") + + "Only apps that accept a new-window argument can honour this.") } Picker(selection: $settings.quicklinkSelectionFallback) { ForEach(QuicklinkSelectionFallback.allCases) { option in diff --git a/Tinycast/Features/Quicklinks/UI/QuicklinkListScreen.swift b/Tinycast/Features/Quicklinks/UI/QuicklinkListScreen.swift index 5d3c3498..28c153ac 100644 --- a/Tinycast/Features/Quicklinks/UI/QuicklinkListScreen.swift +++ b/Tinycast/Features/Quicklinks/UI/QuicklinkListScreen.swift @@ -111,8 +111,9 @@ enum QuicklinkActionsMenu { }) items.append( quicklink.isPinned - ? PopoverMenuItem(title: "Unpin Quicklink", systemImage: "pin.slash", shortcut: "⌘P") - { core.quicklinkCoordinator.toggleQuicklinkPinned(id: quicklink.id) } + ? PopoverMenuItem(title: "Unpin Quicklink", systemImage: "pin.slash", shortcut: "⌘P") { + core.quicklinkCoordinator.toggleQuicklinkPinned(id: quicklink.id) + } : PopoverMenuItem(title: "Pin Quicklink", systemImage: "pin", shortcut: "⌘P") { core.quicklinkCoordinator.toggleQuicklinkPinned(id: quicklink.id) }) @@ -122,11 +123,13 @@ enum QuicklinkActionsMenu { ? "Hide from Root Search" : "Show in Root Search", systemImage: quicklink.showsInRootSearch ? "eye.slash" : "eye" ) { - core.quicklinkCoordinator.setQuicklinkShowsInRootSearch(!quicklink.showsInRootSearch, id: quicklink.id) + core.quicklinkCoordinator.setQuicklinkShowsInRootSearch( + !quicklink.showsInRootSearch, id: quicklink.id) }) // Revealing needs a real path, which a template lacks until it expands. if case .path(let path)? = QuicklinkDestination.detect(quicklink.link), - !QuicklinkDestination.containsPlaceholder(quicklink.link) { + !QuicklinkDestination.containsPlaceholder(quicklink.link) + { items.append( PopoverMenuItem(title: "Show in Finder", systemImage: "folder", shortcut: "⌘F") { core.paletteCoordinator.hidePalette(restoreFocus: false) diff --git a/Tinycast/Features/Snippets/Model/SnippetKeywordPolicy.swift b/Tinycast/Features/Snippets/Model/SnippetKeywordPolicy.swift index 317fec1b..2d3a0389 100644 --- a/Tinycast/Features/Snippets/Model/SnippetKeywordPolicy.swift +++ b/Tinycast/Features/Snippets/Model/SnippetKeywordPolicy.swift @@ -92,7 +92,8 @@ struct SnippetKeywordPolicy: Sendable { } mutating func update(_ keywords: [Keyword]) { - self.keywords = keywords + self.keywords = + keywords .filter { !$0.value.isEmpty && $0.deletionCount <= Self.maximumBufferLength } .sorted { if $0.value.count != $1.value.count { return $0.value.count > $1.value.count } diff --git a/Tinycast/Features/Snippets/Model/SnippetRepository.swift b/Tinycast/Features/Snippets/Model/SnippetRepository.swift index a31e947a..19394c5d 100644 --- a/Tinycast/Features/Snippets/Model/SnippetRepository.swift +++ b/Tinycast/Features/Snippets/Model/SnippetRepository.swift @@ -86,7 +86,8 @@ struct SnippetRepository: Sendable { var errorDescription: String? { switch self { case .conflict(let fileURL, _, _): - return "The snippet changed on disk. Reload it before saving or deleting. (\(fileURL.lastPathComponent))" + return + "The snippet changed on disk. Reload it before saving or deleting. (\(fileURL.lastPathComponent))" case .fileNotFound(let fileURL): return "The snippet file no longer exists. (\(fileURL.lastPathComponent))" case .invalidFileLocation(let fileURL): @@ -136,10 +137,11 @@ struct SnippetRepository: Sendable { let snippet = try SnippetMarkdownSerializer.parse( content: content, fileURL: fileURL) - records.append(StoredSnippet( - fileURL: fileURL, - snippet: snippet, - sourceRevision: SnippetSourceRevision(content: content))) + records.append( + StoredSnippet( + fileURL: fileURL, + snippet: snippet, + sourceRevision: SnippetSourceRevision(content: content))) } catch { issues.append(Issue(fileURL: fileURL, message: error.localizedDescription)) } @@ -245,16 +247,18 @@ struct SnippetRepository: Sendable { try FileManager.default.contentsOfDirectory( at: directory, includingPropertiesForKeys: [.isRegularFileKey], - options: [.skipsHiddenFiles]) - .filter { $0.pathExtension.lowercased() == "md" } - .filter(Self.isLoadableFile) - .sorted { $0.lastPathComponent < $1.lastPathComponent } + options: [.skipsHiddenFiles] + ) + .filter { $0.pathExtension.lowercased() == "md" } + .filter(Self.isLoadableFile) + .sorted { $0.lastPathComponent < $1.lastPathComponent } } // Keeps a directory or device node named `*.md` out; only non-files pay for resolving. private static func isLoadableFile(_ url: URL) -> Bool { if (try? url.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true { return true } - return (try? url.resolvingSymlinksInPath() + return + (try? url.resolvingSymlinksInPath() .resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true } diff --git a/Tinycast/Features/Snippets/Model/SnippetTemplateEngine.swift b/Tinycast/Features/Snippets/Model/SnippetTemplateEngine.swift index 118effee..d26b7125 100644 --- a/Tinycast/Features/Snippets/Model/SnippetTemplateEngine.swift +++ b/Tinycast/Features/Snippets/Model/SnippetTemplateEngine.swift @@ -237,7 +237,8 @@ enum SnippetTemplateEngine { case .literal(let value): result.append(value) case .clipboard(let offset, let modifiers): - let value = offset < context.clipboardHistory.count + let value = + offset < context.clipboardHistory.count ? context.clipboardHistory[offset] : "" result.append(apply(modifiers, to: value, encoding: encoding)) case .selection(let modifiers): @@ -267,15 +268,16 @@ enum SnippetTemplateEngine { } var nestedVisited = visitedIDs nestedVisited.insert(target.id) - result.append(expandText( - target.snippet.text, - snippets: snippets, - context: context, - userArguments: userArguments, - encoding: encoding, - depth: depth + 1, - visitedIDs: nestedVisited - )) + result.append( + expandText( + target.snippet.text, + snippets: snippets, + context: context, + userArguments: userArguments, + encoding: encoding, + depth: depth + 1, + visitedIDs: nestedVisited + )) } } return result @@ -336,7 +338,8 @@ enum SnippetTemplateEngine { var position = source.startIndex while position < source.endIndex, - let opening = source[position...].firstIndex(of: "{") { + let opening = source[position...].firstIndex(of: "{") + { if position < opening { segments.append(.literal(String(source[position.. Void) { diff --git a/Tinycast/Features/Snippets/Service/SnippetTextInjector.swift b/Tinycast/Features/Snippets/Service/SnippetTextInjector.swift index ebf1a048..3d1c3220 100644 --- a/Tinycast/Features/Snippets/Service/SnippetTextInjector.swift +++ b/Tinycast/Features/Snippets/Service/SnippetTextInjector.swift @@ -887,8 +887,7 @@ struct PasteboardSnapshot { pasteboardItem.setData(Data(), forType: ClipboardManager.internalType) else { return nil } } - for value in item.values where index != 0 || firstString == nil || value.type != .string - { + for value in item.values where index != 0 || firstString == nil || value.type != .string { guard pasteboardItem.setData(value.data, forType: value.type) else { return nil } } pasteboardItems.append(pasteboardItem) diff --git a/Tinycast/Features/Snippets/Settings/SnippetsSettingsView.swift b/Tinycast/Features/Snippets/Settings/SnippetsSettingsView.swift index bbf2a892..3723a77b 100644 --- a/Tinycast/Features/Snippets/Settings/SnippetsSettingsView.swift +++ b/Tinycast/Features/Snippets/Settings/SnippetsSettingsView.swift @@ -331,7 +331,8 @@ private struct SnippetEditorSheet: View { /// Replaces the selection or lands at the caret; appends when there is no usable one. private func insert(_ token: String) { if let selection, case .selection(let range) = selection.indices, - range.lowerBound >= text.startIndex, range.upperBound <= text.endIndex { + range.lowerBound >= text.startIndex, range.upperBound <= text.endIndex + { text.replaceSubrange(range, with: token) } else { text += token diff --git a/Tinycast/Features/Snippets/UI/SnippetExpansionCoordinator.swift b/Tinycast/Features/Snippets/UI/SnippetExpansionCoordinator.swift index fa8fa664..9eaf8fcf 100644 --- a/Tinycast/Features/Snippets/UI/SnippetExpansionCoordinator.swift +++ b/Tinycast/Features/Snippets/UI/SnippetExpansionCoordinator.swift @@ -180,9 +180,10 @@ final class SnippetExpansionCoordinator { automaticGeneration: UInt?, confirmation: String? ) { - guard let arguments = SnippetArgumentsPrompt.run( - snippetName: record.snippet.name, - arguments: missingArgs) + guard + let arguments = SnippetArgumentsPrompt.run( + snippetName: record.snippet.name, + arguments: missingArgs) else { injector.cancelArgumentPrompt( automaticGeneration: automaticGeneration, diff --git a/Tinycast/Features/SystemActions/Service/SystemActionRunner.swift b/Tinycast/Features/SystemActions/Service/SystemActionRunner.swift index cbabceee..851b71e1 100644 --- a/Tinycast/Features/SystemActions/Service/SystemActionRunner.swift +++ b/Tinycast/Features/SystemActions/Service/SystemActionRunner.swift @@ -44,8 +44,11 @@ enum SystemActionRunner { } /// What an action reports on success; only the ones whose effect is invisible do. - static func run(_ id: SystemAction.ID, previousApp: NSRunningApplication?) async throws - -> SystemActionFeedback? { + static func run( + _ id: SystemAction.ID, previousApp: NSRunningApplication? + ) async throws + -> SystemActionFeedback? + { switch id { case .lockScreen: try postKey(keyCode: CGKeyCode(kVK_ANSI_Q), flags: [.maskControl, .maskCommand]) @@ -65,13 +68,14 @@ enum SystemActionRunner { throw SystemActionFailure("The macOS screen saver could not be found.") } NSWorkspace.shared.openApplication( - at: url, configuration: NSWorkspace.OpenConfiguration()) { _, error in - guard let error else { return } - Task { @MainActor in - onAsyncFailure?( - .showScreenSaver, SystemActionFailure(error.localizedDescription)) - } + at: url, configuration: NSWorkspace.OpenConfiguration() + ) { _, error in + guard let error else { return } + Task { @MainActor in + onAsyncFailure?( + .showScreenSaver, SystemActionFailure(error.localizedDescription)) } + } case .playPause: try postMediaKey(16) case .nextTrack: @@ -103,7 +107,8 @@ enum SystemActionRunner { case .toggleAppearance: // The script returns the resulting state, so the confirmation can name it. let result = try await runAppleScript( - "tell application \"System Events\" to tell appearance preferences to set dark mode to not dark mode") + "tell application \"System Events\" to tell appearance preferences to set dark mode to not dark mode" + ) let dark = result.flag return SystemActionFeedback(dark ? "Dark Appearance" : "Light Appearance") case .toggleStageManager: @@ -244,8 +249,11 @@ enum SystemActionRunner { return elements } - private static func volumeAddress(element: AudioObjectPropertyElement) - -> AudioObjectPropertyAddress { + private static func volumeAddress( + element: AudioObjectPropertyElement + ) + -> AudioObjectPropertyAddress + { AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyVolumeScalar, mScope: kAudioDevicePropertyScopeOutput, @@ -284,7 +292,8 @@ enum SystemActionRunner { var muted: UInt32 = 0 var size = UInt32(MemoryLayout.size) if AudioObjectHasProperty(device, &address), - AudioObjectGetPropertyData(device, &address, 0, nil, &size, &muted) == noErr { + AudioObjectGetPropertyData(device, &address, 0, nil, &size, &muted) == noErr + { try setMuted(muted == 0, on: device) return } @@ -356,7 +365,8 @@ enum SystemActionRunner { for app in NSWorkspace.shared.runningApplications where app.activationPolicy == .regular && app.processIdentifier != ownPID - && app.processIdentifier != keptPID { + && app.processIdentifier != keptPID + { app.hide() } previousApp?.unhide() @@ -368,8 +378,9 @@ enum SystemActionRunner { let keys: Set = [ .volumeIsEjectableKey, .volumeIsInternalKey, .volumeIsLocalKey ] - let urls = FileManager.default.mountedVolumeURLs( - includingResourceValuesForKeys: Array(keys), options: [.skipHiddenVolumes]) ?? [] + let urls = + FileManager.default.mountedVolumeURLs( + includingResourceValuesForKeys: Array(keys), options: [.skipHiddenVolumes]) ?? [] let ejectable = urls.filter { url in guard let values = try? url.resourceValues(forKeys: keys) else { return false } return values.volumeIsEjectable == true @@ -390,14 +401,16 @@ enum SystemActionRunner { } } guard failures.isEmpty else { - throw SystemActionFailure("Some disks could not be ejected:\n\n" + failures.joined(separator: "\n")) + throw SystemActionFailure( + "Some disks could not be ejected:\n\n" + failures.joined(separator: "\n")) } return ejected } private static func mountedVolumeExists(_ url: URL) -> Bool { - let mounted = FileManager.default.mountedVolumeURLs( - includingResourceValuesForKeys: nil, options: [.skipHiddenVolumes]) ?? [] + let mounted = + FileManager.default.mountedVolumeURLs( + includingResourceValuesForKeys: nil, options: [.skipHiddenVolumes]) ?? [] return mounted.contains { $0.standardizedFileURL == url.standardizedFileURL } } @@ -445,8 +458,10 @@ enum SystemActionRunner { "Allow Tinycast to control your Mac in Accessibility settings, then try again.", settings: .accessibility) } - guard let app = NSRunningApplication.runningApplications( - withBundleIdentifier: "com.apple.notificationcenterui").first + guard + let app = NSRunningApplication.runningApplications( + withBundleIdentifier: "com.apple.notificationcenterui" + ).first else { return 0 } let root = AXUIElementCreateApplication(app.processIdentifier) var dismissed = 0 @@ -506,8 +521,9 @@ enum SystemActionRunner { private static func axChildren(_ element: AXUIElement) -> [AXUIElement] { var value: CFTypeRef? - guard AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &value) - == .success, + guard + AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &value) + == .success, let children = value as? [AXUIElement] else { return [] } return children @@ -596,7 +612,9 @@ enum SystemActionRunner { process.standardOutput = stdout process.standardError = stderr do { try process.run() } catch { - throw SystemActionFailure("\(URL(fileURLWithPath: executable).lastPathComponent) could not start: \(error.localizedDescription)") + throw SystemActionFailure( + "\(URL(fileURLWithPath: executable).lastPathComponent) could not start: \(error.localizedDescription)" + ) } process.waitUntilExit() let outData = stdout.fileHandleForReading.readDataToEndOfFile() diff --git a/Tinycast/Features/Uninstall/Model/UninstallProtection.swift b/Tinycast/Features/Uninstall/Model/UninstallProtection.swift index 1e4440be..8128b70e 100644 --- a/Tinycast/Features/Uninstall/Model/UninstallProtection.swift +++ b/Tinycast/Features/Uninstall/Model/UninstallProtection.swift @@ -61,8 +61,7 @@ enum UninstallProtection: String, Hashable, Sendable, CaseIterable { enum UninstallProtectionRules { /// Precedence is asserted; a SIP file is also root-owned, and "part of macOS" reads best. - static func classify(_ facts: PathFacts, environment: UninstallEnvironment) -> UninstallProtection - { + static func classify(_ facts: PathFacts, environment: UninstallEnvironment) -> UninstallProtection { guard facts.exists else { return .missing } if facts.isSystemRestricted || facts.volumeIsReadOnly { return .systemProtected } if facts.isUserImmutable { return .userLocked } diff --git a/Tinycast/Features/Uninstall/Service/UninstallScanner.swift b/Tinycast/Features/Uninstall/Service/UninstallScanner.swift index 6cb188fa..13560994 100644 --- a/Tinycast/Features/Uninstall/Service/UninstallScanner.swift +++ b/Tinycast/Features/Uninstall/Service/UninstallScanner.swift @@ -121,7 +121,9 @@ enum UninstallScanner { } /// Serial: four directories of cheap symlink reads, and nothing here needs a walk. - private static func binRows(environment: UninstallEnvironment, bundlePath: String) throws + private static func binRows( + environment: UninstallEnvironment, bundlePath: String + ) throws -> [UninstallCandidate] { var rows: [UninstallCandidate] = [] @@ -174,7 +176,9 @@ enum UninstallScanner { } /// `lstat`, never `stat`: a symlink is judged as the link, not as whatever it points at. - private static func inspect(_ path: String, parent: ParentFacts?) + private static func inspect( + _ path: String, parent: ParentFacts? + ) -> (facts: PathFacts, isDirectory: Bool, byteSize: Int64)? { var info = stat() diff --git a/Tinycast/Features/Uninstall/Service/UninstallSession.swift b/Tinycast/Features/Uninstall/Service/UninstallSession.swift index df870e7b..81975cf7 100644 --- a/Tinycast/Features/Uninstall/Service/UninstallSession.swift +++ b/Tinycast/Features/Uninstall/Service/UninstallSession.swift @@ -108,7 +108,9 @@ final class UninstallSession { } /// Off-main: it opens a file. - private nonisolated static func makeTarget(url: URL, name: String, bundleID: String?) + private nonisolated static func makeTarget( + url: URL, name: String, bundleID: String? + ) -> UninstallTarget { let info = Bundle(url: url)?.infoDictionary diff --git a/Tinycast/Features/Uninstall/UI/UninstallScreen.swift b/Tinycast/Features/Uninstall/UI/UninstallScreen.swift index 151344f3..14c75e48 100644 --- a/Tinycast/Features/Uninstall/UI/UninstallScreen.swift +++ b/Tinycast/Features/Uninstall/UI/UninstallScreen.swift @@ -116,8 +116,7 @@ enum UninstallActionsMenu { core.uninstallCoordinator.showUninstallItemInFinder(candidate) }) items.append( - PopoverMenuItem(title: "Show Info in Finder", systemImage: "info.circle", shortcut: "⇧⌘I") - { + PopoverMenuItem(title: "Show Info in Finder", systemImage: "info.circle", shortcut: "⇧⌘I") { core.uninstallCoordinator.showUninstallItemInfo(candidate) }) return PopoverMenuContent(header: session.app?.name ?? candidate.name, items: items) diff --git a/Tinycast/Features/WindowManagement/WindowLayout.swift b/Tinycast/Features/WindowManagement/WindowLayout.swift index 14f0963a..3e37649f 100644 --- a/Tinycast/Features/WindowManagement/WindowLayout.swift +++ b/Tinycast/Features/WindowManagement/WindowLayout.swift @@ -24,8 +24,11 @@ enum WindowLayout { /// Places `size` inside `slot` per the anchor, when an app clamped itself larger. func place(_ size: CGSize, in slot: CGRect) -> CGRect { - func origin(_ axis: Axis, slotMin: CGFloat, slotLength: CGFloat, length: CGFloat) - -> CGFloat { + func origin( + _ axis: Axis, slotMin: CGFloat, slotLength: CGFloat, length: CGFloat + ) + -> CGFloat + { switch axis { case .min: return slotMin case .center: return slotMin + (slotLength - length) / 2 @@ -204,8 +207,11 @@ enum WindowLayout { } } - private static func displayPlacement(_ input: Input, from host: Screen, gap: CGFloat) - -> Placement? { + private static func displayPlacement( + _ input: Input, from host: Screen, gap: CGFloat + ) + -> Placement? + { let ordered = ordered(input.screens) // A single display makes both commands a quiet no-op rather than a pointless re-place. guard ordered.count > 1, let index = ordered.firstIndex(where: { $0.id == host.id }) @@ -383,8 +389,11 @@ enum WindowLayout { return rounded(clamped(centred, into: canvas)) } - private static func nudged(_ frame: CGRect, in canvas: CGRect, command: WindowCommand.ID) - -> CGRect { + private static func nudged( + _ frame: CGRect, in canvas: CGRect, command: WindowCommand.ID + ) + -> CGRect + { let dx = (canvas.width * stepFraction).rounded() let dy = (canvas.height * stepFraction).rounded() var moved = frame diff --git a/Tinycast/Features/WindowManagement/WindowMover.swift b/Tinycast/Features/WindowManagement/WindowMover.swift index a9f4663c..7150cf27 100644 --- a/Tinycast/Features/WindowManagement/WindowMover.swift +++ b/Tinycast/Features/WindowManagement/WindowMover.swift @@ -227,7 +227,9 @@ final class WindowMover { // MARK: - Screens /// Cocoa screens converted into the AX space `WindowLayout` works in. - private static func screens(_ screens: [NSScreen], geometry: AXGeometry) + private static func screens( + _ screens: [NSScreen], geometry: AXGeometry + ) -> [WindowLayout.Screen] { screens.enumerated().map { index, screen in @@ -278,8 +280,7 @@ final class WindowMover { return size } - private func axValue(_ element: AXUIElement, _ attribute: String, type: AXValueType) -> AXValue? - { + private func axValue(_ element: AXUIElement, _ attribute: String, type: AXValueType) -> AXValue? { var value: CFTypeRef? guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success, diff --git a/Tinycast/Palette/PaletteCoordinator.swift b/Tinycast/Palette/PaletteCoordinator.swift index 3dea73e4..6db697a3 100644 --- a/Tinycast/Palette/PaletteCoordinator.swift +++ b/Tinycast/Palette/PaletteCoordinator.swift @@ -30,7 +30,6 @@ final class PaletteCoordinator { ? windowController.previousApp : NSWorkspace.shared.frontmostApplication } - func togglePalette() { if windowController.isVisible, palette.mode == .launcher { hidePalette() diff --git a/Tinycast/Palette/PalettePanel.swift b/Tinycast/Palette/PalettePanel.swift index 25fac0df..7c507392 100644 --- a/Tinycast/Palette/PalettePanel.swift +++ b/Tinycast/Palette/PalettePanel.swift @@ -39,13 +39,15 @@ final class PalettePanel: NSPanel { if event.type == .keyDown, paletteState?.menuOpen == true, event.modifierFlags.isDisjoint(with: [.command, .control]), - !Self.menuNavKeys.contains(Int(event.keyCode)) { + !Self.menuNavKeys.contains(Int(event.keyCode)) + { return } if event.type == .keyDown, Int(event.keyCode) == kVK_Delete, event.modifierFlags.isDisjoint(with: [.command, .option, .control, .shift]), - onBareBackspace?() == true { + onBareBackspace?() == true + { return } // The controller owns the chords the field editor or a missing main menu would eat. diff --git a/Tinycast/Palette/PaletteWindowController.swift b/Tinycast/Palette/PaletteWindowController.swift index cfc2e518..53422281 100644 --- a/Tinycast/Palette/PaletteWindowController.swift +++ b/Tinycast/Palette/PaletteWindowController.swift @@ -77,7 +77,8 @@ final class PaletteWindowController: NSObject, NSWindowDelegate { core.palette.prepare(mode: .launcher) return } - popToRootTimer = Timer.scheduledTimer(withTimeInterval: timeout.interval, repeats: false) { [weak self] _ in + popToRootTimer = Timer.scheduledTimer(withTimeInterval: timeout.interval, repeats: false) { + [weak self] _ in MainActor.assumeIsolated { self?.popToRootTimer = nil self?.core.palette.prepare(mode: .launcher) @@ -149,7 +150,8 @@ final class PaletteWindowController: NSObject, NSWindowDelegate { else { return false } // The argument form steps back through the answers first, one key per field. if core.palette.mode == .quicklinkArguments, - let previous = core.quicklinkArguments.retreat() { + let previous = core.quicklinkArguments.retreat() + { core.palette.query = previous core.palette.selection = 0 return true diff --git a/Tinycast/Palette/RootPaletteView.swift b/Tinycast/Palette/RootPaletteView.swift index b564b492..28dd14cd 100644 --- a/Tinycast/Palette/RootPaletteView.swift +++ b/Tinycast/Palette/RootPaletteView.swift @@ -354,7 +354,8 @@ struct RootPaletteView: View { searchField // Compact pins favorites beside the field; expanded shows them as rows. if isCollapsed, settings.showFavoritesInCompactMode, - let launcher = screen as? LauncherScreen { + let launcher = screen as? LauncherScreen + { let slots = launcher.compactFavoriteSlots if !slots.isEmpty { CompactFavoritesRow( diff --git a/Tinycast/Platform/Images/IconCache.swift b/Tinycast/Platform/Images/IconCache.swift index 4e987aae..acf20f44 100644 --- a/Tinycast/Platform/Images/IconCache.swift +++ b/Tinycast/Platform/Images/IconCache.swift @@ -77,7 +77,8 @@ enum IconCache { let config = NSImage.SymbolConfiguration(pointSize: 21, weight: .medium) .applying(.init(paletteColors: [tint])) if let symbol = NSImage(systemSymbolName: name, accessibilityDescription: nil)? - .withSymbolConfiguration(config) { + .withSymbolConfiguration(config) + { return symbol } guard let asset = NSImage(named: name) else { return nil } @@ -160,7 +161,8 @@ enum IconCache { /// Rasterize the multi-rep icon into one square bitmap, with its decoded byte cost. private static func downsampled(_ source: NSImage) -> (NSImage, Int) { - rasterized(source, into: NSRect(origin: .zero, size: NSSize(width: displayPixel, height: displayPixel))) + rasterized( + source, into: NSRect(origin: .zero, size: NSSize(width: displayPixel, height: displayPixel))) } /// Draws `source` into `frame` on a `displayPixel`-square canvas. diff --git a/Tinycast/Platform/Signposts.swift b/Tinycast/Platform/Signposts.swift index 937a4efc..c91d9102 100644 --- a/Tinycast/Platform/Signposts.swift +++ b/Tinycast/Platform/Signposts.swift @@ -12,7 +12,9 @@ enum Signposts { return try work() } - static func interval(_ name: StaticString, around work: () async throws -> T) async rethrows + static func interval( + _ name: StaticString, around work: () async throws -> T + ) async rethrows -> T { let state = signposter.beginInterval(name) diff --git a/Tinycast/Windows/About/AboutView.swift b/Tinycast/Windows/About/AboutView.swift index 9b549902..32f70e04 100644 --- a/Tinycast/Windows/About/AboutView.swift +++ b/Tinycast/Windows/About/AboutView.swift @@ -12,7 +12,8 @@ struct AboutView: View { @MainActor private static let appIcon: NSImage = { if let name = Bundle.main.infoDictionary?["CFBundleIconFile"] as? String, let url = Bundle.main.url(forResource: name, withExtension: "icns"), - let image = NSImage(contentsOf: url) { + let image = NSImage(contentsOf: url) + { return image } return NSApp.applicationIconImage @@ -161,7 +162,11 @@ private struct AboutLinkRow: View { .foregroundStyle(hovered ? .secondary : .tertiary) } } label: { - Label { Text(link.title) } icon: { glyph } + Label { + Text(link.title) + } icon: { + glyph + } } .contentShape(Rectangle()) } diff --git a/Tinycast/Windows/Dialog/DialogController.swift b/Tinycast/Windows/Dialog/DialogController.swift index 2ac34e08..87389700 100644 --- a/Tinycast/Windows/Dialog/DialogController.swift +++ b/Tinycast/Windows/Dialog/DialogController.swift @@ -29,8 +29,11 @@ final class DialogController: NSObject, NSWindowDelegate { } /// Something already went wrong, unlike `confirm`; true if recovery was taken. - func reportFailure(title: String, message: String, symbol: String, recovery: String?) async - -> Bool { + func reportFailure( + title: String, message: String, symbol: String, recovery: String? + ) async + -> Bool + { var actions = [DialogAction(title: "OK", role: .cancel)] if let recovery { actions.append(DialogAction(title: recovery)) } // ↵ lands on the recovery action when there is one to take, not on the OK dismissal. diff --git a/docs/decisions.md b/docs/decisions.md index c046a9c4..518d59b0 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -304,6 +304,16 @@ For the same reason SwiftLint's `empty_count` rule is **disabled**: it flags the **What would change this:** several regular contributors, at which point re-run this experiment — but keep `isEmpty`/`empty_count` off, and verify against the harnesses before committing the result. +**Amended:** format-on-save is now **on** in `.vscode/settings.json`, by explicit request, with a +`.swift-format` at the repo root tuning `swift-format` to this tree — 4-space indent, `lineLength` 110, +`respectsExistingLineBreaks`, trailing commas off, and the rules that rewrite rather than lay out +disabled. That cuts the blast radius from **200 files of 200** at stock settings (which default to +2-space indent) to **58**. It does not cut it to zero, and the objection above still stands for those +58 — this entry is the record of what a formatter costs here, not a claim that one is now safe. +`EmojiData.generated.swift` is the sharpest edge: saving it rewrites ~4 000 lines, and generated files +are never hand-edited. Have `Scripts/gen-emoji.js` emit `// swift-format-ignore-file` before relying on +it. + ### 26a — SwiftLint carries the two comment rules The 100-character cap and the ban on stacked comment lines are `custom_rules` in `.swiftlint.yml`, diff --git a/docs/development.md b/docs/development.md index 35ff3ae0..701b4c9a 100644 --- a/docs/development.md +++ b/docs/development.md @@ -63,20 +63,28 @@ Xcode works out of the box and needs nothing here. Everything below is optional, use is your business — the repo prescribes none of it. VS Code gets code intelligence from SourceKit-LSP, which needs a `buildServer.json` because there is no -`Package.swift`. Generate it once: +`Package.swift`. Build once, then hand the log to the sync script — that writes both `buildServer.json` +and the flag database: ```sh brew install xcode-build-server -xcode-build-server config -project Tinycast.xcodeproj -scheme Tinycast \ - --build_root "$PWD/build/DerivedData" +xcodebuild -project Tinycast.xcodeproj -scheme Tinycast -configuration Debug \ + -derivedDataPath build/DerivedData build 2>&1 | tee /tmp/tinycast-build.log +./Scripts/sync-lsp.sh /tmp/tinycast-build.log ``` -`--build_root` must match the `-derivedDataPath` in `.vscode/tasks.json`, or the editor indexes a -different build than the one **F5** runs. The file is git-ignored because it embeds an absolute path, -and `sourcekit-lsp` looks for it at the workspace root by name, so it cannot live in a subfolder. +Both files are git-ignored because they embed absolute paths, and `sourcekit-lsp` looks for +`buildServer.json` at the workspace root by name, so it cannot live in a subfolder. After this the +**Build Tinycast.app (debug)** task (⌘⇧B) and **F5** re-run the script on every build, so new and +renamed files keep resolving. -Then populate the index with one build — the **Build Tinycast.app (debug)** task (⌘⇧B) or **F5**. That -task keeps `.compile` in sync afterwards, so new and renamed files keep resolving. +**Do not run `xcode-build-server config`.** It writes `kind: xcode`, and in that mode the server ignores +`.compile` entirely — it serves flags from a cache it scrapes out of `.xcactivitylog` instead. That +cache is only refreshed when `LogStoreManifest.plist` advances, and when the manifest stops updating +(it does) the editor silently pins itself to the source list from some older build: every reference to a +file added since reads *cannot find type X in scope*, in every file, until you restart the server. It +also mixes Release entries in with Debug and lets them win. `Scripts/sync-lsp.sh` keeps the mode +`manual`, where `.compile` is the single source of truth. ### Symbols in `Tests/` @@ -89,9 +97,12 @@ find in scope*. Measured on `fuzz-test.swift`: 60 errors with no entry, 0 with o ``` It reads the source lists from `run-tests.sh` itself, so they cannot drift from what the suite actually -compiles. The build task runs it too. Two things it has to get right, both of which fail silently -otherwise: every path is absolute, because `sourcekit-lsp` resolves the command itself and does not -apply `directory` to relative arguments; and the command carries an explicit `-sdk`. +compiles. `Scripts/sync-lsp.sh` runs it too. Three things it has to get right, all of which fail +silently otherwise: every path is absolute, because `sourcekit-lsp` resolves the command itself and does +not apply `directory` to relative arguments; the command carries an explicit `-sdk`; and each entry +claims **only its own harness** in `files`. The command still lists every shipped source it compiles, so +symbols resolve inside the harness — but claiming those sources too would hand them this three-file +command instead of the app's, and `.compile` is last-wins. Re-run it after adding a harness, then **Swift: Restart LSP Server** from the Command Palette — an already-running server does not re-read `.compile`. @@ -107,6 +118,26 @@ already-running server does not re-read `.compile`. the repo root excludes the generated files and the two off-limits files in `DesignSystem/Scrolling/`, and carries the two comment rules from [standards.md](standards.md#comments) as `custom_rules`. +## Formatting + +```sh +./Scripts/format.sh # format Tinycast/ and Tests/ in place +./Scripts/format.sh --check # report what would change, write nothing (exit 1 if any) +``` + +`swift-format` from the Xcode toolchain — the same binary sourcekit-lsp formats with, so ⌘S in VS Code +and this script cannot disagree. `.swift-format` at the repo root tunes it to this tree; without it the +stock config defaults to 2-space indent and rewrites all 200 files. + +Both `*.generated.swift` files are excluded: formatting one is hand-editing it, and the next +`node Scripts/gen-emoji.js` would revert it. swift-format also refuses any file that does not parse, so +a failure from either command is a syntax error rather than a tooling problem — and it is why ⌘S looks +like it does nothing while a file is mid-edit with unbalanced braces. + +**Read [decisions.md](decisions.md) entry 26 before leaning on this.** A formatter was rejected here on +measured evidence, and the entry stands: running it over the tree touched 68 files, and 67 of those +changed more than whitespace. + The config sticks to rules that catch defects and stays quiet about style, because **there is no formatter** — see [decisions.md](decisions.md) entry 26 for the measurements behind that. Formatting is Xcode's re-indent (⌃I), as it always has been. Two consequences worth knowing: