From e5bd7c436c887cd7db719d9b3490ec03802c23f2 Mon Sep 17 00:00:00 2001 From: 4evy Date: Fri, 10 Jul 2026 14:25:00 +0300 Subject: [PATCH] alt-tab-macos: build from source --- ...ere-with-filter-.count-for-Swift-5.1.patch | 39 ++ ...raLarge-with-.large-for-macOS-14-SDK.patch | 39 ++ ...ispatch-for-Liquid-Glass-with-SDK-14.patch | 193 +++++++++ ...-hide-settings-for-disabled-services.patch | 43 ++ pkgs/by-name/al/alt-tab-macos/package.nix | 396 +++++++++++++++++- .../stubs/AppCenterApplication.m | 7 + .../stubs/AppCenterCrashesStub.swift | 25 ++ .../alt-tab-macos/stubs/AppCenterStub.swift | 6 + .../stubs/ShortcutRecorder.modulemap | 5 + .../al/alt-tab-macos/stubs/SparkleStub.swift | 59 +++ 10 files changed, 797 insertions(+), 15 deletions(-) create mode 100644 pkgs/by-name/al/alt-tab-macos/0001-replace-count-where-with-filter-.count-for-Swift-5.1.patch create mode 100644 pkgs/by-name/al/alt-tab-macos/0002-replace-.extraLarge-with-.large-for-macOS-14-SDK.patch create mode 100644 pkgs/by-name/al/alt-tab-macos/0003-use-runtime-dispatch-for-Liquid-Glass-with-SDK-14.patch create mode 100644 pkgs/by-name/al/alt-tab-macos/0004-hide-settings-for-disabled-services.patch create mode 100644 pkgs/by-name/al/alt-tab-macos/stubs/AppCenterApplication.m create mode 100644 pkgs/by-name/al/alt-tab-macos/stubs/AppCenterCrashesStub.swift create mode 100644 pkgs/by-name/al/alt-tab-macos/stubs/AppCenterStub.swift create mode 100644 pkgs/by-name/al/alt-tab-macos/stubs/ShortcutRecorder.modulemap create mode 100644 pkgs/by-name/al/alt-tab-macos/stubs/SparkleStub.swift diff --git a/pkgs/by-name/al/alt-tab-macos/0001-replace-count-where-with-filter-.count-for-Swift-5.1.patch b/pkgs/by-name/al/alt-tab-macos/0001-replace-count-where-with-filter-.count-for-Swift-5.1.patch new file mode 100644 index 000000000000..3d26fdb7a744 --- /dev/null +++ b/pkgs/by-name/al/alt-tab-macos/0001-replace-count-where-with-filter-.count-for-Swift-5.1.patch @@ -0,0 +1,39 @@ +From 85a2e0440f8af42ae02cf7fee1119255b2459016 Mon Sep 17 00:00:00 2001 +From: nixpkgs +Date: Fri, 10 Jul 2026 12:21:11 +0300 +Subject: [PATCH 1/4] replace count(where:) with filter{}.count for Swift 5.10 + +--- + src/events/TrackpadEvents.swift | 2 +- + src/switcher/main-window/StatusIconsView.swift | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/events/TrackpadEvents.swift b/src/events/TrackpadEvents.swift +index 2f1909df..5e187064 100644 +--- a/src/events/TrackpadEvents.swift ++++ b/src/events/TrackpadEvents.swift +@@ -76,7 +76,7 @@ class TrackpadEvents { + // on macOS, the finger contact surface is not exposed in NSTouch, so we can't detect big contact == palm, for example + // the closest thing we can do to detect resting inputs is remove touches which have .phase == .stationary + let activeTouches = touches.filter { !$0.isResting && ($0.phase == .began || $0.phase == .moved) } +- let fingersDown = touches.count { $0.phase == .began || $0.phase == .moved || $0.phase == .stationary } ++ let fingersDown = touches.filter { $0.phase == .began || $0.phase == .moved || $0.phase == .stationary }.count + let requiredFingers = Preferences.nextWindowGesture.isThreeFinger() ? 3 : 4 + if SwitcherSession.isActive { + handleEventIfAppIsBeingUsed(fingersDown, activeTouches, requiredFingers) +diff --git a/src/switcher/main-window/StatusIconsView.swift b/src/switcher/main-window/StatusIconsView.swift +index beeb2a54..2f35bddc 100644 +--- a/src/switcher/main-window/StatusIconsView.swift ++++ b/src/switcher/main-window/StatusIconsView.swift +@@ -76,7 +76,7 @@ class StatusIconsView: FlippedView { + icons[Self.fullscreenIdx].visible = isFullscreen + icons[Self.minimizedIdx].visible = isMinimized + icons[Self.spaceIdx].visible = showSpace +- visibleCount = icons.count(where: { $0.visible }) ++ visibleCount = icons.filter { $0.visible }.count + } + + func setSpaceStar() { +-- +2.55.0 + diff --git a/pkgs/by-name/al/alt-tab-macos/0002-replace-.extraLarge-with-.large-for-macOS-14-SDK.patch b/pkgs/by-name/al/alt-tab-macos/0002-replace-.extraLarge-with-.large-for-macOS-14-SDK.patch new file mode 100644 index 000000000000..15e284d6611f --- /dev/null +++ b/pkgs/by-name/al/alt-tab-macos/0002-replace-.extraLarge-with-.large-for-macOS-14-SDK.patch @@ -0,0 +1,39 @@ +From 7313d220616e756c4bb4f2c8cc5cf6b6b2e4789c Mon Sep 17 00:00:00 2001 +From: nixpkgs +Date: Fri, 10 Jul 2026 12:21:11 +0300 +Subject: [PATCH 2/4] replace .extraLarge with .large for macOS 14 SDK + +--- + src/preferences/settings-window/SettingsWindow.swift | 2 +- + src/switcher/main-window/TilesView.swift | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/preferences/settings-window/SettingsWindow.swift b/src/preferences/settings-window/SettingsWindow.swift +index 866329ea..44f1da76 100644 +--- a/src/preferences/settings-window/SettingsWindow.swift ++++ b/src/preferences/settings-window/SettingsWindow.swift +@@ -462,7 +462,7 @@ class SettingsWindow: NSWindow { + searchField.sendsWholeSearchString = true + searchField.bezelStyle = .roundedBezel + if #available(macOS 26.0, *) { +- searchField.controlSize = .extraLarge ++ searchField.controlSize = .large + } else if #available(macOS 13.0, *) { + searchField.controlSize = .large + } +diff --git a/src/switcher/main-window/TilesView.swift b/src/switcher/main-window/TilesView.swift +index d6c7fb69..b6f374c9 100644 +--- a/src/switcher/main-window/TilesView.swift ++++ b/src/switcher/main-window/TilesView.swift +@@ -142,7 +142,7 @@ class TilesView { + searchField.sendsWholeSearchString = true + searchField.bezelStyle = .roundedBezel + if #available(macOS 26.0, *) { +- searchField.controlSize = .extraLarge ++ searchField.controlSize = .large + } else if #available(macOS 13.0, *) { + searchField.controlSize = .large + } else { +-- +2.55.0 + diff --git a/pkgs/by-name/al/alt-tab-macos/0003-use-runtime-dispatch-for-Liquid-Glass-with-SDK-14.patch b/pkgs/by-name/al/alt-tab-macos/0003-use-runtime-dispatch-for-Liquid-Glass-with-SDK-14.patch new file mode 100644 index 000000000000..ebd914a0bd0e --- /dev/null +++ b/pkgs/by-name/al/alt-tab-macos/0003-use-runtime-dispatch-for-Liquid-Glass-with-SDK-14.patch @@ -0,0 +1,193 @@ +From 727f4c5aed5960bb429c7df0bbc3a547e05db7dd Mon Sep 17 00:00:00 2001 +From: nixpkgs +Date: Fri, 10 Jul 2026 12:21:12 +0300 +Subject: [PATCH 3/4] use runtime dispatch for Liquid Glass with SDK 14 + +--- + src/switcher/Appearance.swift | 2 +- + .../TilesPanelBackgroundView.swift | 122 +++++++++++------- + 2 files changed, 77 insertions(+), 47 deletions(-) + +diff --git a/src/switcher/Appearance.swift b/src/switcher/Appearance.swift +index dd033fa4..efb70c77 100644 +--- a/src/switcher/Appearance.swift ++++ b/src/switcher/Appearance.swift +@@ -83,7 +83,7 @@ class Appearance { + lightTheme() + } + // for Liquid Glass, we don't want a shadow around the panel +- if #available(macOS 26.0, *), currentStyle == .appIcons && LiquidGlass.canUsePrivateLook { ++ if #available(macOS 26.0, *), currentStyle == .appIcons && LiquidGlassEffectView.canUsePrivateLook { + enablePanelShadow = false + } else { + enablePanelShadow = true +diff --git a/src/switcher/main-window/TilesPanelBackgroundView.swift b/src/switcher/main-window/TilesPanelBackgroundView.swift +index cea0819c..99d12b06 100644 +--- a/src/switcher/main-window/TilesPanelBackgroundView.swift ++++ b/src/switcher/main-window/TilesPanelBackgroundView.swift +@@ -7,12 +7,81 @@ protocol EffectView: NSView { + } + + @available(macOS 26.0, *) +-extension NSGlassEffectView: EffectView { +- func updateAppearance() { +- cornerRadius = Appearance.windowCornerRadius ++class LiquidGlassEffectView: NSView, EffectView { ++ private typealias SetIntType = @convention(c) (AnyObject, Selector, Int) -> Void ++ private typealias SetCGFloatType = @convention(c) (AnyObject, Selector, CGFloat) -> Void ++ private typealias SetObjectType = @convention(c) (AnyObject, Selector, AnyObject) -> Void ++ private static let setVariantSelector = NSSelectorFromString("set_variant:") ++ private static let setStyleSelector = NSSelectorFromString("setStyle:") ++ private static let setCornerRadiusSelector = NSSelectorFromString("setCornerRadius:") ++ private static let setContentViewSelector = NSSelectorFromString("setContentView:") ++ private static let glassClass: AnyClass? = NSClassFromString("NSGlassEffectView") ++ ++ static let canUsePrivateLook: Bool = { ++ guard let cls = glassClass else { return false } ++ return class_getInstanceMethod(cls, setVariantSelector) != nil ++ }() ++ ++ private var glassView: NSView? ++ private var contentHost: NSView! ++ ++ var hostView: NSView { contentHost } ++ ++ convenience init(clear: Bool) { ++ self.init(frame: .zero) ++ wantsLayer = true ++ layer!.masksToBounds = true ++ ++ let glass: NSView ++ if let cls = Self.glassClass as? NSView.Type { ++ glass = cls.init(frame: .zero) ++ } else { ++ glass = NSView(frame: .zero) ++ } ++ glass.translatesAutoresizingMaskIntoConstraints = false ++ addSubview(glass) ++ NSLayoutConstraint.activate([ ++ glass.leadingAnchor.constraint(equalTo: leadingAnchor), ++ glass.trailingAnchor.constraint(equalTo: trailingAnchor), ++ glass.topAnchor.constraint(equalTo: topAnchor), ++ glass.bottomAnchor.constraint(equalTo: bottomAnchor), ++ ]) ++ glassView = glass ++ ++ // NSGlassEffectView.Style: regular = 0, clear = 1 ++ Self.invokeIntSetter(glass, Self.setStyleSelector, clear ? 1 : 0) ++ if clear { ++ Self.invokeIntSetter(glass, Self.setVariantSelector, 3) ++ } ++ contentHost = NSView(frame: .zero) ++ Self.invokeObjectSetter(glass, Self.setContentViewSelector, contentHost) ++ updateAppearance() ++ } ++ ++ private static func invokeIntSetter(_ target: AnyObject, _ selector: Selector, _ value: Int) { ++ guard let method = class_getInstanceMethod(object_getClass(target), selector) else { return } ++ let function = unsafeBitCast(method_getImplementation(method), to: SetIntType.self) ++ function(target, selector, value) + } + +- var hostView: NSView { contentView! } ++ private static func invokeCGFloatSetter(_ target: AnyObject, _ selector: Selector, _ value: CGFloat) { ++ guard let method = class_getInstanceMethod(object_getClass(target), selector) else { return } ++ let function = unsafeBitCast(method_getImplementation(method), to: SetCGFloatType.self) ++ function(target, selector, value) ++ } ++ ++ private static func invokeObjectSetter(_ target: AnyObject, _ selector: Selector, _ value: AnyObject) { ++ guard let method = class_getInstanceMethod(object_getClass(target), selector) else { return } ++ let function = unsafeBitCast(method_getImplementation(method), to: SetObjectType.self) ++ function(target, selector, value) ++ } ++ ++ func updateAppearance() { ++ let radius = Appearance.windowCornerRadius ++ layer?.cornerRadius = radius ++ guard let glass = glassView else { return } ++ Self.invokeCGFloatSetter(glass, Self.setCornerRadiusSelector, radius) ++ } + } + + class FrostedGlassEffectView: NSVisualEffectView, EffectView { +@@ -51,28 +120,6 @@ class FrostedGlassEffectView: NSVisualEffectView, EffectView { + } + } + +-/// The App Icons style uses the private `set_variant:` on `NSGlassEffectView` to get the macOS +-/// Cmd-Tab-like clear glass look. `style = .clear` alone renders nearly fully transparent, so the +-/// variant is what makes the panel visible. Lives here as a free helper (no NSGlassEffectView subclass). +-enum LiquidGlass { +- private static let setVariantSelector = NSSelectorFromString("set_variant:") +- private typealias SetVariantFn = @convention(c) (AnyObject, Selector, Int) -> Void +- +- static let canUsePrivateLook: Bool = { +- if #available(macOS 26.0, *) { +- return class_getInstanceMethod(object_getClass(NSGlassEffectView()), setVariantSelector) != nil +- } +- return false +- }() +- +- @available(macOS 26.0, *) +- static func applyClearVariant(_ view: NSGlassEffectView) { +- guard let method = class_getInstanceMethod(object_getClass(view), setVariantSelector) else { return } +- let f = unsafeBitCast(method_getImplementation(method), to: SetVariantFn.self) +- f(view, setVariantSelector, 3) +- } +-} +- + enum EffectViewKind { + case frosted + case liquidGlassRegular +@@ -82,7 +129,7 @@ enum EffectViewKind { + func requiredEffectViewKind() -> EffectViewKind { + if #available(macOS 26.0, *) { + if Preferences.effectiveAppearanceStyle(SwitcherSession.activeShortcutIndex) == .appIcons, +- LiquidGlass.canUsePrivateLook { ++ LiquidGlassEffectView.canUsePrivateLook { + return .liquidGlassClear + } + return .liquidGlassRegular +@@ -90,28 +137,11 @@ func requiredEffectViewKind() -> EffectViewKind { + return .frosted + } + +-@available(macOS 26.0, *) +-private func makeGlassEffectView(clear: Bool) -> NSGlassEffectView { +- let glass = NSGlassEffectView() +- glass.style = clear ? .clear : .regular +- if clear { +- LiquidGlass.applyClearVariant(glass) +- } +- // NSGlassEffectView only renders views embedded in `contentView`; this single host holds the +- // scroll view, search field and empty-state label so they all sit inside the glass. +- glass.contentView = NSView() +- glass.updateAppearance() +- // without this, there are weird shadows around the corners (most visible with .regular glass) +- glass.wantsLayer = true +- glass.layer!.masksToBounds = true +- return glass +-} +- + func makeEffectView(for kind: EffectViewKind) -> EffectView { + if #available(macOS 26.0, *) { + switch kind { + case .liquidGlassClear: +- return makeGlassEffectView(clear: true) ++ return LiquidGlassEffectView(clear: true) + case .liquidGlassRegular: + if Preferences.effectiveAppearanceStyle(SwitcherSession.activeShortcutIndex) == .appIcons { + Logger.error { +@@ -119,7 +149,7 @@ func makeEffectView(for kind: EffectViewKind) -> EffectView { + return "Private API set_variant is no longer available. macOS version: \(os.majorVersion).\(os.minorVersion).\(os.patchVersion)" + } + } +- return makeGlassEffectView(clear: false) ++ return LiquidGlassEffectView(clear: false) + case .frosted: + break + } +-- +2.55.0 + diff --git a/pkgs/by-name/al/alt-tab-macos/0004-hide-settings-for-disabled-services.patch b/pkgs/by-name/al/alt-tab-macos/0004-hide-settings-for-disabled-services.patch new file mode 100644 index 000000000000..11608646cf18 --- /dev/null +++ b/pkgs/by-name/al/alt-tab-macos/0004-hide-settings-for-disabled-services.patch @@ -0,0 +1,43 @@ +From cb68a038f6a0fc79cb60570bbec7bf3c45a5d8d2 Mon Sep 17 00:00:00 2001 +From: nixpkgs +Date: Fri, 10 Jul 2026 13:52:57 +0300 +Subject: [PATCH 4/4] hide settings for disabled services + +--- + .../settings-window/tabs/GeneralTab.swift | 13 ------------- + 1 file changed, 13 deletions(-) + +diff --git a/src/preferences/settings-window/tabs/GeneralTab.swift b/src/preferences/settings-window/tabs/GeneralTab.swift +index ab64ea9c..19dbef5e 100644 +--- a/src/preferences/settings-window/tabs/GeneralTab.swift ++++ b/src/preferences/settings-window/tabs/GeneralTab.swift +@@ -20,12 +20,6 @@ class GeneralTab { + ]) + let language = TableGroupView.Row(leftTitle: NSLocalizedString("Language", comment: ""), + rightViews: [LabelAndControl.makeDropdown("language", LanguagePreference.allCases, extraAction: setLanguageCallback)]) +- updatesPolicyDropdown = LabelAndControl.makeDropdown("updatePolicy", UpdatePolicyPreference.allCases) +- let checkForUpdates = NSButton(title: NSLocalizedString("Check for updates now…", comment: ""), target: nil, action: nil) +- checkForUpdates.onAction = { control in checkForUpdatesNow(control) } +- crashPolicyDropdown = LabelAndControl.makeDropdown("crashPolicy", CrashPolicyPreference.allCases) +- let crashPolicy = TableGroupView.Row(leftTitle: NSLocalizedString("Crash reports policy", comment: ""), +- rightViews: [crashPolicyDropdown!]) + for i in 0..` link order. + # + # Build shapes (exactly one of): + # stubSrc: single-file Swift stub (+ optional stubExtraFlags) + # objc = { ... }: Objective-C framework; see buildObjcFramework for keys + # + # Install extras (all optional): + # plistOverrides: extra CFBundle* keys merged into the framework Info.plist + # localizations: directory to scan for *.lproj dirs to copy into Resources + # xcassets: .xcassets directory compiled with actool into Assets.car + frameworks = [ + { + name = "ShortcutRecorder"; + objc = { + moduleMap = ./stubs/ShortcutRecorder.modulemap; + headerGlob = ''"${shortcutRecorderDir}"/include/ShortcutRecorder/*.h''; + sourceGlob = ''"${shortcutRecorderDir}"/*.m''; + includes = [ + "${shortcutRecorderDir}/include" + "${shortcutRecorderDir}/include/ShortcutRecorder" + ]; + sysFrameworks = [ + "Carbon" + "AppKit" + "Foundation" + "CoreData" + ]; + }; + # ShortcutRecorder looks up its own bundle via SRBundle() by identifier + plistOverrides = { + CFBundleIdentifier = "com.kulakov.ShortcutRecorder"; + CFBundleVersion = "3.1"; + CFBundleShortVersionString = "3.1"; + }; + localizations = shortcutRecorderDir; + xcassets = "${shortcutRecorderDir}/Images.xcassets"; + } + # Updates are managed by Nix, and upstream's AppCenter package depends on + # a prebuilt PLCrashReporter xcframework. API-compatible stubs keep both + # network-facing services disabled while preserving a source-only build. + { + name = "Sparkle"; + stubSrc = ./stubs/SparkleStub.swift; + } + { + name = "AppCenter"; + stubSrc = ./stubs/AppCenterStub.swift; + } + { + name = "AppCenterCrashes"; + stubSrc = ./stubs/AppCenterCrashesStub.swift; + stubExtraFlags = [ + "-I" + "$buildDir" + "-L" + "$buildDir" + "-lAppCenter" + ]; + } + ]; + + allFrameworks = lib.catAttrs "name" frameworks; + objcFrameworks = lib.filter (fw: fw ? objc) frameworks; + + # apple-sdk_26 cannot be used here because swiftPackages compiles its own modules + # against apple-sdk_14; adding it to buildInputs redirects SDKROOT and breaks Swift's + # foundational modules + commonSwiftFlags = [ + "-O" + "-swift-version" + "5" + "-disable-bridging-pch" + "-Xlinker" + "-platform_version" + "-Xlinker" + "macos" + "-Xlinker" + deploymentTarget + "-Xlinker" + sdkVersion + ]; + + # These are the non-localized files in upstream's PBXResourcesBuildPhase. + appResources = [ + "resources/icons/app/app.icns" + "resources/icons/menubar/*.pdf" + "resources/illustrations/*.heic" + "resources/*.otf" + "docs/contributors.md" + "docs/acknowledgments.md" + ]; + + infoPlistSubstitutions = version: { + API_DOMAIN = "alt-tab.app/api"; + APPCENTER_SECRET = ""; + CURRENT_PROJECT_VERSION = version; + DOMAIN = "alt-tab.app"; + EXECUTABLE_NAME = "AltTab"; + MACOSX_DEPLOYMENT_TARGET = deploymentTarget; + PRODUCT_BUNDLE_IDENTIFIER = "com.lwouis.alt-tab-macos"; + PRODUCT_NAME = "AltTab"; }; - sourceRoot = "."; + substitutePlistVariables = + substitutions: + lib.concatStringsSep " \\\n " ( + lib.mapAttrsToList ( + name: value: "--replace-fail ${lib.escapeShellArg "$(${name})"} ${lib.escapeShellArg value}" + ) substitutions + ); - nativeBuildInputs = [ unzip ]; + frameworkPlist = + name: extra: + toPlist ( + { + CFBundleExecutable = name; + CFBundleIdentifier = "com.lwouis.alt-tab-macos.${name}"; + CFBundleInfoDictionaryVersion = "6.0"; + CFBundleName = name; + CFBundlePackageType = "FMWK"; + CFBundleVersion = "1"; + } + // extra + ); + + # Shared swiftc invocation for emitting a dylib + swiftmodule for . + # `sourcesExpr` is inlined into the swiftc command line: either a single + # path (stub), or a bash array expansion like "${foo[@]}" (glob output). + swiftFrameworkLink = + { + name, + sourcesExpr, + extraFlags ? [ ], + }: + '' + swiftc "''${commonSwiftFlags[@]}" \ + -emit-module -emit-library \ + -module-name ${name} -module-link-name ${name} \ + -emit-module-path "$buildDir/${name}.swiftmodule" \ + ${lib.concatStringsSep " " extraFlags} \ + -Xlinker -install_name -Xlinker "@rpath/${name}.framework/${name}" \ + ${sourcesExpr} -o "$buildDir/lib${name}.dylib" + ''; + + # Build a single-file Swift stub framework. + buildSwiftStubFramework = fw: '' + nixLog "Building ${fw.name} (stub)" + ${swiftFrameworkLink { + inherit (fw) name; + sourcesExpr = "${fw.stubSrc}"; + extraFlags = fw.stubExtraFlags or [ ]; + }} + ''; + + # Build an Objective-C dynamic framework: symlink headers + modulemap under + # $buildDir/_headers//, compile each .m to .o, then link a dylib. + # moduleMap: path to a .modulemap file (copied in place) + # headerGlob: bash expression expanding to header paths (for ln -s) + # sourceGlob: bash expression expanding to .m paths (one .o per source) + # arc: enable -fobjc-arc (default true) + # includes: extra -I paths passed to clang + # sysFrameworks: macOS system frameworks to link + buildObjcFramework = + fw: + let + o = fw.objc; + arc = o.arc or true; + arcFlag = if arc then "-fobjc-arc" else "-fno-objc-arc"; + includeFlags = lib.concatMapStringsSep " " (p: ''-I "${p}"'') (o.includes or [ ]); + sysFrameworkFlags = lib.concatMapStringsSep " " (f: "-framework ${f}") (o.sysFrameworks or [ ]); + in + '' + ( + nixLog "Building ${fw.name}" + hdrDir="$buildDir/${fw.name}_headers/${fw.name}" + mkdir -p "$hdrDir" + for h in ${o.headerGlob}; do + ln -s "$(realpath "$h")" "$hdrDir/$(basename "$h")" + done + cp ${o.moduleMap} "$hdrDir/module.modulemap" + + objs=() + for f in ${o.sourceGlob}; do + obj="$buildDir/${fw.name}_$(basename "$f" .m).o" + clang ${arcFlag} -O2 -Wno-deprecated-declarations \ + -I "$buildDir/${fw.name}_headers" ${includeFlags} \ + -c "$f" -o "$obj" + objs+=("$obj") + done + + clang -fuse-ld=lld -dynamiclib ${lib.optionalString arc "-fobjc-arc"} "''${objs[@]}" \ + ${sysFrameworkFlags} \ + -install_name "@rpath/${fw.name}.framework/${fw.name}" \ + -o "$buildDir/lib${fw.name}.dylib" + ) + ''; + + # Dispatch to the right builder based on descriptor shape. + buildFramework = + fw: + if fw ? stubSrc then + buildSwiftStubFramework fw + else if fw ? objc then + buildObjcFramework fw + else + throw "framework ${fw.name} has no build descriptor"; + + # Module-map flags for the final AltTab swiftc invocation, so Swift code can + # `import `. Flat single line so the surrounding `\` + # continuation in the swiftc call stays intact. + objcModuleMapFlags = lib.concatMapStringsSep " " ( + fw: + ''-Xcc -fmodule-map-file="$buildDir/${fw.name}_headers/${fw.name}/module.modulemap" -Xcc -I"$buildDir/${fw.name}_headers"'' + ) objcFrameworks; + + # Compile an xcassets catalog into Assets.car in destDir + compileAssetCatalog = + { destDir, catalog }: + '' + actool --compile "${destDir}" \ + --platform macosx --minimum-deployment-target ${deploymentTarget} \ + --output-partial-info-plist /dev/null \ + "${catalog}" + ''; + + # Assemble one framework bundle: dylib + Info.plist + optional resources. + # Runs in the install phase where $app and $buildDir are set. + installFramework = fw: '' + fwDir="$app/Contents/Frameworks/${fw.name}.framework" + mkdir -p "$fwDir/Resources" + cp "$buildDir/lib${fw.name}.dylib" "$fwDir/${fw.name}" + printf '%s' ${ + lib.escapeShellArg (frameworkPlist fw.name (fw.plistOverrides or { })) + } > "$fwDir/Resources/Info.plist" + ${lib.optionalString (fw ? localizations) '' + cp -R "${fw.localizations}"/*.lproj "$fwDir/Resources/" + ''} + ${lib.optionalString (fw ? xcassets) (compileAssetCatalog { + destDir = "$fwDir/Resources"; + catalog = fw.xcassets; + })} + ''; +in +stdenv.mkDerivation (finalAttrs: { + pname = "alt-tab-macos"; + version = "11.4.3"; + + src = fetchFromGitHub { + owner = "lwouis"; + repo = "alt-tab-macos"; + tag = "v${finalAttrs.version}"; + hash = "sha256-izPiRGV8bD67rvOyNWShcpTbUujn6WHnzenPAMAlKoU="; + }; + + nativeBuildInputs = [ + swift + actool + lld + makeWrapper + rcodesign + ]; + + patches = [ + # Swift 5.10 compatibility: count(where:), .extraLarge, Liquid Glass + ./0001-replace-count-where-with-filter-.count-for-Swift-5.1.patch + ./0002-replace-.extraLarge-with-.large-for-macOS-14-SDK.patch + ./0003-use-runtime-dispatch-for-Liquid-Glass-with-SDK-14.patch + # Don't offer preferences for services disabled by the source-only stubs. + ./0004-hide-settings-for-disabled-services.patch + ]; + + # Remove trailing comma incompatible with Swift 5.10 + postPatch = '' + substituteInPlace Info.plist \ + ${substitutePlistVariables (infoPlistSubstitutions finalAttrs.version)} + + substituteInPlace src/secondary-windows/permission-window/PermissionsWindow.swift \ + --replace-fail \ + '"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",' \ + '"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"' + + # Swift 5.10 compat: additional call site using trailing-closure + # sugar for count(where:). See 0001-replace-count-where-...patch. + substituteInPlace src/util/UsageStats.swift \ + --replace-fail \ + 'getTimestamps(key).count { $0 >= threshold }' \ + 'getTimestamps(key).filter { $0 >= threshold }.count' + ''; + + dontConfigure = true; + + buildPhase = '' + runHook preBuild + + buildDir="$PWD/build" + mkdir -p "$buildDir" + + ${lib.toShellVars { inherit commonSwiftFlags; }} + + ${lib.concatMapStringsSep "\n" buildFramework frameworks} + + nixLog "Compiling AppCenterApplication" + clang -fobjc-arc -fmodules -fmodules-cache-path="$buildDir/module-cache" -O2 \ + -c ${./stubs/AppCenterApplication.m} -o "$buildDir/AppCenterApplication.o" + + nixLog "Compiling ObjCExceptionCatcher" + clang -fobjc-arc -fmodules -fmodules-cache-path="$buildDir/module-cache" -O2 \ + -c src/vendors/ObjCExceptionCatcher.m -o "$buildDir/ObjCExceptionCatcher.o" + + nixLog "Building AltTab" + mapfile -d ''' files < <(find src -name '*.swift' \ + -not -name '*Tests.swift' \ + -not -path '*/_test-support/*' \ + -not -path '*/experimentations/*' \ + -print0) + + swiftc "''${commonSwiftFlags[@]}" \ + -emit-executable -module-name AltTab \ + -import-objc-header alt-tab-macos-Bridging-Header.h \ + -Xcc -Isrc/ui \ + -Xcc -Isrc/vendors \ + -I "$buildDir" -L "$buildDir" \ + ${objcModuleMapFlags} \ + ${lib.concatMapStringsSep " " (fw: "-l${fw}") allFrameworks} \ + -F /System/Library/PrivateFrameworks \ + -framework SkyLight -framework Carbon -framework AppKit -framework Cocoa \ + -framework ScreenCaptureKit -framework ApplicationServices \ + -framework CoreGraphics -framework CoreText \ + -Xlinker -rpath -Xlinker "@executable_path/../Frameworks" \ + "$buildDir/AppCenterApplication.o" \ + "$buildDir/ObjCExceptionCatcher.o" \ + "''${files[@]}" -o "$buildDir/AltTab" + + runHook postBuild + ''; installPhase = '' runHook preInstall - mkdir -p $out/Applications - cp -r *.app $out/Applications + app="$out/Applications/AltTab.app" + mkdir -p "$app/Contents/"{MacOS,Frameworks,Resources} + + cp "$buildDir/AltTab" "$app/Contents/MacOS/AltTab" + + # Install each framework: dylib + Info.plist + optional resources + ${lib.concatMapStringsSep "\n" installFramework frameworks} + + cp Info.plist "$app/Contents/Info.plist" + printf 'APPL????' > "$app/Contents/PkgInfo" + + # Xcode flattens these PBXGroup resources into Contents/Resources. + cp ${lib.concatStringsSep " " appResources} "$app/Contents/Resources/" + cp -R resources/l10n/*.lproj "$app/Contents/Resources/" + + makeWrapper "$app/Contents/MacOS/AltTab" "$out/bin/alt-tab" runHook postInstall ''; + # Sign the complete app rather than each Mach-O independently, so its nested + # frameworks and resources are sealed as one valid bundle. Match upstream's + # hardened-runtime flags and entitlements on the main executable. This must + # remain ad-hoc: a stable designated requirement needs a private signing + # identity, which cannot be embedded in a public, sandboxed Nix build. + postFixup = '' + ${lib.getExe rcodesign} sign \ + --code-signature-flags runtime \ + --entitlements-xml-file ${finalAttrs.src}/alt_tab_macos.entitlements \ + "$out/Applications/AltTab.app" + ''; + passthru.updateScript = nix-update-script { }; meta = { description = "Windows alt-tab on macOS"; homepage = "https://alt-tab.app"; - license = lib.licenses.gpl3Plus; + license = with lib.licenses; [ + gpl3Plus + cc-by-40 # ShortcutRecorder + ]; + mainProgram = "alt-tab"; maintainers = with lib.maintainers; [ _4evy emilytrau Br1ght0ne ]; platforms = lib.platforms.darwin; - sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ]; + sourceProvenance = with lib.sourceTypes; [ fromSource ]; }; }) diff --git a/pkgs/by-name/al/alt-tab-macos/stubs/AppCenterApplication.m b/pkgs/by-name/al/alt-tab-macos/stubs/AppCenterApplication.m new file mode 100644 index 000000000000..497e8f107ab6 --- /dev/null +++ b/pkgs/by-name/al/alt-tab-macos/stubs/AppCenterApplication.m @@ -0,0 +1,7 @@ +#import + +@interface AppCenterApplication : NSApplication +@end + +@implementation AppCenterApplication +@end diff --git a/pkgs/by-name/al/alt-tab-macos/stubs/AppCenterCrashesStub.swift b/pkgs/by-name/al/alt-tab-macos/stubs/AppCenterCrashesStub.swift new file mode 100644 index 000000000000..c7f270eb304d --- /dev/null +++ b/pkgs/by-name/al/alt-tab-macos/stubs/AppCenterCrashesStub.swift @@ -0,0 +1,25 @@ +import Foundation + +@objc public enum UserConfirmation: UInt { + case send = 0 + case dontSend = 1 + case always = 2 +} + +@objc public class ErrorReport: NSObject {} + +@objc public class ErrorAttachmentLog: NSObject { + @objc public class func attachment(withText text: String, filename: String) -> ErrorAttachmentLog? { + return ErrorAttachmentLog() + } +} + +@objc public protocol CrashesDelegate: NSObjectProtocol { + @objc optional func attachments(with crashes: Crashes, for errorReport: ErrorReport) -> [ErrorAttachmentLog]? +} + +@objc public class Crashes: NSObject { + @objc public static weak var delegate: CrashesDelegate? + @objc public static var userConfirmationHandler: (([ErrorReport]) -> Bool)? + @objc public static func notify(with confirmation: UserConfirmation) {} +} diff --git a/pkgs/by-name/al/alt-tab-macos/stubs/AppCenterStub.swift b/pkgs/by-name/al/alt-tab-macos/stubs/AppCenterStub.swift new file mode 100644 index 000000000000..19ddf7d259ae --- /dev/null +++ b/pkgs/by-name/al/alt-tab-macos/stubs/AppCenterStub.swift @@ -0,0 +1,6 @@ +import Foundation + +@objc public class AppCenter: NSObject { + @objc public static var networkRequestsAllowed: Bool = false + @objc public static func start(withAppSecret appSecret: String, services: [AnyClass]) {} +} diff --git a/pkgs/by-name/al/alt-tab-macos/stubs/ShortcutRecorder.modulemap b/pkgs/by-name/al/alt-tab-macos/stubs/ShortcutRecorder.modulemap new file mode 100644 index 000000000000..d81d400a5cee --- /dev/null +++ b/pkgs/by-name/al/alt-tab-macos/stubs/ShortcutRecorder.modulemap @@ -0,0 +1,5 @@ +module ShortcutRecorder { + umbrella header "ShortcutRecorder.h" + export * + module * { export * } +} diff --git a/pkgs/by-name/al/alt-tab-macos/stubs/SparkleStub.swift b/pkgs/by-name/al/alt-tab-macos/stubs/SparkleStub.swift new file mode 100644 index 000000000000..2645dd9b3aa9 --- /dev/null +++ b/pkgs/by-name/al/alt-tab-macos/stubs/SparkleStub.swift @@ -0,0 +1,59 @@ +import Foundation +import AppKit + +@objc public protocol SPUUpdaterDelegate: NSObjectProtocol { + @objc optional func updaterDidNotFindUpdate(_ updater: SPUUpdater) +} + +@objc public class SUAppcastItem: NSObject { + @objc public var displayVersionString: String + + @objc public init(displayVersionString: String = "") { + self.displayVersionString = displayVersionString + } +} + +@objc public class SPUUpdater: NSObject { + private weak var delegate: SPUUpdaterDelegate? + @objc public var automaticallyDownloadsUpdates: Bool = false + @objc public var automaticallyChecksForUpdates: Bool = false + @objc public var sessionInProgress: Bool = false + + @objc public init(delegate: SPUUpdaterDelegate? = nil) { + self.delegate = delegate + } + + @objc public func checkForUpdateInformation() { + delegate?.updaterDidNotFindUpdate?(self) + } +} + +@objc public class SPUStandardUpdaterController: NSObject { + @objc public let updater: SPUUpdater + + @objc public init(startingUpdater: Bool, updaterDelegate: SPUUpdaterDelegate?, userDriverDelegate: Any?) { + updater = SPUUpdater(delegate: updaterDelegate) + } + @objc public func startUpdater() {} + @objc public func checkForUpdates(_ sender: Any?) { + updater.checkForUpdateInformation() + showNixUpdateMessage() + } +} + +@objc public class SUUpdater: NSObject { + private static let instance = SUUpdater() + @objc public class func shared() -> SUUpdater { return instance } + @objc public var automaticallyDownloadsUpdates: Bool = false + @objc public var automaticallyChecksForUpdates: Bool = false + @objc public func checkForUpdates(_ sender: Any?) { + showNixUpdateMessage() + } +} + +private func showNixUpdateMessage() { + let alert = NSAlert() + alert.messageText = "Updates are managed by Nix" + alert.informativeText = "Update AltTab through the Nix profile or system configuration that installed it." + alert.runModal() +}