diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e43b0f98 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 00000000..0720fc10 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,30 @@ +1.6.6: Fix responsiveness of status bar icon +1.6.5: Flash status bar icon when space has no shortcut key +1.6.4: Flash status bar icon when space has no shortcut key +1.6.3 : Merged 1.5.12 into 1.6.2 +1.6.2 : Merged 1.5.11 into 1.6.1-R +1.6.1-R: Merged 1.5.10-R into 1.6.0-R +1.6.0-R: Add support for clicking icon in status bar +1.5.12 : Minor updates to preferences dialog (github repo url, modifier keys) +1.5.11 : Updated disk image; corrected handling incompatible user defaults +1.5.10-R: Fix: corrected space names in menu and in preferences drop-down +1.5.9-R: Fix: fullscreen spaces have no shortcut keys; disable menu options that lack a shortcut key +1.5.8-R: Correction to numbering fullscreen spaces +1.5.7-R: New status bar icon style: names without numbers +1.5.6-R: Add button for opening System Settings panel from dialog; Fix checking for updates +1.5.5-R: Support for restarting numbering in menu and numbers-only rendering +1.5.4-R: Minor refactoring +1.5.3-R: Tweaked menu icon opacity +1.5.2-R: Removed obsolete configuration option +1.5.1-R: Display icons in menu +1.5.0-R: Sort displays left/right (by Michael Lehenauer) +1.4.0-R: Add Applescript refresh command (by Michael Lehenauer) +1.3.1-R: Fix editing space names +1.3.0-R: Remember Space Names +1.2.5-R: Improved preferences layout; updated documentation +1.2.4-R: Added Makefile for commandline-driven distribution +1.2.3-R: Fix space number jump in properties dialog after refresh +1.2.2-R: Fix saving of space name other than using "Update name" button +1.2.1-R: Better support for space names consisting of four characters +1.2.0-R: Show space numbers in menu +1.1.1-R: Desktop switcher using menu options diff --git a/Images/Accessibility-0.png b/Images/Accessibility-0.png new file mode 100644 index 00000000..f8170e30 Binary files /dev/null and b/Images/Accessibility-0.png differ diff --git a/Images/Accessibility-1.png b/Images/Accessibility-1.png new file mode 100644 index 00000000..8dfa5ba2 Binary files /dev/null and b/Images/Accessibility-1.png differ diff --git a/Images/Accessibility-2.png b/Images/Accessibility-2.png new file mode 100644 index 00000000..8eab3252 Binary files /dev/null and b/Images/Accessibility-2.png differ diff --git a/Images/Accessibility-2.xcf b/Images/Accessibility-2.xcf new file mode 100644 index 00000000..105fa04a Binary files /dev/null and b/Images/Accessibility-2.xcf differ diff --git a/Images/Menu.png b/Images/Menu.png new file mode 100644 index 00000000..5eb0ec62 Binary files /dev/null and b/Images/Menu.png differ diff --git a/Images/Preferences-4.png b/Images/Preferences-4.png new file mode 100644 index 00000000..4ab78ecd Binary files /dev/null and b/Images/Preferences-4.png differ diff --git a/Images/Preferences-4a.png b/Images/Preferences-4a.png new file mode 100644 index 00000000..34f88927 Binary files /dev/null and b/Images/Preferences-4a.png differ diff --git a/Images/Preferences-4b.png b/Images/Preferences-4b.png new file mode 100644 index 00000000..71e77a8d Binary files /dev/null and b/Images/Preferences-4b.png differ diff --git a/Images/Preferences.png b/Images/Preferences.png deleted file mode 100644 index b47920b7..00000000 Binary files a/Images/Preferences.png and /dev/null differ diff --git a/Images/Shortcuts.png b/Images/Shortcuts.png new file mode 100644 index 00000000..6be413bf Binary files /dev/null and b/Images/Shortcuts.png differ diff --git a/Images/Spaceman_Example.png b/Images/Spaceman_Example.png index 41b5910e..f252f7c3 100644 Binary files a/Images/Spaceman_Example.png and b/Images/Spaceman_Example.png differ diff --git a/Images/Switching-Spaces.gif b/Images/Switching-Spaces.gif new file mode 100644 index 00000000..f40d6ea2 Binary files /dev/null and b/Images/Switching-Spaces.gif differ diff --git a/Images/background.png b/Images/background.png new file mode 100644 index 00000000..264234d0 Binary files /dev/null and b/Images/background.png differ diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..99d115e8 --- /dev/null +++ b/Makefile @@ -0,0 +1,79 @@ + +PROJECT = Spaceman +APPNAME = $(PROJECT).app +BUILDDIR = build +ARCHIVE = $(BUILDDIR)/$(PROJECT).xcarchive +IMAGEDIR = $(BUILDDIR)/diskimage +APPFILE = $(IMAGEDIR)/$(APPNAME) +PBXPROJ = $(PROJECT).xcodeproj/project.pbxproj +VERSION = $(shell awk -F'["; ]*' '/MARKETING_VERSION/ { print $$3; exit }' $(PBXPROJ)) +IMAGE = $(BUILDDIR)/$(PROJECT)-$(VERSION).dmg + +.DEFAULT_GOAL := help + +.PHONY: help # See https://tinyurl.com/makefile-autohelp +help: ## Print help for each target + @awk -v tab=15 'BEGIN{FS="(:.*## |##@ |@## )";c="\033[36m";m="\033[0m";y=" ";a=2;h()}function t(s){gsub(/[ \t]+$$/,"",s);gsub(/^[ \t]+/,"",s);return s}function u(g,d){split(t(g),f," ");for(j in f)printf"%s%s%-"tab"s%s%s\n",y,c,t(f[j]),m,d}function h(){printf"\nUsage:\n%smake %s%s\n\nRecognized targets:\n",y,c,m}/\\$$/{gsub(/\\$$/,"");b=b$$0;next}b{$$0=b$$0;b=""}/^[-a-zA-Z0-9*\/%_. ]+:.*## /{p=sprintf("\n%"(tab+a)"s"y,"");gsub(/\\n/,p);if($$1~/%/&&$$2~/^%:/){n=split($$2,q,/%:|:% */);for(i=2;i website/appcast.xml + git add website/appcast.xml + @printf "\nCreated appcast.xml, now please commit it\n" + +.PHONY: publish +publish: ## Publish the main branch appcast on Github Pages + git subtree push --prefix website origin main:github-pages + +.PHONY: publish-force +# git subtree split --prefix website -b github-pages # create a local github-pages branch containing the splitted output folder +# git push -f origin github-pages:github-pages # force push the github-pages branch to origin +publish-force: ## Publish the main branch appcast on Github Pages (force push) + git checkout main + git push --force-with-lease origin `git subtree split --prefix website main`:github-pages + diff --git a/README-Yabai.md b/README-Yabai.md new file mode 100644 index 00000000..66be92e7 --- /dev/null +++ b/README-Yabai.md @@ -0,0 +1,33 @@ + +## Feature: Add Apple Script Refresh Command + +[mike-jl](https://github.com/mike-jl) [commented on May 30](https://github.com/Jaysce/Spaceman/pull/34): + +It is possible to refresh Spaceman using the following Apple Script command: + +```applescript +tell application "Spaceman" to refresh +``` + +When using yabai, you can make sure that the icons stay up to date with the following code added to the end of yabairc: + +```sh +signals=( + "space_changed" + "display_added" + "display_removed" + "display_moved" + "display_changed" + "mission_control_enter" + "mission_control_exit" + "space_created" + "space_destroyed" +) +for signal in "${signals[@]}" +do + yabai --message signal \ + --add \ + event=$signal \ + action="osascript -e 'tell application \"Spaceman\" to refresh'" +done +``` diff --git a/README.md b/README.md index cfee8dc4..50acb8aa 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,32 @@ + ![Spaceman Example](Images/Header.png) + ## About + Spaceman is an application for macOS that allows you to view your Spaces / Virtual Desktops in the menu bar. Spaceman allows you to see which space you are currently on (or spaces if you are using multiple displays) relative to the other spaces you have. Naming these spaces is also an option in order to organise separate spaces for your workflow. +Also, the menu and statusbar icons enable switching between spaces. + **Spaceman requires macOS 11 Big Sur or greater.** ## Install -### Homebrew -``` -brew install spaceman -``` + ### GitHub -Go to the releases tab and download **Spaceman.dmg** from the latest release. + +Go to the [releases](https://github.com/ruittenb/Spaceman/releases) tab and download **Spaceman.dmg** from the latest release. + ## Usage - + + The above image shows the possible icons that you will see depending on the style you choose. -There are 4 styles to choose from, from top to bottom: +There are five styles to choose from, from top to bottom: - Rectangles - Numbers - Rectangles with Numbers -- Named Spaces +- Names +- Names with Numbers The meaning of the icons from left to right are: @@ -31,14 +37,76 @@ The meaning of the icons from left to right are: - Inactive Space - Active Fullscreen App - +## Preferences + + + +The style and the name of a space can be changed in preferences (shown above). A space is named by selecting the space from the dropdown, typing a name up to 4 characters and clicking the 'Update name' button. + +If the icon fails to update, you can choose to force a refresh of the icon using a custom keyboard shortcut or allow Spaceman to refresh them automatically every 5 seconds by enabling 'Refresh spaces in background'. + +### Switching Spaces + +Icons in the status bar can be clicked to switch spaces: + + + +The menu shows a list of space names. Selecting one will cause Spaceman to switch to that space. + + -The style and the name of a space can be changed in preferences (shown above). A space is named by selecting the space from the dropdown, typing a name up to 3 characters and clicking the 'Update name' button or pressing enter. +Spaceman switches spaces by sending a keyboard shortcut to Mission Control using `osascript`. +Note that only the first ten spaces will have shortcut keys assigned. +For extra spaces, switching will not be available; the status bar icon will flash if +selected, and the menu option will be disabled. -If the icon fails to update, you can choose to force a refresh of the icon using a custom keyboard shortcut or allow Spaceman to refresh them automatically every 5 seconds by enabling 'Refresh spaces in background'. See [#2](https://github.com/Jaysce/Spaceman/issues/2). +**For switching to work successfully, the following things need to be configured:** + +- Spaceman needs authorization for Accessibility: + + + + +- Shortcut keys need to have been defined for Mission Control: + + + +- Spaceman needs to know which shortcuts to send: + + + +## Remote Refresh + +The list of spaces can also be refreshed using Applescript: + +```sh +$ osascript -e 'tell application "Spaceman" to refresh' +``` + +For details on how to maximize usefulness of this, see [MikeJL's Comments](README-Yabai.md) + +## Troubleshooting + +If Spaceman does not start, or does not start correctly, after an upgrade: you may need to delete the application defaults: + +```sh +$ defaults delete dev.ruittenb.Spaceman +``` ## Attributions + - This project is based on [WhichSpace](https://github.com/gechr/WhichSpace) - This project uses [Sparkle](https://sparkle-project.org) for update delivery - This project makes use of [LaunchAtLogin](https://github.com/sindresorhus/LaunchAtLogin) - This project makes use of [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) +- Authors: + - [Sasindu Jayasinghe](https://github.com/Jaysce/Spaceman) + - [René Uittenbogaard](https://github.com/ruittenb/Spaceman) +- Contributions by: + - [Dmitry Poznyak](https://github.com/triangular-sneaky/Spaceman) + - [Grzegorz Milka](https://github.com/gregorias) + - [Michael Lehenauer](https://github.com/mike-jl/Spaceman) + - [Logan Savage](https://github.com/lxsavage/Spaceman) + - [Yakir Lugasy](https://github.com/yakirlog/Spaceman) + - [aaplmath](https://github.com/aaplmath) + diff --git a/Spaceman.xcodeproj/project.pbxproj b/Spaceman.xcodeproj/project.pbxproj index cc2f226f..adea1c05 100644 --- a/Spaceman.xcodeproj/project.pbxproj +++ b/Spaceman.xcodeproj/project.pbxproj @@ -7,6 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + 4C02BD6D2C0894DC00F07491 /* Scriptable.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 4C02BD6C2C0894DC00F07491 /* Scriptable.sdef */; }; + 4C02BD6F2C0894F500F07491 /* RefreshCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C02BD6E2C0894F500F07491 /* RefreshCommand.swift */; }; + E70587F32C80A5DA00D9B729 /* ShortcutHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70587F22C80A5DA00D9B729 /* ShortcutHelper.swift */; }; + E70587F52C827E5600D9B729 /* exportOptions.plist in Resources */ = {isa = PBXBuildFile; fileRef = E70587F42C827E5600D9B729 /* exportOptions.plist */; }; + E70587F72C864C8A00D9B729 /* SpaceNameCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70587F62C864C8A00D9B729 /* SpaceNameCache.swift */; }; + E7A066232C7F65F1002DFC52 /* SpaceSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7A066222C7F65F1002DFC52 /* SpaceSwitcher.swift */; }; + E7B0862F2C8980130016C851 /* OSVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B0862E2C8980130016C851 /* OSVersion.swift */; }; E828E6552737CBA0007075E8 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E828E6542737CBA0007075E8 /* VisualEffectView.swift */; }; E828E6582737D2E4007075E8 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = E828E6572737D2E4007075E8 /* Constants.swift */; }; E87334E9256B51CF0012586E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87334E8256B51CF0012586E /* AppDelegate.swift */; }; @@ -30,6 +37,13 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 4C02BD6C2C0894DC00F07491 /* Scriptable.sdef */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = Scriptable.sdef; sourceTree = ""; }; + 4C02BD6E2C0894F500F07491 /* RefreshCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshCommand.swift; sourceTree = ""; }; + E70587F22C80A5DA00D9B729 /* ShortcutHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutHelper.swift; sourceTree = ""; }; + E70587F42C827E5600D9B729 /* exportOptions.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = exportOptions.plist; sourceTree = ""; }; + E70587F62C864C8A00D9B729 /* SpaceNameCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceNameCache.swift; sourceTree = ""; }; + E7A066222C7F65F1002DFC52 /* SpaceSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceSwitcher.swift; sourceTree = ""; }; + E7B0862E2C8980130016C851 /* OSVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSVersion.swift; sourceTree = ""; }; E828E6542737CBA0007075E8 /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = ""; }; E828E6572737D2E4007075E8 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; E87334E5256B51CF0012586E /* Spaceman.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Spaceman.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -95,17 +109,19 @@ isa = PBXGroup; children = ( E87334E8256B51CF0012586E /* AppDelegate.swift */, + E8733516256C6F3A0012586E /* Extensions.swift */, E8E2AEF5257CB4A500319BBC /* View */, E8E2AEF6257CB4AC00319BBC /* ViewModel */, E8E2AEFC257CB51C00319BBC /* Model */, E8733509256B575D0012586E /* Helpers */, E828E6562737D2CF007075E8 /* Utilities */, - E8733516256C6F3A0012586E /* Extensions.swift */, - E87334FC256B52650012586E /* Spaceman-Bridging-Header.h */, + E70587F42C827E5600D9B729 /* exportOptions.plist */, E87334EC256B51CF0012586E /* Assets.xcassets */, E87334F1256B51CF0012586E /* Main.storyboard */, E87334F4256B51CF0012586E /* Info.plist */, + 4C02BD6C2C0894DC00F07491 /* Scriptable.sdef */, E87334F5256B51CF0012586E /* Spaceman.entitlements */, + E87334FC256B52650012586E /* Spaceman-Bridging-Header.h */, E87334EE256B51CF0012586E /* Preview Content */, ); path = Spaceman; @@ -122,8 +138,12 @@ E8733509256B575D0012586E /* Helpers */ = { isa = PBXGroup; children = ( - E8733506256B57400012586E /* SpaceObserver.swift */, E8733510256B779B0012586E /* IconCreator.swift */, + E7B0862E2C8980130016C851 /* OSVersion.swift */, + 4C02BD6E2C0894F500F07491 /* RefreshCommand.swift */, + E70587F22C80A5DA00D9B729 /* ShortcutHelper.swift */, + E8733506256B57400012586E /* SpaceObserver.swift */, + E7A066222C7F65F1002DFC52 /* SpaceSwitcher.swift */, ); path = Helpers; sourceTree = ""; @@ -144,6 +164,7 @@ isa = PBXGroup; children = ( E8E2AEF7257CB4D700319BBC /* PreferencesViewModel.swift */, + E70587F62C864C8A00D9B729 /* SpaceNameCache.swift */, ); path = ViewModel; sourceTree = ""; @@ -190,8 +211,9 @@ E87334DD256B51CF0012586E /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1220; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1430; TargetAttributes = { E87334E4256B51CF0012586E = { CreatedOnToolsVersion = 12.2; @@ -226,9 +248,11 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C02BD6D2C0894DC00F07491 /* Scriptable.sdef in Resources */, E87334F3256B51CF0012586E /* Main.storyboard in Resources */, E87334F0256B51CF0012586E /* Preview Assets.xcassets in Resources */, E87334ED256B51CF0012586E /* Assets.xcassets in Resources */, + E70587F52C827E5600D9B729 /* exportOptions.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -261,11 +285,16 @@ buildActionMask = 2147483647; files = ( E873350E256B5F0F0012586E /* Space.swift in Sources */, + 4C02BD6F2C0894F500F07491 /* RefreshCommand.swift in Sources */, + E7B0862F2C8980130016C851 /* OSVersion.swift in Sources */, E8E2AEF8257CB4D700319BBC /* PreferencesViewModel.swift in Sources */, + E7A066232C7F65F1002DFC52 /* SpaceSwitcher.swift in Sources */, E8733503256B532D0012586E /* StatusBar.swift in Sources */, E87334EB256B51CF0012586E /* PreferencesView.swift in Sources */, E8C5AFF52580848100614F50 /* AboutView.swift in Sources */, E8733507256B57400012586E /* SpaceObserver.swift in Sources */, + E70587F32C80A5DA00D9B729 /* ShortcutHelper.swift in Sources */, + E70587F72C864C8A00D9B729 /* SpaceNameCache.swift in Sources */, E8733511256B779B0012586E /* IconCreator.swift in Sources */, E87334E9256B51CF0012586E /* AppDelegate.swift in Sources */, E8E2AF03257CB5CE00319BBC /* SpaceNameInfo.swift in Sources */, @@ -329,6 +358,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -391,6 +421,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -415,24 +446,27 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Spaceman/Spaceman.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1.0; + CURRENT_PROJECT_VERSION = 1.6.6; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Spaceman/Preview Content\""; - DEVELOPMENT_TEAM = P2Q7LG69J2; + DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Spaceman/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.jaysce.Spaceman; + MARKETING_VERSION = 1.6.6; + PRODUCT_BUNDLE_IDENTIFIER = dev.ruittenb.Spaceman; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Spaceman/Spaceman-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; @@ -445,24 +479,27 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Spaceman/Spaceman.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1.0; + CURRENT_PROJECT_VERSION = 1.6.6; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Spaceman/Preview Content\""; - DEVELOPMENT_TEAM = P2Q7LG69J2; + DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Spaceman/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.jaysce.Spaceman; + MARKETING_VERSION = 1.6.6; + PRODUCT_BUNDLE_IDENTIFIER = dev.ruittenb.Spaceman; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Spaceman/Spaceman-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; diff --git a/Spaceman.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Spaceman.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4d6c6ad7..13707b97 100644 --- a/Spaceman.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Spaceman.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sindresorhus/KeyboardShortcuts", "state" : { - "revision" : "81e72f4787f2d642d788594834f42431f079e505", - "version" : "0.6.0" + "revision" : "f09de800a4f05d66fe2a9924c81a932454a9f39a", + "version" : "0.7.1" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sindresorhus/LaunchAtLogin", "state" : { - "revision" : "0f39982b9d6993eef253b81219d3c39ba1e680f3", - "version" : "4.0.0" + "revision" : "e8171b3e38a2816f579f58f3dac1522aa39efe41", + "version" : "4.2.0" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "7918c1c8fc68baa37917eeaa67286b077ad5e393", - "version" : "1.27.1" + "revision" : "10d96f2b9b9905b0f529f09e517219d4e20125c0", + "version" : "1.27.3" } } ], diff --git a/Spaceman.xcodeproj/project.xcworkspace/xcuserdata/.gitignore b/Spaceman.xcodeproj/project.xcworkspace/xcuserdata/.gitignore new file mode 100644 index 00000000..fa09e0d5 --- /dev/null +++ b/Spaceman.xcodeproj/project.xcworkspace/xcuserdata/.gitignore @@ -0,0 +1 @@ +UserInterfaceState.xcuserstate diff --git a/Spaceman.xcodeproj/xcshareddata/xcschemes/Spaceman.xcscheme b/Spaceman.xcodeproj/xcshareddata/xcschemes/Spaceman.xcscheme index 92ae551c..f11a18c9 100644 --- a/Spaceman.xcodeproj/xcshareddata/xcschemes/Spaceman.xcscheme +++ b/Spaceman.xcodeproj/xcshareddata/xcschemes/Spaceman.xcscheme @@ -1,6 +1,6 @@ + + diff --git a/Spaceman/AppDelegate.swift b/Spaceman/AppDelegate.swift index 5c6e8369..c3a082de 100644 --- a/Spaceman/AppDelegate.swift +++ b/Spaceman/AppDelegate.swift @@ -10,18 +10,21 @@ import KeyboardShortcuts final class AppDelegate: NSObject, NSApplicationDelegate { + private var iconCreator: IconCreator! private var statusBar: StatusBar! private var spaceObserver: SpaceObserver! - private var iconCreator: IconCreator! func applicationDidFinishLaunching(_ aNotification: Notification) { - - statusBar = StatusBar() - spaceObserver = SpaceObserver() + iconCreator = IconCreator() + + statusBar = StatusBar() + statusBar.iconCreator = iconCreator + spaceObserver = SpaceObserver() spaceObserver.delegate = self spaceObserver.updateSpaceInformation() + NSApp.activate(ignoringOtherApps: true) KeyboardShortcuts.onKeyUp(for: .refresh) { [] in self.spaceObserver.updateSpaceInformation() @@ -36,7 +39,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { extension AppDelegate: SpaceObserverDelegate { func didUpdateSpaces(spaces: [Space]) { let icon = iconCreator.getIcon(for: spaces) - statusBar.updateStatusBar(withIcon: icon) + statusBar.updateStatusBar(withIcon: icon, withSpaces: spaces) } } @@ -46,11 +49,8 @@ struct SpacemanApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { - Settings { EmptyView() } - } - } diff --git a/Spaceman/Base.lproj/Main.storyboard b/Spaceman/Base.lproj/Main.storyboard index bcc53e3b..d4106119 100644 --- a/Spaceman/Base.lproj/Main.storyboard +++ b/Spaceman/Base.lproj/Main.storyboard @@ -1,7 +1,8 @@ - + - + + diff --git a/Spaceman/Helpers/IconCreator.swift b/Spaceman/Helpers/IconCreator.swift index aa2e4f4e..b01308f4 100644 --- a/Spaceman/Helpers/IconCreator.swift +++ b/Spaceman/Helpers/IconCreator.swift @@ -9,26 +9,46 @@ import AppKit import Foundation class IconCreator { + // 23 = 277 px ; button distance + // 18 = 219 px ; button width + // 10 = 120 px ; left margin + // 5 = 60 px ; gap + // 2.5 = 30 px ; semi gap + // 7.5 = 90 px ; void left + + static let WIDTH_SMALL = 18 + static let WIDTH_LARGE = 34 + static let WIDTH_XLARGE = 49 + static let HEIGHT = 12 + private let defaults = UserDefaults.standard - private var iconSize = NSSize(width: 18, height: 12) + private var iconSize = NSSize(width: WIDTH_SMALL, height: HEIGHT) private let gapWidth = CGFloat(5) private let displayGapWidth = CGFloat(15) private var displayCount = 1 + public var widths: [CGFloat] = [] + func getIcon(for spaces: [Space]) -> NSImage { - iconSize.width = 18 + iconSize.width = CGFloat(IconCreator.WIDTH_SMALL) let spacemanStyle = SpacemanStyle(rawValue: defaults.integer(forKey: "displayStyle")) var icons = [NSImage]() for s in spaces { let iconResourceName: String - switch (s.isCurrentSpace, s.isFullScreen) { - case (true, true): - iconResourceName = spacemanStyle == .text ? "NamedFullActive" : "SpaceManIconFullEn" - case (true, false): + switch (s.isCurrentSpace, s.isFullScreen, spacemanStyle) { + case (true, true, .names), + (true, true, .numbersAndNames): + iconResourceName = "NamedFullActive" + case (true, true, _): + iconResourceName = "SpaceManIconFullEn" + case (true, false, _): iconResourceName = "SpaceManIcon" - case (false, true): - iconResourceName = spacemanStyle == .text ? "NamedFullInactive" : "SpaceManIconFullDis" + case (false, true, .names), + (false, true, .numbersAndNames): + iconResourceName = "NamedFullInactive" + case (false, true, _): + iconResourceName = "SpaceManIconFullDis" default: iconResourceName = "SpaceManIconBorder" } @@ -40,12 +60,9 @@ class IconCreator { case .numbers: icons = createNumberedIcons(spaces) case .numbersAndRects: - icons = createRectWithNumbersIcons(icons, spaces, desktopsOnly: false) - case .desktopNumbersAndRects: - icons = createRectWithNumbersIcons(icons, spaces, desktopsOnly: true) - case .text: - iconSize.width = 49 - icons = createNamedIcons(icons, spaces) + icons = createRectWithNumbersIcons(icons, spaces) + case .names, .numbersAndNames: + icons = createNamedIcons(icons, spaces, withNumbers: spacemanStyle == .numbersAndNames) default: break } @@ -53,17 +70,19 @@ class IconCreator { let iconsWithDisplayProperties = getIconsWithDisplayProps(icons: icons, spaces: spaces) return mergeIcons(iconsWithDisplayProperties) } - + private func createNumberedIcons(_ spaces: [Space]) -> [NSImage] { var newIcons = [NSImage]() for s in spaces { let textRect = NSRect(origin: CGPoint.zero, size: iconSize) - let spaceNumber = NSString(string: String(s.spaceNumber)) + let restartNumberingByDesktop = defaults.bool(forKey: "restartNumberingByDesktop") + let spaceId = restartNumberingByDesktop ? s.desktopID : String(s.spaceNumber) + let image = NSImage(size: iconSize) image.lockFocus() - spaceNumber.drawVerticallyCentered( + spaceId.drawVerticallyCentered( in: textRect, withAttributes: getStringAttributes( alpha: !s.isCurrentSpace ? 0.4 : 1, @@ -72,59 +91,69 @@ class IconCreator { newIcons.append(image) } - return newIcons } - private func createRectWithNumbersIcons(_ icons: [NSImage], _ spaces: [Space], desktopsOnly: Bool) -> [NSImage] { + func createRectWithNumberIcon(icons: [NSImage], index: Int, space: Space, fraction: Float = 1.0) -> NSImage { + let textRect = NSRect(origin: CGPoint.zero, size: iconSize) + let spaceID = space.desktopID + + let iconImage = NSImage(size: iconSize) + let numberImage = NSImage(size: iconSize) + + numberImage.lockFocus() + spaceID.drawVerticallyCentered( + in: textRect, + withAttributes: getStringAttributes(alpha: 1)) + numberImage.unlockFocus() + + iconImage.lockFocus() + icons[index].draw( + in: textRect, + from: NSRect.zero, + operation: NSCompositingOperation.sourceOver, + fraction: CGFloat(fraction)) + numberImage.draw( + in: textRect, + from: NSRect.zero, + operation: NSCompositingOperation.destinationOut, + fraction: 1.0) + iconImage.isTemplate = true + iconImage.unlockFocus() + return iconImage + } + + private func createRectWithNumbersIcons(_ icons: [NSImage], _ spaces: [Space]) -> [NSImage] { var index = 0 var newIcons = [NSImage]() - for s in spaces { - let textRect = NSRect(origin: CGPoint.zero, size: iconSize) - let number = desktopsOnly ? s.desktopNumber : s.spaceNumber - let iconImage = NSImage(size: iconSize) - let numberImage = NSImage(size: iconSize) - - if (number != nil) { - numberImage.lockFocus() - let spaceNumber = NSString(string: String(number!)) - spaceNumber.drawVerticallyCentered( - in: textRect, - withAttributes: getStringAttributes(alpha: 1)) - numberImage.unlockFocus() - } - - iconImage.lockFocus() - icons[index].draw( - in: textRect, - from: NSRect.zero, - operation: NSCompositingOperation.sourceOver, - fraction: 1.0) - numberImage.draw( - in: textRect, - from: NSRect.zero, - operation: NSCompositingOperation.destinationOut, - fraction: 1.0) - iconImage.isTemplate = true - iconImage.unlockFocus() - + let iconImage = createRectWithNumberIcon(icons: icons, index: index, space: s) newIcons.append(iconImage) index += 1 } - return newIcons } - private func createNamedIcons(_ icons: [NSImage], _ spaces: [Space]) -> [NSImage] { + private func createNamedIcons(_ icons: [NSImage], _ spaces: [Space], withNumbers: Bool) -> [NSImage] { var index = 0 var newIcons = [NSImage]() + iconSize.width = CGFloat(withNumbers ? IconCreator.WIDTH_XLARGE : IconCreator.WIDTH_LARGE) + for s in spaces { - let textRect = NSRect(origin: CGPoint.zero, size: iconSize) - let spaceText = NSString(string: "\(s.spaceNumber): \(s.spaceName.uppercased())") - let iconImage = NSImage(size: iconSize) - let textImage = NSImage(size: iconSize) + + let restartNumberingByDesktop = defaults.bool(forKey: "restartNumberingByDesktop") + let spaceId = restartNumberingByDesktop ? s.desktopID : String(s.spaceNumber) + let spaceNumberPrefix = withNumbers ? "\(spaceId): " : "" + let spaceText = NSString(string: "\(spaceNumberPrefix)\(s.spaceName.uppercased())") + let textSize = spaceText.size(withAttributes: getStringAttributes(alpha: 1)) + let textWithMarginSize = NSMakeSize(textSize.width + 4, CGFloat(IconCreator.HEIGHT)) + + // Check if the text width exceeds the icon's width + let textImageSize = textSize.width > iconSize.width ? textWithMarginSize : iconSize + let iconImage = NSImage(size: textImageSize) + let textImage = NSImage(size: textImageSize) + let textRect = NSRect(origin: CGPoint.zero, size: textImageSize) textImage.lockFocus() spaceText.drawVerticallyCentered( @@ -158,10 +187,15 @@ class IconCreator { var currentDisplayID = spaces[0].displayID displayCount = 1 + let shouldBypassInactiveSpaces = defaults.bool(forKey: "hideInactiveSpaces") for index in 0 ..< spaces.count { + if shouldBypassInactiveSpaces && !spaces[index].isCurrentSpace { + continue + } + var nextSpaceIsOnDifferentDisplay = false - if index + 1 < spaces.count { + if !shouldBypassInactiveSpaces && index + 1 < spaces.count { let thisDispID = spaces[index + 1].displayID if thisDispID != currentDisplayID { currentDisplayID = thisDispID @@ -178,7 +212,9 @@ class IconCreator { func mergeIcons(_ iconsWithDisplayProperties: [(image: NSImage, nextSpaceOnDifferentDisplay: Bool)]) -> NSImage { let numIcons = iconsWithDisplayProperties.count - let combinedIconWidth = CGFloat(numIcons) * iconSize.width + let combinedIconWidth = CGFloat(iconsWithDisplayProperties.reduce(0) { (result, icon) in + result + icon.image.size.width + }) let accomodatingGapWidth = CGFloat(numIcons - 1) * gapWidth let accomodatingDisplayGapWidth = CGFloat(displayCount - 1) * displayGapWidth let totalWidth = combinedIconWidth + accomodatingGapWidth + accomodatingDisplayGapWidth @@ -186,18 +222,24 @@ class IconCreator { image.lockFocus() var x = CGFloat.zero + widths = [x] for icon in iconsWithDisplayProperties { icon.image.draw( at: NSPoint(x: x, y: 0), from: NSRect.zero, operation: NSCompositingOperation.sourceOver, fraction: 1.0) - if icon.nextSpaceOnDifferentDisplay { x += iconSize.width + displayGapWidth} - else { x += iconSize.width + gapWidth } + if icon.nextSpaceOnDifferentDisplay { + x += icon.image.size.width + displayGapWidth + } else { + x += icon.image.size.width + gapWidth + } + widths.append(x + 4) /* TODO FIXME left margin */ } image.isTemplate = true image.unlockFocus() - + widths.removeLast() + return image } diff --git a/Spaceman/Helpers/OSVersion.swift b/Spaceman/Helpers/OSVersion.swift new file mode 100644 index 00000000..94a50d70 --- /dev/null +++ b/Spaceman/Helpers/OSVersion.swift @@ -0,0 +1,31 @@ +// +// OSVersion.swift +// Spaceman +// +// Created by René Uittenbogaard on 05/09/2024. +// + +import Foundation + +class OSVersion { + public let version = ProcessInfo.processInfo.operatingSystemVersion + public let versionStr: String + + init() { + versionStr = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" + } + + func exceeds(_ maj: Int, _ min: Int, _ patch: Int = 0) -> Bool { + if (version.majorVersion > maj) { + return true + } else if (version.majorVersion < maj) { + return false + } else if (version.minorVersion > min) { + return true + } else if (version.minorVersion < min) { + return false + } else { + return version.patchVersion >= patch + } + } +} diff --git a/Spaceman/Helpers/RefreshCommand.swift b/Spaceman/Helpers/RefreshCommand.swift new file mode 100644 index 00000000..237334db --- /dev/null +++ b/Spaceman/Helpers/RefreshCommand.swift @@ -0,0 +1,17 @@ +// +// ScriptableCommand.swift +// Spaceman +// +// Created by Michael Lehenauer on 30.05.24. +// + +import Foundation +import Cocoa + +class RefreshCommand: NSScriptCommand { + override func performDefaultImplementation() -> Any? { + NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ButtonPressed"), object: nil) + return nil + } +} + diff --git a/Spaceman/Helpers/ShortcutHelper.swift b/Spaceman/Helpers/ShortcutHelper.swift new file mode 100644 index 00000000..9b4c146b --- /dev/null +++ b/Spaceman/Helpers/ShortcutHelper.swift @@ -0,0 +1,125 @@ +// +// ShortcutHelper.swift +// Spaceman +// +// Created by René Uittenbogaard on 28/08/2024. +// +// See https://apple.stackexchange.com/questions/36943/how-do-i-automate-a-key-press-in-applescript + +import Foundation +import SwiftUI + +class ShortcutHelper { + + private let defaults = UserDefaults.standard + + /** + * Uses the number keys on the top row of the keyboard + */ + private func getKeyCodeTopRow(spaceNumber: Int) -> Int { + let keyCode: Int + switch (spaceNumber) { + case 1: + keyCode = 18 // kVK_ANSI_1 + case 2: + keyCode = 19 // kVK_ANSI_2 + case 3: + keyCode = 20 // kVK_ANSI_3 + case 4: + keyCode = 21 // kVK_ANSI_4 + case 5: + keyCode = 23 // kVK_ANSI_5 (!) + case 6: + keyCode = 22 // kVK_ANSI_6 + case 7: + keyCode = 26 // kVK_ANSI_7 + case 8: + keyCode = 28 // kVK_ANSI_8 + case 9: + keyCode = 25 // kVK_ANSI_9 + case 10: + keyCode = 29 // kVK_ANSI_0 + default: + keyCode = -1 + } + return keyCode + } + + /** + * Uses the number keys on the numeric keypad + */ + private func getKeyCodeNumPad(spaceNumber: Int) -> Int { + let keyCode: Int + switch (spaceNumber) { + case 1: + keyCode = 83 // kVK_ANSI_Keypad1 + case 2: + keyCode = 84 // kVK_ANSI_Keypad2 + case 3: + keyCode = 85 // kVK_ANSI_Keypad3 + case 4: + keyCode = 86 // kVK_ANSI_Keypad4 + case 5: + keyCode = 87 // kVK_ANSI_Keypad5 + case 6: + keyCode = 88 // kVK_ANSI_Keypad6 + case 7: + keyCode = 89 // kVK_ANSI_Keypad7 + case 8: + keyCode = 91 // kVK_ANSI_Keypad8 (!) + case 9: + keyCode = 92 // kVK_ANSI_Keypad9 + case 10: + keyCode = 82 // kVK_ANSI_Keypad0 + default: + keyCode = -1 + } + return keyCode + } + + func getKeyCode(spaceNumber: Int) -> Int { + let schema = defaults.string(forKey: "schema") + switch (schema) { + case "toprow": + return getKeyCodeTopRow(spaceNumber: spaceNumber) + case "numpad": + return getKeyCodeNumPad(spaceNumber: spaceNumber) + default: + return getKeyCodeTopRow(spaceNumber: spaceNumber) + } + } + + func getModifiers() -> String { + var modifiers: [String] = [] + if defaults.bool(forKey: "withShift") { + modifiers.append("shift down") + } + if defaults.bool(forKey: "withControl") { + modifiers.append("control down") + } + if defaults.bool(forKey: "withOption") { + modifiers.append("option down") + } + if defaults.bool(forKey: "withCommand") { + modifiers.append("command down") + } + return modifiers.joined(separator: ",") + } + + func getModifiersAsFlags() -> NSEvent.ModifierFlags { + var mask = NSEvent.ModifierFlags() + if defaults.bool(forKey: "withShift") { + mask = mask.union(NSEvent.ModifierFlags.shift) + } + if defaults.bool(forKey: "withControl") { + mask = mask.union(NSEvent.ModifierFlags.control) + } + if defaults.bool(forKey: "withOption") { + mask = mask.union(NSEvent.ModifierFlags.option) + } + if defaults.bool(forKey: "withCommand") { + mask = mask.union(NSEvent.ModifierFlags.command) + } + return mask + } +} diff --git a/Spaceman/Helpers/SpaceObserver.swift b/Spaceman/Helpers/SpaceObserver.swift index ac2e23b2..212192ac 100644 --- a/Spaceman/Helpers/SpaceObserver.swift +++ b/Spaceman/Helpers/SpaceObserver.swift @@ -7,11 +7,13 @@ import Cocoa import Foundation +import SwiftUI class SpaceObserver { private let workspace = NSWorkspace.shared private let conn = _CGSDefaultConnection() private let defaults = UserDefaults.standard + private let spaceNameCache = SpaceNameCache() weak var delegate: SpaceObserverDelegate? init() { @@ -27,10 +29,48 @@ class SpaceObserver { object: nil) } + func display1IsLeft(display1: NSDictionary, display2: NSDictionary) -> Bool { + let d1Center = getDisplayCenter(display: display1) + let d2Center = getDisplayCenter(display: display2) + return d1Center.x < d2Center.x + } + + func getDisplayCenter(display: NSDictionary) -> CGPoint { + guard let uuidString = display["Display Identifier"] as? String + else { + return CGPoint(x: 0, y: 0) + } + let uuid = CFUUIDCreateFromString(kCFAllocatorDefault, uuidString as CFString) + let dId = CGDisplayGetDisplayIDFromUUID(uuid) + let bounds = CGDisplayBounds(dId); + return CGPoint(x: bounds.origin.x + bounds.size.width/2, y: bounds.origin.y + bounds.size.height/2) + } + @objc public func updateSpaceInformation() { - let displays = CGSCopyManagedDisplaySpaces(conn) as! [NSDictionary] + var displays = CGSCopyManagedDisplaySpaces(conn)!.takeRetainedValue() as! [NSDictionary] + + // create dict with correct sorting before changing it + var displayNumber: [String: Int] = [:] + var spacesIndex = 1 + for d in displays { + guard let spaces = d["Spaces"] as? [[String: Any]] + else { + continue + } + + for s in spaces { + let spaceID = String(s["ManagedSpaceID"] as! Int) + displayNumber[spaceID] = spacesIndex + spacesIndex += 1 + } + } + + // sort displays based on location + displays.sort(by: { + display1IsLeft(display1: $0, display2: $1) + }) + var activeSpaceID = -1 - var spacesIndex = 0 var allSpaces = [Space]() var updatedDict = [String: SpaceNameInfo]() @@ -52,43 +92,55 @@ class SpaceObserver { } var lastDesktopNumber = 0 + var lastFullScreenNumber = 0 for s in spaces { let spaceID = String(s["ManagedSpaceID"] as! Int) - let spaceNumber: Int = spacesIndex + 1 + let spaceNumber = displayNumber[spaceID]! let isCurrentSpace = activeSpaceID == s["ManagedSpaceID"] as! Int let isFullScreen = s["TileLayoutManager"] as? [String: Any] != nil - var desktopNumber : Int? + let desktopID: String if !isFullScreen { lastDesktopNumber += 1 - desktopNumber = lastDesktopNumber + desktopID = String(lastDesktopNumber) + } else { + lastFullScreenNumber += 1 + desktopID = "F\(lastFullScreenNumber)" } + + while spaceNumber >= spaceNameCache.cache.count { + // Make sure that the cache is large enough + spaceNameCache.extend() + } + let spaceName = spaceNameCache.cache[spaceNumber] var space = Space(displayID: displayID, spaceID: spaceID, - spaceName: "N/A", + spaceName: spaceName, spaceNumber: spaceNumber, - desktopNumber: desktopNumber, + desktopID: desktopID, isCurrentSpace: isCurrentSpace, isFullScreen: isFullScreen) if let data = defaults.value(forKey:"spaceNames") as? Data, let dict = try? PropertyListDecoder().decode(Dictionary.self, from: data), - let saved = dict[spaceID] { + let saved = dict[spaceID] + { space.spaceName = saved.spaceName } else if isFullScreen { if let pid = s["pid"] as? pid_t, let app = NSRunningApplication(processIdentifier: pid), - let name = app.localizedName { - space.spaceName = name.prefix(3).uppercased() + let name = app.localizedName + { + space.spaceName = name.prefix(4).uppercased() } else { - space.spaceName = "FUL" + space.spaceName = "FULL" } } + spaceNameCache.cache[spaceNumber] = space.spaceName - let nameInfo = SpaceNameInfo(spaceNum: spaceNumber, spaceName: space.spaceName) + let nameInfo = SpaceNameInfo(spaceNum: spaceNumber, spaceName: space.spaceName, desktopID: desktopID) updatedDict[spaceID] = nameInfo allSpaces.append(space) - spacesIndex += 1 } } diff --git a/Spaceman/Helpers/SpaceSwitcher.swift b/Spaceman/Helpers/SpaceSwitcher.swift new file mode 100644 index 00000000..2c6ff0c6 --- /dev/null +++ b/Spaceman/Helpers/SpaceSwitcher.swift @@ -0,0 +1,88 @@ +// +// SpaceSwitcher.swift +// Spaceman +// +// Created by René Uittenbogaard on 28/08/2024. +// + +import Foundation +import SwiftUI + +class SpaceSwitcher { + private var shortcutHelper: ShortcutHelper! + + init() { + shortcutHelper = ShortcutHelper() + } + + func switchToSpace(spaceNumber: Int, onError: () -> Void) { + let keyCode = shortcutHelper.getKeyCode(spaceNumber: spaceNumber) + if keyCode < 0 { + return onError() + } + let modifiers = shortcutHelper.getModifiers() + + let pipe = Pipe() + let file = pipe.fileHandleForReading + + let script = "tell application \"System Events\" to key code \(keyCode) using {\(modifiers)}" + let task = Process() + task.launchPath = "/usr/bin/osascript" + task.arguments = ["-e", script] + task.standardError = pipe + + do { + // Launch the task. + try task.run() + task.waitUntilExit() + let exitCode = task.terminationStatus + let data = file.readDataToEndOfFile() + file.closeFile() + + // We may run into an execution error: + // "System Events got an error: osascript is not allowed to send keystrokes" + if exitCode > 0 { + let errorString = String(data: data, encoding: .utf8) + if errorString == nil { + alert(msg: "Error: osascript exited with code \(exitCode)") + } else { + alert(msg: "Error: \(errorString!)") + + } + } + } catch { + alert(msg: "Error launching task: \(error)") + } + } + + func switchUsingLocation(widths: [CGFloat], horizontal: CGFloat, onError: () -> Void) { + var index = 0 + while index < widths.count && horizontal > widths[index] { + index += 1 + } + switchToSpace(spaceNumber: index, onError: onError) + } + + func alert(msg: String) { + var settingsTitle: String + if #available(macOS 13.0, *) { + settingsTitle = "Settings" + } else { + settingsTitle = "Preferences" + } + DispatchQueue.main.async { + let alert = NSAlert.init() + alert.messageText = "Spaceman" + alert.informativeText = msg + alert.addButton(withTitle: "Dismiss") + alert.addButton(withTitle: "System \(settingsTitle)...") + let response = alert.runModal() + if (response == .alertSecondButtonReturn) { + let task = Process() + task.launchPath = "/usr/bin/open" + task.arguments = ["/System/Library/PreferencePanes/Security.prefPane"] + try? task.run() + } + } + } +} diff --git a/Spaceman/Info.plist b/Spaceman/Info.plist index a5950c81..c84e70f2 100644 --- a/Spaceman/Info.plist +++ b/Spaceman/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile @@ -26,13 +28,17 @@ $(MACOSX_DEPLOYMENT_TARGET) LSUIElement + NSAppleScriptEnabled + NSMainStoryboardFile Main NSPrincipalClass NSApplication + OSAScriptingDefinition + Scriptable.sdef SUFeedURL - https://s3.amazonaws.com/dev.jaysce.spaceman/appcast.xml + https://ruittenb.github.io/Spaceman/appcast.xml SUPublicEDKey - eDkdnKWaZWZdQ3sGFiSOhwoaNDZKUEwg1aE6CZNl4Q8= + hxbpNVrkExVi+v7OqbFnp+avkoB7w4UV4ALgxvm9pSI= diff --git a/Spaceman/Model/Space.swift b/Spaceman/Model/Space.swift index a849e31d..8b0c86c7 100644 --- a/Spaceman/Model/Space.swift +++ b/Spaceman/Model/Space.swift @@ -12,7 +12,7 @@ struct Space { var spaceID: String var spaceName: String var spaceNumber: Int - var desktopNumber: Int? + var desktopID: String var isCurrentSpace: Bool var isFullScreen: Bool } diff --git a/Spaceman/Model/SpaceNameInfo.swift b/Spaceman/Model/SpaceNameInfo.swift index 0d9be33c..c227fac1 100644 --- a/Spaceman/Model/SpaceNameInfo.swift +++ b/Spaceman/Model/SpaceNameInfo.swift @@ -10,4 +10,5 @@ import Foundation struct SpaceNameInfo: Hashable, Codable { let spaceNum: Int let spaceName: String + let desktopID: String } diff --git a/Spaceman/Model/SpacemanStyle.swift b/Spaceman/Model/SpacemanStyle.swift index 95eef79f..2b3469e3 100644 --- a/Spaceman/Model/SpacemanStyle.swift +++ b/Spaceman/Model/SpacemanStyle.swift @@ -8,5 +8,5 @@ import Foundation enum SpacemanStyle: Int { - case none, numbers, numbersAndRects, desktopNumbersAndRects, text + case rects, numbers, numbersAndRects, names, numbersAndNames } diff --git a/Spaceman/Scriptable.sdef b/Spaceman/Scriptable.sdef new file mode 100644 index 00000000..b5466dc6 --- /dev/null +++ b/Spaceman/Scriptable.sdef @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Spaceman/Spaceman-Bridging-Header.h b/Spaceman/Spaceman-Bridging-Header.h index 9eb0ac38..3a76aefc 100644 --- a/Spaceman/Spaceman-Bridging-Header.h +++ b/Spaceman/Spaceman-Bridging-Header.h @@ -11,7 +11,7 @@ #import int _CGSDefaultConnection(); -id CGSCopyManagedDisplaySpaces(int conn); +CFArrayRef CGSCopyManagedDisplaySpaces(int conn); id CGSCopyActiveMenuBarDisplayIdentifier(int conn); #endif /* Spaceman_Bridging_Header_h */ diff --git a/Spaceman/Spaceman.entitlements b/Spaceman/Spaceman.entitlements index 311b32bd..e10f5b1d 100644 --- a/Spaceman/Spaceman.entitlements +++ b/Spaceman/Spaceman.entitlements @@ -4,6 +4,8 @@ com.apple.security.app-sandbox + com.apple.security.cs.disable-library-validation + com.apple.security.files.user-selected.read-only diff --git a/Spaceman/Utilities/Constants.swift b/Spaceman/Utilities/Constants.swift index 62ff4d15..12e01dc1 100644 --- a/Spaceman/Utilities/Constants.swift +++ b/Spaceman/Utilities/Constants.swift @@ -11,7 +11,7 @@ enum Constants { enum AppInfo { static let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - static let repo = URL(string: "https://github.com/Jaysce/Spaceman")! + static let repo = URL(string: "https://github.com/ruittenb/Spaceman")! static let website = URL(string: "https://jaysce.dev/projects/spaceman")! } diff --git a/Spaceman/View/PreferencesView.swift b/Spaceman/View/PreferencesView.swift index 68a95dc3..02c21bef 100644 --- a/Spaceman/View/PreferencesView.swift +++ b/Spaceman/View/PreferencesView.swift @@ -16,6 +16,13 @@ struct PreferencesView: View { @AppStorage("displayStyle") private var selectedStyle = 0 @AppStorage("spaceNames") private var data = Data() @AppStorage("autoRefreshSpaces") private var autoRefreshSpaces = false + @AppStorage("hideInactiveSpaces") private var hideInactiveSpaces = false + @AppStorage("restartNumberingByDesktop") private var restartNumberingByDesktop = false + @AppStorage("schema") private var schema = "toprow" + @AppStorage("withShift") private var withShift = false + @AppStorage("withControl") private var withControl = false + @AppStorage("withOption") private var withOption = false + @AppStorage("withCommand") private var withCommand = false @StateObject private var prefsVM = PreferencesViewModel() // MARK: - Main Body @@ -27,7 +34,7 @@ struct PreferencesView: View { closeButton appInfo } - .frame(maxWidth: .infinity, minHeight: 60, maxHeight: 60, alignment: .center) + .frame(maxWidth: .infinity, minHeight: 60, maxHeight: 90, alignment: .center) .offset(y: 1) // Looked like it was off center Divider() @@ -103,47 +110,99 @@ struct PreferencesView: View { private var preferencePanes: some View { VStack(alignment: .leading, spacing: 0) { - // General Pane - VStack(alignment: .leading) { - Text("General") - .font(.title2) - .fontWeight(.semibold) - LaunchAtLogin.Toggle(){Text("Launch Spaceman at login")} - Toggle("Refresh spaces in background", isOn: $autoRefreshSpaces) - shortcutRecorder.disabled(autoRefreshSpaces ? true : false) + generalPane + Divider() + spacesPane + Divider() + switchingPane + .padding(.bottom, 40) + } + } + + // MARK: - General pane + private var generalPane: some View { + VStack(alignment: .leading) { + Text("General") + .font(.title2) + .fontWeight(.semibold) + LaunchAtLogin.Toggle(){Text("Launch Spaceman at login")} + Toggle("Refresh spaces in background", isOn: $autoRefreshSpaces) + shortcutRecorder.disabled(autoRefreshSpaces ? true : false) + } + .padding() + .onChange(of: autoRefreshSpaces) { enabled in + if enabled { + prefsVM.startTimer() + KeyboardShortcuts.disable(.refresh) } - .padding() - .onChange(of: autoRefreshSpaces) { enabled in - if enabled { - prefsVM.startTimer() - KeyboardShortcuts.disable(.refresh) - } - else { - prefsVM.pauseTimer() - KeyboardShortcuts.enable(.refresh) - } + else { + prefsVM.pauseTimer() + KeyboardShortcuts.enable(.refresh) } + } + } + + // MARK: - Spaces pane + private var spacesPane: some View { + VStack(alignment: .leading) { + Text("Spaces") + .font(.title2) + .fontWeight(.semibold) + spacesStylePicker + spaceNameEditor //.disabled(selectedStyle != SpacemanStyle.text.rawValue ? true : false) - Divider() - - // Spaces Pane - VStack(alignment: .leading) { - Text("Spaces") - .font(.title2) - .fontWeight(.semibold) -// Toggle("Use single icon indicator", isOn: .constant(false)) // TODO: Implement this - spacesStylePicker - spaceNameEditor.disabled(selectedStyle != SpacemanStyle.text.rawValue ? true : false) + Toggle("Only show active spaces", isOn: $hideInactiveSpaces) + .disabled(selectedStyle == 0) // Rectangles style + Toggle("Restart space numbering by desktop", isOn: $restartNumberingByDesktop) + } + .padding() + .onChange(of: hideInactiveSpaces) { _ in + NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ButtonPressed"), object: nil) + } + } + + + // MARK: - Switching pane + private var switchingPane: some View { + // Switching Pane + VStack(alignment: .leading) { + Text("Switching Spaces") + .font(.title2) + .fontWeight(.semibold) + Picker("Shortcut keys", selection: $schema) { + Text("number keys on top row").tag("toprow") + Text("numeric keypad").tag("numpad") + } + .pickerStyle(.radioGroup) + .disabled(false) + HStack(alignment: .top) { + Text("With modifiers") + Spacer() + VStack(alignment: .leading) { + Toggle("Shift ⇧", isOn: $withShift) + Toggle("Control ⌃", isOn: $withControl) + } + Spacer() + VStack(alignment: .leading) { + Toggle("Option ⌥", isOn: $withOption) + Toggle("Command ⌘", isOn: $withCommand) + } + Spacer() } - .padding() - + } + .padding() + .onChange(of: schema) { _ in + NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ButtonPressed"), object: nil) + } + .onChange(of: [withShift, withControl, withCommand, withOption]) { _ in + NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ButtonPressed"), object: nil) } } // MARK: - Shortcut Recorder private var shortcutRecorder: some View { HStack { - Text("Force icon refresh shortcut") + Text("Force refresh shortcut") Spacer() KeyboardShortcuts.Recorder(for: .refresh) } @@ -151,14 +210,19 @@ struct PreferencesView: View { // MARK: - Style Picker private var spacesStylePicker: some View { + Picker(selection: $selectedStyle, label: Text("Style")) { - Text("Rectangles").tag(SpacemanStyle.none.rawValue) + Text("Rectangles").tag(SpacemanStyle.rects.rawValue) Text("Numbers").tag(SpacemanStyle.numbers.rawValue) Text("Rectangles with numbers").tag(SpacemanStyle.numbersAndRects.rawValue) - Text("Rectangles with desktop numbers").tag(SpacemanStyle.desktopNumbersAndRects.rawValue) - Text("Named spaces").tag(SpacemanStyle.text.rawValue) + Text("Names").tag(SpacemanStyle.names.rawValue) + Text("Names with numbers").tag(SpacemanStyle.numbersAndNames.rawValue) } .onChange(of: selectedStyle) { val in + if val == 0 { // Rectangles style + hideInactiveSpaces = false + } + selectedStyle = val NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ButtonPressed"), object: nil) } @@ -168,16 +232,24 @@ struct PreferencesView: View { private var spaceNameEditor: some View { HStack { Picker(selection: $prefsVM.selectedSpace, label: Text("Space")) { - ForEach(0.. val) { + prefsVM.spaceName = prefsVM.sortedSpaceNamesDict[val].value.spaceName + } else { + prefsVM.spaceName = "-" } } + TextField( - "Name (max 3 char.)", + "Name (max 4 char.)", text: Binding( get: {prefsVM.spaceName}, - set: {prefsVM.spaceName = $0.prefix(3).trimmingCharacters(in: .whitespacesAndNewlines)}), - onCommit: updateName) + set: {prefsVM.spaceName = $0.prefix(4).trimmingCharacters(in: .whitespacesAndNewlines)}), + onCommit: {}) // removed `updateName` callback so as to avoid unwanted writes Button("Update name") { updateName() diff --git a/Spaceman/View/PreferencesWindow.swift b/Spaceman/View/PreferencesWindow.swift index 3ef06328..e5e7455a 100644 --- a/Spaceman/View/PreferencesWindow.swift +++ b/Spaceman/View/PreferencesWindow.swift @@ -11,7 +11,7 @@ import AppKit class PreferencesWindow: NSWindow { init() { super.init( - contentRect: NSRect(x: 0, y: 0, width: 400, height: 314), + contentRect: NSRect(x: 0, y: 0, width: 400, height: 330), styleMask: [.titled, .fullSizeContentView], backing: .buffered, defer: false diff --git a/Spaceman/View/StatusBar.swift b/Spaceman/View/StatusBar.swift index a300556f..a347a64e 100644 --- a/Spaceman/View/StatusBar.swift +++ b/Spaceman/View/StatusBar.swift @@ -9,15 +9,26 @@ import Foundation import SwiftUI import Sparkle -class StatusBar { +class StatusBar: NSObject, NSMenuDelegate { private var statusBarItem: NSStatusItem! private var statusBarMenu: NSMenu! private var prefsWindow: PreferencesWindow! + private var spaceSwitcher: SpaceSwitcher! + private var shortcutHelper: ShortcutHelper! + private let defaults = UserDefaults.standard - init() { + public var iconCreator: IconCreator! + + override init() { + super.init() + + shortcutHelper = ShortcutHelper() + spaceSwitcher = SpaceSwitcher() statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) statusBarMenu = NSMenu() + statusBarMenu.autoenablesItems = false + statusBarMenu.delegate = self prefsWindow = PreferencesWindow() let hostedPrefsView = NSHostingView(rootView: PreferencesView(parentWindow: prefsWindow)) @@ -48,20 +59,82 @@ class StatusBar { statusBarMenu.addItem(about) statusBarMenu.addItem(NSMenuItem.separator()) + statusBarMenu.addItem(NSMenuItem.separator()) statusBarMenu.addItem(updates) statusBarMenu.addItem(pref) statusBarMenu.addItem(quit) - statusBarItem.menu = statusBarMenu + //statusBarItem.menu = statusBarMenu + + statusBarItem.button?.action = #selector(handleClick) + statusBarItem.button?.target = self + statusBarItem.button?.sendAction(on: [.rightMouseDown, .leftMouseDown]) } - - func updateStatusBar(withIcon icon: NSImage) { + + @objc func handleClick(_ sbButton: NSStatusBarButton) { + guard let event = NSApp.currentEvent else { + return + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + if event.type == .rightMouseDown { + // Show the menu on right-click + if let sbMenu = self.statusBarMenu { + let buttonFrame = sbButton.window?.convertToScreen(sbButton.frame) ?? .zero + let menuOrigin = CGPoint(x: buttonFrame.minX, y: buttonFrame.minY - CGFloat(IconCreator.HEIGHT) / 2) + sbMenu.minimumWidth = buttonFrame.width + sbMenu.popUp(positioning: nil, at: menuOrigin, in: nil) + sbButton.isHighlighted = false + } + } else if (event.type == .leftMouseDown) { + // Switch desktops on left click + let locationInButton = sbButton.convert(event.locationInWindow, from: sbButton) + + self.spaceSwitcher.switchUsingLocation( + widths: self.iconCreator.widths, + horizontal: locationInButton.x, + onError: self.flashStatusBar) + } else { + print("Other event: \(event.type)") + } + } + } + + func flashStatusBar() { + if let button = statusBarItem.button { + let blinkInterval: TimeInterval = 0.1 + button.isHighlighted = true + DispatchQueue.main.asyncAfter(deadline: .now() + blinkInterval) { + button.isHighlighted = false + DispatchQueue.main.asyncAfter(deadline: .now() + blinkInterval) { + button.isHighlighted = true + DispatchQueue.main.asyncAfter(deadline: .now() + blinkInterval) { + button.isHighlighted = false + } + } + } + } + } + + func updateStatusBar(withIcon icon: NSImage, withSpaces spaces: [Space]) { + // update icon if let statusBarButton = statusBarItem.button { statusBarButton.image = icon } + // update menu + guard spaces.count > 0 else { + return + } + var removeCandidateItem = statusBarMenu.items[2] + while (!removeCandidateItem.isSeparatorItem) { + statusBarMenu.removeItem(removeCandidateItem) + removeCandidateItem = statusBarMenu.items[2] + } + for space in spaces.reversed() { + let switchItem = makeSwitchToSpaceItem(space: space) + statusBarMenu.insertItem(switchItem, at: 2) + } } - + @objc func showPreferencesWindow(_ sender: AnyObject) { - if prefsWindow == nil { prefsWindow = PreferencesWindow() let hostedPrefsView = NSHostingView(rootView: PreferencesView(parentWindow: prefsWindow)) @@ -72,4 +145,50 @@ class StatusBar { prefsWindow.makeKeyAndOrderFront(nil) NSApplication.shared.activate(ignoringOtherApps: true) } + + func makeSwitchToSpaceItem(space: Space) -> NSMenuItem { + let spaceNumber = space.spaceNumber + let spaceName = space.spaceName + let desktopID = Int(space.desktopID) ?? 99 + + let mask = shortcutHelper.getModifiersAsFlags() + var shortcutKey = "" + if desktopID < 10 { + shortcutKey = space.desktopID + } else if desktopID == 10 { + shortcutKey = "0" + } + + let icon = NSImage(imageLiteralResourceName: "SpaceManIcon") + let menuIcon = IconCreator().createRectWithNumberIcon( + icons: [icon], + index: 0, + space: space, + fraction: 0.6) + let item = NSMenuItem( + title: spaceName, + action: #selector(switchToSpace(_:)), + keyEquivalent: shortcutKey) + item.keyEquivalentModifierMask = mask + item.target = self + item.tag = spaceNumber + item.image = menuIcon + if space.isCurrentSpace || shortcutKey == "" { + item.isEnabled = false + // item.state = NSControl.StateValue.on // tick mark + //if OSVersion().exceeds(14, 0) { + //if #available(macOS 14.0, *) { + // item.badge = NSMenuItemBadge(string: "Current") + //} + } + return item + } + + @objc func switchToSpace(_ sender: NSMenuItem) { + let spaceNumber = sender.tag + guard (spaceNumber >= 1 && spaceNumber <= 10) else { + return + } + spaceSwitcher.switchToSpace(spaceNumber: spaceNumber, onError: flashStatusBar) + } } diff --git a/Spaceman/ViewModel/PreferencesViewModel.swift b/Spaceman/ViewModel/PreferencesViewModel.swift index 3cac2c50..f494a9e4 100644 --- a/Spaceman/ViewModel/PreferencesViewModel.swift +++ b/Spaceman/ViewModel/PreferencesViewModel.swift @@ -12,12 +12,13 @@ class PreferencesViewModel: ObservableObject { @AppStorage("autoRefreshSpaces") private var autoRefreshSpaces = false @Published var selectedSpace = 0 @Published var spaceName = "" + @Published var desktopID = "" var spaceNamesDict: [String: SpaceNameInfo]! var sortedSpaceNamesDict: [Dictionary.Element]! var timer: Timer! init() { - selectedSpace = 0 + selectedSpace = -1 spaceName = "" spaceNamesDict = [String: SpaceNameInfo]() sortedSpaceNamesDict = [Dictionary.Element]() @@ -26,11 +27,10 @@ class PreferencesViewModel: ObservableObject { } func loadData() { - guard let data = UserDefaults.standard.value(forKey:"spaceNames") as? Data else { + guard let data = UserDefaults.standard.value(forKey: "spaceNames") as? Data else { return } - self.selectedSpace = 0 let decoded = try! PropertyListDecoder().decode(Dictionary.self, from: data) self.spaceNamesDict = decoded @@ -39,12 +39,28 @@ class PreferencesViewModel: ObservableObject { } sortedSpaceNamesDict = sorted + if (selectedSpace < 0 || selectedSpace >= sortedSpaceNamesDict.count) { + selectedSpace = 0 + if (sortedSpaceNamesDict.count < 1) { + sortedSpaceNamesDict.append( + (key: "0", + value: SpaceNameInfo(spaceNum: 0, spaceName: "DISP", desktopID: "1") + ) + ) + } + spaceName = sortedSpaceNamesDict[selectedSpace].value.spaceName + desktopID = sortedSpaceNamesDict[selectedSpace].value.desktopID + } } func updateSpace() { let key = sortedSpaceNamesDict[selectedSpace].key let spaceNum = sortedSpaceNamesDict[selectedSpace].value.spaceNum - spaceNamesDict[key] = SpaceNameInfo(spaceNum: spaceNum, spaceName: spaceName.isEmpty ? "N/A" : spaceName) + let desktopID = sortedSpaceNamesDict[selectedSpace].value.desktopID + spaceNamesDict[key] = SpaceNameInfo( + spaceNum: spaceNum, + spaceName: spaceName.isEmpty ? "-" : spaceName, + desktopID: desktopID) } func startTimer() { @@ -56,7 +72,6 @@ class PreferencesViewModel: ObservableObject { } @objc func refreshSpaces() { - print("Updating spaces") NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ButtonPressed"), object: nil) } } diff --git a/Spaceman/ViewModel/SpaceNameCache.swift b/Spaceman/ViewModel/SpaceNameCache.swift new file mode 100644 index 00000000..71d6fcfe --- /dev/null +++ b/Spaceman/ViewModel/SpaceNameCache.swift @@ -0,0 +1,35 @@ +// +// SpaceNameCache.swift +// Spaceman +// +// Created by René Uittenbogaard on 02/09/2024. +// + +import Foundation +import SwiftUI + +class SpaceNameCache { + @AppStorage("spaceNameCache") private var spaceNameCacheString: String = "" + private let empty = Array.init(repeating: "-", count: 5) + + var cache: [String] { + get { + if let data = spaceNameCacheString.data(using: .utf8) { + let decoded = try? JSONDecoder().decode([String].self, from: data) + if (decoded != nil) { + return decoded! + } + } + return empty + } + set { + if let encoded = try? JSONEncoder().encode(newValue) { + spaceNameCacheString = String(data: encoded, encoding: .utf8) ?? "" + } + } + } + + func extend() { + cache.append(contentsOf: empty) + } +} diff --git a/Spaceman/exportOptions.plist b/Spaceman/exportOptions.plist new file mode 100644 index 00000000..b08a0372 --- /dev/null +++ b/Spaceman/exportOptions.plist @@ -0,0 +1,17 @@ + + + + + method + mac-application + teamID + None + provisioningProfiles + + dev.jaysce.Spaceman + None + + signingStyle + manual + + diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 00000000..38dd39c6 --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,5 @@ +# Build artifacts +Spaceman*.dmg +Spaceman.app +Spaceman.xcarchive + diff --git a/build/diskimage/.VolumeIcon.icns b/build/diskimage/.VolumeIcon.icns new file mode 100644 index 00000000..93451135 Binary files /dev/null and b/build/diskimage/.VolumeIcon.icns differ diff --git a/build/diskimage/.background/dmg-background.tiff b/build/diskimage/.background/dmg-background.tiff new file mode 100644 index 00000000..657590a6 Binary files /dev/null and b/build/diskimage/.background/dmg-background.tiff differ diff --git a/build/make-appcast.sh b/build/make-appcast.sh new file mode 100755 index 00000000..8550abc1 --- /dev/null +++ b/build/make-appcast.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +GITROOT=$(git rev-parse --show-toplevel) +AUTHOR=ruittenb +PROJECT=Spaceman +PBXPROJ=$GITROOT/$PROJECT.xcodeproj/project.pbxproj +BUILDDIR=build +URL=https://api.github.com/repos/$AUTHOR/$PROJECT/releases/latest + +############################################################################ +# functions + +print_xml() { + cat <<-EOF + + + + ${PROJECT} + + ${title} + + + ${description} + + ]]> + + ${pubDate} + ${minimumSystemVersion} + + + + +EOF +} + +get_github_release() { + release_data=$(wget -qO- "$URL") +} + +gather_data() { + local sparkle_dir=$( + ls -d1 ~/Library/Developer/Xcode/DerivedData/Spaceman-*/SourcePackages/artifacts/sparkle/Sparkle/bin | head -1 + ) + + local body=$(echo "$release_data" | jq -r .body) + local publishedAt=$(echo "$release_data" | jq -r .published_at) + local vversion=$(echo "$release_data" | jq -r .tag_name) + + title=$(echo "$release_data" | jq -r .name) + imageFile=$(echo "$release_data" | jq -r .assets[].name) + + description=$(printf "$body" | awk '{ gsub("\r", ""); print "
  • " $0 "
  • " }') + pubDate=$(gdate -R -d "$publishedAt") + version=${vversion#v} + friendlyVersion=${version} + numericVersion=${version%-R} + minimumSystemVersion=$(awk -F'[=; ]{1,}' '/MACOSX_DEPLOYMENT_TARGET/ { print $2; exit }' "$PBXPROJ") + + signatureAndLength=$("$sparkle_dir"/sign_update "$BUILDDIR/$imageFile" | awk '{ print $2 "\n" $1 }') +} + +main() { + get_github_release + gather_data + print_xml +} + +############################################################################ +# main + +main "$@" + diff --git a/build/releases.json b/build/releases.json new file mode 100644 index 00000000..8b71c36b --- /dev/null +++ b/build/releases.json @@ -0,0 +1,74 @@ +{ + "url": "https://api.github.com/repos/ruittenb/Spaceman/releases/173315209", + "assets_url": "https://api.github.com/repos/ruittenb/Spaceman/releases/173315209/assets", + "upload_url": "https://uploads.github.com/repos/ruittenb/Spaceman/releases/173315209/assets{?name,label}", + "html_url": "https://github.com/ruittenb/Spaceman/releases/tag/v1.5.5-R", + "id": 173315209, + "author": { + "login": "ruittenb", + "id": 4320070, + "node_id": "MDQ6VXNlcjQzMjAwNzA=", + "avatar_url": "https://avatars.githubusercontent.com/u/4320070?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ruittenb", + "html_url": "https://github.com/ruittenb", + "followers_url": "https://api.github.com/users/ruittenb/followers", + "following_url": "https://api.github.com/users/ruittenb/following{/other_user}", + "gists_url": "https://api.github.com/users/ruittenb/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ruittenb/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ruittenb/subscriptions", + "organizations_url": "https://api.github.com/users/ruittenb/orgs", + "repos_url": "https://api.github.com/users/ruittenb/repos", + "events_url": "https://api.github.com/users/ruittenb/events{/privacy}", + "received_events_url": "https://api.github.com/users/ruittenb/received_events", + "type": "User", + "site_admin": false + }, + "node_id": "RE_kwDOMpGcG84KVJSJ", + "tag_name": "v1.5.5-R", + "target_commitish": "main", + "name": "Better support for restarting numbering by desktop", + "draft": false, + "prerelease": false, + "created_at": "2024-09-03T22:35:52Z", + "published_at": "2024-09-03T22:39:23Z", + "assets": [ + { + "url": "https://api.github.com/repos/ruittenb/Spaceman/releases/assets/190216110", + "id": 190216110, + "node_id": "RA_kwDOMpGcG84LVneu", + "name": "Spaceman-1.5.5-R.dmg", + "label": null, + "uploader": { + "login": "ruittenb", + "id": 4320070, + "node_id": "MDQ6VXNlcjQzMjAwNzA=", + "avatar_url": "https://avatars.githubusercontent.com/u/4320070?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ruittenb", + "html_url": "https://github.com/ruittenb", + "followers_url": "https://api.github.com/users/ruittenb/followers", + "following_url": "https://api.github.com/users/ruittenb/following{/other_user}", + "gists_url": "https://api.github.com/users/ruittenb/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ruittenb/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ruittenb/subscriptions", + "organizations_url": "https://api.github.com/users/ruittenb/orgs", + "repos_url": "https://api.github.com/users/ruittenb/repos", + "events_url": "https://api.github.com/users/ruittenb/events{/privacy}", + "received_events_url": "https://api.github.com/users/ruittenb/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 2433595, + "download_count": 0, + "created_at": "2024-09-03T22:39:19Z", + "updated_at": "2024-09-03T22:39:21Z", + "browser_download_url": "https://github.com/ruittenb/Spaceman/releases/download/v1.5.5-R/Spaceman-1.5.5-R.dmg" + } + ], + "tarball_url": "https://api.github.com/repos/ruittenb/Spaceman/tarball/v1.5.5-R", + "zipball_url": "https://api.github.com/repos/ruittenb/Spaceman/zipball/v1.5.5-R", + "body": "1.5.5-R: Support for restarting numbering in menu and numbers-only rendering\r\n1.5.4-R: Minor refactoring\r\n1.5.3-R: Tweaked menu icon opacity" +} diff --git a/website/appcast.xml b/website/appcast.xml new file mode 100644 index 00000000..fd1dd740 --- /dev/null +++ b/website/appcast.xml @@ -0,0 +1,26 @@ + + + + Spaceman + + v1.6.6: Fix responsiveness of status bar icon + + +
  • Fixes responsiveness of status bar icon after a popup menu or dialog was displayed.
  • + + ]]> +
    + Wed, 25 Sep 2024 20:04:23 +0200 + 11.0 + +
    +
    +
    diff --git a/website/index.html b/website/index.html new file mode 100644 index 00000000..1a788291 --- /dev/null +++ b/website/index.html @@ -0,0 +1,10 @@ + + + + Spaceman + + + + Spaceman + +