alt-tab-macos: build from source

This commit is contained in:
4evy
2026-07-10 14:25:00 +03:00
parent 220ea10b4b
commit e5bd7c436c
10 changed files with 797 additions and 15 deletions
@@ -0,0 +1,39 @@
From 85a2e0440f8af42ae02cf7fee1119255b2459016 Mon Sep 17 00:00:00 2001
From: nixpkgs <nix@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
@@ -0,0 +1,39 @@
From 7313d220616e756c4bb4f2c8cc5cf6b6b2e4789c Mon Sep 17 00:00:00 2001
From: nixpkgs <nix@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
@@ -0,0 +1,193 @@
From 727f4c5aed5960bb429c7df0bbc3a547e05db7dd Mon Sep 17 00:00:00 2001
From: nixpkgs <nix@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
@@ -0,0 +1,43 @@
From cb68a038f6a0fc79cb60570bbec7bf3c45a5d8d2 Mon Sep 17 00:00:00 2001
From: nixpkgs <nix@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..<MenubarIconPreference.allCases.count {
let image = NSImage.initCopy("menubar-\(i)")
image.isTemplate = i < 2
@@ -45,13 +39,6 @@ class GeneralTab {
table.addRow(captureWindowsInBackground)
table.addNewTable()
table.addRow(language)
- table.addNewTable()
- table.addRow(leftViews: [TableGroupView.makeText(NSLocalizedString("Updates policy", comment: ""))],
- rightViews: [updatesPolicyDropdown!],
- secondaryViews: [checkForUpdates],
- secondaryViewsAlignment: .right,
- secondaryViewsTopGap: 8)
- table.addRow(crashPolicy)
let exportButton = NSButton(title: NSLocalizedString("Export settings…", comment: ""), target: nil, action: nil)
exportButton.onAction = { _ in exportSettings() }
let importButton = NSButton(title: NSLocalizedString("Import settings…", comment: ""), target: nil, action: nil)
--
2.55.0
+381 -15
View File
@@ -1,45 +1,411 @@
{
lib,
stdenvNoCC,
fetchurl,
swiftPackages,
fetchFromGitHub,
actool,
lld,
makeWrapper,
nix-update-script,
unzip,
rcodesign,
}:
stdenvNoCC.mkDerivation (finalAttrs: {
pname = "alt-tab-macos";
version = "11.4.2";
let
inherit (swiftPackages) stdenv swift;
src = fetchurl {
url = "https://github.com/lwouis/alt-tab-macos/releases/download/v${finalAttrs.version}/AltTab-${finalAttrs.version}.zip";
hash = "sha256-eqD9jXebqwXVgAPf7e5ZjY3Vh8ra1EDncOV2hZdpTuM=";
toPlist = lib.generators.toPlist { escape = true; };
deploymentTarget = "14.0";
sdkVersion = "26.0";
shortcutRecorderDir = "vendor/ShortcutRecorder/Sources/ShortcutRecorder";
# Framework descriptors drive both the build (compilation) and the install
# (bundle layout, Info.plist, optional resources). Order below is the final
# `-l<name>` 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 <name>.
# `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/<name>_headers/<name>/, 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 <ObjcFrameworkName>`. 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 ];
};
})
@@ -0,0 +1,7 @@
#import <Cocoa/Cocoa.h>
@interface AppCenterApplication : NSApplication
@end
@implementation AppCenterApplication
@end
@@ -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) {}
}
@@ -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]) {}
}
@@ -0,0 +1,5 @@
module ShortcutRecorder {
umbrella header "ShortcutRecorder.h"
export *
module * { export * }
}
@@ -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()
}