├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ └── config.yml
└── workflows
│ └── build.yml
├── .gitignore
├── .swift-version
├── .swiftformat
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── MenuBarExtraAccess-CI.xcscheme
├── Demo
├── Demo.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Demo.xcscheme
└── Demo
│ ├── Assets.xcassets
│ ├── 0.circle.fill.symbolset
│ │ ├── 0.circle.fill.svg
│ │ └── Contents.json
│ ├── 1.circle.fill.symbolset
│ │ ├── 1.circle.fill.svg
│ │ └── Contents.json
│ ├── 2.circle.fill.symbolset
│ │ ├── 2.circle.fill.svg
│ │ └── Contents.json
│ ├── 3.circle.fill.symbolset
│ │ ├── 3.circle.fill.svg
│ │ └── Contents.json
│ ├── 4.circle.fill.symbolset
│ │ ├── 4.circle.fill.svg
│ │ └── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── ContentView.swift
│ ├── Dock.swift
│ ├── MenuBarExtraAccessDemoApp.entitlements
│ ├── MenuBarExtraAccessDemoApp.swift
│ └── MenuBarView.swift
├── LICENSE
├── Package.swift
├── README.md
└── Sources
└── MenuBarExtraAccess
├── MenuBarExtra Window Introspection.swift
├── MenuBarExtraAccess.swift
├── MenuBarExtraUtils
├── MenuBarExtraUtils.swift
└── StatusItemIdentity.swift
├── NSControl Extensions.swift
├── NSEvent Extensions.swift
├── NSStatusItem Extensions.swift
├── NSWindow Extensions.swift
├── Unused
└── Unused Code.swift
└── Utilities.swift
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: orchetect
4 | # patreon: # Replace with a single Patreon username
5 | # open_collective: # Replace with a single Open Collective username
6 | # ko_fi: # Replace with a single Ko-fi username
7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | # liberapay: # Replace with a single Liberapay username
10 | # issuehunt: # Replace with a single IssueHunt username
11 | # otechie: # Replace with a single Otechie username
12 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: Create a bug report about a reproducible problem.
3 | labels: bug
4 | body:
5 | - type: textarea
6 | id: bug-description
7 | attributes:
8 | label: Bug Description, Steps to Reproduce, Crash Logs, Screenshots, etc.
9 | description: "A clear and concise description of the bug and steps to reproduce. Include system details (OS version) and build environment particulars (Xcode version, etc.)."
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Feature request
4 | url: https://github.com/orchetect/MenuBarExtraAccess/discussions
5 | about: Suggest new features or improvements.
6 | - name: I need help setting up or troubleshooting
7 | url: https://github.com/orchetect/MenuBarExtraAccess/discussions
8 | about: Questions not answered in the documentation, discussions forum, or example projects.
9 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths-ignore:
7 | - '**/*.md' # .md files anywhere in the repo
8 | - '**/LICENSE' # LICENSE files anywhere in the repo
9 | - '**/.gitignore' # .gitignore files anywhere in the repo
10 | - '**/*.png' # .png image files anywhere in the repo
11 | - '**/*.pdf' # .pdf files anywhere in the repo
12 |
13 | pull_request:
14 | branches: [main]
15 | paths-ignore:
16 | - '**/*.md' # .md files anywhere in the repo
17 | - '**/LICENSE' # LICENSE files anywhere in the repo
18 | - '**/.gitignore' # .gitignore files anywhere in the repo
19 | - '**/*.png' # .png image files anywhere in the repo
20 | - '**/*.pdf' # .pdf files anywhere in the repo
21 |
22 | workflow_dispatch:
23 |
24 | schedule:
25 | - cron: '35 13 * * *' # once a day @ 2:35am UTC (7:35am PST)
26 |
27 | env:
28 | SCHEME: "MenuBarExtraAccess-CI"
29 |
30 | jobs:
31 | macOS-13:
32 | name: macOS 13
33 | runs-on: macos-13
34 | steps:
35 | - uses: actions/checkout@main
36 | - uses: maxim-lobanov/setup-xcode@v1
37 | with:
38 | xcode-version: latest-stable
39 | - name: Build
40 | run: xcodebuild build -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "generic/platform=macOS,name=Any Mac" | xcbeautify && exit ${PIPESTATUS[0]}
41 | # - name: Unit Tests
42 | # run: xcodebuild test -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "platform=macOS" | xcbeautify && exit ${PIPESTATUS[0]}
43 |
44 | macOS-14:
45 | name: macOS 14
46 | runs-on: macos-14
47 | steps:
48 | - uses: actions/checkout@main
49 | - uses: maxim-lobanov/setup-xcode@v1
50 | with:
51 | xcode-version: latest-stable
52 | - name: Build
53 | run: xcodebuild build -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "generic/platform=macOS,name=Any Mac" | xcbeautify && exit ${PIPESTATUS[0]}
54 | # - name: Unit Tests
55 | # run: xcodebuild test -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "platform=macOS" | xcbeautify && exit ${PIPESTATUS[0]}
56 |
57 | macOS:
58 | name: macOS 15
59 | runs-on: macos-15
60 | steps:
61 | - uses: actions/checkout@main
62 | - uses: maxim-lobanov/setup-xcode@v1
63 | with:
64 | xcode-version: latest-stable
65 | - name: Build
66 | run: xcodebuild build -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "generic/platform=macOS,name=Any Mac" | xcbeautify && exit ${PIPESTATUS[0]}
67 | # - name: Unit Tests
68 | # run: xcodebuild test -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "platform=macOS" | xcbeautify && exit ${PIPESTATUS[0]}
69 |
70 | macOS-swift6:
71 | name: macOS 15 (Swift 6)
72 | runs-on: macos-15
73 | steps:
74 | - uses: actions/checkout@main
75 | - uses: maxim-lobanov/setup-xcode@v1
76 | with:
77 | xcode-version: latest-stable
78 | - name: Set Package to Swift 6.0
79 | run: swift package tools-version --set "6.0"
80 | - name: Build
81 | run: xcodebuild build -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "generic/platform=macOS,name=Any Mac" | xcbeautify && exit ${PIPESTATUS[0]}
82 | # - name: Unit Tests
83 | # run: xcodebuild test -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "platform=macOS" | xcbeautify && exit ${PIPESTATUS[0]}
84 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Custom
2 | [Dd]ev/
3 |
4 | # Xcode
5 |
6 | # macOS
7 | .DS_Store
8 |
9 | ## Build generated
10 | build/
11 | DerivedData/
12 |
13 | ## Various settings
14 | *.pbxuser
15 | !default.pbxuser
16 | *.mode1v3
17 | !default.mode1v3
18 | *.mode2v3
19 | !default.mode2v3
20 | *.perspectivev3
21 | !default.perspectivev3
22 | xcuserdata/
23 |
24 | ## Other
25 | *.moved-aside
26 | *.xccheckout
27 | *.xcscmblueprint
28 |
29 | ## Obj-C/Swift specific
30 | *.hmap
31 | *.ipa
32 | *.dSYM.zip
33 | *.dSYM
34 |
35 | ## Playgrounds
36 | timeline.xctimeline
37 | playground.xcworkspace
38 |
39 | ## SPM support in Xcode
40 | # .swiftpm - for shared CI schemes we need these checked in:
41 | # -> .swiftpm/xcode/package.xcworkspace
42 | # -> .swiftpm/xcode/xcshareddata/xcschemes/*.*
43 |
44 | # Swift Package Manager
45 | #
46 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
47 | Packages/
48 | Package.pins
49 | Package.resolved
50 | .build/
51 |
52 | # CocoaPods
53 | #
54 | # We recommend against adding the Pods directory to your .gitignore. However
55 | # you should judge for yourself, the pros and cons are mentioned at:
56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
57 |
58 | Pods/
59 |
60 | # Carthage
61 | #
62 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
63 | # Carthage/Checkouts
64 |
65 | Carthage/Build
66 |
67 | # fastlane
68 | #
69 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
70 | # screenshots whenever they are needed.
71 | # For more information about the recommended setup visit:
72 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
73 |
74 | fastlane/report.xml
75 | fastlane/Preview.html
76 | fastlane/screenshots/**/*.png
77 | fastlane/test_output
78 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 5.7
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | --acronyms ID,URL,UUID
2 | --allman false
3 | --assetliterals visual-width
4 | --beforemarks
5 | --binarygrouping 8,8
6 | --categorymark "MARK: %c"
7 | --classthreshold 0
8 | --closingparen balanced
9 | --closurevoid remove
10 | --commas inline
11 | --conflictmarkers reject
12 | --decimalgrouping ignore
13 | --elseposition same-line
14 | --emptybraces spaced
15 | --enumthreshold 0
16 | --exponentcase lowercase
17 | --exponentgrouping disabled
18 | --extensionacl on-declarations
19 | --extensionlength 0
20 | --extensionmark "MARK: - %t + %c"
21 | --fractiongrouping disabled
22 | --fragment false
23 | --funcattributes prev-line
24 | --groupedextension "MARK: %c"
25 | --guardelse auto
26 | --header "\n {file}\n MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess\n © {year} Steffan Andrews • Licensed under MIT License\n"
27 | --hexgrouping 4,8
28 | --hexliteralcase uppercase
29 | --ifdef no-indent
30 | --importgrouping alpha
31 | --indent 4
32 | --indentcase false
33 | --indentstrings true
34 | --lifecycle
35 | --lineaftermarks true
36 | --linebreaks lf
37 | --markcategories true
38 | --markextensions always
39 | --marktypes always
40 | --maxwidth 100
41 | --modifierorder
42 | --nevertrailing
43 | --nospaceoperators
44 | --nowrapoperators
45 | --octalgrouping 4,8
46 | --operatorfunc spaced
47 | --organizetypes actor,class,enum,struct
48 | --patternlet hoist
49 | --ranges spaced
50 | --redundanttype infer-locals-only
51 | --self remove
52 | --selfrequired
53 | --semicolons inline
54 | --shortoptionals always
55 | --smarttabs enabled
56 | --stripunusedargs always
57 | --structthreshold 0
58 | --tabwidth unspecified
59 | --trailingclosures
60 | --trimwhitespace nonblank-lines
61 | --typeattributes preserve
62 | --typemark "MARK: - %t"
63 | --varattributes preserve
64 | --voidtype void
65 | --wraparguments before-first
66 | --wrapcollections before-first
67 | --wrapconditions after-first
68 | --wrapparameters before-first
69 | --wrapreturntype preserve
70 | --wrapternary before-operators
71 | --wraptypealiases before-first
72 | --xcodeindentation enabled
73 | --yodaswap always
74 | --disable blankLinesAroundMark,consecutiveSpaces,preferKeyPath,redundantParens,sortDeclarations,sortedImports,unusedArguments
75 | --enable blankLinesBetweenImports,blockComments,isEmpty,wrapEnumCases
76 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/MenuBarExtraAccess-CI.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
35 |
41 |
42 |
43 |
44 |
45 |
55 |
56 |
62 |
63 |
69 |
70 |
71 |
72 |
74 |
75 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 60;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | E212FC1B295167E4007E27BB /* MenuBarExtraAccessDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E212FC1A295167E4007E27BB /* MenuBarExtraAccessDemoApp.swift */; };
11 | E212FC1F295167E5007E27BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E212FC1E295167E5007E27BB /* Assets.xcassets */; };
12 | E214EC0D2CAFA71200B91076 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E214EC0C2CAFA71200B91076 /* ContentView.swift */; };
13 | E2B6A8A42CFBBFDA00A4B22E /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B6A8A32CFBBFD600A4B22E /* Dock.swift */; };
14 | E2C1790B2CF45CCD00BE21A1 /* MenuBarExtraAccess in Frameworks */ = {isa = PBXBuildFile; productRef = E2C1790A2CF45CCD00BE21A1 /* MenuBarExtraAccess */; };
15 | E2ED15CD2CAFA6B100972C1A /* MenuBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2ED15CC2CAFA6B100972C1A /* MenuBarView.swift */; };
16 | /* End PBXBuildFile section */
17 |
18 | /* Begin PBXFileReference section */
19 | E212FC17295167E4007E27BB /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
20 | E212FC1A295167E4007E27BB /* MenuBarExtraAccessDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarExtraAccessDemoApp.swift; sourceTree = ""; };
21 | E212FC1E295167E5007E27BB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
22 | E212FC23295167E5007E27BB /* MenuBarExtraAccessDemoApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MenuBarExtraAccessDemoApp.entitlements; sourceTree = ""; };
23 | E214EC0C2CAFA71200B91076 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
24 | E2B6A8A32CFBBFD600A4B22E /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = ""; };
25 | E2ED15CC2CAFA6B100972C1A /* MenuBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarView.swift; sourceTree = ""; };
26 | /* End PBXFileReference section */
27 |
28 | /* Begin PBXFrameworksBuildPhase section */
29 | E212FC14295167E4007E27BB /* Frameworks */ = {
30 | isa = PBXFrameworksBuildPhase;
31 | buildActionMask = 2147483647;
32 | files = (
33 | E2C1790B2CF45CCD00BE21A1 /* MenuBarExtraAccess in Frameworks */,
34 | );
35 | runOnlyForDeploymentPostprocessing = 0;
36 | };
37 | /* End PBXFrameworksBuildPhase section */
38 |
39 | /* Begin PBXGroup section */
40 | E212FC0E295167E4007E27BB = {
41 | isa = PBXGroup;
42 | children = (
43 | E212FC19295167E4007E27BB /* Demo */,
44 | E212FC18295167E4007E27BB /* Products */,
45 | );
46 | sourceTree = "";
47 | };
48 | E212FC18295167E4007E27BB /* Products */ = {
49 | isa = PBXGroup;
50 | children = (
51 | E212FC17295167E4007E27BB /* Demo.app */,
52 | );
53 | name = Products;
54 | sourceTree = "";
55 | };
56 | E212FC19295167E4007E27BB /* Demo */ = {
57 | isa = PBXGroup;
58 | children = (
59 | E212FC1A295167E4007E27BB /* MenuBarExtraAccessDemoApp.swift */,
60 | E2ED15CC2CAFA6B100972C1A /* MenuBarView.swift */,
61 | E214EC0C2CAFA71200B91076 /* ContentView.swift */,
62 | E2B6A8A32CFBBFD600A4B22E /* Dock.swift */,
63 | E212FC1E295167E5007E27BB /* Assets.xcassets */,
64 | E212FC23295167E5007E27BB /* MenuBarExtraAccessDemoApp.entitlements */,
65 | );
66 | path = Demo;
67 | sourceTree = "";
68 | };
69 | /* End PBXGroup section */
70 |
71 | /* Begin PBXNativeTarget section */
72 | E212FC16295167E4007E27BB /* Demo */ = {
73 | isa = PBXNativeTarget;
74 | buildConfigurationList = E212FC26295167E5007E27BB /* Build configuration list for PBXNativeTarget "Demo" */;
75 | buildPhases = (
76 | E212FC13295167E4007E27BB /* Sources */,
77 | E212FC14295167E4007E27BB /* Frameworks */,
78 | E212FC15295167E4007E27BB /* Resources */,
79 | );
80 | buildRules = (
81 | );
82 | dependencies = (
83 | );
84 | name = Demo;
85 | packageProductDependencies = (
86 | E2C1790A2CF45CCD00BE21A1 /* MenuBarExtraAccess */,
87 | );
88 | productName = "MacControlCenterSlider Demo";
89 | productReference = E212FC17295167E4007E27BB /* Demo.app */;
90 | productType = "com.apple.product-type.application";
91 | };
92 | /* End PBXNativeTarget section */
93 |
94 | /* Begin PBXProject section */
95 | E212FC0F295167E4007E27BB /* Project object */ = {
96 | isa = PBXProject;
97 | attributes = {
98 | BuildIndependentTargetsInParallel = 1;
99 | LastSwiftUpdateCheck = 1420;
100 | LastUpgradeCheck = 1420;
101 | TargetAttributes = {
102 | E212FC16295167E4007E27BB = {
103 | CreatedOnToolsVersion = 14.2;
104 | };
105 | };
106 | };
107 | buildConfigurationList = E212FC12295167E4007E27BB /* Build configuration list for PBXProject "Demo" */;
108 | compatibilityVersion = "Xcode 13.0";
109 | developmentRegion = en;
110 | hasScannedForEncodings = 0;
111 | knownRegions = (
112 | en,
113 | Base,
114 | );
115 | mainGroup = E212FC0E295167E4007E27BB;
116 | packageReferences = (
117 | E2C179092CF45CCD00BE21A1 /* XCLocalSwiftPackageReference "../../MenuBarExtraAccess" */,
118 | );
119 | productRefGroup = E212FC18295167E4007E27BB /* Products */;
120 | projectDirPath = "";
121 | projectRoot = "";
122 | targets = (
123 | E212FC16295167E4007E27BB /* Demo */,
124 | );
125 | };
126 | /* End PBXProject section */
127 |
128 | /* Begin PBXResourcesBuildPhase section */
129 | E212FC15295167E4007E27BB /* Resources */ = {
130 | isa = PBXResourcesBuildPhase;
131 | buildActionMask = 2147483647;
132 | files = (
133 | E212FC1F295167E5007E27BB /* Assets.xcassets in Resources */,
134 | );
135 | runOnlyForDeploymentPostprocessing = 0;
136 | };
137 | /* End PBXResourcesBuildPhase section */
138 |
139 | /* Begin PBXSourcesBuildPhase section */
140 | E212FC13295167E4007E27BB /* Sources */ = {
141 | isa = PBXSourcesBuildPhase;
142 | buildActionMask = 2147483647;
143 | files = (
144 | E212FC1B295167E4007E27BB /* MenuBarExtraAccessDemoApp.swift in Sources */,
145 | E2ED15CD2CAFA6B100972C1A /* MenuBarView.swift in Sources */,
146 | E2B6A8A42CFBBFDA00A4B22E /* Dock.swift in Sources */,
147 | E214EC0D2CAFA71200B91076 /* ContentView.swift in Sources */,
148 | );
149 | runOnlyForDeploymentPostprocessing = 0;
150 | };
151 | /* End PBXSourcesBuildPhase section */
152 |
153 | /* Begin XCBuildConfiguration section */
154 | E212FC24295167E5007E27BB /* Debug */ = {
155 | isa = XCBuildConfiguration;
156 | buildSettings = {
157 | ALWAYS_SEARCH_USER_PATHS = NO;
158 | CLANG_ANALYZER_NONNULL = YES;
159 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
160 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
161 | CLANG_ENABLE_MODULES = YES;
162 | CLANG_ENABLE_OBJC_ARC = YES;
163 | CLANG_ENABLE_OBJC_WEAK = YES;
164 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
165 | CLANG_WARN_BOOL_CONVERSION = YES;
166 | CLANG_WARN_COMMA = YES;
167 | CLANG_WARN_CONSTANT_CONVERSION = YES;
168 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
169 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
170 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
171 | CLANG_WARN_EMPTY_BODY = YES;
172 | CLANG_WARN_ENUM_CONVERSION = YES;
173 | CLANG_WARN_INFINITE_RECURSION = YES;
174 | CLANG_WARN_INT_CONVERSION = YES;
175 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
176 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
177 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
178 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
179 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
180 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
181 | CLANG_WARN_STRICT_PROTOTYPES = YES;
182 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
183 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
184 | CLANG_WARN_UNREACHABLE_CODE = YES;
185 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
186 | COPY_PHASE_STRIP = NO;
187 | DEBUG_INFORMATION_FORMAT = dwarf;
188 | ENABLE_STRICT_OBJC_MSGSEND = YES;
189 | ENABLE_TESTABILITY = YES;
190 | GCC_C_LANGUAGE_STANDARD = gnu11;
191 | GCC_DYNAMIC_NO_PIC = NO;
192 | GCC_NO_COMMON_BLOCKS = YES;
193 | GCC_OPTIMIZATION_LEVEL = 0;
194 | GCC_PREPROCESSOR_DEFINITIONS = (
195 | "DEBUG=1",
196 | "$(inherited)",
197 | );
198 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
199 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
200 | GCC_WARN_UNDECLARED_SELECTOR = YES;
201 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
202 | GCC_WARN_UNUSED_FUNCTION = YES;
203 | GCC_WARN_UNUSED_VARIABLE = YES;
204 | MACOSX_DEPLOYMENT_TARGET = 14.0;
205 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
206 | MTL_FAST_MATH = YES;
207 | ONLY_ACTIVE_ARCH = YES;
208 | SDKROOT = macosx;
209 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
210 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
211 | SWIFT_VERSION = 6.0;
212 | };
213 | name = Debug;
214 | };
215 | E212FC25295167E5007E27BB /* Release */ = {
216 | isa = XCBuildConfiguration;
217 | buildSettings = {
218 | ALWAYS_SEARCH_USER_PATHS = NO;
219 | CLANG_ANALYZER_NONNULL = YES;
220 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
221 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
222 | CLANG_ENABLE_MODULES = YES;
223 | CLANG_ENABLE_OBJC_ARC = YES;
224 | CLANG_ENABLE_OBJC_WEAK = YES;
225 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
226 | CLANG_WARN_BOOL_CONVERSION = YES;
227 | CLANG_WARN_COMMA = YES;
228 | CLANG_WARN_CONSTANT_CONVERSION = YES;
229 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
230 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
231 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
232 | CLANG_WARN_EMPTY_BODY = YES;
233 | CLANG_WARN_ENUM_CONVERSION = YES;
234 | CLANG_WARN_INFINITE_RECURSION = YES;
235 | CLANG_WARN_INT_CONVERSION = YES;
236 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
237 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
238 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
239 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
240 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
241 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
242 | CLANG_WARN_STRICT_PROTOTYPES = YES;
243 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
244 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
245 | CLANG_WARN_UNREACHABLE_CODE = YES;
246 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
247 | COPY_PHASE_STRIP = NO;
248 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
249 | ENABLE_NS_ASSERTIONS = NO;
250 | ENABLE_STRICT_OBJC_MSGSEND = YES;
251 | GCC_C_LANGUAGE_STANDARD = gnu11;
252 | GCC_NO_COMMON_BLOCKS = YES;
253 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
254 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
255 | GCC_WARN_UNDECLARED_SELECTOR = YES;
256 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
257 | GCC_WARN_UNUSED_FUNCTION = YES;
258 | GCC_WARN_UNUSED_VARIABLE = YES;
259 | MACOSX_DEPLOYMENT_TARGET = 14.0;
260 | MTL_ENABLE_DEBUG_INFO = NO;
261 | MTL_FAST_MATH = YES;
262 | SDKROOT = macosx;
263 | SWIFT_COMPILATION_MODE = wholemodule;
264 | SWIFT_OPTIMIZATION_LEVEL = "-O";
265 | SWIFT_VERSION = 6.0;
266 | };
267 | name = Release;
268 | };
269 | E212FC27295167E5007E27BB /* Debug */ = {
270 | isa = XCBuildConfiguration;
271 | buildSettings = {
272 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
273 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
274 | CODE_SIGN_ENTITLEMENTS = Demo/MenuBarExtraAccessDemoApp.entitlements;
275 | CODE_SIGN_STYLE = Automatic;
276 | COMBINE_HIDPI_IMAGES = YES;
277 | CURRENT_PROJECT_VERSION = 1;
278 | DEVELOPMENT_TEAM = "";
279 | ENABLE_HARDENED_RUNTIME = YES;
280 | GENERATE_INFOPLIST_FILE = YES;
281 | INFOPLIST_KEY_CFBundleDisplayName = "MenuBarExtraAccess Demo";
282 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
283 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
284 | LD_RUNPATH_SEARCH_PATHS = (
285 | "$(inherited)",
286 | "@executable_path/../Frameworks",
287 | );
288 | MARKETING_VERSION = 1.0;
289 | PRODUCT_BUNDLE_IDENTIFIER = "com.orchetect.MenuBarExtraAccess.Demo${DEVELOPMENT_TEAM}";
290 | PRODUCT_NAME = "$(TARGET_NAME)";
291 | SUPPORTED_PLATFORMS = macosx;
292 | SUPPORTS_MACCATALYST = NO;
293 | SWIFT_EMIT_LOC_STRINGS = YES;
294 | };
295 | name = Debug;
296 | };
297 | E212FC28295167E5007E27BB /* Release */ = {
298 | isa = XCBuildConfiguration;
299 | buildSettings = {
300 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
301 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
302 | CODE_SIGN_ENTITLEMENTS = Demo/MenuBarExtraAccessDemoApp.entitlements;
303 | CODE_SIGN_STYLE = Automatic;
304 | COMBINE_HIDPI_IMAGES = YES;
305 | CURRENT_PROJECT_VERSION = 1;
306 | DEVELOPMENT_TEAM = "";
307 | ENABLE_HARDENED_RUNTIME = YES;
308 | GENERATE_INFOPLIST_FILE = YES;
309 | INFOPLIST_KEY_CFBundleDisplayName = "MenuBarExtraAccess Demo";
310 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
311 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
312 | LD_RUNPATH_SEARCH_PATHS = (
313 | "$(inherited)",
314 | "@executable_path/../Frameworks",
315 | );
316 | MARKETING_VERSION = 1.0;
317 | PRODUCT_BUNDLE_IDENTIFIER = "com.orchetect.MenuBarExtraAccess.Demo${DEVELOPMENT_TEAM}";
318 | PRODUCT_NAME = "$(TARGET_NAME)";
319 | SUPPORTED_PLATFORMS = macosx;
320 | SUPPORTS_MACCATALYST = NO;
321 | SWIFT_EMIT_LOC_STRINGS = YES;
322 | };
323 | name = Release;
324 | };
325 | /* End XCBuildConfiguration section */
326 |
327 | /* Begin XCConfigurationList section */
328 | E212FC12295167E4007E27BB /* Build configuration list for PBXProject "Demo" */ = {
329 | isa = XCConfigurationList;
330 | buildConfigurations = (
331 | E212FC24295167E5007E27BB /* Debug */,
332 | E212FC25295167E5007E27BB /* Release */,
333 | );
334 | defaultConfigurationIsVisible = 0;
335 | defaultConfigurationName = Release;
336 | };
337 | E212FC26295167E5007E27BB /* Build configuration list for PBXNativeTarget "Demo" */ = {
338 | isa = XCConfigurationList;
339 | buildConfigurations = (
340 | E212FC27295167E5007E27BB /* Debug */,
341 | E212FC28295167E5007E27BB /* Release */,
342 | );
343 | defaultConfigurationIsVisible = 0;
344 | defaultConfigurationName = Release;
345 | };
346 | /* End XCConfigurationList section */
347 |
348 | /* Begin XCLocalSwiftPackageReference section */
349 | E2C179092CF45CCD00BE21A1 /* XCLocalSwiftPackageReference "../../MenuBarExtraAccess" */ = {
350 | isa = XCLocalSwiftPackageReference;
351 | relativePath = ../../MenuBarExtraAccess;
352 | };
353 | /* End XCLocalSwiftPackageReference section */
354 |
355 | /* Begin XCSwiftPackageProductDependency section */
356 | E2C1790A2CF45CCD00BE21A1 /* MenuBarExtraAccess */ = {
357 | isa = XCSwiftPackageProductDependency;
358 | productName = MenuBarExtraAccess;
359 | };
360 | /* End XCSwiftPackageProductDependency section */
361 | };
362 | rootObject = E212FC0F295167E4007E27BB /* Project object */;
363 | }
364 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
44 |
46 |
52 |
53 |
54 |
55 |
61 |
63 |
69 |
70 |
71 |
72 |
74 |
75 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/0.circle.fill.symbolset/0.circle.fill.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
162 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/0.circle.fill.symbolset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "symbols" : [
7 | {
8 | "filename" : "0.circle.fill.svg",
9 | "idiom" : "universal"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/1.circle.fill.symbolset/1.circle.fill.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
162 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/1.circle.fill.symbolset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "symbols" : [
7 | {
8 | "filename" : "1.circle.fill.svg",
9 | "idiom" : "universal"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/2.circle.fill.symbolset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "symbols" : [
7 | {
8 | "filename" : "2.circle.fill.svg",
9 | "idiom" : "universal"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/3.circle.fill.symbolset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "symbols" : [
7 | {
8 | "filename" : "3.circle.fill.svg",
9 | "idiom" : "universal"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/4.circle.fill.symbolset/4.circle.fill.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
162 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/4.circle.fill.symbolset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "symbols" : [
7 | {
8 | "filename" : "4.circle.fill.svg",
9 | "idiom" : "universal"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x",
6 | "size" : "16x16"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "2x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "1x",
16 | "size" : "32x32"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "2x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "1x",
26 | "size" : "128x128"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "1x",
36 | "size" : "256x256"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "2x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "1x",
46 | "size" : "512x512"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "2x",
51 | "size" : "512x512"
52 | }
53 | ],
54 | "info" : {
55 | "author" : "xcode",
56 | "version" : 1
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/Demo/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuBarExtraAccessDemoApp.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct ContentView: View {
10 | @Binding var isMenu0Presented: Bool
11 | @Binding var isMenu1Presented: Bool
12 | @Binding var isMenu2Presented: Bool
13 | @Binding var isMenu3Presented: Bool
14 | @Binding var isMenu4Presented: Bool
15 |
16 | @State private var dock = Dock()
17 |
18 | var body: some View {
19 | VStack(spacing: 40) {
20 | HStack(spacing: 20) {
21 | MenuStateView(num: 4, isMenuPresented: $isMenu4Presented)
22 | MenuStateView(num: 3, isMenuPresented: $isMenu3Presented)
23 | MenuStateView(num: 2, isMenuPresented: $isMenu2Presented)
24 | MenuStateView(num: 1, isMenuPresented: $isMenu1Presented)
25 | MenuStateView(num: 0, isMenuPresented: $isMenu0Presented)
26 | }
27 |
28 | Toggle("Dock Icon Visible", isOn: $dock.isVisible)
29 |
30 | Text(
31 | """
32 | Try opening and closing the menus in the status bar. The toggles will update in response because the state binding is being updated. Clicking on the toggles will also open or close the menus by setting the binding value.")
33 |
34 | Note that due to how Apple implemented MenuBarExtra, menu-based status items hijack the main runloop and therefore you won't see state update here if you click on their status bar button. Additionally, they cannot be dismissed by setting the binding to false because no SwiftUI state updates occur while the menu is open - the user must select a menu item or dismiss the menu. But the menu can be opened programmatically by setting the binding to true.
35 | """
36 | )
37 | }
38 | .padding()
39 | .frame(minWidth: 400, minHeight: 350)
40 | .toggleStyle(.switch)
41 | }
42 |
43 | struct MenuStateView: View {
44 | let num: Int
45 | @Binding var isMenuPresented: Bool
46 |
47 | var body: some View {
48 | VStack(spacing: 20) {
49 | Image(systemName: "\(num).circle.fill")
50 | .resizable()
51 | .frame(width: 20, height: 20)
52 | Toggle("", isOn: $isMenuPresented)
53 | .toggleStyle(.switch)
54 | .labelsHidden()
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Demo/Demo/Dock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Dock.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 | import SwiftUI
9 |
10 | @Observable @MainActor final class Dock {
11 | var isVisible: Bool {
12 | get { NSApplication.shared.activationPolicy() == .regular }
13 | set { NSApplication.shared.setActivationPolicy(newValue ? .regular : .accessory) }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Demo/Demo/MenuBarExtraAccessDemoApp.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Demo/Demo/MenuBarExtraAccessDemoApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuBarExtraAccessDemoApp.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import SwiftUI
8 | import MenuBarExtraAccess
9 |
10 | @main
11 | struct MenuBarExtraAccessDemoApp: App {
12 | @State var isMenu0Presented: Bool = false
13 | @State var isMenu1Presented: Bool = false
14 | @State var isMenu2Presented: Bool = false
15 | @State var isMenu3Presented: Bool = false
16 | @State var isMenu4Presented: Bool = false
17 |
18 | @State var menu0StatusItem: NSStatusItem?
19 |
20 | var body: some Scene {
21 | // MARK: - Info Window
22 |
23 | WindowGroup {
24 | ContentView(
25 | isMenu0Presented: $isMenu0Presented,
26 | isMenu1Presented: $isMenu1Presented,
27 | isMenu2Presented: $isMenu2Presented,
28 | isMenu3Presented: $isMenu3Presented,
29 | isMenu4Presented: $isMenu4Presented
30 | )
31 | }
32 | .windowResizability(.contentSize)
33 |
34 | // MARK: - MenuBarExtra Scenes
35 |
36 | // 💡 NOTE: There are 5 menu extras here simply to demonstrate (and test)
37 | // various implementations of MenuBarExtra
38 |
39 | // 💡 NOTE: Even if the menu extras get reordered in the menu bar by the user (by holding Cmd
40 | // and dragging them), the indexes still remain consistent with the order in which
41 | // the MenuBarExtra definitions appear below.
42 |
43 | // MARK: Standard Menu
44 |
45 | MenuBarExtra("Menu: Index 0", systemImage: "0.circle.fill") {
46 | Button("Menu Item A") { print("Menu Item A") }
47 | Button("Menu Item B") { print("Menu Item B") }
48 | }
49 | .menuBarExtraAccess(index: 0, isPresented: $isMenu0Presented) { statusItem in
50 | // can do one-time setup of NSStatusItem here or if access to it
51 | // is needed later, it may be stored in a local state var like this:
52 | menu0StatusItem = statusItem
53 | }
54 | .menuBarExtraStyle(.menu)
55 |
56 | // MARK: Standard menu using named image
57 |
58 | MenuBarExtra("Menu: Index 1", image: "1.circle.fill") {
59 | Button("Menu Item A") { print("Menu Item A") }
60 | Button("Menu Item B") { print("Menu Item B") }
61 | Button("Menu Item C") { print("Menu Item C") }
62 | Button("Toggle Menu 0 Disabled State") {
63 | menu0StatusItem?.button?.appearsDisabled.toggle()
64 | }
65 | }
66 | .menuBarExtraAccess(index: 1, isPresented: $isMenu1Presented)
67 | .menuBarExtraStyle(.menu)
68 |
69 | // MARK: Window-style using systemImage
70 |
71 | MenuBarExtra("Menu: Index 2", systemImage: "2.circle.fill") {
72 | MenuBarView(index: 2, isMenuPresented: $isMenu2Presented)
73 | }
74 | .menuBarExtraAccess(index: 2, isPresented: $isMenu2Presented)
75 | .menuBarExtraStyle(.window)
76 |
77 | // MARK: Window-style using named image
78 |
79 | MenuBarExtra("Menu: Index 3", image: "3.circle.fill") {
80 | MenuBarView(index: 3, isMenuPresented: $isMenu3Presented)
81 | .introspectMenuBarExtraWindow(index: 3) { window in
82 | window.alphaValue = 0.5
83 | }
84 | }
85 | .menuBarExtraAccess(index: 3, isPresented: $isMenu3Presented)
86 | .menuBarExtraStyle(.window)
87 |
88 | // MARK: Window-style using custom label
89 |
90 | MenuBarExtra {
91 | MenuBarView(index: 4, isMenuPresented: $isMenu4Presented)
92 | } label: {
93 | Image(systemName: "4.circle.fill")
94 | Text("Four")
95 | }
96 | .menuBarExtraAccess(index: 4, isPresented: $isMenu4Presented)
97 | .menuBarExtraStyle(.window)
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Demo/Demo/MenuBarView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuBarExtraAccessDemoApp.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import SwiftUI
8 |
9 | struct MenuBarView: View {
10 | let index: Int
11 | @Binding var isMenuPresented: Bool
12 |
13 | var body: some View {
14 | VStack(spacing: 40) {
15 | Image(systemName: "\(index).circle.fill")
16 | .resizable()
17 | .foregroundColor(.secondary)
18 | .frame(width: 80, height: 80)
19 | Button("Close Menu") {
20 | isMenuPresented = false
21 | }
22 | }
23 | .padding()
24 | .frame(width: 250, height: 300)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Steffan Andrews - https://github.com/orchetect
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "MenuBarExtraAccess",
7 | platforms: [.macOS(.v10_15)],
8 | products: [
9 | .library(name: "MenuBarExtraAccess", targets: ["MenuBarExtraAccess"])
10 | ],
11 | targets: [
12 | .target(
13 | name: "MenuBarExtraAccess",
14 | swiftSettings: [
15 | // un-comment to enable debug logging
16 | // .define("MENUBAREXTRAACCESS_DEBUG_LOGGING=1")
17 | ]
18 | )
19 | ]
20 | )
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MenuBarExtraAccess
2 |
3 | [](https://developer.apple.com/swift) [](https://swiftpackageindex.com/orchetect/MenuBarExtraAccess) [](https://developer.apple.com/swift) [](https://github.com/orchetect/MenuBarExtraAccess/blob/main/LICENSE)
4 |
5 | #### **Gives you *Extra* access to SwiftUI `MenuBarExtra`.**
6 |
7 | - Programmatically hide, show, or toggle the menu (by way of a Bool binding)
8 | - Access to the underlying `NSStatusItem`
9 | - Access to the underlying `NSWindow` (when using the `.window` style)
10 | - Works with one or [multiple](#Multiple-MenuBarExtra) `MenuBarExtra`
11 | - Works with both [`menu`](#Standard-Menu-Style) and [`window`](#Window-Style) based styles (see [Known Issues](#Known-Issues))
12 |
13 | #### Why?
14 |
15 | There is no 1st-party MenuBarExtra API to get or set the menu presentation state, access the status item, or access the popup's NSWindow. (Still as of Xcode 16.1)
16 |
17 | #### Library Features
18 |
19 | - A new `.menuBarExtraAccess(isPresented:) { statusItem in }` scene modifier with
20 | - a binding to hide/show/toggle the menu, and
21 | - direct access to the `NSStatusItem` if needed
22 | - A new `.introspectMenuBarExtraWindow { window in }` view modifier passing in the `NSWindow` reference
23 | - Window-based menu extra status items now remain highlighted while the window is open so it feels more like a native menu
24 | - No private API used, so it's Mac App Store safe
25 |
26 | ## Getting Started
27 |
28 | The library is available as a Swift Package Manager (SPM) package.
29 |
30 | Use the URL `https://github.com/orchetect/MenuBarExtraAccess` when adding the library to a project or Swift package.
31 |
32 | Then import the library:
33 |
34 | ```swift
35 | import SwiftUI
36 | import MenuBarExtraAccess
37 | ```
38 |
39 | ### Standard Menu Style
40 |
41 | An example of showing the menu extra menu by clicking a button in a window:
42 |
43 | ```swift
44 | @main struct MyApp: App {
45 | @State var isMenuPresented: Bool = false
46 |
47 | var body: some Scene {
48 | WindowGroup {
49 | Button("Show Menu") { isMenuPresented = true }
50 | }
51 |
52 | MenuBarExtra("MyApp Menu", systemImage: "folder") {
53 | Button("Menu Item 1") { print("Menu Item 1") }
54 | Button("Menu Item 2") { print("Menu Item 2") }
55 | }
56 | .menuBarExtraStyle(.menu)
57 | .menuBarExtraAccess(isPresented: $isMenuPresented) { statusItem in // <-- the magic ✨
58 | // access status item or store it in a @State var
59 | }
60 | }
61 | }
62 | ```
63 |
64 | ### Window Style
65 |
66 | An example of a button in the popup window dismissing the popup and performing an action:
67 |
68 | ```swift
69 | @main struct MyApp: App {
70 | @State var isMenuPresented: Bool = false
71 |
72 | var body: some Scene {
73 | MenuBarExtra("MyApp Menu", systemImage: "folder") {
74 | MyMenu(isMenuPresented: $isMenuPresented)
75 | .introspectMenuBarExtraWindow { window in // <-- the magic ✨
76 | window.animationBehavior = .alertPanel
77 | }
78 | }
79 | .menuBarExtraStyle(.window)
80 | .menuBarExtraAccess(isPresented: $isMenuPresented) { statusItem in // <-- the magic ✨
81 | // access status item or store it in a @State var
82 | }
83 | }
84 | }
85 |
86 | struct MyMenu: View {
87 | @Binding var isMenuPresented: Bool
88 |
89 | var body: some View {
90 | Button("Perform Action") {
91 | isMenuPresented = false
92 | performSomeAction()
93 | }
94 | }
95 | }
96 | ```
97 |
98 | ### Multiple MenuBarExtra
99 |
100 | MenuBarExtraAccess is fully compatible with one or multiple MenuBarExtra in an app.
101 |
102 | Just add an index number parameter to `.menuBarExtraAccess()` and `.introspectMenuBarExtraWindow()` that reflects the order of `MenuBarExtra` declarations.
103 |
104 | ```swift
105 | var body: some Scene {
106 | MenuBarExtra("MyApp Menu A", systemImage: "folder") {
107 | MyMenu(isMenuPresented: $isMenuPresented)
108 | .introspectMenuBarExtraWindow(index: 0) { window in // <-- add index 0
109 | // ...
110 | }
111 | }
112 | .menuBarExtraStyle(.window)
113 | .menuBarExtraAccess(index: 0, isPresented: $isMenuPresented) // <-- add index 0
114 |
115 | MenuBarExtra("MyApp Menu B", systemImage: "folder") {
116 | MyMenu(isMenuPresented: $isMenuPresented)
117 | .introspectMenuBarExtraWindow(index: 1) { window in // <-- add index 1
118 | // ...
119 | }
120 | }
121 | .menuBarExtraStyle(.window)
122 | .menuBarExtraAccess(index: 1, isPresented: $isMenuPresented) // <-- add index 1
123 | }
124 | ```
125 |
126 | ## Future
127 |
128 | The hope is that Apple implements native versions of these features (and more) in future iterations of SwiftUI!
129 |
130 | Until then, a radar has been filed as a feature request: [FB11984872](https://github.com/feedback-assistant/reports/issues/383)
131 |
132 | ## Menu Builder
133 |
134 | Check out [MacControlCenterUI](https://github.com/orchetect/MacControlCenterUI), a SwiftUI package built on MenuBarExtraAccess for easily building Control Center style menus.
135 |
136 | ## Known Issues
137 |
138 | - When using `.menuBarExtraStyle(.menu)`, SwiftUI causes the popup menu to block the runloop while the menu is open, which means:
139 | - Observing the `isPresented` binding will not work as expected.
140 | - Setting the `isPresented` binding to `false` while the menu is presented has no effect.
141 | - The user must dismiss the menu themself to allow event flow to continue. We have no control over this until Apple decides to change the MenuBarExtra behavior.
142 |
143 | ## Author
144 |
145 | Coded by a bunch of 🐹 hamsters in a trenchcoat that calls itself [@orchetect](https://github.com/orchetect).
146 |
147 | ## License
148 |
149 | Licensed under the MIT license. See [LICENSE](https://github.com/orchetect/MenuBarExtraAccess/blob/master/LICENSE) for details.
150 |
151 | ## Sponsoring
152 |
153 | If you enjoy using MenuBarExtraAccess and want to contribute to open-source financially, GitHub sponsorship is much appreciated. Feedback and code contributions are also welcome.
154 |
155 | ## Contributions
156 |
157 | Contributions are welcome. Posting in [Discussions](https://github.com/orchetect/MenuBarExtraAccess/discussions) first prior to new submitting PRs for features or modifications is encouraged.
158 |
--------------------------------------------------------------------------------
/Sources/MenuBarExtraAccess/MenuBarExtra Window Introspection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuBarExtra Window Introspection.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | #if os(macOS)
8 |
9 | import SwiftUI
10 |
11 | @MainActor // required for Xcode 15 builds
12 | extension View {
13 | /// Provides introspection on the underlying window presented by `MenuBarExtra`.
14 | /// Add this view modifier to the top level of the View that occupies the `MenuBarExtra` content.
15 | /// If more than one MenuBarExtra are used in the app, provide the sequential index number of the `MenuBarExtra`.
16 | public func introspectMenuBarExtraWindow(
17 | index: Int = 0,
18 | _ block: @escaping (_ window: NSWindow) -> Void
19 | ) -> some View {
20 | self
21 | .onAppear {
22 | guard let window = MenuBarExtraUtils.window(for: .index(index)) else {
23 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
24 | print("Cannot call introspection block for status item because its window could not be found.")
25 | #endif
26 |
27 | return
28 | }
29 |
30 | block(window)
31 | }
32 | }
33 | }
34 |
35 | #endif
36 |
--------------------------------------------------------------------------------
/Sources/MenuBarExtraAccess/MenuBarExtraAccess.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuBarExtraAccess.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | #if os(macOS)
8 |
9 | import SwiftUI
10 | import Combine
11 |
12 | @available(macOS 13.0, *)
13 | @available(iOS, unavailable)
14 | @available(tvOS, unavailable)
15 | @available(watchOS, unavailable)
16 | @MainActor // required for Xcode 15 builds
17 | extension Scene {
18 | /// Adds a presentation state binding to `MenuBarExtra`.
19 | /// If more than one MenuBarExtra are used in the app, provide the sequential index number of the `MenuBarExtra`.
20 | public func menuBarExtraAccess(
21 | index: Int = 0,
22 | isPresented: Binding,
23 | statusItem: (@MainActor @Sendable (_ statusItem: NSStatusItem) -> Void)? = nil
24 | ) -> some Scene {
25 | // FYI: SwiftUI will reinitialize the MenuBarExtra (and this view modifier)
26 | // if its title/label content changes, which means the stored ID will always be up-to-date
27 |
28 | MenuBarExtraAccess(
29 | index: index,
30 | statusItemIntrospection: statusItem,
31 | menuBarExtra: self,
32 | isMenuPresented: isPresented
33 | )
34 | }
35 | }
36 |
37 | @available(macOS 13.0, *)
38 | @available(iOS, unavailable)
39 | @available(tvOS, unavailable)
40 | @available(watchOS, unavailable)
41 | @MainActor // required for Xcode 15 builds
42 | struct MenuBarExtraAccess: Scene {
43 | let index: Int
44 | let statusItemIntrospection: (@MainActor @Sendable (_ statusItem: NSStatusItem) -> Void)?
45 | let menuBarExtra: Content
46 | @Binding var isMenuPresented: Bool
47 |
48 | init(
49 | index: Int,
50 | statusItemIntrospection: (@MainActor @Sendable (_ statusItem: NSStatusItem) -> Void)?,
51 | menuBarExtra: Content,
52 | isMenuPresented: Binding
53 | ) {
54 | self.index = index
55 | self.statusItemIntrospection = statusItemIntrospection
56 | self.menuBarExtra = menuBarExtra
57 | self._isMenuPresented = isMenuPresented
58 | }
59 |
60 | var body: some Scene {
61 | menuBarExtra
62 | .onChange(of: observerSetup()) { newValue in
63 | // do nothing here - the method runs setup when polled by SwiftUI
64 | }
65 | .onChange(of: isMenuPresented) { newValue in
66 | setPresented(newValue)
67 | }
68 | }
69 |
70 | private func togglePresented() {
71 | MenuBarExtraUtils.togglePresented(for: .index(index))
72 | }
73 |
74 | private func setPresented(_ state: Bool) {
75 | MenuBarExtraUtils.setPresented(for: .index(index), state: state)
76 | }
77 |
78 | // MARK: Observer
79 |
80 | /// A workaround since `onAppear {}` is not available in a SwiftUI Scene.
81 | /// We need to set up the observer, but it can't be set up in the scene init because it needs to
82 | /// update scene state from an escaping closure.
83 | /// This returns a bogus value, but because we call it in an `onChange {}` block, SwiftUI
84 | /// is forced to evaluate the method and run our code at the appropriate time.
85 | private func observerSetup() -> Int {
86 | observerContainer.setupStatusItemIntrospection {
87 | guard let statusItem = MenuBarExtraUtils.statusItem(for: .index(index)) else { return }
88 | statusItemIntrospection?(statusItem)
89 | }
90 |
91 | // note that we can't use the button state value itself since MenuBarExtra seems to treat it
92 | // as a toggle and not an absolute on/off value. Its polarity can invert itself when clicking
93 | // in an empty area of the menubar or a different app's status item in order to dismiss the window,
94 | // for example.
95 | observerContainer.setupStatusItemButtonStateObserver {
96 | MenuBarExtraUtils.newStatusItemButtonStateObserver(index: index) { change in
97 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
98 | print("Status item button state observer: called with change: \(change.newValue?.description ?? "nil")")
99 | #endif
100 |
101 | // only continue if the MenuBarExtra is menu-based.
102 | // window-based MenuBarExtras are handled with app-bound window observers instead.
103 | guard MenuBarExtraUtils.statusItem(for: .index(index))?
104 | .isMenuBarExtraMenuBased == true
105 | else { return }
106 |
107 | guard let newVal = change.newValue else { return }
108 | let newBool = newVal != .off
109 | if isMenuPresented != newBool {
110 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
111 | print("Status item button state observer: Setting isMenuPresented to \(newBool)")
112 | #endif
113 |
114 | isMenuPresented = newBool
115 | }
116 | }
117 | }
118 |
119 | // TODO: this mouse event observer is now redundant and can be deleted in the future
120 |
121 | // observerContainer.setupGlobalMouseDownMonitor {
122 | // // note that this won't fire when mouse events within the app cause the window to dismiss
123 | // MenuBarExtraUtils.newGlobalMouseDownEventsMonitor { event in
124 | // #if MENUBAREXTRAACCESS_DEBUG_LOGGING
125 | // print("Global mouse-down events monitor: called with event: \(event.type.name)")
126 | // #endif
127 | //
128 | // // close window when user clicks outside of it
129 | //
130 | // MenuBarExtraUtils.setPresented(for: .index(index), state: false)
131 | //
132 | // #if MENUBAREXTRAACCESS_DEBUG_LOGGING
133 | // print("Global mouse-down events monitor: Setting isMenuPresented to false")
134 | // #endif
135 | //
136 | // isMenuPresented = false
137 | // }
138 | // }
139 |
140 | observerContainer.setupWindowObservers(
141 | index: index,
142 | didBecomeKey: { window in
143 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
144 | print("MenuBarExtra index \(index) drop-down window did become key.")
145 | #endif
146 |
147 | MenuBarExtraUtils.setKnownPresented(for: .index(index), state: true)
148 | isMenuPresented = true
149 | },
150 | didResignKey: { window in
151 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
152 | print("MenuBarExtra index \(index) drop-down window did resign as key.")
153 | #endif
154 |
155 | // it's possible for a window to resign key without actually closing, so let's
156 | // close it as a failsafe.
157 | if window.isVisible {
158 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
159 | print("Closing MenuBarExtra index \(index) drop-down window as a result of it resigning as key.")
160 | #endif
161 |
162 | window.close()
163 | }
164 |
165 | MenuBarExtraUtils.setKnownPresented(for: .index(index), state: false)
166 | isMenuPresented = false
167 | }
168 | )
169 |
170 | return 0
171 | }
172 |
173 | // MARK: Observers
174 |
175 | private var observerContainer = ObserverContainer()
176 |
177 | @MainActor
178 | private class ObserverContainer {
179 | private var statusItemIntrospectionSetup: Bool = false
180 | private var observer: NSStatusItem.ButtonStateObserver?
181 | private var eventsMonitor: Any?
182 | private var windowDidBecomeKeyObserver: AnyCancellable?
183 | private var windowDidResignKeyObserver: AnyCancellable?
184 |
185 | init() { }
186 |
187 | func setupStatusItemIntrospection(
188 | _ block: @MainActor @escaping @Sendable () -> Void
189 | ) {
190 | guard !statusItemIntrospectionSetup else { return }
191 | // run async so that it can execute after SwiftUI sets up the NSStatusItem
192 | Task { @MainActor in
193 | block()
194 | }
195 | }
196 |
197 | func setupStatusItemButtonStateObserver(
198 | _ block: @MainActor @escaping @Sendable () -> NSStatusItem.ButtonStateObserver?
199 | ) {
200 | // run async so that it can execute after SwiftUI sets up the NSStatusItem
201 | Task { @MainActor [self] in
202 | observer = block()
203 | }
204 | }
205 |
206 | func setupGlobalMouseDownMonitor(
207 | _ block: @MainActor @escaping @Sendable () -> Any?
208 | ) {
209 | // run async so that it can execute after SwiftUI sets up the NSStatusItem
210 | Task { @MainActor [self] in
211 | // tear down old monitor, if one exists
212 | if let eventsMonitor = eventsMonitor {
213 | NSEvent.removeMonitor(eventsMonitor)
214 | }
215 |
216 | eventsMonitor = block()
217 | }
218 | }
219 |
220 | func setupWindowObservers(
221 | index: Int,
222 | didBecomeKey didBecomeKeyBlock: @MainActor @escaping @Sendable (_ window: NSWindow) -> Void,
223 | didResignKey didResignKeyBlock: @MainActor @escaping @Sendable (_ window: NSWindow) -> Void
224 | ) {
225 | // run async so that it can execute after SwiftUI sets up the NSStatusItem
226 | Task { @MainActor [self] in
227 | windowDidBecomeKeyObserver = MenuBarExtraUtils.newWindowObserver(
228 | index: index,
229 | for: NSWindow.didBecomeKeyNotification
230 | ) { window in didBecomeKeyBlock(window) }
231 |
232 | windowDidResignKeyObserver = MenuBarExtraUtils.newWindowObserver(
233 | index: index,
234 | for: NSWindow.didResignKeyNotification
235 | ) { window in didResignKeyBlock(window) }
236 |
237 | }
238 | }
239 | }
240 | }
241 |
242 | #endif
243 |
--------------------------------------------------------------------------------
/Sources/MenuBarExtraAccess/MenuBarExtraUtils/MenuBarExtraUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuBarExtraUtils.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | #if os(macOS)
8 |
9 | import AppKit
10 | import SwiftUI
11 | import Combine
12 |
13 | /// Global static utility methods for interacting the app's menu bar extras (status items).
14 | @MainActor
15 | enum MenuBarExtraUtils {
16 | // MARK: - Menu Extra Manipulation
17 |
18 | /// Toggle MenuBarExtra menu/window presentation state.
19 | static func togglePresented(for ident: StatusItemIdentity? = nil) {
20 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
21 | print("MenuBarExtraUtils.\(#function) called for status item \(ident?.description ?? "nil")")
22 | #endif
23 |
24 | statusItem(for: ident)?.togglePresented()
25 | }
26 |
27 | /// Set MenuBarExtra menu/window presentation state.
28 | static func setPresented(for ident: StatusItemIdentity? = nil, state: Bool) {
29 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
30 | print("MenuBarExtraUtils.\(#function) called for status item \(ident?.description ?? "nil") with state \(state)")
31 | #endif
32 |
33 | guard let item = statusItem(for: ident) else { return }
34 | item.setPresented(state: state)
35 | }
36 |
37 | /// Set MenuBarExtra menu/window presentation state only when its state is reliably known.
38 | static func setKnownPresented(for ident: StatusItemIdentity? = nil, state: Bool) {
39 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
40 | print("MenuBarExtraUtils.\(#function) called for status item \(ident?.description ?? "nil") with state \(state)")
41 | #endif
42 |
43 | guard let item = statusItem(for: ident) else { return }
44 | item.setKnownPresented(state: state)
45 | }
46 | }
47 |
48 | // MARK: - Objects and Metadata
49 |
50 | @MainActor
51 | extension MenuBarExtraUtils {
52 | /// Returns the underlying status item(s) created by `MenuBarExtra` instances.
53 | ///
54 | /// Each `MenuBarExtra` creates one status item.
55 | ///
56 | /// If the `isInserted` binding on a `MenuBarExtra` is set to false, it may not return a status
57 | /// item. This may also change its index.
58 | static var statusItems: [NSStatusItem] {
59 | NSApp.windows
60 | .filter {
61 | $0.className.contains("NSStatusBarWindow")
62 | }
63 | .compactMap { window -> NSStatusItem? in
64 | // On Macs with only one display, there should only be one result.
65 | // On Macs with two or more displays and system prefs set to "Displays have Separate
66 | // Spaces", one NSStatusBarWindow instance per display will be returned.
67 | // - the main/active instance has a statusItem property of type NSStatusItem
68 | // - the other(s) have a statusItem property of type NSStatusItemReplicant
69 |
70 | // NSStatusItemReplicant is a replica for displaying the status item on inactive
71 | // spaces/screens that happens to be an NSStatusItem subclass.
72 | // both respond to the action selector being sent to them.
73 | // We only need to interact with the main non-replica status item.
74 | guard let statusItem = window.fetchStatusItem(),
75 | statusItem.className == "NSStatusItem"
76 | else { return nil }
77 | return statusItem
78 | }
79 | }
80 |
81 | /// Returns the underlying status items created by `MenuBarExtra` for the
82 | /// `MenuBarExtra` with the specified index.
83 | ///
84 | /// Each `MenuBarExtra` creates one status item.
85 | ///
86 | /// If the `isInserted` binding on a `MenuBarExtra` is set to false, it may not return a status
87 | /// item. This may also change its index.
88 | static func statusItem(for ident: StatusItemIdentity? = nil) -> NSStatusItem? {
89 | let statusItems = statusItems
90 |
91 | guard let ident else { return statusItems.first }
92 |
93 | switch ident {
94 | case .id(let menuBarExtraID):
95 | return statusItems.filter { $0.menuBarExtraID == menuBarExtraID }.first
96 | case .index(let index):
97 | guard statusItems.indices.contains(index) else { return nil }
98 | return statusItems[index]
99 | }
100 | }
101 |
102 | /// Returns window associated with a window-based MenuBarExtra.
103 | /// Always returns `nil` for a menu-based MenuBarExtra.
104 | static func window(for ident: StatusItemIdentity? = nil) -> NSWindow? {
105 | // we can't use NSStatusItem.window because it won't work
106 |
107 | let menuBarWindows = NSApp.windows.filter {
108 | $0.className.contains("MenuBarExtraWindow")
109 | }
110 |
111 | guard let ident else { return menuBarWindows.first }
112 |
113 | switch ident {
114 | case .id(let menuBarExtraID):
115 | guard let match = menuBarWindows.first(where: { $0.menuBarExtraID == menuBarExtraID }) else {
116 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
117 | print("MenuBarExtraUtils.\(#function): Window could not be found for status item with ID \"\(menuBarExtraID).")
118 | #endif
119 |
120 | return nil
121 | }
122 | return match
123 | case .index(_):
124 | guard let item = statusItem(for: ident) else { return nil }
125 |
126 | return menuBarWindows.first { window in
127 | guard let statusItem = window.fetchStatusItem() else { return false }
128 | return item == statusItem
129 | }
130 | }
131 | }
132 | }
133 |
134 | // MARK: - Observers
135 |
136 | @MainActor
137 | extension MenuBarExtraUtils {
138 | /// Call from MenuBarExtraAccess init to set up observer.
139 | static func newStatusItemButtonStateObserver(
140 | index: Int,
141 | _ handler: @MainActor @escaping @Sendable (_ change: NSKeyValueObservedChange) -> Void
142 | ) -> NSStatusItem.ButtonStateObserver? {
143 | guard let statusItem = MenuBarExtraUtils.statusItem(for: .index(index)) else {
144 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
145 | print("Can't register menu bar extra state observer: Can't find status item. It may not yet exist.")
146 | #endif
147 |
148 | return nil
149 | }
150 |
151 | guard let observer = statusItem.stateObserverMenuBased(handler)
152 | else {
153 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
154 | print("Can't register menu bar extra state observer: Can't generate observer.")
155 | #endif
156 |
157 | return nil
158 | }
159 |
160 | return observer
161 | }
162 |
163 | /// Adds global event monitor to catch mouse events outside the application.
164 | static func newGlobalMouseDownEventsMonitor(
165 | _ handler: @escaping @Sendable (NSEvent) -> Void
166 | ) -> Any? {
167 | NSEvent.addGlobalMonitorForEvents(
168 | matching: [
169 | .leftMouseDown,
170 | .rightMouseDown,
171 | .otherMouseDown
172 | ],
173 | handler: handler
174 | )
175 | }
176 |
177 | /// Adds local event monitor to catch mouse events within the application.
178 | static func newLocalMouseDownEventsMonitor(
179 | _ handler: @escaping @Sendable (NSEvent) -> NSEvent?
180 | ) -> Any? {
181 | NSEvent.addLocalMonitorForEvents(
182 | matching: [
183 | .leftMouseDown,
184 | .rightMouseDown,
185 | .otherMouseDown
186 | ],
187 | handler: handler
188 | )
189 | }
190 |
191 | static func newStatusItemButtonStatePublisher(
192 | index: Int
193 | ) -> NSStatusItem.ButtonStatePublisher? {
194 | guard let statusItem = MenuBarExtraUtils.statusItem(for: .index(index)) else {
195 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
196 | print("Can't register menu bar extra state observer: Can't find status item. It may not yet exist.")
197 | #endif
198 |
199 | return nil
200 | }
201 |
202 | guard let publisher = statusItem.buttonStatePublisher()
203 | else {
204 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
205 | print("Can't register menu bar extra state observer: Can't generate publisher.")
206 | #endif
207 |
208 | return nil
209 | }
210 |
211 | return publisher
212 | }
213 |
214 | /// Wraps `newStatusItemButtonStatePublisher` in a sink.
215 | static func newStatusItemButtonStatePublisherSink(
216 | index: Int,
217 | block: @MainActor @escaping @Sendable (_ newValue: NSControl.StateValue?) -> Void
218 | ) -> AnyCancellable? {
219 | newStatusItemButtonStatePublisher(index: index)?
220 | .flatMap { value in
221 | Just(value)
222 | .tryMap { value throws -> NSControl.StateValue in value }
223 | .replaceError(with: nil)
224 | }
225 | .sink(receiveValue: { value in
226 | block(value)
227 | })
228 | }
229 |
230 | static func newWindowObserver(
231 | index: Int,
232 | for notification: Notification.Name,
233 | block: @MainActor @escaping @Sendable (_ window: NSWindow) -> Void
234 | ) -> AnyCancellable? {
235 | NotificationCenter.default.publisher(for: notification)
236 | .filter { output in
237 | guard let window = output.object as? NSWindow else { return false }
238 | guard let windowWithIndex = MenuBarExtraUtils.window(for: .index(index)) else { return false }
239 | return window == windowWithIndex
240 | }
241 | .sink { output in
242 | guard let window = output.object as? NSWindow else { return }
243 | block(window)
244 | }
245 | }
246 | }
247 |
248 | // MARK: - NSStatusItem Introspection
249 |
250 | @MainActor
251 | extension NSStatusItem {
252 | var menuBarExtraIndex: Int {
253 | MenuBarExtraUtils.statusItems.firstIndex(of: self) ?? 0
254 | }
255 |
256 | /// Returns the ID string for the status item.
257 | /// Returns `nil` if the status item does not contain a `MacControlCenterMenu`
258 | fileprivate var menuBarExtraID: String? {
259 | // Note: this is not ideal, but it's currently the ONLY way to achieve this
260 | // until Apple adds a 1st-party solution to MenuBarExtra state
261 |
262 | // dump(statusItem.button!.target):
263 | // ▿ some: SwiftUI.WindowMenuBarExtraBehavior #0
264 | // ▿ super: SwiftUI.MenuBarExtraBehavior
265 | // - statusItem: #1
266 | // ▿ configuration: SwiftUI.MenuBarExtraConfiguration
267 | // ▿ label: SwiftUI.AnyView ---> contains the status item label, icon
268 | // ▿ mainContent: SwiftUI.AnyView ---> contains the view content
269 | // - storage
270 | // - view
271 | // - ... ---> properties of the view will be itemized here
272 | // - shouldQuitWhenRemoved: true
273 | // ▿ _isInserted: SwiftUI.Binding ---> isInserted binding backing
274 | // - isMenuBased: false
275 | // - implicitID: "YourApp.Menu"
276 | // - resizability: SwiftUI.WindowResizability.Role.automatic
277 | // - defaultSize: nil
278 | // ▿ environment: [] ---> SwiftUI environment vars/locale/openWindow method
279 |
280 | // this may require a less brittle solution if the child path may change, such as grabbing
281 | // String(dump: behavior) and using RegEx to find the value
282 |
283 | guard let behavior = button?.target, // SwiftUI.WindowMenuBarExtraBehavior <- internal
284 | let mirror = Mirror(reflecting: behavior).superclassMirror
285 | else {
286 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
287 | print("Could not find status item's target.")
288 | #endif
289 |
290 | return nil
291 | }
292 |
293 | return mirror.menuBarExtraID()
294 | }
295 |
296 | var isMenuBarExtraMenuBased: Bool {
297 | // if window-based, target will be the internal type SwiftUI.WindowMenuBarExtraBehavior
298 | // if menu-based, target will be nil
299 | guard let behavior = button?.target
300 | else {
301 | return true
302 | }
303 |
304 | // the rest of this is probably redundant given the check above covers both scenarios.
305 | // however, WindowMenuBarExtraBehavior does contain an explicit `isMenuBased` Bool we can read
306 | guard let mirror = Mirror(reflecting: behavior).superclassMirror
307 | else {
308 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
309 | print("Could not find status item's target.")
310 | #endif
311 |
312 | return false
313 | }
314 |
315 | return mirror.isMenuBarExtraMenuBased()
316 | }
317 | }
318 |
319 | // MARK: - NSWindow Introspection
320 |
321 | @MainActor
322 | extension NSWindow {
323 | fileprivate var menuBarExtraID: String? {
324 | // Note: this is not ideal, but it's currently the ONLY way to achieve this
325 | // until Apple adds a 1st-party solution to MenuBarExtra state
326 |
327 | let mirror = Mirror(reflecting: self)
328 | return mirror.menuBarExtraID()
329 | }
330 | }
331 |
332 | @MainActor
333 | extension Mirror {
334 | fileprivate func menuBarExtraID() -> String? {
335 | // Note: this is not ideal, but it's currently the ONLY way to achieve this
336 | // until Apple adds a 1st-party solution to MenuBarExtra state
337 |
338 | // this may require a less brittle solution if the child path may change, such as grabbing
339 | // String(dump: behavior) and using RegEx to find the value
340 |
341 | // when using MenuBarExtra(title string, content) this is the mirror path:
342 | if let id = descendant(
343 | "configuration",
344 | "label",
345 | "storage",
346 | "view",
347 | "content",
348 | "title",
349 | "storage",
350 | "anyTextStorage",
351 | "key",
352 | "key"
353 | ) as? String {
354 | return id
355 | }
356 |
357 | // this won't work. it differs when checked from MenuBarExtraAccess / menuBarExtraAccess(isPresented:)
358 | // internals. MenuBarExtra wraps the label in additional modifiers/AnyView here.
359 | //
360 | // otherwise, when using a MenuBarExtra initializer that produces Label view content:
361 | // we'll basically grab the hashed contents of the label
362 | if let anyView = descendant(
363 | "configuration",
364 | "label"
365 | ) as? any View {
366 | let hashed = MenuBarExtraUtils.hash(anyView: anyView)
367 | print("hash:", hashed)
368 | return hashed
369 | }
370 |
371 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
372 | print("Could not determine MenuBarExtra ID")
373 | #endif
374 |
375 | return nil
376 | }
377 |
378 | fileprivate func isMenuBarExtraMenuBased() -> Bool {
379 | descendant(
380 | "configuration",
381 | "isMenuBased"
382 | ) as? Bool ?? false
383 | }
384 | }
385 |
386 | // MARK: - Misc.
387 |
388 | @MainActor
389 | extension MenuBarExtraUtils {
390 | static func hash(anyView: any View) -> String {
391 | // can't hash `any View`
392 | //
393 | // var h = Hasher()
394 | // let ah = AnyHashable(anyView)
395 | // h.combine(anyView)
396 | // let i = h.finalize()
397 | // return String(i)
398 |
399 | // return "\(anyView)"
400 | return String("\(anyView)".hashValue)
401 | }
402 | }
403 |
404 | #endif
405 |
--------------------------------------------------------------------------------
/Sources/MenuBarExtraAccess/MenuBarExtraUtils/StatusItemIdentity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuBarExtraUtils.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | #if os(macOS)
8 |
9 | enum StatusItemIdentity: Equatable, Hashable {
10 | case index(Int)
11 | case id(String)
12 | }
13 |
14 | extension StatusItemIdentity: CustomStringConvertible {
15 | var description: String {
16 | switch self {
17 | case .index(let int):
18 | return "index \(int)"
19 | case .id(let string):
20 | return "ID \"\(string)\""
21 | }
22 | }
23 | }
24 |
25 | #endif
26 |
--------------------------------------------------------------------------------
/Sources/MenuBarExtraAccess/NSControl Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSControl Extensions.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | #if os(macOS)
8 |
9 | import AppKit
10 |
11 | extension NSControl.StateValue {
12 | @_disfavoredOverload
13 | public var name: String {
14 | switch self {
15 | case .on:
16 | return "on"
17 | case .off:
18 | return "off"
19 | case .mixed:
20 | return "mixed"
21 | default:
22 | return "unknown"
23 | }
24 | }
25 | }
26 |
27 | #endif
28 |
--------------------------------------------------------------------------------
/Sources/MenuBarExtraAccess/NSEvent Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSEvent Extensions.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | #if os(macOS)
8 |
9 | import AppKit
10 |
11 | extension NSEvent.EventType {
12 | @_disfavoredOverload
13 | public var name: String {
14 | switch self {
15 | case .leftMouseDown: return "leftMouseDown"
16 | case .leftMouseUp: return "leftMouseUp"
17 | case .rightMouseDown: return "rightMouseDown"
18 | case .rightMouseUp: return "rightMouseUp"
19 | case .mouseMoved: return "mouseMoved"
20 | case .leftMouseDragged: return "leftMouseDragged"
21 | case .rightMouseDragged: return "rightMouseDragged"
22 | case .mouseEntered: return "mouseEntered"
23 | case .mouseExited: return "mouseExited"
24 | case .keyDown: return "keyDown"
25 | case .keyUp: return "keyUp"
26 | case .flagsChanged: return "flagsChanged"
27 | case .appKitDefined: return "appKitDefined"
28 | case .systemDefined: return "systemDefined"
29 | case .applicationDefined: return "applicationDefined"
30 | case .periodic: return "periodic"
31 | case .cursorUpdate: return "cursorUpdate"
32 | case .scrollWheel: return "scrollWheel"
33 | case .tabletPoint: return "tabletPoint"
34 | case .tabletProximity: return "tabletProximity"
35 | case .otherMouseDown: return "otherMouseDown"
36 | case .otherMouseUp: return "otherMouseUp"
37 | case .otherMouseDragged: return "otherMouseDragged"
38 | case .gesture: return "gesture"
39 | case .magnify: return "magnify"
40 | case .swipe: return "swipe"
41 | case .rotate: return "rotate"
42 | case .beginGesture: return "beginGesture"
43 | case .endGesture: return "endGesture"
44 | case .smartMagnify: return "smartMagnify"
45 | case .quickLook: return "quickLook"
46 | case .pressure: return "pressure"
47 | case .directTouch: return "directTouch"
48 | case .changeMode: return "changeMode"
49 | @unknown default:
50 | assertionFailure("Unhandled `NSEvent.EventType` case with raw value: \(rawValue)")
51 | return "\(rawValue)"
52 | }
53 | }
54 | }
55 |
56 | #endif
57 |
--------------------------------------------------------------------------------
/Sources/MenuBarExtraAccess/NSStatusItem Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSStatusItem Extensions.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | #if os(macOS)
8 |
9 | import AppKit
10 | import SwiftUI
11 |
12 | @MainActor
13 | extension NSStatusItem {
14 | /// Toggles the menu/window state by mimicking a menu item button press.
15 | @_disfavoredOverload
16 | public func togglePresented() {
17 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
18 | print("NSStatusItem.\(#function) called")
19 | #endif
20 |
21 | // this also works but only for window-based MenuBarExtra
22 | // (button.target and button.action are nil when menu-based):
23 | // - mimic user pressing the menu item button
24 | // which convinces MenuBarExtra to close the window and properly reset its state
25 | // let actionSelector = button?.action // "toggleWindow:" selector
26 | // button?.sendAction(actionSelector, to: button?.target)
27 |
28 | button?.performClick(button)
29 | updateHighlight()
30 | }
31 |
32 | /// Toggles the menu/window state by mimicking a menu item button press.
33 | @_disfavoredOverload
34 | internal func setPresented(state: Bool) {
35 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
36 | print("NSStatusItem.\(#function) called with state: \(state)")
37 | #endif
38 |
39 | // read current state and selectively call toggle if state differs
40 | let currentState = button?.state != .off
41 | guard state != currentState else {
42 | updateHighlight()
43 | return
44 | }
45 | togglePresented()
46 | }
47 |
48 | @_disfavoredOverload
49 | internal func updateHighlight() {
50 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
51 | print("NSStatusItem.\(#function) called")
52 | #endif
53 |
54 | let s = button?.state != .off
55 |
56 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING
57 | print("NSStatusItem.\(#function): State detected as \(s)")
58 | #endif
59 |
60 | button?.isHighlighted = s
61 | }
62 |
63 | /// Only call this when the state of the drop-down window is known.
64 | internal func setKnownPresented(state: Bool) {
65 | switch state {
66 | case true:
67 | button?.state = .on
68 | case false:
69 | button?.state = .off
70 | }
71 | }
72 | }
73 |
74 | // MARK: - KVO Observer
75 |
76 | @MainActor
77 | extension NSStatusItem {
78 | @MainActor
79 | internal class ButtonStateObserver: NSObject {
80 | private weak var objectToObserve: NSStatusBarButton?
81 | private var observation: NSKeyValueObservation?
82 |
83 | init(
84 | object: NSStatusBarButton,
85 | _ handler: @MainActor @escaping @Sendable (_ change: NSKeyValueObservedChange) -> Void
86 | ) {
87 | objectToObserve = object
88 | super.init()
89 |
90 | observation = object.observe(
91 | \.cell!.state,
92 | options: [.initial, .new]
93 | ) { ob, change in
94 | Task { @MainActor in handler(change) }
95 | }
96 | }
97 |
98 | deinit {
99 | observation?.invalidate()
100 | }
101 | }
102 |
103 | internal func stateObserverMenuBased(
104 | _ handler: @MainActor @escaping @Sendable (_ change: NSKeyValueObservedChange) -> Void
105 | ) -> ButtonStateObserver? {
106 | guard let button else { return nil }
107 | let newStatusItemButtonStateObserver = ButtonStateObserver(object: button, handler)
108 | return newStatusItemButtonStateObserver
109 | }
110 | }
111 |
112 | // MARK: - KVO Publisher
113 |
114 | @MainActor
115 | extension NSStatusItem {
116 | typealias ButtonStatePublisher = KeyValueObservingPublisher
117 |
118 | internal func buttonStatePublisher() -> ButtonStatePublisher? {
119 | button?.publisher(for: \.cell!.state, options: [.initial, .new])
120 | }
121 | }
122 |
123 | #endif
124 |
--------------------------------------------------------------------------------
/Sources/MenuBarExtraAccess/NSWindow Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSWindow Extensions.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | #if os(macOS)
8 |
9 | import AppKit
10 |
11 | extension NSWindow /* actually NSStatusBarWindow but it's a private AppKit type */ {
12 | /// When called on an `NSStatusBarWindow` instance, returns the associated `NSStatusItem`.
13 | /// Always returns `nil` for any other `NSWindow` subclass.
14 | @_disfavoredOverload
15 | func fetchStatusItem() -> NSStatusItem? {
16 | // statusItem is a private key not exposed to Swift but we can get it using Key-Value coding
17 | value(forKey: "statusItem") as? NSStatusItem
18 | ?? Mirror(reflecting: self).descendant("statusItem") as? NSStatusItem
19 | }
20 | }
21 |
22 | #endif
23 |
--------------------------------------------------------------------------------
/Sources/MenuBarExtraAccess/Unused/Unused Code.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Unused Code.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | #if os(macOS)
8 |
9 | // import SwiftUI
10 | // import Combine
11 | //
12 | // @available(macOS 11.0, *)
13 | // extension Scene {
14 | // fileprivate func menuBarExtraID() -> String? {
15 | // // Note: this is not ideal, but it's currently the ONLY way to achieve this
16 | // // until Apple adds a 1st-party solution to MenuBarExtra state
17 | //
18 | // // this may require a less brittle solution if the child path may change, such as grabbing
19 | // // String(dump: behavior) and using RegEx to find the value
20 | //
21 | // let m = Mirror(reflecting: self)
22 | //
23 | // // TODO: detect if style is .menu or .window
24 | //
25 | // // when using MenuBarExtra(title string, content) this is the mirror path:
26 | // if let id = m.descendant(
27 | // "label",
28 | // "title",
29 | // "storage",
30 | // "anyTextStorage",
31 | // "key",
32 | // "key"
33 | // ) as? String {
34 | // return id
35 | // }
36 | //
37 | // // this won't work. it differs when checked from NSStatusItem.menuBarExtraID
38 | // //
39 | // // otherwise, when using a MenuBarExtra initializer that produces Label view content:
40 | // // we'll basically grab the hashed contents of the label
41 | // if let anyView = m.descendant(
42 | // "label"
43 | // ) as? any View {
44 | // let hashed = MenuBarExtraUtils.hash(anyView: anyView)
45 | // return hashed
46 | // }
47 | //
48 | // print("Could not determine MenuBarExtra ID")
49 | //
50 | // return nil
51 | // }
52 | // }
53 |
54 | #endif
55 |
--------------------------------------------------------------------------------
/Sources/MenuBarExtraAccess/Utilities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utilities.swift
3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess
4 | // © 2023 Steffan Andrews • Licensed under MIT License
5 | //
6 |
7 | import Foundation
8 |
9 | extension String {
10 | /// Captures the output of `dump()` for the passed object instance.
11 | init(dump object: Any) {
12 | var dumpOutput = String()
13 | dump(object, to: &dumpOutput)
14 | self = dumpOutput
15 | }
16 | }
17 |
--------------------------------------------------------------------------------