alt-tab-macos: build from source
This commit is contained in:
+39
@@ -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
|
||||
|
||||
+39
@@ -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
|
||||
|
||||
+193
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user