├── Docs ├── Contents │ ├── Overview.pxd │ ├── 简体中文 │ │ └── Overview.png │ ├── English │ │ └── Overview.png │ ├── Icons │ │ ├── Dark │ │ │ ├── Dot.png │ │ │ ├── Line.png │ │ │ └── DottedLine.png │ │ └── Light │ │ │ ├── Dot.png │ │ │ ├── Line.png │ │ │ └── DottedLine.png │ └── Status Bar-Raw.png ├── ADD_A_LOCALIZATION.md └── 简体中文.md ├── Abyssal ├── Assets.xcassets │ ├── .DS_Store │ ├── Themes │ │ ├── .DS_Store │ │ ├── Abyssal │ │ │ ├── Dot.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── Line.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── DottedLine.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Colons │ │ │ ├── Colon.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── TheFace │ │ │ ├── Face.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Droplets │ │ │ ├── Drops.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── LDrop.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── MDrop.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── NotSoHappy │ │ │ ├── Cat.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── Sad.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── Amazed.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── Happy.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── Pale.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── HiddenBar │ │ │ ├── ic_left.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── ic_line.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── ic_right.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── ic_line_translucent.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Implication │ │ │ ├── Since.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── Implies.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── Therefore.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── MetresAway │ │ │ ├── Line.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── MetreLine.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Approaching │ │ │ ├── Primary.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── Secondary.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── Tertiary.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── ElectroDiagram │ │ │ ├── Diagram.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── DiagramHead.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ ├── DiagramTail.imageset │ │ │ │ ├── 2x.png │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── icon_128x128.png │ │ ├── icon_256x256.png │ │ ├── icon_512x512.png │ │ ├── icon_128x128@2x@2x.png │ │ ├── icon_256x256@2x@2x.png │ │ ├── icon_512x512@2x@2x.png │ │ └── Contents.json │ ├── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── Extensions │ ├── Foundation │ │ ├── Bool+Extensions.swift │ │ ├── Double+Extensions.swift │ │ ├── URL+Extensions.swift │ │ ├── Bundle+Extensions.swift │ │ ├── Range+Extensions.swift │ │ └── DispatchQueue+Extensions.swift │ ├── AppKit │ │ ├── NSStatusItem+Extension.swift │ │ ├── NSSlider+Extension.swift │ │ ├── NSScreen+Extensions.swift │ │ └── NSRect+Extension.swift │ ├── KeyboardShortcuts+Extensions.swift │ ├── SwiftUI │ │ └── EnvironmentValues+Extention.swift │ ├── Defaults+Extensions.swift │ └── Defaults+Structures.swift ├── Info.plist ├── Managers │ ├── ApplicationMenuManager.swift │ ├── AppManager.swift │ ├── MathHelper.swift │ ├── ExternalMenuBarManager.swift │ ├── ScreenManager.swift │ └── ActivationPolicyManager.swift ├── Settings │ ├── Views │ │ ├── UnifiedVStack.swift │ │ ├── EmptyFormWrapper.swift │ │ ├── PopoverHint.swift │ │ ├── TipWrapper.swift │ │ └── Box.swift │ ├── Sections │ │ ├── ShortcutsSection.swift │ │ ├── GeneralSection.swift │ │ ├── AdvancedSection.swift │ │ ├── FunctionsSection.swift │ │ ├── BehaviorsSection.swift │ │ └── ModifierSection.swift │ ├── Tips │ │ ├── TipDeadZoneTitle.swift │ │ ├── TipFeedbackTitle.swift │ │ ├── TipTimeoutTitle.swift │ │ └── TipTitle.swift │ ├── Controls │ │ ├── Toggles │ │ │ ├── StartsWithMacOSControl.swift │ │ │ ├── RespectNotchControl.swift │ │ │ ├── AlwaysHideAreaControl.swift │ │ │ └── AutoShowsControl.swift │ │ ├── ShortcutsControl.swift │ │ ├── Popups │ │ │ ├── MenuBarOverrideControl.swift │ │ │ ├── ThemeControl.swift │ │ │ └── ActiveStrategyControl.swift │ │ └── Sliders │ │ │ ├── TimeoutControl.swift │ │ │ ├── FeedbackControl.swift │ │ │ └── DeadZoneControl.swift │ ├── SettingsViewController.swift │ ├── SettingsView.swift │ ├── SettingsVersionView.swift │ └── SettingsTrafficsView.swift ├── Miscellaneous │ ├── Formattable │ │ ├── UnitFormat.swift │ │ ├── TimeFormat.swift │ │ └── Formattable.swift │ ├── WithIntermediateState.swift │ ├── ObservationTrackingStream.swift │ ├── Events │ │ └── EventMonitor.swift │ └── WindowInfo.swift ├── Models │ ├── MouseModel.swift │ ├── KeyboardModel.swift │ └── VersionModel.swift ├── main.swift ├── InfoPlist.xcstrings ├── Contents │ ├── Icon.swift │ └── Separator.swift ├── mul.lproj │ └── Main.xcstrings ├── AppDelegate.swift └── Menu Bar │ └── StatusBarController.swift ├── Design └── Themes │ ├── Abyssal │ ├── Dot │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── Line │ │ ├── 2x.png │ │ └── 2x.pxd │ └── DottedLine │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── Colons │ └── Colon │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── TheFace │ └── Face │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── Droplets │ ├── Drops │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── LDrop │ │ ├── 2x.png │ │ └── 2x.pxd │ └── MDrop │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── MetresAway │ ├── Line │ │ ├── 2x.png │ │ └── 2x.pxd │ └── MetreLine │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── NotSoHappy │ ├── Cat │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── Pale │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── Sad │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── Amazed │ │ ├── 2x.png │ │ └── 2x.pxd │ └── Happy │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── HiddenBar │ ├── ic_left │ │ └── 2x.png │ ├── ic_line │ │ └── 2x.png │ ├── ic_right │ │ └── 2x.png │ └── ic_line_translucent │ │ └── 2x.png │ ├── Implication │ ├── Since │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── Implies │ │ ├── 2x.png │ │ └── 2x.pxd │ └── Therefore │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── Approaching │ ├── Primary │ │ ├── 2x.png │ │ └── 2x.pxd │ ├── Tertiary │ │ ├── 2x.png │ │ └── 2x.pxd │ └── Secondary │ │ ├── 2x.png │ │ └── 2x.pxd │ └── Electrocardiagram │ ├── Diagram │ ├── 2x.png │ └── 2x.pxd │ ├── DiagramHead │ ├── 2x.png │ └── 2x.pxd │ └── DiagramTail │ ├── 2x.png │ └── 2x.pxd ├── .gitignore ├── Abyssal.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ ├── Kr.xcuserdatad │ │ ├── IDEFindNavigatorScopes.plist │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ │ └── litekr.xcuserdatad │ │ └── WorkspaceSettings.xcsettings ├── xcuserdata │ ├── Kr.xcuserdatad │ │ ├── xcdebugger │ │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ └── litekr.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist └── xcshareddata │ └── xcschemes │ ├── AbyssalTests.xcscheme │ └── Abyssal.xcscheme ├── Abyssal.entitlements ├── AbyssalTests └── Miscellaneous │ └── VersionTests.swift └── README.md /Docs/Contents/Overview.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Docs/Contents/Overview.pxd -------------------------------------------------------------------------------- /Docs/Contents/简体中文/Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Docs/Contents/简体中文/Overview.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/.DS_Store -------------------------------------------------------------------------------- /Design/Themes/Abyssal/Dot/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Abyssal/Dot/2x.png -------------------------------------------------------------------------------- /Design/Themes/Abyssal/Dot/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Abyssal/Dot/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/Abyssal/Line/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Abyssal/Line/2x.png -------------------------------------------------------------------------------- /Design/Themes/Abyssal/Line/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Abyssal/Line/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/Colons/Colon/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Colons/Colon/2x.png -------------------------------------------------------------------------------- /Design/Themes/Colons/Colon/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Colons/Colon/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/TheFace/Face/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/TheFace/Face/2x.png -------------------------------------------------------------------------------- /Design/Themes/TheFace/Face/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/TheFace/Face/2x.pxd -------------------------------------------------------------------------------- /Docs/Contents/English/Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Docs/Contents/English/Overview.png -------------------------------------------------------------------------------- /Docs/Contents/Icons/Dark/Dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Docs/Contents/Icons/Dark/Dot.png -------------------------------------------------------------------------------- /Docs/Contents/Icons/Dark/Line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Docs/Contents/Icons/Dark/Line.png -------------------------------------------------------------------------------- /Docs/Contents/Icons/Light/Dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Docs/Contents/Icons/Light/Dot.png -------------------------------------------------------------------------------- /Docs/Contents/Icons/Light/Line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Docs/Contents/Icons/Light/Line.png -------------------------------------------------------------------------------- /Docs/Contents/Status Bar-Raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Docs/Contents/Status Bar-Raw.png -------------------------------------------------------------------------------- /Design/Themes/Droplets/Drops/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Droplets/Drops/2x.png -------------------------------------------------------------------------------- /Design/Themes/Droplets/Drops/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Droplets/Drops/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/Droplets/LDrop/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Droplets/LDrop/2x.png -------------------------------------------------------------------------------- /Design/Themes/Droplets/LDrop/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Droplets/LDrop/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/Droplets/MDrop/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Droplets/MDrop/2x.png -------------------------------------------------------------------------------- /Design/Themes/Droplets/MDrop/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Droplets/MDrop/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/MetresAway/Line/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/MetresAway/Line/2x.png -------------------------------------------------------------------------------- /Design/Themes/MetresAway/Line/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/MetresAway/Line/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/NotSoHappy/Cat/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/NotSoHappy/Cat/2x.png -------------------------------------------------------------------------------- /Design/Themes/NotSoHappy/Cat/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/NotSoHappy/Cat/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/NotSoHappy/Pale/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/NotSoHappy/Pale/2x.png -------------------------------------------------------------------------------- /Design/Themes/NotSoHappy/Pale/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/NotSoHappy/Pale/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/NotSoHappy/Sad/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/NotSoHappy/Sad/2x.png -------------------------------------------------------------------------------- /Design/Themes/NotSoHappy/Sad/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/NotSoHappy/Sad/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/Abyssal/DottedLine/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Abyssal/DottedLine/2x.png -------------------------------------------------------------------------------- /Design/Themes/Abyssal/DottedLine/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Abyssal/DottedLine/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/HiddenBar/ic_left/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/HiddenBar/ic_left/2x.png -------------------------------------------------------------------------------- /Design/Themes/HiddenBar/ic_line/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/HiddenBar/ic_line/2x.png -------------------------------------------------------------------------------- /Design/Themes/HiddenBar/ic_right/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/HiddenBar/ic_right/2x.png -------------------------------------------------------------------------------- /Design/Themes/Implication/Since/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Implication/Since/2x.png -------------------------------------------------------------------------------- /Design/Themes/Implication/Since/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Implication/Since/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/NotSoHappy/Amazed/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/NotSoHappy/Amazed/2x.png -------------------------------------------------------------------------------- /Design/Themes/NotSoHappy/Amazed/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/NotSoHappy/Amazed/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/NotSoHappy/Happy/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/NotSoHappy/Happy/2x.png -------------------------------------------------------------------------------- /Design/Themes/NotSoHappy/Happy/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/NotSoHappy/Happy/2x.pxd -------------------------------------------------------------------------------- /Docs/Contents/Icons/Dark/DottedLine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Docs/Contents/Icons/Dark/DottedLine.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/.DS_Store -------------------------------------------------------------------------------- /Design/Themes/Approaching/Primary/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Approaching/Primary/2x.png -------------------------------------------------------------------------------- /Design/Themes/Approaching/Primary/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Approaching/Primary/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/Approaching/Tertiary/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Approaching/Tertiary/2x.png -------------------------------------------------------------------------------- /Design/Themes/Approaching/Tertiary/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Approaching/Tertiary/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/Implication/Implies/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Implication/Implies/2x.png -------------------------------------------------------------------------------- /Design/Themes/Implication/Implies/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Implication/Implies/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/MetresAway/MetreLine/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/MetresAway/MetreLine/2x.png -------------------------------------------------------------------------------- /Design/Themes/MetresAway/MetreLine/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/MetresAway/MetreLine/2x.pxd -------------------------------------------------------------------------------- /Docs/Contents/Icons/Light/DottedLine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Docs/Contents/Icons/Light/DottedLine.png -------------------------------------------------------------------------------- /Design/Themes/Approaching/Secondary/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Approaching/Secondary/2x.png -------------------------------------------------------------------------------- /Design/Themes/Approaching/Secondary/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Approaching/Secondary/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/Implication/Therefore/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Implication/Therefore/2x.png -------------------------------------------------------------------------------- /Design/Themes/Implication/Therefore/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Implication/Therefore/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/Electrocardiagram/Diagram/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Electrocardiagram/Diagram/2x.png -------------------------------------------------------------------------------- /Design/Themes/Electrocardiagram/Diagram/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Electrocardiagram/Diagram/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/Electrocardiagram/DiagramHead/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Electrocardiagram/DiagramHead/2x.png -------------------------------------------------------------------------------- /Design/Themes/Electrocardiagram/DiagramHead/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Electrocardiagram/DiagramHead/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/Electrocardiagram/DiagramTail/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Electrocardiagram/DiagramTail/2x.png -------------------------------------------------------------------------------- /Design/Themes/Electrocardiagram/DiagramTail/2x.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/Electrocardiagram/DiagramTail/2x.pxd -------------------------------------------------------------------------------- /Design/Themes/HiddenBar/ic_line_translucent/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Design/Themes/HiddenBar/ic_line_translucent/2x.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | *.DS_Store 3 | 4 | # Xcode 5 | *.pbxuser 6 | *.mode1v3 7 | *.mode2v3 8 | *.perspectivev3 9 | *.xcuserstate 10 | xcuserdata 11 | Build/ 12 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Abyssal/Dot.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/Abyssal/Dot.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Abyssal/Line.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/Abyssal/Line.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Colons/Colon.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/Colons/Colon.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/TheFace/Face.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/TheFace/Face.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Droplets/Drops.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/Droplets/Drops.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Droplets/LDrop.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/Droplets/LDrop.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Droplets/MDrop.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/Droplets/MDrop.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/NotSoHappy/Cat.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/NotSoHappy/Cat.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/NotSoHappy/Sad.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/NotSoHappy/Sad.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/HiddenBar/ic_left.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/HiddenBar/ic_left.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/HiddenBar/ic_line.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/HiddenBar/ic_line.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Implication/Since.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/Implication/Since.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/MetresAway/Line.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/MetresAway/Line.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/NotSoHappy/Amazed.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/NotSoHappy/Amazed.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/NotSoHappy/Happy.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/NotSoHappy/Happy.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/NotSoHappy/Pale.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/NotSoHappy/Pale.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Abyssal/DottedLine.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/Abyssal/DottedLine.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Approaching/Primary.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/Approaching/Primary.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/HiddenBar/ic_right.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/HiddenBar/ic_right.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Implication/Implies.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/Implication/Implies.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Approaching/Secondary.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/Approaching/Secondary.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Approaching/Tertiary.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/Approaching/Tertiary.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/ElectroDiagram/Diagram.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/ElectroDiagram/Diagram.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Implication/Therefore.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/Implication/Therefore.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/MetresAway/MetreLine.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/MetresAway/MetreLine.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/ElectroDiagram/DiagramHead.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/ElectroDiagram/DiagramHead.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/ElectroDiagram/DiagramTail.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/ElectroDiagram/DiagramTail.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "compression-type" : "automatic" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/HiddenBar/ic_line_translucent.imageset/2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cement-Labs/Abyssal/HEAD/Abyssal/Assets.xcassets/Themes/HiddenBar/ic_line_translucent.imageset/2x.png -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Abyssal/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Colons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Droplets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/TheFace/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Approaching/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/HiddenBar/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Implication/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/MetresAway/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/NotSoHappy/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Docs/ADD_A_LOCALIZATION.md: -------------------------------------------------------------------------------- 1 | # Add a Localization for Abyssal 2 | 3 | In order to contribute to **Abyssal** by localizing it, you need to follow the steps below. 4 | 5 | ## The Abyssal App 6 | 7 | ## The Documentations 8 | -------------------------------------------------------------------------------- /Abyssal.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/ElectroDiagram/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Abyssal.xcodeproj/xcuserdata/Kr.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /Abyssal.xcodeproj/xcuserdata/litekr.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /Abyssal.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Abyssal.xcodeproj/project.xcworkspace/xcuserdata/Kr.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Abyssal.xcodeproj/project.xcworkspace/xcuserdata/Kr.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Abyssal/Extensions/Foundation/Bool+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bool+Extensions.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/22. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bool { 11 | static func &=(lhs: inout Bool, rhs: Bool) { 12 | lhs = lhs && rhs 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Abyssal/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSServices 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Abyssal/Managers/ApplicationMenuManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApplicationMenuManager.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/7/2. 6 | // 7 | 8 | import AppKit 9 | 10 | struct ApplicationMenuManager { 11 | static func apply(_ menu: NSMenu?) { 12 | NSApp.mainMenu = menu 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Abyssal.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Abyssal/Extensions/AppKit/NSStatusItem+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSStatusBarButton+Extension.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2023/6/19. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSStatusItem { 11 | var origin: CGPoint? { 12 | return self.button?.window?.frame.origin 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Abyssal.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Abyssal/Extensions/AppKit/NSSlider+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSSlider+Extension.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2023/10/13. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | extension NSSlider { 12 | @objc dynamic var knobRect: NSRect { 13 | (cell as? NSSliderCell)?.knobRect(flipped: false) ?? visibleRect 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Abyssal/Extensions/Foundation/Double+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double+Extensions.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Double { 11 | func rounded(digits: Int) -> Double { 12 | let multiplier = pow(10.0, Double(digits)) 13 | return (self * multiplier).rounded() / multiplier 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Abyssal/Extensions/AppKit/NSScreen+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSScreen+Extensions.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSScreen { 11 | var displayID: CGDirectDisplayID? { 12 | return deviceDescription[NSDeviceDescriptionKey(rawValue: "NSScreenNumber")] as? CGDirectDisplayID 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Abyssal/Settings/Views/UnifiedVStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnifiedVStack.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UnifiedVStack: View where Content: View { 11 | @ViewBuilder var content: () -> Content 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: 21) { 15 | content() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Abyssal/Managers/AppManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppManager.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/26. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | @Observable 12 | class AppManager { 13 | static var frontmost: NSRunningApplication { 14 | NSWorkspace.shared.frontmostApplication ?? .current 15 | } 16 | 17 | static var fronsmostName: String { 18 | frontmost.localizedName ?? String(frontmost.hashValue) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Abyssal/Miscellaneous/Formattable/UnitFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnitFormat.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UnitFormat: DoubleFormattable { 11 | case percentage 12 | case pixel 13 | 14 | var format: String { 15 | switch self { 16 | case .percentage: 17 | .init(localized: "%g%%") 18 | case .pixel: 19 | .init(localized: "%g Pixels") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Abyssal/Extensions/KeyboardShortcuts+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardShortcuts+Extensions.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/30. 6 | // 7 | 8 | import Foundation 9 | import KeyboardShortcuts 10 | 11 | extension KeyboardShortcuts.Name { 12 | static let toggleActive = Self("toggleActive", default: .init(.backtick, modifiers: [.control])) 13 | static let toggleMenuBarOverride = Self("toggleMenuBarOverride", default: .init(.backtick, modifiers: [.control, .option])) 14 | } 15 | -------------------------------------------------------------------------------- /Abyssal/Extensions/Foundation/URL+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Extensions.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URL { 11 | static let source = URL(string: "https://github.com/\(repository)")! 12 | 13 | static let release = URL(string: "https://github.com/\(repository)/releases/\(Version.remote.string)")! 14 | 15 | static let releaseTags = URL(string: "https://api.github.com/repos/\(repository)/tags")! 16 | } 17 | -------------------------------------------------------------------------------- /Abyssal/Extensions/SwiftUI/EnvironmentValues+Extention.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentValues+Extention.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension EnvironmentValues { 11 | var hasTitle: Bool { 12 | get { self[HasTitleEnvironmentKey.self] } 13 | set { self[HasTitleEnvironmentKey.self] = newValue } 14 | } 15 | } 16 | 17 | struct HasTitleEnvironmentKey: EnvironmentKey { 18 | static var defaultValue: Bool = true 19 | } 20 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Abyssal/Dot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Abyssal/Line.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Colons/Colon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Droplets/Drops.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Droplets/LDrop.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Droplets/MDrop.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/NotSoHappy/Cat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/NotSoHappy/Sad.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/TheFace/Face.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Abyssal/DottedLine.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Approaching/Primary.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/HiddenBar/ic_left.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/HiddenBar/ic_line.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/HiddenBar/ic_right.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Implication/Implies.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Implication/Since.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/MetresAway/Line.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/NotSoHappy/Amazed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/NotSoHappy/Happy.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/NotSoHappy/Pale.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Approaching/Secondary.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Approaching/Tertiary.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/ElectroDiagram/Diagram.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/Implication/Therefore.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/MetresAway/MetreLine.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/ElectroDiagram/DiagramHead.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/ElectroDiagram/DiagramTail.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/Themes/HiddenBar/ic_line_translucent.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abyssal.xcodeproj/project.xcworkspace/xcuserdata/litekr.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | ShowSharedSchemesAutomaticallyEnabled 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Abyssal/Settings/Views/EmptyFormWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyFormWrapper.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EmptyFormWrapper: View where Content: View { 11 | var normalizePadding: Bool = true 12 | @ViewBuilder var content: () -> Content 13 | 14 | var body: some View { 15 | Form { 16 | content() 17 | } 18 | .formStyle(.columns) 19 | .padding(0) // Otherwise the nested Form will cause layout to overflow 20 | .padding(.leading, normalizePadding ? -8 : 0) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Abyssal/Settings/Sections/ShortcutsSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShortcutsSection.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/30. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ShortcutsSection: View { 11 | @Environment(\.hasTitle) private var hasTitle 12 | 13 | var body: some View { 14 | Section { 15 | ShortcutsControl() 16 | } header: { 17 | if hasTitle { 18 | Text("Shortcuts") 19 | } 20 | } 21 | } 22 | } 23 | 24 | #Preview { 25 | Form { 26 | ShortcutsSection() 27 | } 28 | .formStyle(.grouped) 29 | } 30 | -------------------------------------------------------------------------------- /Abyssal/Miscellaneous/Formattable/TimeFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeFormat.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum TimeFormat: DoubleFormattable { 11 | case second 12 | case minute 13 | 14 | static let instant = String(localized: "Instant") 15 | static let forever = String(localized: "Forever") 16 | 17 | var format: String { 18 | switch self { 19 | case .second: 20 | .init(localized: "%g Seconds") 21 | case .minute: 22 | .init(localized: "%g Minutes") 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Abyssal/Miscellaneous/WithIntermediateState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WithIntermediateState.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/30. 6 | // 7 | 8 | import Foundation 9 | 10 | struct WithIntermediateState where Value: Equatable { 11 | var intermediate: Value? 12 | var value: () -> Value 13 | 14 | var needsUpdate: Bool { 15 | intermediate != value() 16 | } 17 | 18 | init(_ value: @escaping () -> Value) { 19 | self.value = value 20 | self.intermediate = nil 21 | } 22 | 23 | mutating func update() { 24 | intermediate = self.value() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Abyssal.xcodeproj/xcuserdata/litekr.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Stalker.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | C66CAB9F2A38871500E4D8E6 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Abyssal/Models/MouseModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MouseModel.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/24. 6 | // 7 | 8 | import AppKit 9 | 10 | @Observable 11 | class MouseModel { 12 | static var shared = MouseModel() 13 | 14 | var none: Bool { 15 | NSEvent.pressedMouseButtons == 0; 16 | } 17 | 18 | var left: Bool { 19 | NSEvent.pressedMouseButtons & 0x1 == 1 20 | } 21 | 22 | var dragging: Bool { 23 | KeyboardModel.shared.command && left 24 | } 25 | 26 | func inside( 27 | _ rect: NSRect? 28 | ) -> Bool { 29 | rect?.contains(NSEvent.mouseLocation) ?? false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Abyssal/Settings/Tips/TipDeadZoneTitle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipDeadZoneTitle.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct TipDeadZoneTitle: View { 12 | @Default(.screenSettings) var screenSettings 13 | 14 | var body: some View { 15 | let label = switch screenSettings.main.deadZone { 16 | case .percentage(let percentage): 17 | UnitFormat.percentage.format(percentage) 18 | case .pixel(let pixel): 19 | UnitFormat.pixel.format(pixel) 20 | } 21 | 22 | TipTitle(value: $screenSettings.main.deadZone) { 23 | Text(label) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Abyssal/Miscellaneous/ObservationTrackingStream.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservationTrackingStream.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/23. 6 | // 7 | 8 | import Foundation 9 | 10 | func observationTrackingStream( 11 | _ apply: @escaping () -> T 12 | ) -> AsyncStream { 13 | .init { continuation in 14 | @Sendable func observe() { 15 | let result = withObservationTracking { 16 | apply() 17 | } onChange: { 18 | DispatchQueue.main.async { 19 | observe() 20 | } 21 | } 22 | 23 | continuation.yield(result) 24 | } 25 | 26 | observe() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Abyssal/Extensions/Foundation/Bundle+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Extensions.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/22. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | var appName: String { getInfo("CFBundleName") } 12 | var displayName: String { getInfo("CFBundleDisplayName") } 13 | var bundleID: String { getInfo("CFBundleIdentifier") } 14 | var copyright: String { getInfo("NSHumanReadableCopyright") } 15 | 16 | var appBuild: String { getInfo("CFBundleVersion") } 17 | var appVersion: String { getInfo("CFBundleShortVersionString") } 18 | 19 | func getInfo(_ str: String) -> String { 20 | infoDictionary?[str] as? String ?? "⚠️" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Abyssal/Settings/Controls/Toggles/StartsWithMacOSControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartsWithMacOSControl.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import SwiftUI 9 | import LaunchAtLogin 10 | 11 | struct StartsWithMacOSControl: View { 12 | var body: some View { 13 | TipWrapper(tip: tip) { tip in 14 | LaunchAtLogin.Toggle { 15 | Text("Starts with macOS") 16 | } 17 | } 18 | } 19 | 20 | private let tip = Tip { 21 | .init(localized: """ 22 | Launch **\(Bundle.main.appName)** right after macOS starts. 23 | """) 24 | } 25 | } 26 | 27 | #Preview { 28 | Form { 29 | StartsWithMacOSControl() 30 | } 31 | .formStyle(.grouped) 32 | } 33 | -------------------------------------------------------------------------------- /Abyssal/Settings/Sections/GeneralSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeneralSection.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GeneralSection: View { 11 | @Environment(\.hasTitle) private var hasTitle 12 | 13 | var body: some View { 14 | Section { 15 | UnifiedVStack { 16 | ThemeControl() 17 | } 18 | 19 | UnifiedVStack { 20 | FeedbackControl() 21 | } 22 | } header: { 23 | if hasTitle { 24 | Text("General") 25 | } 26 | } 27 | } 28 | } 29 | 30 | #Preview { 31 | Form { 32 | GeneralSection() 33 | } 34 | .formStyle(.grouped) 35 | } 36 | -------------------------------------------------------------------------------- /Abyssal/Settings/Sections/AdvancedSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdvancedSection.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AdvancedSection: View { 11 | @Environment(\.hasTitle) private var hasTitle 12 | 13 | var body: some View { 14 | Section { 15 | UnifiedVStack { 16 | TimeoutControl() 17 | } 18 | 19 | UnifiedVStack { 20 | StartsWithMacOSControl() 21 | } 22 | } header: { 23 | if hasTitle { 24 | Text("Advanced") 25 | } 26 | } 27 | } 28 | } 29 | 30 | #Preview { 31 | Form { 32 | AdvancedSection() 33 | } 34 | .formStyle(.grouped) 35 | } 36 | -------------------------------------------------------------------------------- /Abyssal/Settings/Tips/TipFeedbackTitle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipFeedbackTitle.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/24. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct TipFeedbackTitle: View { 12 | @Default(.feedback) private var feedback 13 | 14 | var body: some View { 15 | let label: String = switch feedback { 16 | case .none: 17 | .init(localized: "None") 18 | case .light: 19 | .init(localized: "Light") 20 | case .medium: 21 | .init(localized: "Medium") 22 | case .heavy: 23 | .init(localized: "Heavy") 24 | } 25 | 26 | TipTitle(value: $feedback) { 27 | Text(label) 28 | } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Abyssal/Models/KeyboardModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardModel.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/24. 6 | // 7 | 8 | import AppKit 9 | import Defaults 10 | 11 | @Observable 12 | class KeyboardModel { 13 | static var shared = KeyboardModel() 14 | 15 | var shift: Bool { 16 | NSEvent.modifierFlags.contains(.shift) 17 | } 18 | 19 | var control: Bool { 20 | NSEvent.modifierFlags.contains(.control) 21 | } 22 | 23 | var option: Bool { 24 | NSEvent.modifierFlags.contains(.option) 25 | } 26 | 27 | var command: Bool { 28 | NSEvent.modifierFlags.contains(.command) 29 | } 30 | 31 | var triggers: Bool { 32 | Defaults[.modifierCompose].triggers(input: Modifier.fromFlags(NSEvent.modifierFlags)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Abyssal/Settings/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewController.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/21. 6 | // 7 | 8 | import AppKit 9 | 10 | class SettingsViewController: NSViewController { 11 | func initializeFrame() { 12 | DispatchQueue.main.async { 13 | AppDelegate.shared?.popover.contentSize = self.view.intrinsicContentSize 14 | } 15 | } 16 | 17 | override func viewWillAppear() { 18 | initializeFrame() 19 | 20 | DispatchQueue.main.async { 21 | AppDelegate.shared?.statusBarController.function() 22 | } 23 | } 24 | 25 | override func viewWillDisappear() { 26 | DispatchQueue.main.async { 27 | AppDelegate.shared?.statusBarController.function() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Abyssal/Settings/Sections/FunctionsSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionsSection.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FunctionsSection: View { 11 | @Environment(\.hasTitle) private var hasTitle 12 | 13 | var body: some View { 14 | Section { 15 | UnifiedVStack { 16 | AutoShowsControl() 17 | 18 | AlwaysHideAreaControl() 19 | } 20 | 21 | UnifiedVStack { 22 | MenuBarOverrideControl() 23 | } 24 | } header: { 25 | if hasTitle { 26 | Text("Functions") 27 | } 28 | } 29 | } 30 | } 31 | 32 | #Preview { 33 | Form { 34 | FunctionsSection() 35 | } 36 | .formStyle(.grouped) 37 | } 38 | -------------------------------------------------------------------------------- /Abyssal/Settings/Tips/TipTimeoutTitle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipTimeoutTitle.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/24. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct TipTimeoutTitle: View { 12 | @Default(.timeout) private var timeout 13 | 14 | var body: some View { 15 | let label = switch timeout { 16 | case .sec5, .sec10, .sec15, .sec30, .sec45: 17 | TimeFormat.second.format(Double(timeout.rawValue)) 18 | case .sec60, .min2, .min3, .min5, .min10: 19 | TimeFormat.minute.format(Double(timeout.rawValue / 60)) 20 | case .instant: 21 | TimeFormat.instant 22 | case .forever: 23 | TimeFormat.forever 24 | } 25 | 26 | TipTitle(value: $timeout) { 27 | Text(label) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Abyssal/Settings/Controls/Toggles/RespectNotchControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RespectNotchControl.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct RespectNotchControl: View { 12 | @Default(.screenSettings) private var screenSettings 13 | 14 | var body: some View { 15 | TipWrapper(tip: respectNotchTip) { tip in 16 | Toggle("Respect notch", isOn: $screenSettings.main.respectNotch) 17 | } 18 | } 19 | 20 | private let respectNotchTip = Tip { 21 | .init(localized: """ 22 | If the screen has a notch, use the menu bar's trailing side of the notch as available area and ignore dead zone settings. 23 | """) 24 | } 25 | } 26 | 27 | #Preview { 28 | Form { 29 | RespectNotchControl() 30 | } 31 | .formStyle(.grouped) 32 | } 33 | -------------------------------------------------------------------------------- /Abyssal/Extensions/AppKit/NSRect+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSRect+Extension.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2023/6/17. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | extension NSRect { 12 | var containsMouse: Bool { 13 | return MouseModel.shared.inside(self) 14 | } 15 | 16 | func getTrackingArea( 17 | _ owner: Any?, 18 | viewToAdd view: NSView? = nil 19 | ) -> NSTrackingArea { 20 | let trackingArea = NSTrackingArea( 21 | rect: self, 22 | options: [.activeAlways, 23 | .inVisibleRect, 24 | .mouseEnteredAndExited], 25 | owner: owner 26 | ) 27 | 28 | if view != nil { 29 | view?.addTrackingArea(trackingArea) 30 | } 31 | 32 | return trackingArea 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Abyssal.xcodeproj/xcuserdata/Kr.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Abyssal.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | AbyssalTests.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 2 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 28D46C322C381BB100EC6EAE 21 | 22 | primary 23 | 24 | 25 | C66CAB9F2A38871500E4D8E6 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Abyssal/Miscellaneous/Formattable/Formattable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Formattable.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol Formattable { 11 | associatedtype Value: CVarArg 12 | 13 | var format: String { get } 14 | 15 | func format(_ values: Value...) -> String 16 | 17 | func eval(_ value: Value) -> Value 18 | } 19 | 20 | extension Formattable { 21 | func format(_ values: Value...) -> String { 22 | .init(format: format, values.map(eval(_:))) 23 | } 24 | 25 | func eval(_ value: Value) -> Value { 26 | value 27 | } 28 | } 29 | 30 | protocol DoubleFormattable: Formattable where Value == Double { 31 | } 32 | 33 | extension DoubleFormattable { 34 | func format(_ value: Double) -> String { 35 | .init(format: format, eval(value)) 36 | } 37 | 38 | func eval(_ value: Value) -> Value { 39 | value.rounded(digits: 1) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Abyssal/Miscellaneous/Events/EventMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventMonitor.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2023/6/13. 6 | // 7 | 8 | import Cocoa 9 | import AppKit 10 | 11 | public class EventMonitor { 12 | private var monitor: Any? 13 | 14 | private let mask: NSEvent.EventTypeMask 15 | 16 | private let handler: ( 17 | NSEvent? 18 | ) -> Void 19 | 20 | public init( 21 | mask: NSEvent.EventTypeMask, 22 | handler: @escaping (NSEvent?) -> Void 23 | ) { 24 | self.mask = mask 25 | self.handler = handler 26 | } 27 | 28 | deinit { 29 | stop() 30 | } 31 | 32 | public func start( 33 | ) { 34 | monitor = NSEvent.addGlobalMonitorForEvents( 35 | matching: mask, 36 | handler: handler 37 | ) 38 | } 39 | 40 | public func stop( 41 | ) { 42 | if monitor != nil { 43 | NSEvent.removeMonitor(monitor!) 44 | monitor = nil 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Abyssal/Settings/Controls/Toggles/AlwaysHideAreaControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlwaysHideAreaControl.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct AlwaysHideAreaControl: View { 12 | @Default(.alwaysHideAreaEnabled) private var alwaysHideAreaEnabled 13 | 14 | var body: some View { 15 | TipWrapper(tip: tip) { tip in 16 | Toggle("Use always hide area", isOn: $alwaysHideAreaEnabled) 17 | } 18 | } 19 | 20 | private let tip = Tip { 21 | .init(localized: """ 22 | Hide certain status items permanently by moving them to the leading of the `Always Hide Separator`, that is, to the **Always Hide Area.** 23 | 24 | The status items inside the **Always Hide Area** will be hidden and kept invisible until the cursor hovers over the spare area with a modifier key down, or while this window is opened. 25 | """) 26 | } 27 | } 28 | 29 | #Preview { 30 | Form { 31 | AlwaysHideAreaControl() 32 | } 33 | .formStyle(.grouped) 34 | } 35 | -------------------------------------------------------------------------------- /Abyssal/Extensions/Foundation/Range+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Range+Extensions.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Range where Bound: FloatingPoint { 11 | func percentage(_ value: Bound) -> Bound { 12 | let base = lowerBound 13 | let span = upperBound - lowerBound 14 | return (value - base) / span 15 | } 16 | 17 | func fromPercentage(_ percentage: Bound) -> Bound { 18 | let base = lowerBound 19 | let span = upperBound - lowerBound 20 | return percentage * span + base 21 | } 22 | } 23 | 24 | extension ClosedRange where Bound: FloatingPoint { 25 | func percentage(_ value: Bound) -> Bound { 26 | let base = lowerBound 27 | let span = upperBound - lowerBound 28 | return (value - base) / span 29 | } 30 | 31 | func fromPercentage(_ percentage: Bound) -> Bound { 32 | let base = lowerBound 33 | let span = upperBound - lowerBound 34 | return percentage * span + base 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Abyssal/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/28. 6 | // 7 | 8 | // https://stackoverflow.com/a/68502031/23452915 9 | 10 | import AppKit 11 | 12 | // MARK: - Application 13 | 14 | let app = NSApplication.shared 15 | let delegate = AppDelegate() // Allocate 16 | 17 | // MARK: - Application Menu 18 | 19 | let appMenu = NSMenu() 20 | let emptyMenu = NSMenu() // Without any siblings 21 | 22 | do { 23 | let appMenuMain = NSMenu(title: Bundle.main.appName) 24 | 25 | appMenuMain.addItem( 26 | withTitle: .init(localized: "Escape from Overriding"), 27 | action: #selector(delegate.escapeFromOverridingMenuBar(_:)), keyEquivalent: "" 28 | ) 29 | 30 | appMenuMain.addItem(.separator()) 31 | 32 | appMenuMain.addItem( 33 | withTitle: .init(localized: "Quit"), 34 | action: #selector(delegate.quit(_:)), keyEquivalent: "q" 35 | ) 36 | 37 | appMenu.addItem({ 38 | let item = NSMenuItem() 39 | item.submenu = appMenuMain 40 | return item 41 | }()) 42 | } 43 | 44 | // MARK: - Run 45 | 46 | app.delegate = delegate 47 | app.mainMenu = appMenu 48 | app.run() 49 | -------------------------------------------------------------------------------- /Abyssal/Settings/Tips/TipTitle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipTitle.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TipTitle: View where Content: View, Value: Equatable { 11 | @Binding var value: Value 12 | @ViewBuilder var content: () -> Content 13 | 14 | init(value: Binding, @ViewBuilder content: @escaping () -> Content) { 15 | self._value = value 16 | self.content = content 17 | } 18 | 19 | init(@ViewBuilder content: @escaping () -> Content) where Value == Bool { 20 | self.init(value: .constant(false), content: content) 21 | } 22 | 23 | init(_ titleKey: LocalizedStringKey, value: Binding) where Content == Text { 24 | self.init(value: value) { 25 | Text(titleKey) 26 | } 27 | } 28 | 29 | init(_ titleKey: LocalizedStringKey) where Content == Text, Value == Bool { 30 | self.init(titleKey, value: .constant(false)) 31 | } 32 | 33 | var body: some View { 34 | content() 35 | .fixedSize() 36 | .font(.title3) 37 | .bold() 38 | 39 | .contentTransition(.numericText(countsDown: true)) 40 | .animation(.smooth, value: value) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Abyssal/Settings/Controls/ShortcutsControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShortcutsControl.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/30. 6 | // 7 | 8 | import SwiftUI 9 | import KeyboardShortcuts 10 | 11 | struct ShortcutsControl: View { 12 | var body: some View { 13 | VStack { 14 | KeyboardShortcuts.Recorder(for: .toggleActive) { 15 | VStack { 16 | Spacer() 17 | Text("Toggle active") 18 | Spacer() 19 | } 20 | } 21 | 22 | KeyboardShortcuts.Recorder(for: .toggleMenuBarOverride) { 23 | VStack { 24 | Spacer() 25 | Text("Toggle menu bar override") 26 | Spacer() 27 | } 28 | } 29 | } 30 | .controlSize(.large) 31 | } 32 | 33 | private let activeTip = Tip { 34 | .init(localized: """ 35 | The global shortcut used to toggle **\(Bundle.main.appName)**'s active state. 36 | """) 37 | } 38 | 39 | private let menuBarOverrideTip = Tip { 40 | .init(localized: """ 41 | The global shortcut used to toggle menu bar override. 42 | """) 43 | } 44 | } 45 | 46 | #Preview { 47 | Form { 48 | ShortcutsControl() 49 | } 50 | .formStyle(.grouped) 51 | } 52 | -------------------------------------------------------------------------------- /Abyssal/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "filename" : "icon_128x128.png", 25 | "idiom" : "mac", 26 | "scale" : "1x", 27 | "size" : "128x128" 28 | }, 29 | { 30 | "filename" : "icon_128x128@2x@2x.png", 31 | "idiom" : "mac", 32 | "scale" : "2x", 33 | "size" : "128x128" 34 | }, 35 | { 36 | "filename" : "icon_256x256.png", 37 | "idiom" : "mac", 38 | "scale" : "1x", 39 | "size" : "256x256" 40 | }, 41 | { 42 | "filename" : "icon_256x256@2x@2x.png", 43 | "idiom" : "mac", 44 | "scale" : "2x", 45 | "size" : "256x256" 46 | }, 47 | { 48 | "filename" : "icon_512x512.png", 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "filename" : "icon_512x512@2x@2x.png", 55 | "idiom" : "mac", 56 | "scale" : "2x", 57 | "size" : "512x512" 58 | } 59 | ], 60 | "info" : { 61 | "author" : "xcode", 62 | "version" : 1 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Abyssal/Managers/MathHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MathHelper.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct MathHelper { 11 | static var lerpThreshold: CGFloat { 12 | return ScreenManager.width / 25 13 | } 14 | 15 | static var lerpRatio: CGFloat { 16 | let baseValue = 0.42 17 | return baseValue * (KeyboardModel.shared.shift ? 0.25 : 1) // Slow down when shift key is down 18 | } 19 | 20 | static func approaching( 21 | _ a: CGFloat, _ b: CGFloat, 22 | _ ignoreSmallValues: Bool = true 23 | ) -> Bool { 24 | abs(a - b) < (ignoreSmallValues ? 1 : 0.001) 25 | } 26 | 27 | static func lerp( 28 | a: CGFloat, 29 | b: CGFloat, 30 | ratio: CGFloat, 31 | _ ignoreSmallValues: Bool = true 32 | ) -> CGFloat { 33 | guard !ignoreSmallValues || !approaching(a, b, ignoreSmallValues) else { return b } 34 | let diff = b - a 35 | 36 | return a + log10ClampWithThreshold(diff, threshold: lerpThreshold) * ratio 37 | } 38 | 39 | static func log10ClampWithThreshold( 40 | _ x: CGFloat, 41 | threshold: CGFloat 42 | ) -> CGFloat { 43 | if x < -threshold { 44 | return -(threshold + log10(-x / threshold) * threshold) 45 | } else if x > threshold { 46 | return threshold + log10(x / threshold) * threshold 47 | } else { 48 | return x 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Abyssal/Settings/Controls/Popups/MenuBarOverrideControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuBarOverrideControl.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/7/3. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct MenuBarOverrideControl: View { 12 | @Default(.autoOverridesMenuBarEnabled) var autoOverridesMenuBarEnabled 13 | @Default(.menuBarOverride) var menuBarOverride 14 | 15 | var body: some View { 16 | TipWrapper(tip: autoOverridesMenuBarTip) { tip in 17 | Toggle(isOn: $autoOverridesMenuBarEnabled) { 18 | Text("Auto overrides menu bar") 19 | } 20 | } 21 | 22 | Picker("Application menu", selection: $menuBarOverride) { 23 | ForEach(MenuBarOverride.allCases, id: \.self) { override in 24 | switch override { 25 | case .app: 26 | Text(Bundle.main.appName) 27 | case .empty: 28 | Text("Nothing") 29 | } 30 | } 31 | } 32 | .onChange(of: menuBarOverride) { _, override in 33 | override.apply() 34 | } 35 | } 36 | 37 | private let autoOverridesMenuBarTip = Tip { 38 | .init(localized: """ 39 | Allow **\(Bundle.main.appName)** to takeover the menu bar when showing this window or toggled manually. **Applies after this window is closed.** 40 | """) 41 | } 42 | } 43 | 44 | #Preview { 45 | Form { 46 | MenuBarOverrideControl() 47 | } 48 | .formStyle(.grouped) 49 | } 50 | -------------------------------------------------------------------------------- /Abyssal/Settings/Controls/Toggles/AutoShowsControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoShowsControl.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct AutoShowsControl: View { 12 | @Default(.autoShowsEnabled) private var autoShowsEnabled 13 | 14 | var body: some View { 15 | TipWrapper(tip: tip) { tip in 16 | Toggle("Auto shows", isOn: $autoShowsEnabled) 17 | } 18 | } 19 | 20 | private let tip = Tip { 21 | .init(localized: """ 22 | Auto shows the status items inside the **Hide Area** while the cursor is hovering over the spare area. 23 | 24 | If this option is enabled, the status items inside the **Hide Area,** which is between the `Hide Separator` (the middle one) and the `Always Hide Separator` (the furthest one from the screen corner), will be hidden and kept invisible, until the cursor hovers over the spare area, where the status items in **Hide Area** used to stay. Otherwise the status items will be hidden until you switch their visibility state manually. 25 | 26 | By left clicking on the `Menu Separator` (the nearest one to the screen corner), or clicking using either of the mouse buttons on the other separators, you can manually switch the visibility state of the status items inside the **Hide Area.** If you set them visible, they will never be hidden again until you manually switch their visibility state. Otherwise they will follow the behavior defined above. 27 | """) 28 | } 29 | } 30 | 31 | #Preview { 32 | Form { 33 | AutoShowsControl() 34 | } 35 | .formStyle(.grouped) 36 | } 37 | -------------------------------------------------------------------------------- /Abyssal/InfoPlist.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "CFBundleDisplayName" : { 5 | "comment" : "Bundle display name", 6 | "extractionState" : "extracted_with_value", 7 | "localizations" : { 8 | "en" : { 9 | "stringUnit" : { 10 | "state" : "new", 11 | "value" : "Abyssal" 12 | } 13 | }, 14 | "zh-Hans" : { 15 | "stringUnit" : { 16 | "state" : "translated", 17 | "value" : "Abyssal" 18 | } 19 | } 20 | } 21 | }, 22 | "CFBundleName" : { 23 | "comment" : "Bundle name", 24 | "extractionState" : "extracted_with_value", 25 | "localizations" : { 26 | "en" : { 27 | "stringUnit" : { 28 | "state" : "new", 29 | "value" : "Abyssal" 30 | } 31 | }, 32 | "zh-Hans" : { 33 | "stringUnit" : { 34 | "state" : "translated", 35 | "value" : "Abyssal" 36 | } 37 | } 38 | } 39 | }, 40 | "NSHumanReadableCopyright" : { 41 | "comment" : "Copyright (human-readable)", 42 | "extractionState" : "extracted_with_value", 43 | "localizations" : { 44 | "en" : { 45 | "stringUnit" : { 46 | "state" : "new", 47 | "value" : "\u0012Copyright © 2024 Cement Labs" 48 | } 49 | }, 50 | "zh-Hans" : { 51 | "stringUnit" : { 52 | "state" : "translated", 53 | "value" : "版权所有 © 2024 Cement Labs" 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "version" : "1.0" 60 | } -------------------------------------------------------------------------------- /Abyssal/Extensions/Foundation/DispatchQueue+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchQueue+Extensions.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/7/24. 6 | // 7 | 8 | import Foundation 9 | 10 | var dispatches: [DispatchQueue: [AnyHashable: DispatchWorkItem]] = [:] 11 | 12 | extension DispatchQueue { 13 | var dispatches: [AnyHashable: DispatchWorkItem] { 14 | get { 15 | if let result = Abyssal.dispatches[self] { 16 | return result 17 | } else { 18 | Abyssal.dispatches[self] = [:] 19 | return [:] 20 | } 21 | } 22 | 23 | set { 24 | Abyssal.dispatches[self] = newValue 25 | } 26 | } 27 | 28 | func asyncAfter( 29 | _ identifier: AnyHashable, 30 | deadline: DispatchTime, 31 | execute work: @escaping @Sendable @convention(block) () -> Void 32 | ) { 33 | let dispatch = DispatchWorkItem { 34 | self.cancel(identifier) 35 | work() 36 | } 37 | 38 | dispatches[identifier] = dispatch 39 | asyncAfter(deadline: deadline, execute: dispatch) 40 | } 41 | 42 | func async( 43 | _ identifier: AnyHashable, 44 | execute work: @escaping @Sendable @convention(block) () -> Void 45 | ) { 46 | let dispatch = DispatchWorkItem { 47 | self.cancel(identifier) 48 | work() 49 | } 50 | 51 | dispatches[identifier] = dispatch 52 | async(execute: dispatch) 53 | } 54 | 55 | func cancel(_ identifier: AnyHashable) { 56 | dispatches[identifier]?.cancel() 57 | dispatches[identifier] = nil 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Abyssal.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "7638e69ae5b3555bf31c45be8bfc8d7de1befb8051aec7987069e0fa355422c0", 3 | "pins" : [ 4 | { 5 | "identity" : "defaults", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/sindresorhus/Defaults", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "0f73f9401a416336931f2d598662cc524a13b21f" 11 | } 12 | }, 13 | { 14 | "identity" : "keyboardshortcuts", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/sindresorhus/KeyboardShortcuts", 17 | "state" : { 18 | "revision" : "2e5f15581fefb821d4b366e57d817be8bf12aa58", 19 | "version" : "2.0.1" 20 | } 21 | }, 22 | { 23 | "identity" : "launchatlogin", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/sindresorhus/LaunchAtLogin", 26 | "state" : { 27 | "branch" : "main", 28 | "revision" : "7ad6331f9c38953eb1ce8737758e18f7607e984a" 29 | } 30 | }, 31 | { 32 | "identity" : "sfsafesymbols", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols", 35 | "state" : { 36 | "revision" : "afd0a1da4ed62bab1413caa6dd6b60a7a7089ed2", 37 | "version" : "5.2.0" 38 | } 39 | }, 40 | { 41 | "identity" : "swiftui-introspect", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/siteline/swiftui-introspect", 44 | "state" : { 45 | "revision" : "668a65735751432b640260c56dfa621cec568368", 46 | "version" : "1.2.0" 47 | } 48 | } 49 | ], 50 | "version" : 3 51 | } 52 | -------------------------------------------------------------------------------- /Abyssal/Settings/Controls/Popups/ThemeControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeControl.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct ThemeControl: View { 12 | @Default(.theme) private var theme 13 | @Default(.reduceAnimationEnabled) private var reduceAnimationEnabled 14 | 15 | var body: some View { 16 | TipWrapper(tip: themeTip) { tip in 17 | Picker("Theme", selection: $theme) { 18 | ForEach(Theme.themes, id: \.self) { theme in 19 | HStack { 20 | Image(nsImage: theme.icon.image) 21 | Text(theme.name) 22 | } 23 | } 24 | } 25 | .onChange(of: theme) { _, _ in 26 | AppDelegate.shared?.statusBarController.function() 27 | } 28 | } 29 | 30 | TipWrapper(tip: reduceAnimationTip) { tip in 31 | Toggle("Reduce animation", isOn: $reduceAnimationEnabled) 32 | } 33 | } 34 | 35 | private let themeTip = Tip { 36 | .init(localized: """ 37 | Some themes will hide the icons inside the separators automatically, while others not. 38 | 39 | Themes that automatically hide the icons will only show them when the status items inside the **Hide Area** are manually set to visible, while other themes indicate this by reducing the separators' opacity. 40 | """) 41 | } 42 | 43 | private let reduceAnimationTip = Tip { 44 | .init(localized: """ 45 | Reduce animation to gain a more performant experience. 46 | """) 47 | } 48 | } 49 | 50 | #Preview { 51 | Form { 52 | ThemeControl() 53 | } 54 | .formStyle(.grouped) 55 | } 56 | -------------------------------------------------------------------------------- /Abyssal/Settings/Controls/Sliders/TimeoutControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeoutControl.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct TimeoutControl: View { 12 | @Default(.timeout) private var timeout 13 | 14 | var body: some View { 15 | VStack(alignment: .leading) { 16 | Text("Timeout") 17 | .opacity(timeout == .forever ? 0.45 : 1) 18 | .animation(.default, value: timeout) 19 | 20 | EmptyFormWrapper { 21 | let maxIndex = Timeout.allCases.endIndex - 1 22 | let binding = Binding { 23 | Double(Timeout.allCases.firstIndex(of: timeout) ?? maxIndex) 24 | } set: { index in 25 | timeout = Timeout.allCases[Int(index)] 26 | } 27 | 28 | TipWrapper(tip: timeoutTip, alwaysVisible: true, value: $timeout) { tip in 29 | Slider(value: binding, in: 0...Double(maxIndex), step: 1) 30 | } 31 | } 32 | } 33 | } 34 | 35 | private let timeoutTip = Tip(preferredEdge: .minY, delay: 0.1) { 36 | TipTimeoutTitle() 37 | } content: { 38 | .init(localized: """ 39 | Time to countdown before disabling **Auto Idling.** 40 | 41 | After interacting with status items that will be automatically hidden, for example, status items inside the **Always Hidden Area,** **Auto Idling** will keep them visible until this timeout is reached, the cursor hovered over the `Hide Separator` or `Always Hide Separator`, or the specified active strategy is met. 42 | """) 43 | } 44 | } 45 | 46 | #Preview { 47 | Form { 48 | TimeoutControl() 49 | } 50 | .formStyle(.grouped) 51 | } 52 | -------------------------------------------------------------------------------- /Abyssal/Settings/Views/PopoverHint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopoverHint.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/7/3. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PopoverHint: View where Content: View { 11 | @State var sustain: TimeInterval = 0.5 12 | @State var arrowEdge: Edge = .top 13 | @ViewBuilder var content: () -> Content 14 | 15 | @State private var isPopoverPresented: Bool = false 16 | @State private var dispatch: DispatchWorkItem? 17 | @State private var lastShown: Date? 18 | 19 | private var timeToLastShown: TimeInterval { 20 | if let lastShown { 21 | Date.now.timeIntervalSince(lastShown) 22 | } else { 23 | 0 24 | } 25 | } 26 | 27 | var body: some View { 28 | VStack { 29 | Circle() 30 | .aspectRatio(1, contentMode: .fit) 31 | .frame(width: 6) 32 | .foregroundStyle(.tint) 33 | 34 | Spacer() 35 | } 36 | .padding(1) 37 | .onHover { isHovering in 38 | if isHovering { 39 | // Open 40 | dispatch?.cancel() 41 | dispatch = nil 42 | 43 | lastShown = .now 44 | isPopoverPresented = true 45 | } else { 46 | // Schedule close 47 | dispatch = .init { 48 | self.isPopoverPresented = false 49 | } 50 | 51 | let interval = max(0, sustain - timeToLastShown) 52 | DispatchQueue.main.asyncAfter(deadline: .now() + interval, execute: dispatch!) 53 | } 54 | } 55 | .popover(isPresented: $isPopoverPresented, arrowEdge: arrowEdge) { 56 | content() 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Abyssal/Settings/Controls/Popups/ActiveStrategyControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActiveStrategyControl.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct ActiveStrategyControl: View { 12 | @Default(.screenSettings) private var screenSettings 13 | 14 | var body: some View { 15 | TipWrapper(tip: activeStrategyTip) { tip in 16 | HStack(alignment: .firstTextBaseline) { 17 | Text("Becomes active again") 18 | 19 | Spacer() 20 | 21 | Menu { 22 | Section("On Change") { 23 | Toggle("Frontmost Application", isOn: $screenSettings.main.activeStrategy.frontmostAppChange) 24 | 25 | Toggle("Main Screen", isOn: $screenSettings.main.activeStrategy.screenChange) 26 | } 27 | 28 | Section("On Invalidation") { 29 | Toggle("Menu Bar Interaction", isOn: $screenSettings.main.activeStrategy.interactionInvalidate) 30 | } 31 | } label: { 32 | Text("When Satisfying Any of the \(screenSettings.main.activeStrategy.enabledCount) Rules") 33 | } 34 | .aspectRatio(contentMode: .fit) 35 | } 36 | } 37 | } 38 | 39 | private let activeStrategyTip = Tip { 40 | .init(localized: """ 41 | The stratrgy used for automatically cancelling **Auto Idling.** When the condition is met, **\(Bundle.main.appName)** will recover the active state where menu bar items in both **Hide Area** and **Always Hide Area** become hidden. 42 | """) 43 | } 44 | } 45 | 46 | #Preview { 47 | Form { 48 | ActiveStrategyControl() 49 | } 50 | .formStyle(.grouped) 51 | } 52 | -------------------------------------------------------------------------------- /Abyssal/Settings/Controls/Sliders/FeedbackControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedbackControl.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct FeedbackControl: View { 12 | @Default(.feedback) private var feedback 13 | 14 | var body: some View { 15 | VStack(alignment: .leading) { 16 | Text("Haptic feedback") 17 | .opacity(feedback == .none ? 0.45 : 1) 18 | .animation(.default, value: feedback) 19 | 20 | EmptyFormWrapper { 21 | let maxIndex = Feedback.allCases.endIndex - 1 22 | let binding = Binding { 23 | Double(Feedback.allCases.firstIndex(of: feedback) ?? 0) 24 | } set: { index in 25 | feedback = Feedback.allCases[Int(index)] 26 | } 27 | 28 | TipWrapper(tip: feedbackTip, alwaysVisible: true, value: $feedback) { tip in 29 | Slider(value: binding, in: 0...Double(maxIndex), step: 1) { 30 | EmptyView() 31 | } 32 | } 33 | .onChange(of: feedback) { _, _ in 34 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 35 | AppDelegate.shared?.statusBarController.startFeedbackTimer() 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | private let feedbackTip = Tip(preferredEdge: .minY, delay: 0.1) { 43 | TipFeedbackTitle() 44 | } content: { 45 | .init(localized: """ 46 | The intensity of feedback given when triggering actions such as _enabling **Auto Shows**_ or _canceling **Auto Idling.**_ 47 | """) 48 | } 49 | } 50 | 51 | #Preview { 52 | Form { 53 | FeedbackControl() 54 | } 55 | .formStyle(.grouped) 56 | } 57 | -------------------------------------------------------------------------------- /Abyssal/Managers/ExternalMenuBarManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExternalMenuBarManager.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/30. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class ExternalMenuBarItem { 12 | let windowInfo: WindowInfo 13 | 14 | init(windowInfo: WindowInfo) { 15 | self.windowInfo = windowInfo 16 | } 17 | 18 | var pid: pid_t { 19 | windowInfo.ownerProcessID 20 | } 21 | 22 | var windowNumber: Int { 23 | windowInfo.windowNumber 24 | } 25 | 26 | var bounds: NSRect { 27 | windowInfo.bounds 28 | } 29 | 30 | var windowsNear: [WindowInfo] { 31 | windowInfo 32 | .processRelatedWindows 33 | .filter(\.isOnscreen) 34 | .filter { $0.isPlacingNear(bounds, edge: .maxY) } 35 | } 36 | 37 | var newWindowsNear: [WindowInfo] { 38 | windowsNear 39 | .filter { !cachedWindowNumbersNear.contains($0.windowNumber) } 40 | } 41 | 42 | private var cachedWindowNumbersNear: [Int] { 43 | get { 44 | ExternalMenuBarManager.cachedWindowNumbersNear[windowNumber] ?? [] 45 | } 46 | 47 | set(windowNumbersNear) { 48 | ExternalMenuBarManager.cachedWindowNumbersNear[windowNumber] = windowNumbersNear 49 | } 50 | } 51 | 52 | func cache() { 53 | cachedWindowNumbersNear = windowsNear.map(\.windowNumber) 54 | } 55 | } 56 | 57 | struct ExternalMenuBarManager { 58 | static let identifier = UUID() 59 | fileprivate static var cachedWindowNumbersNear: [Int: [Int]] = [:] 60 | 61 | static var menuBarWindowInfos: [WindowInfo] { 62 | WindowInfo.allOnScreenWindows 63 | .filter(\.isStatusMenuItem) 64 | .filter { !$0.isFromAbyssal } 65 | } 66 | 67 | static var menuBarItems: [ExternalMenuBarItem] { 68 | menuBarWindowInfos 69 | .map(ExternalMenuBarItem.init) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Abyssal/Settings/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/21. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftUIIntrospect 10 | 11 | struct SettingsView: View { 12 | var body: some View { 13 | HStack(spacing: -20) { 14 | ZStack { 15 | Form { 16 | ModifierSection() 17 | 18 | ShortcutsSection() 19 | 20 | AdvancedSection() 21 | } 22 | .padding(1) 23 | .defaultScrollAnchor(.bottom) 24 | 25 | VStack { 26 | Spacer() 27 | .frame(height: 85) 28 | 29 | VStack { 30 | Text(Bundle.main.appName) 31 | .font(.title) 32 | .bold() 33 | .padding(8) 34 | 35 | SettingsVersionView() 36 | } 37 | 38 | Spacer() 39 | } 40 | } 41 | .frame(width: 400) 42 | 43 | VStack(spacing: 0) { 44 | SettingsTrafficsView() 45 | .padding(20) 46 | 47 | Form { 48 | GeneralSection() 49 | .environment(\.hasTitle, false) 50 | 51 | FunctionsSection() 52 | 53 | BehaviorsSection() 54 | } 55 | .defaultScrollAnchor(.bottom) 56 | .padding(1) 57 | .padding(.top, -20) 58 | } 59 | .frame(width: 450) 60 | } 61 | .controlSize(.regular) 62 | .formStyle(.grouped) 63 | .scrollDisabled(true) 64 | } 65 | } 66 | 67 | #Preview { 68 | SettingsView() 69 | .frame(minHeight: 800) 70 | } 71 | -------------------------------------------------------------------------------- /AbyssalTests/Miscellaneous/VersionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionTests.swift 3 | // AbyssalTests 4 | // 5 | // Created by KrLite on 2024/7/5. 6 | // 7 | 8 | import Testing 9 | @testable import Abyssal 10 | 11 | struct VersionTests { 12 | @Test func parseComponent() async throws { 13 | let componentString = "42" 14 | let component = Version.Component(parsing: componentString) 15 | 16 | #expect(component != nil) 17 | #expect(component == .number(42)) 18 | } 19 | 20 | @Test func parseVersion() async throws { 21 | let versionString = "1.0.0.0-alpha. 2.34 .567 -patch. beta-2, 1 ,2.3" 22 | let version = Version(from: versionString) 23 | 24 | #expect(version != nil) 25 | #expect(version?.string == "1.0.0.0-alpha.2.34.567-patch-beta.3") 26 | } 27 | 28 | @Test func compareVersions() async throws { 29 | let version1 = Version(from: "3.3.1")! 30 | let version2 = Version(from: "4.0.0-alpha.1")! 31 | let version3 = Version(from: "4.0.0")! 32 | 33 | // Equalities 34 | #expect(!(version1 < version1)) 35 | #expect(!(version1 > version1)) 36 | 37 | #expect(!(version2 < version2)) 38 | #expect(!(version2 > version2)) 39 | 40 | #expect(!(version3 < version3)) 41 | #expect(!(version3 > version3)) 42 | 43 | // Comparisons 44 | #expect(version1 != version2) 45 | #expect(version1 < version2) 46 | #expect(version2 > version1) 47 | #expect(!(version1 > version2)) 48 | #expect(!(version2 < version1)) 49 | 50 | #expect(version1 != version3) 51 | #expect(version1 < version3) 52 | #expect(version3 > version1) 53 | #expect(!(version1 > version3)) 54 | #expect(!(version3 < version1)) 55 | 56 | #expect(version2 != version3) 57 | #expect(version2 < version3) 58 | #expect(version3 > version2) 59 | #expect(!(version2 > version3)) 60 | #expect(!(version3 < version2)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Abyssal.xcodeproj/xcshareddata/xcschemes/AbyssalTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 17 | 20 | 26 | 27 | 28 | 29 | 30 | 40 | 41 | 47 | 48 | 50 | 51 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Abyssal/Extensions/Defaults+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/2/8. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import Defaults 11 | import LaunchAtLogin 12 | 13 | extension Defaults.Keys { 14 | /// - `true`: the expanded state and menu bar itms are hidden. 15 | /// - `false`: the shrunk state and menu bar items are visible. 16 | static let isActive = Key("isActive", default: false) 17 | 18 | static let tipsEnabled = Key("tipsEnabled", default: true) 19 | 20 | 21 | 22 | static let alwaysHideAreaEnabled = Key("alwaysHideAreaEnabled", default: true) 23 | 24 | static let reduceAnimationEnabled = Key("reduceAnimationEnabled", default: false) 25 | 26 | static let autoShowsEnabled = Key("autoShowsEnabled", default: true) 27 | 28 | static let autoOverridesMenuBarEnabled = Key("autoOverridesMenuBar", default: false) 29 | 30 | 31 | 32 | static let theme = Key("theme", default: .defaultTheme) 33 | 34 | static let modifier = Key( 35 | "modifier", 36 | default: [.option, .command] 37 | ) 38 | 39 | static let modifierCompose = Key( 40 | "modifierCompose", 41 | default: .any 42 | ) 43 | 44 | static let timeout = Key( 45 | "timeout", 46 | default: .sec30 47 | ) 48 | 49 | static let feedback = Key( 50 | "feedback", 51 | default: .medium 52 | ) 53 | 54 | 55 | 56 | static let screenSettings = Key( 57 | "screenSettings", 58 | default: .init( 59 | global: .init( 60 | activeStrategy: .init( 61 | frontmostAppChange: true, 62 | interactionInvalidate: true, 63 | screenChange: false 64 | ), 65 | deadZone: .percentage(50), 66 | respectNotch: true 67 | ), 68 | unique: [:] 69 | ) 70 | ) 71 | 72 | static let menuBarOverride = Key( 73 | "menuBarOverride", 74 | default: .app 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /Abyssal/Settings/Views/TipWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TipWrapper.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/23. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | import SwiftUIIntrospect 11 | 12 | struct TipWrapper: View 13 | where Content: View, Title: View, Value: Equatable { 14 | typealias Tip = Abyssal.Tip 15 | 16 | @Default(.tipsEnabled) private var tipsEnabled 17 | 18 | var tip: Tip 19 | var alwaysVisible: Bool = false 20 | @Binding var value: Value 21 | @ViewBuilder var content: (Tip) -> Content 22 | 23 | @State private var isHovering: Bool = false 24 | 25 | init( 26 | tip: Tip, 27 | alwaysVisible: Bool = false, 28 | value: Binding<Value>, 29 | @ViewBuilder content: @escaping (Tip) -> Content 30 | ) { 31 | self.tip = tip 32 | self.alwaysVisible = alwaysVisible 33 | self._value = value 34 | self.content = content 35 | } 36 | 37 | init( 38 | tip: Tip, 39 | alwaysVisible: Bool = false, 40 | @ViewBuilder content: @escaping (Tip) -> Content 41 | ) where Value == Bool { 42 | self.init(tip: tip, alwaysVisible: alwaysVisible, value: .constant(false), content: content) 43 | } 44 | 45 | var body: some View { 46 | content(tip) 47 | // Show on hover 48 | .onHover { isHovering in 49 | self.isHovering = isHovering 50 | 51 | if (alwaysVisible || tipsEnabled) { 52 | tip.toggle(show: isHovering) 53 | } 54 | } 55 | 56 | // Update when value changes 57 | .onChange(of: value) { _, _ in 58 | tip.updateFrame() 59 | tip.updatePosition() 60 | } 61 | 62 | // Cache view 63 | .introspect(.slider, on: .macOS(.v14, .v15)) { slider in 64 | tip.cache(slider) 65 | 66 | tip.positionRect = { 67 | slider.knobRect 68 | } 69 | tip.hasReactivePosition = true 70 | } 71 | .introspect(.view, on: .macOS(.v14, .v15)) { view in 72 | tip.cache(view) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Abyssal/Contents/Icon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Icon.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/4/27. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import SFSafeSymbols 11 | 12 | protocol Icon { 13 | var image: NSImage { get } 14 | var width: CGFloat { get } 15 | var opacity: CGFloat { get } 16 | } 17 | 18 | struct NamedIcon: Icon { 19 | var name: String 20 | 21 | var image: NSImage { 22 | .init(named: .init(name))! 23 | } 24 | 25 | var width: CGFloat 26 | var opacity: CGFloat = 1 27 | } 28 | 29 | struct SymbolIcon: Icon { 30 | var symbol: SFSymbol 31 | var configuration: NSImage.SymbolConfiguration? 32 | 33 | var image: NSImage { 34 | let image = NSImage(systemSymbol: symbol) 35 | 36 | if 37 | let configuration, 38 | let image = image.withSymbolConfiguration(configuration) 39 | { 40 | return image 41 | } else { 42 | return image 43 | } 44 | } 45 | 46 | var width: CGFloat 47 | var opacity: CGFloat = 1 48 | } 49 | 50 | protocol IconBuilder { 51 | associatedtype Target: Icon 52 | 53 | var width: CGFloat { get } 54 | var opacity: CGFloat { get } 55 | 56 | func build(identifier: String) -> Target 57 | func build(identifier: String, width: CGFloat) -> Target 58 | } 59 | 60 | extension IconBuilder { 61 | func build(identifier: String) -> Target { 62 | build(identifier: identifier, width: width) 63 | } 64 | } 65 | 66 | struct NamedIconBuilder: IconBuilder { 67 | typealias Target = NamedIcon 68 | 69 | var name: String 70 | var width: CGFloat 71 | var opacity: CGFloat = 1 72 | 73 | func build(identifier: String, width: CGFloat) -> NamedIcon { 74 | .init(name: "Themes/\(identifier)/" + name, width: width, opacity: opacity) 75 | } 76 | } 77 | 78 | struct SymbolIconBuilder: IconBuilder { 79 | typealias Target = SymbolIcon 80 | 81 | var symbol: SFSymbol 82 | var configuration: NSImage.SymbolConfiguration? 83 | var width: CGFloat 84 | var opacity: CGFloat = 1 85 | 86 | func build(identifier: String, width: CGFloat) -> SymbolIcon { 87 | .init(symbol: symbol, configuration: configuration, width: width, opacity: opacity) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Abyssal/Managers/ScreenManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenManager.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/24. 6 | // 7 | 8 | import AppKit 9 | import Defaults 10 | 11 | struct ScreenManager { 12 | static var main: NSScreen? { 13 | .main 14 | } 15 | 16 | static var frame: NSRect { 17 | main?.frame ?? .zero 18 | } 19 | 20 | static var hasNotch: Bool { 21 | main?.safeAreaInsets.top != 0 22 | } 23 | 24 | static var width: CGFloat { 25 | frame.size.width 26 | } 27 | 28 | static var height: CGFloat { 29 | frame.size.height 30 | } 31 | 32 | static var origin: CGPoint { 33 | frame.origin 34 | } 35 | 36 | static var maxWidth: CGFloat { 37 | let screens = NSScreen.screens 38 | var maxWidth: CGFloat = 0 39 | 40 | for screen in screens { 41 | let screenFrame = screen.visibleFrame 42 | if screenFrame.size.width > maxWidth { 43 | maxWidth = screenFrame.size.width 44 | } 45 | } 46 | 47 | return maxWidth 48 | } 49 | 50 | static var maxHeight: CGFloat { 51 | let screens = NSScreen.screens 52 | var maxHeight: CGFloat = 0 53 | 54 | for screen in screens { 55 | let screenFrame = screen.visibleFrame 56 | if screenFrame.size.height > maxHeight { 57 | maxHeight = screenFrame.size.height 58 | } 59 | } 60 | 61 | return maxHeight 62 | } 63 | 64 | static var menuBarLeftEdge: CGFloat { 65 | let origin = origin 66 | let setting = Defaults[.screenSettings].main 67 | 68 | if hasNotch && setting.respectNotch { 69 | // Respect notch area on screens with notches 70 | let notchWidth = 250.0 71 | return origin.x + width / 2.0 + notchWidth / 2.0 72 | } else { 73 | switch setting.deadZone { 74 | case .percentage(let percentage): 75 | let rightEdge = AppDelegate.shared?.statusBarController.edge ?? width 76 | return origin.x + 50 + (rightEdge - 50) * (percentage / 100) // Apple icon + app name should be at least 50 pixels wide. 77 | case .pixel(let pixel): 78 | return pixel 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Abyssal/Managers/ActivationPolicyManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivationPolicyManager.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/23. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | struct ActivationPolicyManager { 12 | static let identifier = UUID() 13 | private static var fallback: NSApplication.ActivationPolicy = .accessory 14 | 15 | static func set( 16 | _ activationPolicy: NSApplication.ActivationPolicy, 17 | asFallback: Bool = false, 18 | deadline: DispatchTime, 19 | andRun: @escaping () -> Void = {} 20 | ) { 21 | DispatchQueue.main.asyncAfter(identifier, deadline: deadline) { 22 | set(activationPolicy, asFallback: asFallback) 23 | andRun() 24 | } 25 | } 26 | 27 | static func set( 28 | _ activationPolicy: NSApplication.ActivationPolicy, 29 | asFallback: Bool = false 30 | ) { 31 | DispatchQueue.main.cancel(identifier) 32 | NSApp.setActivationPolicy(activationPolicy) 33 | 34 | if asFallback { 35 | fallback = activationPolicy 36 | } 37 | } 38 | 39 | static func setToFallback( 40 | deadline: DispatchTime, 41 | andRun: @escaping () -> Void = {} 42 | ) { 43 | set(fallback, deadline: deadline, andRun: andRun) 44 | } 45 | 46 | static func setToFallback() { 47 | set(fallback) 48 | } 49 | 50 | static func toggleBetweenFallback( 51 | _ activationPolicy: NSApplication.ActivationPolicy, 52 | deadline: DispatchTime, 53 | andRun: @escaping () -> Void = {} 54 | ) -> Bool { 55 | guard activationPolicy != fallback else { 56 | setToFallback(deadline: deadline, andRun: andRun) 57 | return false 58 | } 59 | 60 | if NSApp.activationPolicy() == fallback { 61 | set(activationPolicy, deadline: deadline, andRun: andRun) 62 | return true 63 | } else { 64 | setToFallback(deadline: deadline, andRun: andRun) 65 | return false 66 | } 67 | } 68 | 69 | static func toggleBetweenFallback( 70 | _ activationPolicy: NSApplication.ActivationPolicy 71 | ) -> Bool { 72 | guard activationPolicy != fallback else { 73 | setToFallback() 74 | return false 75 | } 76 | 77 | if NSApp.activationPolicy() == fallback { 78 | set(activationPolicy) 79 | return true 80 | } else { 81 | setToFallback() 82 | return false 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Abyssal/Settings/Sections/BehaviorsSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BehaviorsSection.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/24. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct BehaviorsSection: View { 12 | @Default(.screenSettings) private var screenSettings 13 | 14 | @Environment(\.hasTitle) private var hasTitle 15 | 16 | @State var isScreenInformationPresented: Bool = false 17 | 18 | var body: some View { 19 | Section { 20 | UnifiedVStack { 21 | ActiveStrategyControl() 22 | 23 | RespectNotchControl() 24 | } 25 | 26 | UnifiedVStack { 27 | DeadZoneControl() 28 | } 29 | } header: { 30 | HStack { 31 | if hasTitle { 32 | Text("Behaviors") 33 | } 34 | 35 | Spacer() 36 | 37 | Group { 38 | Box(isOn: $screenSettings.isMainUnique, behavior: .toggle) { 39 | Text("Remember This Screen") 40 | .padding(.horizontal, 10) 41 | .frame(height: 24) 42 | } 43 | 44 | Box(isOn: $isScreenInformationPresented, behavior: .toggle) { 45 | Image(systemSymbol: .info) 46 | .frame(width: 24, height: 24) 47 | } 48 | .popover(isPresented: $isScreenInformationPresented) { 49 | EmptyFormWrapper(normalizePadding: false) { 50 | Section("Screen Information") { 51 | if let id = ScreenManager.main?.displayID { 52 | LabeledContent("Display ID") { 53 | Text(String(id)) 54 | .monospaced() 55 | } 56 | 57 | LabeledContent("Width") { 58 | Text(UnitFormat.pixel.format(Double(ScreenManager.frame.width))) 59 | .monospaced() 60 | } 61 | 62 | LabeledContent("Height") { 63 | Text(UnitFormat.pixel.format(Double(ScreenManager.frame.height))) 64 | .monospaced() 65 | } 66 | } 67 | } 68 | } 69 | .padding() 70 | } 71 | } 72 | .font(.subheadline) 73 | } 74 | } 75 | } 76 | } 77 | 78 | #Preview { 79 | Form { 80 | BehaviorsSection() 81 | } 82 | .formStyle(.grouped) 83 | } 84 | -------------------------------------------------------------------------------- /Abyssal/Settings/Controls/Sliders/DeadZoneControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeadZoneControl.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/29. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct DeadZoneControl: View { 12 | @Default(.screenSettings) private var screenSettings 13 | 14 | @FocusState private var isTextFieldFocused: Bool 15 | 16 | var body: some View { 17 | VStack(alignment: .leading) { 18 | Picker(selection: $screenSettings.main.deadZone.mode) { 19 | ForEach(DeadZone.Mode.allCases, id: \.self) { mode in 20 | switch mode { 21 | case .percentage: 22 | Text("%") 23 | case .pixel: 24 | Text("px") 25 | } 26 | } 27 | } label: { 28 | HStack(alignment: .firstTextBaseline) { 29 | Text("Dead zone") 30 | .opacity(screenSettings.main.respectNotch ? 0.45 : 1) 31 | 32 | Spacer() 33 | 34 | Stepper(value: $screenSettings.main.deadZone.value, in: screenSettings.main.deadZone.range, step: 5) { 35 | TextField(value: $screenSettings.main.deadZone.value, format: .number.precision(.fractionLength(1))) { 36 | EmptyView() 37 | } 38 | .onSubmit { 39 | isTextFieldFocused = false 40 | } 41 | .aspectRatio(contentMode: .fit) 42 | .textFieldStyle(.plain) 43 | 44 | .monospaced() 45 | .multilineTextAlignment(.trailing) 46 | .lineLimit(1) 47 | 48 | .focused($isTextFieldFocused) 49 | .focusable(false) 50 | 51 | .animation(.none, value: screenSettings.main.deadZone) 52 | } 53 | } 54 | } 55 | 56 | EmptyFormWrapper { 57 | TipWrapper(tip: deadZoneTip, alwaysVisible: true, value: $screenSettings.main.deadZone) { tip in 58 | Slider(value: $screenSettings.main.deadZone.value, in: screenSettings.main.deadZone.range) { 59 | EmptyView() 60 | } 61 | } 62 | } 63 | } 64 | .disabled(screenSettings.main.respectNotch) 65 | .animation(.smooth, value: screenSettings.main.deadZone) 66 | .animation(.smooth, value: screenSettings.main.respectNotch) 67 | } 68 | 69 | private let deadZoneTip = Tip(preferredEdge: .minY) { 70 | TipDeadZoneTitle() 71 | } content: { 72 | .init(localized: """ 73 | Controls the dead zone area on the screen. 74 | 75 | **\(Bundle.main.appName)** will treat dead zone area as if it is not a part of the screen, which means only the non dead zone menu bar area is capable for interactions and hiding menu bar items. 76 | """) 77 | } 78 | } 79 | 80 | #Preview { 81 | Form { 82 | DeadZoneControl() 83 | } 84 | .formStyle(.grouped) 85 | } 86 | -------------------------------------------------------------------------------- /Abyssal/Settings/Sections/ModifierSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifierSection.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/22. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct ModifierSection: View { 12 | @Default(.modifier) private var modifiers 13 | @Default(.modifierCompose) private var modifierCompose 14 | 15 | @Environment(\.hasTitle) private var hasTitle 16 | 17 | private let modifierTip = Tip { 18 | .init(localized: """ 19 | The modifier keys to use for showing the **Always Hide Area.** It is recommended to keep `⌘` enabled. 20 | """) 21 | } 22 | 23 | var body: some View { 24 | Section { 25 | VStack(spacing: 8) { 26 | TipWrapper(tip: modifierTip) { tip in 27 | HStack(spacing: 8) { 28 | Box(isOn: $modifiers.control, behavior: .toggle) { 29 | Image(systemSymbol: .control) 30 | .frame(maxWidth: .infinity, maxHeight: .infinity) 31 | } 32 | 33 | Box(isOn: $modifiers.option, behavior: .toggle) { 34 | Image(systemSymbol: .option) 35 | .frame(maxWidth: .infinity, maxHeight: .infinity) 36 | } 37 | 38 | Box(isOn: $modifiers.command, behavior: .toggle) { 39 | Image(systemSymbol: .command) 40 | .frame(maxWidth: .infinity, maxHeight: .infinity) 41 | } 42 | } 43 | .frame(height: 32) 44 | .bold() 45 | .controlSize(.large) 46 | } 47 | 48 | HStack { 49 | // Use a column styled Form to diminish the Picker's empty label 50 | EmptyFormWrapper(normalizePadding: false) { 51 | HStack(alignment: .firstTextBaseline) { 52 | Picker(selection: $modifierCompose) { 53 | ForEach(Modifier.Compose.allCases, id: \.self) { mode in 54 | switch mode { 55 | case .all: Text("all") 56 | case .any: Text("any") 57 | } 58 | } 59 | } label: { 60 | Text("[before modifier mode picker]") 61 | } 62 | 63 | Text("[after modifier mode picker]") 64 | .fixedSize() 65 | } 66 | .foregroundStyle(.placeholder) 67 | } 68 | .fixedSize() 69 | 70 | Spacer() 71 | } 72 | .padding(.horizontal, 2) 73 | } 74 | } header: { 75 | if hasTitle { 76 | Text("Modifier") 77 | } 78 | } 79 | } 80 | } 81 | 82 | #Preview { 83 | Form { 84 | ModifierSection() 85 | } 86 | .formStyle(.grouped) 87 | } 88 | -------------------------------------------------------------------------------- /Abyssal.xcodeproj/xcshareddata/xcschemes/Abyssal.xcscheme: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <Scheme 3 | LastUpgradeVersion = "1600" 4 | version = "1.7"> 5 | <BuildAction 6 | parallelizeBuildables = "YES" 7 | buildImplicitDependencies = "YES" 8 | buildArchitectures = "Automatic"> 9 | <BuildActionEntries> 10 | <BuildActionEntry 11 | buildForTesting = "YES" 12 | buildForRunning = "YES" 13 | buildForProfiling = "YES" 14 | buildForArchiving = "YES" 15 | buildForAnalyzing = "YES"> 16 | <BuildableReference 17 | BuildableIdentifier = "primary" 18 | BlueprintIdentifier = "C66CAB9F2A38871500E4D8E6" 19 | BuildableName = "Abyssal.app" 20 | BlueprintName = "Abyssal" 21 | ReferencedContainer = "container:Abyssal.xcodeproj"> 22 | </BuildableReference> 23 | </BuildActionEntry> 24 | </BuildActionEntries> 25 | </BuildAction> 26 | <TestAction 27 | buildConfiguration = "Debug" 28 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 29 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 30 | shouldUseLaunchSchemeArgsEnv = "YES"> 31 | <TestPlans> 32 | <TestPlanReference 33 | reference = "container:Abyssal.xctestplan" 34 | default = "YES"> 35 | </TestPlanReference> 36 | </TestPlans> 37 | </TestAction> 38 | <LaunchAction 39 | buildConfiguration = "Debug" 40 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 41 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 42 | launchStyle = "0" 43 | useCustomWorkingDirectory = "NO" 44 | ignoresPersistentStateOnLaunch = "NO" 45 | debugDocumentVersioning = "YES" 46 | debugServiceExtension = "internal" 47 | allowLocationSimulation = "YES"> 48 | <BuildableProductRunnable 49 | runnableDebuggingMode = "0"> 50 | <BuildableReference 51 | BuildableIdentifier = "primary" 52 | BlueprintIdentifier = "C66CAB9F2A38871500E4D8E6" 53 | BuildableName = "Abyssal.app" 54 | BlueprintName = "Abyssal" 55 | ReferencedContainer = "container:Abyssal.xcodeproj"> 56 | </BuildableReference> 57 | </BuildableProductRunnable> 58 | </LaunchAction> 59 | <ProfileAction 60 | buildConfiguration = "Release" 61 | shouldUseLaunchSchemeArgsEnv = "YES" 62 | savedToolIdentifier = "" 63 | useCustomWorkingDirectory = "NO" 64 | debugDocumentVersioning = "YES"> 65 | <BuildableProductRunnable 66 | runnableDebuggingMode = "0"> 67 | <BuildableReference 68 | BuildableIdentifier = "primary" 69 | BlueprintIdentifier = "C66CAB9F2A38871500E4D8E6" 70 | BuildableName = "Abyssal.app" 71 | BlueprintName = "Abyssal" 72 | ReferencedContainer = "container:Abyssal.xcodeproj"> 73 | </BuildableReference> 74 | </BuildableProductRunnable> 75 | </ProfileAction> 76 | <AnalyzeAction 77 | buildConfiguration = "Debug"> 78 | </AnalyzeAction> 79 | <ArchiveAction 80 | buildConfiguration = "Release" 81 | revealArchiveInOrganizer = "YES"> 82 | </ArchiveAction> 83 | </Scheme> 84 | -------------------------------------------------------------------------------- /Abyssal/Settings/SettingsVersionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsVersionView.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsVersionView: View { 11 | private let updateTip = Tip(permanent: true) { 12 | switch VersionModel.shared.fetchState { 13 | case .initialized, .finished: 14 | Version.hasUpdate 15 | ? String(localized: """ 16 | An update is available. Click to access the download page. 17 | """) 18 | : String(localized: "Click to check for updates.") 19 | case .fetching: 20 | String(localized: "Fetching for latest version…") 21 | case .failed: 22 | String(localized: "Failed to fetch for latest version.") 23 | } 24 | } 25 | 26 | @State private var versionModel = VersionModel.shared 27 | 28 | @Environment(\.openURL) private var openUrl 29 | 30 | var body: some View { 31 | TipWrapper(tip: updateTip, alwaysVisible: true, value: $versionModel.fetchState) { tip in 32 | HStack { 33 | if versionModel.fetchState == .fetching { 34 | ProgressView() 35 | .controlSize(.small) 36 | } 37 | 38 | Button { 39 | if versionModel.hasUpdate { 40 | // Access the download page 41 | openUrl(.release) 42 | } else { 43 | versionModel.fetchLatest() 44 | } 45 | 46 | tip.hide() 47 | tip.show(nil) 48 | } label: { 49 | if versionModel.fetchState == .failed { 50 | Image(systemSymbol: .exclamationmarkCircleFill) 51 | } else if versionModel.hasUpdate { 52 | Image(systemSymbol: .shiftFill) 53 | } 54 | 55 | let version = versionModel.hasUpdate 56 | ? Text("\(versionModel.app.string) \(Image(systemSymbol: .arrowRight)) \(versionModel.remote.string)") 57 | : Text(versionModel.app.string) 58 | 59 | Text("Version \(version.monospaced())") 60 | .foregroundStyle(.secondary) 61 | } 62 | .buttonStyle(.plain) 63 | .foregroundStyle(versionModel.fetchState == .failed 64 | ? AnyShapeStyle(.red) 65 | : versionModel.hasUpdate 66 | ? AnyShapeStyle(.tint) 67 | : AnyShapeStyle(.placeholder) 68 | ) 69 | .disabled(!versionModel.fetchState.idle) 70 | 71 | #if DEBUG 72 | Button("Debug Fetch State") { 73 | versionModel.fetchState = switch versionModel.fetchState { 74 | case .initialized: .fetching 75 | case .fetching: .finished 76 | case .finished: .failed 77 | case .failed: .initialized 78 | } 79 | } 80 | #endif 81 | } 82 | .frame(height: 24) 83 | } 84 | } 85 | } 86 | 87 | #Preview { 88 | SettingsVersionView() 89 | .padding() 90 | } 91 | -------------------------------------------------------------------------------- /Abyssal/Settings/SettingsTrafficsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsTrafficsView.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/22. 6 | // 7 | 8 | import SwiftUI 9 | import Defaults 10 | 11 | struct SettingsTrafficsView: View { 12 | @Default(.tipsEnabled) private var tipsEnabled 13 | 14 | @Environment(\.openURL) private var openUrl 15 | 16 | var body: some View { 17 | HStack { 18 | // Quit 19 | Box(isOn: false) { 20 | AppDelegate.shared?.closePopover(nil) 21 | 22 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 23 | AppDelegate.shared?.quit(nil) 24 | } 25 | } content: { 26 | HStack { 27 | Image(systemSymbol: .xmark) 28 | .bold() 29 | 30 | Text("Quit \(Bundle.main.appName)") 31 | .fixedSize() 32 | .frame(maxWidth: .infinity) 33 | } 34 | .padding(.horizontal, 12) 35 | .frame(maxHeight: .infinity) 36 | } 37 | .tint(.red) 38 | .foregroundStyle(.red) 39 | .keyboardShortcut("q", modifiers: .command) 40 | 41 | Spacer(minLength: 32) 42 | 43 | // Tips 44 | Box(isOn: $tipsEnabled, behavior: .toggle) { 45 | HStack { 46 | Image(systemSymbol: tipsEnabled ? .tagFill : .tagSlashFill) 47 | .bold() 48 | 49 | Text("Tips") 50 | .fixedSize() 51 | } 52 | .contentTransition(.symbolEffect(.replace)) 53 | .animation(.bouncy, value: tipsEnabled) 54 | 55 | .padding(.horizontal, 12) 56 | .frame(maxHeight: .infinity) 57 | } 58 | 59 | // Source 60 | TipWrapper(tip: sourceTip) { tip in 61 | Box(isOn: false) { 62 | DispatchQueue.main.async { 63 | AppDelegate.shared?.closePopover(nil) 64 | } 65 | 66 | DispatchQueue.main.async { 67 | self.openUrl(.source) 68 | } 69 | } content: { 70 | Image(systemSymbol: .barcode) 71 | .frame(maxWidth: .infinity, maxHeight: .infinity) 72 | } 73 | .frame(width: 32) 74 | } 75 | 76 | // Minimize 77 | Box(isOn: false) { 78 | AppDelegate.shared?.closePopover(nil) 79 | } content: { 80 | Image(systemSymbol: .arrowDownRightAndArrowUpLeft) 81 | .frame(maxWidth: .infinity, maxHeight: .infinity) 82 | } 83 | .frame(width: 32) 84 | .tint(.orange) 85 | .foregroundStyle(.orange) 86 | .keyboardShortcut("w", modifiers: .command) 87 | } 88 | .frame(height: 32) 89 | } 90 | 91 | private let sourceTip = Tip(preferredEdge: .minY) { 92 | .init(localized: """ 93 | **\(Bundle.main.appName)** is open source. Click to access the source code. 94 | """) 95 | } 96 | } 97 | 98 | #Preview { 99 | SettingsTrafficsView() 100 | .padding() 101 | } 102 | -------------------------------------------------------------------------------- /Abyssal/Contents/Separator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Separator.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2023/10/25. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import Defaults 11 | 12 | struct Separator { 13 | init( 14 | order: Int, 15 | _ itemsProvider: @escaping () -> [NSStatusItem] 16 | ) { 17 | self.order = order 18 | self.itemsProvider = itemsProvider 19 | } 20 | 21 | // MARK: - Stored Properties 22 | 23 | var order: Int 24 | 25 | var itemsProvider: () -> [NSStatusItem] 26 | 27 | var item: NSStatusItem { 28 | itemsProvider()[order] 29 | } 30 | 31 | // MARK: - Computed Properties 32 | 33 | var isVisible: Bool { 34 | get { 35 | item.isVisible 36 | } 37 | 38 | set(flag) { 39 | item.isVisible = flag 40 | } 41 | } 42 | 43 | var origin: NSPoint? { 44 | item.origin 45 | } 46 | 47 | var button: NSStatusBarButton? { 48 | item.button 49 | } 50 | 51 | var alpha: CGFloat? { 52 | get { 53 | item.button?.alphaValue 54 | } 55 | 56 | set(alpha) { 57 | item.button?.alphaValue = alpha ?? 0 58 | } 59 | } 60 | 61 | var length: CGFloat { 62 | get { 63 | item.length 64 | } 65 | 66 | set(legnth) { 67 | item.length = legnth 68 | } 69 | } 70 | 71 | private var _targetLength = CGFloat.zero 72 | 73 | var targetAlpha = CGFloat.zero 74 | var targetLength: CGFloat { 75 | get { 76 | _targetLength 77 | } 78 | 79 | set { 80 | _targetLength = min(newValue, 10000) 81 | } 82 | } 83 | 84 | var wasUnstable = false 85 | var wasActive = false 86 | 87 | var lastOrigin: NSPoint? 88 | 89 | var isAvailable: Bool { 90 | if let origin, let width = button?.window?.frame.width { 91 | return origin.x + width > ScreenManager.menuBarLeftEdge 92 | } else { 93 | return true 94 | } 95 | } 96 | } 97 | 98 | extension Separator { 99 | mutating func lerpAlpha(noAnimation: Bool = false) -> Bool { 100 | if noAnimation || Defaults[.reduceAnimationEnabled] { 101 | alpha = targetAlpha 102 | return true 103 | } else if let alpha { 104 | self.alpha = MathHelper.lerp( 105 | a: alpha, 106 | b: targetAlpha, 107 | ratio: MathHelper.lerpRatio, 108 | false 109 | ) 110 | 111 | return MathHelper.approaching(alpha, targetAlpha, false) 112 | } else { 113 | return true 114 | } 115 | } 116 | 117 | mutating func lerpLength(noAnimation: Bool = false) -> Bool { 118 | if noAnimation || Defaults[.reduceAnimationEnabled] { 119 | length = targetLength 120 | return true 121 | } else { 122 | length = MathHelper.lerp( 123 | a: length, 124 | b: targetLength, 125 | ratio: MathHelper.lerpRatio 126 | ) 127 | 128 | return MathHelper.approaching(length, targetLength) 129 | } 130 | } 131 | 132 | mutating func applyAlpha() { 133 | alpha = targetAlpha 134 | } 135 | 136 | mutating func applyLength() { 137 | length = targetLength 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Abyssal/Settings/Views/Box.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Box.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Box<Content>: View 11 | where Content: View { 12 | enum Behavior { 13 | case button 14 | case toggle 15 | } 16 | 17 | @Binding var isOn: Bool 18 | 19 | @ViewBuilder var content: () -> Content 20 | 21 | @State private var isHovering: Bool = false 22 | 23 | var cornerSize: CGSize = .init(width: 8, height: 8) 24 | var behavior: Behavior = .button 25 | var action: () -> Void = {} 26 | 27 | init( 28 | isOn: Bool, 29 | cornerSize: CGSize = .init(width: 8, height: 8), 30 | behavior: Behavior = .button, 31 | action: @escaping () -> Void = {}, 32 | @ViewBuilder content: @escaping () -> Content 33 | ) { 34 | self._isOn = .constant(isOn) 35 | self.cornerSize = cornerSize 36 | self.behavior = behavior 37 | self.action = action 38 | self.content = content 39 | } 40 | 41 | init( 42 | isOn: Binding<Bool>, 43 | cornerSize: CGSize = .init(width: 8, height: 8), 44 | behavior: Behavior = .button, 45 | action: @escaping () -> Void = {}, 46 | @ViewBuilder content: @escaping () -> Content 47 | ) { 48 | self._isOn = isOn 49 | self.cornerSize = cornerSize 50 | self.behavior = behavior 51 | self.action = action 52 | self.content = content 53 | } 54 | 55 | var body: some View { 56 | Button { 57 | action() 58 | 59 | if behavior == .toggle { 60 | isOn.toggle() 61 | } 62 | } label: { 63 | ZStack { 64 | content() 65 | .foregroundStyle(.background) 66 | .opacity(isOn ? 1 : 0) 67 | 68 | content() 69 | .foregroundStyle(.primary) 70 | .opacity(isOn ? 0 : 1) 71 | } 72 | } 73 | .buttonStyle(.borderless) 74 | .buttonBorderShape(.capsule) 75 | .background { 76 | if isOn { 77 | RoundedRectangle(cornerSize: cornerSize) 78 | .fill(.tint) 79 | .strokeBorder(.fill.opacity(isHovering ? 0.25 : 0)) 80 | .brightness(isHovering ? -0.05 : 0) 81 | } else { 82 | if isHovering { 83 | RoundedRectangle(cornerSize: cornerSize) 84 | .fill(.tint.opacity(0.1)) 85 | .strokeBorder(.tint.opacity(0.5)) 86 | .transition(.opacity) 87 | } else { 88 | RoundedRectangle(cornerSize: cornerSize) 89 | .fill(.fill.opacity(0.1)) 90 | .strokeBorder(.fill.opacity(0.5)) 91 | .transition(.opacity) 92 | } 93 | } 94 | } 95 | .animation(.default.speed(2), value: isOn) 96 | .onHover { isHovering in 97 | withAnimation(.default.speed(2)) { 98 | self.isHovering = isHovering 99 | } 100 | } 101 | } 102 | } 103 | 104 | #Preview { 105 | VStack { 106 | Box(isOn: true) { 107 | Text("On") 108 | .padding() 109 | } 110 | 111 | Box(isOn: false) { 112 | Text("Off") 113 | .padding() 114 | } 115 | } 116 | .padding() 117 | } 118 | -------------------------------------------------------------------------------- /Docs/简体中文.md: -------------------------------------------------------------------------------- 1 | <blockquote> 2 | <details> 3 | <summary> 4 | <code>あ ←→ A</code> 5 | </summary> 6 | <!--Head--> 7 |   <sub><b>Abyssal</b>支持以下语言。<a href="/Docs/ADD_A_LOCALIZATION.md"><code>↗ 添加一种语言</code></a></sub> 8 | <br /> 9 | <!--Body--> 10 | <br /> 11 |   <a href="/">English</a> 12 | <br /> 13 |   简体中文 14 | </details> 15 | </blockquote> 16 | 17 | ### <div><!--Empty Lines--><br /><br /></div> 18 | 19 | # <p align="center"><img width="172" src="/Abyssal/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png?raw=true" /><br />Abyssal</p><br /> 20 | 21 | ###### <p align="center">简化、整理、掌控你的macOS菜单栏[^menu_bar]。</p> 22 | 23 | [^menu_bar]: 又称*状态栏。* 24 | 25 | ### <div><!--Empty Lines--><br /><br /></div> 26 | 27 | > [!IMPORTANT] 28 | > 29 | > **Abyssal**需要运行在**macOS 14.0 Sonoma**[^check_your_macos_version]及以上的系统中。 30 | 31 | [^check_your_macos_version]: [`↗ 确定你的 Mac 使用的是哪个 macOS 版本`](https://support.apple.com/zh-cn/HT201260) 32 | 33 | ## 使用手册 34 | 35 | <div align="center"> 36 | <img width="700" src="/Docs/Contents/简体中文/Overview.png?raw=true" /> 37 | </div> 38 | 39 | ### 基础知识 40 | 41 | **Abyssal**将你的菜单栏分成三个区域 - **永远隐藏区域、隐藏区域**以及**显示区域:** 42 | 43 | - **永远隐藏区域** 在此区域内的图标将会被<i>永远隐藏,</i>除非你手动查看它们。 44 | - **隐藏区域** 在此区域内的图标遵循某些显示规则。大多数时候,_你将不会见到它们。_ 45 | - **显示区域** 在此区域内的图标将会正常显示,_不受限制。_ 46 | 47 | 这三个区域被两个分隔符切分 - `永远隐藏分隔符` <sub><picture><source media="(prefers-color-scheme: dark)" srcset="/Docs/Contents/Icons/Dark/DottedLine.png?raw=true" /><img height="17" src="/Docs/Contents/Icons/Light/DottedLine.png?raw=true" /></picture></sub>(位于离屏幕角落最远的一侧)和`隐藏分隔符` <sub><picture><source media="(prefers-color-scheme: dark)" srcset="/Docs/Contents/Icons/Dark/Line.png?raw=true" /><img height="17" src="/Docs/Contents/Icons/Light/Line.png?raw=true" /></picture></sub>(位于中间)。除了这两个分隔符,还有一个特殊分隔符位于离屏幕角落最近的一侧 - `菜单分隔符` <sub><picture><source media="(prefers-color-scheme: dark)" srcset="/Docs/Contents/Icons/Dark/Dot.png?raw=true" /><img height="17" src="/Docs/Contents/Icons/Light/Dot.png?raw=true" /></picture></sub>,它的摆放位置无关紧要,但起着相当大的作用。 48 | 49 | > **Abyssal**会自动判断三个分隔符的顺序,这意味着你不需要手动管理它们。例如,你可以将`菜单分隔符`移到`永远隐藏分隔符`的另一侧,它们会立即自动切换为正确的功能。 50 | 51 | <br /> 52 | 53 | ### 显示和移动分隔符 54 | 55 | 在包括默认主题在内的很多主题中,分隔符默认不可见(显示为透明)。当你<i>打开菜单,</i>或<i>将光标移到菜单栏上[^cursor_onto_status_bar]并按下修饰键</i>时,所有分隔符将变得可见。在其它主题中,分隔符将会一直可见,但它们的外观可能会根据**Abyssal**的状态而改变。 56 | 57 | 通过观察分隔符的外观,你可以得知一些信息: 58 | 59 | - 当使用分隔符默认不可见的主题时,`菜单分隔符`将会指示**隐藏区域**内菜单图标的可见性。如果`菜单分隔符`<b>可见,</b>则**隐藏区域**内的菜单图标<b>被显示,</b>否则这些图标**被隐藏。** 60 | - 当使用分隔符永远可见的主题时,所有分隔符将会一同指示**隐藏区域**内菜单图标的可见性。如果所有分隔符<b>半透明,</b>则**隐藏区域**内的菜单图标<b>被显示,</b>否则这些图标**被隐藏。** 61 | 62 | [^cursor_onto_status_bar]: 你需要将光标移至比`菜单分隔符`离屏幕角落更远的位置以触发动作。在一些带有刘海的屏幕上,你可能还需要将光标置于<i>刘海右侧和`菜单分隔符`之间。</i> 63 | 64 | 在拖动图标的同时按下<kbd>⌘ command</kbd>可以重新排序菜单图标和分隔符。例如,将一些菜单图标移入或移出**隐藏区域.** 65 | 66 | <br /> 67 | 68 | ### 点按分隔符 69 | 70 | 通过点按**Abyssal**的分隔符,你可以出发一些动作,无论分隔符是否可见: 71 | 72 | <br /> 73 | 74 | #### 永远隐藏区域分隔符 75 | 76 | - **<kbd>单击</kbd> / <kbd>右键单击</kbd>** 77 | 78 | **显示 / 隐藏**位于**隐藏区域**内的菜单图标。 79 | 80 | <br /> 81 | 82 | #### 隐藏区域分隔符 83 | 84 | - **<kbd>单击</kbd> / <kbd>右键单击</kbd>** 85 | 86 | **显示 / 隐藏**位于**隐藏区域**内的菜单图标。 87 | 88 | <br /> 89 | 90 | #### 菜单分隔符 91 | 92 | - **<kbd>单击</kbd>** 93 | 94 | **显示 / 隐藏**位于**隐藏区域**内的菜单图标。 95 | 96 | - **<kbd>⌥ option</kbd> <kbd>单击</kbd>** 97 | 98 | **打开 / 关闭**配置菜单。 99 | 100 | <br /> 101 | 102 | ### 特殊功能:自动闲置 103 | 104 | 由于macOS的限制,**Abyssal**无法得知你是否正在与位于**永远隐藏区域**或**隐藏区域**内的菜单图标交互。如果**自动隐藏**功能在鼠标移开菜单栏后立即隐藏这些图标,它们展开的菜单也会随之移开。因此,**Abyssal**采用了一种能够很大限度避免此问题的变通方法。 105 | 106 | 具体来说,当你点击菜单栏上**可能有菜单图标,且这些图标很可能位于隐藏区域或永远隐藏区域内**的区域时,**Abyssal**会自动暂停**自动隐藏**功能并进入**自动闲置**状态。当你完成操作后,只需将光标**移过**`永远隐藏分隔符`或`隐藏分隔符`<b>上方,</b>即可取消**自动闲置**状态并恢复**自动隐藏**功能以隐藏菜单图标。**Abyssal**还提供了一个可选的超时限制设置用于自动取消**自动闲置**状态,你可以在配置菜单中调整它。 107 | 108 | **自动闲置**会判断你的点击位置并在合适的时候启用。它会区分位于**永远隐藏区域**和位于**隐藏区域**的点击位置——不同区域的点击会触发不同的行为。只有在**自动隐藏**功能启用时,**自动闲置**才会起作用。 109 | 110 | 在你**触发或取消自动隐藏或自动闲置**后,**Abyssal**会产生一个Haptic触感反馈[^haptic_feedback_support_needed]。 111 | 112 | [^haptic_feedback_support_needed]: 你的设备需要支持*Haptic触感反馈。* 113 | 114 | <br /> 115 | 116 | ## 安装和运行 117 | 118 | > [!NOTE] 119 | > 120 | > 作为一个开源且免费的软件,**Abyssal**暂无能力支付[Apple开发者账户](https://developer.apple.com/cn/help/account)的费用。因此,你无法从 App Store 中直接安装**Abyssal。**你也许还需要允许**Abyssal**以未认证的应用身份运行[^open_as_unidentified]。 121 | > 122 | > 你暂时只能从[Releases](https://github.com/KrLite/Abyssal/releases)页面下载**Abyssal**的压缩应用程序文件。 123 | 124 | [^open_as_unidentified]: [`↗ 打开来自身份不明开发者的 Mac App`](https://support.apple.com/zh-cn/guide/mac-help/mh40616/mac) 125 | -------------------------------------------------------------------------------- /Abyssal/Miscellaneous/WindowInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowInfo.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/30. 6 | // 7 | 8 | import AppKit 9 | import CoreGraphics 10 | 11 | // https://stackoverflow.com/a/77304045/23452915 12 | struct WindowInfo { 13 | static var allOnScreenWindows: [WindowInfo] { 14 | let windowInfoDicts = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as! [NSDictionary] 15 | return windowInfoDicts.map(WindowInfo.init) 16 | } 17 | 18 | let name: String? 19 | let ownerName: String 20 | let ownerProcessID: pid_t 21 | let layer: Int 22 | let bounds: NSRect 23 | let alpha: Double 24 | let isOnscreen: Bool 25 | let memoryUsage: Measurement<UnitInformationStorage> 26 | let windowNumber: Int 27 | let sharingState: CGWindowSharingType 28 | let backingStoreType: CGWindowBackingType 29 | let otherAttributes: NSDictionary 30 | var isStatusMenuItem: Bool { layer == 25 } 31 | var isThirdPartyItem: Bool { ownerName != "SystemUIServer" && ownerName != "Control Center" } 32 | var isFromAbyssal: Bool { ownerProcessID == ProcessInfo.processInfo.processIdentifier } 33 | } 34 | 35 | extension WindowInfo { 36 | init(fromDict dict: NSDictionary) { 37 | let boundsDict = dict[kCGWindowBounds] as! NSDictionary 38 | 39 | var bounds = NSRect() 40 | assert(CGRectMakeWithDictionaryRepresentation(boundsDict, &bounds)) 41 | 42 | let otherAttributes = NSMutableDictionary(dictionary: dict) 43 | otherAttributes.removeObjects(forKeys: [ 44 | kCGWindowName, 45 | kCGWindowOwnerName, 46 | kCGWindowOwnerPID, 47 | kCGWindowLayer, 48 | kCGWindowBounds, 49 | kCGWindowAlpha, 50 | kCGWindowIsOnscreen, 51 | kCGWindowMemoryUsage, 52 | kCGWindowNumber, 53 | kCGWindowSharingState, 54 | kCGWindowStoreType, 55 | ]) 56 | 57 | self.init( 58 | name: dict[kCGWindowName] as! String?, 59 | ownerName: dict[kCGWindowOwnerName] as! String, 60 | ownerProcessID: dict[kCGWindowOwnerPID] as! pid_t, 61 | layer: dict[kCGWindowLayer] as! Int, 62 | bounds: bounds, 63 | alpha: dict[kCGWindowAlpha] as! Double, 64 | isOnscreen: dict[kCGWindowIsOnscreen] as! Bool, 65 | memoryUsage: Measurement<UnitInformationStorage>(value: dict[kCGWindowMemoryUsage] as! Double, unit: .bytes), 66 | windowNumber: dict[kCGWindowNumber] as! Int, 67 | sharingState: CGWindowSharingType(rawValue: dict[kCGWindowSharingState] as! UInt32)!, 68 | backingStoreType: CGWindowBackingType(rawValue: dict[kCGWindowStoreType] as! UInt32)!, 69 | otherAttributes: otherAttributes 70 | ) 71 | } 72 | } 73 | 74 | extension WindowInfo { 75 | var processRelatedWindows: [WindowInfo] { 76 | WindowInfo.allOnScreenWindows 77 | .filter { $0.ownerProcessID == ownerProcessID } 78 | .filter { $0.windowNumber != windowNumber } 79 | } 80 | 81 | var containsMouse: Bool { 82 | // Change mouse coordinate (with an upside y-axis) to screen coordinate (with a down y-axis) 83 | let mouseInScreen = NSEvent.mouseLocation 84 | .applying(.init(translationX: 0, y: -ScreenManager.frame.height)) 85 | .applying(.init(scaleX: 1, y: -1)) 86 | 87 | return bounds.contains(mouseInScreen) 88 | } 89 | 90 | func isPlacingNear(_ rect: NSRect, edge: NSRectEdge) -> Bool { 91 | let gap: Double = 25 92 | switch edge { 93 | case .minX: 94 | let verticallyOK = inGap(bounds.maxX, rect.minX, gap: gap) 95 | let horizontallyOK = bounds.minY <= rect.minY && bounds.maxY >= rect.maxY 96 | return verticallyOK && horizontallyOK 97 | case .minY: 98 | let verticallyOK = inGap(bounds.maxY, rect.minY, gap: gap) 99 | let horizontallyOK = bounds.minX <= rect.minX && bounds.maxX >= rect.maxX 100 | return verticallyOK && horizontallyOK 101 | case .maxX: 102 | let verticallyOK = inGap(bounds.minX, rect.maxX, gap: gap) 103 | let horizontallyOK = bounds.minY <= rect.minY && bounds.maxY >= rect.maxY 104 | return verticallyOK && horizontallyOK 105 | case .maxY: 106 | let verticallyOK = inGap(bounds.minY, rect.maxY, gap: gap) 107 | let horizontallyOK = bounds.minX <= rect.minX && bounds.maxX >= rect.maxX 108 | return verticallyOK && horizontallyOK 109 | @unknown default: 110 | return false 111 | } 112 | } 113 | 114 | private func inGap(_ a: Double, _ b: Double, gap: Double) -> Bool { 115 | abs(a - b) <= abs(gap) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Abyssal/mul.lproj/Main.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "08v-Vw-Cs3.title" : { 5 | "comment" : "Class = \"NSTextFieldCell\"; title = \"Starts with macOS\"; ObjectID = \"08v-Vw-Cs3\";", 6 | "extractionState" : "stale", 7 | "localizations" : { 8 | "en" : { 9 | "stringUnit" : { 10 | "state" : "new", 11 | "value" : "Starts with macOS" 12 | } 13 | }, 14 | "zh-Hans" : { 15 | "stringUnit" : { 16 | "state" : "translated", 17 | "value" : "随macOS启动" 18 | } 19 | } 20 | } 21 | }, 22 | "AYu-sK-qS6.title" : { 23 | "comment" : "Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\";", 24 | "extractionState" : "extracted_with_value", 25 | "localizations" : { 26 | "en" : { 27 | "stringUnit" : { 28 | "state" : "new", 29 | "value" : "Main Menu" 30 | } 31 | }, 32 | "zh-Hans" : { 33 | "stringUnit" : { 34 | "state" : "translated", 35 | "value" : "" 36 | } 37 | } 38 | } 39 | }, 40 | "BCa-un-ZBt.title" : { 41 | "comment" : "Class = \"NSButtonCell\"; title = \"Quit app\"; ObjectID = \"BCa-un-ZBt\";", 42 | "extractionState" : "stale", 43 | "localizations" : { 44 | "en" : { 45 | "stringUnit" : { 46 | "state" : "new", 47 | "value" : "Quit app" 48 | } 49 | }, 50 | "zh-Hans" : { 51 | "stringUnit" : { 52 | "state" : "translated", 53 | "value" : "退出应用" 54 | } 55 | } 56 | } 57 | }, 58 | "CAQ-b2-2Ky.title" : { 59 | "comment" : "Class = \"NSButtonCell\"; title = \"Tips\"; ObjectID = \"CAQ-b2-2Ky\";", 60 | "extractionState" : "stale", 61 | "localizations" : { 62 | "en" : { 63 | "stringUnit" : { 64 | "state" : "new", 65 | "value" : "Tips" 66 | } 67 | }, 68 | "zh-Hans" : { 69 | "stringUnit" : { 70 | "state" : "translated", 71 | "value" : "提示" 72 | } 73 | } 74 | } 75 | }, 76 | "E4B-vn-v6T.title" : { 77 | "comment" : "Class = \"NSTextFieldCell\"; title = \"Feedback intensity\"; ObjectID = \"E4B-vn-v6T\";", 78 | "extractionState" : "stale", 79 | "localizations" : { 80 | "en" : { 81 | "stringUnit" : { 82 | "state" : "new", 83 | "value" : "Feedback intensity" 84 | } 85 | }, 86 | "zh-Hans" : { 87 | "stringUnit" : { 88 | "state" : "translated", 89 | "value" : "反馈强度" 90 | } 91 | } 92 | } 93 | }, 94 | "f3z-uV-2YB.title" : { 95 | "comment" : "Class = \"NSTextFieldCell\"; title = \"Reduce animation\"; ObjectID = \"f3z-uV-2YB\";", 96 | "extractionState" : "stale", 97 | "localizations" : { 98 | "en" : { 99 | "stringUnit" : { 100 | "state" : "new", 101 | "value" : "Reduce animation" 102 | } 103 | }, 104 | "zh-Hans" : { 105 | "stringUnit" : { 106 | "state" : "translated", 107 | "value" : "减少动画" 108 | } 109 | } 110 | } 111 | }, 112 | "H6v-Qm-J1v.title" : { 113 | "comment" : "Class = \"NSTextFieldCell\"; title = \"Theme\"; ObjectID = \"H6v-Qm-J1v\";", 114 | "extractionState" : "stale", 115 | "localizations" : { 116 | "en" : { 117 | "stringUnit" : { 118 | "state" : "new", 119 | "value" : "Theme" 120 | } 121 | }, 122 | "zh-Hans" : { 123 | "stringUnit" : { 124 | "state" : "translated", 125 | "value" : "主题" 126 | } 127 | } 128 | } 129 | }, 130 | "hml-Gq-OgY.title" : { 131 | "comment" : "Class = \"NSTextFieldCell\"; title = \"Timeout\"; ObjectID = \"hml-Gq-OgY\";", 132 | "extractionState" : "stale", 133 | "localizations" : { 134 | "en" : { 135 | "stringUnit" : { 136 | "state" : "new", 137 | "value" : "Timeout" 138 | } 139 | }, 140 | "zh-Hans" : { 141 | "stringUnit" : { 142 | "state" : "translated", 143 | "value" : "超时" 144 | } 145 | } 146 | } 147 | }, 148 | "MUA-YQ-vfC.title" : { 149 | "comment" : "Class = \"NSTextFieldCell\"; title = \"Dead zone\"; ObjectID = \"MUA-YQ-vfC\";", 150 | "extractionState" : "stale", 151 | "localizations" : { 152 | "en" : { 153 | "stringUnit" : { 154 | "state" : "new", 155 | "value" : "Dead zone" 156 | } 157 | }, 158 | "zh-Hans" : { 159 | "stringUnit" : { 160 | "state" : "translated", 161 | "value" : "盲区宽度" 162 | } 163 | } 164 | } 165 | }, 166 | "nbo-M8-Xyb.title" : { 167 | "comment" : "Class = \"NSTextFieldCell\"; title = \"to trigger\"; ObjectID = \"nbo-M8-Xyb\";", 168 | "extractionState" : "stale", 169 | "localizations" : { 170 | "en" : { 171 | "stringUnit" : { 172 | "state" : "new", 173 | "value" : "to trigger" 174 | } 175 | }, 176 | "zh-Hans" : { 177 | "stringUnit" : { 178 | "state" : "translated", 179 | "value" : "修饰键以触发" 180 | } 181 | } 182 | } 183 | }, 184 | "o5n-Ec-8Il.title" : { 185 | "comment" : "Class = \"NSTextFieldCell\"; title = \"Press\"; ObjectID = \"o5n-Ec-8Il\";", 186 | "extractionState" : "stale", 187 | "localizations" : { 188 | "en" : { 189 | "stringUnit" : { 190 | "state" : "new", 191 | "value" : "Press" 192 | } 193 | }, 194 | "zh-Hans" : { 195 | "stringUnit" : { 196 | "state" : "translated", 197 | "value" : "按下" 198 | } 199 | } 200 | } 201 | }, 202 | "ots-RU-Cm0.title" : { 203 | "comment" : "Class = \"NSTextFieldCell\"; title = \"Auto shows\"; ObjectID = \"ots-RU-Cm0\";", 204 | "extractionState" : "stale", 205 | "localizations" : { 206 | "en" : { 207 | "stringUnit" : { 208 | "state" : "new", 209 | "value" : "Auto shows" 210 | } 211 | }, 212 | "zh-Hans" : { 213 | "stringUnit" : { 214 | "state" : "translated", 215 | "value" : "自动显示" 216 | } 217 | } 218 | } 219 | }, 220 | "TpP-gd-jMY.title" : { 221 | "comment" : "Class = \"NSTextFieldCell\"; title = \"Use always hide area\"; ObjectID = \"TpP-gd-jMY\";", 222 | "extractionState" : "stale", 223 | "localizations" : { 224 | "en" : { 225 | "stringUnit" : { 226 | "state" : "new", 227 | "value" : "Use always hide area" 228 | } 229 | }, 230 | "zh-Hans" : { 231 | "stringUnit" : { 232 | "state" : "translated", 233 | "value" : "使用永远隐藏区域" 234 | } 235 | } 236 | } 237 | } 238 | }, 239 | "version" : "1.0" 240 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <blockquote> 2 | <details> 3 | <summary> 4 | <code>あ ←→ A</code> 5 | </summary> 6 | <!--Head--> 7 |   <sub><b>Abyssal</b> supports the following languages. <a href="/Docs/ADD_A_LOCALIZATION.md"><code>↗ Add a localization</code></a></sub> 8 | <br /> 9 | <!--Body--> 10 | <br /> 11 |   English 12 | <br /> 13 |   <a href="/Docs/简体中文.md">简体中文</a> 14 | </details> 15 | </blockquote> 16 | 17 | ### <div><!--Empty Lines--><br /><br /></div> 18 | 19 | # <p align="center"><img width="172" src="/Abyssal/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png?raw=true" /><br />Abyssal</p><br /> 20 | 21 | ###### <p align="center">Simplify, tidy and master your macOS menu bar[^menu_bar].</p> 22 | 23 | [^menu_bar]: Also known as _Status Bar._ 24 | 25 | ### <div><!--Empty Lines--><br /><br /></div> 26 | 27 | > [!IMPORTANT] 28 | > 29 | > **Abyssal** requires **macOS 14.0 Sonoma**[^check_your_macos_version] or above to run. 30 | 31 | [^check_your_macos_version]: [`↗ Find out which macOS your Mac is using`](https://support.apple.com/en-us/HT201260) 32 | 33 | ## Introduction & Usage 34 | 35 | <div align="center"> 36 | <img width="700" src="/Docs/Contents/English/Overview.png?raw=true" /> 37 | </div> 38 | 39 | ### Fundamentals 40 | 41 | **Abyssal** divides your menu bar into three areas - the **Always Hide Area,** the **Hide Area** and the **Visible Area:** 42 | 43 | - The **Always Hide Area** Icons inside this area will be _hided forever,_ unless you menually check them. 44 | - The **Hide Area** Icons inside this area follow certain rules. More often than not, you _don't see them._ 45 | - The **Visible Area** Icons inside this area suffer no restrictions. You can see them _all the time._ 46 | 47 | The three areas are separated by two separators - the `Always Hide Separator` <sub><picture><source media="(prefers-color-scheme: dark)" srcset="/Docs/Contents/Icons/Dark/DottedLine.png?raw=true" /><img height="17" src="/Docs/Contents/Icons/Light/DottedLine.png?raw=true" /></picture></sub> (the furthest one from the screen corner) and the `Hide Separator` <sub><picture><source media="(prefers-color-scheme: dark)" srcset="/Docs/Contents/Icons/Dark/Line.png?raw=true" /><img height="17" src="/Docs/Contents/Icons/Light/Line.png?raw=true" /></picture></sub> (the middle one). Apart from these, there's another separator on the nearest side to the screen corner - the `Menu Separator` <sub><picture><source media="(prefers-color-scheme: dark)" srcset="/Docs/Contents/Icons/Dark/Dot.png?raw=true" /><img height="17" src="/Docs/Contents/Icons/Light/Dot.png?raw=true" /></picture></sub>, which's position doesn't matter, but plays an important role. 48 | 49 | > **Abyssal** will automatically judge the order of the three separators, which means you don't need to care much about their position. For example, you are allowed to put the `Menu Separator` to the other side of the `Always Hide Separator`, as they will automatically swap their roles to the correct ones after your operation. 50 | 51 | <br /> 52 | 53 | ### Showing & Moving the Separators 54 | 55 | In many themes including the default theme, the separators are invisible (transparent) by default. If you _open the menu,_ or _move your cursor onto the menu bar[^cursor_onto_status_bar] and press the chosen modifiers,_ the separators will be visible (partly opaque). In the rest of the themes, the separators will always be visible, but their appearance may change automatically according to the status of **Abyssal** 56 | 57 | The visibilities of the separators can also indicate: 58 | 59 | - When using themes that automatically hide the separators, the `Menu Separator` will indicate the visibility of the status icons inside the **Hide Area**. If the `Menu Separator` **is visible,** then the status icons inside the **Hide Area** are **visible.** Otherwise the icons are **hidden.** 60 | - When using other themes, all the separators perform together. If all of them are **translucent,** then the status icons inside the **Hide Area** are **visible.** Otherwise the icons are **hidden.** 61 | 62 | [^cursor_onto_status_bar]: You need to move your cursor further away from the screen corner than the `Menu Separator` in order to trigger something. On monitors with notches, you may also need to move your cursor _between the the screen notch and the `Menu Separator`._ 63 | 64 | Dragging the icons while holding <kbd>⌘ command</kbd> can reorder the status icons and the separators. For example, to put more icons into or out of the **Hide Area.** 65 | 66 | <br /> 67 | 68 | ### Clicking on the Separators 69 | 70 | You can perform different actions by clicking on the separators of **Abyssal,** no matter whether they are visible: 71 | 72 | <br /> 73 | 74 | #### The Always Hide Separator 75 | 76 | - **<kbd>click</kbd> / <kbd>right click</kbd>** 77 | 78 | **Show / hide** the status icons inside the **Hide Area.** 79 | 80 | <br /> 81 | 82 | #### The Hide Separator 83 | 84 | - **<kbd>click</kbd> / <kbd>right click</kbd>** 85 | 86 | **Show / hide** the status icons inside the **Hide Area.** 87 | 88 | <br /> 89 | 90 | #### The Menu Separator 91 | 92 | - **<kbd>click</kbd>** 93 | 94 | **Show / hide** the status icons inside the **Hide Area.** 95 | 96 | - **<kbd>⌥ option</kbd> <kbd>click</kbd>** 97 | 98 | **Open / close** the preferences menu. 99 | 100 | <br /> 101 | 102 | ### What's More: Auto Idling 103 | 104 | Due to the limitations of macOS, **Abyssal** cannot know whether you have opened a menu in the **Always Hide Area** or the **Hide Area.** If the **Auto Hide** function hides these status icons rashly after your cursor leave the menu bar, their menus will also move away. Therefore, **Abyssal** adopts an approach to avoid similar situations to the greatest extent. 105 | 106 | Speaking specifically, when you click on a place in the menu bar **where there is likely to have other status icons, and the status icon is likely to be inside the Hide Area or the Always Hide Area,** **Abyssal** will choose to pause the **Auto Hide** and enter the **Auto Idling** state. When you finish the operation, just move the cursor **over** the `Always Hide Separator` or the `Hide Separator`, and you can cancel the **Auto Idling** state and resume **Auto Hide** to hide the status icons. **Abyssal** also provides an optional timeout to automatically disable the **Auto Idling** state, which can be configured in the preferences menu. 107 | 108 | **Auto Idling** will enable automatically accordng to your clicking position, and it will distinguish between the **Always Hide Area** and the **Hide Area** - different areas trigger different reactions. It will only be activated when **Auto Hide** is enabled. 109 | 110 | After you **triggered or canceled** **Auto Hide or Auto Idling,** **Abyssal** will generate a haptic feedback[^haptic_feedback_support_needed]. 111 | 112 | [^haptic_feedback_support_needed]: Your device must support _haptic feedback._ 113 | 114 | <br /> 115 | 116 | ## Install & Run 117 | 118 | > [!NOTE] 119 | > 120 | > As an open source and free software, **Abyssal** can't afford an [Apple Developer Account.](https://developer.apple.com/help/account) Therefore, you can't install **Abyssal** directly from App Store, and you may need to allow **Abyssal** to run as an unidentified app[^open_as_unidentified]. 121 | > 122 | > You can download the zipped app of **Abyssal** only from [Releases](https://github.com?KrLite/Abyssal/releases) page manually for now. 123 | 124 | [^open_as_unidentified]: [`↗ Open a Mac app from an unidentified developer`](https://support.apple.com/guide/mac-help/mh40616/mac) 125 | -------------------------------------------------------------------------------- /Abyssal/Models/VersionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionModel.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Version: Codable { 11 | enum Component: Codable { 12 | case number(UInt) 13 | case beta 14 | case alpha 15 | case patch 16 | case blank 17 | 18 | var nonNumeric: Bool { 19 | switch self { 20 | case .number(_), .blank: false 21 | case .beta, .alpha, .patch: true 22 | } 23 | } 24 | 25 | var semantic: String { 26 | switch self { 27 | case .number(let uInt): 28 | String(uInt) 29 | case .beta: 30 | "beta" 31 | case .alpha: 32 | "alpha" 33 | case .patch: 34 | "patch" 35 | case .blank: 36 | "<blank>" 37 | } 38 | } 39 | 40 | var separator: String { 41 | nonNumeric ? "-" : "." 42 | } 43 | } 44 | 45 | var components: [Component] 46 | 47 | var string: String { 48 | components 49 | .reduce("") { result, component in 50 | if result.isEmpty { 51 | component.semantic 52 | } else { 53 | result + component.separator + component.semantic 54 | } 55 | } 56 | } 57 | 58 | static var empty: Version = .init(components: []) 59 | static var app: Version { 60 | .init(from: Bundle.main.appVersion) ?? .empty 61 | } 62 | static var remote: Version { 63 | VersionModel.shared.fetchedRemoteVersion 64 | } 65 | 66 | static var hasUpdate: Bool { 67 | app < remote 68 | } 69 | 70 | init(components: [Component]) { 71 | self.components = components 72 | } 73 | 74 | init?(from: String) { 75 | let parts = from 76 | .replacing(/\s/, with: "") 77 | .split(separator: /[\.-]/) // Split by `.` or `-` 78 | let components = parts.compactMap({ Component(parsing: String($0)) }) 79 | 80 | if components.isEmpty { 81 | return nil 82 | } else { 83 | self.components = components 84 | } 85 | } 86 | } 87 | 88 | extension Version.Component: Comparable { 89 | static func <(lhs: Self, rhs: Self) -> Bool { 90 | guard lhs != rhs else { return false } 91 | 92 | return switch lhs { 93 | case .number(let this): 94 | switch rhs { 95 | case .number(let other): 96 | this < other 97 | case .beta, .alpha, .patch, .blank: false 98 | } 99 | case .beta: 100 | switch rhs { 101 | case .number(_), .blank: true 102 | case .beta, .alpha, .patch: false 103 | } 104 | case .alpha: 105 | switch rhs { 106 | case .number(_), .beta, .blank: true 107 | case .alpha, .patch: false 108 | } 109 | case .patch: 110 | switch rhs { 111 | case .number(_), .beta, .alpha, .patch, .blank: false 112 | } 113 | case .blank: 114 | switch rhs { 115 | case .number(_): true 116 | case .beta, .alpha, .patch, .blank: false 117 | } 118 | } 119 | } 120 | } 121 | 122 | extension Version.Component { 123 | init?(parsing: String) { 124 | for nonNumeric in Self.nonNumerics { 125 | if parsing.lowercased() == nonNumeric.semantic.lowercased() { 126 | self = nonNumeric 127 | return 128 | } 129 | } 130 | 131 | if let number = UInt(parsing) { 132 | self = .number(number) 133 | return 134 | } 135 | 136 | return nil 137 | } 138 | 139 | static var iterables: [Self] { 140 | [.beta, .alpha, .patch, .blank] 141 | } 142 | 143 | static var nonNumerics: [Self] { 144 | iterables.filter(\.nonNumeric) 145 | } 146 | } 147 | 148 | extension Version: Comparable { 149 | static func <(lhs: Self, rhs: Self) -> Bool { 150 | guard lhs != rhs else { return false } 151 | 152 | let count = max(lhs.components.count, rhs.components.count) 153 | for index in 0..<count { 154 | let lhsComponent = index < lhs.components.count ? lhs.components[index] : .blank 155 | let rhsComponent = index < rhs.components.count ? rhs.components[index] : .blank 156 | 157 | if lhsComponent < rhsComponent { 158 | return true 159 | } else if lhsComponent > rhsComponent { 160 | return false 161 | } 162 | 163 | // Two components are equal, continue 164 | } 165 | 166 | return false 167 | } 168 | } 169 | 170 | @Observable 171 | class VersionModel { 172 | static var shared = VersionModel() 173 | 174 | enum FetchState { 175 | case initialized // Before first fetch 176 | case fetching 177 | case finished // Fetch succeed 178 | case failed // Fetch failed 179 | 180 | var idle: Bool { 181 | switch self { 182 | case .fetching: 183 | false 184 | case .initialized, .finished, .failed: 185 | true 186 | } 187 | } 188 | } 189 | 190 | fileprivate var fetchedRemoteVersion: Version = .app 191 | 192 | private var task: URLSessionTask? 193 | 194 | var fetchState: FetchState = .initialized 195 | 196 | var empty: Version { 197 | .empty 198 | } 199 | 200 | var app: Version { 201 | .app 202 | } 203 | 204 | var remote: Version { 205 | fetchedRemoteVersion 206 | } 207 | 208 | var hasUpdate: Bool { 209 | Version.hasUpdate 210 | } 211 | 212 | func fetchLatest() { 213 | task?.cancel() 214 | 215 | print("Started fetching latest version...") 216 | fetchState = .fetching 217 | 218 | task = URLSession.shared.dataTask(with: .releaseTags) { (data, response, error) in 219 | guard let data else { 220 | self.fetchState = .failed 221 | return 222 | } 223 | 224 | do { 225 | if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] { 226 | let tags = json 227 | .compactMap { element in 228 | element["name"] as? String 229 | } 230 | .compactMap { Version(from: $0) } 231 | .sorted(by: >) 232 | 233 | if let remote = tags.first, remote > .app { 234 | self.fetchedRemoteVersion = remote 235 | print("Fetched latest version: \(remote.string)") 236 | print(self.app < self.remote) 237 | } else { 238 | print("No newer version available.") 239 | } 240 | 241 | self.fetchState = .finished 242 | } 243 | } catch { 244 | self.fetchState = .failed 245 | print(error.localizedDescription) 246 | } 247 | } 248 | task?.resume() 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /Abyssal/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2023/6/13. 6 | // 7 | 8 | import Cocoa 9 | import SwiftUI 10 | import AppKit 11 | import Defaults 12 | import LaunchAtLogin 13 | 14 | let repository = "Cement-Labs/Abyssal" 15 | 16 | //@main 17 | class AppDelegate: NSObject, NSApplicationDelegate { 18 | static var shared: AppDelegate? { 19 | NSApplication.shared.delegate as? AppDelegate 20 | } 21 | 22 | let popover: NSPopover = NSPopover() 23 | 24 | let statusBarController = StatusBarController() 25 | 26 | // MARK: - Event Monitors 27 | 28 | var mouseEventMonitor: EventMonitor? 29 | 30 | // MARK: - Application Methods 31 | 32 | func applicationDidFinishLaunching( 33 | _ aNotification: Notification 34 | ) { 35 | // Set activation policy to `prohibited` after launched 36 | ActivationPolicyManager.set(.prohibited, asFallback: true) 37 | 38 | // Initialize view controller 39 | let controller = SettingsViewController() 40 | controller.view = NSHostingView(rootView: SettingsView()) 41 | popover.contentViewController = controller 42 | 43 | // Pre-initialize view frame 44 | controller.initializeFrame() 45 | 46 | // Fetch latest version 47 | VersionModel.shared.fetchLatest() 48 | 49 | popover.behavior = .applicationDefined 50 | popover.delegate = self 51 | 52 | mouseEventMonitor = EventMonitor( 53 | mask: [.leftMouseDown, 54 | .rightMouseDown] 55 | ) { [weak self] event in 56 | if let strongSelf = self { 57 | if strongSelf.popover.isShown { 58 | // Close popover when clicked outside 59 | strongSelf.closePopover(event) 60 | } 61 | } 62 | } 63 | } 64 | 65 | func applicationWillTerminate( 66 | _ aNotification: Notification 67 | ) { 68 | } 69 | 70 | func applicationSupportsSecureRestorableState( 71 | _ app: NSApplication 72 | ) -> Bool { 73 | true 74 | } 75 | } 76 | 77 | extension AppDelegate: NSPopoverDelegate { 78 | func popoverShouldDetach(_ popover: NSPopover) -> Bool { 79 | true 80 | } 81 | } 82 | 83 | extension AppDelegate { 84 | @objc func quit( 85 | _ sender: Any? 86 | ) { 87 | NSApplication.shared.terminate(sender) 88 | } 89 | 90 | @objc func escapeFromOverridingMenuBar( 91 | _ sender: Any? 92 | ) { 93 | if popover.isShown { 94 | closePopover(sender) 95 | } else { 96 | ActivationPolicyManager.set(.prohibited, asFallback: true) 97 | statusBarController.unidleHideArea() 98 | } 99 | } 100 | 101 | // MARK: - Toggles 102 | 103 | @objc func toggle( 104 | _ sender: Any? 105 | ) { 106 | guard sender as? NSStatusBarButton == AppDelegate.shared?.statusBarController.head.button else { 107 | toggleActive(sender) 108 | return 109 | } 110 | 111 | if KeyboardModel.shared.option { 112 | togglePopover(sender) 113 | } else { 114 | if let event = NSApp.currentEvent, event.type == .rightMouseUp { 115 | togglePopover(sender) 116 | } else { 117 | toggleActive(sender) 118 | } 119 | } 120 | } 121 | 122 | @objc func toggleActive( 123 | _ sender: Any? 124 | ) { 125 | statusBarController.function() 126 | 127 | guard !(statusBarController.idling.hide || statusBarController.idling.alwaysHide) else { 128 | statusBarController.unidleHideArea() 129 | return 130 | } 131 | 132 | if Defaults[.isActive] { 133 | statusBarController.deactivate() 134 | } else { 135 | statusBarController.activate() 136 | } 137 | } 138 | 139 | @objc func togglePopover( 140 | _ sender: Any? 141 | ) { 142 | if popover.isShown { 143 | closePopover(sender) 144 | } else { 145 | showPopover(sender) 146 | } 147 | } 148 | 149 | func showPopover( 150 | _ sender: Any? 151 | ) { 152 | if let controller = popover.contentViewController { 153 | if let button = statusBarController.head.button ?? sender as? NSButton { 154 | // Position popover 155 | 156 | let buttonRect = button.convert(button.bounds, to: nil) 157 | let screenRect = button.window!.convertToScreen(buttonRect) 158 | 159 | let invisiblePanel = NSPanel( 160 | contentRect: NSMakeRect(0, 0, 1, 5), 161 | styleMask: [.borderless], 162 | backing: .buffered, 163 | defer: false, 164 | screen: .main 165 | ) 166 | invisiblePanel.isFloatingPanel = true 167 | invisiblePanel.alphaValue = 0 168 | 169 | invisiblePanel.setFrameOrigin(NSPoint( 170 | x: screenRect.maxX, 171 | y: screenRect.maxY 172 | )) 173 | invisiblePanel.makeKeyAndOrderFront(nil) 174 | 175 | popover.show( 176 | relativeTo: invisiblePanel.contentView!.frame, 177 | of: invisiblePanel.contentView!, 178 | preferredEdge: .maxY 179 | ) 180 | 181 | // Set to foreground activation policy 182 | 183 | let overridesMenuBar = Defaults[.autoOverridesMenuBarEnabled] 184 | let activationPolicy: NSApplication.ActivationPolicy = overridesMenuBar ? .regular : .accessory 185 | 186 | Defaults[.menuBarOverride].apply() 187 | ActivationPolicyManager.set(activationPolicy, asFallback: true) 188 | NSApp.activate() 189 | 190 | DispatchQueue.main.async(popover) { 191 | controller.viewWillAppear() 192 | controller.view.window?.makeKeyAndOrderFront(nil) 193 | controller.viewDidAppear() 194 | } 195 | } 196 | 197 | //mouseEventMonitor?.start() 198 | } 199 | } 200 | 201 | func closePopover( 202 | _ sender: Any? 203 | ) { 204 | if let controller = popover.contentViewController { 205 | DispatchQueue.main.async(popover) { 206 | controller.viewWillDisappear() 207 | self.popover.close() // Force it to close, thus closing all nested popovers 208 | controller.viewDidDisappear() 209 | } 210 | 211 | // Restore activation policy 212 | 213 | // 1. Set to `accessory` after closed to prevent the popover from not being able to open properly again 214 | ActivationPolicyManager.set(.accessory, asFallback: true, deadline: .now() + 0.2) { 215 | // 2. Set to `prohibited` asynchronously to order out 216 | ActivationPolicyManager.set(.prohibited, asFallback: true, deadline: .now()) 217 | } 218 | 219 | // Stop functioning 220 | 221 | mouseEventMonitor?.stop() 222 | statusBarController.function() 223 | statusBarController.triggerIgnoring() 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /Abyssal/Menu Bar/StatusBarController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusBarController.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2023/6/13. 6 | // 7 | 8 | import AppKit 9 | import Defaults 10 | 11 | class StatusBarController { 12 | // MARK: - Lazy States 13 | 14 | lazy var mouseOnStatusBar: WithIntermediateState<Bool> = .init { 15 | guard 16 | let headOrigin = self.head.button?.window?.frame.origin, 17 | let headSize = self.head.button?.window?.frame.size 18 | else { return false } 19 | let mouseLocation = NSEvent.mouseLocation 20 | return mouseLocation.x >= ScreenManager.menuBarLeftEdge && mouseLocation.y >= headOrigin.y && mouseLocation.y <= headOrigin.y + headSize.height 21 | } 22 | 23 | lazy var mouseInHideArea: WithIntermediateState<Bool> = .init { 24 | guard 25 | let bodyOrigin = self.body.button?.window?.frame.origin, 26 | let tailOrigin = self.tail.button?.window?.frame.origin, 27 | let tailSize = self.tail.button?.window?.frame.size 28 | else { return false } 29 | return self.mouseOnStatusBar.value() && NSEvent.mouseLocation.x >= tailOrigin.x + tailSize.width && NSEvent.mouseLocation.x <= bodyOrigin.x 30 | } 31 | 32 | lazy var mouseInAlwaysHideArea: WithIntermediateState<Bool> = .init { 33 | guard let origin = self.tail.button?.window?.frame.origin else { return false } 34 | return self.mouseOnStatusBar.value() && NSEvent.mouseLocation.x <= origin.x 35 | } 36 | 37 | lazy var mouseSpare: WithIntermediateState<Bool> = .init { 38 | !self.ignoring && self.mouseOnStatusBar.value() && NSEvent.mouseLocation.x <= self.edge 39 | } 40 | 41 | 42 | 43 | lazy var mouseOverHead: WithIntermediateState<Bool> = .init { 44 | guard 45 | let origin = self.head.button?.window?.frame.origin, 46 | let width = self.head.button?.window?.frame.width 47 | else { return false } 48 | return self.mouseOnStatusBar.value() && NSEvent.mouseLocation.x >= origin.x && NSEvent.mouseLocation.x <= origin.x + width 49 | } 50 | 51 | lazy var mouseOverBody: WithIntermediateState<Bool> = .init { 52 | guard 53 | let origin = self.body.button?.window?.frame.origin, 54 | let width = self.body.button?.window?.frame.width 55 | else { return false } 56 | return self.mouseOnStatusBar.value() && NSEvent.mouseLocation.x >= origin.x && NSEvent.mouseLocation.x <= origin.x + width 57 | } 58 | 59 | lazy var mouseOverTail: WithIntermediateState<Bool> = .init { 60 | guard 61 | let origin = self.tail.button?.window?.frame.origin, 62 | let width = self.tail.button?.window?.frame.width 63 | else { return false } 64 | return self.mouseOnStatusBar.value() && NSEvent.mouseLocation.x >= origin.x && NSEvent.mouseLocation.x <= origin.x + width 65 | } 66 | 67 | lazy var mouseDragging: WithIntermediateState<Bool> = .init { 68 | MouseModel.shared.dragging && self.mouseOnStatusBar.value() 69 | } 70 | 71 | 72 | 73 | lazy var hasExternalMenus: WithIntermediateState<Bool> = .init { 74 | !self.externalMenus.isEmpty 75 | } 76 | 77 | lazy var keyboardTriggers: WithIntermediateState<Bool> = .init { 78 | KeyboardModel.shared.triggers 79 | } 80 | 81 | lazy var focusedApp: WithIntermediateState<NSRunningApplication> = .init { 82 | AppManager.frontmost 83 | } 84 | 85 | lazy var mainScreen: WithIntermediateState<NSScreen?> = .init { 86 | ScreenManager.main 87 | } 88 | 89 | 90 | 91 | lazy var blocking: WithIntermediateState<Bool> = .init { 92 | self.hasExternalMenus.value() 93 | } 94 | 95 | 96 | 97 | // MARK: - Variable States 98 | 99 | var shouldPresentFeedback: Bool { 100 | !timeout && MouseModel.shared.none 101 | } 102 | 103 | var maxLength: CGFloat { 104 | ScreenManager.maxWidth 105 | } 106 | 107 | var popoverShown: Bool { 108 | AppDelegate.shared?.popover.isShown ?? false 109 | } 110 | 111 | 112 | 113 | var edge = CGFloat.zero 114 | 115 | var shouldEdgeUpdate = (now: false, will: false) 116 | 117 | var idling = (hide: false, alwaysHide: false) 118 | 119 | var noAnimation = false 120 | 121 | var ignoring = false 122 | 123 | var timeout = false 124 | 125 | var externalMenus: [WindowInfo] = [] 126 | 127 | 128 | 129 | var feedbackCount = Int.zero 130 | 131 | var shouldTimersStop = (flag: false, count: Int.zero) 132 | 133 | var mouseWasSpareOrUnidled = false 134 | 135 | var draggedToDeactivate = (dragging: false, count: Int.zero) 136 | 137 | 138 | 139 | // MARK: - Timers & Event Monitors 140 | 141 | var animationTimer: Timer? 142 | 143 | var actionTimer: Timer? 144 | 145 | var feedbackTimer: Timer? 146 | 147 | var triggerTimer: Timer? 148 | 149 | var timeoutTimer: Timer? 150 | 151 | var ignoringTimer: Timer? 152 | 153 | 154 | 155 | var mouseEventMonitor: EventMonitor? 156 | 157 | 158 | 159 | // MARK: - Icons 160 | 161 | // Status items 162 | 163 | private static let _item0 = NSStatusBar.system.statusItem( 164 | withLength: NSStatusItem.variableLength 165 | ) 166 | 167 | private static let _item1 = NSStatusBar.system.statusItem( 168 | withLength: NSStatusItem.variableLength 169 | ) 170 | 171 | private static let _item2 = NSStatusBar.system.statusItem( 172 | withLength: NSStatusItem.variableLength 173 | ) 174 | 175 | private static var _items = [_item0, _item1, _item2] 176 | 177 | // Separators 178 | 179 | var head = Separator(order: 2) { StatusBarController._items } 180 | 181 | var body = Separator(order: 1) { StatusBarController._items } 182 | 183 | var tail = Separator(order: 0) { StatusBarController._items } 184 | 185 | // MARK: - Inits 186 | 187 | init() { 188 | // Init separators 189 | 190 | // By default, _item0 is the most left while _item2 is the most right. 191 | // However this will change to conserve the relative position of the separators. 192 | sort() 193 | 194 | if let button = StatusBarController._item0.button { 195 | button.action = #selector(AppDelegate.toggle(_:)) 196 | button.sendAction(on: [.leftMouseUp, .rightMouseUp]) 197 | } 198 | 199 | if let button = StatusBarController._item1.button { 200 | button.action = #selector(AppDelegate.toggle(_:)) 201 | button.sendAction(on: [.leftMouseUp, .rightMouseUp]) 202 | } 203 | 204 | if let button = StatusBarController._item2.button { 205 | button.action = #selector(AppDelegate.toggle(_:)) 206 | button.sendAction(on: [.leftMouseUp, .rightMouseUp]) 207 | } 208 | 209 | // Start services 210 | 211 | startAnimationTimer() 212 | startActionTimer() 213 | 214 | startTriggerTimer() 215 | 216 | registerShortcuts() 217 | } 218 | 219 | deinit { 220 | // Stop services 221 | 222 | stopTimer(&animationTimer) 223 | stopTimer(&actionTimer) 224 | 225 | stopTimer(&triggerTimer) 226 | 227 | stopTimer(&timeoutTimer) 228 | stopTimer(&ignoringTimer) 229 | 230 | stopMonitor(&mouseEventMonitor) 231 | } 232 | } 233 | 234 | extension StatusBarController { 235 | func sort() { 236 | // Make sure the rightmost separator is positioned further back in the array 237 | StatusBarController._items.sort { (first, second) in 238 | if !first.isVisible { 239 | // The first one is invisible -> the first one is more lefty 240 | return true 241 | } else if !second.isVisible { 242 | // The first one is visible while the second one is invisible -> the second one is more lefty 243 | return false 244 | } else if let x1 = first.origin?.x, let x2 = second.origin?.x { 245 | // Both have reasonable x positions -> the leftmost one is more lefty 246 | return x1 <= x2 247 | } else { return true } 248 | } 249 | } 250 | 251 | func updateEdge() { 252 | edge = (body.origin?.x ?? 0) + body.length 253 | } 254 | 255 | func updateExternalMenus() { 256 | externalMenus = ExternalMenuBarManager.menuBarItems.flatMap { 257 | $0.newWindowsNear 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /Abyssal/Extensions/Defaults+Structures.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults+Structures.swift 3 | // Abyssal 4 | // 5 | // Created by KrLite on 2024/2/8. 6 | // 7 | 8 | import Foundation 9 | import Defaults 10 | import AppKit 11 | import SwiftUI 12 | 13 | extension Theme: Defaults.Serializable { 14 | struct Bridge: Defaults.Bridge { 15 | typealias Value = Theme 16 | typealias Serializable = String 17 | 18 | func serialize(_ value: Theme?) -> String? { 19 | guard let value else { 20 | return nil 21 | } 22 | 23 | return value.id 24 | } 25 | 26 | func deserialize(_ object: String?) -> Theme? { 27 | guard 28 | let id = object, 29 | Theme.themeIds.contains(id) 30 | else { 31 | return nil 32 | } 33 | 34 | return Theme.themes.first { $0.id == id } 35 | } 36 | } 37 | 38 | static let bridge = Bridge() 39 | } 40 | 41 | struct Modifier: OptionSet, Defaults.Serializable { 42 | let rawValue: UInt8 43 | 44 | static let control = Modifier(rawValue: 1 << 0) 45 | static let option = Modifier(rawValue: 1 << 1) 46 | static let command = Modifier(rawValue: 1 << 2) 47 | 48 | static let none: Modifier = [] 49 | static let all: Modifier = [.control, .option, .command] 50 | 51 | var control: Bool { 52 | get { 53 | self.contains(.control) 54 | } 55 | 56 | set { 57 | if newValue { 58 | self.formUnion(.control) 59 | } else { 60 | self.remove(.control) 61 | } 62 | } 63 | } 64 | 65 | var option: Bool { 66 | get { 67 | self.contains(.option) 68 | } 69 | 70 | set { 71 | if newValue { 72 | self.formUnion(.option) 73 | } else { 74 | self.remove(.option) 75 | } 76 | } 77 | } 78 | 79 | var command: Bool { 80 | get { 81 | self.contains(.command) 82 | } 83 | 84 | set { 85 | if newValue { 86 | self.formUnion(.command) 87 | } else { 88 | self.remove(.command) 89 | } 90 | } 91 | } 92 | 93 | var flags: NSEvent.ModifierFlags { 94 | var result = NSEvent.ModifierFlags() 95 | 96 | if self.contains(.control) { 97 | result.formUnion(.control) 98 | } 99 | if self.contains(.option) { 100 | result.formUnion(.option) 101 | } 102 | if self.contains(.command) { 103 | result.formUnion(.command) 104 | } 105 | 106 | return result 107 | } 108 | 109 | static func fromFlags(_ flags: NSEvent.ModifierFlags) -> Modifier { 110 | var result = Modifier() 111 | 112 | if flags.contains(.control) { 113 | result.formUnion(.control) 114 | } 115 | if flags.contains(.option) { 116 | result.formUnion(.option) 117 | } 118 | if flags.contains(.command) { 119 | result.formUnion(.command) 120 | } 121 | 122 | return result 123 | } 124 | } 125 | 126 | extension Modifier { 127 | enum Compose: String, CaseIterable, Codable, Defaults.Serializable { 128 | case any = "any" 129 | case all = "all" 130 | 131 | func triggers(input: Modifier) -> Bool { 132 | switch self { 133 | case .any: 134 | // OK if the two sets have any member in common 135 | !Defaults[.modifier].isDisjoint(with: input) 136 | case .all: 137 | // OK if the input is a superset of the configured 138 | input.isSuperset(of: Defaults[.modifier]) 139 | } 140 | } 141 | } 142 | } 143 | 144 | enum Timeout: Int, CaseIterable, Defaults.Serializable { 145 | case instant = 0 146 | 147 | case sec5 = 5 148 | case sec10 = 10 149 | case sec15 = 15 150 | case sec30 = 30 151 | case sec45 = 45 152 | case sec60 = 60 153 | 154 | case min2 = 120 155 | case min3 = 180 156 | case min5 = 300 157 | case min10 = 600 158 | 159 | case forever = -1 160 | 161 | var attribute: Int? { 162 | switch self { 163 | case .forever: nil 164 | default: self.rawValue 165 | } 166 | } 167 | } 168 | 169 | enum Feedback: Int, CaseIterable, Defaults.Serializable { 170 | case none = 0 171 | case light = 1 172 | case medium = 2 173 | case heavy = 3 174 | 175 | var pattern: [NSHapticFeedbackManager.FeedbackPattern?] { 176 | switch self { 177 | case .light: [.levelChange] 178 | case .medium: [.generic, nil, .alignment] 179 | case .heavy: [.levelChange, .alignment, .alignment, nil, nil, nil, .levelChange] 180 | 181 | default: [] 182 | } 183 | } 184 | } 185 | 186 | enum DeadZone: Codable, Defaults.Serializable { 187 | case percentage(Double) 188 | case pixel(Double) 189 | 190 | var range: ClosedRange<Double> { 191 | mode.range 192 | } 193 | 194 | var value: Double { 195 | get { 196 | switch self { 197 | case .percentage(let percentage): 198 | percentage 199 | case .pixel(let pixel): 200 | pixel 201 | } 202 | } 203 | 204 | set { 205 | self = mode.wrap(newValue) 206 | } 207 | } 208 | 209 | var sliderPercentage: Double { 210 | get { 211 | range.percentage(value) 212 | } 213 | 214 | set(percentage) { 215 | value = range.fromPercentage(percentage) 216 | } 217 | } 218 | 219 | var screenPixel: Double { 220 | switch self { 221 | case .percentage(_): 222 | Mode.pixel.range.percentage(sliderPercentage) 223 | case .pixel(let pixel): 224 | pixel 225 | } 226 | } 227 | } 228 | 229 | extension DeadZone { 230 | enum Mode: CaseIterable { 231 | case percentage 232 | case pixel 233 | 234 | var range: ClosedRange<Double> { 235 | switch self { 236 | case .percentage: 237 | 0...75 238 | case .pixel: 239 | 0...ScreenManager.width 240 | } 241 | } 242 | 243 | func wrap(_ value: Double) -> DeadZone { 244 | switch self { 245 | case .percentage: 246 | .percentage(value) 247 | case .pixel: 248 | .pixel(value) 249 | } 250 | } 251 | 252 | func from(_ deadZone: DeadZone) -> Double { 253 | guard self != deadZone.mode else { 254 | return deadZone.value 255 | } 256 | 257 | return switch self { 258 | case .percentage: 259 | switch deadZone { 260 | case .pixel(_): 261 | deadZone.sliderPercentage * 100 262 | default: deadZone.value 263 | } 264 | case .pixel: 265 | switch deadZone { 266 | case .percentage(let percentage): 267 | range.fromPercentage(percentage / 100) 268 | default: deadZone.value 269 | } 270 | } 271 | } 272 | } 273 | 274 | var mode: Mode { 275 | get { 276 | switch self { 277 | case .percentage(_): 278 | .percentage 279 | case .pixel(_): 280 | .pixel 281 | } 282 | } 283 | 284 | set(type) { 285 | guard type != self.mode else { return } 286 | 287 | self = type.wrap(type.from(self)) 288 | } 289 | } 290 | } 291 | 292 | extension DeadZone: Equatable { 293 | 294 | } 295 | 296 | struct ActiveStrategy: Codable, Defaults.Serializable { 297 | /// When frontmost app changes 298 | var frontmostAppChange: Bool 299 | /// When cursor interaction invalidates in menus 300 | var interactionInvalidate: Bool 301 | /// When current screen changes 302 | var screenChange: Bool 303 | 304 | var values: [Bool] { 305 | [ 306 | frontmostAppChange, 307 | interactionInvalidate, 308 | screenChange 309 | ] 310 | } 311 | 312 | var enabledCount: Int { 313 | values.count { $0 } 314 | } 315 | } 316 | 317 | struct ScreenSettings: Codable, Defaults.Serializable { 318 | struct Individual: Codable, Defaults.Serializable { 319 | var activeStrategy: ActiveStrategy 320 | var deadZone: DeadZone 321 | 322 | var respectNotch: Bool 323 | } 324 | 325 | var global: Individual 326 | var unique: [CGDirectDisplayID: Individual] 327 | 328 | var main: Individual { 329 | get { 330 | guard let id = ScreenManager.main?.displayID else { 331 | return global 332 | } 333 | 334 | return unique[id] ?? global 335 | } 336 | 337 | set(individual) { 338 | guard 339 | let id = ScreenManager.main?.displayID, 340 | isMainUnique 341 | else { 342 | global = individual 343 | return 344 | } 345 | 346 | unique[id] = individual 347 | } 348 | } 349 | 350 | var isMainUnique: Bool { 351 | get { 352 | guard let id = ScreenManager.main?.displayID else { 353 | return false 354 | } 355 | 356 | return unique.keys.contains { $0 == id } 357 | } 358 | 359 | set(isUnique) { 360 | guard let id = ScreenManager.main?.displayID else { 361 | return 362 | } 363 | 364 | if isUnique { 365 | let encoder = JSONEncoder() 366 | let data = try? encoder.encode(global) 367 | if 368 | let data, 369 | let copied = try? JSONDecoder().decode(Individual.self, from: data) 370 | { 371 | unique[id] = copied 372 | } 373 | } else { 374 | unique.removeValue(forKey: id) 375 | } 376 | } 377 | } 378 | } 379 | 380 | enum MenuBarOverride: CaseIterable, Codable, Defaults.Serializable { 381 | case app 382 | case empty 383 | 384 | var menu: NSMenu? { 385 | switch self { 386 | case .app: 387 | appMenu 388 | case .empty: 389 | emptyMenu 390 | } 391 | } 392 | 393 | func apply() { 394 | ApplicationMenuManager.apply(menu) 395 | } 396 | } 397 | --------------------------------------------------------------------------------