Showing preview only (723K chars total). Download the full file or copy to clipboard to get everything.
Repository: jordanbaird/Ice
Branch: main
Commit: 11edd39115f3
Files: 152
Total size: 677.5 KB
Directory structure:
gitextract_awypvfy4/
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ └── workflows/
│ └── lint.yml
├── .gitignore
├── .swiftlint.yml
├── CODE_OF_CONDUCT.md
├── FREQUENT_ISSUES.md
├── Ice/
│ ├── Assets.xcassets/
│ │ ├── AccentColor.colorset/
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── ControlItemImages/
│ │ │ ├── Contents.json
│ │ │ ├── Dot/
│ │ │ │ ├── Contents.json
│ │ │ │ ├── DotFill.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── DotStroke.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Ellipsis/
│ │ │ │ ├── Contents.json
│ │ │ │ ├── EllipsisFill.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── EllipsisStroke.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── IceCube/
│ │ │ ├── Contents.json
│ │ │ ├── IceCubeFill.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── IceCubeStroke.imageset/
│ │ │ └── Contents.json
│ │ ├── DefaultLayoutBarColor.colorset/
│ │ │ └── Contents.json
│ │ └── Warning.imageset/
│ │ └── Contents.json
│ ├── Bridging/
│ │ ├── Bridging.swift
│ │ └── Shims/
│ │ ├── Deprecated.swift
│ │ └── Private.swift
│ ├── Events/
│ │ ├── EventManager.swift
│ │ ├── EventMonitors/
│ │ │ ├── GlobalEventMonitor.swift
│ │ │ ├── LocalEventMonitor.swift
│ │ │ ├── RunLoopLocalEventMonitor.swift
│ │ │ └── UniversalEventMonitor.swift
│ │ └── EventTap.swift
│ ├── Hotkeys/
│ │ ├── Hotkey.swift
│ │ ├── HotkeyAction.swift
│ │ ├── HotkeyRegistry.swift
│ │ ├── KeyCode.swift
│ │ ├── KeyCombination.swift
│ │ └── Modifiers.swift
│ ├── Ice.entitlements
│ ├── Info.plist
│ ├── Main/
│ │ ├── AppDelegate.swift
│ │ ├── AppState.swift
│ │ ├── IceApp.swift
│ │ └── Navigation/
│ │ ├── AppNavigationState.swift
│ │ └── NavigationIdentifiers/
│ │ ├── NavigationIdentifier.swift
│ │ └── SettingsNavigationIdentifier.swift
│ ├── MenuBar/
│ │ ├── Appearance/
│ │ │ ├── Configurations/
│ │ │ │ ├── MenuBarAppearanceConfigurationV1.swift
│ │ │ │ └── MenuBarAppearanceConfigurationV2.swift
│ │ │ ├── MenuBarAppearanceEditor/
│ │ │ │ ├── MenuBarAppearanceEditor.swift
│ │ │ │ ├── MenuBarAppearanceEditorPanel.swift
│ │ │ │ └── MenuBarShapePicker.swift
│ │ │ ├── MenuBarAppearanceManager.swift
│ │ │ ├── MenuBarOverlayPanel.swift
│ │ │ ├── MenuBarShape.swift
│ │ │ └── MenuBarTintKind.swift
│ │ ├── ControlItem/
│ │ │ ├── ControlItem.swift
│ │ │ ├── ControlItemImage.swift
│ │ │ └── ControlItemImageSet.swift
│ │ ├── MenuBarItems/
│ │ │ ├── MenuBarItem.swift
│ │ │ ├── MenuBarItemImageCache.swift
│ │ │ ├── MenuBarItemInfo.swift
│ │ │ └── MenuBarItemManager.swift
│ │ ├── MenuBarManager.swift
│ │ ├── MenuBarSection.swift
│ │ ├── Search/
│ │ │ └── MenuBarSearchPanel.swift
│ │ └── Spacing/
│ │ └── MenuBarItemSpacingManager.swift
│ ├── Permissions/
│ │ ├── Permission.swift
│ │ ├── PermissionsManager.swift
│ │ ├── PermissionsView.swift
│ │ └── PermissionsWindow.swift
│ ├── Resources/
│ │ └── Acknowledgements.rtf
│ ├── Settings/
│ │ ├── SettingsManagers/
│ │ │ ├── AdvancedSettingsManager.swift
│ │ │ ├── GeneralSettingsManager.swift
│ │ │ ├── HotkeySettingsManager.swift
│ │ │ └── SettingsManager.swift
│ │ ├── SettingsPanes/
│ │ │ ├── AboutSettingsPane.swift
│ │ │ ├── AdvancedSettingsPane.swift
│ │ │ ├── GeneralSettingsPane.swift
│ │ │ ├── HotkeysSettingsPane.swift
│ │ │ ├── MenuBarAppearanceSettingsPane.swift
│ │ │ └── MenuBarLayoutSettingsPane.swift
│ │ ├── SettingsView.swift
│ │ └── SettingsWindow.swift
│ ├── Swizzling/
│ │ └── NSSplitViewItem+swizzledCanCollapse.swift
│ ├── UI/
│ │ ├── HotkeyRecorder/
│ │ │ ├── HotkeyRecorder.swift
│ │ │ └── HotkeyRecorderModel.swift
│ │ ├── IceBar/
│ │ │ ├── IceBar.swift
│ │ │ ├── IceBarColorManager.swift
│ │ │ └── IceBarLocation.swift
│ │ ├── IceUI/
│ │ │ ├── IceForm.swift
│ │ │ ├── IceGroupBox.swift
│ │ │ ├── IceLabeledContent.swift
│ │ │ ├── IceMenu.swift
│ │ │ ├── IcePicker.swift
│ │ │ ├── IceSection.swift
│ │ │ └── IceSlider.swift
│ │ ├── LayoutBar/
│ │ │ ├── LayoutBar.swift
│ │ │ ├── LayoutBarContainer.swift
│ │ │ ├── LayoutBarItemView.swift
│ │ │ ├── LayoutBarPaddingView.swift
│ │ │ └── LayoutBarScrollView.swift
│ │ ├── Pickers/
│ │ │ ├── CustomColorPicker/
│ │ │ │ └── CustomColorPicker.swift
│ │ │ └── CustomGradientPicker/
│ │ │ ├── ColorStop.swift
│ │ │ ├── CustomGradient.swift
│ │ │ └── CustomGradientPicker.swift
│ │ ├── Shapes/
│ │ │ └── AnyInsettableShape.swift
│ │ ├── ViewModifiers/
│ │ │ ├── BottomBar.swift
│ │ │ ├── ErasedToAnyView.swift
│ │ │ ├── LayoutBarStyle.swift
│ │ │ ├── LocalEventMonitorModifier.swift
│ │ │ ├── OnFrameChange.swift
│ │ │ ├── OnKeyDown.swift
│ │ │ ├── Once.swift
│ │ │ ├── ReadWindow.swift
│ │ │ └── RemoveSidebarToggle.swift
│ │ └── Views/
│ │ ├── AnnotationView.swift
│ │ ├── BetaBadge.swift
│ │ ├── SectionedList.swift
│ │ └── VisualEffectView.swift
│ ├── Updates/
│ │ └── UpdatesManager.swift
│ ├── UserNotifications/
│ │ ├── UserNotificationIdentifier.swift
│ │ └── UserNotificationManager.swift
│ └── Utilities/
│ ├── BindingExposable.swift
│ ├── CodableColor.swift
│ ├── Constants.swift
│ ├── Defaults.swift
│ ├── Extensions.swift
│ ├── IconResource.swift
│ ├── Injection.swift
│ ├── LocalizedErrorWrapper.swift
│ ├── Logging.swift
│ ├── MigrationManager.swift
│ ├── MouseCursor.swift
│ ├── Notifications.swift
│ ├── ObjectStorage.swift
│ ├── Predicates.swift
│ ├── RehideStrategy.swift
│ ├── ScreenCapture.swift
│ ├── StatusItemDefaults.swift
│ ├── SystemAppearance.swift
│ ├── TaskTimeout.swift
│ └── WindowInfo.swift
├── Ice.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm/
│ │ └── Package.resolved
│ └── xcshareddata/
│ └── xcschemes/
│ └── Ice.xcscheme
├── LICENSE
├── README.md
└── Resources/
└── Icon.fig
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
*.rtf linguist-vendored
================================================
FILE: .github/FUNDING.yml
================================================
github: jordanbaird
buy_me_a_coffee: jordanbaird
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: Submit a bug report.
title: "[Bug]: "
labels: Bug
body:
- type: checkboxes
attributes:
label: Search for Similar Reports
description: |
Please use the search feature on the repository's [Issues](https://github.com/jordanbaird/Ice/issues) page to see if a report already exists for the bug you encountered. Make sure to include both open and closed issues in your search.
**Important**: Only submit a new issue if the bug you encountered hasn't been reported in an existing open issue, or if a regression has occurred with a previously closed issue.
options:
- label: I have searched existing issues for similar reports
required: true
- type: textarea
attributes:
label: Description
placeholder: A clear and concise description of the bug...
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce
description: Steps to reliably reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: input
id: app_version
attributes:
label: App Version
placeholder: e.g. 1.2.3
validations:
required: true
- type: input
id: macos_version
attributes:
label: macOS Version
placeholder: e.g. 15.3.2
validations:
required: true
- type: textarea
attributes:
label: Additional Information
placeholder: Screenshots, screen recordings, logs, crash reports, or anything else you think might be helpful...
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature Request
description: Request a feature or suggest an idea.
title: "[Feature Request]: "
labels: Feature
body:
- type: checkboxes
attributes:
label: Search for Similar Requests
description: |
Please use the search feature on the repository's [Issues](https://github.com/jordanbaird/Ice/issues) page to see if this feature has already been requested. Make sure to include both open and closed issues in your search.
**Important**: Only submit a new issue if the feature you want hasn't been requested in another issue.
options:
- label: I have searched existing issues for similar requests
required: true
- type: textarea
attributes:
label: Description
placeholder: A clear and concise description of the feature...
validations:
required: true
- type: textarea
attributes:
label: Additional Information
placeholder: Screenshots, screen recordings, mockups, or anything else you think might be helpful...
================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint Swift Files
on:
push:
branches: [ "main" ]
paths:
- ".github/workflows/lint.yml"
- ".swiftlint.yml"
- "**/*.swift"
pull_request:
paths:
- ".github/workflows/lint.yml"
- ".swiftlint.yml"
- "**/*.swift"
jobs:
swiftlint:
if: '!github.event.pull_request.merged'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run SwiftLint
uses: norio-nomura/action-swiftlint@3.2.1
with:
args: --strict
================================================
FILE: .gitignore
================================================
.DS_Store
build/
xcuserdata/
================================================
FILE: .swiftlint.yml
================================================
included:
- Ice
disabled_rules:
- cyclomatic_complexity
- file_length
- function_body_length
- function_parameter_count
- generic_type_name
- identifier_name
- large_tuple
- line_length
- nesting
- opening_brace
- todo
- type_body_length
opt_in_rules:
- closure_end_indentation
- closure_spacing
- collection_alignment
- convenience_type
- discouraged_object_literal
- empty_count
- fatal_error_message
- file_header
- force_unwrapping
- implicitly_unwrapped_optional
- indentation_width
- literal_expression_end_indentation
- lower_acl_than_parent
- modifier_order
- multiline_arguments
- multiline_arguments_brackets
- multiline_literal_brackets
- multiline_parameters
- multiline_parameters_brackets
- period_spacing
- unavailable_function
- vertical_parameter_alignment_on_call
- vertical_whitespace_closing_braces
- yoda_condition
custom_rules:
objc_dynamic:
name: "@objc dynamic"
message: "`dynamic` modifier should immediately follow `@objc` attribute"
regex: '@objc\b(\(\w*\))?+\s*(\S+|\v+\S*)\s*\bdynamic'
match_kinds: attribute.builtin
prefer_spaces_over_tabs:
name: Prefer Spaces Over Tabs
message: "Indentation should use 4 spaces per indentation level instead of tabs"
regex: ^\t
file_header:
required_pattern: |
//
// SWIFTLINT_CURRENT_FILENAME
// Ice
//
modifier_order:
preferred_modifier_order:
- acl
- setterACL
- override
- mutators
- lazy
- final
- required
- convenience
- typeMethods
- owned
trailing_comma:
mandatory_comma: true
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
jordanbaird.dev@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
================================================
FILE: FREQUENT_ISSUES.md
================================================
# Frequent Issues <!-- omit in toc -->
- [Items are moved to the always-hidden section](#items-are-moved-to-the-always-hidden-section)
- [Ice removed an item](#ice-removed-an-item)
- [Ice does not remember the order of items](#ice-does-not-remember-the-order-of-items)
- [How do I solve the `Ice cannot arrange menu bar items in automatically hidden menu bars` error?](#how-do-i-solve-the-ice-cannot-arrange-menu-bar-items-in-automatically-hidden-menu-bars-error)
## Items are moved to the always-hidden section
By default, macOS adds new items to the far left of the menu bar, which is also the location of Ice's always-hidden section. Most apps are configured
to remember the positions of their items, but some are not. macOS treats the items of these apps as new items each time they appear. This results in
these items appearing in the always-hidden section, even if they have been previously been moved.
Ice does not currently manage individual items, and in fact cannot, as of the current release. Once issues
[#6](https://github.com/jordanbaird/Ice/issues/6) and [#26](https://github.com/jordanbaird/Ice/issues/26) are implemented, Ice will be able to
monitor the items in the menu bar, and move the ones it recognizes to their previous locations, even if macOS rearranges them.
## Ice removed an item
Ice does not have the ability to move or remove items. It likely got placed in the always-hidden section by macOS. Option + click the Ice icon to show
the always-hidden section, then Command + drag the item into a different section.
## Ice does not remember the order of items
This is not a bug, but a missing feature. It is being tracked in [#26](https://github.com/jordanbaird/Ice/issues/26).
## How do I solve the `Ice cannot arrange menu bar items in automatically hidden menu bars` error?
1. Open `System Settings` on your Mac
2. Go to `Control Center`
3. Select `Never` as shown in the image below
4. Update your `Menu Bar Items` in `Ice`
5. Return `Automatically hide and show the menu bar` to your preferred settings

================================================
FILE: Ice/Assets.xcassets/AccentColor.colorset/Contents.json
================================================
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Ice/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"filename" : "icon_16x16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon_16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon_32x32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon_32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "icon_128x128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "icon_128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "icon_256x256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "icon_256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "icon_512x512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon_512x512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Ice/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Ice/Assets.xcassets/ControlItemImages/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Ice/Assets.xcassets/ControlItemImages/Dot/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Ice/Assets.xcassets/ControlItemImages/Dot/DotFill.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "DotFill.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Ice/Assets.xcassets/ControlItemImages/Dot/DotStroke.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "DotStroke.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Ice/Assets.xcassets/ControlItemImages/Ellipsis/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Ice/Assets.xcassets/ControlItemImages/Ellipsis/EllipsisFill.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "EllipsisFill.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Ice/Assets.xcassets/ControlItemImages/Ellipsis/EllipsisStroke.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "EllipsisStroke.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Ice/Assets.xcassets/ControlItemImages/IceCube/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Ice/Assets.xcassets/ControlItemImages/IceCube/IceCubeFill.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "IceCubeFill.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Ice/Assets.xcassets/ControlItemImages/IceCube/IceCubeStroke.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "IceCubeStroke.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Ice/Assets.xcassets/DefaultLayoutBarColor.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "0.170",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "0.070",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Ice/Assets.xcassets/Warning.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Warning.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Ice/Bridging/Bridging.swift
================================================
//
// Bridging.swift
// Ice
//
import Cocoa
/// A namespace for bridged functionality.
enum Bridging { }
// MARK: - CGSConnection
extension Bridging {
/// Sets a value for the given key in the current connection to the window server.
///
/// - Parameters:
/// - value: The value to set for `key`.
/// - key: A key associated with the current connection to the window server.
static func setConnectionProperty(_ value: Any?, forKey key: String) {
let result = CGSSetConnectionProperty(
CGSMainConnectionID(),
CGSMainConnectionID(),
key as CFString,
value as CFTypeRef
)
if result != .success {
Logger.bridging.error("CGSSetConnectionProperty failed with error \(result.logString)")
}
}
/// Returns the value for the given key in the current connection to the window server.
///
/// - Parameter key: A key associated with the current connection to the window server.
/// - Returns: The value associated with `key` in the current connection to the window server.
static func getConnectionProperty(forKey key: String) -> Any? {
var value: Unmanaged<CFTypeRef>?
let result = CGSCopyConnectionProperty(
CGSMainConnectionID(),
CGSMainConnectionID(),
key as CFString,
&value
)
if result != .success {
Logger.bridging.error("CGSCopyConnectionProperty failed with error \(result.logString)")
}
return value?.takeRetainedValue()
}
}
// MARK: - CGSWindow
extension Bridging {
/// Returns the frame for the window with the specified identifier.
///
/// - Parameter windowID: An identifier for a window.
/// - Returns: The frame -- specified in screen coordinates -- of the window associated
/// with `windowID`, or `nil` if the operation failed.
static func getWindowFrame(for windowID: CGWindowID) -> CGRect? {
var rect = CGRect.zero
let result = CGSGetScreenRectForWindow(CGSMainConnectionID(), windowID, &rect)
guard result == .success else {
Logger.bridging.error("CGSGetScreenRectForWindow failed with error \(result.logString)")
return nil
}
return rect
}
}
// MARK: Private Window List Helpers
extension Bridging {
private static func getWindowCount() -> Int {
var count: Int32 = 0
let result = CGSGetWindowCount(CGSMainConnectionID(), 0, &count)
if result != .success {
Logger.bridging.error("CGSGetWindowCount failed with error \(result.logString)")
}
return Int(count)
}
private static func getOnScreenWindowCount() -> Int {
var count: Int32 = 0
let result = CGSGetOnScreenWindowCount(CGSMainConnectionID(), 0, &count)
if result != .success {
Logger.bridging.error("CGSGetOnScreenWindowCount failed with error \(result.logString)")
}
return Int(count)
}
private static func getWindowList() -> [CGWindowID] {
let windowCount = getWindowCount()
var list = [CGWindowID](repeating: 0, count: windowCount)
var realCount: Int32 = 0
let result = CGSGetWindowList(
CGSMainConnectionID(),
0,
Int32(windowCount),
&list,
&realCount
)
guard result == .success else {
Logger.bridging.error("CGSGetWindowList failed with error \(result.logString)")
return []
}
return [CGWindowID](list[..<Int(realCount)])
}
private static func getOnScreenWindowList() -> [CGWindowID] {
let windowCount = getOnScreenWindowCount()
var list = [CGWindowID](repeating: 0, count: windowCount)
var realCount: Int32 = 0
let result = CGSGetOnScreenWindowList(
CGSMainConnectionID(),
0,
Int32(windowCount),
&list,
&realCount
)
guard result == .success else {
Logger.bridging.error("CGSGetOnScreenWindowList failed with error \(result.logString)")
return []
}
return [CGWindowID](list[..<Int(realCount)])
}
private static func getMenuBarWindowList() -> [CGWindowID] {
let windowCount = getWindowCount()
var list = [CGWindowID](repeating: 0, count: windowCount)
var realCount: Int32 = 0
let result = CGSGetProcessMenuBarWindowList(
CGSMainConnectionID(),
0,
Int32(windowCount),
&list,
&realCount
)
guard result == .success else {
Logger.bridging.error("CGSGetProcessMenuBarWindowList failed with error \(result.logString)")
return []
}
return [CGWindowID](list[..<Int(realCount)])
}
private static func getOnScreenMenuBarWindowList() -> [CGWindowID] {
let onScreenList = Set(getOnScreenWindowList())
return getMenuBarWindowList().filter(onScreenList.contains)
}
}
// MARK: Public Window List API
extension Bridging {
/// Options that determine the window identifiers to return in a window list.
struct WindowListOption: OptionSet {
let rawValue: Int
/// Specifies windows that are currently on-screen.
static let onScreen = WindowListOption(rawValue: 1 << 0)
/// Specifies windows that represent items in the menu bar.
static let menuBarItems = WindowListOption(rawValue: 1 << 1)
/// Specifies windows on the currently active space.
static let activeSpace = WindowListOption(rawValue: 1 << 2)
}
/// The total number of windows.
static var windowCount: Int {
getWindowCount()
}
/// The number of windows currently on-screen.
static var onScreenWindowCount: Int {
getOnScreenWindowCount()
}
/// Returns a list of window identifiers using the given options.
///
/// - Parameter option: Options that filter the returned list.
static func getWindowList(option: WindowListOption = []) -> [CGWindowID] {
let list = if option.contains(.menuBarItems) {
if option.contains(.onScreen) {
getOnScreenMenuBarWindowList()
} else {
getMenuBarWindowList()
}
} else if option.contains(.onScreen) {
getOnScreenWindowList()
} else {
getWindowList()
}
return if option.contains(.activeSpace) {
list.filter(isWindowOnActiveSpace)
} else {
list
}
}
}
// MARK: - CGSSpace
extension Bridging {
/// Options that determine the space identifiers to return in a space list.
enum SpaceListOption {
case allSpaces, visibleSpaces
}
/// The identifier of the active space.
static var activeSpaceID: CGSSpaceID {
CGSGetActiveSpace(CGSMainConnectionID())
}
/// Returns an array of identifiers for the spaces containing the window with
/// the given identifier.
///
/// - Parameter windowID: An identifier for a window.
static func getSpaceList(for windowID: CGWindowID, option: SpaceListOption) -> [CGSSpaceID] {
let mask: CGSSpaceMask = switch option {
case .allSpaces: .allSpaces
case .visibleSpaces: .allVisibleSpaces
}
guard let spaces = CGSCopySpacesForWindows(CGSMainConnectionID(), mask, [windowID] as CFArray) else {
Logger.bridging.error("CGSCopySpacesForWindows failed")
return []
}
guard let spaceIDs = spaces.takeRetainedValue() as? [CGSSpaceID] else {
Logger.bridging.error("CGSCopySpacesForWindows returned array of unexpected type")
return []
}
return spaceIDs
}
/// Returns a Boolean value that indicates whether the window with the
/// given identifier is on the active space.
///
/// - Parameter windowID: An identifier for a window.
static func isWindowOnActiveSpace(_ windowID: CGWindowID) -> Bool {
getSpaceList(for: windowID, option: .allSpaces).contains(activeSpaceID)
}
/// Returns a Boolean value that indicates whether the space with the given
/// identifier is a fullscreen space.
///
/// - Parameter spaceID: An identifier for a space.
static func isSpaceFullscreen(_ spaceID: CGSSpaceID) -> Bool {
let type = CGSSpaceGetType(CGSMainConnectionID(), spaceID)
return type == .fullscreen
}
}
// MARK: - Process Responsivity
extension Bridging {
/// Constants that indicate the responsivity of an app.
enum Responsivity {
case responsive, unresponsive, unknown
}
/// Returns the responsivity of the given process.
///
/// - Parameter pid: The Unix process identifier of the process to check.
static func responsivity(for pid: pid_t) -> Responsivity {
var psn = ProcessSerialNumber()
let result = GetProcessForPID(pid, &psn)
guard result == noErr else {
Logger.bridging.error("GetProcessForPID failed with error \(result)")
return .unknown
}
if CGSEventIsAppUnresponsive(CGSMainConnectionID(), &psn) {
return .unresponsive
}
return .responsive
}
}
// MARK: - Logger
private extension Logger {
static let bridging = Logger(category: "Bridging")
}
================================================
FILE: Ice/Bridging/Shims/Deprecated.swift
================================================
//
// Deprecated.swift
// Ice
//
import ApplicationServices
/// Returns a PSN for a given PID.
@_silgen_name("GetProcessForPID")
func GetProcessForPID(
_ pid: pid_t,
_ psn: inout ProcessSerialNumber
) -> OSStatus
================================================
FILE: Ice/Bridging/Shims/Private.swift
================================================
//
// Private.swift
// Ice
//
import CoreGraphics
// MARK: - Bridged Types
typealias CGSConnectionID = Int32
typealias CGSSpaceID = size_t
enum CGSSpaceType: UInt32 {
case user = 0
case system = 2
case fullscreen = 4
}
struct CGSSpaceMask: OptionSet {
let rawValue: UInt32
static let includesCurrent = CGSSpaceMask(rawValue: 1 << 0)
static let includesOthers = CGSSpaceMask(rawValue: 1 << 1)
static let includesUser = CGSSpaceMask(rawValue: 1 << 2)
static let includesVisible = CGSSpaceMask(rawValue: 1 << 16)
static let currentSpace: CGSSpaceMask = [.includesUser, .includesCurrent]
static let otherSpaces: CGSSpaceMask = [.includesOthers, .includesCurrent]
static let allSpaces: CGSSpaceMask = [.includesUser, .includesOthers, .includesCurrent]
static let allVisibleSpaces: CGSSpaceMask = [.includesVisible, .allSpaces]
}
// MARK: - CGSConnection Functions
@_silgen_name("CGSMainConnectionID")
func CGSMainConnectionID() -> CGSConnectionID
@_silgen_name("CGSCopyConnectionProperty")
func CGSCopyConnectionProperty(
_ cid: CGSConnectionID,
_ targetCID: CGSConnectionID,
_ key: CFString,
_ outValue: inout Unmanaged<CFTypeRef>?
) -> CGError
@_silgen_name("CGSSetConnectionProperty")
func CGSSetConnectionProperty(
_ cid: CGSConnectionID,
_ targetCID: CGSConnectionID,
_ key: CFString,
_ value: CFTypeRef
) -> CGError
// MARK: - CGSEvent Functions
@_silgen_name("CGSEventIsAppUnresponsive")
func CGSEventIsAppUnresponsive(
_ cid: CGSConnectionID,
_ psn: inout ProcessSerialNumber
) -> Bool
// MARK: - CGSSpace Functions
@_silgen_name("CGSGetActiveSpace")
func CGSGetActiveSpace(_ cid: CGSConnectionID) -> CGSSpaceID
@_silgen_name("CGSCopySpacesForWindows")
func CGSCopySpacesForWindows(
_ cid: CGSConnectionID,
_ mask: CGSSpaceMask,
_ windowIDs: CFArray
) -> Unmanaged<CFArray>?
@_silgen_name("CGSSpaceGetType")
func CGSSpaceGetType(
_ cid: CGSConnectionID,
_ sid: CGSSpaceID
) -> CGSSpaceType
// MARK: - CGSWindow Functions
@_silgen_name("CGSGetWindowList")
func CGSGetWindowList(
_ cid: CGSConnectionID,
_ targetCID: CGSConnectionID,
_ count: Int32,
_ list: UnsafeMutablePointer<CGWindowID>,
_ outCount: inout Int32
) -> CGError
@_silgen_name("CGSGetOnScreenWindowList")
func CGSGetOnScreenWindowList(
_ cid: CGSConnectionID,
_ targetCID: CGSConnectionID,
_ count: Int32,
_ list: UnsafeMutablePointer<CGWindowID>,
_ outCount: inout Int32
) -> CGError
@_silgen_name("CGSGetProcessMenuBarWindowList")
func CGSGetProcessMenuBarWindowList(
_ cid: CGSConnectionID,
_ targetCID: CGSConnectionID,
_ count: Int32,
_ list: UnsafeMutablePointer<CGWindowID>,
_ outCount: inout Int32
) -> CGError
@_silgen_name("CGSGetWindowCount")
func CGSGetWindowCount(
_ cid: CGSConnectionID,
_ targetCID: CGSConnectionID,
_ outCount: inout Int32
) -> CGError
@_silgen_name("CGSGetOnScreenWindowCount")
func CGSGetOnScreenWindowCount(
_ cid: CGSConnectionID,
_ targetCID: CGSConnectionID,
_ outCount: inout Int32
) -> CGError
@_silgen_name("CGSGetScreenRectForWindow")
func CGSGetScreenRectForWindow(
_ cid: CGSConnectionID,
_ wid: CGWindowID,
_ outRect: inout CGRect
) -> CGError
================================================
FILE: Ice/Events/EventManager.swift
================================================
//
// EventManager.swift
// Ice
//
import Cocoa
import Combine
/// Manager for the various event monitors maintained by the app.
@MainActor
final class EventManager {
/// The shared app state.
private weak var appState: AppState?
/// Storage for internal observers.
private var cancellables = Set<AnyCancellable>()
// MARK: Monitors
/// Monitor for mouse down events.
private(set) lazy var mouseDownMonitor = UniversalEventMonitor(
mask: [.leftMouseDown, .rightMouseDown]
) { [weak self] event in
guard let self else {
return event
}
switch event.type {
case .leftMouseDown:
handleShowOnClick()
handleSmartRehide(with: event)
case .rightMouseDown:
handleShowRightClickMenu()
default:
break
}
handlePreventShowOnHover(with: event)
return event
}
/// Monitor for mouse up events.
private(set) lazy var mouseUpMonitor = UniversalEventMonitor(
mask: .leftMouseUp
) { [weak self] event in
self?.handleLeftMouseUp()
return event
}
/// Monitor for mouse dragged events.
private(set) lazy var mouseDraggedMonitor = UniversalEventMonitor(
mask: .leftMouseDragged
) { [weak self] event in
self?.handleLeftMouseDragged(with: event)
return event
}
/// Monitor for mouse moved events.
private(set) lazy var mouseMovedMonitor = UniversalEventMonitor(
mask: .mouseMoved
) { [weak self] event in
self?.handleShowOnHover()
return event
}
/// Monitor for scroll wheel events.
private(set) lazy var scrollWheelMonitor = UniversalEventMonitor(
mask: .scrollWheel
) { [weak self] event in
self?.handleShowOnScroll(with: event)
return event
}
// MARK: All Monitors
/// All monitors maintained by the app.
private lazy var allMonitors = [
mouseDownMonitor,
mouseUpMonitor,
mouseDraggedMonitor,
mouseMovedMonitor,
scrollWheelMonitor,
]
// MARK: Initializers
/// Creates an event manager with the given app state.
init(appState: AppState) {
self.appState = appState
}
/// Sets up the manager.
func performSetup() {
startAll()
configureCancellables()
}
/// Configures the internal observers for the manager.
private func configureCancellables() {
var c = Set<AnyCancellable>()
if let appState {
if let hiddenSection = appState.menuBarManager.section(withName: .hidden) {
// In fullscreen mode, the menu bar slides down from the top on hover. Observe
// the frame of the hidden section's control item, which we know will always be
// in the menu bar, and run the show-on-hover check when it changes.
Publishers.CombineLatest(
hiddenSection.controlItem.$windowFrame,
appState.$isActiveSpaceFullscreen
)
.sink { [weak self] _, isFullscreen in
guard
let self,
isFullscreen
else {
return
}
handleShowOnHover()
}
.store(in: &c)
}
}
cancellables = c
}
// MARK: Start/Stop
/// Starts all monitors.
func startAll() {
for monitor in allMonitors {
monitor.start()
}
}
/// Stops all monitors.
func stopAll() {
for monitor in allMonitors {
monitor.stop()
}
}
}
// MARK: - Handlers
extension EventManager {
// MARK: Handle Show On Click
private func handleShowOnClick() {
guard
let appState,
appState.settingsManager.generalSettingsManager.showOnClick,
isMouseInsideEmptyMenuBarSpace
else {
return
}
Task {
// Short delay helps the toggle action feel more natural.
try? await Task.sleep(for: .milliseconds(50))
if NSEvent.modifierFlags == .control {
handleShowRightClickMenu()
} else if
NSEvent.modifierFlags == .option,
appState.settingsManager.advancedSettingsManager.canToggleAlwaysHiddenSection
{
if let alwaysHiddenSection = appState.menuBarManager.section(withName: .alwaysHidden) {
alwaysHiddenSection.toggle()
}
} else {
if let hiddenSection = appState.menuBarManager.section(withName: .hidden) {
hiddenSection.toggle()
}
}
}
}
// MARK: Handle Smart Rehide
private func handleSmartRehide(with event: NSEvent) {
guard
let appState,
appState.settingsManager.generalSettingsManager.autoRehide,
case .smart = appState.settingsManager.generalSettingsManager.rehideStrategy
else {
return
}
if let visibleSection = appState.menuBarManager.section(withName: .visible) {
guard event.window !== visibleSection.controlItem.window else {
return
}
}
// Make sure clicking the Ice Bar doesn't trigger rehide.
guard event.window !== appState.menuBarManager.iceBarPanel else {
return
}
// Only continue if a section is currently visible.
guard appState.menuBarManager.sections.contains(where: { !$0.isHidden }) else {
return
}
// Make sure the mouse is not in the menu bar.
guard !isMouseInsideMenuBar else {
return
}
Task {
let initialSpaceID = Bridging.activeSpaceID
// Sleep for a bit to give the window under the mouse a chance to focus.
try? await Task.sleep(for: .seconds(0.25))
// If clicking caused a space change, don't bother with the window check.
if Bridging.activeSpaceID != initialSpaceID {
for section in appState.menuBarManager.sections {
section.hide()
}
return
}
// Get the window that the user has clicked into.
guard
let mouseLocation = MouseCursor.locationCoreGraphics,
let windowUnderMouse = WindowInfo.getOnScreenWindows(excludeDesktopWindows: false)
.filter({ $0.layer < CGWindowLevelForKey(.cursorWindow) })
.first(where: { $0.frame.contains(mouseLocation) && $0.title?.isEmpty == false }),
let owningApplication = windowUnderMouse.owningApplication
else {
return
}
// The dock is an exception to the following check.
if owningApplication.bundleIdentifier != "com.apple.dock" {
// Only continue if the user has clicked into an active window with
// a regular activation policy.
guard
owningApplication.isActive,
owningApplication.activationPolicy == .regular
else {
return
}
}
// If all the above checks have passed, hide all sections.
for section in appState.menuBarManager.sections {
section.hide()
}
}
}
// MARK: Handle Show Right Click Menu
private func handleShowRightClickMenu() {
guard
let appState,
appState.settingsManager.advancedSettingsManager.showContextMenuOnRightClick,
isMouseInsideEmptyMenuBarSpace,
let mouseLocation = MouseCursor.locationAppKit
else {
return
}
appState.menuBarManager.showRightClickMenu(at: mouseLocation)
}
// MARK: Handle Prevent Show On Hover
private func handlePreventShowOnHover(with event: NSEvent) {
guard
let appState,
appState.settingsManager.generalSettingsManager.showOnHover,
!appState.settingsManager.generalSettingsManager.useIceBar,
isMouseInsideMenuBar
else {
return
}
if isMouseInsideMenuBarItem {
switch event.type {
case .leftMouseDown:
if appState.menuBarManager.sections.contains(where: { !$0.isHidden }) || isMouseInsideIceIcon {
// We have a left click that is inside the menu bar while at least one
// section is visible or the mouse is inside the Ice icon.
appState.preventShowOnHover()
}
case .rightMouseDown:
if appState.menuBarManager.sections.contains(where: { !$0.isHidden }) {
// We have a right click that is inside the menu bar while at least one
// section is visible.
appState.preventShowOnHover()
}
default:
break
}
} else if !isMouseInsideApplicationMenu {
// We have a left or right click that is inside the menu bar, outside
// a menu bar item, and outside the application menu, so it _must_ be
// inside an empty menu bar space.
appState.preventShowOnHover()
}
}
// MARK: Handle Left Mouse Up
private func handleLeftMouseUp() {
guard let appearanceManager = appState?.appearanceManager else {
return
}
appearanceManager.setIsDraggingMenuBarItem(false)
}
// MARK: Handle Left Mouse Dragged
private func handleLeftMouseDragged(with event: NSEvent) {
guard
let appState,
event.modifierFlags.contains(.command),
isMouseInsideMenuBar
else {
return
}
// Notify each overlay panel that a menu bar item is being dragged.
appState.appearanceManager.setIsDraggingMenuBarItem(true)
// Don't continue if the setting to show the sections is disabled.
guard appState.settingsManager.advancedSettingsManager.showAllSectionsOnUserDrag else {
return
}
// Show all items, including section dividers.
for section in appState.menuBarManager.sections {
section.controlItem.state = .showItems
guard
section.controlItem.isSectionDivider,
!section.controlItem.isVisible
else {
continue
}
section.controlItem.isVisible = true
}
}
// MARK: Handle Show On Hover
private func handleShowOnHover() {
guard let appState else {
return
}
// Make sure the "ShowOnHover" feature is enabled and not prevented.
guard
appState.settingsManager.generalSettingsManager.showOnHover,
!appState.isShowOnHoverPrevented
else {
return
}
// Only continue if we have a hidden section (we should).
guard let hiddenSection = appState.menuBarManager.section(withName: .hidden) else {
return
}
let delay = appState.settingsManager.advancedSettingsManager.showOnHoverDelay
Task {
if hiddenSection.isHidden {
guard self.isMouseInsideEmptyMenuBarSpace else {
return
}
try? await Task.sleep(for: .seconds(delay))
// Make sure the mouse is still inside.
guard self.isMouseInsideEmptyMenuBarSpace else {
return
}
hiddenSection.show()
} else {
guard
!self.isMouseInsideMenuBar,
!self.isMouseInsideIceBar
else {
return
}
try? await Task.sleep(for: .seconds(delay))
// Make sure the mouse is still outside.
guard
!self.isMouseInsideMenuBar,
!self.isMouseInsideIceBar
else {
return
}
hiddenSection.hide()
}
}
}
// MARK: Handle Show On Scroll
private func handleShowOnScroll(with event: NSEvent) {
guard let appState else {
return
}
// Make sure the "ShowOnScroll" feature is enabled.
guard appState.settingsManager.generalSettingsManager.showOnScroll else {
return
}
// Make sure the mouse is inside the menu bar.
guard isMouseInsideMenuBar else {
return
}
// Only continue if we have a hidden section (we should).
guard let hiddenSection = appState.menuBarManager.section(withName: .hidden) else {
return
}
let averageDelta = (event.scrollingDeltaX + event.scrollingDeltaY) / 2
if averageDelta > 5 {
hiddenSection.show()
} else if averageDelta < -5 {
hiddenSection.hide()
}
}
}
// MARK: - Helpers
extension EventManager {
/// Returns the best screen to use for event manager calculations.
var bestScreen: NSScreen? {
guard let appState else {
return nil
}
if appState.isActiveSpaceFullscreen {
return NSScreen.screenWithMouse ?? NSScreen.main
} else {
return NSScreen.main
}
}
/// A Boolean value that indicates whether the mouse pointer is within
/// the bounds of the menu bar.
var isMouseInsideMenuBar: Bool {
guard
let screen = bestScreen,
let appState
else {
return false
}
if appState.menuBarManager.isMenuBarHiddenBySystem || appState.isActiveSpaceFullscreen {
if
let mouseLocation = MouseCursor.locationCoreGraphics,
let menuBarWindow = WindowInfo.getMenuBarWindow(for: screen.displayID)
{
return menuBarWindow.frame.contains(mouseLocation)
}
} else if let mouseLocation = MouseCursor.locationAppKit {
return mouseLocation.y > screen.visibleFrame.maxY && mouseLocation.y <= screen.frame.maxY
}
return false
}
/// A Boolean value that indicates whether the mouse pointer is within
/// the bounds of the current application menu.
var isMouseInsideApplicationMenu: Bool {
guard
let mouseLocation = MouseCursor.locationCoreGraphics,
let screen = bestScreen,
let appState,
var applicationMenuFrame = appState.menuBarManager.getApplicationMenuFrame(for: screen.displayID)
else {
return false
}
applicationMenuFrame.size.width += applicationMenuFrame.origin.x - screen.frame.origin.x
applicationMenuFrame.origin.x = screen.frame.origin.x
return applicationMenuFrame.contains(mouseLocation)
}
/// A Boolean value that indicates whether the mouse pointer is within
/// the bounds of a menu bar item.
var isMouseInsideMenuBarItem: Bool {
guard
let screen = bestScreen,
let mouseLocation = MouseCursor.locationCoreGraphics
else {
return false
}
let menuBarItems = MenuBarItem.getMenuBarItems(on: screen.displayID, onScreenOnly: true, activeSpaceOnly: true)
return menuBarItems.contains { $0.frame.contains(mouseLocation) }
}
/// A Boolean value that indicates whether the mouse pointer is within
/// the bounds of the screen's notch, if it has one.
///
/// If the screen returned from ``bestScreen`` does not have a notch,
/// this property returns `false`.
var isMouseInsideNotch: Bool {
guard
let screen = bestScreen,
let mouseLocation = MouseCursor.locationAppKit,
let frameOfNotch = screen.frameOfNotch
else {
return false
}
return frameOfNotch.contains(mouseLocation)
}
/// A Boolean value that indicates whether the mouse pointer is within
/// the bounds of an empty space in the menu bar.
var isMouseInsideEmptyMenuBarSpace: Bool {
isMouseInsideMenuBar &&
!isMouseInsideApplicationMenu &&
!isMouseInsideMenuBarItem &&
!isMouseInsideNotch
}
/// A Boolean value that indicates whether the mouse pointer is within
/// the bounds of the Ice Bar panel.
var isMouseInsideIceBar: Bool {
guard
let appState,
let mouseLocation = MouseCursor.locationAppKit
else {
return false
}
let panel = appState.menuBarManager.iceBarPanel
// Pad the frame to be more forgiving if the user accidentally
// moves their mouse outside of the Ice Bar.
let paddedFrame = panel.frame.insetBy(dx: -10, dy: -10)
return paddedFrame.contains(mouseLocation)
}
/// A Boolean value that indicates whether the mouse pointer is within
/// the bounds of the Ice icon.
var isMouseInsideIceIcon: Bool {
guard
let appState,
let visibleSection = appState.menuBarManager.section(withName: .visible),
let iceIconFrame = visibleSection.controlItem.windowFrame,
let mouseLocation = MouseCursor.locationAppKit
else {
return false
}
return iceIconFrame.contains(mouseLocation)
}
}
// MARK: - Logger
private extension Logger {
static let eventManager = Logger(category: "EventManager")
}
================================================
FILE: Ice/Events/EventMonitors/GlobalEventMonitor.swift
================================================
//
// GlobalEventMonitor.swift
// Ice
//
import Cocoa
import Combine
/// A type that monitors for events outside the scope of the current process.
final class GlobalEventMonitor {
private let mask: NSEvent.EventTypeMask
private let handler: (NSEvent) -> Void
private var monitor: Any?
/// Creates an event monitor with the given event type mask and handler.
///
/// - Parameters:
/// - mask: An event type mask specifying which events to monitor.
/// - handler: A handler to execute when the event monitor receives
/// an event corresponding to the event types in `mask`.
init(mask: NSEvent.EventTypeMask, handler: @escaping (_ event: NSEvent) -> Void) {
self.mask = mask
self.handler = handler
}
deinit {
stop()
}
/// Starts monitoring for events.
func start() {
guard monitor == nil else {
return
}
monitor = NSEvent.addGlobalMonitorForEvents(
matching: mask,
handler: handler
)
}
/// Stops monitoring for events.
func stop() {
guard let monitor else {
return
}
NSEvent.removeMonitor(monitor)
self.monitor = nil
}
}
extension GlobalEventMonitor {
/// A publisher that emits global events for an event type mask.
struct GlobalEventPublisher: Publisher {
typealias Output = NSEvent
typealias Failure = Never
let mask: NSEvent.EventTypeMask
func receive<S: Subscriber<Output, Failure>>(subscriber: S) {
let subscription = GlobalEventSubscription(mask: mask, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
/// Returns a publisher that emits global events for the given event type mask.
///
/// - Parameter mask: An event type mask specifying which events to publish.
static func publisher(for mask: NSEvent.EventTypeMask) -> GlobalEventPublisher {
GlobalEventPublisher(mask: mask)
}
}
extension GlobalEventMonitor.GlobalEventPublisher {
private final class GlobalEventSubscription<S: Subscriber<Output, Failure>>: Subscription {
var subscriber: S?
let monitor: GlobalEventMonitor
init(mask: NSEvent.EventTypeMask, subscriber: S) {
self.subscriber = subscriber
self.monitor = GlobalEventMonitor(mask: mask) { event in
_ = subscriber.receive(event)
}
monitor.start()
}
func request(_ demand: Subscribers.Demand) { }
func cancel() {
monitor.stop()
subscriber = nil
}
}
}
================================================
FILE: Ice/Events/EventMonitors/LocalEventMonitor.swift
================================================
//
// LocalEventMonitor.swift
// Ice
//
import Cocoa
import Combine
/// A type that monitors for events within the scope of the current process.
final class LocalEventMonitor {
private let mask: NSEvent.EventTypeMask
private let handler: (NSEvent) -> NSEvent?
private var monitor: Any?
/// Creates an event monitor with the given event type mask and handler.
///
/// - Parameters:
/// - mask: An event type mask specifying which events to monitor.
/// - handler: A handler to execute when the event monitor receives
/// an event corresponding to the event types in `mask`.
init(mask: NSEvent.EventTypeMask, handler: @escaping (_ event: NSEvent) -> NSEvent?) {
self.mask = mask
self.handler = handler
}
deinit {
stop()
}
/// Starts monitoring for events.
func start() {
guard monitor == nil else {
return
}
monitor = NSEvent.addLocalMonitorForEvents(
matching: mask,
handler: handler
)
}
/// Stops monitoring for events.
func stop() {
guard let monitor else {
return
}
NSEvent.removeMonitor(monitor)
self.monitor = nil
}
}
extension LocalEventMonitor {
/// A publisher that emits local events for an event type mask.
struct LocalEventPublisher: Publisher {
typealias Output = NSEvent
typealias Failure = Never
let mask: NSEvent.EventTypeMask
func receive<S: Subscriber<Output, Failure>>(subscriber: S) {
let subscription = LocalEventSubscription(mask: mask, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
/// Returns a publisher that emits local events for the given event type mask.
///
/// - Parameter mask: An event type mask specifying which events to publish.
static func publisher(for mask: NSEvent.EventTypeMask) -> LocalEventPublisher {
LocalEventPublisher(mask: mask)
}
}
extension LocalEventMonitor.LocalEventPublisher {
private final class LocalEventSubscription<S: Subscriber<Output, Failure>>: Subscription {
var subscriber: S?
let monitor: LocalEventMonitor
init(mask: NSEvent.EventTypeMask, subscriber: S) {
self.subscriber = subscriber
self.monitor = LocalEventMonitor(mask: mask) { event in
_ = subscriber.receive(event)
return event
}
monitor.start()
}
func request(_ demand: Subscribers.Demand) { }
func cancel() {
monitor.stop()
subscriber = nil
}
}
}
================================================
FILE: Ice/Events/EventMonitors/RunLoopLocalEventMonitor.swift
================================================
//
// RunLoopLocalEventMonitor.swift
// Ice
//
import Cocoa
import Combine
final class RunLoopLocalEventMonitor {
private let runLoop = CFRunLoopGetCurrent()
private let mode: RunLoop.Mode
private let handler: (NSEvent) -> NSEvent?
private let observer: CFRunLoopObserver
/// Creates an event monitor with the given event type mask and handler.
///
/// - Parameters:
/// - mask: An event type mask specifying which events to monitor.
/// - handler: A handler to execute when the event monitor receives
/// an event corresponding to the event types in `mask`.
init(
mask: NSEvent.EventTypeMask,
mode: RunLoop.Mode,
handler: @escaping (_ event: NSEvent) -> NSEvent?
) {
self.mode = mode
self.handler = handler
self.observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
CFRunLoopActivity.beforeSources.rawValue,
true,
0
) { _, _ in
var events = [NSEvent]()
while let event = NSApp.nextEvent(matching: .any, until: nil, inMode: .default, dequeue: true) {
events.append(event)
}
for event in events {
var handledEvent: NSEvent?
if !mask.contains(NSEvent.EventTypeMask(rawValue: 1 << event.type.rawValue)) {
handledEvent = event
} else if let eventFromHandler = handler(event) {
handledEvent = eventFromHandler
}
guard let handledEvent else {
continue
}
NSApp.postEvent(handledEvent, atStart: false)
}
}
}
deinit {
stop()
}
func start() {
CFRunLoopAddObserver(
runLoop,
observer,
CFRunLoopMode(mode.rawValue as CFString)
)
}
func stop() {
CFRunLoopRemoveObserver(
runLoop,
observer,
CFRunLoopMode(mode.rawValue as CFString)
)
}
}
extension RunLoopLocalEventMonitor {
/// A publisher that emits local events for an event type mask.
struct RunLoopLocalEventPublisher: Publisher {
typealias Output = NSEvent
typealias Failure = Never
let mask: NSEvent.EventTypeMask
let mode: RunLoop.Mode
func receive<S: Subscriber<Output, Failure>>(subscriber: S) {
let subscription = RunLoopLocalEventSubscription(mask: mask, mode: mode, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
/// Returns a publisher that emits local events for the given event type mask.
///
/// - Parameter mask: An event type mask specifying which events to publish.
static func publisher(for mask: NSEvent.EventTypeMask, mode: RunLoop.Mode) -> RunLoopLocalEventPublisher {
RunLoopLocalEventPublisher(mask: mask, mode: mode)
}
}
extension RunLoopLocalEventMonitor.RunLoopLocalEventPublisher {
private final class RunLoopLocalEventSubscription<S: Subscriber<Output, Failure>>: Subscription {
var subscriber: S?
let monitor: RunLoopLocalEventMonitor
init(mask: NSEvent.EventTypeMask, mode: RunLoop.Mode, subscriber: S) {
self.subscriber = subscriber
self.monitor = RunLoopLocalEventMonitor(mask: mask, mode: mode) { event in
_ = subscriber.receive(event)
return event
}
monitor.start()
}
func request(_ demand: Subscribers.Demand) { }
func cancel() {
monitor.stop()
subscriber = nil
}
}
}
================================================
FILE: Ice/Events/EventMonitors/UniversalEventMonitor.swift
================================================
//
// UniversalEventMonitor.swift
// Ice
//
import Cocoa
import Combine
/// A type that monitors for local and global events.
final class UniversalEventMonitor {
private let local: LocalEventMonitor
private let global: GlobalEventMonitor
/// Creates an event monitor with the given event type mask and handler.
///
/// - Parameters:
/// - mask: An event type mask specifying which events to monitor.
/// - handler: A handler to execute when the event monitor receives
/// an event corresponding to the event types in `mask`.
init(mask: NSEvent.EventTypeMask, handler: @escaping (_ event: NSEvent) -> NSEvent?) {
self.local = LocalEventMonitor(mask: mask, handler: handler)
self.global = GlobalEventMonitor(mask: mask, handler: { _ = handler($0) })
}
deinit {
stop()
}
/// Starts monitoring for events.
func start() {
local.start()
global.start()
}
/// Stops monitoring for events.
func stop() {
local.stop()
global.stop()
}
}
extension UniversalEventMonitor {
/// A publisher that emits local and global events for an event type mask.
struct UniversalEventPublisher: Publisher {
typealias Output = NSEvent
typealias Failure = Never
let mask: NSEvent.EventTypeMask
func receive<S: Subscriber<Output, Failure>>(subscriber: S) {
let subscription = UniversalEventSubscription(mask: mask, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
/// Returns a publisher that emits local and global events for the given
/// event type mask.
///
/// - Parameter mask: An event type mask specifying which events to publish.
static func publisher(for mask: NSEvent.EventTypeMask) -> UniversalEventPublisher {
UniversalEventPublisher(mask: mask)
}
}
extension UniversalEventMonitor.UniversalEventPublisher {
private final class UniversalEventSubscription<S: Subscriber<Output, Failure>>: Subscription {
var subscriber: S?
let monitor: UniversalEventMonitor
init(mask: NSEvent.EventTypeMask, subscriber: S) {
self.subscriber = subscriber
self.monitor = UniversalEventMonitor(mask: mask) { event in
_ = subscriber.receive(event)
return event
}
monitor.start()
}
func request(_ demand: Subscribers.Demand) { }
func cancel() {
monitor.stop()
subscriber = nil
}
}
}
================================================
FILE: Ice/Events/EventTap.swift
================================================
//
// EventTap.swift
// Ice
//
import Cocoa
/// A type that receives system events from various locations within the
/// event stream.
@MainActor
final class EventTap {
/// Constants that specify the possible tapping locations for events.
enum Location {
/// The location where HID system events enter the window server.
case hidEventTap
/// The location where HID system and remote control events enter
/// a login session.
case sessionEventTap
/// The location where session events have been annotated to flow
/// to an application.
case annotatedSessionEventTap
/// The location where annotated events are delivered to a specific
/// process.
case pid(pid_t)
var logString: String {
switch self {
case .hidEventTap: "HID event tap"
case .sessionEventTap: "session event tap"
case .annotatedSessionEventTap: "annotated session event tap"
case .pid(let pid): "PID \(pid)"
}
}
}
/// A proxy for an event tap.
///
/// Event tap proxies are passed to an event tap's callback, and can be
/// used to post additional events to the tap before the callback returns
/// or to disable the tap from within the callback.
@MainActor
struct Proxy {
private let tap: EventTap
private let pointer: CGEventTapProxy
/// The label associated with the event tap.
var label: String {
tap.label
}
/// A Boolean value that indicates whether the event tap is enabled.
var isEnabled: Bool {
tap.isEnabled
}
fileprivate init(tap: EventTap, pointer: CGEventTapProxy) {
self.tap = tap
self.pointer = pointer
}
/// Posts an event into the event stream from the location of this tap.
func postEvent(_ event: CGEvent) {
event.tapPostEvent(pointer)
}
/// Enables the event tap.
func enable() {
tap.enable()
}
/// Enables the event tap with the given timeout.
func enable(timeout: Duration, onTimeout: @escaping () -> Void) {
tap.enable(timeout: timeout, onTimeout: onTimeout)
}
/// Disables the event tap.
func disable() {
tap.disable()
}
}
private let runLoop = CFRunLoopGetCurrent()
private let mode: CFRunLoopMode = .commonModes
private nonisolated let callback: (EventTap, CGEventTapProxy, CGEventType, CGEvent) -> Unmanaged<CGEvent>?
private var machPort: CFMachPort?
private var source: CFRunLoopSource?
/// The label associated with the event tap.
let label: String
/// A Boolean value that indicates whether the event tap is enabled.
var isEnabled: Bool {
guard let machPort else {
return false
}
return CGEvent.tapIsEnabled(tap: machPort)
}
/// Creates a new event tap.
///
/// - Parameters:
/// - label: The label associated with the tap.
/// - kind: The kind of tap to create.
/// - location: The location to listen for events.
/// - placement: The placement of the tap relative to other active taps.
/// - types: The event types to listen for.
/// - callback: A callback function to perform when the tap receives events.
init(
label: String = #function,
options: CGEventTapOptions,
location: Location,
place: CGEventTapPlacement,
types: [CGEventType],
callback: @MainActor @escaping (_ proxy: Proxy, _ type: CGEventType, _ event: CGEvent) -> CGEvent?
) {
self.label = label
self.callback = { @MainActor tap, pointer, type, event in
callback(Proxy(tap: tap, pointer: pointer), type, event).map(Unmanaged.passUnretained)
}
guard let machPort = Self.createTapMachPort(
location: location,
place: place,
options: options,
eventsOfInterest: types.reduce(into: 0) { $0 |= 1 << $1.rawValue },
callback: handleEvent,
userInfo: Unmanaged.passUnretained(self).toOpaque()
) else {
Logger.eventTap.error("Error creating mach port for event tap \"\(self.label)\"")
return
}
guard let source = CFMachPortCreateRunLoopSource(nil, machPort, 0) else {
Logger.eventTap.error("Error creating run loop source for event tap \"\(self.label)\"")
return
}
self.machPort = machPort
self.source = source
}
deinit {
guard let machPort else {
return
}
CFRunLoopRemoveSource(runLoop, source, mode)
CGEvent.tapEnable(tap: machPort, enable: false)
CFMachPortInvalidate(machPort)
}
fileprivate nonisolated static func performCallback(
for eventTap: EventTap,
proxy: CGEventTapProxy,
type: CGEventType,
event: CGEvent
) -> Unmanaged<CGEvent>? {
let callback = eventTap.callback
return callback(eventTap, proxy, type, event)
}
private static func createTapMachPort(
location: Location,
place: CGEventTapPlacement,
options: CGEventTapOptions,
eventsOfInterest: CGEventMask,
callback: CGEventTapCallBack,
userInfo: UnsafeMutableRawPointer?
) -> CFMachPort? {
if case .pid(let pid) = location {
return CGEvent.tapCreateForPid(
pid: pid,
place: place,
options: options,
eventsOfInterest: eventsOfInterest,
callback: callback,
userInfo: userInfo
)
}
let tap: CGEventTapLocation? = switch location {
case .hidEventTap: .cghidEventTap
case .sessionEventTap: .cgSessionEventTap
case .annotatedSessionEventTap: .cgAnnotatedSessionEventTap
case .pid: nil
}
guard let tap else {
return nil
}
return CGEvent.tapCreate(
tap: tap,
place: place,
options: options,
eventsOfInterest: eventsOfInterest,
callback: callback,
userInfo: userInfo
)
}
private func withUnwrappedComponents(body: @MainActor (CFRunLoop, CFRunLoopSource, CFMachPort) -> Void) {
guard let runLoop else {
Logger.eventTap.error("Missing run loop for event tap \"\(self.label)\"")
return
}
guard let source else {
Logger.eventTap.error("Missing run loop source for event tap \"\(self.label)\"")
return
}
guard let machPort else {
Logger.eventTap.error("Missing mach port for event tap \"\(self.label)\"")
return
}
body(runLoop, source, machPort)
}
/// Enables the event tap.
func enable() {
withUnwrappedComponents { runLoop, source, machPort in
CFRunLoopAddSource(runLoop, source, mode)
CGEvent.tapEnable(tap: machPort, enable: true)
}
}
/// Enables the event tap with the given timeout.
func enable(timeout: Duration, onTimeout: @escaping () -> Void) {
enable()
Task { [weak self] in
try await Task.sleep(for: timeout)
if self?.isEnabled == true {
onTimeout()
}
}
}
/// Disables the event tap.
func disable() {
withUnwrappedComponents { runLoop, source, machPort in
CFRunLoopRemoveSource(runLoop, source, mode)
CGEvent.tapEnable(tap: machPort, enable: false)
}
}
}
// MARK: - Handle Event
private func handleEvent(
proxy: CGEventTapProxy,
type: CGEventType,
event: CGEvent,
refcon: UnsafeMutableRawPointer?
) -> Unmanaged<CGEvent>? {
guard let refcon else {
return Unmanaged.passRetained(event)
}
let eventTap = Unmanaged<EventTap>.fromOpaque(refcon).takeUnretainedValue()
return EventTap.performCallback(for: eventTap, proxy: proxy, type: type, event: event)
}
// MARK: - Logger
private extension Logger {
static let eventTap = Logger(category: "EventTap")
}
================================================
FILE: Ice/Hotkeys/Hotkey.swift
================================================
//
// Hotkey.swift
// Ice
//
import Combine
/// A combination of a key and modifiers that can be used to
/// trigger actions on system-wide key-up or key-down events.
final class Hotkey: ObservableObject {
private weak var appState: AppState?
private var listener: Listener?
let action: HotkeyAction
@Published var keyCombination: KeyCombination? {
didSet {
enable()
}
}
var isEnabled: Bool {
listener != nil
}
init(keyCombination: KeyCombination?, action: HotkeyAction) {
self.keyCombination = keyCombination
self.action = action
}
func assignAppState(_ appState: AppState) {
self.appState = appState
enable()
}
func enable() {
disable()
listener = Listener(hotkey: self, eventKind: .keyDown, appState: appState)
}
func disable() {
listener?.invalidate()
listener = nil
}
}
extension Hotkey {
/// An object that manges the lifetime of a hotkey observation.
private final class Listener {
private weak var appState: AppState?
private var id: UInt32?
var isValid: Bool {
id != nil
}
init?(hotkey: Hotkey, eventKind: HotkeyRegistry.EventKind, appState: AppState?) {
guard
let appState,
hotkey.keyCombination != nil
else {
return nil
}
let id = appState.hotkeyRegistry.register(
hotkey: hotkey,
eventKind: eventKind
) { [weak appState] in
guard let appState else {
return
}
Task {
await hotkey.action.perform(appState: appState)
}
}
guard let id else {
return nil
}
self.appState = appState
self.id = id
}
deinit {
invalidate()
}
func invalidate() {
guard isValid else {
return
}
guard let appState else {
Logger.hotkey.error("Error invalidating hotkey: Missing AppState")
return
}
defer {
id = nil
}
if let id {
appState.hotkeyRegistry.unregister(id)
}
}
}
}
// MARK: Hotkey: Codable
extension Hotkey: Codable {
private enum CodingKeys: CodingKey {
case keyCombination
case action
}
convenience init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try self.init(
keyCombination: container.decode(KeyCombination?.self, forKey: .keyCombination),
action: container.decode(HotkeyAction.self, forKey: .action)
)
}
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(keyCombination, forKey: .keyCombination)
try container.encode(action, forKey: .action)
}
}
// MARK: Hotkey: Equatable
extension Hotkey: Equatable {
static func == (lhs: Hotkey, rhs: Hotkey) -> Bool {
lhs.keyCombination == rhs.keyCombination &&
lhs.action == rhs.action
}
}
// MARK: Hotkey: Hashable
extension Hotkey: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(keyCombination)
hasher.combine(action)
}
}
// MARK: - Logger
private extension Logger {
static let hotkey = Logger(category: "Hotkey")
}
================================================
FILE: Ice/Hotkeys/HotkeyAction.swift
================================================
//
// HotkeyAction.swift
// Ice
//
enum HotkeyAction: String, Codable, CaseIterable {
// Menu Bar Sections
case toggleHiddenSection = "ToggleHiddenSection"
case toggleAlwaysHiddenSection = "ToggleAlwaysHiddenSection"
// Menu Bar Items
case searchMenuBarItems = "SearchMenuBarItems"
// Other
case enableIceBar = "EnableIceBar"
case showSectionDividers = "ShowSectionDividers"
case toggleApplicationMenus = "ToggleApplicationMenus"
@MainActor
func perform(appState: AppState) async {
switch self {
case .toggleHiddenSection:
guard let section = appState.menuBarManager.section(withName: .hidden) else {
return
}
section.toggle()
// Prevent the section from automatically rehiding after mouse movement.
if !section.isHidden {
appState.preventShowOnHover()
}
case .toggleAlwaysHiddenSection:
guard let section = appState.menuBarManager.section(withName: .alwaysHidden) else {
return
}
section.toggle()
// Prevent the section from automatically rehiding after mouse movement.
if !section.isHidden {
appState.preventShowOnHover()
}
case .searchMenuBarItems:
await appState.menuBarManager.searchPanel.toggle()
case .enableIceBar:
appState.settingsManager.generalSettingsManager.useIceBar.toggle()
case .showSectionDividers:
appState.settingsManager.advancedSettingsManager.showSectionDividers.toggle()
case .toggleApplicationMenus:
appState.menuBarManager.toggleApplicationMenus()
}
}
}
================================================
FILE: Ice/Hotkeys/HotkeyRegistry.swift
================================================
//
// HotkeyRegistry.swift
// Ice
//
import Carbon.HIToolbox
import Cocoa
import Combine
/// An object that manages the registration, storage, and unregistration of hotkeys.
final class HotkeyRegistry {
/// The event kinds that a hotkey can be registered for.
enum EventKind {
case keyUp
case keyDown
fileprivate init?(event: EventRef) {
switch Int(GetEventKind(event)) {
case kEventHotKeyPressed:
self = .keyDown
case kEventHotKeyReleased:
self = .keyUp
default:
return nil
}
}
}
/// An object that stores the information needed to cancel a registration.
private final class Registration {
let eventKind: EventKind
let key: KeyCode
let modifiers: Modifiers
let hotKeyID: EventHotKeyID
var hotKeyRef: EventHotKeyRef?
let handler: () -> Void
init(
eventKind: EventKind,
key: KeyCode,
modifiers: Modifiers,
hotKeyID: EventHotKeyID,
hotKeyRef: EventHotKeyRef,
handler: @escaping () -> Void
) {
self.eventKind = eventKind
self.key = key
self.modifiers = modifiers
self.hotKeyID = hotKeyID
self.hotKeyRef = hotKeyRef
self.handler = handler
}
}
private let signature = OSType(1231250720) // OSType for Ice
private var eventHandlerRef: EventHandlerRef?
private var registrations = [UInt32: Registration]()
private var cancellables = Set<AnyCancellable>()
/// Installs the global event handler reference, if it isn't already installed.
private func installIfNeeded() -> OSStatus {
guard eventHandlerRef == nil else {
return noErr
}
NotificationCenter.default
.publisher(for: NSMenu.didBeginTrackingNotification)
.sink { [weak self] _ in
self?.unregisterAndRetainAll()
}
.store(in: &cancellables)
NotificationCenter.default
.publisher(for: NSMenu.didEndTrackingNotification)
.sink { [weak self] _ in
self?.registerAllRetained()
}
.store(in: &cancellables)
let handler: EventHandlerUPP = { _, event, userData in
guard
let event,
let userData
else {
return OSStatus(eventNotHandledErr)
}
let registry = Unmanaged<HotkeyRegistry>.fromOpaque(userData).takeUnretainedValue()
return registry.performEventHandler(for: event)
}
let eventTypes: [EventTypeSpec] = [
EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)),
EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased)),
]
return InstallEventHandler(
GetEventDispatcherTarget(),
handler,
eventTypes.count,
eventTypes,
Unmanaged.passUnretained(self).toOpaque(),
&eventHandlerRef
)
}
/// Registers the given hotkey for the given event kind and returns the
/// identifier of the registration on success.
///
/// The returned identifier can be used to unregister the hotkey using
/// the ``unregister(_:)`` function.
///
/// - Parameters:
/// - hotkey: The hotkey to register the handler with.
/// - eventKind: The event kind to register the handler with.
/// - handler: The handler to perform when `hotkey` is triggered with
/// the event kind specified by `eventKind`.
///
/// - Returns: The registration's identifier on success, `nil` on failure.
func register(hotkey: Hotkey, eventKind: EventKind, handler: @escaping () -> Void) -> UInt32? {
enum Context {
static var currentID: UInt32 = 0
}
defer {
Context.currentID += 1
}
guard let keyCombination = hotkey.keyCombination else {
Logger.hotkeyRegistry.error("Hotkey does not have a valid key combination")
return nil
}
var status = installIfNeeded()
guard status == noErr else {
Logger.hotkeyRegistry.error("Hotkey event handler installation failed with status \(status)")
return nil
}
let id = Context.currentID
guard registrations[id] == nil else {
Logger.hotkeyRegistry.error("Hotkey already registered for id \(id)")
return nil
}
let hotKeyID = EventHotKeyID(signature: signature, id: id)
var hotKeyRef: EventHotKeyRef?
status = RegisterEventHotKey(
UInt32(keyCombination.key.rawValue),
UInt32(keyCombination.modifiers.carbonFlags),
hotKeyID,
GetEventDispatcherTarget(),
0,
&hotKeyRef
)
guard status == noErr else {
Logger.hotkeyRegistry.error("Hotkey registration failed with status \(status)")
return nil
}
guard let hotKeyRef else {
Logger.hotkeyRegistry.error("Hotkey registration failed due to invalid EventHotKeyRef")
return nil
}
let registration = Registration(
eventKind: eventKind,
key: keyCombination.key,
modifiers: keyCombination.modifiers,
hotKeyID: hotKeyID,
hotKeyRef: hotKeyRef,
handler: handler
)
registrations[id] = registration
return id
}
/// Unregisters the key combination with the given identifier, retaining
/// its registration in an inactive state.
private func retainedUnregister(_ id: UInt32) {
guard let registration = registrations[id] else {
Logger.hotkeyRegistry.error("No registered key combination for id \(id)")
return
}
let status = UnregisterEventHotKey(registration.hotKeyRef)
guard status == noErr else {
Logger.hotkeyRegistry.error("Hotkey unregistration failed with status \(status)")
return
}
registration.hotKeyRef = nil
}
/// Unregisters the key combination with the given identifier.
///
/// - Parameter id: An identifier returned from a call to the
/// ``register(hotkey:eventKind:handler:)`` function.
func unregister(_ id: UInt32) {
retainedUnregister(id)
registrations.removeValue(forKey: id)
}
/// Unregisters all key combinations, retaining their registrations
/// in an inactive state.
private func unregisterAndRetainAll() {
for (id, _) in registrations {
retainedUnregister(id)
}
}
/// Registers all registrations that were retained during a call to
/// ``retainedUnregister(_:)``
private func registerAllRetained() {
for registration in registrations.values {
guard registration.hotKeyRef == nil else {
continue
}
var hotKeyRef: EventHotKeyRef?
let status = RegisterEventHotKey(
UInt32(registration.key.rawValue),
UInt32(registration.modifiers.carbonFlags),
registration.hotKeyID,
GetEventDispatcherTarget(),
0,
&hotKeyRef
)
guard
status == noErr,
let hotKeyRef
else {
registrations.removeValue(forKey: registration.hotKeyID.id)
Logger.hotkeyRegistry.error("Hotkey registration failed with status \(status)")
continue
}
registration.hotKeyRef = hotKeyRef
}
}
/// Retrieves and performs the event handler stored under the
/// identifier for the specified event.
private func performEventHandler(for event: EventRef?) -> OSStatus {
guard let event else {
return OSStatus(eventNotHandledErr)
}
// create a hot key id from the event
var hotKeyID = EventHotKeyID()
let status = GetEventParameter(
event,
EventParamName(kEventParamDirectObject),
EventParamType(typeEventHotKeyID),
nil,
MemoryLayout<EventHotKeyID>.size,
nil,
&hotKeyID
)
// make sure creation was successful
guard status == noErr else {
return status
}
// make sure the event signature matches our signature and
// that an event handler is registered for the event
guard
hotKeyID.signature == signature,
let registration = registrations[hotKeyID.id],
registration.eventKind == EventKind(event: event)
else {
return OSStatus(eventNotHandledErr)
}
// all checks passed; perform the event handler
registration.handler()
return noErr
}
}
// MARK: - Logger
private extension Logger {
static let hotkeyRegistry = Logger(category: "HotkeyRegistry")
}
================================================
FILE: Ice/Hotkeys/KeyCode.swift
================================================
//
// KeyCode.swift
// Ice
//
import Carbon.HIToolbox
/// Representation of a physical key on a keyboard.
struct KeyCode: Codable, Hashable, RawRepresentable {
let rawValue: Int
// MARK: Letters
static let a = KeyCode(rawValue: kVK_ANSI_A)
static let b = KeyCode(rawValue: kVK_ANSI_B)
static let c = KeyCode(rawValue: kVK_ANSI_C)
static let d = KeyCode(rawValue: kVK_ANSI_D)
static let e = KeyCode(rawValue: kVK_ANSI_E)
static let f = KeyCode(rawValue: kVK_ANSI_F)
static let g = KeyCode(rawValue: kVK_ANSI_G)
static let h = KeyCode(rawValue: kVK_ANSI_H)
static let i = KeyCode(rawValue: kVK_ANSI_I)
static let j = KeyCode(rawValue: kVK_ANSI_J)
static let k = KeyCode(rawValue: kVK_ANSI_K)
static let l = KeyCode(rawValue: kVK_ANSI_L)
static let m = KeyCode(rawValue: kVK_ANSI_M)
static let n = KeyCode(rawValue: kVK_ANSI_N)
static let o = KeyCode(rawValue: kVK_ANSI_O)
static let p = KeyCode(rawValue: kVK_ANSI_P)
static let q = KeyCode(rawValue: kVK_ANSI_Q)
static let r = KeyCode(rawValue: kVK_ANSI_R)
static let s = KeyCode(rawValue: kVK_ANSI_S)
static let t = KeyCode(rawValue: kVK_ANSI_T)
static let u = KeyCode(rawValue: kVK_ANSI_U)
static let v = KeyCode(rawValue: kVK_ANSI_V)
static let w = KeyCode(rawValue: kVK_ANSI_W)
static let x = KeyCode(rawValue: kVK_ANSI_X)
static let y = KeyCode(rawValue: kVK_ANSI_Y)
static let z = KeyCode(rawValue: kVK_ANSI_Z)
// MARK: Numbers
static let zero = KeyCode(rawValue: kVK_ANSI_0)
static let one = KeyCode(rawValue: kVK_ANSI_1)
static let two = KeyCode(rawValue: kVK_ANSI_2)
static let three = KeyCode(rawValue: kVK_ANSI_3)
static let four = KeyCode(rawValue: kVK_ANSI_4)
static let five = KeyCode(rawValue: kVK_ANSI_5)
static let six = KeyCode(rawValue: kVK_ANSI_6)
static let seven = KeyCode(rawValue: kVK_ANSI_7)
static let eight = KeyCode(rawValue: kVK_ANSI_8)
static let nine = KeyCode(rawValue: kVK_ANSI_9)
// MARK: Symbols
static let equal = KeyCode(rawValue: kVK_ANSI_Equal)
static let minus = KeyCode(rawValue: kVK_ANSI_Minus)
static let rightBracket = KeyCode(rawValue: kVK_ANSI_RightBracket)
static let leftBracket = KeyCode(rawValue: kVK_ANSI_LeftBracket)
static let quote = KeyCode(rawValue: kVK_ANSI_Quote)
static let semicolon = KeyCode(rawValue: kVK_ANSI_Semicolon)
static let backslash = KeyCode(rawValue: kVK_ANSI_Backslash)
static let comma = KeyCode(rawValue: kVK_ANSI_Comma)
static let slash = KeyCode(rawValue: kVK_ANSI_Slash)
static let period = KeyCode(rawValue: kVK_ANSI_Period)
static let grave = KeyCode(rawValue: kVK_ANSI_Grave)
// MARK: Keypad
static let keypad0 = KeyCode(rawValue: kVK_ANSI_Keypad0)
static let keypad1 = KeyCode(rawValue: kVK_ANSI_Keypad1)
static let keypad2 = KeyCode(rawValue: kVK_ANSI_Keypad2)
static let keypad3 = KeyCode(rawValue: kVK_ANSI_Keypad3)
static let keypad4 = KeyCode(rawValue: kVK_ANSI_Keypad4)
static let keypad5 = KeyCode(rawValue: kVK_ANSI_Keypad5)
static let keypad6 = KeyCode(rawValue: kVK_ANSI_Keypad6)
static let keypad7 = KeyCode(rawValue: kVK_ANSI_Keypad7)
static let keypad8 = KeyCode(rawValue: kVK_ANSI_Keypad8)
static let keypad9 = KeyCode(rawValue: kVK_ANSI_Keypad9)
static let keypadDecimal = KeyCode(rawValue: kVK_ANSI_KeypadDecimal)
static let keypadMultiply = KeyCode(rawValue: kVK_ANSI_KeypadMultiply)
static let keypadPlus = KeyCode(rawValue: kVK_ANSI_KeypadPlus)
static let keypadClear = KeyCode(rawValue: kVK_ANSI_KeypadClear)
static let keypadDivide = KeyCode(rawValue: kVK_ANSI_KeypadDivide)
static let keypadEnter = KeyCode(rawValue: kVK_ANSI_KeypadEnter)
static let keypadMinus = KeyCode(rawValue: kVK_ANSI_KeypadMinus)
static let keypadEquals = KeyCode(rawValue: kVK_ANSI_KeypadEquals)
// MARK: Editing
static let space = KeyCode(rawValue: kVK_Space)
static let tab = KeyCode(rawValue: kVK_Tab)
static let `return` = KeyCode(rawValue: kVK_Return)
static let delete = KeyCode(rawValue: kVK_Delete)
static let forwardDelete = KeyCode(rawValue: kVK_ForwardDelete)
// MARK: Modifiers
static let control = KeyCode(rawValue: kVK_Control)
static let option = KeyCode(rawValue: kVK_Option)
static let shift = KeyCode(rawValue: kVK_Shift)
static let command = KeyCode(rawValue: kVK_Command)
static let rightControl = KeyCode(rawValue: kVK_RightControl)
static let rightOption = KeyCode(rawValue: kVK_RightOption)
static let rightShift = KeyCode(rawValue: kVK_RightShift)
static let rightCommand = KeyCode(rawValue: kVK_RightCommand)
static let capsLock = KeyCode(rawValue: kVK_CapsLock)
static let function = KeyCode(rawValue: kVK_Function)
// MARK: Function
static let f1 = KeyCode(rawValue: kVK_F1)
static let f2 = KeyCode(rawValue: kVK_F2)
static let f3 = KeyCode(rawValue: kVK_F3)
static let f4 = KeyCode(rawValue: kVK_F4)
static let f5 = KeyCode(rawValue: kVK_F5)
static let f6 = KeyCode(rawValue: kVK_F6)
static let f7 = KeyCode(rawValue: kVK_F7)
static let f8 = KeyCode(rawValue: kVK_F8)
static let f9 = KeyCode(rawValue: kVK_F9)
static let f10 = KeyCode(rawValue: kVK_F10)
static let f11 = KeyCode(rawValue: kVK_F11)
static let f12 = KeyCode(rawValue: kVK_F12)
static let f13 = KeyCode(rawValue: kVK_F13)
static let f14 = KeyCode(rawValue: kVK_F14)
static let f15 = KeyCode(rawValue: kVK_F15)
static let f16 = KeyCode(rawValue: kVK_F16)
static let f17 = KeyCode(rawValue: kVK_F17)
static let f18 = KeyCode(rawValue: kVK_F18)
static let f19 = KeyCode(rawValue: kVK_F19)
static let f20 = KeyCode(rawValue: kVK_F20)
// MARK: Navigation
static let pageUp = KeyCode(rawValue: kVK_PageUp)
static let pageDown = KeyCode(rawValue: kVK_PageDown)
static let home = KeyCode(rawValue: kVK_Home)
static let end = KeyCode(rawValue: kVK_End)
static let escape = KeyCode(rawValue: kVK_Escape)
static let help = KeyCode(rawValue: kVK_Help)
static let leftArrow = KeyCode(rawValue: kVK_LeftArrow)
static let rightArrow = KeyCode(rawValue: kVK_RightArrow)
static let downArrow = KeyCode(rawValue: kVK_DownArrow)
static let upArrow = KeyCode(rawValue: kVK_UpArrow)
// MARK: Media
static let volumeUp = KeyCode(rawValue: kVK_VolumeUp)
static let volumeDown = KeyCode(rawValue: kVK_VolumeDown)
static let mute = KeyCode(rawValue: kVK_Mute)
}
// MARK: Key Equivalent
extension KeyCode {
/// System representation.
var keyEquivalent: String {
guard
let inputSource = TISCopyCurrentASCIICapableKeyboardLayoutInputSource()?.takeRetainedValue(),
let layoutData = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData)
else {
return ""
}
let layoutBytes = CFDataGetBytePtr(unsafeBitCast(layoutData, to: CFData.self))
let layoutPtr = unsafeBitCast(layoutBytes, to: UnsafePointer<UCKeyboardLayout>.self)
let modifierKeyState: UInt32 = 0 // empty modifier key state
var deadKeyState: UInt32 = 0
let maxLength = 4
var actualLength = 0
var codeUnits = [UniChar](repeating: 0, count: maxLength)
let status = UCKeyTranslate(
layoutPtr,
UInt16(rawValue),
UInt16(kUCKeyActionDisplay),
modifierKeyState,
UInt32(LMGetKbdType()),
OptionBits(kUCKeyTranslateNoDeadKeysBit),
&deadKeyState,
maxLength,
&actualLength,
&codeUnits
)
guard status == noErr else {
return ""
}
return String(utf16CodeUnits: codeUnits, count: actualLength)
}
}
// MARK: Custom String Mappings
private let customStringMappings = [
// standard keys
KeyCode.space: "Space",
KeyCode.tab: "⇥",
KeyCode.return: "⏎",
KeyCode.delete: "⌫",
KeyCode.forwardDelete: "⌦",
KeyCode.f1: "F1",
KeyCode.f2: "F2",
KeyCode.f3: "F3",
KeyCode.f4: "F4",
KeyCode.f5: "F5",
KeyCode.f6: "F6",
KeyCode.f7: "F7",
KeyCode.f8: "F8",
KeyCode.f9: "F9",
KeyCode.f10: "F10",
KeyCode.f11: "F11",
KeyCode.f12: "F12",
KeyCode.f13: "F13",
KeyCode.f14: "F14",
KeyCode.f15: "F15",
KeyCode.f16: "F16",
KeyCode.f17: "F17",
KeyCode.f18: "F18",
KeyCode.f19: "F19",
KeyCode.f20: "F20",
KeyCode.pageUp: "⇞",
KeyCode.pageDown: "⇟",
KeyCode.home: "↖",
KeyCode.end: "↘",
KeyCode.escape: "⎋",
KeyCode.leftArrow: "←",
KeyCode.rightArrow: "→",
KeyCode.downArrow: "↓",
KeyCode.upArrow: "↑",
KeyCode.capsLock: "⇪",
KeyCode.control: "⌃",
KeyCode.option: "⌥",
KeyCode.shift: "⇧",
KeyCode.command: "⌘",
KeyCode.rightControl: "⌃",
KeyCode.rightOption: "⌥",
KeyCode.rightShift: "⇧",
KeyCode.rightCommand: "⌘",
KeyCode.keypadClear: "⌧",
KeyCode.keypadEnter: "⌤",
// media keys
KeyCode.volumeUp: "\u{1F50A}", // 'SPEAKER WITH THREE SOUND WAVES'
KeyCode.volumeDown: "\u{1F509}", // 'SPEAKER WITH ONE SOUND WAVE'
KeyCode.mute: "\u{1F507}", // 'SPEAKER WITH CANCELLATION STROKE'
// keypad keys
KeyCode.keypad0: "0⃣",
KeyCode.keypad1: "1⃣",
KeyCode.keypad2: "2⃣",
KeyCode.keypad3: "3⃣",
KeyCode.keypad4: "4⃣",
KeyCode.keypad5: "5⃣",
KeyCode.keypad6: "6⃣",
KeyCode.keypad7: "7⃣",
KeyCode.keypad8: "8⃣",
KeyCode.keypad9: "9⃣",
KeyCode.keypadDecimal: ".⃣",
KeyCode.keypadDivide: "/⃣",
KeyCode.keypadEquals: "=⃣",
KeyCode.keypadMinus: "-⃣",
KeyCode.keypadMultiply: "*⃣",
KeyCode.keypadPlus: "+⃣",
// other keys
KeyCode.function: "🌐︎︎",
KeyCode.help: "?⃝",
]
// MARK: String Value
extension KeyCode {
/// Custom string representation.
var stringValue: String {
customStringMappings[self, default: keyEquivalent]
}
}
================================================
FILE: Ice/Hotkeys/KeyCombination.swift
================================================
//
// KeyCombination.swift
// Ice
//
import Carbon.HIToolbox
import Cocoa
struct KeyCombination: Hashable {
let key: KeyCode
let modifiers: Modifiers
var stringValue: String {
modifiers.symbolicValue + key.stringValue
}
init(key: KeyCode, modifiers: Modifiers) {
self.key = key
self.modifiers = modifiers
}
init(event: NSEvent) {
let key = KeyCode(rawValue: Int(event.keyCode))
let modifiers = Modifiers(nsEventFlags: event.modifierFlags)
self.init(key: key, modifiers: modifiers)
}
}
private func getSystemReservedKeyCombinations() -> [KeyCombination] {
var symbolicHotkeys: Unmanaged<CFArray>?
let status = CopySymbolicHotKeys(&symbolicHotkeys)
guard status == noErr else {
Logger.keyCombination.error("CopySymbolicHotKeys returned invalid status: \(status)")
return []
}
guard let reservedHotkeys = symbolicHotkeys?.takeRetainedValue() as? [[String: Any]] else {
Logger.keyCombination.error("Failed to serialize symbolic hotkeys")
return []
}
return reservedHotkeys.compactMap { hotkey in
guard
hotkey[kHISymbolicHotKeyEnabled] as? Bool == true,
let keyCode = hotkey[kHISymbolicHotKeyCode] as? Int,
let modifiers = hotkey[kHISymbolicHotKeyModifiers] as? Int
else {
return nil
}
return KeyCombination(
key: KeyCode(rawValue: keyCode),
modifiers: Modifiers(carbonFlags: modifiers)
)
}
}
extension KeyCombination {
/// Returns a Boolean value that indicates whether this key
/// combination is reserved for system use.
var isReservedBySystem: Bool {
getSystemReservedKeyCombinations().contains(self)
}
}
extension KeyCombination: Codable {
init(from decoder: any Decoder) throws {
var container = try decoder.unkeyedContainer()
guard container.count == 2 else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Expected 2 encoded values, found \(container.count ?? 0)"
)
)
}
self.key = try KeyCode(rawValue: container.decode(Int.self))
self.modifiers = try Modifiers(rawValue: container.decode(Int.self))
}
func encode(to encoder: any Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(key.rawValue)
try container.encode(modifiers.rawValue)
}
}
// MARK: - Logger
private extension Logger {
static let keyCombination = Logger(category: "KeyCombination")
}
================================================
FILE: Ice/Hotkeys/Modifiers.swift
================================================
//
// Modifiers.swift
// Ice
//
import Carbon.HIToolbox
import Cocoa
/// A bit mask containing the modifier keys for a hotkey.
struct Modifiers: OptionSet, Codable, Hashable {
let rawValue: Int
static let control = Modifiers(rawValue: 1 << 0)
static let option = Modifiers(rawValue: 1 << 1)
static let shift = Modifiers(rawValue: 1 << 2)
static let command = Modifiers(rawValue: 1 << 3)
}
extension Modifiers {
/// All modifiers in the order displayed by the system,
/// according to Apple's style guide.
static let canonicalOrder = [control, option, shift, command]
/// A symbolic string representation of the modifiers.
var symbolicValue: String {
var result = ""
if contains(.control) {
result.append("⌃")
}
if contains(.option) {
result.append("⌥")
}
if contains(.shift) {
result.append("⇧")
}
if contains(.command) {
result.append("⌘")
}
return result
}
/// A string representation of the modifiers that is
/// suitable for display in a label.
var labelValue: String {
var result = [String]()
if contains(.control) {
result.append("Control")
}
if contains(.option) {
result.append("Option")
}
if contains(.shift) {
result.append("Shift")
}
if contains(.command) {
result.append("Command")
}
return result.joined(separator: " + ")
}
/// A combined string representation of the modifiers
/// that is suitable for display.
var combinedValue: String {
"\(labelValue) (\(symbolicValue))"
}
/// Cocoa flags.
var nsEventFlags: NSEvent.ModifierFlags {
var result: NSEvent.ModifierFlags = []
if contains(.control) {
result.insert(.control)
}
if contains(.option) {
result.insert(.option)
}
if contains(.shift) {
result.insert(.shift)
}
if contains(.command) {
result.insert(.command)
}
return result
}
/// CoreGraphics flags.
var cgEventFlags: CGEventFlags {
var result: CGEventFlags = []
if contains(.control) {
result.insert(.maskControl)
}
if contains(.option) {
result.insert(.maskAlternate)
}
if contains(.shift) {
result.insert(.maskShift)
}
if contains(.command) {
result.insert(.maskCommand)
}
return result
}
/// Raw Carbon flags.
var carbonFlags: Int {
var result = 0
if contains(.control) {
result |= controlKey
}
if contains(.option) {
result |= optionKey
}
if contains(.shift) {
result |= shiftKey
}
if contains(.command) {
result |= cmdKey
}
return result
}
init(nsEventFlags: NSEvent.ModifierFlags) {
self.init()
if nsEventFlags.contains(.control) {
insert(.control)
}
if nsEventFlags.contains(.option) {
insert(.option)
}
if nsEventFlags.contains(.shift) {
insert(.shift)
}
if nsEventFlags.contains(.command) {
insert(.command)
}
}
init(cgEventFlags: CGEventFlags) {
self.init()
if cgEventFlags.contains(.maskControl) {
insert(.control)
}
if cgEventFlags.contains(.maskAlternate) {
insert(.option)
}
if cgEventFlags.contains(.maskShift) {
insert(.shift)
}
if cgEventFlags.contains(.maskCommand) {
insert(.command)
}
}
init(carbonFlags: Int) {
self.init()
if carbonFlags & controlKey == controlKey {
insert(.control)
}
if carbonFlags & optionKey == optionKey {
insert(.option)
}
if carbonFlags & shiftKey == shiftKey {
insert(.shift)
}
if carbonFlags & cmdKey == cmdKey {
insert(.command)
}
}
}
================================================
FILE: Ice/Ice.entitlements
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>
================================================
FILE: Ice/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SUFeedURL</key>
<string>https://jordanbaird.github.io/ice-releases/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>3nfIGMOD8DALPE8vIdFo2tUOIVc2MVbzhc+2J9JLn+Q=</string>
</dict>
</plist>
================================================
FILE: Ice/Main/AppDelegate.swift
================================================
//
// AppDelegate.swift
// Ice
//
import SwiftUI
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
private weak var appState: AppState?
// MARK: NSApplicationDelegate Methods
func applicationWillFinishLaunching(_ notification: Notification) {
guard let appState else {
Logger.appDelegate.warning("Missing app state in applicationWillFinishLaunching")
return
}
// Assign the delegate to the shared app state.
appState.assignAppDelegate(self)
// Allow the app to set the cursor in the background.
appState.setsCursorInBackground = true
}
func applicationDidFinishLaunching(_ notification: Notification) {
guard let appState else {
Logger.appDelegate.warning("Missing app state in applicationDidFinishLaunching")
return
}
// Dismiss the windows.
appState.dismissSettingsWindow()
appState.dismissPermissionsWindow()
// Hide the main menu to make more space in the menu bar.
if let mainMenu = NSApp.mainMenu {
for item in mainMenu.items {
item.isHidden = true
}
}
// Perform setup after a small delay to ensure that the settings window
// has been assigned.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
guard !appState.isPreview else {
return
}
// If we have the required permissions, set up the shared app state.
// Otherwise, open the permissions window.
switch appState.permissionsManager.permissionsState {
case .hasAllPermissions, .hasRequiredPermissions:
appState.performSetup()
case .missingPermissions:
appState.activate(withPolicy: .regular)
appState.openPermissionsWindow()
}
}
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
// Deactivate and set the policy to accessory when all windows are closed.
appState?.deactivate(withPolicy: .accessory)
return false
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
// MARK: Other Methods
/// Assigns the app state to the delegate.
func assignAppState(_ appState: AppState) {
guard self.appState == nil else {
Logger.appDelegate.warning("Multiple attempts made to assign app state")
return
}
self.appState = appState
}
/// Opens the settings window and activates the app.
@objc func openSettingsWindow() {
guard let appState else {
Logger.appDelegate.error("Failed to open settings window")
return
}
// Small delay makes this more reliable.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
appState.activate(withPolicy: .regular)
appState.openSettingsWindow()
}
}
}
// MARK: - Logger
private extension Logger {
static let appDelegate = Logger(category: "AppDelegate")
}
================================================
FILE: Ice/Main/AppState.swift
================================================
//
// AppState.swift
// Ice
//
import Combine
import SwiftUI
/// The model for app-wide state.
@MainActor
final class AppState: ObservableObject {
/// A Boolean value that indicates whether the active space is fullscreen.
@Published private(set) var isActiveSpaceFullscreen = Bridging.isSpaceFullscreen(Bridging.activeSpaceID)
/// Manager for the menu bar's appearance.
private(set) lazy var appearanceManager = MenuBarAppearanceManager(appState: self)
/// Manager for events received by the app.
private(set) lazy var eventManager = EventManager(appState: self)
/// Manager for menu bar items.
private(set) lazy var itemManager = MenuBarItemManager(appState: self)
/// Manager for the state of the menu bar.
private(set) lazy var menuBarManager = MenuBarManager(appState: self)
/// Manager for app permissions.
private(set) lazy var permissionsManager = PermissionsManager(appState: self)
/// Manager for the app's settings.
private(set) lazy var settingsManager = SettingsManager(appState: self)
/// Manager for app updates.
private(set) lazy var updatesManager = UpdatesManager(appState: self)
/// Manager for user notifications.
private(set) lazy var userNotificationManager = UserNotificationManager(appState: self)
/// Global cache for menu bar item images.
private(set) lazy var imageCache = MenuBarItemImageCache(appState: self)
/// Manager for menu bar item spacing.
let spacingManager = MenuBarItemSpacingManager()
/// Model for app-wide navigation.
let navigationState = AppNavigationState()
/// The app's hotkey registry.
nonisolated let hotkeyRegistry = HotkeyRegistry()
/// The app's delegate.
private(set) weak var appDelegate: AppDelegate?
/// The window that contains the settings interface.
private(set) weak var settingsWindow: NSWindow?
/// The window that contains the permissions interface.
private(set) weak var permissionsWindow: NSWindow?
/// A Boolean value that indicates whether the "ShowOnHover" feature is prevented.
private(set) var isShowOnHoverPrevented = false
/// Storage for internal observers.
private var cancellables = Set<AnyCancellable>()
/// A Boolean value that indicates whether the app is running as a SwiftUI preview.
let isPreview: Bool = {
#if DEBUG
let environment = ProcessInfo.processInfo.environment
let key = "XCODE_RUNNING_FOR_PREVIEWS"
return environment[key] != nil
#else
return false
#endif
}()
/// A Boolean value that indicates whether the application can set the cursor
/// in the background.
var setsCursorInBackground: Bool {
get { Bridging.getConnectionProperty(forKey: "SetsCursorInBackground") as? Bool ?? false }
set { Bridging.setConnectionProperty(newValue, forKey: "SetsCursorInBackground") }
}
/// Configures the internal observers for the app state.
private func configureCancellables() {
var c = Set<AnyCancellable>()
Publishers.Merge3(
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.activeSpaceDidChangeNotification)
.mapToVoid(),
// Frontmost application change can indicate a space change from one display to
// another, which gets ignored by NSWorkspace.activeSpaceDidChangeNotification.
NSWorkspace.shared
.publisher(for: \.frontmostApplication)
.mapToVoid(),
// Clicking into a fullscreen space from another space is also ignored.
UniversalEventMonitor
.publisher(for: .leftMouseDown)
.delay(for: 0.1, scheduler: DispatchQueue.main)
.mapToVoid()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self else {
return
}
isActiveSpaceFullscreen = Bridging.isSpaceFullscreen(Bridging.activeSpaceID)
}
.store(in: &c)
NSWorkspace.shared.publisher(for: \.frontmostApplication)
.receive(on: DispatchQueue.main)
.sink { [weak self] frontmostApplication in
guard let self else {
return
}
navigationState.isAppFrontmost = frontmostApplication == .current
}
.store(in: &c)
if let settingsWindow {
settingsWindow.publisher(for: \.isVisible)
.debounce(for: 0.05, scheduler: DispatchQueue.main)
.sink { [weak self] isVisible in
guard let self else {
return
}
navigationState.isSettingsPresented = isVisible
}
.store(in: &c)
} else {
Logger.appState.warning("No settings window!")
}
Publishers.Merge(
navigationState.$isAppFrontmost,
navigationState.$isSettingsPresented
)
.debounce(for: 0.1, scheduler: DispatchQueue.main)
.sink { [weak self] shouldUpdate in
guard
let self,
shouldUpdate
else {
return
}
Task.detached {
if ScreenCapture.cachedCheckPermissions(reset: true) {
await self.imageCache.updateCacheWithoutChecks(sections: MenuBarSection.Name.allCases)
}
}
}
.store(in: &c)
menuBarManager.objectWillChange
.sink { [weak self] in
self?.objectWillChange.send()
}
.store(in: &c)
permissionsManager.objectWillChange
.sink { [weak self] in
self?.objectWillChange.send()
}
.store(in: &c)
settingsManager.objectWillChange
.sink { [weak self] in
self?.objectWillChange.send()
}
.store(in: &c)
updatesManager.objectWillChange
.sink { [weak self] in
self?.objectWillChange.send()
}
.store(in: &c)
cancellables = c
}
/// Sets up the app state.
func performSetup() {
configureCancellables()
permissionsManager.stopAllChecks()
menuBarManager.performSetup()
appearanceManager.performSetup()
eventManager.performSetup()
settingsManager.performSetup()
itemManager.performSetup()
imageCache.performSetup()
updatesManager.performSetup()
userNotificationManager.performSetup()
}
/// Assigns the app delegate to the app state.
func assignAppDelegate(_ appDelegate: AppDelegate) {
guard self.appDelegate == nil else {
Logger.appState.warning("Multiple attempts made to assign app delegate")
return
}
self.appDelegate = appDelegate
}
/// Assigns the settings window to the app state.
func assignSettingsWindow(_ window: NSWindow) {
guard window.identifier?.rawValue == Constants.settingsWindowID else {
Logger.appState.warning("Window \(window.identifier?.rawValue ?? "<NIL>") is not the settings window!")
return
}
settingsWindow = window
configureCancellables()
}
/// Assigns the permissions window to the app state.
func assignPermissionsWindow(_ window: NSWindow) {
guard window.identifier?.rawValue == Constants.permissionsWindowID else {
Logger.appState.warning("Window \(window.identifier?.rawValue ?? "<NIL>") is not the permissions window!")
return
}
permissionsWindow = window
configureCancellables()
}
/// Opens the settings window.
func openSettingsWindow() {
with(EnvironmentValues()) { environment in
environment.openWindow(id: Constants.settingsWindowID)
}
}
/// Dismisses the settings window.
func dismissSettingsWindow() {
with(EnvironmentValues()) { environment in
environment.dismissWindow(id: Constants.settingsWindowID)
}
}
/// Opens the permissions window.
func openPermissionsWindow() {
with(EnvironmentValues()) { environment in
environment.openWindow(id: Constants.permissionsWindowID)
}
}
/// Dismisses the permissions window.
func dismissPermissionsWindow() {
with(EnvironmentValues()) { environment in
environment.dismissWindow(id: Constants.permissionsWindowID)
}
}
/// Activates the app and sets its activation policy to the given value.
func activate(withPolicy policy: NSApplication.ActivationPolicy) {
// Store whether the app has previously activated inside an internal
// context to keep it isolated.
enum Context {
static let hasActivated = ObjectStorage<Bool>()
}
func activate() {
if let frontApp = NSWorkspace.shared.frontmostApplication {
NSRunningApplication.current.activate(from: frontApp)
} else {
NSApp.activate()
}
NSApp.setActivationPolicy(policy)
}
if Context.hasActivated.value(for: self) == true {
activate()
} else {
Context.hasActivated.set(true, for: self)
Logger.appState.debug("First time activating app, so going through Dock")
// Hack to make sure the app properly activates for the first time.
NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.dock").first?.activate()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
activate()
}
}
}
/// Deactivates the app and sets its activation policy to the given value.
func deactivate(withPolicy policy: NSApplication.ActivationPolicy) {
if let nextApp = NSWorkspace.shared.runningApplications.first(where: { $0 != .current }) {
NSApp.yieldActivation(to: nextApp)
} else {
NSApp.deactivate()
}
NSApp.setActivationPolicy(policy)
}
/// Prevents the "ShowOnHover" feature.
func preventShowOnHover() {
isShowOnHoverPrevented = true
}
/// Allows the "ShowOnHover" feature.
func allowShowOnHover() {
isShowOnHoverPrevented = false
}
}
// MARK: AppState: BindingExposable
extension AppState: BindingExposable { }
// MARK: - Logger
private extension Logger {
/// The logger to use for the app state.
static let appState = Logger(category: "AppState")
}
================================================
FILE: Ice/Main/IceApp.swift
================================================
//
// IceApp.swift
// Ice
//
import SwiftUI
@main
struct IceApp: App {
@NSApplicationDelegateAdaptor var appDelegate: AppDelegate
@ObservedObject var appState = AppState()
init() {
NSSplitViewItem.swizzle()
MigrationManager.migrateAll(appState: appState)
appDelegate.assignAppState(appState)
}
var body: some Scene {
SettingsWindow(appState: appState)
PermissionsWindow(appState: appState)
}
}
================================================
FILE: Ice/Main/Navigation/AppNavigationState.swift
================================================
//
// AppNavigationState.swift
// Ice
//
import Combine
/// The model for app-wide navigation.
@MainActor
final class AppNavigationState: ObservableObject {
@Published var isAppFrontmost = false
@Published var isSettingsPresented = false
@Published var isIceBarPresented = false
@Published var isSearchPresented = false
@Published var settingsNavigationIdentifier: SettingsNavigationIdentifier = .general
}
================================================
FILE: Ice/Main/Navigation/NavigationIdentifiers/NavigationIdentifier.swift
================================================
//
// NavigationIdentifier.swift
// Ice
//
import SwiftUI
/// A type that represents an identifier used for navigation in a user interface.
protocol NavigationIdentifier: CaseIterable, Hashable, Identifiable, RawRepresentable {
/// A localized description of the identifier that can be presented to the user.
var localized: LocalizedStringKey { get }
}
extension NavigationIdentifier where ID == Int {
var id: Int { hashValue }
}
extension NavigationIdentifier where RawValue == String {
var localized: LocalizedStringKey { LocalizedStringKey(rawValue) }
}
================================================
FILE: Ice/Main/Navigation/NavigationIdentifiers/SettingsNavigationIdentifier.swift
================================================
//
// SettingsNavigationIdentifier.swift
// Ice
//
/// An identifier used for navigation in the settings interface.
enum SettingsNavigationIdentifier: String, NavigationIdentifier {
case general = "General"
case menuBarLayout = "Menu Bar Layout"
case menuBarAppearance = "Menu Bar Appearance"
case hotkeys = "Hotkeys"
case advanced = "Advanced"
case about = "About"
}
================================================
FILE: Ice/MenuBar/Appearance/Configurations/MenuBarAppearanceConfigurationV1.swift
================================================
//
// MenuBarAppearanceConfigurationV1.swift
// Ice
//
import CoreGraphics
import Foundation
/// Configuration for the menu bar's appearance.
struct MenuBarAppearanceConfigurationV1: Hashable {
var hasShadow: Bool
var hasBorder: Bool
var isInset: Bool
var borderColor: CGColor
var borderWidth: Double
var shapeKind: MenuBarShapeKind
var fullShapeInfo: MenuBarFullShapeInfo
var splitShapeInfo: MenuBarSplitShapeInfo
var tintKind: MenuBarTintKind
var tintColor: CGColor
var tintGradient: CustomGradient
var hasRoundedShape: Bool {
switch shapeKind {
case .none: false
case .full: fullShapeInfo.hasRoundedShape
case .split: splitShapeInfo.hasRoundedShape
}
}
/// Creates a configuration by migrating from the deprecated appearance-related
/// keys stored in `UserDefaults`, storing the new configuration and deleting
/// the deprecated keys.
static func migrate(encoder: JSONEncoder, decoder: JSONDecoder) throws -> Self {
// Try to load an already migrated configuration first. Otherwise, load each
// value from the deprecated keys.
if let data = Defaults.data(forKey: .menuBarAppearanceConfiguration) {
return try decoder.decode(Self.self, from: data)
} else {
var configuration = Self.defaultConfiguration
Defaults.ifPresent(key: .menuBarHasShadow, assign: &configuration.hasShadow)
Defaults.ifPresent(key: .menuBarHasBorder, assign: &configuration.hasBorder)
Defaults.ifPresent(key: .menuBarBorderWidth, assign: &configuration.borderWidth)
Defaults.ifPresent(key: .menuBarTintKind) { rawValue in
if let tintKind = MenuBarTintKind(rawValue: rawValue) {
configuration.tintKind = tintKind
}
}
if let borderColorData = Defaults.data(forKey: .menuBarBorderColor) {
configuration.borderColor = try decoder.decode(CodableColor.self, from: borderColorData).cgColor
}
if let tintColorData = Defaults.data(forKey: .menuBarTintColor) {
configuration.tintColor = try decoder.decode(CodableColor.self, from: tintColorData).cgColor
}
if let tintGradientData = Defaults.data(forKey: .menuBarTintGradient) {
configuration.tintGradient = try decoder.decode(CustomGradient.self, from: tintGradientData)
}
if let shapeKindData = Defaults.data(forKey: .menuBarShapeKind) {
configuration.shapeKind = try decoder.decode(MenuBarShapeKind.self, from: shapeKindData)
}
if let fullShapeData = Defaults.data(forKey: .menuBarFullShapeInfo) {
configuration.fullShapeInfo = try decoder.decode(MenuBarFullShapeInfo.self, from: fullShapeData)
}
if let splitShapeData = Defaults.data(forKey: .menuBarSplitShapeInfo) {
configuration.splitShapeInfo = try decoder.decode(MenuBarSplitShapeInfo.self, from: splitShapeData)
}
// Store the configuration to complete the migration.
let configurationData = try encoder.encode(configuration)
Defaults.set(configurationData, forKey: .menuBarAppearanceConfiguration)
// Remove the deprecated keys.
let keys: [Defaults.Key] = [
.menuBarHasShadow,
.menuBarHasBorder,
.menuBarBorderWidth,
.menuBarTintKind,
.menuBarBorderColor,
.menuBarTintColor,
.menuBarTintGradient,
.menuBarShapeKind,
.menuBarFullShapeInfo,
.menuBarSplitShapeInfo,
]
for key in keys {
Defaults.removeObject(forKey: key)
}
return configuration
}
}
}
// MARK: Default Configuration
extension MenuBarAppearanceConfigurationV1 {
static let defaultConfiguration = MenuBarAppearanceConfigurationV1(
hasShadow: false,
hasBorder: false,
isInset: true,
borderColor: .black,
borderWidth: 1,
shapeKind: .none,
fullShapeInfo: .default,
splitShapeInfo: .default,
tintKind: .none,
tintColor: .black,
tintGradient: .defaultMenuBarTint
)
}
// MARK: MenuBarAppearanceConfigurationV1: Codable
extension MenuBarAppearanceConfigurationV1: Codable {
private enum CodingKeys: CodingKey {
case hasShadow
case hasBorder
case isInset
case borderColor
case borderWidth
case shapeKind
case fullShapeInfo
case splitShapeInfo
case tintKind
case tintColor
case tintGradient
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try self.init(
hasShadow: container.decodeIfPresent(Bool.self, forKey: .hasShadow) ?? Self.defaultConfiguration.hasShadow,
hasBorder: container.decodeIfPresent(Bool.self, forKey: .hasBorder) ?? Self.defaultConfiguration.hasBorder,
isInset: container.decodeIfPresent(Bool.self, forKey: .isInset) ?? Self.defaultConfiguration.isInset,
borderColor: container.decodeIfPresent(CodableColor.self, forKey: .borderColor)?.cgColor ?? Self.defaultConfiguration.borderColor,
borderWidth: container.decodeIfPresent(Double.self, forKey: .borderWidth) ?? Self.defaultConfiguration.borderWidth,
shapeKind: container.decodeIfPresent(MenuBarShapeKind.self, forKey: .shapeKind) ?? Self.defaultConfiguration.shapeKind,
fullShapeInfo: container.decodeIfPresent(MenuBarFullShapeInfo.self, forKey: .fullShapeInfo) ?? Self.defaultConfiguration.fullShapeInfo,
splitShapeInfo: container.decodeIfPresent(MenuBarSplitShapeInfo.self, forKey: .splitShapeInfo) ?? Self.defaultConfiguration.splitShapeInfo,
tintKind: container.decodeIfPresent(MenuBarTintKind.self, forKey: .tintKind) ?? Self.defaultConfiguration.tintKind,
tintColor: container.decodeIfPresent(CodableColor.self, forKey: .tintColor)?.cgColor ?? Self.defaultConfiguration.tintColor,
tintGradient: container.decodeIfPresent(CustomGradient.self, forKey: .tintGradient) ?? Self.defaultConfiguration.tintGradient
)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(hasShadow, forKey: .hasShadow)
try container.encode(hasBorder, forKey: .hasBorder)
try container.encode(isInset, forKey: .isInset)
try container.encode(CodableColor(cgColor: borderColor), forKey: .borderColor)
try container.encode(borderWidth, forKey: .borderWidth)
try container.encode(shapeKind, forKey: .shapeKind)
try container.encode(fullShapeInfo, forKey: .fullShapeInfo)
try container.encode(splitShapeInfo, forKey: .splitShapeInfo)
try container.encode(tintKind, forKey: .tintKind)
try container.encode(CodableColor(cgColor: tintColor), forKey: .tintColor)
try container.encode(tintGradient, forKey: .tintGradient)
}
}
================================================
FILE: Ice/MenuBar/Appearance/Configurations/MenuBarAppearanceConfigurationV2.swift
================================================
//
// MenuBarAppearanceConfigurationV2.swift
// Ice
//
import CoreGraphics
import Foundation
struct MenuBarAppearanceConfigurationV2: Hashable {
var lightModeConfiguration: MenuBarAppearancePartialConfiguration
var darkModeConfiguration: MenuBarAppearancePartialConfiguration
var staticConfiguration: MenuBarAppearancePartialConfiguration
var shapeKind: MenuBarShapeKind
var fullShapeInfo: MenuBarFullShapeInfo
var splitShapeInfo: MenuBarSplitShapeInfo
var isInset: Bool
var isDynamic: Bool
var hasRoundedShape: Bool {
switch shapeKind {
case .none: false
case .full: fullShapeInfo.hasRoundedShape
case .split: splitShapeInfo.hasRoundedShape
}
}
var current: MenuBarAppearancePartialConfiguration {
if isDynamic {
switch SystemAppearance.current {
case .light: lightModeConfiguration
case .dark: darkModeConfiguration
}
} else {
staticConfiguration
}
}
}
// MARK: Default Configuration
extension MenuBarAppearanceConfigurationV2 {
static let defaultConfiguration = MenuBarAppearanceConfigurationV2(
lightModeConfiguration: .defaultConfiguration,
darkModeConfiguration: .defaultConfiguration,
staticConfiguration: .defaultConfiguration,
shapeKind: .none,
fullShapeInfo: .default,
splitShapeInfo: .default,
isInset: true,
isDynamic: false
)
}
extension MenuBarAppearanceConfigurationV2: Codable {
private enum CodingKeys: CodingKey {
case lightModeConfiguration
case darkModeConfiguration
case staticConfiguration
case shapeKind
case fullShapeInfo
case splitShapeInfo
case isInset
case isDynamic
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try self.init(
lightModeConfiguration: container.decodeIfPresent(MenuBarAppearancePartialConfiguration.self, forKey: .lightModeConfiguration) ?? Self.defaultConfiguration.lightModeConfiguration,
darkModeConfiguration: container.decodeIfPresent(MenuBarAppearancePartialConfiguration.self, forKey: .darkModeConfiguration) ?? Self.defaultConfiguration.darkModeConfiguration,
staticConfiguration: container.decodeIfPresent(MenuBarAppearancePartialConfiguration.self, forKey: .staticConfiguration) ?? Self.defaultConfiguration.staticConfiguration,
shapeKind: container.decodeIfPresent(MenuBarShapeKind.self, forKey: .shapeKind) ?? Self.defaultConfiguration.shapeKind,
fullShapeInfo: container.decodeIfPresent(MenuBarFullShapeInfo.self, forKey: .fullShapeInfo) ?? Self.defaultConfiguration.fullShapeInfo,
splitShapeInfo: container.decodeIfPresent(MenuBarSplitShapeInfo.self, forKey: .splitShapeInfo) ?? Self.defaultConfiguration.splitShapeInfo,
isInset: container.decodeIfPresent(Bool.self, forKey: .isInset) ?? Self.defaultConfiguration.isInset,
isDynamic: container.decodeIfPresent(Bool.self, forKey: .isDynamic) ?? Self.defaultConfiguration.isDynamic
)
}
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(lightModeConfiguration, forKey: .lightModeConfiguration)
try container.encode(darkModeConfiguration, forKey: .darkModeConfiguration)
try container.encode(staticConfiguration, forKey: .staticConfiguration)
try container.encode(shapeKind, forKey: .shapeKind)
try container.encode(fullShapeInfo, forKey: .fullShapeInfo)
try container.encode(splitShapeInfo, forKey: .splitShapeInfo)
try container.encode(isInset, forKey: .isInset)
try container.encode(isDynamic, forKey: .isDynamic)
}
}
// MARK: - MenuBarAppearancePartialConfiguration
struct MenuBarAppearancePartialConfiguration: Hashable {
var hasShadow: Bool
var hasBorder: Bool
var borderColor: CGColor
var borderWidth: Double
var tintKind: MenuBarTintKind
var tintColor: CGColor
var tintGradient: CustomGradient
}
// MARK: Default Partial Configuration
extension MenuBarAppearancePartialConfiguration {
static let defaultConfiguration = MenuBarAppearancePartialConfiguration(
hasShadow: false,
hasBorder: false,
borderColor: .black,
borderWidth: 1,
tintKind: .none,
tintColor: .black,
tintGradient: .defaultMenuBarTint
)
}
// MARK: MenuBarAppearancePartialConfiguration: Codable
extension MenuBarAppearancePartialConfiguration: Codable {
private enum CodingKeys: CodingKey {
case hasShadow
case hasBorder
case borderColor
case borderWidth
case shapeKind
case fullShapeInfo
case splitShapeInfo
case tintKind
case tintColor
case tintGradient
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try self.init(
hasShadow: container.decodeIfPresent(Bool.self, forKey: .hasShadow) ?? Self.defaultConfiguration.hasShadow,
hasBorder: container.decodeIfPresent(Bool.self, forKey: .hasBorder) ?? Self.defaultConfiguration.hasBorder,
borderColor: container.decodeIfPresent(CodableColor.self, forKey: .borderColor)?.cgColor ?? Self.defaultConfiguration.borderColor,
borderWidth: container.decodeIfPresent(Double.self, forKey: .borderWidth) ?? Self.defaultConfiguration.borderWidth,
tintKind: container.decodeIfPresent(MenuBarTintKind.self, forKey: .tintKind) ?? Self.defaultConfiguration.tintKind,
tintColor: container.decodeIfPresent(CodableColor.self, forKey: .tintColor)?.cgColor ?? Self.defaultConfiguration.tintColor,
tintGradient: container.decodeIfPresent(CustomGradient.self, forKey: .tintGradient) ?? Self.defaultConfiguration.tintGradient
)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(hasShadow, forKey: .hasShadow)
try container.encode(hasBorder, forKey: .hasBorder)
try container.encode(CodableColor(cgColor: borderColor), forKey: .borderColor)
try container.encode(borderWidth, forKey: .borderWidth)
try container.encode(tintKind, forKey: .tintKind)
try container.encode(CodableColor(cgColor: tintColor), forKey: .tintColor)
try container.encode(tintGradient, forKey: .tintGradient)
}
}
================================================
FILE: Ice/MenuBar/Appearance/MenuBarAppearanceEditor/MenuBarAppearanceEditor.swift
================================================
//
// MenuBarAppearanceEditor.swift
// Ice
//
import SwiftUI
struct MenuBarAppearanceEditor: View {
enum Location {
case settings
case popover(closePopover: () -> Void)
}
@EnvironmentObject var appState: AppState
@EnvironmentObject var appearanceManager: MenuBarAppearanceManager
let location: Location
private var mainFormPadding: EdgeInsets {
with(EdgeInsets(all: 20)) { insets in
switch location {
case .settings: break
case .popover: insets.top = 0
}
}
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
stackHeader
stackBody
}
}
@ViewBuilder
private var stackHeader: some View {
if case .popover(let closePopover) = location {
ZStack {
Text("Menu Bar Appearance")
.font(.title2)
.frame(maxWidth: .infinity, alignment: .center)
Button("Done", action: closePopover)
.controlSize(.large)
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding(20)
}
}
@ViewBuilder
private var stackBody: some View {
if appState.menuBarManager.isMenuBarHiddenBySystemUserDefaults {
cannotEdit
} else {
mainForm
}
}
@ViewBuilder
private var mainForm: some View {
IceForm(padding: mainFormPadding) {
IceSection {
isDynamicToggle
}
if appearanceManager.configuration.isDynamic {
LabeledPartialEditor(appearance: .light)
LabeledPartialEditor(appearance: .dark)
} else {
StaticPartialEditor()
}
IceSection("Menu Bar Shape") {
shapePicker
isInset
}
if case .settings = location {
IceGroupBox {
AnnotationView(
alignment: .center,
font: .callout.bold()
) {
Label {
Text("Tip: you can also edit these settings by right-clicking in an empty area of the menu bar")
} icon: {
Image(systemName: "lightbulb")
}
}
}
}
if
!appState.menuBarManager.isMenuBarHiddenBySystemUserDefaults,
appearanceManager.configuration != .defaultConfiguration
{
Button("Reset") {
appearanceManager.configuration = .defaultConfiguration
}
.controlSize(.large)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
}
}
}
@ViewBuilder
private var isDynamicToggle: some View {
Toggle("Use dynamic appearance", isOn: appearanceManager.bindings.configuration.isDynamic)
.annotation("Apply different settings based on the current system appearance")
}
@ViewBuilder
private var cannotEdit: some View {
Text("Ice cannot edit the appearance of automatically hidden menu bars")
.font(.title3)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
@ViewBuilder
private var shapePicker: some View {
MenuBarShapePicker()
.fixedSize(horizontal: false, vertical: true)
}
@ViewBuilder
private var isInset: some View {
if appearanceManager.configuration.shapeKind != .none {
Toggle(
"Use inset shape on screens with notch",
isOn: appearanceManager.bindings.configuration.isInset
)
}
}
}
private struct UnlabeledPartialEditor: View {
@Binding var configuration: MenuBarAppearancePartialConfiguration
var body: some View {
IceSection {
tintPicker
shadowToggle
}
IceSection {
borderToggle
borderColor
borderWidth
}
}
@ViewBuilder
private var tintPicker: some View {
IceLabeledContent("Tint") {
HStack {
IcePicker("Tint", selection: $configuration.tintKind) {
ForEach(MenuBarTintKind.allCases) { tintKind in
Text(tintKind.localized).tag(tintKind)
}
}
.labelsHidden()
switch configuration.tintKind {
case .none:
EmptyView()
case .solid:
CustomColorPicker(
selection: $configuration.tintColor,
supportsOpacity: false,
mode: .crayon
)
case .gradient:
CustomGradientPicker(
gradient: $configuration.tintGradient,
supportsOpacity: false,
allowsEmptySelections: false,
mode: .crayon
)
}
}
.frame(height: 24)
}
}
@ViewBuilder
private var shadowToggle: some View {
Toggle("Shadow", isOn: $configuration.hasShadow)
}
@ViewBuilder
private var borderToggle: some View {
Toggle("Border", isOn: $configuration.hasBorder)
}
@ViewBuilder
private var borderColor: some View {
if configuration.hasBorder {
IceLabeledContent("Border Color") {
CustomColorPicker(
selection: $configuration.borderColor,
supportsOpacity: true,
mode: .crayon
)
}
}
}
@ViewBuilder
private var borderWidth: some View {
if configuration.hasBorder {
IcePicker(
"Border Width",
selection: $configuration.borderWidth
) {
Text("1").tag(1.0)
Text("2").tag(2.0)
Text("3").tag(3.0)
}
}
}
}
private struct LabeledPartialEditor: View {
@EnvironmentObject var appearanceManager: MenuBarAppearanceManager
@State private var currentAppearance = SystemAppearance.current
@State private var textFrame = CGRect.zero
let appearance: SystemAppearance
var body: some View {
IceSection(options: .plain) {
labelStack
} content: {
partialEditor
}
.onReceive(NSApp.publisher(for: \.effectiveAppearance)) { _ in
currentAppearance = .current
}
}
@ViewBuilder
private var labelStack: some View {
HStack {
Text(appearance.titleKey)
.font(.headline)
.onFrameChange(update: $textFrame)
if currentAppearance != appearance {
previewButton
}
}
.frame(height: textFrame.height)
}
@ViewBuilder
private var previewButton: some View {
switch appearance {
case .light:
PreviewButton(configuration: appearanceManager.configuration.lightModeConfiguration)
case .dark:
PreviewButton(configuration: appearanceManager.configuration.darkModeConfiguration)
}
}
@ViewBuilder
private var partialEditor: some View {
switch appearance {
case .light:
UnlabeledPartialEditor(configuration: appearanceManager.bindings.configuration.lightModeConfiguration)
case .dark:
UnlabeledPartialEditor(configuration: appearanceManager.bindings.configuration.darkModeConfiguration)
}
}
}
private struct StaticPartialEditor: View {
@EnvironmentObject var appearanceManager: MenuBarAppearanceManager
var body: some View {
UnlabeledPartialEditor(configuration: appearanceManager.bindings.configuration.staticConfiguration)
}
}
private struct PreviewButton: View {
private struct DummyButton: NSViewRepresentable {
@Binding var isPressed: Bool
func makeNSView(context: Context) -> NSButton {
let button = NSButton()
button.title = ""
button.bezelStyle = .accessoryBarAction
return button
}
func updateNSView(_ nsView: NSButton, context: Context) {
nsView.isHighlighted = isPressed
}
}
@EnvironmentObject var appearanceManager: MenuBarAppearanceManager
@State private var frame = CGRect.zero
@State private var isPressed = false
let configuration: MenuBarAppearancePartialConfiguration
var body: some View {
ZStack {
DummyButton(isPressed: $isPressed)
.allowsHitTesting(false)
Text("Hold to Preview")
.baselineOffset(1.5)
.padding(.horizontal, 10)
.contentShape(Rectangle())
}
.fixedSize()
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
isPressed = frame.contains(value.location)
}
.onEnded { _ in
isPressed = false
}
)
.onChange(of: isPressed) { _, newValue in
appearanceManager.previewConfiguration = newValue ? configuration : nil
}
.onFrameChange(update: $frame)
}
}
================================================
FILE: Ice/MenuBar/Appearance/MenuBarAppearanceEditor/MenuBarAppearanceEditorPanel.swift
================================================
//
// MenuBarAppearanceEditorPanel.swift
// Ice
//
import Combine
import SwiftUI
// MARK: - MenuBarAppearanceEditorPanel
/// A panel that manages the appearance editor popover.
final class MenuBarAppearanceEditorPanel: NSPanel {
/// The shared app state.
private weak var appState: AppState?
/// Storage for internal observers.
private var cancellables = Set<AnyCancellable>()
init(appState: AppState) {
super.init(
contentRect: CGRect(x: 0, y: 0, width: 1, height: 1),
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false
)
self.appState = appState
self.isFloatingPanel = true
self.backgroundColor = .clear
configureCancellables()
}
private func configureCancellables() {
var c = Set<AnyCancellable>()
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.activeSpaceDidChangeNotification)
.sink { [weak self] _ in
self?.orderOut(self)
NSColorPanel.shared.close()
NSColorPanel.shared.hidesOnDeactivate = true
}
.store(in: &c)
cancellables = c
}
/// Shows the appearance editor popover.
func showAppearanceEditorPopover() {
guard
let appState,
let contentView,
let screen = NSScreen.screens.first(where: { $0.frame.contains(NSEvent.mouseLocation) }),
let menuBarHeight = NSApp.mainMenu?.menuBarHeight
else {
return
}
setFrameOrigin(CGPoint(x: screen.frame.midX - frame.width / 2, y: screen.frame.maxY - menuBarHeight))
let popover = MenuBarAppearanceEditorPopover(appState: appState)
popover.delegate = self
popover.show(relativeTo: .zero, of: contentView, preferredEdge: .minY)
popover.contentViewController?.view.window?.makeKey()
NSColorPanel.shared.hidesOnDeactivate = false
}
}
// MARK: MenuBarAppearanceEditorPanel: NSPopoverDelegate
extension MenuBarAppearanceEditorPanel: NSPopoverDelegate {
func popoverDidClose(_ notification: Notification) {
if let popover = notification.object as? MenuBarAppearanceEditorPopover {
popover.mouseDownMonitor.stop()
orderOut(popover)
NSColorPanel.shared.close()
NSColorPanel.shared.hidesOnDeactivate = true
}
}
}
// MARK: - MenuBarAppearanceEditorPopover
/// A popover that displays the menu bar appearance editor
/// at a centered location under the menu bar.
private final class MenuBarAppearanceEditorPopover: NSPopover {
private weak var appState: AppState?
private(set) lazy var mouseDownMonitor = GlobalEventMonitor(mask: .leftMouseDown) { [weak self] _ in
self?.performClose(self)
}
@ViewBuilder
private var contentView: some View {
if let appState {
MenuBarAppearanceEditor(
location: .popover(closePopover: { [weak self] in
self?.performClose(self)
})
)
.environmentObject(appState)
.environmentObject(appState.appearanceManager)
}
}
init(appState: AppState) {
super.init()
self.appState = appState
self.contentViewController = NSHostingController(rootView: contentView)
self.contentSize = CGSize(width: 550, height: 600)
self.behavior = .applicationDefined
self.mouseDownMonitor.start()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
================================================
FILE: Ice/MenuBar/Appearance/MenuBarAppearanceEditor/MenuBarShapePicker.swift
================================================
//
// MenuBarShapePicker.swift
// Ice
//
import SwiftUI
struct MenuBarShapePicker: View {
@EnvironmentObject var appearanceManager: MenuBarAppearanceManager
@Environment(\.colorScheme) private var colorScheme
var body: some View {
shapeKindPicker
exampleView
}
@ViewBuilder
private var shapeKindPicker: some View {
IcePicker("Shape Kind", selection: appearanceManager.bindings.configuration.shapeKind) {
ForEach(MenuBarShapeKind.allCases, id: \.self) { shape in
switch shape {
case .none:
Text("None").tag(shape)
case .full:
Text("Full").tag(shape)
case .split:
Text("Split").tag(shape)
}
}
}
}
@ViewBuilder
private var exampleView: some View {
switch appearanceManager.configuration.shapeKind {
case .none:
Text("No shape kind selected")
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .center)
case .full:
MenuBarFullShapeExampleView(info: appearanceManager.bindings.configuration.fullShapeInfo)
.equatable()
.foregroundStyle(colorScheme == .dark ? .primary : .secondary)
case .split:
MenuBarSplitShapeExampleView(info: appearanceManager.bindings.configuration.splitShapeInfo)
.equatable()
.foregroundStyle(colorScheme == .dark ? .primary : .secondary)
}
}
}
private struct MenuBarFullShapeExampleView: View, Equatable {
@Binding var info: MenuBarFullShapeInfo
var body: some View {
VStack {
pickerStack
exampleStack
}
}
@ViewBuilder
private var pickerStack: some View {
HStack(spacing: 0) {
leadingEndCapPicker
Spacer()
trailingEndCapPicker
}
.labelsHidden()
.pickerStyle(.segmented)
}
@ViewBuilder
private var exampleStack: some View {
HStack(spacing: 0) {
leadingEndCapExample
Rectangle()
trailingEndCapExample
}
.frame(height: 24)
}
@ViewBuilder
private func endCapPickerContentView(endCap: MenuBarEndCap, edge: HorizontalEdge) -> some View {
switch endCap {
case .square:
Image(size: CGSize(width: 12, height: 12)) { context in
context.fill(Path(context.clipBoundingRect), with: .foreground)
}
.resizable()
.help("Square Cap")
.tag(endCap)
case .round:
Image(size: CGSize(width: 12, height: 12)) { context in
let remainder = context.clipBoundingRect
.divided(atDistance: context.clipBoundingRect.width / 2, from: cgRectEdge(for: edge))
.remainder
let path1 = Path(remainder)
let path2 = Path(ellipseIn: context.clipBoundingRect)
context.fill(path1.union(path2), with: .foreground)
}
.resizable()
.help("Round Cap")
.tag(endCap)
}
}
@ViewBuilder
private var leadingEndCapPicker: some View {
Picker("Leading End Cap", selection: $info.leadingEndCap) {
ForEach(MenuBarEndCap.allCases.reversed(), id: \.self) { endCap in
endCapPickerContentView(endCap: endCap, edge: .leading)
}
}
.fixedSize()
}
@ViewBuilder
private var trailingEndCapPicker: some View {
Picker("Trailing End Cap", selection: $info.trailingEndCap) {
ForEach(MenuBarEndCap.allCases, id: \.self) { endCap in
endCapPickerContentView(endCap: endCap, edge: .trailing)
}
}
.fixedSize()
}
@ViewBuilder
private var leadingEndCapExample: some View {
MenuBarEndCapExampleView(
endCap: info.leadingEndCap,
edge: .leading
)
}
@ViewBuilder
private var trailingEndCapExample: some View {
MenuBarEndCapExampleView(
endCap: info.trailingEndCap,
edge: .trailing
)
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.info == rhs.info
}
private func cgRectEdge(for edge: HorizontalEdge) -> CGRectEdge {
switch edge {
case .leading: .minXEdge
case .trailing: .maxXEdge
}
}
}
private struct MenuBarEndCapExampleView: View {
@State private var radius: CGFloat = 0
let endCap: MenuBarEndCap
let edge: HorizontalEdge
var body: some View {
switch endCap {
case .square:
Rectangle()
case .round:
switch edge {
case .leading:
UnevenRoundedRectangle(
topLeadingRadius: radius,
bottomLeadingRadius: radius,
style: .circular
)
.onFrameChange { frame in
radius = frame.height / 2
}
case .trailing:
UnevenRoundedRectangle(
bottomTrailingRadius: radius,
topTrailingRadius: radius,
style: .circular
)
.onFrameChange { frame in
radius = frame.height / 2
}
}
}
}
}
private struct MenuBarSplitShapeExampleView: View, Equatable {
@Binding var info: MenuBarSplitShapeInfo
var body: some View {
HStack {
MenuBarFullShapeExampleView(info: $info.leading)
.equatable()
Divider()
.padding(.horizontal)
MenuBarFullShapeExampleView(info: $info.trailing)
.equatable()
}
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.info == rhs.info
}
}
================================================
FILE: Ice/MenuBar/Appearance/MenuBarAppearanceManager.swift
================================================
//
// MenuBarAppearanceManager.swift
// Ice
//
import Cocoa
import Combine
/// A manager for the appearance of the menu bar.
@MainActor
final class MenuBarAppearanceManager: ObservableObject {
/// The current menu bar appearance configuration.
@Published var configuration: MenuBarAppearanceConfigurationV2 = .defaultConfiguration
/// The currently previewed partial configuration.
@Published var previewConfiguration: MenuBarAppearancePartialConfiguration?
/// The shared app state.
private weak var appState: AppState?
/// Encoder for UserDefaults values.
private let encoder = JSONEncoder()
/// Decoder for UserDefaults values.
private let decoder = JSONDecoder()
/// Storage for internal observers.
private var cancellables = Set<AnyCancellable>()
/// The currently managed menu bar overlay panels.
private(set) var overlayPanels = Set<MenuBarOverlayPanel>()
/// The amount to inset the menu bar if called for by the configuration.
let menuBarInsetAmount: CGFloat = 5
/// Creates a manager with the given app state.
init(appState: AppState) {
self.appState = appState
}
/// Performs initial setup of the manager.
func performSetup() {
loadInitialState()
configureCancellables()
}
/// Loads the initial values for the configuration.
private func loadInitialState() {
do {
if let data = Defaults.data(forKey: .menuBarAppearanceConfigurationV2) {
configuration = try decoder.decode(MenuBarAppearanceConfigurationV2.self, from: data)
}
} catch {
Logger.appearanceManager.error("Error decoding configuration: \(error)")
}
}
/// Configures the internal observers for the manager.
private func configureCancellables() {
var c = Set<AnyCancellable>()
NotificationCenter.default
.publisher(for: NSApplication.didChangeScreenParametersNotification)
.debounce(for: 0.1, scheduler: DispatchQueue.main)
.sink { [weak self] _ in
guard let self else {
return
}
while let panel = overlayPanels.popFirst() {
panel.orderOut(self)
}
if Set(overlayPanels.map { $0.owningScreen }) != Set(NSScreen.screens) {
configureOverlayPanels(with: configuration)
}
}
.store(in: &c)
$configuration
.encode(encoder: encoder)
.receive(on: DispatchQueue.main)
.sink { completion in
if case .failure(let error) = completion {
Logger.appearanceManager.error("Error encoding configuration: \(error)")
}
} receiveValue: { data in
Defaults.set(data, forKey: .menuBarAppearanceConfigurationV2)
}
.store(in: &c)
$configuration
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] configuration in
guard let self else {
return
}
// The overlay panels may not have been configured yet. Since some of the
// properties on the manager might call for them, try to configure now.
if overlayPanels.isEmpty {
configureOverlayPanels(with: configuration)
}
}
.store(in: &c)
cancellables = c
}
/// Returns a Boolean value that indicates whether a set of overlay panels
/// is needed for the given configuration.
private func needsOverlayPanels(for configuration: MenuBarAppearanceConfigurationV2) -> Bool {
let current = configuration.current
if current.hasShadow {
return true
}
if current.hasBorder {
return true
}
if configuration.shapeKind != .none {
return true
}
if current.tintKind != .none {
return true
}
return false
}
/// Configures the manager's overlay panels, if required by the given configuration.
private func configureOverlayPanels(with configuration: MenuBarAppearanceConfigurationV2) {
guard
let appState,
needsOverlayPanels(for: configuration)
else {
while let panel = overlayPanels.popFirst() {
panel.close()
}
return
}
var overlayPanels = Set<MenuBarOverlayPanel>()
for screen in NSScreen.screens {
let panel = MenuBarOverlayPanel(appState: appState, owningScreen: screen)
overlayPanels.insert(panel)
panel.needsShow = true
}
self.overlayPanels = overlayPanels
}
/// Sets the value of ``MenuBarOverlayPanel/isDraggingMenuBarItem`` for each
/// of the manager's overlay panels.
func setIsDraggingMenuBarItem(_ isDragging: Bool) {
for panel in overlayPanels {
panel.isDraggingMenuBarItem = isDragging
}
}
}
// MARK: MenuBarAppearanceManager: BindingExposable
extension MenuBarAppearanceManager: BindingExposable { }
// MARK: - Logger
private extension Logger {
/// The logger to use for the menu bar appearance manager.
static let appearanceManager = Logger(category: "MenuBarAppearanceManager")
}
================================================
FILE: Ice/MenuBar/Appearance/MenuBarOverlayPanel.swift
================================================
//
// MenuBarOverlayPanel.swift
// Ice
//
import Cocoa
import Combine
// MARK: - Overlay Panel
/// A subclass of `NSPanel` that sits atop the menu bar to alter its appearance.
final class MenuBarOverlayPanel: NSPanel {
/// Flags representing the updatable components of a panel.
enum UpdateFlag: String, CustomStringConvertible {
case applicationMenuFrame
case desktopWallpaper
var description: String { rawValue }
}
/// The kind of validation that occurs before an update.
private enum ValidationKind {
case showing
case updates
}
/// A context that manages panel update tasks.
private final class UpdateTaskContext {
private var tasks = [UpdateFlag: Task<Void, any Error>]()
/// Sets the task for the given update flag.
///
/// Setting the task cancels the previous task for the flag, if there is one.
///
/// - Parameters:
/// - flag: The update flag to set the task for.
/// - timeout: The timeout of the task.
/// - operation: The operation for the task to perform.
func setTask(for flag: UpdateFlag, timeout: Duration, operation: @escaping () async throws -> Void) {
cancelTask(for: flag)
tasks[flag] = Task.detached(timeout: timeout) {
try await operation()
}
}
/// Cancels the task for the given update flag.
///
/// - Parameter flag: The update flag to cancel the task for.
func cancelTask(for flag: UpdateFlag) {
tasks.removeValue(forKey: flag)?.cancel()
}
}
/// A Boolean value that indicates whether the panel needs to be shown.
@Published var needsShow = false
/// A Boolean value that indicates whether the user is dragging a menu bar item.
@Published var isDraggingMenuBarItem = false
/// Flags representing the components of the panel currently in need of an update.
@Published private(set) var updateFlags = Set<UpdateFlag>()
/// The frame of the application menu.
@Published private(set) var applicationMenuFrame: CGRect?
/// The current desktop wallpaper, clipped to the bounds of the menu bar.
@Published private(set) var desktopWallpaper: CGImage?
/// Storage for internal observers.
private var cancellables = Set<AnyCancellable>()
/// The context that manages panel update tasks.
private let updateTaskContext = UpdateTaskContext()
/// The shared app state.
private(set) weak var appState: AppState?
/// The screen that owns the panel.
let owningScreen: NSScreen
/// Creates an overlay panel with the given app state and owning screen.
init(appState: AppState, owningScreen: NSScreen) {
self.appState = appState
self.owningScreen = owningScreen
super.init(
contentRect: .zero,
styleMask: [.borderless, .fullSizeContentView, .nonactivatingPanel],
backing: .buffered,
defer: false
)
self.level = .statusBar
self.title = "Menu Bar Overlay"
self.backgroundColor = .clear
self.hasShadow = false
self.ignoresMouseEvents = true
self.collectionBehavior = [.fullScreenNone, .ignoresCycle, .moveToActiveSpace]
self.contentView = MenuBarOverlayPanelContentView()
configureCancellables()
}
private func configureCancellables() {
var c = Set<AnyCancellable>()
// Show the panel on the active space.
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.activeSpaceDidChangeNotification)
.debounce(for: 0.1, scheduler: DispatchQueue.main)
.sink { [weak self] _ in
self?.needsShow = true
}
.store(in: &c)
// Update when light/dark mode changes.
DistributedNotificationCenter.default()
.publisher(for: DistributedNotificationCenter.interfaceThemeChangedNotification)
.debounce(for: 0.1, scheduler: DispatchQueue.main)
.sink { [weak self] _ in
guard let self else {
return
}
updateTaskContext.setTask(for: .desktopWallpaper, timeout: .seconds(5)) {
while true {
try Task.checkCancellation()
self.insertUpdateFlag(.desktopWallpaper)
try await Task.sleep(for: .seconds(1))
}
}
}
.store(in: &c)
// Update application menu frame when the menu bar owning or frontmost app changes.
Publishers.Merge(
NSWorkspace.shared.publisher(for: \.menuBarOwningApplication, options: .old)
.combineLatest(NSWorkspace.shared.publisher(for: \.menuBarOwningApplication, options: .new))
.compactMap { $0 == $1 ? nil : $0 },
NSWorkspace.shared.publisher(for: \.frontmostApplication, options: .old)
.combineLatest(NSWorkspace.shared.publisher(for: \.frontmostApplication, options: .new))
.compactMap { $0 == $1 ? nil : $0 }
)
.removeDuplicates()
.sink { [weak self] _ in
guard
let self,
let appState
else {
return
}
let displayID = owningScreen.displayID
updateTaskContext.setTask(for: .applicationMenuFrame, timeout: .seconds(10)) {
var hasDoneInitialUpdate = false
while true {
try Task.checkCancellation()
guard
let latestFrame = appState.menuBarManager.getApplicationMenuFrame(for: displayID),
latestFrame != self.applicationMenuFrame
else {
if hasDoneInitialUpdate {
try await Task.sleep(for: .seconds(1))
} else {
try await Task.sleep(for: .milliseconds(1))
}
continue
}
self.insertUpdateFlag(.applicationMenuFrame)
hasDoneInitialUpdate = true
}
}
Task {
try? await Task.sleep(for: .milliseconds(100))
if self.owningScreen != NSScreen.main {
self.updateTaskContext.cancelTask(for: .applicationMenuFrame)
}
}
}
.store(in: &c)
// Special cases for when the user drags an app onto or clicks into another space.
Publishers.Merge(
publisher(for: \.isOnActiveSpace)
.receive(on: DispatchQueue.main)
.mapToVoid(),
UniversalEventMonitor.publisher(for: .leftMouseUp)
.filter { [weak self] _ in self?.isOnActiveSpace ?? false }
.mapToVoid()
)
.debounce(for: 0.05, scheduler: DispatchQueue.main)
.sink { [weak self] in
self?.insertUpdateFlag(.applicationMenuFrame)
}
.store(in: &c)
// Continually update the desktop wallpaper. Ideally, we would set up an observer
// for a wallpaper change notification, but macOS doesn't post one anymore.
Timer.publish(every: 5, on: .main, in: .default)
.autoconnect()
.sink { [weak self] _ in
self?.insertUpdateFlag(.desktopWallpaper)
}
.store(in: &c)
Timer.publish(every: 10, on: .main, in: .default)
.autoconnect()
.sink { [weak self] _ in
self?.insertUpdateFlag(.applicationMenuFrame)
}
.store(in: &c)
$needsShow
.debounce(for: 0.05, scheduler: DispatchQueue.main)
.sink { [weak self] needsShow in
guard let self, needsShow else {
return
}
defer {
self.needsShow = false
}
show()
}
.store(in: &c)
$updateFlags
.sink { [weak self] flags in
guard let self, !flags.isEmpty else {
return
}
Task {
// Must be run async, or this will not remove the flags.
self.updateFlags.removeAll()
}
let windows = WindowInfo.getOnScreenWindows()
guard let owningDisplay = self.validate(for: .updates, with: windows) else {
return
}
performUpdates(for: flags, windows: windows, display: owningDisplay)
}
.store(in: &c)
if let appState {
appState.menuBarManager.$isMenuBarHiddenBySystem
.sink { [weak self] isHidden in
self?.alphaValue = isHidden ? 0 : 1
}
.store(in: &c)
}
cancellables = c
}
/// Inserts the given update flag into the panel's current list of update flags.
private func insertUpdateFlag(_ flag: UpdateFlag) {
updateFlags.insert(flag)
}
/// Performs validation for the given validation kind. Returns the panel's
/// owning display if successful. Returns `nil` on failure.
private func validate(for kind: ValidationKind, with windows: [WindowInfo]) -> CGDirectDisplayID? {
lazy var actionMessage = switch kind {
case .showing: "Preventing overlay panel from showing."
case .updates: "Preventing overlay panel from updating."
}
guard let appState else {
Logger.overlayPanel.debug("No app state. \(actionMessage)")
return nil
}
guard !appState.menuBarManager.isMenuBarHiddenBySystemUserDefaults else {
Logger.overlayPanel.debug("Menu bar is hidden by system. \(actionMessage)")
return nil
}
guard !appState.isActiveSpaceFullscreen else {
Logger.overlayPanel.debug("Active space is fullscreen. \(actionMessage)")
return nil
}
let owningDisplay = owningScreen.displayID
guard appState.menuBarManager.hasValidMenuBar(in: windows, for: owningDisplay) else {
Logger.overlayPanel.debug("No valid menu bar found. \(actionMessage)")
return nil
}
return owningDisplay
}
/// Stores the frame of the menu bar's application menu.
private func updateApplicationMenuFrame(for display: CGDirectDisplayID) {
guard
let menuBarManager = appState?.menuBarManager,
!menuBarManager.isMenuBarHiddenBySystem
else {
return
}
applicationMenuFrame = menuBarManager.getApplicationMenuFrame(for: display)
}
/// Stores the area of the desktop wallpaper that is under the menu bar
/// of the given display.
private func updateDesktopWallpaper(for display: CGDirectDisplayID, with windows: [WindowInfo]) {
guard
let wallpaperWindow = WindowInfo.getWallpaperWindow(from: windows, for: display),
let menuBarWindow = WindowInfo.getMenuBarWindow(from: windows, for: display)
else {
return
}
let wallpaper = ScreenCapture.captureWindow(wallpaperWindow.windowID, screenBounds: menuBarWindow.frame)
if desktopWallpaper?.dataProvider?.data != wallpaper?.dataProvider?.data {
desktopWallpaper = wallpaper
}
}
/// Updates the panel to prepare for display.
private func performUpdates(for flags: Set<UpdateFlag>, windows: [WindowInfo], display: CGDirectDisplayID) {
if flags.contains(.applicationMenuFrame) {
updateApplicationMenuFrame(for: display)
}
if flags.contains(.desktopWallpaper) {
updateDesktopWallpaper(for: display, with: windows)
}
}
/// Shows the panel.
private func show() {
guard
let appState,
!appState.isPreview
else {
return
}
guard appState.appearanceManager.overlayPanels.contains(self) else {
Logger.overlayPanel.warning("Overlay panel \(self) not retained")
return
}
guard let menuBarHeight = owningScreen.getMenuBarHeight() else {
return
}
let newFrame = CGRect(
x: owningScreen.frame.minX,
y: (owningScreen.frame.maxY - menuBarHeight) - 5,
width: owningScreen.frame.width,
height: menuBarHeight + 5
)
alphaValue = 0
setFrame(newFrame, display: false)
orderFrontRegardless()
updateFlags = [.applicationMenuFrame, .desktopWallpaper]
if !appState.menuBarManager.isMenuBarHiddenBySystem {
animator().alphaValue = 1
}
}
override func isAccessibilityElement() -> Bool {
return false
}
}
// MARK: - Content View
private final class MenuBarOverlayPanelContentView: NSView {
@Published private var fullConfiguration: MenuBarAppearanceConfigurationV2 = .defaultConfiguration
@Published private var previewConfiguration: MenuBarAppearancePartialConfiguration?
private var cancellables = Set<AnyCancellable>()
/// The overlay panel that contains the content view.
private var overlayPanel: MenuBarOverlayPanel? {
window as? MenuBarOverlayPanel
}
/// The currently displayed configuration.
private var configuration: MenuBarAppearancePartialConfiguration {
previewConfiguration ?? fullConfiguration.current
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
configureCancellables()
}
private func configureCancellables() {
var c = Set<AnyCancellable>()
if let overlayPanel {
if let appState = overlayPanel.appState {
appState.appearanceManager.$configuration
.removeDuplicates()
.assign(to: &$fullConfiguration)
appState.appearanceManager.$previewConfiguration
.removeDuplicates()
.assign(to: &$previewConfiguration)
for section in appState.menuBarManager.sections {
// Redraw whenever the window frame of a control item changes.
//
// - NOTE: A previous attempt was made to redraw the view when the
// section's `isHidden` property was changed. This would be semantically
// ideal, but the property sometimes changes before the menu bar items
// are actually updated on-screen. Since the view's drawing process relies
// on getting an accurate position of each menu bar item, we need to use
// something that publishes its changes only after the items are updated.
section.controlItem.$windowFrame
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.needsDisplay = true
}
.store(in: &c)
// Redraw whenever the visibility of a control item changes.
//
// - NOTE: If the "ShowSectionDividers" setting is disabled, the window
// frame does not update when the section is hidden or shown, but the
// visibility does. We observe both to ensure the update occurs.
section.controlItem.$isVisible
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.needsDisplay = true
}
.store(in: &c)
}
}
// Fade out whenever a menu bar item is being dragged.
overlayPanel.$isDraggingMenuBarItem
.removeDuplicates()
.sink { [weak self] isDragging in
if isDragging {
self?.animator().alphaValue = 0
} else {
self?.animator().alphaValue = 1
}
}
.store(in: &c)
// Redraw whenever the application menu frame changes.
overlayPanel.$applicationMenuFrame
.sink { [weak self] _ in
self?.needsDisplay = true
}
.store(in: &c)
// Redraw whenever the desktop wallpaper changes.
overlayPanel.$desktopWallpaper
.sink { [weak self] _ in
self?.needsDisplay = true
}
.store(in: &c)
}
// Redraw whenever the configurations change.
$fullConfiguration.mapToVoid()
.merge(with: $previewConfiguration.mapToVoid())
.sink { [weak self] _ in
self?.needsDisplay = true
}
.store(in: &c)
cancellables = c
}
/// Returns a path in the given rectangle, with the given end caps,
/// and inset by the given amounts.
private func shapePath(in rect: CGRect, leadingEndCap: MenuBarEndCap, trailingEndCap: MenuBarEndCap, screen: NSScreen) -> NSBezierPath {
let insetRect: CGRect = if !screen.hasNotch {
switch (leadingEndCap, trailingEndCap) {
case (.square, .square):
CGRect(x: rect.origin.x, y: rect.origin.y + 1, width: rect.width, height: rect.height - 2)
case (.square, .round):
CGRect(x: rect.origin.x, y: rect.origin.y + 1, width: rect.width - 1, height: rect.height - 2)
case (.round, .square):
CGRect(x: rect.origin.x + 1, y: rect.origin.y + 1, width: rect.width - 1, height: rect.height - 2)
case (.round, .round):
CGRect(x: rect.origin.x + 1, y: rect.origin.y + 1, width: rect.width - 2, height: rect.height - 2)
}
} else {
rect
}
let shapeBounds = CGRect(
x: insetRect.minX + insetRect.height / 2,
y: insetRect.minY,
width: insetRect.width - insetRect.height,
height: insetRect.height
)
let leadingEndCapBounds = CGRect(
x: insetRect.minX,
y: insetRect.minY,
width: insetRect.height,
height: insetRect.height
)
let trailingEndCapBounds = CGRect(
x: insetRect.maxX - insetRect.height,
y: insetRect.minY,
width: insetRect.height,
height: insetRect.height
)
var path = NSBezierPath(rect: shapeBounds)
path = switch leadingEndCap {
case .square: path.union(NSBezierPath(rect: leadingEndCapBounds))
case .round: path.union(NSBezierPath(ovalIn: leadingEndCapBounds))
}
path = switch trailingEndCap {
case .square: path.union(NSBezierPath(rect: trailingEndCapBounds))
case .round: path.union(NSBezierPath(ovalIn: trailingEndCapBounds))
}
return path
}
/// Returns a path for the ``MenuBarShapeKind/full`` shape kind.
private func pathForFullShape(in rect: CGRect, info: MenuBarFullShapeInfo, isInset: Bool, screen: NSScreen) -> NSBezierPath {
guard let appearanceManager = overlayPanel?.appState?.appearanceManager else {
return NSBezierPath()
}
var rect = rect
let shouldInset = isInset && screen.hasNotch
if shouldInset {
rect = rect.insetBy(dx: 0, dy: appearanceManager.menuBarInsetAmount)
if info.leadingEndCap == .round {
rect.origin.x += appearanceManager.menuBarInsetAmount
rect.size.width -= appearanceManager.menuBarInsetAmount
}
if info.trailingEndCap == .round {
rect.size.width -= appearanceManager.menuBarInsetAmount
}
}
return shapePath(
in: rect,
leadingEndCap: info.leadingEndCap,
trailingEndCap: info.trailingEndCap,
screen: screen
)
}
/// Returns a path for the ``MenuBarShapeKind/split`` shape kind.
private func pathForSplitShape(in rect: CGRect, info: MenuBarSplitShapeInfo, isInset: Bool, screen: NSScreen) -> NSBezierPath {
guard let appearanceManager = overlayPanel?.appState?.appearanceManager else {
return NSBezierPath()
}
var rect = rect
let shouldInset = isInset && screen.hasNotch
if shouldInset {
rect = rect.insetBy(dx: 0, dy: appearanceManager.menuBarInsetAmount)
if info.leading.leadingEndCap == .round {
rect.origin.x += appearanceManager.menuBarInsetAmount
rect.size.width -= appearanceManager.menuBarInsetAmount
}
if info.trailing.trailingEndCap == .round {
rect.size.width -= appearanceManager.menuBarInsetAmount
}
}
let leadingPathBounds: CGRect = {
guard
var maxX = overlayPanel?.applicationMenuFrame?.width,
maxX > 0
else {
return .zero
}
if shouldInset {
maxX += 10
if info.leading.leadingEndCap == .square {
maxX += appearanceManager.menuBarInsetAmount
}
} else {
maxX += 20
}
return CGRect(x: rect.minX, y: rect.minY, width: maxX, height: rect.height)
}()
let trailingPathBounds: CGRect = {
let items = MenuBarItem.getMenuBarItems(on: screen.displayID, onScreenOnly: true, activeSpaceOnly: false)
guard !items.isEmpty else {
return .zero
}
let totalWidth = items.reduce(into: 0) { width, item in
width += item.frame.width
}
var position = rect.maxX - totalWidth
if shouldInset {
position += 4
if info.trailing.trailingEndCap == .square {
position -= appearanceManager.menuBarInsetAmount
}
} else {
position -= 7
}
return CGRect(x: position, y: rect.minY, width: rect.maxX - position, height: rect.height)
}()
if leadingPathBounds == .zero || trailingPathBounds == .zero || leadingPathBounds.intersects(trailingPathBounds) {
return shapePath(
in: rect,
leadingEndCap: info.leading.leadingEndCap,
trailingEndCap: info.trailing.trailingEndCap,
screen: screen
)
} else {
let leadingPath = shapePath(
in: leadingPathBounds,
leadingEndCap: info.leading.leadingEndCap,
trailingEndCap: info.leading.trailingEndCap,
screen: screen
)
let trailingPath = shapePath(
in: trailingPathBounds,
leadingEndCap: info.trailing.leadingEndCap,
trailingEndCap: info.trailing.trailingEndCap,
screen: screen
)
let path = NSBezierPath()
path.append(leadingPath)
path.append(trailingPath)
return path
}
}
/// Returns the bounds that the view's drawn content can occupy.
private func getDrawableBounds() -> CGRect {
return CGRect(
x: bounds.origin.x,
y: bounds.origin.y + 5,
width: bounds.width,
height: bounds.height - 5
)
}
/// Draws the tint defined by the given configuration in the given rectangle.
private func drawTint(in rect: CGRect) {
switch configuration.tintKind {
case .none:
break
case .solid:
if let tintColor = NSColor(cgColor: configuration.tintColor)?.withAlphaComponent(0.2) {
tintColor.setFill()
rect.fill()
}
case .gradient:
if let tintGradient = configuration.tintGradient.withAlphaComponent(0.2).nsGradient {
tintGradient.draw(in: rect, angle: 0)
}
}
}
override func draw(_ dirtyRect: NSRect) {
guard
let overlayPanel,
let context = NSGraphicsContext.current
else {
return
}
let drawableBounds = getDrawableBounds()
let shapePath = switch fullConfiguration.shapeKind {
case .none:
NSBezierPath(rect: drawableBounds)
case .full:
pathForFullShape(
in: drawableBounds,
info: fullConfiguration.fullShapeInfo,
isInset: fullConfiguration.isInset,
screen: overlayPanel.owningScreen
)
case .split:
pathForSplitShape(
in: drawableBounds,
info: fullConfiguration.splitShapeInfo,
isInset: fullConfiguration.isInset,
screen: overlayPanel.owningScreen
)
}
var hasBorder = false
switch fullConfiguration.shapeKind {
case .none:
if configuration.hasShadow {
let gradient = NSGradient(
colors: [
NSColor(white: 0.0, alpha: 0.0),
NSColor(white: 0.0, alpha: 0.2),
]
)
let shadowBounds = CGRect(
x: bounds.minX,
y: bounds.minY,
width: bounds.width,
height: 5
)
gradient?.draw(in: shadowBounds, angle: 90)
}
drawTint(in: drawableBounds)
if configuration.hasBorder {
let borderBounds = CGRect(
x: bounds.minX,
y: bounds.minY + 5,
width: bounds.width,
height: configuration.borderWidth
)
NSColor(cgColor: configuration.borderColor)?.setFill()
NSBezierPath(rect: borderBounds).fill()
}
case .full, .split:
if let desktopWallpaper = overlayPanel.desktopWallpaper {
context.saveGraphicsState()
defer {
context.restoreGraphicsState()
}
let invertedClipPath = NSBezierPath(rect: drawableBounds)
invertedClipPath.append(shapePath.reversed)
invertedClipPath.setClip()
context.cgContext.draw(desktopWallpaper, in: drawableBounds)
}
if configuration.hasShadow {
context.saveGraphicsState()
defer {
context.restoreGraphicsState()
}
let shadowClipPath = NSBezierPath(rect: bounds)
shadowClipPath.append(shapePath.reversed)
shadowClipPath.setClip()
shapePath.drawShadow(color: .black.withAlphaComponent(0.5), radius: 5)
}
if configuration.hasBorder {
hasBorder = true
}
do {
context.saveGraphicsState()
defer {
context.restoreGraphicsState()
}
shapePath.setClip()
drawTint(in: drawableBounds)
}
if
hasBorder,
let borderColor = NSColor(cgColor: configuration.borderColor)
{
context.saveGraphicsState()
defer {
context.restoreGraphicsState()
}
let borderPath = switch fullConfiguration.shapeKind {
case .none:
NSBezierPath(rect: drawableBounds)
case .full:
pathForFullShape(
in: drawableBounds,
info: fullConfiguration.fullShapeInfo,
isInset: fullConfiguration.isInset,
screen: overlayPanel.owningScreen
)
case .split:
pathForSplitShape(
in: drawableBounds,
info: fullConfiguration.splitShapeInfo,
isInset: fullConfiguration.isInset,
screen: overlayPanel.owningScreen
)
}
// HACK: Insetting a path to get an "inside" stroke is surprisingly
// difficult. We can fake the correct line width by doubling it, as
// anything outside the shape path will be clipped.
borderPath.lineWidth = configuration.borderWidth * 2
borderPath.setClip()
borderColor.setStroke()
borderPath.stroke()
}
}
}
}
// MARK: - Logger
private extension Logger {
static let overlayPanel = Logger(category: "MenuBarOverlayPanel")
}
================================================
FILE: Ice/MenuBar/Appearance/MenuBarShape.swift
================================================
//
// MenuBarShape.swift
// Ice
//
import CoreGraphics
/// An end cap in a menu bar shape.
enum MenuBarEndCap: Int, Codable, Hashable, CaseIterable {
/// An end cap with a square shape.
case square = 0
/// An end cap with a rounded shape.
case round = 1
}
/// A type that specifies a custom shape kind for the menu bar.
enum MenuBarShapeKind: Int, Codable, Hashable, CaseIterable {
/// The menu bar does not use a custom shape.
case none = 0
/// A custom shape that takes up the full menu bar.
case full = 1
/// A custom shape that splits the menu bar between
/// its leading and trailing sides.
case split = 2
}
/// Information for the ``MenuBarShapeKind/full`` menu bar
/// shape kind.
struct MenuBarFullShapeInfo: Codable, Hashable {
/// The leading end cap of the shape.
var leadingEndCap: MenuBarEndCap
/// The trailing end cap of the shape.
var trailingEndCap: MenuBarEndCap
}
extension MenuBarFullShapeInfo {
var hasRoundedShape: Bool {
leadingEndCap == .round || trailingEndCap == .round
}
}
extension MenuBarFullShapeInfo {
static let `default` = MenuBarFullShapeInfo(leadingEndCap: .round, trailingEndCap: .round)
}
/// Information for the ``MenuBarShapeKind/split`` menu bar
/// shape kind.
struct MenuBarSplitShapeInfo: Codable, Hashable {
/// The leading information of the shape.
var leading: MenuBarFullShapeInfo
/// The trailing information of the shape.
var trailing: MenuBarFullShapeInfo
}
extension MenuBarSplitShapeInfo {
var hasRoundedShape: Bool {
leading.hasRoundedShape || trailing.hasRoundedShape
}
}
extension MenuBarSplitShapeInfo {
static let `default` = MenuBarSplitShapeInfo(leading: .default, trailing: .default)
}
================================================
FILE: Ice/MenuBar/Appearance/MenuBarTintKind.swift
================================================
//
// MenuBarTintKind.swift
// Ice
//
import SwiftUI
/// A type that specifies how the menu bar is tinted.
enum MenuBarTintKind: Int, CaseIterable, Codable, Identifiable {
/// The menu bar is not tinted.
case none = 0
/// The menu bar is tinted with a solid color.
case solid = 1
/// The menu bar is tinted with a gradient.
case gradient = 2
var id: Int { rawValue }
/// Localized string key representation.
var localized: LocalizedStringKey {
switch self {
case .none: "None"
case .solid: "Solid"
case .gradient: "Gradient"
}
}
}
================================================
FILE: Ice/MenuBar/ControlItem/ControlItem.swift
================================================
//
// ControlItem.swift
// Ice
//
import Cocoa
import Combine
/// A status item that controls a section in the menu bar.
@MainActor
final class ControlItem {
/// Possible identifiers for control items.
enum Identifier: String, CaseIterable {
case iceIcon = "SItem"
case hidden = "HItem"
case alwaysHidden = "AHItem"
}
/// Possible hiding states for control items.
enum HidingState {
case hideItems, showItems
}
/// Possible lengths for control items.
enum Lengths {
static let standard: CGFloat = NSStatusItem.variableLength
static let expanded: CGFloat = 10_000
}
/// The control item's hiding state (`@Published`).
@Published var state = HidingState.hideItems
/// A Boolean value that indicates whether the control item is visible (`@Published`).
@Published var isVisible = true
/// The frame of the control item's window (`@Published`).
@Published private(set) var windowFrame: CGRect?
/// The shared app state.
private weak var appState: AppState?
/// The control item's underlying status item.
private let statusItem: NSStatusItem
/// A horizontal constraint for the control item's content view.
private let constraint: NSLayoutConstraint?
/// The control item's identifier.
private let identifier: Identifier
/// Storage for internal observers.
private var cancellables = Set<AnyCancellable>()
/// The menu bar section associated with the control item.
private weak var section: MenuBarSection? {
appState?.menuBarManager.sections.first { $0.controlItem === self }
}
/// The control item's window.
var window: NSWindow? {
statusItem.button?.window
}
/// The identifier of the control item's window.
var windowID: CGWindowID? {
guard let window else {
return nil
}
return CGWindowID(window.windowNumber)
}
/// A Boolean value that indicates whether the control item serves as
/// a divider between sections.
var isSectionDivider: Bool {
identifier != .iceIcon
}
/// A Boolean value that indicates whether the control item is currently
/// displayed in the menu bar.
var isAddedToMenuBar: Bool {
statusItem.isVisible
}
/// Creates a control item with the given identifier and app state.
init(identifier: Identifier, appState: AppState) {
let autosaveName = identifier.rawValue
// If the status item doesn't have a preferred position, set it
// according to the identifier.
if StatusItemDefaults[.preferredPosition, autosaveName] == nil {
switch identifier {
case .iceIcon:
StatusItemDefaults[.preferredPosition, autosaveName] = 0
case .hidden:
StatusItemDefaults[.preferredPosition, autosaveName] = 1
case .alwaysHidden:
break
}
}
self.statusItem = NSStatusBar.system.statusItem(withLength: 0)
self.statusItem.autosaveName = autosaveName
self.identifier = identifier
self.appState = appState
// This could break in a new macOS release, but we need this constraint in order to be
// able to hide the control item when the `ShowSectionDividers` setting is disabled. A
// previous implementation used the status item's `isVisible` property, which was more
// robust, but would completel
gitextract_awypvfy4/
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ └── workflows/
│ └── lint.yml
├── .gitignore
├── .swiftlint.yml
├── CODE_OF_CONDUCT.md
├── FREQUENT_ISSUES.md
├── Ice/
│ ├── Assets.xcassets/
│ │ ├── AccentColor.colorset/
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── ControlItemImages/
│ │ │ ├── Contents.json
│ │ │ ├── Dot/
│ │ │ │ ├── Contents.json
│ │ │ │ ├── DotFill.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── DotStroke.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Ellipsis/
│ │ │ │ ├── Contents.json
│ │ │ │ ├── EllipsisFill.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── EllipsisStroke.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── IceCube/
│ │ │ ├── Contents.json
│ │ │ ├── IceCubeFill.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── IceCubeStroke.imageset/
│ │ │ └── Contents.json
│ │ ├── DefaultLayoutBarColor.colorset/
│ │ │ └── Contents.json
│ │ └── Warning.imageset/
│ │ └── Contents.json
│ ├── Bridging/
│ │ ├── Bridging.swift
│ │ └── Shims/
│ │ ├── Deprecated.swift
│ │ └── Private.swift
│ ├── Events/
│ │ ├── EventManager.swift
│ │ ├── EventMonitors/
│ │ │ ├── GlobalEventMonitor.swift
│ │ │ ├── LocalEventMonitor.swift
│ │ │ ├── RunLoopLocalEventMonitor.swift
│ │ │ └── UniversalEventMonitor.swift
│ │ └── EventTap.swift
│ ├── Hotkeys/
│ │ ├── Hotkey.swift
│ │ ├── HotkeyAction.swift
│ │ ├── HotkeyRegistry.swift
│ │ ├── KeyCode.swift
│ │ ├── KeyCombination.swift
│ │ └── Modifiers.swift
│ ├── Ice.entitlements
│ ├── Info.plist
│ ├── Main/
│ │ ├── AppDelegate.swift
│ │ ├── AppState.swift
│ │ ├── IceApp.swift
│ │ └── Navigation/
│ │ ├── AppNavigationState.swift
│ │ └── NavigationIdentifiers/
│ │ ├── NavigationIdentifier.swift
│ │ └── SettingsNavigationIdentifier.swift
│ ├── MenuBar/
│ │ ├── Appearance/
│ │ │ ├── Configurations/
│ │ │ │ ├── MenuBarAppearanceConfigurationV1.swift
│ │ │ │ └── MenuBarAppearanceConfigurationV2.swift
│ │ │ ├── MenuBarAppearanceEditor/
│ │ │ │ ├── MenuBarAppearanceEditor.swift
│ │ │ │ ├── MenuBarAppearanceEditorPanel.swift
│ │ │ │ └── MenuBarShapePicker.swift
│ │ │ ├── MenuBarAppearanceManager.swift
│ │ │ ├── MenuBarOverlayPanel.swift
│ │ │ ├── MenuBarShape.swift
│ │ │ └── MenuBarTintKind.swift
│ │ ├── ControlItem/
│ │ │ ├── ControlItem.swift
│ │ │ ├── ControlItemImage.swift
│ │ │ └── ControlItemImageSet.swift
│ │ ├── MenuBarItems/
│ │ │ ├── MenuBarItem.swift
│ │ │ ├── MenuBarItemImageCache.swift
│ │ │ ├── MenuBarItemInfo.swift
│ │ │ └── MenuBarItemManager.swift
│ │ ├── MenuBarManager.swift
│ │ ├── MenuBarSection.swift
│ │ ├── Search/
│ │ │ └── MenuBarSearchPanel.swift
│ │ └── Spacing/
│ │ └── MenuBarItemSpacingManager.swift
│ ├── Permissions/
│ │ ├── Permission.swift
│ │ ├── PermissionsManager.swift
│ │ ├── PermissionsView.swift
│ │ └── PermissionsWindow.swift
│ ├── Resources/
│ │ └── Acknowledgements.rtf
│ ├── Settings/
│ │ ├── SettingsManagers/
│ │ │ ├── AdvancedSettingsManager.swift
│ │ │ ├── GeneralSettingsManager.swift
│ │ │ ├── HotkeySettingsManager.swift
│ │ │ └── SettingsManager.swift
│ │ ├── SettingsPanes/
│ │ │ ├── AboutSettingsPane.swift
│ │ │ ├── AdvancedSettingsPane.swift
│ │ │ ├── GeneralSettingsPane.swift
│ │ │ ├── HotkeysSettingsPane.swift
│ │ │ ├── MenuBarAppearanceSettingsPane.swift
│ │ │ └── MenuBarLayoutSettingsPane.swift
│ │ ├── SettingsView.swift
│ │ └── SettingsWindow.swift
│ ├── Swizzling/
│ │ └── NSSplitViewItem+swizzledCanCollapse.swift
│ ├── UI/
│ │ ├── HotkeyRecorder/
│ │ │ ├── HotkeyRecorder.swift
│ │ │ └── HotkeyRecorderModel.swift
│ │ ├── IceBar/
│ │ │ ├── IceBar.swift
│ │ │ ├── IceBarColorManager.swift
│ │ │ └── IceBarLocation.swift
│ │ ├── IceUI/
│ │ │ ├── IceForm.swift
│ │ │ ├── IceGroupBox.swift
│ │ │ ├── IceLabeledContent.swift
│ │ │ ├── IceMenu.swift
│ │ │ ├── IcePicker.swift
│ │ │ ├── IceSection.swift
│ │ │ └── IceSlider.swift
│ │ ├── LayoutBar/
│ │ │ ├── LayoutBar.swift
│ │ │ ├── LayoutBarContainer.swift
│ │ │ ├── LayoutBarItemView.swift
│ │ │ ├── LayoutBarPaddingView.swift
│ │ │ └── LayoutBarScrollView.swift
│ │ ├── Pickers/
│ │ │ ├── CustomColorPicker/
│ │ │ │ └── CustomColorPicker.swift
│ │ │ └── CustomGradientPicker/
│ │ │ ├── ColorStop.swift
│ │ │ ├── CustomGradient.swift
│ │ │ └── CustomGradientPicker.swift
│ │ ├── Shapes/
│ │ │ └── AnyInsettableShape.swift
│ │ ├── ViewModifiers/
│ │ │ ├── BottomBar.swift
│ │ │ ├── ErasedToAnyView.swift
│ │ │ ├── LayoutBarStyle.swift
│ │ │ ├── LocalEventMonitorModifier.swift
│ │ │ ├── OnFrameChange.swift
│ │ │ ├── OnKeyDown.swift
│ │ │ ├── Once.swift
│ │ │ ├── ReadWindow.swift
│ │ │ └── RemoveSidebarToggle.swift
│ │ └── Views/
│ │ ├── AnnotationView.swift
│ │ ├── BetaBadge.swift
│ │ ├── SectionedList.swift
│ │ └── VisualEffectView.swift
│ ├── Updates/
│ │ └── UpdatesManager.swift
│ ├── UserNotifications/
│ │ ├── UserNotificationIdentifier.swift
│ │ └── UserNotificationManager.swift
│ └── Utilities/
│ ├── BindingExposable.swift
│ ├── CodableColor.swift
│ ├── Constants.swift
│ ├── Defaults.swift
│ ├── Extensions.swift
│ ├── IconResource.swift
│ ├── Injection.swift
│ ├── LocalizedErrorWrapper.swift
│ ├── Logging.swift
│ ├── MigrationManager.swift
│ ├── MouseCursor.swift
│ ├── Notifications.swift
│ ├── ObjectStorage.swift
│ ├── Predicates.swift
│ ├── RehideStrategy.swift
│ ├── ScreenCapture.swift
│ ├── StatusItemDefaults.swift
│ ├── SystemAppearance.swift
│ ├── TaskTimeout.swift
│ └── WindowInfo.swift
├── Ice.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm/
│ │ └── Package.resolved
│ └── xcshareddata/
│ └── xcschemes/
│ └── Ice.xcscheme
├── LICENSE
├── README.md
└── Resources/
└── Icon.fig
Condensed preview — 152 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (730K chars).
[
{
"path": ".gitattributes",
"chars": 24,
"preview": "*.rtf linguist-vendored\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 49,
"preview": "github: jordanbaird\nbuy_me_a_coffee: jordanbaird\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 1662,
"preview": "name: Bug Report\ndescription: Submit a bug report.\ntitle: \"[Bug]: \"\nlabels: Bug\nbody:\n - type: checkboxes\n attribute"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 28,
"preview": "blank_issues_enabled: false\n"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 1021,
"preview": "name: Feature Request\ndescription: Request a feature or suggest an idea.\ntitle: \"[Feature Request]: \"\nlabels: Feature\nbo"
},
{
"path": ".github/workflows/lint.yml",
"chars": 516,
"preview": "name: Lint Swift Files\non:\n push:\n branches: [ \"main\" ]\n paths:\n - \".github/workflows/lint.yml\"\n - \".sw"
},
{
"path": ".gitignore",
"chars": 29,
"preview": ".DS_Store\nbuild/\nxcuserdata/\n"
},
{
"path": ".swiftlint.yml",
"chars": 1627,
"preview": "included:\n - Ice\n\ndisabled_rules:\n - cyclomatic_complexity\n - file_length\n - function_body_length\n - function_param"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 5227,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
},
{
"path": "FREQUENT_ISSUES.md",
"chars": 2153,
"preview": "# Frequent Issues <!-- omit in toc -->\n\n- [Items are moved to the always-hidden section](#items-are-moved-to-the-always-"
},
{
"path": "Ice/Assets.xcassets/AccentColor.colorset/Contents.json",
"chars": 123,
"preview": "{\n \"colors\" : [\n {\n \"idiom\" : \"universal\"\n }\n ],\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }"
},
{
"path": "Ice/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 1301,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"icon_16x16.png\",\n \"idiom\" : \"mac\",\n \"scale\" : \"1x\",\n \"size\" : "
},
{
"path": "Ice/Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Ice/Assets.xcassets/ControlItemImages/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Ice/Assets.xcassets/ControlItemImages/Dot/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Ice/Assets.xcassets/ControlItemImages/Dot/DotFill.imageset/Contents.json",
"chars": 374,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n {\n \"filename\" : \"DotFill.png\",\n"
},
{
"path": "Ice/Assets.xcassets/ControlItemImages/Dot/DotStroke.imageset/Contents.json",
"chars": 376,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n {\n \"filename\" : \"DotStroke.png\""
},
{
"path": "Ice/Assets.xcassets/ControlItemImages/Ellipsis/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Ice/Assets.xcassets/ControlItemImages/Ellipsis/EllipsisFill.imageset/Contents.json",
"chars": 379,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n {\n \"filename\" : \"EllipsisFill.p"
},
{
"path": "Ice/Assets.xcassets/ControlItemImages/Ellipsis/EllipsisStroke.imageset/Contents.json",
"chars": 381,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n {\n \"filename\" : \"EllipsisStroke"
},
{
"path": "Ice/Assets.xcassets/ControlItemImages/IceCube/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Ice/Assets.xcassets/ControlItemImages/IceCube/IceCubeFill.imageset/Contents.json",
"chars": 378,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n {\n \"filename\" : \"IceCubeFill.pn"
},
{
"path": "Ice/Assets.xcassets/ControlItemImages/IceCube/IceCubeStroke.imageset/Contents.json",
"chars": 380,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n {\n \"filename\" : \"IceCubeStroke."
},
{
"path": "Ice/Assets.xcassets/DefaultLayoutBarColor.colorset/Contents.json",
"chars": 707,
"preview": "{\n \"colors\" : [\n {\n \"color\" : {\n \"color-space\" : \"display-p3\",\n \"components\" : {\n \"alpha"
},
{
"path": "Ice/Assets.xcassets/Warning.imageset/Contents.json",
"chars": 305,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n {\n \"filename\" : \"Warning.png\",\n"
},
{
"path": "Ice/Bridging/Bridging.swift",
"chars": 9475,
"preview": "//\n// Bridging.swift\n// Ice\n//\n\nimport Cocoa\n\n/// A namespace for bridged functionality.\nenum Bridging { }\n\n// MARK: -"
},
{
"path": "Ice/Bridging/Shims/Deprecated.swift",
"chars": 225,
"preview": "//\n// Deprecated.swift\n// Ice\n//\n\nimport ApplicationServices\n\n/// Returns a PSN for a given PID.\n@_silgen_name(\"GetPro"
},
{
"path": "Ice/Bridging/Shims/Private.swift",
"chars": 3288,
"preview": "//\n// Private.swift\n// Ice\n//\n\nimport CoreGraphics\n\n// MARK: - Bridged Types\n\ntypealias CGSConnectionID = Int32\ntypeal"
},
{
"path": "Ice/Events/EventManager.swift",
"chars": 18008,
"preview": "//\n// EventManager.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\n/// Manager for the various event monitors maintained"
},
{
"path": "Ice/Events/EventMonitors/GlobalEventMonitor.swift",
"chars": 2686,
"preview": "//\n// GlobalEventMonitor.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\n/// A type that monitors for events outside the"
},
{
"path": "Ice/Events/EventMonitors/LocalEventMonitor.swift",
"chars": 2707,
"preview": "//\n// LocalEventMonitor.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\n/// A type that monitors for events within the s"
},
{
"path": "Ice/Events/EventMonitors/RunLoopLocalEventMonitor.swift",
"chars": 3741,
"preview": "//\n// RunLoopLocalEventMonitor.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\nfinal class RunLoopLocalEventMonitor {\n "
},
{
"path": "Ice/Events/EventMonitors/UniversalEventMonitor.swift",
"chars": 2597,
"preview": "//\n// UniversalEventMonitor.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\n/// A type that monitors for local and globa"
},
{
"path": "Ice/Events/EventTap.swift",
"chars": 8335,
"preview": "//\n// EventTap.swift\n// Ice\n//\n\nimport Cocoa\n\n/// A type that receives system events from various locations within the"
},
{
"path": "Ice/Hotkeys/Hotkey.swift",
"chars": 3665,
"preview": "//\n// Hotkey.swift\n// Ice\n//\n\nimport Combine\n\n/// A combination of a key and modifiers that can be used to\n/// trigger"
},
{
"path": "Ice/Hotkeys/HotkeyAction.swift",
"chars": 1754,
"preview": "//\n// HotkeyAction.swift\n// Ice\n//\n\nenum HotkeyAction: String, Codable, CaseIterable {\n // Menu Bar Sections\n ca"
},
{
"path": "Ice/Hotkeys/HotkeyRegistry.swift",
"chars": 9301,
"preview": "//\n// HotkeyRegistry.swift\n// Ice\n//\n\nimport Carbon.HIToolbox\nimport Cocoa\nimport Combine\n\n/// An object that manages "
},
{
"path": "Ice/Hotkeys/KeyCode.swift",
"chars": 10063,
"preview": "//\n// KeyCode.swift\n// Ice\n//\n\nimport Carbon.HIToolbox\n\n/// Representation of a physical key on a keyboard.\nstruct Key"
},
{
"path": "Ice/Hotkeys/KeyCombination.swift",
"chars": 2720,
"preview": "//\n// KeyCombination.swift\n// Ice\n//\n\nimport Carbon.HIToolbox\nimport Cocoa\n\nstruct KeyCombination: Hashable {\n let "
},
{
"path": "Ice/Hotkeys/Modifiers.swift",
"chars": 4265,
"preview": "//\n// Modifiers.swift\n// Ice\n//\n\nimport Carbon.HIToolbox\nimport Cocoa\n\n/// A bit mask containing the modifier keys for"
},
{
"path": "Ice/Ice.entitlements",
"chars": 311,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Ice/Info.plist",
"chars": 372,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Ice/Main/AppDelegate.swift",
"chars": 3192,
"preview": "//\n// AppDelegate.swift\n// Ice\n//\n\nimport SwiftUI\n\n@MainActor\nfinal class AppDelegate: NSObject, NSApplicationDelegate"
},
{
"path": "Ice/Main/AppState.swift",
"chars": 10797,
"preview": "//\n// AppState.swift\n// Ice\n//\n\nimport Combine\nimport SwiftUI\n\n/// The model for app-wide state.\n@MainActor\nfinal clas"
},
{
"path": "Ice/Main/IceApp.swift",
"chars": 464,
"preview": "//\n// IceApp.swift\n// Ice\n//\n\nimport SwiftUI\n\n@main\nstruct IceApp: App {\n @NSApplicationDelegateAdaptor var appDele"
},
{
"path": "Ice/Main/Navigation/AppNavigationState.swift",
"chars": 431,
"preview": "//\n// AppNavigationState.swift\n// Ice\n//\n\nimport Combine\n\n/// The model for app-wide navigation.\n@MainActor\nfinal clas"
},
{
"path": "Ice/Main/Navigation/NavigationIdentifiers/NavigationIdentifier.swift",
"chars": 579,
"preview": "//\n// NavigationIdentifier.swift\n// Ice\n//\n\nimport SwiftUI\n\n/// A type that represents an identifier used for navigati"
},
{
"path": "Ice/Main/Navigation/NavigationIdentifiers/SettingsNavigationIdentifier.swift",
"chars": 395,
"preview": "//\n// SettingsNavigationIdentifier.swift\n// Ice\n//\n\n/// An identifier used for navigation in the settings interface.\ne"
},
{
"path": "Ice/MenuBar/Appearance/Configurations/MenuBarAppearanceConfigurationV1.swift",
"chars": 7305,
"preview": "//\n// MenuBarAppearanceConfigurationV1.swift\n// Ice\n//\n\nimport CoreGraphics\nimport Foundation\n\n/// Configuration for t"
},
{
"path": "Ice/MenuBar/Appearance/Configurations/MenuBarAppearanceConfigurationV2.swift",
"chars": 6660,
"preview": "//\n// MenuBarAppearanceConfigurationV2.swift\n// Ice\n//\n\nimport CoreGraphics\nimport Foundation\n\nstruct MenuBarAppearanc"
},
{
"path": "Ice/MenuBar/Appearance/MenuBarAppearanceEditor/MenuBarAppearanceEditor.swift",
"chars": 9700,
"preview": "//\n// MenuBarAppearanceEditor.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct MenuBarAppearanceEditor: View {\n enum Locatio"
},
{
"path": "Ice/MenuBar/Appearance/MenuBarAppearanceEditor/MenuBarAppearanceEditorPanel.swift",
"chars": 3703,
"preview": "//\n// MenuBarAppearanceEditorPanel.swift\n// Ice\n//\n\nimport Combine\nimport SwiftUI\n\n// MARK: - MenuBarAppearanceEditorP"
},
{
"path": "Ice/MenuBar/Appearance/MenuBarAppearanceEditor/MenuBarShapePicker.swift",
"chars": 6050,
"preview": "//\n// MenuBarShapePicker.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct MenuBarShapePicker: View {\n @EnvironmentObject var"
},
{
"path": "Ice/MenuBar/Appearance/MenuBarAppearanceManager.swift",
"chars": 5495,
"preview": "//\n// MenuBarAppearanceManager.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\n/// A manager for the appearance of the m"
},
{
"path": "Ice/MenuBar/Appearance/MenuBarOverlayPanel.swift",
"chars": 29649,
"preview": "//\n// MenuBarOverlayPanel.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\n// MARK: - Overlay Panel\n\n/// A subclass of `N"
},
{
"path": "Ice/MenuBar/Appearance/MenuBarShape.swift",
"chars": 1773,
"preview": "//\n// MenuBarShape.swift\n// Ice\n//\n\nimport CoreGraphics\n\n/// An end cap in a menu bar shape.\nenum MenuBarEndCap: Int, "
},
{
"path": "Ice/MenuBar/Appearance/MenuBarTintKind.swift",
"chars": 617,
"preview": "//\n// MenuBarTintKind.swift\n// Ice\n//\n\nimport SwiftUI\n\n/// A type that specifies how the menu bar is tinted.\nenum Menu"
},
{
"path": "Ice/MenuBar/ControlItem/ControlItem.swift",
"chars": 20390,
"preview": "//\n// ControlItem.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\n/// A status item that controls a section in the menu "
},
{
"path": "Ice/MenuBar/ControlItem/ControlItemImage.swift",
"chars": 3617,
"preview": "//\n// ControlItemImage.swift\n// Ice\n//\n\nimport Cocoa\n\n/// A Codable image for a control item.\nenum ControlItemImage: C"
},
{
"path": "Ice/MenuBar/ControlItem/ControlItemImageSet.swift",
"chars": 2465,
"preview": "//\n// ControlItemImageSet.swift\n// Ice\n//\n\n/// A named set of images that are used by control items.\n///\n/// An image "
},
{
"path": "Ice/MenuBar/MenuBarItems/MenuBarItem.swift",
"chars": 8015,
"preview": "//\n// MenuBarItem.swift\n// Ice\n//\n\nimport Cocoa\n\n// MARK: - MenuBarItem\n\n/// A representation of an item in the menu b"
},
{
"path": "Ice/MenuBar/MenuBarItems/MenuBarItemImageCache.swift",
"chars": 10080,
"preview": "//\n// MenuBarItemImageCache.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\n/// Cache for menu bar item images.\nfinal cl"
},
{
"path": "Ice/MenuBar/MenuBarItems/MenuBarItemInfo.swift",
"chars": 6884,
"preview": "//\n// MenuBarItemInfo.swift\n// Ice\n//\n\n/// A simplified version of a menu bar item.\nstruct MenuBarItemInfo: Hashable, "
},
{
"path": "Ice/MenuBar/MenuBarItems/MenuBarItemManager.swift",
"chars": 62421,
"preview": "//\n// MenuBarItemManager.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\n/// Manager for menu bar items.\n@MainActor\nfina"
},
{
"path": "Ice/MenuBar/MenuBarManager.swift",
"chars": 15317,
"preview": "//\n// MenuBarManager.swift\n// Ice\n//\n\nimport AXSwift\nimport Combine\nimport SwiftUI\n\n/// Manager for the state of the m"
},
{
"path": "Ice/MenuBar/MenuBarSection.swift",
"chars": 9928,
"preview": "//\n// MenuBarSection.swift\n// Ice\n//\n\nimport Cocoa\n\n/// A representation of a section in a menu bar.\n@MainActor\nfinal "
},
{
"path": "Ice/MenuBar/Search/MenuBarSearchPanel.swift",
"chars": 14536,
"preview": "//\n// MenuBarSearchPanel.swift\n// Ice\n//\n\nimport Combine\nimport Ifrit\nimport SwiftUI\n\n/// A panel that contains the me"
},
{
"path": "Ice/MenuBar/Spacing/MenuBarItemSpacingManager.swift",
"chars": 7905,
"preview": "//\n// MenuBarItemSpacingManager.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\n/// Manager for menu bar item spacing.\n@"
},
{
"path": "Ice/Permissions/Permission.swift",
"chars": 4807,
"preview": "//\n// Permission.swift\n// Ice\n//\n\nimport AXSwift\nimport Combine\nimport Cocoa\nimport ScreenCaptureKit\n\n// MARK: - Permi"
},
{
"path": "Ice/Permissions/PermissionsManager.swift",
"chars": 2200,
"preview": "//\n// PermissionsManager.swift\n// Ice\n//\n\nimport Combine\nimport Foundation\n\n/// A type that manages the permissions of"
},
{
"path": "Ice/Permissions/PermissionsView.swift",
"chars": 5974,
"preview": "//\n// PermissionsView.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct PermissionsView: View {\n @EnvironmentObject var permi"
},
{
"path": "Ice/Permissions/PermissionsWindow.swift",
"chars": 650,
"preview": "//\n// PermissionsWindow.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct PermissionsWindow: Scene {\n @ObservedObject var app"
},
{
"path": "Ice/Resources/Acknowledgements.rtf",
"chars": 12506,
"preview": "{\\rtf1\\ansi\\ansicpg1252\\cocoartf2761\n\\cocoatextscaling0\\cocoaplatform0{\\fonttbl\\f0\\fnil\\fcharset0 HelveticaNeue-Bold;\\f1"
},
{
"path": "Ice/Settings/SettingsManagers/AdvancedSettingsManager.swift",
"chars": 4355,
"preview": "//\n// AdvancedSettingsManager.swift\n// Ice\n//\n\nimport Combine\nimport Foundation\n\n@MainActor\nfinal class AdvancedSettin"
},
{
"path": "Ice/Settings/SettingsManagers/GeneralSettingsManager.swift",
"chars": 7445,
"preview": "//\n// GeneralSettingsManager.swift\n// Ice\n//\n\nimport Combine\nimport Foundation\n\n@MainActor\nfinal class GeneralSettings"
},
{
"path": "Ice/Settings/SettingsManagers/HotkeySettingsManager.swift",
"chars": 2531,
"preview": "//\n// HotkeySettingsManager.swift\n// Ice\n//\n\nimport Combine\nimport Foundation\n\n@MainActor\nfinal class HotkeySettingsMa"
},
{
"path": "Ice/Settings/SettingsManagers/SettingsManager.swift",
"chars": 1816,
"preview": "//\n// SettingsManager.swift\n// Ice\n//\n\nimport Combine\n\n@MainActor\nfinal class SettingsManager: ObservableObject {\n "
},
{
"path": "Ice/Settings/SettingsPanes/AboutSettingsPane.swift",
"chars": 5342,
"preview": "//\n// AboutSettingsPane.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct AboutSettingsPane: View {\n @EnvironmentObject var a"
},
{
"path": "Ice/Settings/SettingsPanes/AdvancedSettingsPane.swift",
"chars": 6023,
"preview": "//\n// AdvancedSettingsPane.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct AdvancedSettingsPane: View {\n @EnvironmentObject"
},
{
"path": "Ice/Settings/SettingsPanes/GeneralSettingsPane.swift",
"chars": 11573,
"preview": "//\n// GeneralSettingsPane.swift\n// Ice\n//\n\nimport LaunchAtLogin\nimport SwiftUI\n\nstruct GeneralSettingsPane: View {\n "
},
{
"path": "Ice/Settings/SettingsPanes/HotkeysSettingsPane.swift",
"chars": 2161,
"preview": "//\n// HotkeysSettingsPane.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct HotkeysSettingsPane: View {\n @EnvironmentObject v"
},
{
"path": "Ice/Settings/SettingsPanes/MenuBarAppearanceSettingsPane.swift",
"chars": 398,
"preview": "//\n// MenuBarAppearanceSettingsPane.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct MenuBarAppearanceSettingsPane: View {\n "
},
{
"path": "Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift",
"chars": 2568,
"preview": "//\n// MenuBarLayoutSettingsPane.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct MenuBarLayoutSettingsPane: View {\n @Environ"
},
{
"path": "Ice/Settings/SettingsView.swift",
"chars": 3054,
"preview": "//\n// SettingsView.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct SettingsView: View {\n @EnvironmentObject var navigationS"
},
{
"path": "Ice/Settings/SettingsWindow.swift",
"chars": 755,
"preview": "//\n// SettingsWindow.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct SettingsWindow: Scene {\n @ObservedObject var appState:"
},
{
"path": "Ice/Swizzling/NSSplitViewItem+swizzledCanCollapse.swift",
"chars": 1020,
"preview": "//\n// NSSplitViewItem+swizzledCanCollapse.swift\n// Ice\n//\n\nimport Cocoa\n\nextension NSSplitViewItem {\n @nonobjc priv"
},
{
"path": "Ice/UI/HotkeyRecorder/HotkeyRecorder.swift",
"chars": 4588,
"preview": "//\n// HotkeyRecorder.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct HotkeyRecorder<Label: View>: View {\n @StateObject priv"
},
{
"path": "Ice/UI/HotkeyRecorder/HotkeyRecorderModel.swift",
"chars": 1974,
"preview": "//\n// HotkeyRecorderModel.swift\n// Ice\n//\n\nimport Combine\nimport SwiftUI\n\n@MainActor\nfinal class HotkeyRecorderModel: "
},
{
"path": "Ice/UI/IceBar/IceBar.swift",
"chars": 16864,
"preview": "//\n// IceBar.swift\n// Ice\n//\n\nimport Combine\nimport SwiftUI\n\n// MARK: - IceBarPanel\n\nfinal class IceBarPanel: NSPanel "
},
{
"path": "Ice/UI/IceBar/IceBarColorManager.swift",
"chars": 4426,
"preview": "//\n// IceBarColorManager.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\nfinal class IceBarColorManager: ObservableObjec"
},
{
"path": "Ice/UI/IceBar/IceBarLocation.swift",
"chars": 692,
"preview": "//\n// IceBarLocation.swift\n// Ice\n//\n\nimport SwiftUI\n\n/// Locations where the Ice Bar can appear.\nenum IceBarLocation:"
},
{
"path": "Ice/UI/IceUI/IceForm.swift",
"chars": 2203,
"preview": "//\n// IceForm.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct IceForm<Content: View>: View {\n @Environment(\\.isScrollEnable"
},
{
"path": "Ice/UI/IceUI/IceGroupBox.swift",
"chars": 2435,
"preview": "//\n// IceGroupBox.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct IceGroupBox<Header: View, Content: View, Footer: View>: View"
},
{
"path": "Ice/UI/IceUI/IceLabeledContent.swift",
"chars": 843,
"preview": "//\n// IceLabeledContent.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct IceLabeledContent<Label: View, Content: View>: View {\n"
},
{
"path": "Ice/UI/IceUI/IceMenu.swift",
"chars": 1706,
"preview": "//\n// IceMenu.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct IceMenu<Title: View, Label: View, Content: View>: View {\n pri"
},
{
"path": "Ice/UI/IceUI/IcePicker.swift",
"chars": 1215,
"preview": "//\n// IcePicker.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct IcePicker<Label: View, SelectionValue: Hashable, Content: View"
},
{
"path": "Ice/UI/IceUI/IceSection.swift",
"chars": 3861,
"preview": "//\n// IceSection.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct IceSectionOptions: OptionSet {\n let rawValue: Int\n\n sta"
},
{
"path": "Ice/UI/IceUI/IceSlider.swift",
"chars": 1640,
"preview": "//\n// IceSlider.swift\n// Ice\n//\n\nimport CompactSlider\nimport SwiftUI\n\nstruct IceSlider<Value: BinaryFloatingPoint, Val"
},
{
"path": "Ice/UI/LayoutBar/LayoutBar.swift",
"chars": 1808,
"preview": "//\n// LayoutBar.swift\n// Ice\n//\n\nimport SwiftUI\n\nstruct LayoutBar: View {\n private struct Representable: NSViewRepr"
},
{
"path": "Ice/UI/LayoutBar/LayoutBarContainer.swift",
"chars": 11252,
"preview": "//\n// LayoutBarContainer.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\n/// A container for the items in the menu bar l"
},
{
"path": "Ice/UI/LayoutBar/LayoutBarItemView.swift",
"chars": 8387,
"preview": "//\n// LayoutBarItemView.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\n// MARK: - LayoutBarItemView\n\n/// A view that di"
},
{
"path": "Ice/UI/LayoutBar/LayoutBarPaddingView.swift",
"chars": 5696,
"preview": "//\n// LayoutBarPaddingView.swift\n// Ice\n//\n\nimport Cocoa\nimport Combine\n\n/// A Cocoa view that manages the menu bar la"
},
{
"path": "Ice/UI/LayoutBar/LayoutBarScrollView.swift",
"chars": 3160,
"preview": "//\n// LayoutBarScrollView.swift\n// Ice\n//\n\nimport Cocoa\n\nfinal class LayoutBarScrollView: NSScrollView {\n private l"
},
{
"path": "Ice/UI/Pickers/CustomColorPicker/CustomColorPicker.swift",
"chars": 3684,
"preview": "//\n// CustomColorPicker.swift\n// Ice\n//\n\nimport Combine\nimport SwiftUI\n\nstruct CustomColorPicker: NSViewRepresentable "
},
{
"path": "Ice/UI/Pickers/CustomGradientPicker/ColorStop.swift",
"chars": 1211,
"preview": "//\n// ColorStop.swift\n// Ice\n//\n\nimport CoreGraphics\n\n/// A color stop in a gradient.\nstruct ColorStop: Hashable {\n "
},
{
"path": "Ice/UI/Pickers/CustomGradientPicker/CustomGradient.swift",
"chars": 3321,
"preview": "//\n// CustomGradient.swift\n// Ice\n//\n\nimport SwiftUI\n\n/// A custom gradient for use with a ``GradientPicker``.\nstruct "
},
{
"path": "Ice/UI/Pickers/CustomGradientPicker/CustomGradientPicker.swift",
"chars": 14022,
"preview": "//\n// CustomGradientPicker.swift\n// Ice\n//\n\nimport Combine\nimport SwiftUI\n\nstruct CustomGradientPicker: View {\n @Bi"
},
{
"path": "Ice/UI/Shapes/AnyInsettableShape.swift",
"chars": 547,
"preview": "//\n// AnyInsettableShape.swift\n// Ice\n//\n\nimport SwiftUI\n\n/// A type-erased insettable shape.\nstruct AnyInsettableShap"
},
{
"path": "Ice/UI/ViewModifiers/BottomBar.swift",
"chars": 580,
"preview": "//\n// BottomBar.swift\n// Ice\n//\n\nimport SwiftUI\n\nextension View {\n /// Adds the given view as a bottom bar to the c"
},
{
"path": "Ice/UI/ViewModifiers/ErasedToAnyView.swift",
"chars": 220,
"preview": "//\n// ErasedToAnyView.swift\n// Ice\n//\n\nimport SwiftUI\n\nextension View {\n /// Returns a view that has been erased to"
},
{
"path": "Ice/UI/ViewModifiers/LayoutBarStyle.swift",
"chars": 1961,
"preview": "//\n// LayoutBarStyle.swift\n// Ice\n//\n\nimport SwiftUI\n\nextension View {\n /// Returns a view that is drawn in the sty"
},
{
"path": "Ice/UI/ViewModifiers/LocalEventMonitorModifier.swift",
"chars": 1413,
"preview": "//\n// LocalEventMonitorModifier.swift\n// Ice\n//\n\nimport SwiftUI\n\nprivate final class LocalEventMonitorModifierState: O"
},
{
"path": "Ice/UI/ViewModifiers/OnFrameChange.swift",
"chars": 1889,
"preview": "//\n// OnFrameChange.swift\n// Ice\n//\n\nimport SwiftUI\n\nprivate struct FramePreferenceKey: PreferenceKey {\n static let"
},
{
"path": "Ice/UI/ViewModifiers/OnKeyDown.swift",
"chars": 452,
"preview": "//\n// OnKeyDown.swift\n// Ice\n//\n\nimport SwiftUI\n\nextension View {\n /// Returns a view that performs the given actio"
},
{
"path": "Ice/UI/ViewModifiers/Once.swift",
"chars": 659,
"preview": "//\n// Once.swift\n// Ice\n//\n\nimport SwiftUI\n\nprivate struct OnceModifier: ViewModifier {\n @State private var hasAppe"
},
{
"path": "Ice/UI/ViewModifiers/ReadWindow.swift",
"chars": 1586,
"preview": "//\n// ReadWindow.swift\n// Ice\n//\n\nimport Combine\nimport SwiftUI\n\nprivate struct WindowReader: NSViewRepresentable {\n "
},
{
"path": "Ice/UI/ViewModifiers/RemoveSidebarToggle.swift",
"chars": 299,
"preview": "//\n// RemoveSidebarToggle.swift\n// Ice\n//\n\nimport SwiftUI\n\nextension View {\n /// Removes the sidebar toggle button "
},
{
"path": "Ice/UI/Views/AnnotationView.swift",
"chars": 6945,
"preview": "//\n// AnnotationView.swift\n// Ice\n//\n\nimport SwiftUI\n\n/// A view that displays content as an annotation below a parent"
},
{
"path": "Ice/UI/Views/BetaBadge.swift",
"chars": 412,
"preview": "//\n// BetaBadge.swift\n// Ice\n//\n\nimport SwiftUI\n\n/// A view that displays a badge indicating a beta feature.\nstruct Be"
},
{
"path": "Ice/UI/Views/SectionedList.swift",
"chars": 6823,
"preview": "//\n// SectionedList.swift\n// Ice\n//\n\nimport SwiftUI\n\n// MARK: - SectionedList\n\n/// A scrollable list of items broken u"
},
{
"path": "Ice/UI/Views/VisualEffectView.swift",
"chars": 786,
"preview": "//\n// VisualEffectView.swift\n// Ice\n//\n\nimport SwiftUI\n\n/// A SwiftUI view that wraps an `NSVisualEffectView`.\nstruct "
},
{
"path": "Ice/Updates/UpdatesManager.swift",
"chars": 4275,
"preview": "//\n// UpdatesManager.swift\n// Ice\n//\n\nimport Sparkle\nimport SwiftUI\n\n/// Manager for app updates.\n@MainActor\nfinal cla"
},
{
"path": "Ice/UserNotifications/UserNotificationIdentifier.swift",
"chars": 176,
"preview": "//\n// UserNotificationIdentifier.swift\n// Ice\n//\n\n/// An identifier for a user notification.\nenum UserNotificationIden"
},
{
"path": "Ice/UserNotifications/UserNotificationManager.swift",
"chars": 2753,
"preview": "//\n// UserNotificationManager.swift\n// Ice\n//\n\nimport UserNotifications\n\n/// Manager for user notifications.\n@MainActo"
},
{
"path": "Ice/Utilities/BindingExposable.swift",
"chars": 1350,
"preview": "//\n// BindingExposable.swift\n// Ice\n//\n\nimport SwiftUI\n\n/// A type that exposes its writable properties as bindings.\np"
},
{
"path": "Ice/Utilities/CodableColor.swift",
"chars": 2547,
"preview": "//\n// CodableColor.swift\n// Ice\n//\n\nimport CoreGraphics\nimport Foundation\n\n/// A Codable wrapper around a CGColor.\nstr"
},
{
"path": "Ice/Utilities/Constants.swift",
"chars": 994,
"preview": "//\n// Constants.swift\n// Ice\n//\n\nimport Foundation\n\nenum Constants {\n // swiftlint:disable force_unwrapping\n ///"
},
{
"path": "Ice/Utilities/Defaults.swift",
"chars": 6968,
"preview": "//\n// Defaults.swift\n// Ice\n//\n\nimport Foundation\n\nenum Defaults {\n /// Returns a dictionary containing the keys an"
},
{
"path": "Ice/Utilities/Extensions.swift",
"chars": 17204,
"preview": "//\n// Extensions.swift\n// Ice\n//\n\nimport Combine\nimport SwiftUI\n\n// MARK: - Bundle\n\nextension Bundle {\n /// The bun"
},
{
"path": "Ice/Utilities/IconResource.swift",
"chars": 761,
"preview": "//\n// IconResource.swift\n// Ice\n//\n\nimport SwiftUI\n\n/// A type that produces a view representing an icon.\nenum IconRes"
},
{
"path": "Ice/Utilities/Injection.swift",
"chars": 1106,
"preview": "//\n// Injection.swift\n// Ice\n//\n\n/// Updates the given value in place using a closure.\n///\n/// Use this function to re"
},
{
"path": "Ice/Utilities/LocalizedErrorWrapper.swift",
"chars": 1081,
"preview": "//\n// LocalizedErrorWrapper.swift\n// Ice\n//\n\nimport Foundation\n\n/// A type that wraps the information of any error ins"
},
{
"path": "Ice/Utilities/Logging.swift",
"chars": 989,
"preview": "//\n// Logging.swift\n// Ice\n//\n\nimport OSLog\n\n/// A type that encapsulates logging behavior for Ice.\nstruct Logger {\n "
},
{
"path": "Ice/Utilities/MigrationManager.swift",
"chars": 14666,
"preview": "//\n// MigrationManager.swift\n// Ice\n//\n\nimport Cocoa\n\n@MainActor\nstruct MigrationManager {\n let appState: AppState\n"
},
{
"path": "Ice/Utilities/MouseCursor.swift",
"chars": 1798,
"preview": "//\n// MouseCursor.swift\n// Ice\n//\n\nimport CoreGraphics\n\n/// A namespace for mouse cursor operations.\nenum MouseCursor "
},
{
"path": "Ice/Utilities/Notifications.swift",
"chars": 293,
"preview": "//\n// Notifications.swift\n// Ice\n//\n\nimport Foundation\n\nextension DistributedNotificationCenter {\n /// A notificati"
},
{
"path": "Ice/Utilities/ObjectStorage.swift",
"chars": 2326,
"preview": "//\n// ObjectStorage.swift\n// Ice\n//\n\nimport ObjectiveC\n\n// MARK: - Object Storage\n\n/// A type that uses the Objective-"
},
{
"path": "Ice/Utilities/Predicates.swift",
"chars": 5107,
"preview": "//\n// Predicates.swift\n// Ice\n//\n\nimport Cocoa\n\n/// A namespace for predicates.\nenum Predicates<Input> {\n /// A thr"
},
{
"path": "Ice/Utilities/RehideStrategy.swift",
"chars": 681,
"preview": "//\n// RehideStrategy.swift\n// Ice\n//\n\nimport SwiftUI\n\n/// A type that determines how the auto-rehide feature works.\nen"
},
{
"path": "Ice/Utilities/ScreenCapture.swift",
"chars": 4243,
"preview": "//\n// ScreenCapture.swift\n// Ice\n//\n\nimport CoreGraphics\nimport ScreenCaptureKit\n\n/// A namespace for screen capture o"
},
{
"path": "Ice/Utilities/StatusItemDefaults.swift",
"chars": 1864,
"preview": "//\n// StatusItemDefaults.swift\n// Ice\n//\n\nimport Cocoa\n\n// MARK: - StatusItemDefaults\n\n/// Proxy getters and setters f"
},
{
"path": "Ice/Utilities/SystemAppearance.swift",
"chars": 2610,
"preview": "//\n// SystemAppearance.swift\n// Ice\n//\n\nimport SwiftUI\n\n/// A value corresponding to a light or dark appearance.\nenum "
},
{
"path": "Ice/Utilities/TaskTimeout.swift",
"chars": 3023,
"preview": "//\n// TaskTimeout.swift\n// Ice\n//\n\nimport Foundation\n\nextension Task where Failure == any Error {\n /// Runs the giv"
},
{
"path": "Ice/Utilities/WindowInfo.swift",
"chars": 12119,
"preview": "//\n// WindowInfo.swift\n// Ice\n//\n\nimport Cocoa\n\n/// Information for a window.\nstruct WindowInfo {\n /// The window i"
},
{
"path": "Ice.xcodeproj/project.pbxproj",
"chars": 16844,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 70;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "Ice.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 135,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:\">\n </FileRef"
},
{
"path": "Ice.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
"chars": 238,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Ice.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved",
"chars": 1449,
"preview": "{\n \"originHash\" : \"a7567d11f06745371832127a8ce2132148ef6a89fb55ecc72d6c313b688387fa\",\n \"pins\" : [\n {\n \"identit"
},
{
"path": "Ice.xcodeproj/xcshareddata/xcschemes/Ice.xcscheme",
"chars": 2806,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"1640\"\n version = \"1.7\">\n <BuildAction\n "
},
{
"path": "LICENSE",
"chars": 35100,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "README.md",
"chars": 4319,
"preview": "<div align=\"center\">\n <img src=\"Ice/Assets.xcassets/AppIcon.appiconset/icon_256x256.png\" width=200 height=200>\n <h"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the jordanbaird/Ice GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 152 files (677.5 KB), approximately 154.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.