boB Rudis 5 роки тому
джерело
коміт
303fe6626c
Не вдалося знайти GPG ключ що відповідає даному підпису Ідентифікатор GPG ключа: 1D7529BE14E2BBA9
  1. 7
      LICENSE
  2. 61
      README.md
  3. 12
      RSwitch.xcodeproj/project.pbxproj
  4. 153
      RSwitch/AppDelegate.swift
  5. 30
      RSwitch/Assets.xcassets/AppIcon.appiconset/Contents.json
  6. BIN
      RSwitch/Assets.xcassets/AppIcon.appiconset/rs-128-1x.png
  7. BIN
      RSwitch/Assets.xcassets/AppIcon.appiconset/rs-128-2x.png
  8. BIN
      RSwitch/Assets.xcassets/AppIcon.appiconset/rs-16-1x.png
  9. BIN
      RSwitch/Assets.xcassets/AppIcon.appiconset/rs-16-2x.png
  10. BIN
      RSwitch/Assets.xcassets/AppIcon.appiconset/rs-256-1x.png
  11. BIN
      RSwitch/Assets.xcassets/AppIcon.appiconset/rs-256-2x.png
  12. BIN
      RSwitch/Assets.xcassets/AppIcon.appiconset/rs-32-1x.png
  13. BIN
      RSwitch/Assets.xcassets/AppIcon.appiconset/rs-32-2x.png
  14. BIN
      RSwitch/Assets.xcassets/AppIcon.appiconset/rs-512-1x.png
  15. BIN
      RSwitch/Assets.xcassets/AppIcon.appiconset/rs-512-2x.png
  16. 18
      RSwitch/Assets.xcassets/RSwitch.imageset/Contents.json
  17. BIN
      RSwitch/Assets.xcassets/RSwitch.imageset/rs-2x.pdf
  18. 36
      RSwitch/Base.lproj/Main.storyboard
  19. 2
      RSwitch/Info.plist
  20. 10
      RSwitch/RSwitch.entitlements
  21. BIN
      releases/RSwitch-1.0.0.app.zip
  22. BIN
      rswitch-menu-example.png

7
LICENSE

@ -0,0 +1,7 @@
Copyright 2019 Bob Rudis
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

61
README.md

@ -0,0 +1,61 @@
# RSwitch
Change 'Current' R version on macOS
## Description
At the bottom of the [R for macOS Developer's Page](http://mac.r-project.org/) there's mention of an "other binary" called "RSwitch" that is _"a small GUI that allows you to switch between R versions quickly (if you have multiple versions of R framework installed)."_ Said switching requires you to use the "tar.gz" versions of R from the R for macOS Developer's Page since the official CRAN binary installers clean up after themselves quite nicely to prevent potentially wacky behavior.
All this GUI does is change the `Current` alias target in `/Library/Frameworks/R.framework/Versions` to the appropriate version. You can do that from the command line but the switcher GUI was created so that means some folks prefer click-switching.
After launching the app, there will be a new menu item in the main macOS menubar with a popup that will look something like this:
![](rswitch-menu-example.png)
The checkmarked version is the default (i.e. the one `Current` points to) and if you select any other version the alias will be changed. You can also launch a new Finder window that opens to the R frameworks directory or quit the app.
## Why?
- I needed to brush up on Swift 5 coding
- I wanted RSwitch as a menubar app vs one with a dialog that I could easily lose across 15 desktops
- I wanted to see if it was possible to make it work sandboxed (TLDR: it isn't)
- The existing RSwitch source code is not available (likely got lost in the move from AT&T's domain to the new `mac.r-project.org` domain)
- The existing RSwitch binaries no longer work starting with macOS Catalina since they are 32-bit
- I'm not exactly enamored with the old-school R icons, including the RSwitch one. That's a personal preference, not a judgement or dig on the existing icons or the creators of said icons.
## What's Inside the Tin?
The `releases` directory contains ZIP files of various versions (see TODO for why there'll be future versions). Just download and extract and enjoy.
Other than that (and the example `png` & `LICENSE` file) the rest is a _super small_ Xcode project. It's commented as well to let you know what's going on.
## CAUTION
You should really build this yourself since installing random binaries from the internet (or, even official ones from things like app stores) is fraught with peril. You have no guarantee that the releases are, indeed, built from this source tree. I could be stealing all your stuff! (NOTE: I'm not). Reading and understanding the source code and building the app on your own is the only way to ensure you're not going to be compromised.
## Contributing
- Follow the [Contributor Code of Conduct](https://www.contributor-covenant.org/version/1/0/0/code-of-conduct.html)
- See the TODO (below)
- File an issue claiming an item (on any social coding site you like)
- Ensure you have [signed commits](https://git-scm.com/book/ms/v2/Git-Tools-Signing-Your-Work)
- File a PR or submit a patch via email (bob at rud dot is)
## TODO
- Allow hiding of the app icon (not sure this is a good idea, tho…pls discuss in an issue!)
- Add Cmd-1, -2, -3, (etc) key equivalents in the menu bar for fast selection (one reason why ^^ might not be a good idea)
- Clean up the icon (which is "dial" by IconMark from the Noun Project). This means having it look better in the menu bar in dark/light mode _including_ the highlight mode for it. Possibly means getting a visible "R" on it somewhere.
- Better/prettier alerting (which also means more sanity checks)
- Add an "about" box (mostly to ensure IconMark gets more credit than a comment and README)
- (add your own TODO suggestions via PR)
## License
Copyright (c) 2019 Bob Rudis, MIT License

12
RSwitch.xcodeproj/project.pbxproj

@ -20,7 +20,6 @@
01F3EF0F230E635500DF5DF9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
01F3EF12230E635500DF5DF9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
01F3EF14230E635500DF5DF9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
01F3EF15230E635500DF5DF9 /* RSwitch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RSwitch.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -58,7 +57,6 @@
01F3EF0F230E635500DF5DF9 /* Assets.xcassets */,
01F3EF11230E635500DF5DF9 /* Main.storyboard */,
01F3EF14230E635500DF5DF9 /* Info.plist */,
01F3EF15230E635500DF5DF9 /* RSwitch.entitlements */,
);
path = RSwitch;
sourceTree = "<group>";
@ -269,16 +267,17 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = RSwitch/RSwitch.entitlements;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = CBY22P58G8;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_HARDENED_RUNTIME = NO;
INFOPLIST_FILE = RSwitch/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = is.rud.bob.RSwitch;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
@ -289,16 +288,17 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = RSwitch/RSwitch.entitlements;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = CBY22P58G8;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_HARDENED_RUNTIME = NO;
INFOPLIST_FILE = RSwitch/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = is.rud.bob.RSwitch;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;

153
RSwitch/AppDelegate.swift

@ -1,26 +1,149 @@
//
// AppDelegate.swift
// RSwitch
//
// Created by hrbrmstr on 8/22/19.
// Copyright © 2019 Bob Rudis. All rights reserved.
//
import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
// Show an informational alert
public func infoAlert(_ message: String, _ extra: String? = nil, style: NSAlert.Style = NSAlert.Style.informational) {
let alert = NSAlert()
alert.messageText = message
if extra != nil { alert.informativeText = extra! }
alert.alertStyle = style
alert.runModal()
}
// Show an informational alert and then quit
public func quitAlert(_ message: String, _ extra: String? = nil) {
infoAlert(message, "The application will now quit.", style: NSAlert.Style.critical)
NSApp.terminate(nil)
}
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
// Where the official R installs go
let macos_r_framework_dir = "/Library/Frameworks/R.framework/Versions"
// Get the bar setup
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
let statusMenu = NSMenu()
let quitItem = NSMenuItem(title: NSLocalizedString("Quit", comment: "Quit menu item"), action: #selector(NSApp.terminate), keyEquivalent: "q")
override init() {
super.init()
statusMenu.delegate = self
// dial by IconMark from the Noun Project
statusItem.button?.image = #imageLiteral(resourceName: "RSwitch")
statusItem.menu = statusMenu
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
// Insert code here to initialize app
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
// Insert code here to tear down app
}
// The core worker function. Receives the basename of the selected directory
// then removes the current alias and creates the new one.
@objc func handleSwitch(_ sender: NSMenuItem?) {
let fm = FileManager.default;
let title = sender?.title
do {
try fm.removeItem(atPath: macos_r_framework_dir + "/" + "Current")
} catch {
infoAlert("Failed to remove 'Current' alias", macos_r_framework_dir + "/" + "Current")
}
do {
try fm.createSymbolicLink(
at: NSURL(fileURLWithPath: macos_r_framework_dir + "/" + "Current") as URL,
withDestinationURL: NSURL(fileURLWithPath: macos_r_framework_dir + "/" + title!) as URL
)
} catch {
infoAlert("Failed to create alias for " + macos_r_framework_dir + "/" + title!)
}
}
// Show the framework dir in a new Finder window
@objc func openFrameworksDir(_ sender: NSMenuItem?) {
NSWorkspace.shared.openFile(macos_r_framework_dir, withApplication: "Finder")
}
}
extension AppDelegate: NSMenuDelegate {
func menuWillOpen(_ menu: NSMenu) {
if (menu != self.statusMenu) { return }
// clear the menu
menu.removeAllItems()
// add selection to open frameworks dir in Finder
menu.addItem(NSMenuItem(title: "Open R Frameworks Directory", action: #selector(openFrameworksDir), keyEquivalent: "" ))
menu.addItem(NSMenuItem.separator())
// populate installed versions
let fm = FileManager.default
var targetPath:String? = nil
do {
// gets a directory listing
let entries = try fm.contentsOfDirectory(atPath: macos_r_framework_dir)
// retrieves all versions (excludes hidden files and the Current alias
let versions = entries.sorted().filter { !($0.hasPrefix(".")) && !($0 == "Current") }
// if there was a Current alias (prbly shld alert if not)
if ((entries.filter { $0 == "Current" })[0] == "Current") {
// get where Current points to
let furl = NSURL(fileURLWithPath: macos_r_framework_dir + "/" + "Current")
if (furl.fileReferenceURL() != nil) {
do {
let fdat = try NSURL(resolvingAliasFileAt: furl as URL, options: [])
targetPath = fdat.lastPathComponent!
} catch {
targetPath = furl.path
}
}
// populate menu items with all installed R versions, ensuring we
// put a checkbox next to the one that is Current
for version in versions {
let item = NSMenuItem(title: version, action: #selector(handleSwitch), keyEquivalent: "")
item.isEnabled = true
if (version == targetPath) { item.state = NSControl.StateValue.on }
item.representedObject = version
menu.addItem(item)
}
}
} catch {
quitAlert("Failed to list contents of R framework directory. You either do not have R installed or have incorrect permissions set on " + macos_r_framework_dir)
}
// Add a Quit item
menu.addItem(NSMenuItem.separator())
menu.addItem(quitItem)
}
}

30
RSwitch/Assets.xcassets/AppIcon.appiconset/Contents.json

@ -3,52 +3,62 @@
{
"idiom" : "mac",
"size" : "16x16",
"scale" : "1x"
"scale" : "1x",
"filename" : "rs-16-1x.png"
},
{
"idiom" : "mac",
"size" : "16x16",
"scale" : "2x"
"scale" : "2x",
"filename" : "rs-16-2x.png"
},
{
"idiom" : "mac",
"size" : "32x32",
"scale" : "1x"
"scale" : "1x",
"filename" : "rs-32-1x.png"
},
{
"idiom" : "mac",
"size" : "32x32",
"scale" : "2x"
"scale" : "2x",
"filename" : "rs-32-2x.png"
},
{
"idiom" : "mac",
"size" : "128x128",
"scale" : "1x"
"scale" : "1x",
"filename" : "rs-128-1x.png"
},
{
"idiom" : "mac",
"size" : "128x128",
"scale" : "2x"
"scale" : "2x",
"filename" : "rs-128-2x.png"
},
{
"idiom" : "mac",
"size" : "256x256",
"scale" : "1x"
"scale" : "1x",
"filename" : "rs-256-1x.png"
},
{
"idiom" : "mac",
"size" : "256x256",
"scale" : "2x"
"scale" : "2x",
"filename" : "rs-256-2x.png"
},
{
"idiom" : "mac",
"size" : "512x512",
"scale" : "1x"
"scale" : "1x",
"filename" : "rs-512-1x.png"
},
{
"idiom" : "mac",
"size" : "512x512",
"scale" : "2x"
"scale" : "2x",
"filename" : "rs-512-2x.png"
}
],
"info" : {

BIN
RSwitch/Assets.xcassets/AppIcon.appiconset/rs-128-1x.png

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 4.3 KiB

BIN
RSwitch/Assets.xcassets/AppIcon.appiconset/rs-128-2x.png

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 9.9 KiB

BIN
RSwitch/Assets.xcassets/AppIcon.appiconset/rs-16-1x.png

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 381 B

BIN
RSwitch/Assets.xcassets/AppIcon.appiconset/rs-16-2x.png

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 821 B

BIN
RSwitch/Assets.xcassets/AppIcon.appiconset/rs-256-1x.png

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 9.9 KiB

BIN
RSwitch/Assets.xcassets/AppIcon.appiconset/rs-256-2x.png

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 21 KiB

BIN
RSwitch/Assets.xcassets/AppIcon.appiconset/rs-32-1x.png

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 821 B

BIN
RSwitch/Assets.xcassets/AppIcon.appiconset/rs-32-2x.png

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 1.8 KiB

BIN
RSwitch/Assets.xcassets/AppIcon.appiconset/rs-512-1x.png

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 21 KiB

BIN
RSwitch/Assets.xcassets/AppIcon.appiconset/rs-512-2x.png

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 46 KiB

18
RSwitch/Assets.xcassets/RSwitch.imageset/Contents.json

@ -0,0 +1,18 @@
{
"images" : [
{
"idiom" : "mac",
"filename" : "rs-2x.pdf",
"scale" : "1x"
},
{
"idiom" : "mac",
"filename" : "rs-2x.pdf",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

BIN
RSwitch/Assets.xcassets/RSwitch.imageset/rs-2x.pdf

Бінарний файл не відображається.

36
RSwitch/Base.lproj/Main.storyboard

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="11134" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14865.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11134"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14865.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Application-->
@ -673,45 +674,24 @@
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections>
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="RSwitch" customModuleProvider="target"/>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="0.0"/>
</scene>
<!--Window Controller-->
<scene sceneID="R2V-B0-nI4">
<objects>
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
<connections>
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
</connections>
</window>
<connections>
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
</connections>
</windowController>
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="250"/>
</scene>
<!--View Controller-->
<scene sceneID="hIz-AP-VOD">
<objects>
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="m2S-Jp-Qdl">
<viewController identifier="main" storyboardIdentifier="main" showSeguePresentationStyle="single" id="XfG-lQ-9wD" customClass="ViewController" customModule="RSwitch" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" identifier="ViewController" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="655"/>
<point key="canvasLocation" x="61" y="225"/>
</scene>
</scenes>
</document>

2
RSwitch/Info.plist

@ -23,7 +23,7 @@
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2019 Bob Rudis. All rights reserved.</string>
<string>Copyright © 2019 Bob Rudis. All rights reserved. MIT Licensed</string>
<key>NSMainStoryboardFile</key>
<string>Main</string>
<key>NSPrincipalClass</key>

10
RSwitch/RSwitch.entitlements

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>

BIN
releases/RSwitch-1.0.0.app.zip

Бінарний файл не відображається.

BIN
rswitch-menu-example.png

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 18 KiB

Завантаження…
Відмінити
Зберегти