├── .github └── FUNDING.yml ├── .gitignore ├── BuildScripts └── build.sh ├── COPYING ├── Common ├── Constants.swift ├── Protocols.swift └── TmpDiskVolume.swift ├── LICENSE ├── README.md ├── TmpDisk.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ ├── WorkspaceSettings.xcsettings │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── tim.xcuserdatad │ │ └── WorkspaceSettings.xcsettings ├── xcshareddata │ └── xcschemes │ │ └── TmpDisk.xcscheme └── xcuserdata │ └── tim.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── TmpDisk ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon-1.png │ │ └── icon.png │ ├── Contents.json │ ├── disk.imageset │ │ ├── Contents.json │ │ ├── disk.png │ │ ├── disk@2x.png │ │ └── disk@3x-2.png │ ├── eject.imageset │ │ ├── Contents.json │ │ └── eject.png │ ├── eject_a.imageset │ │ ├── Contents.json │ │ └── eject_a.png │ ├── recreate.imageset │ │ ├── Contents.json │ │ └── recreate.png │ └── recreate_a.imageset │ │ ├── Contents.json │ │ └── recreate_a.png ├── Base.lproj │ └── Main.storyboard ├── IconUtil.swift ├── Info.plist ├── StatusBarController.swift ├── TmpDisk.entitlements ├── TmpDiskManager.swift ├── Util.swift ├── Views │ ├── AboutViewController.swift │ ├── AutoCreateManagerViewController.swift │ ├── CheckBoxTableCellView.swift │ ├── NewTmpDiskViewController.swift │ ├── PreferencesViewController.swift │ └── TmpDiskMenuItem.swift ├── WindowManager.swift ├── XPCClient.swift ├── dsa_pub.pem ├── en.lproj │ └── Localizable.strings ├── es.lproj │ ├── Localizable.strings │ └── Main.strings ├── icon.icns └── zh-Hans.lproj │ ├── Localizable.strings │ └── Main.strings ├── TmpDiskLauncher ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon-1.png │ │ └── icon.png │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Info.plist ├── TmpDiskLauncher.entitlements └── es.lproj │ └── Main.strings ├── TmpDiskTests ├── SMJobBlessUtil.py └── TmpDiskTests.swift ├── TmpDiskUITests ├── TmpDiskUITests.swift └── TmpDiskUITestsLaunchTests.swift ├── appdmg.json ├── com.imothee.TmpDiskHelper ├── Info.plist ├── NSXPCConnection+AuditToken.h ├── TmpDiskCreator.swift ├── XPCServer.swift ├── com.imothee.TmpDiskHelper-Bridging-Header.h ├── launchd ├── launchd.plist └── main.swift ├── dmgbg.png └── package.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: imothee 2 | polar: imothee 3 | patreon: imothee 4 | buy_me_a_coffee: imothee 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | design 3 | TmpDisk.app 4 | TmpDisk*.dmg 5 | .DS_Store 6 | keys 7 | 8 | # User settings 9 | xcuserdata/ 10 | 11 | # Devenv 12 | .devenv* 13 | devenv.local.nix 14 | 15 | # direnv 16 | .direnv 17 | 18 | # pre-commit 19 | .pre-commit-config.yaml 20 | 21 | node_modules/ 22 | -------------------------------------------------------------------------------- /BuildScripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # build.sh 4 | # TmpDisk 5 | # 6 | # Created by Tim on 7/21/24. 7 | # 8 | 9 | MAIN="com.imothee.TmpDisk" 10 | HELPER="com.imothee.TmpDiskHelper" 11 | GENERIC="anchor apple generic" 12 | 13 | echo "Running build script for $PRODUCT_BUNDLE_IDENTIFIER" 14 | 15 | if [ "$PRODUCT_BUNDLE_IDENTIFIER" == "$MAIN" ]; then 16 | echo "Building main target" 17 | 18 | # Bump build number 19 | #buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${INFOPLIST_FILE}") 20 | #buildNumber=$(($buildNumber + 1)) 21 | #/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${PROJECT_DIR}/${INFOPLIST_FILE}" 22 | 23 | # Sync Developer Account 24 | 25 | IDENTIFIER="identifier \"$HELPER\"" 26 | ORG_UNIT="certificate leaf[subject.OU] = \"$DEVELOPMENT_TEAM\"" 27 | REQUIREMENTS="$GENERIC and $IDENTIFIER and $ORG_UNIT" 28 | 29 | echo "Set :SMPrivilegedExecutables:$HELPER \"$REQUIREMENTS\"" 30 | echo "$PROJECT_DIR/$INFOPLIST_FILE" 31 | 32 | /usr/libexec/PlistBuddy -c "Set :SMPrivilegedExecutables:$HELPER \"$REQUIREMENTS\"" "$PROJECT_DIR/$INFOPLIST_FILE" 33 | fi 34 | 35 | if [ "$PRODUCT_BUNDLE_IDENTIFIER" == "$HELPER" ]; then 36 | echo "Building helper target" 37 | 38 | # Set the helper versions to be the latest version 39 | /usr/libexec/PlistBuddy -c "Set :TmpDiskHelperVersion $CURRENT_PROJECT_VERSION" "${PROJECT_DIR}/TmpDisk/Info.plist" 40 | /usr/libexec/PlistBuddy -c "Set :Build $CURRENT_PROJECT_VERSION" "${PROJECT_DIR}/com.imothee.TmpDiskHelper/launchd.plist" 41 | 42 | # Sync Developer Account 43 | 44 | IDENTIFIER="identifier \"$MAIN\"" 45 | ORG_UNIT="certificate leaf[subject.OU] = \"$DEVELOPMENT_TEAM\"" 46 | REQUIREMENTS="$GENERIC and $IDENTIFIER and $ORG_UNIT" 47 | 48 | /usr/libexec/PlistBuddy -c "Set :SMAuthorizedClients:0 \"$REQUIREMENTS\"" "$INFOPLIST_FILE" 49 | fi 50 | -------------------------------------------------------------------------------- /Common/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // TmpDiskHelper 4 | // 5 | // Created by Tim on 2/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Constant { 11 | static let helperMachLabel = "com.imothee.TmpDiskHelper" 12 | } 13 | 14 | enum TmpDiskError: Error { 15 | case noName 16 | case exists 17 | case invalidSize 18 | case failed 19 | case helperInvalidated 20 | case helperFailed 21 | } 22 | -------------------------------------------------------------------------------- /Common/Protocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Protocols.swift 3 | // TmpDiskHelper 4 | // 5 | // Created by Tim on 2/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | @objc protocol TmpDiskCreator { 11 | func createTmpDisk(_ command: String, onCreate: @escaping (Bool) -> Void) 12 | func uninstall() 13 | } 14 | -------------------------------------------------------------------------------- /Common/TmpDiskVolume.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TmpDiskVolume.swift 3 | // TmpDisk 4 | // 5 | // Created by Tim on 2/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct TmpDiskVolume: Hashable, Codable { 11 | var name: String = "" 12 | var size: Int = 16 13 | var autoCreate: Bool = false 14 | var indexed: Bool = false 15 | var hidden: Bool = false 16 | var tmpFs: Bool = false 17 | var caseSensitive: Bool = false 18 | var journaled: Bool = false 19 | var warnOnEject: Bool = false 20 | var folders: [String] = [] 21 | var icon: String? 22 | 23 | func path() -> String { 24 | if tmpFs { 25 | return "\(TmpDiskManager.rootFolder)/\(name)" 26 | } 27 | return "/Volumes/\(name)" 28 | } 29 | 30 | func URL() -> URL { 31 | return NSURL.fileURL(withPath: self.path()) 32 | } 33 | 34 | func dictionary() -> Dictionary { 35 | return [ 36 | "name": name, 37 | "size": size, 38 | "indexed": indexed, 39 | "hidden": hidden, 40 | "tmpFs": tmpFs, 41 | "caseSensitive": caseSensitive, 42 | "journaled": journaled, 43 | "warnOnEject": warnOnEject, 44 | "folders": folders, 45 | "icon": icon ?? "", 46 | ] 47 | } 48 | 49 | func showWarning() -> Bool { 50 | if warnOnEject { 51 | if let files = try? FileManager.default.contentsOfDirectory(atPath: self.path()) { 52 | if !files.filter({ ![".DS_Store", ".tmpdisk", ".fseventsd"].contains($0) }).isEmpty { 53 | return true 54 | } 55 | } 56 | } 57 | return false 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | TmpDisk is free software: you can redistribute it and/or modify 2 | it under the terms of the GNU General Public License as published by 3 | the Free Software Foundation, either version 3 of the License, or 4 | (at your option) any later version. 5 | 6 | TmpDisk is distributed in the hope that it will be useful, 7 | but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | GNU General Public License for more details. 10 | 11 | You should have received a copy of the GNU General Public License 12 | along with TmpDisk. If not, see . 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TmpDisk 2 | ======= 3 | 4 | TmpDisk is an Open Source simple RamDisk management tool. RamDisks are disks that use your memory (RAM) to create virtual hard disks on your Mac. RamDisks can be any size, limited only by your total memory and lightning fast. They are also temporary. Any files stored on a RamDisk will be permanently deleted when the disk is ejected. No need to worry about deleting, trash or cleaning up temporary files anymore. **Warning** A TmpDisk will not survive a restart or ejecting. Any files or information on the disk will be PERMANENTLY deleted once the computer is shutdown or the disk ejected. TmpDisks are perfect for 5 | * Saving large files such as photos while editing 6 | * Downloading and extracting zip files 7 | * Storing of temporary logs and files 8 | * Easily cleaning up tmp files 9 | 10 | Support 11 | ------- 12 | 13 | For support, tracking updates, announcement and to participate in the Imothee Community please join us on [Discord](https://discord.gg/5UgyRYaEq6) 14 | 15 | Maintaining TmpDisk for over [10 years](https://www.macupdate.com/app/mac/44022/tmpdisk) has been expensive and exhausting. If you can, please support us! We're looking for testers, volunteers, translators, advocates and donations. 16 | 17 | Installing 18 | ---------- 19 | 20 | After 2.0.4 TmpDisk supports a minimum MacOS version of 10.14.6. 21 | 22 | Down the latest release https://github.com/imothee/tmpdisk/releases/latest or clone the repository and build the application yourself. All required files are included. 23 | 24 | Usage 25 | ----- 26 | 27 | TmpDisk installs as a Status Bar application. You can use the menu to create new tmpdisks, eject existing tmpdisks and click on a disk to open it in finder. 28 | 29 | As of 1.0.4 you can also run the app from the command line and pass in arguments to create a TmpDisk on startup. 30 | 31 | `open -a /Applications/TmpDisk.app --args -name=TestDisk -size=64` 32 | 33 | Will create a TmpDisk name TestDisk with 64MB space. 34 | -------------------------------------------------------------------------------- /TmpDisk.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TmpDisk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TmpDisk.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TmpDisk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "sparkle", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/sparkle-project/Sparkle", 7 | "state" : { 8 | "revision" : "c1bf68f91f5c37ba5b3ac5028e34ceb5d444de12", 9 | "version" : "2.2.0" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /TmpDisk.xcodeproj/project.xcworkspace/xcuserdata/tim.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | IssueFilterStyle 12 | ShowActiveSchemeOnly 13 | LiveSourceIssuesEnabled 14 | 15 | ShowSharedSchemesAutomaticallyEnabled 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /TmpDisk.xcodeproj/xcshareddata/xcschemes/TmpDisk.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /TmpDisk.xcodeproj/xcuserdata/tim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /TmpDisk.xcodeproj/xcuserdata/tim.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | TmpDisk.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | TmpDiskHelper.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 2 16 | 17 | TmpDiskLauncher.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 1 21 | 22 | com.imothee.TmpDiskHelper.xcscheme_^#shared#^_ 23 | 24 | orderHint 25 | 2 26 | 27 | 28 | SuppressBuildableAutocreation 29 | 30 | 2200823F2765504E00A8524B 31 | 32 | primary 33 | 34 | 35 | 220082502765505100A8524B 36 | 37 | primary 38 | 39 | 40 | 2200825A2765505100A8524B 41 | 42 | primary 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /TmpDisk/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TmpDisk 4 | // 5 | // Created by @imothee on 12/11/21. 6 | // 7 | // This file is part of TmpDisk. 8 | // 9 | // TmpDisk is free software: you can redistribute it and/or modify 10 | // it under the terms of the GNU General Public License as published by 11 | // the Free Software Foundation, either version 3 of the License, or 12 | // (at your option) any later version. 13 | // 14 | // TmpDisk is distributed in the hope that it will be useful, 15 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | // GNU General Public License for more details. 18 | // 19 | // You should have received a copy of the GNU General Public License 20 | // along with TmpDisk. If not, see . 21 | 22 | import Cocoa 23 | 24 | import ServiceManagement 25 | 26 | extension Notification.Name { 27 | static let killLauncher = Notification.Name("killLauncher") 28 | static let tmpDiskMounted = Notification.Name("TmpDiskMounted") 29 | } 30 | 31 | @main 32 | class AppDelegate: NSObject, NSApplicationDelegate { 33 | 34 | var statusBar: StatusBarController? 35 | 36 | func applicationDidFinishLaunching(_ aNotification: Notification) { 37 | statusBar = StatusBarController.init() 38 | 39 | // Check if TmpDisk was loaded with command line args 40 | let (name, size) = getArgs() 41 | if let name = name, let size = size { 42 | let volume = TmpDiskVolume(name: name, size: size) 43 | TmpDiskManager.shared.createTmpDisk(volume: volume, onCreate: {_ in}) 44 | } 45 | 46 | NotificationCenter.default.addObserver(forName: .tmpDiskMounted, object: nil, queue: .main) { notification in 47 | self.statusBar?.buildCurrentTmpDiskMenu() 48 | } 49 | 50 | NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.didUnmountNotification, object: nil, queue: .main) { notification in 51 | if let path = notification.userInfo?["NSDevicePath"] as? String { 52 | if TmpDiskManager.shared.diskEjected(path: path) { 53 | // We had a TmpDisk removed so we need to refresh the statusbar 54 | self.statusBar?.buildCurrentTmpDiskMenu() 55 | } 56 | } 57 | } 58 | 59 | // Kill the launcher app if it's around 60 | let launcherAppId = "com.imothee.TmpDiskLauncher" 61 | let runningApps = NSWorkspace.shared.runningApplications 62 | let isRunning = !runningApps.filter { $0.bundleIdentifier == launcherAppId }.isEmpty 63 | if isRunning { 64 | DistributedNotificationCenter.default().post(name: .killLauncher, object: Bundle.main.bundleIdentifier!) 65 | } 66 | 67 | // Check the helper status 68 | checkHelperStatus() 69 | } 70 | 71 | func applicationWillTerminate(_ aNotification: Notification) { 72 | // Insert code here to tear down your application 73 | } 74 | 75 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 76 | return true 77 | } 78 | 79 | // We expect args in the format -argname=argval 80 | private func getArgs() -> (String?, Int?) { 81 | let args = ProcessInfo.processInfo.arguments 82 | var name: String? 83 | var size: Int? 84 | 85 | for arg in args { 86 | let values = arg.components(separatedBy: "=") 87 | if values.count == 2 { 88 | switch values[0] { 89 | case "-name": 90 | name = values[1] 91 | break 92 | case "-size": 93 | size = (values[1] as NSString).integerValue 94 | break 95 | default: 96 | break 97 | } 98 | } 99 | } 100 | return (name, size) 101 | } 102 | 103 | // Check if the helper is installed, if it is check if it needs to be updated 104 | private func checkHelperStatus() { 105 | if let build = Util.checkHelperVersion() { 106 | if let newestHelperVersion = Util.latestEmbeddedHelperVersion() { 107 | if newestHelperVersion > build { 108 | _ = Util.installHelper(update: true) 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /TmpDisk/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 | -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "filename" : "icon.png", 40 | "idiom" : "mac", 41 | "scale" : "2x", 42 | "size" : "256x256" 43 | }, 44 | { 45 | "filename" : "icon-1.png", 46 | "idiom" : "mac", 47 | "scale" : "1x", 48 | "size" : "512x512" 49 | }, 50 | { 51 | "idiom" : "mac", 52 | "scale" : "2x", 53 | "size" : "512x512" 54 | } 55 | ], 56 | "info" : { 57 | "author" : "xcode", 58 | "version" : 1 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/AppIcon.appiconset/icon-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imothee/tmpdisk/c5db4f3f8567beaa5eac4a477c86a5591658ebf0/TmpDisk/Assets.xcassets/AppIcon.appiconset/icon-1.png -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/AppIcon.appiconset/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imothee/tmpdisk/c5db4f3f8567beaa5eac4a477c86a5591658ebf0/TmpDisk/Assets.xcassets/AppIcon.appiconset/icon.png -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/disk.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "disk.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "disk@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "disk@3x-2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/disk.imageset/disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imothee/tmpdisk/c5db4f3f8567beaa5eac4a477c86a5591658ebf0/TmpDisk/Assets.xcassets/disk.imageset/disk.png -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/disk.imageset/disk@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imothee/tmpdisk/c5db4f3f8567beaa5eac4a477c86a5591658ebf0/TmpDisk/Assets.xcassets/disk.imageset/disk@2x.png -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/disk.imageset/disk@3x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imothee/tmpdisk/c5db4f3f8567beaa5eac4a477c86a5591658ebf0/TmpDisk/Assets.xcassets/disk.imageset/disk@3x-2.png -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/eject.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "eject.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 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 | } 22 | -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/eject.imageset/eject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imothee/tmpdisk/c5db4f3f8567beaa5eac4a477c86a5591658ebf0/TmpDisk/Assets.xcassets/eject.imageset/eject.png -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/eject_a.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "eject_a.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 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 | } 22 | -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/eject_a.imageset/eject_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imothee/tmpdisk/c5db4f3f8567beaa5eac4a477c86a5591658ebf0/TmpDisk/Assets.xcassets/eject_a.imageset/eject_a.png -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/recreate.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "recreate.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 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 | } 22 | -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/recreate.imageset/recreate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imothee/tmpdisk/c5db4f3f8567beaa5eac4a477c86a5591658ebf0/TmpDisk/Assets.xcassets/recreate.imageset/recreate.png -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/recreate_a.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "recreate_a.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 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 | } 22 | -------------------------------------------------------------------------------- /TmpDisk/Assets.xcassets/recreate_a.imageset/recreate_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imothee/tmpdisk/c5db4f3f8567beaa5eac4a477c86a5591658ebf0/TmpDisk/Assets.xcassets/recreate_a.imageset/recreate_a.png -------------------------------------------------------------------------------- /TmpDisk/IconUtil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IconUtil.swift 3 | // TmpDisk 4 | // 5 | // Created by Tim on 12/17/22. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class IconUtil { 12 | private let iconutilPath = "/usr/bin/iconutil" 13 | 14 | static let shared: IconUtil = { 15 | let instance = IconUtil() 16 | // setup code 17 | return instance 18 | }() 19 | 20 | private func createIconSet(image: NSImage, iconSetURL: URL) throws { 21 | // Create the iconset directory 22 | try FileManager.default.createDirectory(at: iconSetURL, withIntermediateDirectories: true) 23 | 24 | // Write out all the icon files 25 | for dimension in [16, 32, 128, 256, 512] { 26 | for scale in [1, 2] { 27 | let size = NSSize(width: dimension * scale, height: dimension * scale) 28 | let filename = "icon_\(dimension)x\(dimension)\(scale == 1 ? "" : "@\(scale)x").png" 29 | 30 | let frame = NSRect(x: 0, y: 0, width: size.width, height: size.height) 31 | guard let representation = image.bestRepresentation(for: frame, context: nil, hints: nil) else { 32 | print("Issue loading tiff from image") 33 | return 34 | } 35 | let newImage = NSImage(size: size, flipped: false, drawingHandler: { (_) -> Bool in 36 | return representation.draw(in: frame) 37 | }) 38 | guard let tiffRep = newImage.tiffRepresentation, 39 | let bitmapRep = NSBitmapImageRep(data: tiffRep), 40 | let pngData = bitmapRep.representation(using: .png, properties: [:]) 41 | else { 42 | return 43 | } 44 | 45 | try pngData.write(to: iconSetURL.appendingPathComponent(filename)) 46 | } 47 | } 48 | } 49 | 50 | func convertImageToICNS(image: NSImage) throws -> String { 51 | let tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) 52 | let iconSetURL = tmpDir.appendingPathComponent("icon.iconset") 53 | let iconURL = tmpDir.appendingPathComponent("icon.icns") 54 | 55 | // Get the iconset of all the sized images 56 | try self.createIconSet(image: image, iconSetURL: iconSetURL) 57 | 58 | // Now we need to convert to an icns 59 | let process = Process() 60 | let pipe = Pipe() 61 | 62 | process.standardOutput = pipe 63 | process.standardError = pipe 64 | process.arguments = ["-c", "icns", iconSetURL.lastPathComponent] 65 | 66 | process.launchPath = iconutilPath 67 | process.currentDirectoryPath = tmpDir.path 68 | process.launch() 69 | process.waitUntilExit() 70 | 71 | if process.terminationStatus != 0 { 72 | print("FAILED") 73 | } 74 | 75 | let data = try Data(contentsOf: iconURL) 76 | try? FileManager.default.removeItem(at: tmpDir) 77 | return data.base64EncodedString() 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /TmpDisk/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SMPrivilegedExecutables 6 | 7 | com.imothee.TmpDiskHelper 8 | anchor apple generic and identifier com.imothee.TmpDiskHelper and certificate leaf[subject.OU] = AGZ3AP53DM 9 | 10 | SUEnableSystemProfiling 11 | 12 | SUFeedURL 13 | https://apps.imothee.com/updates/tmpdisk.xml 14 | SUPublicDSAKeyFile 15 | dsa_pub.pem 16 | SUPublicEDKey 17 | cHYVa9jmyIHtzvsj9w8WQ/L19rbBklLx75GNou5ipjw= 18 | TmpDiskHelperVersion 19 | 4 20 | 21 | 22 | -------------------------------------------------------------------------------- /TmpDisk/StatusBarController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusBarController.swift 3 | // TmpDisk 4 | // 5 | // Created by Tim on 12/11/21. 6 | // 7 | // This file is part of TmpDisk. 8 | // 9 | // TmpDisk is free software: you can redistribute it and/or modify 10 | // it under the terms of the GNU General Public License as published by 11 | // the Free Software Foundation, either version 3 of the License, or 12 | // (at your option) any later version. 13 | // 14 | // TmpDisk is distributed in the hope that it will be useful, 15 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | // GNU General Public License for more details. 18 | // 19 | // You should have received a copy of the GNU General Public License 20 | // along with TmpDisk. If not, see . 21 | 22 | import AppKit 23 | import ServiceManagement 24 | import Sparkle 25 | 26 | class StatusBarController { 27 | private var statusItem: NSStatusItem 28 | private var statusMenu: NSMenu 29 | private var currentTmpDisksMenu: NSMenu 30 | 31 | private let launcherAppId = "com.imothee.TmpDiskLauncher" 32 | private let updaterController: SPUStandardUpdaterController 33 | 34 | private let windowManager = WindowManager() 35 | 36 | init() { 37 | statusItem = NSStatusBar.system.statusItem(withLength: 28.0) 38 | statusMenu = NSMenu() 39 | currentTmpDisksMenu = NSMenu() 40 | 41 | // Sparkle 42 | updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) 43 | 44 | // Check to see the app is in the login items 45 | let jobDicts = SMCopyAllJobDictionaries( kSMDomainUserLaunchd ).takeRetainedValue() as NSArray as! [[String:AnyObject]] 46 | let startOnLogin = jobDicts.filter { $0["Label"] as! String == self.launcherAppId }.isEmpty == false 47 | 48 | // Create the menu 49 | if let statusBarButton = statusItem.button { 50 | statusBarButton.image = NSImage(named: "disk") 51 | statusBarButton.image?.size = NSSize(width: 18.0, height: 18.0) 52 | statusBarButton.image?.isTemplate = true 53 | } 54 | 55 | // New TmpDisk section 56 | let newTmpDiskItem = NSMenuItem(title: NSLocalizedString("New TmpDisk", comment: ""), action: #selector(newTmpDisk(sender:)), keyEquivalent: "n") 57 | newTmpDiskItem.target = self 58 | statusMenu.addItem(newTmpDiskItem) 59 | 60 | statusMenu.addItem(NSMenuItem.separator()) 61 | 62 | // Existing TmpDisk section 63 | let currentTmpDisksItem = NSMenuItem(title: NSLocalizedString("Current TmpDisks", comment: ""), action: nil, keyEquivalent: "") 64 | 65 | statusMenu.addItem(currentTmpDisksItem) 66 | statusMenu.setSubmenu(self.currentTmpDisksMenu, for: currentTmpDisksItem) 67 | 68 | self.buildCurrentTmpDiskMenu() 69 | 70 | // Recreate All 71 | let recreateAllItem = NSMenuItem(title: NSLocalizedString("Recreate All", comment: ""), action: #selector(recreateAll(sender:)), keyEquivalent: "") 72 | recreateAllItem.target = self 73 | statusMenu.addItem(recreateAllItem) 74 | 75 | // AutocreateManager 76 | let autoCreateManagerItem = NSMenuItem(title: NSLocalizedString("AutoCreate Manager", comment: ""), action: #selector(autoCreateManager(sender:)), keyEquivalent: "") 77 | autoCreateManagerItem.target = self 78 | statusMenu.addItem(autoCreateManagerItem) 79 | 80 | // Separator 81 | statusMenu.addItem(NSMenuItem.separator()) 82 | 83 | // Settings section 84 | let startLoginItem = NSMenuItem(title: NSLocalizedString("Always Start on Login", comment: ""), action: #selector(toggleStartOnLogin(sender:)), keyEquivalent: "") 85 | startLoginItem.target = self 86 | startLoginItem.state = startOnLogin ? .on : .off 87 | statusMenu.addItem(startLoginItem) 88 | 89 | let checkUpdateItem = NSMenuItem(title: NSLocalizedString("Check for Updates", comment: ""), action:nil, keyEquivalent: "") 90 | checkUpdateItem.target = updaterController 91 | checkUpdateItem.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:)) 92 | statusMenu.addItem(checkUpdateItem) 93 | 94 | let preferencesItem = NSMenuItem(title: NSLocalizedString("Preferences", comment: ""), action: #selector(preferences(sender:)), keyEquivalent: "") 95 | preferencesItem.target = self 96 | statusMenu.addItem(preferencesItem) 97 | 98 | // Separator 99 | statusMenu.addItem(NSMenuItem.separator()) 100 | 101 | // About 102 | 103 | let aboutItem = NSMenuItem(title: NSLocalizedString("About TmpDisk", comment: ""), action: #selector(about(sender:)), keyEquivalent: "") 104 | aboutItem.target = self 105 | statusMenu.addItem(aboutItem) 106 | 107 | // Separator 108 | statusMenu.addItem(NSMenuItem.separator()) 109 | 110 | // Quit 111 | let quitItem = NSMenuItem(title: NSLocalizedString("Quit", comment: ""), action: #selector(quit(sender:)), keyEquivalent: "") 112 | quitItem.target = self 113 | statusMenu.addItem(quitItem) 114 | 115 | // Add the menu to the item 116 | statusItem.menu = statusMenu 117 | } 118 | 119 | func windowWillClose(window: NSWindowController) { 120 | 121 | } 122 | 123 | // MARK: - Internal 124 | 125 | func confirmEject(volume: TmpDiskVolume) -> Bool { 126 | if (volume.showWarning()) { 127 | let alert = NSAlert() 128 | alert.messageText = NSLocalizedString("Volume contains files, are you sure you want to eject?", comment: "") 129 | alert.alertStyle = .warning 130 | alert.addButton(withTitle: "OK") 131 | alert.addButton(withTitle: "Cancel") 132 | return alert.runModal() == .alertFirstButtonReturn 133 | } 134 | return true 135 | } 136 | 137 | func confirmRecreateAll() -> Bool { 138 | let alert = NSAlert() 139 | alert.messageText = NSLocalizedString("This will eject and recreate all TmpDisks. All existing data will be lost.", comment: "") 140 | alert.alertStyle = .warning 141 | alert.addButton(withTitle: "OK") 142 | alert.addButton(withTitle: "Cancel") 143 | return alert.runModal() == .alertFirstButtonReturn 144 | } 145 | 146 | func buildCurrentTmpDiskMenu() { 147 | self.currentTmpDisksMenu.removeAllItems() 148 | for volume in TmpDiskManager.shared.volumes { 149 | let volumeItem = TmpDiskMenuItem.init(title: volume.name, action: nil, keyEquivalent: "", clickHandler: { 150 | NSWorkspace.shared.open(volume.URL()) 151 | self.statusMenu.cancelTracking() 152 | }, recreateHandler: { 153 | if self.confirmEject(volume: volume) { 154 | TmpDiskManager.shared.ejectTmpDisksWithName(names: [volume.name], recreate: true) 155 | } 156 | self.statusMenu.cancelTracking() 157 | }, ejectHandler: { 158 | if self.confirmEject(volume: volume) { 159 | TmpDiskManager.shared.ejectTmpDisksWithName(names: [volume.name], recreate: false) 160 | } 161 | self.statusMenu.cancelTracking() 162 | }) 163 | volumeItem.target = self 164 | self.currentTmpDisksMenu.addItem(volumeItem) 165 | } 166 | } 167 | 168 | // MARK: - Actions 169 | 170 | @objc func newTmpDisk(sender: AnyObject) { 171 | windowManager.showNewTmpDiskWindow() 172 | } 173 | 174 | @objc func recreateAll(sender: AnyObject) { 175 | if confirmRecreateAll() { 176 | TmpDiskManager.shared.ejectAllTmpDisks(recreate: true) 177 | } 178 | } 179 | 180 | @objc func autoCreateManager(sender: AnyObject) { 181 | windowManager.showAutoCreateManagerWindow() 182 | } 183 | 184 | @objc func toggleStartOnLogin(sender: AnyObject) { 185 | if let menuItem = sender as? NSMenuItem { 186 | if menuItem.state == .on { 187 | SMLoginItemSetEnabled(self.launcherAppId as CFString, false) 188 | menuItem.state = .off 189 | } else { 190 | SMLoginItemSetEnabled(self.launcherAppId as CFString, true) 191 | menuItem.state = .on 192 | } 193 | } 194 | } 195 | 196 | @objc func preferences(sender: AnyObject) { 197 | windowManager.showPreferencesWindow() 198 | } 199 | 200 | @objc func about(sender: AnyObject) { 201 | windowManager.showAboutWindow() 202 | } 203 | 204 | @objc func quit(sender: AnyObject) { 205 | NSApp.terminate(nil) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /TmpDisk/TmpDisk.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TmpDisk/TmpDiskManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TmpDiskManager.swift 3 | // TmpDisk 4 | // 5 | // Created by @imothee on 12/11/21. 6 | // 7 | // This file is part of TmpDisk. 8 | // 9 | // TmpDisk is free software: you can redistribute it and/or modify 10 | // it under the terms of the GNU General Public License as published by 11 | // the Free Software Foundation, either version 3 of the License, or 12 | // (at your option) any later version. 13 | // 14 | // TmpDisk is distributed in the hope that it will be useful, 15 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | // GNU General Public License for more details. 18 | // 19 | // You should have received a copy of the GNU General Public License 20 | // along with TmpDisk. If not, see . 21 | 22 | import Foundation 23 | import AppKit 24 | 25 | let TmpDiskHome = "\(NSHomeDirectory())/.tmpdisk" 26 | 27 | class TmpDiskManager { 28 | 29 | static let shared: TmpDiskManager = { 30 | let instance = TmpDiskManager() 31 | // setup code 32 | return instance 33 | }() 34 | 35 | static var rootFolder = UserDefaults.standard.object(forKey: "rootFolder") as? String ?? TmpDiskHome 36 | var volumes: Set = [] 37 | 38 | init() { 39 | // Check for existing tmpdisks 40 | if let vols = try? FileManager.default.contentsOfDirectory(atPath: "/Volumes") { 41 | for vol in vols { 42 | let tmpdiskFilePath = "/Volumes/\(vol)/.tmpdisk" 43 | if FileManager.default.fileExists(atPath: tmpdiskFilePath) { 44 | if let jsonData = FileManager.default.contents(atPath: tmpdiskFilePath) { 45 | if let volume = try? JSONDecoder().decode(TmpDiskVolume.self, from: jsonData) { 46 | self.volumes.insert(volume) 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | // Check for existing tmpfs 54 | if let vols = try? FileManager.default.contentsOfDirectory(atPath: TmpDiskManager.rootFolder) { 55 | for vol in vols { 56 | // For now we don't check if it's mounted, just check for the tmpdisk file 57 | let tmpdiskFilePath = "\(TmpDiskManager.rootFolder)/\(vol)/.tmpdisk" 58 | if FileManager.default.fileExists(atPath: tmpdiskFilePath) { 59 | if let jsonData = FileManager.default.contents(atPath: tmpdiskFilePath) { 60 | if let volume = try? JSONDecoder().decode(TmpDiskVolume.self, from: jsonData) { 61 | self.volumes.insert(volume) 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | // AutoCreate any saved TmpDisks 69 | for volume in self.getAutoCreateVolumes() { 70 | self.createTmpDisk(volume: volume) { error in 71 | if let error = error { 72 | // TODO: Add autocreate error 73 | } 74 | } 75 | } 76 | } 77 | 78 | // MARK: - TmpDiskManager API 79 | 80 | func getAutoCreateVolumes() -> Set { 81 | var autoCreateVolumes: Set = [] 82 | if let autoCreate = UserDefaults.standard.object(forKey: "autoCreate") as? [Dictionary] { 83 | for vol in autoCreate { 84 | if let name = vol["name"] as? String, let size = vol["size"] as? Int, let indexed = vol["indexed"] as? Bool, let hidden = vol["hidden"] as? Bool, let tmpFs = vol["tmpFs"] as? Bool { 85 | 86 | let caseSensitive = vol["caseSensitive"] as? Bool ?? false 87 | let journaled = vol["journaled"] as? Bool ?? false 88 | let warnOnEject = vol["warnOnEject"] as? Bool ?? false 89 | let folders = vol["folders"] as? [String] ?? [] 90 | let icon = vol["icon"] as? String 91 | 92 | let volume = TmpDiskVolume( 93 | name: name, 94 | size: size, 95 | indexed: indexed, 96 | hidden: hidden, 97 | tmpFs: tmpFs, 98 | caseSensitive: caseSensitive, 99 | journaled: journaled, 100 | warnOnEject: warnOnEject, 101 | folders: folders, 102 | icon: icon 103 | ) 104 | autoCreateVolumes.insert(volume) 105 | } 106 | } 107 | } 108 | return autoCreateVolumes 109 | } 110 | 111 | func addAutoCreateVolume(volume: TmpDiskVolume) { 112 | var autoCreateVolumes = self.getAutoCreateVolumes() 113 | autoCreateVolumes.insert(volume) 114 | self.saveAutoCreateVolumes(volumes: autoCreateVolumes) 115 | } 116 | 117 | func saveAutoCreateVolumes(volumes: Set) { 118 | let value = volumes.map { $0.dictionary() } 119 | UserDefaults.standard.set(value, forKey: "autoCreate") 120 | } 121 | 122 | func createTmpDisk(volume: TmpDiskVolume, onCreate: @escaping (TmpDiskError?) -> Void) { 123 | if volume.name.isEmpty { 124 | return onCreate(.noName) 125 | } 126 | 127 | if volume.size <= 0 { 128 | return onCreate(.invalidSize) 129 | } 130 | 131 | if volumes.contains(where: { $0.name == volume.name }) || self.exists(volume: volume) { 132 | return onCreate(.exists) 133 | } 134 | 135 | if Util.checkHelperVersion() != nil { 136 | let client = XPCClient() 137 | 138 | var task: String? 139 | if volume.tmpFs { 140 | task = try? self.createTmpFsTask(volume: volume) 141 | } else { 142 | task = self.createRamDiskTask(volume: volume) 143 | } 144 | 145 | guard let task = task else { 146 | return onCreate(.failed) 147 | } 148 | 149 | client.createVolume(task) { error in 150 | if error != nil { 151 | return onCreate(error) 152 | } 153 | self.diskCreated(volume: volume) 154 | onCreate(nil) 155 | } 156 | return 157 | } 158 | 159 | // Old flow without the helper installed 160 | 161 | let task: Process? 162 | if volume.tmpFs { 163 | task = try? self.createTmpFs(volume: volume) 164 | } else { 165 | task = self.createRamDisk(volume: volume) 166 | } 167 | 168 | guard let task = task else { 169 | return onCreate(.failed) 170 | } 171 | 172 | task.terminationHandler = { process in 173 | if process.terminationStatus != 0 { 174 | return onCreate(.failed) 175 | } 176 | self.diskCreated(volume: volume) 177 | onCreate(nil) 178 | } 179 | if #available(macOS 10.13, *) { 180 | do { 181 | try task.run() 182 | } catch { 183 | print(error) 184 | } 185 | } else { 186 | task.launch() 187 | } 188 | } 189 | 190 | func ejectAllTmpDisks(recreate: Bool) { 191 | let names = self.volumes.map { $0.name } 192 | self.ejectTmpDisksWithName(names: names, recreate: recreate) 193 | } 194 | 195 | func ejectTmpDisksWithName(names: [String], recreate: Bool) { 196 | let group = DispatchGroup() 197 | for volume in self.volumes.filter({ names.contains($0.name) }) { 198 | group.enter() 199 | 200 | let ws = NSWorkspace() 201 | do { 202 | try ws.unmountAndEjectDevice(at: volume.URL()) 203 | if volume.tmpFs { 204 | try FileManager.default.removeItem(atPath: volume.path()) 205 | } 206 | DispatchQueue.main.async { 207 | self.volumes.remove(volume) 208 | if recreate { 209 | self.createTmpDisk(volume: volume, onCreate: {_ in }) 210 | } 211 | } 212 | } catch let error as NSError { 213 | if error.code == fBsyErr { 214 | DispatchQueue.main.async { 215 | self.ejectErrorDiskInUse(name: volume.name, recreate: recreate) 216 | } 217 | } else { 218 | DispatchQueue.main.async { 219 | self.ejectError(name: volume.name) 220 | } 221 | } 222 | } 223 | 224 | group.leave() 225 | } 226 | 227 | } 228 | 229 | func forceEject(name: String, recreate: Bool) { 230 | guard let volume = self.volumes.first(where: { name == $0.name }) else { 231 | return 232 | } 233 | 234 | let task = Process() 235 | task.launchPath = "/sbin/umount" 236 | 237 | let command = volume.path() 238 | task.arguments = ["-f", command] 239 | 240 | task.terminationHandler = { process in 241 | if process.terminationStatus != 0 { 242 | DispatchQueue.main.async { 243 | self.ejectError(name: name) 244 | } 245 | return 246 | } 247 | 248 | DispatchQueue.main.async { 249 | self.volumes.remove(volume) 250 | 251 | if recreate { 252 | // We've force ejected so recreate the TmpDisk 253 | self.createTmpDisk(volume: volume, onCreate: {_ in }) 254 | } 255 | } 256 | } 257 | 258 | if #available(macOS 10.13, *) { 259 | do { 260 | try task.run() 261 | } catch { 262 | print(error) 263 | } 264 | } else { 265 | task.launch() 266 | } 267 | } 268 | 269 | func ejectErrorDiskInUse(name: String, recreate: Bool) { 270 | let alert = NSAlert() 271 | alert.messageText = String(format: NSLocalizedString("The volume \"%@\" wasn't ejected because one or more programs may be using it", comment: ""), name) 272 | alert.informativeText = NSLocalizedString("To eject the disk immediately, hit the Force Eject button", comment: "") 273 | alert.alertStyle = .warning 274 | alert.addButton(withTitle: NSLocalizedString("Force Eject", comment: "")) 275 | alert.addButton(withTitle: "Cancel") 276 | if alert.runModal() == .alertFirstButtonReturn { 277 | self.forceEject(name: name, recreate: recreate) 278 | } 279 | } 280 | 281 | func ejectError(name: String) { 282 | let alert = NSAlert() 283 | alert.messageText = String(format: NSLocalizedString("Failed to eject \"%@\"", comment: ""), name) 284 | alert.alertStyle = .warning 285 | alert.addButton(withTitle: "OK") 286 | alert.runModal() 287 | } 288 | 289 | /* 290 | diskEjected takes a path and checks to see if it's a TmpDisk 291 | If it is, remove it from the volumes and return true so we can refresh the menubar 292 | */ 293 | func diskEjected(path: String) -> Bool { 294 | for volume in self.volumes { 295 | if volume.path() == path { 296 | self.volumes.remove(volume) 297 | return true 298 | } 299 | } 300 | return false 301 | } 302 | 303 | // MARK: - Helper functions 304 | 305 | func diskCreated(volume: TmpDiskVolume) { 306 | if let jsonData = try? JSONEncoder().encode(volume) { 307 | let jsonString = String(data: jsonData, encoding: .utf8)! 308 | try? jsonString.write(toFile: "\(volume.path())/.tmpdisk", atomically: true, encoding: .utf8) 309 | } 310 | 311 | if volume.indexed { 312 | self.indexVolume(volume: volume) 313 | } 314 | 315 | // Create the folders if there are any set 316 | self.createFolders(volume: volume) 317 | 318 | // Create the icon if there is one set 319 | self.createIcon(volume: volume) 320 | 321 | if volume.autoCreate { 322 | self.addAutoCreateVolume(volume: volume) 323 | } 324 | 325 | DispatchQueue.main.async { 326 | self.volumes.insert(volume) 327 | } 328 | NotificationCenter.default.post(name: .tmpDiskMounted, object: nil) 329 | } 330 | 331 | func createTmpFsTask(volume: TmpDiskVolume) throws -> String { 332 | // Setup the volume mount folder 333 | if !FileManager.default.fileExists(atPath: volume.path()) { 334 | try FileManager.default.createDirectory(atPath: volume.path(), withIntermediateDirectories: true, attributes: nil) 335 | } 336 | 337 | return "mount_tmpfs -s\(volume.size)M \(volume.path())" 338 | } 339 | 340 | func createRamDiskTask(volume: TmpDiskVolume) -> String { 341 | let dSize = UInt64(volume.size) * 2048 342 | 343 | let filesystem: String = { 344 | switch (volume.caseSensitive, volume.journaled) { 345 | case (false, false): 346 | return "HFS+" // Mac OS Extended 347 | case (true, false): 348 | return "HFSX" // Mac OS Extended (Case-sensitive) 349 | case (false, true): 350 | return "JHFS+" // Mac OS Extended (Journaled) 351 | case (true, true): 352 | return "JHFSX" // Mac OS Extended (Case-sensitive, Journaled) 353 | } 354 | }() 355 | 356 | if volume.hidden { 357 | return "d=$(hdiutil attach -nomount ram://\(dSize)) && diskutil eraseDisk \(filesystem) %noformat% $d && newfs_hfs -v \"\(volume.name)\" \"$(echo $d | tr -d ' ')s1\" && hdiutil attach -nomount $d && hdiutil attach -nobrowse \"$(echo $d | tr -d ' ')s1\"" 358 | } else { 359 | return "diskutil eraseVolume \(filesystem) \"\(volume.name)\" `hdiutil attach -nomount ram://\(dSize)`" 360 | } 361 | } 362 | 363 | func createTmpFs(volume: TmpDiskVolume) throws -> Process { 364 | // Run the process 365 | let task = Process() 366 | task.launchPath = "/usr/bin/osascript" 367 | let command = try createTmpFsTask(volume: volume) 368 | let script = """ 369 | do shell script "\(command)" with administrator privileges 370 | """ 371 | 372 | task.arguments = ["-e", script] 373 | return task 374 | } 375 | 376 | func createRamDisk(volume: TmpDiskVolume) -> Process { 377 | let task = Process() 378 | task.launchPath = "/bin/sh" 379 | 380 | let command = createRamDiskTask(volume: volume) 381 | task.arguments = ["-c", command] 382 | return task 383 | } 384 | 385 | func createFolders(volume: TmpDiskVolume) { 386 | for folder in volume.folders { 387 | let path = "\(volume.path())/\(folder)" 388 | if !FileManager.default.fileExists(atPath: path) { 389 | try? FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) 390 | } 391 | } 392 | } 393 | 394 | func indexVolume(volume: TmpDiskVolume) { 395 | let task = Process() 396 | task.launchPath = "/bin/sh" 397 | 398 | let command = "mdutil -i on \(volume.path())" 399 | task.arguments = ["-c", command] 400 | task.launch() 401 | } 402 | 403 | func exists(volume: TmpDiskVolume) -> Bool { 404 | if volume.tmpFs { 405 | // TODO: lookup mount instead of just the tmpdisk file 406 | return FileManager.default.fileExists(atPath: "\(volume.path())/.tmpdisk") 407 | } 408 | return FileManager.default.fileExists(atPath: volume.path()) 409 | } 410 | 411 | func createIcon(volume: TmpDiskVolume) { 412 | if let icon = volume.icon { 413 | if let data = Data(base64Encoded: icon) { 414 | let image = NSImage(data: data) 415 | NSWorkspace.shared.setIcon(image, forFile: volume.path()) 416 | } 417 | } 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /TmpDisk/Util.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Util.swift 3 | // TmpDisk 4 | // 5 | // Created by Tim on 2/28/24. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import SecurityFoundation 11 | import ServiceManagement 12 | 13 | struct Util { 14 | 15 | static func askAuthorization() -> AuthorizationRef? { 16 | 17 | var auth: AuthorizationRef? 18 | let status: OSStatus = AuthorizationCreate(nil, nil, [], &auth) 19 | if status != errAuthorizationSuccess { 20 | NSLog("[SMJBS]: Authorization failed with status code \(status)") 21 | 22 | return nil 23 | } 24 | 25 | return auth 26 | } 27 | 28 | @discardableResult 29 | static func blessHelper(label: String, authorization: AuthorizationRef) -> Bool { 30 | 31 | var error: Unmanaged? 32 | let blessStatus = SMJobBless(kSMDomainSystemLaunchd, label as CFString, authorization, &error) 33 | 34 | if !blessStatus { 35 | NSLog("[SMJBS]: Helper bless failed with error \(error!.takeUnretainedValue())") 36 | } 37 | 38 | return blessStatus 39 | } 40 | 41 | // Returns a version number or nil if helper is not installed 42 | static func checkHelperVersion() -> Int? { 43 | // Registered with launchd 44 | let process = Process() 45 | process.launchPath = "/bin/launchctl" 46 | process.arguments = ["print", "system/\(Constant.helperMachLabel)"] 47 | process.qualityOfService = QualityOfService.userInitiated 48 | process.standardOutput = nil 49 | process.standardError = nil 50 | process.launch() 51 | process.waitUntilExit() 52 | let registeredWithLaunchd = (process.terminationStatus == 0) 53 | 54 | if registeredWithLaunchd { 55 | let plist = NSDictionary(contentsOfFile: "/Library/LaunchDaemons/com.imothee.TmpDiskHelper.plist") 56 | if let build = plist?["Build"] as? String { 57 | return Int(build) 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | static func latestEmbeddedHelperVersion() -> Int? { 64 | if let build = Bundle.main.infoDictionary?["TmpDiskHelperVersion"] as? String { 65 | return Int(build) 66 | } 67 | return nil 68 | } 69 | 70 | static func installHelper(update: Bool = false) -> Bool { 71 | let messageText = update ? 72 | NSLocalizedString("There is an updated TmpDiskHelper, do you wish to install it? Requires admin password.", comment: "") : 73 | NSLocalizedString("Do you wish to install TmpDiskHelper? It will require your admin password.", comment: "") 74 | 75 | let alert = NSAlert() 76 | alert.messageText = messageText 77 | alert.alertStyle = .warning 78 | alert.addButton(withTitle: "OK") 79 | alert.addButton(withTitle: "Cancel") 80 | if alert.runModal() == .alertFirstButtonReturn { 81 | guard let auth = Util.askAuthorization() else { 82 | fatalError("Authorization not acquired.") 83 | } 84 | 85 | Util.blessHelper(label: Constant.helperMachLabel, authorization: auth) 86 | return true 87 | } 88 | return false 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /TmpDisk/Views/AboutViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutViewController.swift 3 | // TmpDisk 4 | // 5 | // Created by Tim on 1/28/23. 6 | // 7 | 8 | import AppKit 9 | 10 | class AboutViewController: NSViewController { 11 | @IBOutlet weak var issuesUrl: NSTextField! 12 | @IBOutlet weak var sourceUrl: NSTextField! 13 | 14 | override func viewDidLoad() { 15 | issuesUrl.allowsEditingTextAttributes = true 16 | issuesUrl.isSelectable = true 17 | 18 | sourceUrl.allowsEditingTextAttributes = true 19 | sourceUrl.isSelectable = true 20 | 21 | let issuesAttributedString = NSMutableAttributedString(string: issuesUrl.stringValue, attributes:[.link: URL(string: issuesUrl.stringValue)!]) 22 | 23 | let sourceAttributedString = NSMutableAttributedString(string: sourceUrl.stringValue, attributes:[.link: URL(string: sourceUrl.stringValue)!]) 24 | 25 | issuesUrl.attributedStringValue = issuesAttributedString 26 | sourceUrl.attributedStringValue = sourceAttributedString 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /TmpDisk/Views/AutoCreateManagerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoCreateManagerViewController.swift 3 | // TmpDisk 4 | // 5 | // Created by @imothee on 12/19/21. 6 | // 7 | // This file is part of TmpDisk. 8 | // 9 | // TmpDisk is free software: you can redistribute it and/or modify 10 | // it under the terms of the GNU General Public License as published by 11 | // the Free Software Foundation, either version 3 of the License, or 12 | // (at your option) any later version. 13 | // 14 | // TmpDisk is distributed in the hope that it will be useful, 15 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | // GNU General Public License for more details. 18 | // 19 | // You should have received a copy of the GNU General Public License 20 | // along with TmpDisk. If not, see . 21 | 22 | import Foundation 23 | import AppKit 24 | 25 | class AutoCreateManagerViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource, NSTextFieldDelegate { 26 | @IBOutlet weak var tableView: NSTableView! 27 | 28 | var volumes: [TmpDiskVolume] = [] 29 | 30 | override func viewDidLoad() { 31 | self.volumes.append(contentsOf: Array(TmpDiskManager.shared.getAutoCreateVolumes())) 32 | } 33 | 34 | // MARK: - NSTableViewDataSource 35 | 36 | func numberOfRows(in tableView: NSTableView) -> Int { 37 | return self.volumes.count 38 | } 39 | 40 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 41 | let volume = self.volumes[row] 42 | 43 | switch (tableColumn?.identifier.rawValue) { 44 | case "name": 45 | let cell = tableView.makeView(withIdentifier: (tableColumn!.identifier), owner: self) as? NSTableCellView 46 | cell?.textField?.stringValue = volume.name 47 | return cell 48 | case "size": 49 | let cell = tableView.makeView(withIdentifier: (tableColumn!.identifier), owner: self) as? NSTableCellView 50 | cell?.textField?.stringValue = String(volume.size) 51 | return cell 52 | case "folders": 53 | let cell = tableView.makeView(withIdentifier: (tableColumn!.identifier), owner: self) as? NSTableCellView 54 | cell?.textField?.stringValue = volume.folders.joined(separator: ",") 55 | return cell 56 | case "tmpFs": 57 | let cell = tableView.makeView(withIdentifier: (tableColumn!.identifier), owner: self) as? CheckboxTableCellView 58 | cell?.checkbox.state = volume.tmpFs ? .on : .off 59 | return cell 60 | case "indexed": 61 | let cell = tableView.makeView(withIdentifier: (tableColumn!.identifier), owner: self) as? CheckboxTableCellView 62 | cell?.checkbox.state = volume.indexed ? .on : .off 63 | return cell 64 | case "hidden": 65 | let cell = tableView.makeView(withIdentifier: (tableColumn!.identifier), owner: self) as? CheckboxTableCellView 66 | cell?.checkbox.state = volume.hidden ? .on : .off 67 | if volume.tmpFs { 68 | cell?.checkbox.isEnabled = false 69 | } else { 70 | cell?.checkbox.isEnabled = true 71 | } 72 | return cell 73 | case "casesensitive": 74 | let cell = tableView.makeView(withIdentifier: (tableColumn!.identifier), owner: self) as? CheckboxTableCellView 75 | cell?.checkbox.state = volume.caseSensitive ? .on : .off 76 | if volume.tmpFs { 77 | cell?.checkbox.isEnabled = false 78 | } else { 79 | cell?.checkbox.isEnabled = true 80 | } 81 | return cell 82 | case "journaled": 83 | let cell = tableView.makeView(withIdentifier: (tableColumn!.identifier), owner: self) as? CheckboxTableCellView 84 | cell?.checkbox.state = volume.journaled ? .on : .off 85 | if volume.tmpFs { 86 | cell?.checkbox.isEnabled = false 87 | } else { 88 | cell?.checkbox.isEnabled = true 89 | } 90 | return cell 91 | case "warn": 92 | let cell = tableView.makeView(withIdentifier: (tableColumn!.identifier), owner: self) as? CheckboxTableCellView 93 | cell?.checkbox.state = volume.warnOnEject ? .on : .off 94 | return cell 95 | default: 96 | let cell = tableView.makeView(withIdentifier: (tableColumn!.identifier), owner: self) as? NSTableCellView 97 | return cell 98 | } 99 | } 100 | 101 | func controlTextDidEndEditing(_ obj: Notification) { 102 | if let textField = obj.object as? NSTextField { 103 | let row = tableView.row(for: textField) 104 | let column = tableView.column(for: textField) 105 | 106 | switch column { 107 | case 0: 108 | self.volumes[row].name = textField.stringValue 109 | break 110 | case 1: 111 | let newSize = textField.integerValue 112 | if newSize > 0 { 113 | self.volumes[row].size = newSize 114 | } else { 115 | let alert = NSAlert() 116 | alert.messageText = "Size must be a positive number in megabytes" 117 | alert.alertStyle = .warning 118 | alert.addButton(withTitle: "OK") 119 | alert.runModal() 120 | return 121 | } 122 | break 123 | case 2: 124 | self.volumes[row].folders = textField.stringValue.split(separator: ",").map { String($0) } 125 | break 126 | default: 127 | return 128 | } 129 | 130 | TmpDiskManager.shared.saveAutoCreateVolumes(volumes: Set(self.volumes)) 131 | } 132 | } 133 | 134 | @IBAction func checkboxDidChange(_ sender: AnyObject) { 135 | if let button = sender as? NSButton { 136 | let row = tableView.row(for: button) 137 | let column = tableView.column(for: button) 138 | 139 | switch column { 140 | case 3: 141 | self.volumes[row].tmpFs = button.state == .on 142 | // Reload the table so the enabled state updates 143 | self.tableView.reloadData() 144 | break 145 | case 4: 146 | self.volumes[row].indexed = button.state == .on 147 | break 148 | case 5: 149 | self.volumes[row].hidden = button.state == .on 150 | break 151 | case 6: 152 | self.volumes[row].caseSensitive = button.state == .on 153 | break 154 | case 7: 155 | self.volumes[row].journaled = button.state == .on 156 | break 157 | case 8: 158 | self.volumes[row].warnOnEject = button.state == .on 159 | break 160 | default: 161 | return 162 | } 163 | 164 | TmpDiskManager.shared.saveAutoCreateVolumes(volumes: Set(self.volumes)) 165 | } 166 | } 167 | 168 | @IBAction func recreate(_ sender: AnyObject) { 169 | if let button = sender as? NSButton { 170 | let row = tableView.row(for: button) 171 | 172 | if confirmRecreate() { 173 | let volume = self.volumes[row] 174 | if TmpDiskManager.shared.exists(volume: volume) { 175 | TmpDiskManager.shared.ejectTmpDisksWithName(names: [volume.name], recreate: true) 176 | } else { 177 | TmpDiskManager.shared.createTmpDisk(volume: volume, onCreate: {_ in }) 178 | } 179 | } 180 | } 181 | } 182 | 183 | @IBAction func removeRow(_ sender: AnyObject) { 184 | if let button = sender as? NSButton { 185 | let row = tableView.row(for: button) 186 | 187 | self.volumes.remove(at: row) 188 | TmpDiskManager.shared.saveAutoCreateVolumes(volumes: Set(self.volumes)) 189 | self.tableView.reloadData() 190 | } 191 | } 192 | 193 | func confirmRecreate() -> Bool { 194 | let alert = NSAlert() 195 | alert.messageText = NSLocalizedString("This will eject and recreate the TmpDisk. All existing data will be lost.", comment: "") 196 | alert.alertStyle = .warning 197 | alert.addButton(withTitle: "OK") 198 | alert.addButton(withTitle: "Cancel") 199 | return alert.runModal() == .alertFirstButtonReturn 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /TmpDisk/Views/CheckBoxTableCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckBoxTableViewCell.swift 3 | // TmpDisk 4 | // 5 | // Created by @imothee on 12/19/21. 6 | // 7 | // This file is part of TmpDisk. 8 | // 9 | // TmpDisk is free software: you can redistribute it and/or modify 10 | // it under the terms of the GNU General Public License as published by 11 | // the Free Software Foundation, either version 3 of the License, or 12 | // (at your option) any later version. 13 | // 14 | // TmpDisk is distributed in the hope that it will be useful, 15 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | // GNU General Public License for more details. 18 | // 19 | // You should have received a copy of the GNU General Public License 20 | // along with TmpDisk. If not, see . 21 | 22 | import AppKit 23 | 24 | class CheckboxTableCellView: NSTableCellView { 25 | @IBOutlet weak var checkbox: NSButton! 26 | } 27 | -------------------------------------------------------------------------------- /TmpDisk/Views/NewTmpDiskViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewTmpDiskView.swift 3 | // TmpDisk 4 | // 5 | // Created by @imothee on 12/11/21. 6 | // 7 | // This file is part of TmpDisk. 8 | // 9 | // TmpDisk is free software: you can redistribute it and/or modify 10 | // it under the terms of the GNU General Public License as published by 11 | // the Free Software Foundation, either version 3 of the License, or 12 | // (at your option) any later version. 13 | // 14 | // TmpDisk is distributed in the hope that it will be useful, 15 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | // GNU General Public License for more details. 18 | // 19 | // You should have received a copy of the GNU General Public License 20 | // along with TmpDisk. If not, see . 21 | 22 | 23 | import Foundation 24 | import AppKit 25 | 26 | class NewTmpDiskViewController: NSViewController, NSTextFieldDelegate { 27 | @IBOutlet weak var diskName: NSTextField! 28 | @IBOutlet weak var useTmpFs: NSButton! 29 | 30 | @IBOutlet weak var diskSizeLabel: NSTextField! 31 | @IBOutlet weak var diskSizeStepper: NSStepper! 32 | @IBOutlet weak var diskSize: NSTextField! 33 | @IBOutlet weak var folders: NSTextField! 34 | 35 | @IBOutlet weak var icon: NSImageView! 36 | 37 | @IBOutlet weak var diskUnitSelector: NSPopUpButton! 38 | @IBOutlet weak var diskSizeSelector: NSPopUpButton! 39 | 40 | @IBOutlet weak var autoCreate: NSButton! 41 | @IBOutlet weak var index: NSButton! 42 | @IBOutlet weak var hidden: NSButton! 43 | @IBOutlet weak var caseSensitive: NSButton! 44 | @IBOutlet weak var journaled: NSButton! 45 | 46 | var volume = TmpDiskVolume() 47 | var unitIndex = 0 48 | 49 | // MARK: - View controller lifecycle 50 | 51 | override public func viewDidAppear() { 52 | super.viewDidAppear() 53 | self.diskName.delegate = self 54 | self.diskSize.delegate = self 55 | self.folders.delegate = self 56 | self.setDefaultUnits() 57 | } 58 | 59 | // MARK: - NSTextFieldDelegate 60 | 61 | public func controlTextDidChange(_ obj: Notification) { 62 | if let textField = obj.object as? NSTextField, self.diskName.identifier == textField.identifier { 63 | self.volume.name = textField.stringValue 64 | } 65 | if let textField = obj.object as? NSTextField, self.diskSize.identifier == textField.identifier { 66 | self.setVolumeSize() 67 | } 68 | if let textField = obj.object as? NSTextField, self.folders.identifier == textField.identifier { 69 | self.volume.folders = textField.stringValue.split(separator: ",").map { String($0) } 70 | } 71 | } 72 | 73 | // MARK: - IBActions 74 | 75 | @IBAction func sizeStepped(_ sender: NSStepper) { 76 | if unitIndex == 0 { 77 | self.diskSize.stringValue = String(sender.integerValue) 78 | } else { 79 | let dSize = sender.integerValue / 1000 80 | self.diskSize.stringValue = String(format: "%.2f", dSize) 81 | } 82 | } 83 | 84 | @IBAction func unitSelected(_ sender: NSPopUpButton) { 85 | switch sender.indexOfSelectedItem { 86 | case 0: 87 | if unitIndex == 1 { 88 | let dSize = self.diskSize.doubleValue * 1000 89 | self.diskSize.stringValue = String(Int(dSize)) 90 | unitIndex = 0 91 | UserDefaults.standard.set(unitIndex, forKey: "defaultUnits") 92 | } 93 | break 94 | case 1: 95 | if unitIndex == 0 { 96 | let dSize = self.diskSize.doubleValue / 1000.0 97 | self.diskSize.stringValue = String(format: "%.2f", dSize) 98 | unitIndex = 1 99 | UserDefaults.standard.set(unitIndex, forKey: "defaultUnits") 100 | } 101 | break 102 | default: 103 | return 104 | } 105 | } 106 | 107 | @IBAction func sizeSelected(_ sender: NSPopUpButton) { 108 | switch sender.indexOfSelectedItem { 109 | case 1, 2, 3, 4: 110 | let percent = [0.1, 0.25, 0.5, 0.75][sender.indexOfSelectedItem - 1] 111 | let dSize = (percent * Double(ProcessInfo.init().physicalMemory)) / 1024 / 1024 112 | if unitIndex == 0 { 113 | self.diskSize.stringValue = String(Int(dSize)) 114 | } else { 115 | let newDSize = dSize / 1000.0 116 | self.diskSize.stringValue = String(format: "%.2f", newDSize) 117 | } 118 | sender.selectItem(at: 0) 119 | break 120 | default: 121 | return 122 | } 123 | } 124 | 125 | @IBAction func onIconChange(_ sender: NSImageView) { 126 | if let image = sender.image{ 127 | guard image.size.width == image.size.height else { 128 | self.showError(message: NSLocalizedString("Icon must be square", comment: "")) 129 | sender.image = nil 130 | return 131 | } 132 | 133 | guard let iconBase64 = try? IconUtil.shared.convertImageToICNS(image: image) else { 134 | self.showError(message: NSLocalizedString("Could not convert image to icon", comment: "")) 135 | sender.image = nil 136 | return 137 | } 138 | 139 | self.volume.icon = iconBase64 140 | } 141 | } 142 | 143 | @IBAction func onUseTmpFsChange(_ sender: NSButton) { 144 | if sender.state == .on { 145 | self.volume.tmpFs = true 146 | self.diskSizeLabel.stringValue = "Max Size" 147 | // Hidden button 148 | self.hidden.isHidden = true 149 | self.hidden.state = .off 150 | self.volume.hidden = false 151 | // Case sensitive button 152 | self.caseSensitive.isHidden = true 153 | self.caseSensitive.state = .off 154 | self.volume.caseSensitive = false 155 | // Journaled button 156 | self.journaled.isHidden = true 157 | self.journaled.state = .off 158 | self.volume.journaled = false 159 | } else { 160 | self.volume.tmpFs = false 161 | self.diskSizeLabel.stringValue = "Disk Size" 162 | self.hidden.isHidden = false 163 | self.caseSensitive.isHidden = false 164 | self.journaled.isHidden = false 165 | } 166 | } 167 | 168 | @IBAction func onAutoCreateChange(_ sender: NSButton) { 169 | self.volume.autoCreate = sender.state == .on 170 | } 171 | 172 | @IBAction func onIndexChange(_ sender: NSButton) { 173 | self.volume.indexed = sender.state == .on 174 | } 175 | 176 | @IBAction func onWarnChange(_ sender: NSButton) { 177 | self.volume.warnOnEject = sender.state == .on 178 | } 179 | 180 | @IBAction func onHiddenChange(_ sender: NSButton) { 181 | self.volume.hidden = sender.state == .on 182 | } 183 | 184 | @IBAction func onCaseSensitiveChange(_ sender: NSButton) { 185 | self.volume.caseSensitive = sender.state == .on 186 | } 187 | 188 | @IBAction func onJournaledChange(_ sender: NSButton) { 189 | self.volume.journaled = sender.state == .on 190 | } 191 | 192 | @IBAction func createTapped(_ sender: NSButton) { 193 | let spinner = NSProgressIndicator(frame: NSRect(x: 58.5, y: 7.5, width: 13, height: 13)) 194 | spinner.style = .spinning 195 | spinner.startAnimation(nil) 196 | 197 | sender.addSubview(spinner) 198 | sender.isEnabled = false 199 | 200 | self.setVolumeSize() 201 | 202 | if self.volume.tmpFs { 203 | // Check if we've ever prompted them to install the helper 204 | let helperPrompted = UserDefaults.standard.object(forKey: "helperPromptShowns") as? Bool 205 | let helperVersion = Util.checkHelperVersion() 206 | 207 | if helperPrompted == nil && helperVersion == nil { 208 | UserDefaults.standard.set(true, forKey: "helperPromptShown") 209 | 210 | let alert = NSAlert() 211 | alert.messageText = NSLocalizedString("You can now install the TmpDiskHelper to create TmpFS volumes without entering a password each time. The helper can be managed in TmpDisk preferences and requires your Admin Password to install.", comment: "") 212 | alert.alertStyle = .warning 213 | alert.addButton(withTitle: "OK") 214 | alert.addButton(withTitle: "Don't ask again") 215 | if alert.runModal() == .alertFirstButtonReturn { 216 | Util.installHelper() 217 | } 218 | } 219 | } 220 | 221 | TmpDiskManager.shared.createTmpDisk(volume: self.volume) { error in 222 | DispatchQueue.main.async { 223 | spinner.removeFromSuperview() 224 | sender.isEnabled = true 225 | } 226 | 227 | if let error = error { 228 | DispatchQueue.main.async { 229 | switch error { 230 | case .noName: 231 | self.showError(message: NSLocalizedString("Your TmpDisk must have a name", comment: "")) 232 | break; 233 | case .exists: 234 | self.showError(message: NSLocalizedString("A volume with this name already exists", comment: "")) 235 | break; 236 | case .invalidSize: 237 | if self.unitIndex == 0 { 238 | self.showError(message: NSLocalizedString("Size must be a number of megabytes > 0", comment: "")) 239 | } else { 240 | self.showError(message: NSLocalizedString("Size must be a number of gigabytes >= 0.01", comment: "")) 241 | } 242 | break; 243 | case .failed: 244 | self.showError(message: NSLocalizedString("Failed to create TmpDisk", comment: "")) 245 | break; 246 | case .helperInvalidated: 247 | self.showError(message: NSLocalizedString("The helper failed connection validation. Please try reinstalling the helper.", comment: "")) 248 | break; 249 | case .helperFailed: 250 | self.showError(message: NSLocalizedString("The helper crashed, please check TmpDisk logs in the Console app and log a bug.", comment: "")) 251 | break; 252 | } 253 | } 254 | return 255 | } 256 | DispatchQueue.main.async { 257 | self.view.window?.close() 258 | } 259 | } 260 | } 261 | 262 | // MARK: - Internal functions 263 | 264 | func setVolumeSize() { 265 | if unitIndex == 0 { 266 | self.volume.size = self.diskSize.integerValue 267 | } else if unitIndex == 1 { 268 | self.volume.size = Int(self.diskSize.doubleValue * 1000) 269 | } 270 | } 271 | 272 | func setDefaultUnits() { 273 | if let defaultUnits = UserDefaults.standard.object(forKey: "defaultUnits") as? Int { 274 | if defaultUnits == 1 { 275 | // Default units is now GB 276 | diskUnitSelector.selectItem(at: defaultUnits) 277 | self.unitIndex = defaultUnits 278 | } 279 | } 280 | } 281 | 282 | func showError(message: String) { 283 | let alert = NSAlert() 284 | alert.messageText = message 285 | alert.alertStyle = .warning 286 | alert.addButton(withTitle: "OK") 287 | alert.runModal() 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /TmpDisk/Views/PreferencesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreferencesWindowViewController.swift 3 | // TmpDisk 4 | // 5 | // Created by @imothee on 12/19/21. 6 | // 7 | // This file is part of TmpDisk. 8 | // 9 | // TmpDisk is free software: you can redistribute it and/or modify 10 | // it under the terms of the GNU General Public License as published by 11 | // the Free Software Foundation, either version 3 of the License, or 12 | // (at your option) any later version. 13 | // 14 | // TmpDisk is distributed in the hope that it will be useful, 15 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | // GNU General Public License for more details. 18 | // 19 | // You should have received a copy of the GNU General Public License 20 | // along with TmpDisk. If not, see . 21 | 22 | import Foundation 23 | import AppKit 24 | 25 | class PreferencesViewController: NSViewController, NSTextFieldDelegate { 26 | @IBOutlet weak var updateHelper: NSButton! 27 | @IBOutlet weak var useHelper: NSSegmentedControl! 28 | @IBOutlet weak var rootFolder: NSTextField! 29 | 30 | var helperInstalled: Bool = false 31 | 32 | override public func viewDidAppear() { 33 | super.viewDidAppear() 34 | if let root = UserDefaults.standard.object(forKey: "rootFolder") as? String { 35 | self.rootFolder.stringValue = root 36 | } 37 | self.rootFolder.placeholderString = TmpDiskHome 38 | 39 | if Util.checkHelperVersion() != nil { 40 | helperInstalled = true 41 | useHelper.setSelected(true, forSegment: 1) 42 | } else { 43 | updateHelper.isHidden = true 44 | } 45 | } 46 | 47 | @IBAction func toggleHelper(_ sender: NSSegmentedControl) { 48 | if useHelper.isSelected(forSegment: 1) { 49 | if !helperInstalled { 50 | let installed = Util.installHelper(update: false) 51 | if installed { 52 | updateHelper.isHidden = false 53 | helperInstalled = true 54 | } else { 55 | useHelper.setSelected(true, forSegment: 0) 56 | } 57 | } 58 | } else { 59 | if helperInstalled { 60 | let client = XPCClient() 61 | client.uninstall() 62 | updateHelper.isHidden = true 63 | helperInstalled = false 64 | } 65 | } 66 | } 67 | 68 | @IBAction func updateHelper(_ sender: NSButton) { 69 | Util.installHelper(update: true) 70 | } 71 | 72 | @IBAction func savePreferences(_ sender: NSButton) { 73 | if !TmpDiskManager.shared.volumes.filter({ $0.tmpFs }).isEmpty { 74 | let alert = NSAlert() 75 | alert.messageText = NSLocalizedString("You can't change the root volume while tmpFS disks are mounted.", comment: "") 76 | alert.alertStyle = .warning 77 | alert.addButton(withTitle: "OK") 78 | alert.runModal() 79 | return 80 | } 81 | 82 | if rootFolder.stringValue.isEmpty { 83 | UserDefaults.standard.removeObject(forKey: "rootFolder") 84 | TmpDiskManager.rootFolder = TmpDiskHome 85 | self.view.window?.close() 86 | return 87 | } 88 | 89 | UserDefaults.standard.set(rootFolder.stringValue, forKey: "rootFolder") 90 | TmpDiskManager.rootFolder = rootFolder.stringValue 91 | self.view.window?.close() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /TmpDisk/Views/TmpDiskMenuItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TmpDiskMenuItem.swift 3 | // TmpDisk 4 | // 5 | // Created by @imothee on 12/19/21. 6 | // 7 | // This file is part of TmpDisk. 8 | // 9 | // TmpDisk is free software: you can redistribute it and/or modify 10 | // it under the terms of the GNU General Public License as published by 11 | // the Free Software Foundation, either version 3 of the License, or 12 | // (at your option) any later version. 13 | // 14 | // TmpDisk is distributed in the hope that it will be useful, 15 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | // GNU General Public License for more details. 18 | // 19 | // You should have received a copy of the GNU General Public License 20 | // along with TmpDisk. If not, see . 21 | 22 | import Foundation 23 | import AppKit 24 | 25 | 26 | 27 | class TmpDiskMenuItem: NSMenuItem { 28 | let clickHandler: () -> Void 29 | let recreateHandler: () -> Void 30 | let ejectHandler: () -> Void 31 | 32 | required init(title string: String, action selector: Selector?, keyEquivalent charCode: String, clickHandler: @escaping () -> Void, recreateHandler: @escaping () -> Void, ejectHandler: @escaping () -> Void) { 33 | self.clickHandler = clickHandler 34 | self.recreateHandler = recreateHandler 35 | self.ejectHandler = ejectHandler 36 | 37 | super.init(title: string, action: selector, keyEquivalent: charCode) 38 | 39 | let view = NSView.init(frame: NSRect(x: 0, y: 0, width: 200, height: 25)) 40 | 41 | let label = NSButton(frame: NSRect(x: 20, y: 2.5, width: 140, height: 20)) 42 | label.action = #selector(onClick(sender:)) 43 | label.target = self 44 | label.title = title 45 | label.isBordered = false 46 | label.alignment = .left 47 | view.addSubview(label) 48 | 49 | let recreate = NSButton(frame: NSRect(x: 160, y: 5, width: 15, height: 15)) 50 | recreate.action = #selector(onRecreate(sender:)) 51 | recreate.target = self 52 | recreate.image = NSImage(named: "recreate_a") 53 | recreate.alternateImage = NSImage(named: "recreate") 54 | recreate.imagePosition = .imageOnly 55 | recreate.isBordered = false 56 | view.addSubview(recreate) 57 | 58 | let eject = NSButton(frame: NSRect(x: 180, y: 5, width: 15, height: 15)) 59 | eject.action = #selector(onEject(sender:)) 60 | eject.target = self 61 | eject.image = NSImage(named: "eject") 62 | eject.alternateImage = NSImage(named: "eject_a") 63 | eject.imagePosition = .imageOnly 64 | eject.isBordered = false 65 | view.addSubview(eject) 66 | 67 | self.view = view 68 | } 69 | 70 | required init(coder: NSCoder) { 71 | fatalError("init(coder:) has not been implemented") 72 | } 73 | 74 | // MARK: - Actions 75 | 76 | @objc func onClick(sender: NSButton) { 77 | self.clickHandler() 78 | } 79 | 80 | @objc func onRecreate(sender: NSButton) { 81 | self.recreateHandler() 82 | } 83 | 84 | @objc func onEject(sender: NSButton) { 85 | self.ejectHandler() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /TmpDisk/WindowManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowManager.swift 3 | // TmpDisk 4 | // 5 | // Created by Tim on 2/12/22. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class WindowManager: NSObject, NSWindowDelegate { 12 | private var newTmpDiskWindow: NSWindowController? 13 | private var autoCreateManagerWindow: NSWindowController? 14 | private var preferencesWindow: NSWindowController? 15 | private var aboutWindow: NSWindowController? 16 | 17 | func windowWillClose(_ notification: Notification) { 18 | if let window = notification.object as? NSWindow { 19 | switch window.windowController { 20 | case newTmpDiskWindow: 21 | newTmpDiskWindow = nil 22 | break 23 | case autoCreateManagerWindow: 24 | autoCreateManagerWindow = nil 25 | break 26 | case preferencesWindow: 27 | preferencesWindow = nil 28 | break 29 | default: 30 | return 31 | } 32 | } 33 | } 34 | 35 | func showNewTmpDiskWindow() { 36 | NSApplication.shared.activate(ignoringOtherApps: true) 37 | 38 | if newTmpDiskWindow == nil { 39 | newTmpDiskWindow = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: "NewTmpDiskWindow") as? NSWindowController 40 | newTmpDiskWindow?.window?.delegate = self 41 | } 42 | 43 | newTmpDiskWindow?.showWindow(nil) 44 | newTmpDiskWindow?.window?.makeKey() 45 | } 46 | 47 | func showAutoCreateManagerWindow() { 48 | NSApplication.shared.activate(ignoringOtherApps: true) 49 | 50 | if autoCreateManagerWindow == nil { 51 | autoCreateManagerWindow = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: "AutoCreateManagerWindow") as? NSWindowController 52 | autoCreateManagerWindow?.window?.delegate = self 53 | } 54 | 55 | autoCreateManagerWindow?.showWindow(nil) 56 | autoCreateManagerWindow?.window?.makeKey() 57 | } 58 | 59 | func showPreferencesWindow() { 60 | NSApplication.shared.activate(ignoringOtherApps: true) 61 | 62 | if preferencesWindow == nil { 63 | preferencesWindow = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: "PreferencesWindow") as? NSWindowController 64 | preferencesWindow?.window?.delegate = self 65 | } 66 | 67 | preferencesWindow?.showWindow(nil) 68 | preferencesWindow?.window?.makeKey() 69 | } 70 | 71 | func showAboutWindow() { 72 | NSApplication.shared.activate(ignoringOtherApps: true) 73 | 74 | if aboutWindow == nil { 75 | aboutWindow = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: "AboutWindow") as? NSWindowController 76 | aboutWindow?.window?.delegate = self 77 | } 78 | 79 | aboutWindow?.showWindow(nil) 80 | aboutWindow?.window?.makeKey() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /TmpDisk/XPCClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XPCClient.swift 3 | // TmpDisk 4 | // 5 | // Created by Tim on 2/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | class XPCClient { 11 | 12 | var connection: NSXPCConnection? 13 | 14 | func initConnection() { 15 | if connection == nil { 16 | connection = NSXPCConnection(machServiceName: Constant.helperMachLabel, 17 | options: .privileged) 18 | 19 | connection?.remoteObjectInterface = NSXPCInterface(with: TmpDiskCreator.self) 20 | 21 | connection?.invalidationHandler = connectionInvalidationHandler 22 | connection?.interruptionHandler = connectionInterruptionHandler 23 | } 24 | } 25 | 26 | func createVolume(_ command: String, onCreate: @escaping (TmpDiskError?) -> Void) { 27 | 28 | initConnection() 29 | connection?.resume() 30 | 31 | let creator = connection?.remoteObjectProxyWithErrorHandler({ error in 32 | let e = error as NSError 33 | if e.code == 4099 { 34 | onCreate(.helperFailed) 35 | } else if e.code == 4097 { 36 | onCreate(.helperInvalidated) 37 | } else { 38 | onCreate(.helperFailed) 39 | } 40 | }) as? TmpDiskCreator 41 | creator?.createTmpDisk(command) { created in 42 | if created { 43 | onCreate(nil) 44 | } else { 45 | onCreate(.failed) 46 | } 47 | } 48 | } 49 | 50 | func uninstall() { 51 | initConnection() 52 | connection?.resume() 53 | 54 | let creator = connection?.remoteObjectProxy as? TmpDiskCreator 55 | creator?.uninstall() 56 | } 57 | 58 | private func connectionInterruptionHandler() { 59 | NSLog("[XPCTEST] \(type(of: self)): connection has been interrupted XPCTEST") 60 | connection = nil 61 | } 62 | 63 | private func connectionInvalidationHandler() { 64 | NSLog("[XPCTEST] \(type(of: self)): connection has been invalidated XPCTEST") 65 | connection = nil 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /TmpDisk/dsa_pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIDOjCCAi0GByqGSM44BAEwggIgAoIBAQCNR3gp//PcQD2jmZpJ9FhN9vKhtj15 3 | Bzd3I2rymHcV2IbXmwb/0rf56hrrM92y6qyWRJcvaCitZFd4hFDm+kjdXBbIL2IK 4 | FPqai5vwJ8k0XEVFix7BHOSVo0pIfRlmWJjh3mYtEhsJ7wIeyyd2mL4tdfDOwy/X 5 | q2in4eKgnkk0yo1+fGzJbcnnnMTmpVDPDjpKFNKvbTIqJivVJ9cSuGSvd5Ccs2xL 6 | dnTiKLIv1J4AY4VdggKE0G3nUL3IVHgKiHU6EStE7WXjC4bp0Ho75hI0HXerzgvf 7 | y2eY7iiLd7zuGzR8/Twrkw1pWwPQfmjiHfolgbhWMPCMa5klZK1n7yOHAhUA7/ew 8 | zy567pYZJH28wlljkxOTHL8CggEABm5CdMC5PEUb4zwMVZcIYqdbZV5OigyzK9n0 9 | T5awKQyUtm/hzmtXVVbG3mqApEtfZxMsX+hL9vqp015X/8TPNvythePScH0RFXW2 10 | 9sCOPBBDgyLarYl5tAT/V9KrNcmMOdcPa36sjf4zSAoMiZYqU0Qya90GRwXx88ji 11 | ym6lhBFKrimHOIuYo4gFzTtSVIyEztMwuzW3wmvLxkP0r9hDqMkmpdQs2n/GpsLj 12 | 4UuqFk16uJkscM4jUc4rZ0lj4RpXKpBW9jv2GHa9Aon25rrnncfJx8JGZrUAoJh9 13 | ygMnwWfB9lKWjOH3jAPyzWiSoqENejusV4uUUZnWx821NcpdSQOCAQUAAoIBADrv 14 | IICw/wVhXb3fdgo7qCG03rvaEQl/SlIqJvo/H499ZpS1I/EkmkKbzERQ5nHCqKJp 15 | c1SycgW4Cn3zykUhf418qryYlQkvCC3aMLHumn9l4fSY6qdmB/CvyAQiieKhXadU 16 | eK5Cpqj0y7klIyO1Xq1Rr1mTHXPSqAZImiu/KCapqebpyGXqA7NOSDQYYLe43O92 17 | Kot7uifo4p7rqErdrLyPcVEGLWg4m8QPp6fLIYQa1S39PMiFiGFBFVH+bbRsjkwH 18 | XVgdATNHQnQmqDe6va6Rdl6GeMZQImuvFmV7D9zDUlVQ8jQX4fgM7wU185bqmGA3 19 | ke5PNT16bZMMNl+JOUE= 20 | -----END PUBLIC KEY----- 21 | -------------------------------------------------------------------------------- /TmpDisk/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | TmpDisk 4 | 5 | Created by Tim on 12/11/21. 6 | 7 | */ 8 | 9 | "New TmpDisk" = "New TmpDisk"; 10 | "Current TmpDisks" = "Current TmpDisks"; 11 | "Recreate All" = "Recreate All"; 12 | "AutoCreate Manager" = "AutoCreate Manager"; 13 | "Always Start on Login" = "Always Start on Login"; 14 | "Check for Updates" = "Check for Updates"; 15 | "About TmpDisk" = "About TmpDisk"; 16 | "Quit" = "Quit"; 17 | "Your TmpDisk must have a name" = "Your TmpDisk must have a name"; 18 | "A volume with this name already exists" = "A volume with this name already exists"; 19 | "Size must be a number of megabytes > 0" = "Size must be a number of megabytes > 0"; 20 | "Size must be a number of gigabytes >= 0.01" = "Size must be a number of gigabytes >= 0.01"; 21 | "Failed to create TmpDisk" = "Failed to create TmpDisk"; 22 | "Preferences" = "Preferences"; 23 | "You can't change the root volume while tmpFS disks are mounted." = "You can't change the root volume while tmpFS disks are mounted."; 24 | "Icon must be square" = "Icon must be square"; 25 | "Could not convert image to icon" = "Could not convert image to icon"; 26 | "Failed to eject \"%@\"" = "Failed to eject \"%@\""; 27 | "The volume \"%@\" wasn't ejected because one or more programs may be using it" = "The volume \"%@\" wasn't ejected because one or more programs may be using it"; 28 | "To eject the disk immediately, hit the Force Eject button" = "To eject the disk immediately, hit the Force Eject button"; 29 | "Force Eject" = "Force Eject"; 30 | -------------------------------------------------------------------------------- /TmpDisk/es.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | TmpDisk 4 | 5 | Created by Tim on 12/11/21. 6 | 7 | */ 8 | 9 | "New TmpDisk" = "New TmpDisk"; 10 | "Current TmpDisks" = "Current TmpDisks"; 11 | "Recreate All" = "Recreate All"; 12 | "AutoCreate Manager" = "AutoCreate Manager"; 13 | "Always Start on Login" = "Always Start on Login"; 14 | "Check for Updates" = "Check for Updates"; 15 | "About TmpDisk" = "About TmpDisk"; 16 | "Quit" = "Quit"; 17 | "Your TmpDisk must have a name" = "Your TmpDisk must have a name"; 18 | "A volume with this name already exists" = "A volume with this name already exists"; 19 | "Size must be a number of megabytes > 0" = "Size must be a number of megabytes > 0"; 20 | "Size must be a number of gigabytes >= 0.01" = "Size must be a number of gigabytes >= 0.01"; 21 | "Failed to create TmpDisk" = "Failed to create TmpDisk"; 22 | "Preferences" = "Preferences"; 23 | "You can't change the root volume while tmpFS disks are mounted." = "You can't change the root volume while tmpFS disks are mounted."; 24 | "Icon must be square" = "Icon must be square"; 25 | "Could not convert image to icon" = "Could not convert image to icon"; 26 | "Failed to eject \"%@\"" = "Failed to eject \"%@\""; 27 | "The volume \"%@\" wasn't ejected because one or more programs may be using it" = "The volume \"%@\" wasn't ejected because one or more programs may be using it"; 28 | "To eject the disk immediately, hit the Force Eject button" = "To eject the disk immediately, hit the Force Eject button"; 29 | "Force Eject" = "Force Eject"; 30 | -------------------------------------------------------------------------------- /TmpDisk/es.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; */ 3 | "1UK-8n-QPP.title" = "Customize Toolbar…"; 4 | 5 | /* Class = "NSMenuItem"; title = "TmpDisk"; ObjectID = "1Xt-HY-uBw"; */ 6 | "1Xt-HY-uBw.title" = "TmpDisk"; 7 | 8 | /* Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; */ 9 | "1b7-l0-nxx.title" = "Find"; 10 | 11 | /* Class = "NSButtonCell"; title = "Spotlight Index Volume"; ObjectID = "1jo-cE-WNd"; */ 12 | "1jo-cE-WNd.title" = "Spotlight Index Volume"; 13 | 14 | /* Class = "NSMenuItem"; title = "Lower"; ObjectID = "1tx-W0-xDw"; */ 15 | "1tx-W0-xDw.title" = "Lower"; 16 | 17 | /* Class = "NSMenuItem"; title = "Raise"; ObjectID = "2h7-ER-AoG"; */ 18 | "2h7-ER-AoG.title" = "Raise"; 19 | 20 | /* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ 21 | "2oI-Rn-ZJC.title" = "Transformations"; 22 | 23 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "2yo-HM-UnI"; */ 24 | "2yo-HM-UnI.title" = "Text Cell"; 25 | 26 | /* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ 27 | "3IN-sU-3Bg.title" = "Spelling"; 28 | 29 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "3Om-Ey-2VK"; */ 30 | "3Om-Ey-2VK.title" = "Use Default"; 31 | 32 | /* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ 33 | "3rS-ZA-NoH.title" = "Speech"; 34 | 35 | /* Class = "NSMenuItem"; title = "Tighten"; ObjectID = "46P-cB-AYj"; */ 36 | "46P-cB-AYj.title" = "Tighten"; 37 | 38 | /* Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; */ 39 | "4EN-yA-p0u.title" = "Find"; 40 | 41 | /* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ 42 | "4J7-dP-txa.title" = "Enter Full Screen"; 43 | 44 | /* Class = "NSTextFieldCell"; title = "Disk Name"; ObjectID = "4jY-m0-8Zh"; */ 45 | "4jY-m0-8Zh.title" = "Disk Name"; 46 | 47 | /* Class = "NSMenuItem"; title = "Quit TmpDisk"; ObjectID = "4sb-4s-VLi"; */ 48 | "4sb-4s-VLi.title" = "Quit TmpDisk"; 49 | 50 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "5D4-lB-5pZ"; */ 51 | "5D4-lB-5pZ.title" = "Text Cell"; 52 | 53 | /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ 54 | "5QF-Oa-p0T.title" = "Edit"; 55 | 56 | /* Class = "NSMenuItem"; title = "Copy Style"; ObjectID = "5Vv-lz-BsD"; */ 57 | "5Vv-lz-BsD.title" = "Copy Style"; 58 | 59 | /* Class = "NSTextFieldCell"; placeholderString = "You must enter a Disk Name"; ObjectID = "5ee-ag-nb5"; */ 60 | "5ee-ag-nb5.placeholderString" = "You must enter a Disk Name"; 61 | 62 | /* Class = "NSMenuItem"; title = "About TmpDisk"; ObjectID = "5kV-Vb-QxS"; */ 63 | "5kV-Vb-QxS.title" = "About TmpDisk"; 64 | 65 | /* Class = "NSTableColumn"; headerCell.title = "TmpFS"; ObjectID = "60P-FP-R7L"; */ 66 | "60P-FP-R7L.headerCell.title" = "TmpFS"; 67 | 68 | /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ 69 | "6dh-zS-Vam.title" = "Redo"; 70 | 71 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "6q7-5O-jmd"; */ 72 | "6q7-5O-jmd.title" = "Text Cell"; 73 | 74 | /* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ 75 | "78Y-hA-62v.title" = "Correct Spelling Automatically"; 76 | 77 | /* Class = "NSTextFieldCell"; title = "https://github.com/imothee/tmpdisk/issues"; ObjectID = "7B3-Xk-LWU"; */ 78 | "7B3-Xk-LWU.title" = "https://github.com/imothee/tmpdisk/issues"; 79 | 80 | /* Class = "NSButtonCell"; title = "Warn on eject if TmpDisk has files"; ObjectID = "87p-Ma-bcT"; */ 81 | "87p-Ma-bcT.title" = "Warn on eject if TmpDisk has files"; 82 | 83 | /* Class = "NSTextFieldCell"; title = "https://github.com/imothee/tmpdisk"; ObjectID = "8Lb-V2-Iun"; */ 84 | "8Lb-V2-Iun.title" = "https://github.com/imothee/tmpdisk"; 85 | 86 | /* Class = "NSMenu"; title = "Writing Direction"; ObjectID = "8mr-sm-Yjd"; */ 87 | "8mr-sm-Yjd.title" = "Writing Direction"; 88 | 89 | /* Class = "NSButtonCell"; title = "Create TmpDisk"; ObjectID = "8vB-Je-FgU"; */ 90 | "8vB-Je-FgU.title" = "Create TmpDisk"; 91 | 92 | /* Class = "NSTextFieldCell"; title = "TmpDisk was created by @imothee to help easily create and manage Ram Disks. Distributed under GPL V2."; ObjectID = "92J-GX-lZp"; */ 93 | "92J-GX-lZp.title" = "TmpDisk was created by @imothee to help easily create and manage Ram Disks. Distributed under GPL V2."; 94 | 95 | /* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ 96 | "9ic-FL-obx.title" = "Substitutions"; 97 | 98 | /* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ 99 | "9yt-4B-nSM.title" = "Smart Copy/Paste"; 100 | 101 | /* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ 102 | "AYu-sK-qS6.title" = "Main Menu"; 103 | 104 | /* Class = "NSTableColumn"; headerCell.title = "Disk Size"; ObjectID = "AkQ-t6-iWy"; */ 105 | "AkQ-t6-iWy.headerCell.title" = "Disk Size"; 106 | 107 | /* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ 108 | "BOF-NM-1cW.title" = "Preferences…"; 109 | 110 | /* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "BgM-ve-c93"; */ 111 | "BgM-ve-c93.title" = "\tLeft to Right"; 112 | 113 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "Bic-Cu-iR6"; */ 114 | "Bic-Cu-iR6.title" = "Text Cell"; 115 | 116 | /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "Bw7-FT-i3A"; */ 117 | "Bw7-FT-i3A.title" = "Save As…"; 118 | 119 | /* Class = "NSTextFieldCell"; title = "Found a bug or Issue?"; ObjectID = "CRo-x0-OMs"; */ 120 | "CRo-x0-OMs.title" = "Found a bug or Issue?"; 121 | 122 | /* Class = "NSMenuItem"; title = "10% of Ram"; ObjectID = "DAh-6M-Jca"; */ 123 | "DAh-6M-Jca.title" = "10% of Ram"; 124 | 125 | /* Class = "NSMenuItem"; title = "Close"; ObjectID = "DVo-aG-piG"; */ 126 | "DVo-aG-piG.title" = "Close"; 127 | 128 | /* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ 129 | "Dv1-io-Yv7.title" = "Spelling and Grammar"; 130 | 131 | /* Class = "NSTableColumn"; headerCell.title = "Journaled"; ObjectID = "EOg-1b-WmF"; */ 132 | "EOg-1b-WmF.headerCell.title" = "Journaled"; 133 | 134 | /* Class = "NSTextFieldCell"; title = "Change where your TmpFS mount points sit. Must be changed when you have no TmpFS volumes mounted."; ObjectID = "EYH-BL-kNS"; */ 135 | "EYH-BL-kNS.title" = "Change where your TmpFS mount points sit. Must be changed when you have no TmpFS volumes mounted."; 136 | 137 | /* Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; */ 138 | "F2S-fz-NVQ.title" = "Help"; 139 | 140 | /* Class = "NSMenuItem"; title = "TmpDisk Help"; ObjectID = "FKE-Sm-Kum"; */ 141 | "FKE-Sm-Kum.title" = "TmpDisk Help"; 142 | 143 | /* Class = "NSMenuItem"; title = "Text"; ObjectID = "Fal-I4-PZk"; */ 144 | "Fal-I4-PZk.title" = "Text"; 145 | 146 | /* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ 147 | "FeM-D8-WVr.title" = "Substitutions"; 148 | 149 | /* Class = "NSMenuItem"; title = "Bold"; ObjectID = "GB9-OM-e27"; */ 150 | "GB9-OM-e27.title" = "Bold"; 151 | 152 | /* Class = "NSMenu"; title = "Format"; ObjectID = "GEO-Iw-cKr"; */ 153 | "GEO-Iw-cKr.title" = "Format"; 154 | 155 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "GUa-eO-cwY"; */ 156 | "GUa-eO-cwY.title" = "Use Default"; 157 | 158 | /* Class = "NSMenuItem"; title = "Font"; ObjectID = "Gi5-1S-RQB"; */ 159 | "Gi5-1S-RQB.title" = "Font"; 160 | 161 | /* Class = "NSMenuItem"; title = "Writing Direction"; ObjectID = "H1b-Si-o9J"; */ 162 | "H1b-Si-o9J.title" = "Writing Direction"; 163 | 164 | /* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ 165 | "H8h-7b-M4v.title" = "View"; 166 | 167 | /* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ 168 | "HFQ-gK-NFA.title" = "Text Replacement"; 169 | 170 | /* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ 171 | "HFo-cy-zxI.title" = "Show Spelling and Grammar"; 172 | 173 | /* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ 174 | "HyV-fh-RgO.title" = "View"; 175 | 176 | /* Class = "NSMenuItem"; title = "Subscript"; ObjectID = "I0S-gh-46l"; */ 177 | "I0S-gh-46l.title" = "Subscript"; 178 | 179 | /* Class = "NSMenuItem"; title = "Open…"; ObjectID = "IAo-SY-fd9"; */ 180 | "IAo-SY-fd9.title" = "Open…"; 181 | 182 | /* Class = "NSMenuItem"; title = "Justify"; ObjectID = "J5U-5w-g23"; */ 183 | "J5U-5w-g23.title" = "Justify"; 184 | 185 | /* Class = "NSMenuItem"; title = "Use None"; ObjectID = "J7y-lM-qPV"; */ 186 | "J7y-lM-qPV.title" = "Use None"; 187 | 188 | /* Class = "NSTextFieldCell"; title = "Icon"; ObjectID = "JAg-eR-WFV"; */ 189 | "JAg-eR-WFV.title" = "Icon"; 190 | 191 | /* Class = "NSMenuItem"; title = "MB"; ObjectID = "JxI-0N-oYo"; */ 192 | "JxI-0N-oYo.title" = "MB"; 193 | 194 | /* Class = "NSMenuItem"; title = "Revert to Saved"; ObjectID = "KaW-ft-85H"; */ 195 | "KaW-ft-85H.title" = "Revert to Saved"; 196 | 197 | /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ 198 | "Kd2-mp-pUS.title" = "Show All"; 199 | 200 | /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ 201 | "LE2-aR-0XJ.title" = "Bring All to Front"; 202 | 203 | /* Class = "NSMenuItem"; title = "Paste Ruler"; ObjectID = "LVM-kO-fVI"; */ 204 | "LVM-kO-fVI.title" = "Paste Ruler"; 205 | 206 | /* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "Lbh-J2-qVU"; */ 207 | "Lbh-J2-qVU.title" = "\tLeft to Right"; 208 | 209 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "Lnn-2h-S6l"; */ 210 | "Lnn-2h-S6l.title" = "Text Cell"; 211 | 212 | /* Class = "NSButtonCell"; title = "AutoCreate when TmpDisk Starts"; ObjectID = "MLQ-EB-OE8"; */ 213 | "MLQ-EB-OE8.title" = "AutoCreate when TmpDisk Starts"; 214 | 215 | /* Class = "NSMenuItem"; title = "Copy Ruler"; ObjectID = "MkV-Pr-PK5"; */ 216 | "MkV-Pr-PK5.title" = "Copy Ruler"; 217 | 218 | /* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ 219 | "NMo-om-nkz.title" = "Services"; 220 | 221 | /* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "Nop-cj-93Q"; */ 222 | "Nop-cj-93Q.title" = "\tDefault"; 223 | 224 | /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ 225 | "OY7-WF-poV.title" = "Minimize"; 226 | 227 | /* Class = "NSMenuItem"; title = "Baseline"; ObjectID = "OaQ-X3-Vso"; */ 228 | "OaQ-X3-Vso.title" = "Baseline"; 229 | 230 | /* Class = "NSTableColumn"; headerCell.title = "Folders"; ObjectID = "Oae-Gb-smK"; */ 231 | "Oae-Gb-smK.headerCell.title" = "Folders"; 232 | 233 | /* Class = "NSMenuItem"; title = "Hide TmpDisk"; ObjectID = "Olw-nP-bQN"; */ 234 | "Olw-nP-bQN.title" = "Hide TmpDisk"; 235 | 236 | /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; */ 237 | "OwM-mh-QMV.title" = "Find Previous"; 238 | 239 | /* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ 240 | "Oyz-dy-DGm.title" = "Stop Speaking"; 241 | 242 | /* Class = "NSWindow"; title = "Preferences"; ObjectID = "PIG-yb-FRO"; */ 243 | "PIG-yb-FRO.title" = "Preferences"; 244 | 245 | /* Class = "NSMenuItem"; title = "Bigger"; ObjectID = "Ptp-SP-VEL"; */ 246 | "Ptp-SP-VEL.title" = "Bigger"; 247 | 248 | /* Class = "NSMenuItem"; title = "Show Fonts"; ObjectID = "Q5e-8K-NDq"; */ 249 | "Q5e-8K-NDq.title" = "Show Fonts"; 250 | 251 | /* Class = "NSMenuItem"; title = "GB"; ObjectID = "QDX-KN-Ki5"; */ 252 | "QDX-KN-Ki5.title" = "GB"; 253 | 254 | /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ 255 | "R4o-n2-Eq4.title" = "Zoom"; 256 | 257 | /* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "RB4-Sm-HuC"; */ 258 | "RB4-Sm-HuC.title" = "\tRight to Left"; 259 | 260 | /* Class = "NSMenuItem"; title = "Superscript"; ObjectID = "Rqc-34-cIF"; */ 261 | "Rqc-34-cIF.title" = "Superscript"; 262 | 263 | /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ 264 | "Ruw-6m-B2m.title" = "Select All"; 265 | 266 | /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; */ 267 | "S0p-oC-mLd.title" = "Jump to Selection"; 268 | 269 | /* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ 270 | "Td7-aD-5lo.title" = "Window"; 271 | 272 | /* Class = "NSWindow"; title = "AutoCreate Manager"; ObjectID = "TsY-Lt-YWL"; */ 273 | "TsY-Lt-YWL.title" = "AutoCreate Manager"; 274 | 275 | /* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ 276 | "UEZ-Bs-lqG.title" = "Capitalize"; 277 | 278 | /* Class = "NSTableColumn"; headerCell.title = "Case Sensitive"; ObjectID = "UPy-wU-HZo"; */ 279 | "UPy-wU-HZo.headerCell.title" = "Case Sensitive"; 280 | 281 | /* Class = "NSMenuItem"; title = "Center"; ObjectID = "VIY-Ag-zcb"; */ 282 | "VIY-Ag-zcb.title" = "Center"; 283 | 284 | /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ 285 | "Vdr-fp-XzO.title" = "Hide Others"; 286 | 287 | /* Class = "NSMenuItem"; title = "Italic"; ObjectID = "Vjx-xi-njq"; */ 288 | "Vjx-xi-njq.title" = "Italic"; 289 | 290 | /* Class = "NSButtonCell"; title = "x"; ObjectID = "VlQ-Mr-VAu"; */ 291 | "VlQ-Mr-VAu.title" = "x"; 292 | 293 | /* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ 294 | "W48-6f-4Dl.title" = "Edit"; 295 | 296 | /* Class = "NSMenuItem"; title = "Underline"; ObjectID = "WRG-CD-K1S"; */ 297 | "WRG-CD-K1S.title" = "Underline"; 298 | 299 | /* Class = "NSMenuItem"; title = "New"; ObjectID = "Was-JA-tGl"; */ 300 | "Was-JA-tGl.title" = "New"; 301 | 302 | /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; */ 303 | "WeT-3V-zwk.title" = "Paste and Match Style"; 304 | 305 | /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "XWF-fr-bC7"; */ 306 | "XWF-fr-bC7.title" = "Table View Cell"; 307 | 308 | /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; */ 309 | "Xz5-n4-O0W.title" = "Find…"; 310 | 311 | /* Class = "NSTextFieldCell"; title = "Disk Size"; ObjectID = "YCV-8p-NZK"; */ 312 | "YCV-8p-NZK.title" = "Disk Size"; 313 | 314 | /* Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; */ 315 | "YEy-JH-Tfz.title" = "Find and Replace…"; 316 | 317 | /* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "YGs-j5-SAR"; */ 318 | "YGs-j5-SAR.title" = "\tDefault"; 319 | 320 | /* Class = "NSTextFieldCell"; title = "Get the source code"; ObjectID = "YQl-gW-wMR"; */ 321 | "YQl-gW-wMR.title" = "Get the source code"; 322 | 323 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "YRf-Nm-YtA"; */ 324 | "YRf-Nm-YtA.title" = "Text Cell"; 325 | 326 | /* Class = "NSTextFieldCell"; title = "3rd Party Libraries"; ObjectID = "Yhh-3o-HGQ"; */ 327 | "Yhh-3o-HGQ.title" = "3rd Party Libraries"; 328 | 329 | /* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ 330 | "Ynk-f8-cLZ.title" = "Start Speaking"; 331 | 332 | /* Class = "NSMenuItem"; title = "Align Left"; ObjectID = "ZM1-6Q-yy1"; */ 333 | "ZM1-6Q-yy1.title" = "Align Left"; 334 | 335 | /* Class = "NSMenuItem"; title = "Paragraph"; ObjectID = "ZvO-Gk-QUH"; */ 336 | "ZvO-Gk-QUH.title" = "Paragraph"; 337 | 338 | /* Class = "NSButtonCell"; title = "Update Preferences"; ObjectID = "aFz-gK-uoy"; */ 339 | "aFz-gK-uoy.title" = "Update Preferences"; 340 | 341 | /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "aTl-1u-JFS"; */ 342 | "aTl-1u-JFS.title" = "Print…"; 343 | 344 | /* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ 345 | "aUF-d1-5bR.title" = "Window"; 346 | 347 | /* Class = "NSMenu"; title = "Font"; ObjectID = "aXa-aM-Jaq"; */ 348 | "aXa-aM-Jaq.title" = "Font"; 349 | 350 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "agt-UL-0e3"; */ 351 | "agt-UL-0e3.title" = "Use Default"; 352 | 353 | /* Class = "NSMenuItem"; title = "Show Colors"; ObjectID = "bgn-CT-cEk"; */ 354 | "bgn-CT-cEk.title" = "Show Colors"; 355 | 356 | /* Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu"; */ 357 | "bib-Uj-vzu.title" = "File"; 358 | 359 | /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; */ 360 | "buJ-ug-pKt.title" = "Use Selection for Find"; 361 | 362 | /* Class = "NSTableColumn"; headerCell.title = "Name"; ObjectID = "bvb-Pe-WVS"; */ 363 | "bvb-Pe-WVS.headerCell.title" = "Name"; 364 | 365 | /* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ 366 | "c8a-y6-VQd.title" = "Transformations"; 367 | 368 | /* Class = "NSMenuItem"; title = "Use None"; ObjectID = "cDB-IK-hbR"; */ 369 | "cDB-IK-hbR.title" = "Use None"; 370 | 371 | /* Class = "NSButtonCell"; title = "Hidden Volume"; ObjectID = "cOC-Ky-V05"; */ 372 | "cOC-Ky-V05.title" = "Hidden Volume"; 373 | 374 | /* Class = "NSMenuItem"; title = "Selection"; ObjectID = "cqv-fj-IhA"; */ 375 | "cqv-fj-IhA.title" = "Selection"; 376 | 377 | /* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ 378 | "cwL-P1-jid.title" = "Smart Links"; 379 | 380 | /* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ 381 | "d9M-CD-aMd.title" = "Make Lower Case"; 382 | 383 | /* Class = "NSMenu"; title = "Text"; ObjectID = "d9c-me-L2H"; */ 384 | "d9c-me-L2H.title" = "Text"; 385 | 386 | /* Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ"; */ 387 | "dMs-cI-mzQ.title" = "File"; 388 | 389 | /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ 390 | "dRJ-4n-Yzg.title" = "Undo"; 391 | 392 | /* Class = "NSTextFieldCell"; placeholderString = "~/.tmpdisk/volumes"; ObjectID = "fMk-fB-fMf"; */ 393 | "fMk-fB-fMf.placeholderString" = "~/.tmpdisk/volumes"; 394 | 395 | /* Class = "NSTableColumn"; headerCell.title = "Hidden"; ObjectID = "fP5-p8-hYR"; */ 396 | "fP5-p8-hYR.headerCell.title" = "Hidden"; 397 | 398 | /* Class = "NSButtonCell"; title = "Case Sensitive"; ObjectID = "gCa-HK-wJZ"; */ 399 | "gCa-HK-wJZ.title" = "Case Sensitive"; 400 | 401 | /* Class = "NSButtonCell"; title = "Journaled"; ObjectID = "gFb-jm-Dxc"; */ 402 | "gFb-jm-Dxc.title" = "Journaled"; 403 | 404 | /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ 405 | "gVA-U4-sdL.title" = "Paste"; 406 | 407 | /* Class = "NSMenuItem"; title = "Custom"; ObjectID = "gcJ-GN-9gz"; */ 408 | "gcJ-GN-9gz.title" = "Custom"; 409 | 410 | /* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ 411 | "hQb-2v-fYv.title" = "Smart Quotes"; 412 | 413 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "hlg-9B-vKO"; */ 414 | "hlg-9B-vKO.title" = "Text Cell"; 415 | 416 | /* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ 417 | "hz2-CU-CR7.title" = "Check Document Now"; 418 | 419 | /* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ 420 | "hz9-B4-Xy5.title" = "Services"; 421 | 422 | /* Class = "NSMenuItem"; title = "Smaller"; ObjectID = "i1d-Er-qST"; */ 423 | "i1d-Er-qST.title" = "Smaller"; 424 | 425 | /* Class = "NSMenu"; title = "Baseline"; ObjectID = "ijk-EB-dga"; */ 426 | "ijk-EB-dga.title" = "Baseline"; 427 | 428 | /* Class = "NSTextFieldCell"; placeholderString = "Comma separated folder names"; ObjectID = "ini-oz-Wpw"; */ 429 | "ini-oz-Wpw.placeholderString" = "Comma separated folder names"; 430 | 431 | /* Class = "NSMenuItem"; title = "Kern"; ObjectID = "jBQ-r6-VK2"; */ 432 | "jBQ-r6-VK2.title" = "Kern"; 433 | 434 | /* Class = "NSTextFieldCell"; title = "Sparkle Framework"; ObjectID = "jFH-29-tnu"; */ 435 | "jFH-29-tnu.title" = "Sparkle Framework"; 436 | 437 | /* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "jFq-tB-4Kx"; */ 438 | "jFq-tB-4Kx.title" = "\tRight to Left"; 439 | 440 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "jWq-m5-D4C"; */ 441 | "jWq-m5-D4C.title" = "Text Cell"; 442 | 443 | /* Class = "NSWindow"; title = "Create New TmpDisk"; ObjectID = "jhY-SC-DO1"; */ 444 | "jhY-SC-DO1.title" = "Create New TmpDisk"; 445 | 446 | /* Class = "NSMenuItem"; title = "Format"; ObjectID = "jxT-CU-nIS"; */ 447 | "jxT-CU-nIS.title" = "Format"; 448 | 449 | /* Class = "NSMenuItem"; title = "Show Sidebar"; ObjectID = "kIP-vf-haE"; */ 450 | "kIP-vf-haE.title" = "Show Sidebar"; 451 | 452 | /* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ 453 | "mK6-2p-4JG.title" = "Check Grammar With Spelling"; 454 | 455 | /* Class = "NSTableColumn"; headerCell.title = "Warn"; ObjectID = "moE-oU-Xjj"; */ 456 | "moE-oU-Xjj.headerCell.title" = "Warn"; 457 | 458 | /* Class = "NSMenuItem"; title = "50% of Ram"; ObjectID = "nXv-G3-d8b"; */ 459 | "nXv-G3-d8b.title" = "50% of Ram"; 460 | 461 | /* Class = "NSTextFieldCell"; placeholderString = "0"; ObjectID = "nYC-7d-c8X"; */ 462 | "nYC-7d-c8X.placeholderString" = "0"; 463 | 464 | /* Class = "NSTextFieldCell"; title = "16"; ObjectID = "nYC-7d-c8X"; */ 465 | "nYC-7d-c8X.title" = "16"; 466 | 467 | /* Class = "NSMenuItem"; title = "Ligatures"; ObjectID = "o6e-r0-MWq"; */ 468 | "o6e-r0-MWq.title" = "Ligatures"; 469 | 470 | /* Class = "NSMenuItem"; title = "75% of Ram"; ObjectID = "oUd-0o-jwy"; */ 471 | "oUd-0o-jwy.title" = "75% of Ram"; 472 | 473 | /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "oas-Oc-fiZ"; */ 474 | "oas-Oc-fiZ.title" = "Open Recent"; 475 | 476 | /* Class = "NSMenuItem"; title = "Loosen"; ObjectID = "ogc-rX-tC1"; */ 477 | "ogc-rX-tC1.title" = "Loosen"; 478 | 479 | /* Class = "NSTableColumn"; headerCell.title = "Indexed"; ObjectID = "oqx-VA-7Zr"; */ 480 | "oqx-VA-7Zr.headerCell.title" = "Indexed"; 481 | 482 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "pL4-3x-WFv"; */ 483 | "pL4-3x-WFv.title" = "Text Cell"; 484 | 485 | /* Class = "NSTextFieldCell"; title = "Warning: When a TmpDisk is ejected or the computer restarted all files on it will be permanently deleted. Please only store temporary files on a TmpDisk."; ObjectID = "pSH-sJ-D0k"; */ 486 | "pSH-sJ-D0k.title" = "Warning: When a TmpDisk is ejected or the computer restarted all files on it will be permanently deleted. Please only store temporary files on a TmpDisk."; 487 | 488 | /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ 489 | "pa3-QI-u2k.title" = "Delete"; 490 | 491 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "pm4-Q2-gSs"; */ 492 | "pm4-Q2-gSs.title" = "Text Cell"; 493 | 494 | /* Class = "NSMenuItem"; title = "Save…"; ObjectID = "pxx-59-PXV"; */ 495 | "pxx-59-PXV.title" = "Save…"; 496 | 497 | /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; */ 498 | "q09-fT-Sye.title" = "Find Next"; 499 | 500 | /* Class = "NSTextFieldCell"; title = "TmpFS Root Folder"; ObjectID = "q71-xV-gGa"; */ 501 | "q71-xV-gGa.title" = "TmpFS Root Folder"; 502 | 503 | /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "qIS-W8-SiK"; */ 504 | "qIS-W8-SiK.title" = "Page Setup…"; 505 | 506 | /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "qXn-wF-aJV"; */ 507 | "qXn-wF-aJV.title" = "Table View Cell"; 508 | 509 | /* Class = "NSWindow"; title = "About"; ObjectID = "qkc-td-g2b"; */ 510 | "qkc-td-g2b.title" = "About"; 511 | 512 | /* Class = "NSTextFieldCell"; title = "Folders"; ObjectID = "qw8-z7-vQx"; */ 513 | "qw8-z7-vQx.title" = "Folders"; 514 | 515 | /* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ 516 | "rbD-Rh-wIN.title" = "Check Spelling While Typing"; 517 | 518 | /* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ 519 | "rgM-f4-ycn.title" = "Smart Dashes"; 520 | 521 | /* Class = "NSMenuItem"; title = "Show Toolbar"; ObjectID = "snW-S8-Cw5"; */ 522 | "snW-S8-Cw5.title" = "Show Toolbar"; 523 | 524 | /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "t3z-IH-wJR"; */ 525 | "t3z-IH-wJR.title" = "Table View Cell"; 526 | 527 | /* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ 528 | "tRr-pd-1PS.title" = "Data Detectors"; 529 | 530 | /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "tXI-mr-wws"; */ 531 | "tXI-mr-wws.title" = "Open Recent"; 532 | 533 | /* Class = "NSMenu"; title = "Kern"; ObjectID = "tlD-Oa-oAM"; */ 534 | "tlD-Oa-oAM.title" = "Kern"; 535 | 536 | /* Class = "NSTextFieldCell"; title = "These volumes will be created automatically whenever TmpDisk starts"; ObjectID = "toS-Ay-uEq"; */ 537 | "toS-Ay-uEq.title" = "These volumes will be created automatically whenever TmpDisk starts"; 538 | 539 | /* Class = "NSMenu"; title = "TmpDisk"; ObjectID = "uQy-DD-JDr"; */ 540 | "uQy-DD-JDr.title" = "TmpDisk"; 541 | 542 | /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ 543 | "uRl-iY-unG.title" = "Cut"; 544 | 545 | /* Class = "NSMenuItem"; title = "25% of Ram"; ObjectID = "ud8-KO-JVf"; */ 546 | "ud8-KO-JVf.title" = "25% of Ram"; 547 | 548 | /* Class = "NSMenuItem"; title = "Paste Style"; ObjectID = "vKC-jM-MkH"; */ 549 | "vKC-jM-MkH.title" = "Paste Style"; 550 | 551 | /* Class = "NSMenuItem"; title = "Show Ruler"; ObjectID = "vLm-3I-IUL"; */ 552 | "vLm-3I-IUL.title" = "Show Ruler"; 553 | 554 | /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "vNY-rz-j42"; */ 555 | "vNY-rz-j42.title" = "Clear Menu"; 556 | 557 | /* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ 558 | "vmV-6d-7jI.title" = "Make Upper Case"; 559 | 560 | /* Class = "NSMenu"; title = "Ligatures"; ObjectID = "w0m-vy-SC9"; */ 561 | "w0m-vy-SC9.title" = "Ligatures"; 562 | 563 | /* Class = "NSMenuItem"; title = "Align Right"; ObjectID = "wb2-vD-lq4"; */ 564 | "wb2-vD-lq4.title" = "Align Right"; 565 | 566 | /* Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd"; */ 567 | "wpr-3q-Mcd.title" = "Help"; 568 | 569 | /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ 570 | "x3v-GG-iWU.title" = "Copy"; 571 | 572 | /* Class = "NSButtonCell"; title = "Use TmpFS (Needs Root Access)"; ObjectID = "xBA-Ze-bf2"; */ 573 | "xBA-Ze-bf2.title" = "Use TmpFS (Needs Root Access)"; 574 | 575 | /* Class = "NSMenuItem"; title = "Use All"; ObjectID = "xQD-1f-W4t"; */ 576 | "xQD-1f-W4t.title" = "Use All"; 577 | 578 | /* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ 579 | "xrE-MZ-jX0.title" = "Speech"; 580 | 581 | /* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ 582 | "z6F-FW-3nz.title" = "Show Substitutions"; 583 | -------------------------------------------------------------------------------- /TmpDisk/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imothee/tmpdisk/c5db4f3f8567beaa5eac4a477c86a5591658ebf0/TmpDisk/icon.icns -------------------------------------------------------------------------------- /TmpDisk/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | TmpDisk 4 | 5 | Created by Tim on 12/11/21. 6 | 7 | */ 8 | 9 | "New TmpDisk" = "创建 TmpDisk"; 10 | "Current TmpDisks" = "当前 TmpDisk"; 11 | "Recreate All" = "全部重新创建"; 12 | "AutoCreate Manager" = "自动创建管理器"; 13 | "Always Start on Login" = "登录时自动启动"; 14 | "Check for Updates" = "检查更新"; 15 | "About TmpDisk" = "关于 TmpDisk"; 16 | "Quit" = "退出"; 17 | "Your TmpDisk must have a name" = "你的 TmpDisk 必须有一个名字"; 18 | "A volume with this name already exists" = "已存在同名的卷"; 19 | "Size must be a number of megabytes > 0" = "大小必须是大于 0 的兆字节数"; 20 | "Size must be a number of gigabytes >= 0.01" = "大小必须是大于或等于 0.01 的吉字节数"; 21 | "Failed to create TmpDisk" = "创建 TmpDisk 失败"; 22 | "Preferences" = "偏好设置"; 23 | "You can't change the root volume while tmpFS disks are mounted." = "你不能在挂载 tmpFS 磁盘时更改根卷的位置。"; 24 | "Icon must be square" = "图标必须是正方形"; 25 | "Could not convert image to icon" = "无法将图片转换为图标"; 26 | "Failed to eject \"%@\"" = "推出 \"%@\" 失败"; 27 | "The volume \"%@\" wasn't ejected because one or more programs may be using it" = "无法推出卷 \"%@\" , 因为有一个或多个程序正在使用"; 28 | "To eject the disk immediately, hit the Force Eject button" = "选择\"强制推出\"按钮以强制推出"; 29 | "Force Eject" = "强制推出"; 30 | "Do you wish to install TmpDiskHelper? It will require your admin password." = "您想安装 TmpDiskHelper 吗?需要管理员密码确认。"; 31 | "There is an updated TmpDiskHelper, do you wish to install it? Requires admin password." = "有一个新版的 TmpDiskHelper,您想安装吗?需要管理员密码确认。"; 32 | -------------------------------------------------------------------------------- /TmpDisk/zh-Hans.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; */ 3 | "1UK-8n-QPP.title" = "自定义工具栏…"; 4 | 5 | /* Class = "NSMenuItem"; title = "TmpDisk"; ObjectID = "1Xt-HY-uBw"; */ 6 | "1Xt-HY-uBw.title" = "TmpDisk"; 7 | 8 | /* Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; */ 9 | "1b7-l0-nxx.title" = "查找"; 10 | 11 | /* Class = "NSButtonCell"; title = "Spotlight Index Volume"; ObjectID = "1jo-cE-WNd"; */ 12 | "1jo-cE-WNd.title" = "索引卷"; 13 | 14 | /* Class = "NSMenuItem"; title = "Lower"; ObjectID = "1tx-W0-xDw"; */ 15 | "1tx-W0-xDw.title" = "Lower"; 16 | 17 | /* Class = "NSMenuItem"; title = "Raise"; ObjectID = "2h7-ER-AoG"; */ 18 | "2h7-ER-AoG.title" = "Raise"; 19 | 20 | /* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ 21 | "2oI-Rn-ZJC.title" = "Transformations"; 22 | 23 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "2yo-HM-UnI"; */ 24 | "2yo-HM-UnI.title" = "Text Cell"; 25 | 26 | /* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ 27 | "3IN-sU-3Bg.title" = "Spelling"; 28 | 29 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "3Om-Ey-2VK"; */ 30 | "3Om-Ey-2VK.title" = "Use Default"; 31 | 32 | /* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ 33 | "3rS-ZA-NoH.title" = "Speech"; 34 | 35 | /* Class = "NSMenuItem"; title = "Tighten"; ObjectID = "46P-cB-AYj"; */ 36 | "46P-cB-AYj.title" = "Tighten"; 37 | 38 | /* Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; */ 39 | "4EN-yA-p0u.title" = "查找"; 40 | 41 | /* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ 42 | "4J7-dP-txa.title" = "进入全屏幕"; 43 | 44 | /* Class = "NSTextFieldCell"; title = "Disk Name"; ObjectID = "4jY-m0-8Zh"; */ 45 | "4jY-m0-8Zh.title" = "磁盘名称"; 46 | 47 | /* Class = "NSMenuItem"; title = "Quit TmpDisk"; ObjectID = "4sb-4s-VLi"; */ 48 | "4sb-4s-VLi.title" = "退出 TmpDisk"; 49 | 50 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "5D4-lB-5pZ"; */ 51 | "5D4-lB-5pZ.title" = "Text Cell"; 52 | 53 | /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ 54 | "5QF-Oa-p0T.title" = "编辑"; 55 | 56 | /* Class = "NSMenuItem"; title = "Copy Style"; ObjectID = "5Vv-lz-BsD"; */ 57 | "5Vv-lz-BsD.title" = "Copy Style"; 58 | 59 | /* Class = "NSTextFieldCell"; placeholderString = "You must enter a Disk Name"; ObjectID = "5ee-ag-nb5"; */ 60 | "5ee-ag-nb5.placeholderString" = "请输入磁盘名称"; 61 | 62 | /* Class = "NSMenuItem"; title = "About TmpDisk"; ObjectID = "5kV-Vb-QxS"; */ 63 | "5kV-Vb-QxS.title" = "关于 TmpDisk"; 64 | 65 | /* Class = "NSTableColumn"; headerCell.title = "TmpFS"; ObjectID = "60P-FP-R7L"; */ 66 | "60P-FP-R7L.headerCell.title" = "TmpFS"; 67 | 68 | /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ 69 | "6dh-zS-Vam.title" = "重做"; 70 | 71 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "6q7-5O-jmd"; */ 72 | "6q7-5O-jmd.title" = "Text Cell"; 73 | 74 | /* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ 75 | "78Y-hA-62v.title" = "自动纠正拼写"; 76 | 77 | /* Class = "NSTextFieldCell"; title = "https://github.com/imothee/tmpdisk/issues"; ObjectID = "7B3-Xk-LWU"; */ 78 | "7B3-Xk-LWU.title" = "https://github.com/imothee/tmpdisk/issues"; 79 | 80 | /* Class = "NSButtonCell"; title = "Warn on eject if TmpDisk has files"; ObjectID = "87p-Ma-bcT"; */ 81 | "87p-Ma-bcT.title" = "Warn on eject if TmpDisk has files"; 82 | 83 | /* Class = "NSTextFieldCell"; title = "https://github.com/imothee/tmpdisk"; ObjectID = "8Lb-V2-Iun"; */ 84 | "8Lb-V2-Iun.title" = "https://github.com/imothee/tmpdisk"; 85 | 86 | /* Class = "NSMenu"; title = "Writing Direction"; ObjectID = "8mr-sm-Yjd"; */ 87 | "8mr-sm-Yjd.title" = "书写方向"; 88 | 89 | /* Class = "NSButtonCell"; title = "Create TmpDisk"; ObjectID = "8vB-Je-FgU"; */ 90 | "8vB-Je-FgU.title" = "创建 TmpDisk"; 91 | 92 | /* Class = "NSTextFieldCell"; title = "TmpDisk was created by @imothee to help easily create and manage Ram Disks. Distributed under GPL V2."; ObjectID = "92J-GX-lZp"; */ 93 | "92J-GX-lZp.title" = "TmpDisk 由@imothee 创建,用于帮助轻松创建和管理 Ram 磁盘。在 GPL V2 下分发。"; 94 | 95 | /* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ 96 | "9ic-FL-obx.title" = "Substitutions"; 97 | 98 | /* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ 99 | "9yt-4B-nSM.title" = "智能复制/粘贴"; 100 | 101 | /* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ 102 | "AYu-sK-qS6.title" = "Main Menu"; 103 | 104 | /* Class = "NSTableColumn"; headerCell.title = "Disk Size"; ObjectID = "AkQ-t6-iWy"; */ 105 | "AkQ-t6-iWy.headerCell.title" = "磁盘大小"; 106 | 107 | /* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ 108 | "BOF-NM-1cW.title" = "偏好设置…"; 109 | 110 | /* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "BgM-ve-c93"; */ 111 | "BgM-ve-c93.title" = "\t从左到右"; 112 | 113 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "Bic-Cu-iR6"; */ 114 | "Bic-Cu-iR6.title" = "Text Cell"; 115 | 116 | /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "Bw7-FT-i3A"; */ 117 | "Bw7-FT-i3A.title" = "另存为…"; 118 | 119 | /* Class = "NSTextFieldCell"; title = "Found a bug or Issue?"; ObjectID = "CRo-x0-OMs"; */ 120 | "CRo-x0-OMs.title" = "Found a bug or Issue?"; 121 | 122 | /* Class = "NSMenuItem"; title = "10% of Ram"; ObjectID = "DAh-6M-Jca"; */ 123 | "DAh-6M-Jca.title" = "10% of Ram"; 124 | 125 | /* Class = "NSMenuItem"; title = "Close"; ObjectID = "DVo-aG-piG"; */ 126 | "DVo-aG-piG.title" = "关闭"; 127 | 128 | /* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ 129 | "Dv1-io-Yv7.title" = "拼写和语法"; 130 | 131 | /* Class = "NSTableColumn"; headerCell.title = "Journaled"; ObjectID = "EOg-1b-WmF"; */ 132 | "EOg-1b-WmF.headerCell.title" = "记事本"; 133 | 134 | /* Class = "NSTextFieldCell"; title = "Change where your TmpFS mount points sit. Must be changed when you have no TmpFS volumes mounted."; ObjectID = "EYH-BL-kNS"; */ 135 | "EYH-BL-kNS.title" = "改变你的 TmpFS 挂载点的位置。必须在你没有安装 TmpFS 卷的情况下进行修改。"; 136 | 137 | /* Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; */ 138 | "F2S-fz-NVQ.title" = "帮助"; 139 | 140 | /* Class = "NSMenuItem"; title = "TmpDisk Help"; ObjectID = "FKE-Sm-Kum"; */ 141 | "FKE-Sm-Kum.title" = "TmpDisk 帮助"; 142 | 143 | /* Class = "NSMenuItem"; title = "Text"; ObjectID = "Fal-I4-PZk"; */ 144 | "Fal-I4-PZk.title" = "文本"; 145 | 146 | /* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ 147 | "FeM-D8-WVr.title" = "Substitutions"; 148 | 149 | /* Class = "NSMenuItem"; title = "Bold"; ObjectID = "GB9-OM-e27"; */ 150 | "GB9-OM-e27.title" = "Bold"; 151 | 152 | /* Class = "NSMenu"; title = "Format"; ObjectID = "GEO-Iw-cKr"; */ 153 | "GEO-Iw-cKr.title" = "格式"; 154 | 155 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "GUa-eO-cwY"; */ 156 | "GUa-eO-cwY.title" = "使用默认值"; 157 | 158 | /* Class = "NSMenuItem"; title = "Font"; ObjectID = "Gi5-1S-RQB"; */ 159 | "Gi5-1S-RQB.title" = "字体"; 160 | 161 | /* Class = "NSMenuItem"; title = "Writing Direction"; ObjectID = "H1b-Si-o9J"; */ 162 | "H1b-Si-o9J.title" = "书写方向"; 163 | 164 | /* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ 165 | "H8h-7b-M4v.title" = "查看"; 166 | 167 | /* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ 168 | "HFQ-gK-NFA.title" = "替换文本"; 169 | 170 | /* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ 171 | "HFo-cy-zxI.title" = "显示拼写和语法"; 172 | 173 | /* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ 174 | "HyV-fh-RgO.title" = "查看"; 175 | 176 | /* Class = "NSMenuItem"; title = "Subscript"; ObjectID = "I0S-gh-46l"; */ 177 | "I0S-gh-46l.title" = "Subscript"; 178 | 179 | /* Class = "NSMenuItem"; title = "Open…"; ObjectID = "IAo-SY-fd9"; */ 180 | "IAo-SY-fd9.title" = "打开…"; 181 | 182 | /* Class = "NSMenuItem"; title = "Justify"; ObjectID = "J5U-5w-g23"; */ 183 | "J5U-5w-g23.title" = "Justify"; 184 | 185 | /* Class = "NSMenuItem"; title = "Use None"; ObjectID = "J7y-lM-qPV"; */ 186 | "J7y-lM-qPV.title" = "Use None"; 187 | 188 | /* Class = "NSTextFieldCell"; title = "Icon"; ObjectID = "JAg-eR-WFV"; */ 189 | "JAg-eR-WFV.title" = "图标"; 190 | 191 | /* Class = "NSMenuItem"; title = "Revert to Saved"; ObjectID = "KaW-ft-85H"; */ 192 | "KaW-ft-85H.title" = "Revert to Saved"; 193 | 194 | /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ 195 | "Kd2-mp-pUS.title" = "显示全部"; 196 | 197 | /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ 198 | "LE2-aR-0XJ.title" = "全部置于顶层"; 199 | 200 | /* Class = "NSMenuItem"; title = "Paste Ruler"; ObjectID = "LVM-kO-fVI"; */ 201 | "LVM-kO-fVI.title" = "Paste Ruler"; 202 | 203 | /* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "Lbh-J2-qVU"; */ 204 | "Lbh-J2-qVU.title" = "\tLeft to Right"; 205 | 206 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "Lnn-2h-S6l"; */ 207 | "Lnn-2h-S6l.title" = "Text Cell"; 208 | 209 | /* Class = "NSButtonCell"; title = "AutoCreate when TmpDisk Starts"; ObjectID = "MLQ-EB-OE8"; */ 210 | "MLQ-EB-OE8.title" = "TmpDisk 启动时自动创建"; 211 | 212 | /* Class = "NSMenuItem"; title = "Copy Ruler"; ObjectID = "MkV-Pr-PK5"; */ 213 | "MkV-Pr-PK5.title" = "Copy Ruler"; 214 | 215 | /* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ 216 | "NMo-om-nkz.title" = "服务"; 217 | 218 | /* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "Nop-cj-93Q"; */ 219 | "Nop-cj-93Q.title" = "\tDefault"; 220 | 221 | /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ 222 | "OY7-WF-poV.title" = "最小化"; 223 | 224 | /* Class = "NSMenuItem"; title = "Baseline"; ObjectID = "OaQ-X3-Vso"; */ 225 | "OaQ-X3-Vso.title" = "Baseline"; 226 | 227 | /* Class = "NSTableColumn"; headerCell.title = "Folders"; ObjectID = "Oae-Gb-smK"; */ 228 | "Oae-Gb-smK.headerCell.title" = "文件夹"; 229 | 230 | /* Class = "NSMenuItem"; title = "Hide TmpDisk"; ObjectID = "Olw-nP-bQN"; */ 231 | "Olw-nP-bQN.title" = "隐藏 TmpDisk"; 232 | 233 | /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; */ 234 | "OwM-mh-QMV.title" = "Find Previous"; 235 | 236 | /* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ 237 | "Oyz-dy-DGm.title" = "Stop Speaking"; 238 | 239 | /* Class = "NSWindow"; title = "Preferences"; ObjectID = "PIG-yb-FRO"; */ 240 | "PIG-yb-FRO.title" = "偏好设置"; 241 | 242 | /* Class = "NSMenuItem"; title = "Bigger"; ObjectID = "Ptp-SP-VEL"; */ 243 | "Ptp-SP-VEL.title" = "Bigger"; 244 | 245 | /* Class = "NSMenuItem"; title = "Show Fonts"; ObjectID = "Q5e-8K-NDq"; */ 246 | "Q5e-8K-NDq.title" = "显示字体"; 247 | 248 | /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ 249 | "R4o-n2-Eq4.title" = "缩放"; 250 | 251 | /* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "RB4-Sm-HuC"; */ 252 | "RB4-Sm-HuC.title" = "\t从右到左"; 253 | 254 | /* Class = "NSMenuItem"; title = "Superscript"; ObjectID = "Rqc-34-cIF"; */ 255 | "Rqc-34-cIF.title" = "Superscript"; 256 | 257 | /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ 258 | "Ruw-6m-B2m.title" = "选择全部"; 259 | 260 | /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; */ 261 | "S0p-oC-mLd.title" = "Jump to Selection"; 262 | 263 | /* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ 264 | "Td7-aD-5lo.title" = "窗口"; 265 | 266 | /* Class = "NSWindow"; title = "AutoCreate Manager"; ObjectID = "TsY-Lt-YWL"; */ 267 | "TsY-Lt-YWL.title" = "自动创建管理器"; 268 | 269 | /* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ 270 | "UEZ-Bs-lqG.title" = "Capitalize"; 271 | 272 | /* Class = "NSTableColumn"; headerCell.title = "Case Sensitive"; ObjectID = "UPy-wU-HZo"; */ 273 | "UPy-wU-HZo.headerCell.title" = "Case Sensitive"; 274 | 275 | /* Class = "NSMenuItem"; title = "Center"; ObjectID = "VIY-Ag-zcb"; */ 276 | "VIY-Ag-zcb.title" = "Center"; 277 | 278 | /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ 279 | "Vdr-fp-XzO.title" = "隐藏其他"; 280 | 281 | /* Class = "NSMenuItem"; title = "Italic"; ObjectID = "Vjx-xi-njq"; */ 282 | "Vjx-xi-njq.title" = "斜体"; 283 | 284 | /* Class = "NSButtonCell"; title = "x"; ObjectID = "VlQ-Mr-VAu"; */ 285 | "VlQ-Mr-VAu.title" = "x"; 286 | 287 | /* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ 288 | "W48-6f-4Dl.title" = "编辑"; 289 | 290 | /* Class = "NSMenuItem"; title = "Underline"; ObjectID = "WRG-CD-K1S"; */ 291 | "WRG-CD-K1S.title" = "Underline"; 292 | 293 | /* Class = "NSMenuItem"; title = "New"; ObjectID = "Was-JA-tGl"; */ 294 | "Was-JA-tGl.title" = "新建"; 295 | 296 | /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; */ 297 | "WeT-3V-zwk.title" = "Paste and Match Style"; 298 | 299 | /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "XWF-fr-bC7"; */ 300 | "XWF-fr-bC7.title" = "Table View Cell"; 301 | 302 | /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; */ 303 | "Xz5-n4-O0W.title" = "查找…"; 304 | 305 | /* Class = "NSTextFieldCell"; title = "Disk Size"; ObjectID = "YCV-8p-NZK"; */ 306 | "YCV-8p-NZK.title" = "磁盘大小"; 307 | 308 | /* Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; */ 309 | "YEy-JH-Tfz.title" = "查找和替换…"; 310 | 311 | /* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "YGs-j5-SAR"; */ 312 | "YGs-j5-SAR.title" = "\tDefault"; 313 | 314 | /* Class = "NSTextFieldCell"; title = "Get the source code"; ObjectID = "YQl-gW-wMR"; */ 315 | "YQl-gW-wMR.title" = "获取源代码"; 316 | 317 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "YRf-Nm-YtA"; */ 318 | "YRf-Nm-YtA.title" = "Text Cell"; 319 | 320 | /* Class = "NSTextFieldCell"; title = "3rd Party Libraries"; ObjectID = "Yhh-3o-HGQ"; */ 321 | "Yhh-3o-HGQ.title" = "第三方库"; 322 | 323 | /* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ 324 | "Ynk-f8-cLZ.title" = "Start Speaking"; 325 | 326 | /* Class = "NSMenuItem"; title = "Align Left"; ObjectID = "ZM1-6Q-yy1"; */ 327 | "ZM1-6Q-yy1.title" = "Align Left"; 328 | 329 | /* Class = "NSMenuItem"; title = "Paragraph"; ObjectID = "ZvO-Gk-QUH"; */ 330 | "ZvO-Gk-QUH.title" = "Paragraph"; 331 | 332 | /* Class = "NSButtonCell"; title = "Update Preferences"; ObjectID = "aFz-gK-uoy"; */ 333 | "aFz-gK-uoy.title" = "更新偏好设置"; 334 | 335 | /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "aTl-1u-JFS"; */ 336 | "aTl-1u-JFS.title" = "打印…"; 337 | 338 | /* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ 339 | "aUF-d1-5bR.title" = "窗口"; 340 | 341 | /* Class = "NSMenu"; title = "Font"; ObjectID = "aXa-aM-Jaq"; */ 342 | "aXa-aM-Jaq.title" = "字体"; 343 | 344 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "agt-UL-0e3"; */ 345 | "agt-UL-0e3.title" = "使用默认值"; 346 | 347 | /* Class = "NSMenuItem"; title = "Show Colors"; ObjectID = "bgn-CT-cEk"; */ 348 | "bgn-CT-cEk.title" = "Show Colors"; 349 | 350 | /* Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu"; */ 351 | "bib-Uj-vzu.title" = "文件"; 352 | 353 | /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; */ 354 | "buJ-ug-pKt.title" = "Use Selection for Find"; 355 | 356 | /* Class = "NSTableColumn"; headerCell.title = "Name"; ObjectID = "bvb-Pe-WVS"; */ 357 | "bvb-Pe-WVS.headerCell.title" = "名称"; 358 | 359 | /* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ 360 | "c8a-y6-VQd.title" = "Transformations"; 361 | 362 | /* Class = "NSMenuItem"; title = "Use None"; ObjectID = "cDB-IK-hbR"; */ 363 | "cDB-IK-hbR.title" = "Use None"; 364 | 365 | /* Class = "NSButtonCell"; title = "Hidden Volume"; ObjectID = "cOC-Ky-V05"; */ 366 | "cOC-Ky-V05.title" = "隐藏卷"; 367 | 368 | /* Class = "NSMenuItem"; title = "Selection"; ObjectID = "cqv-fj-IhA"; */ 369 | "cqv-fj-IhA.title" = "Selection"; 370 | 371 | /* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ 372 | "cwL-P1-jid.title" = "Smart Links"; 373 | 374 | /* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ 375 | "d9M-CD-aMd.title" = "Make Lower Case"; 376 | 377 | /* Class = "NSMenu"; title = "Text"; ObjectID = "d9c-me-L2H"; */ 378 | "d9c-me-L2H.title" = "文本"; 379 | 380 | /* Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ"; */ 381 | "dMs-cI-mzQ.title" = "文件"; 382 | 383 | /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ 384 | "dRJ-4n-Yzg.title" = "撤销"; 385 | 386 | /* Class = "NSTextFieldCell"; placeholderString = "~/.tmpdisk/volumes"; ObjectID = "fMk-fB-fMf"; */ 387 | "fMk-fB-fMf.placeholderString" = "~/.tmpdisk/volumes"; 388 | 389 | /* Class = "NSTableColumn"; headerCell.title = "Hidden"; ObjectID = "fP5-p8-hYR"; */ 390 | "fP5-p8-hYR.headerCell.title" = "隐藏卷"; 391 | 392 | /* Class = "NSButtonCell"; title = "Case Sensitive"; ObjectID = "gCa-HK-wJZ"; */ 393 | "gCa-HK-wJZ.title" = "Case Sensitive"; 394 | 395 | /* Class = "NSButtonCell"; title = "Journaled"; ObjectID = "gFb-jm-Dxc"; */ 396 | "gFb-jm-Dxc.title" = "日志式"; 397 | 398 | /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ 399 | "gVA-U4-sdL.title" = "粘贴"; 400 | 401 | /* Class = "NSMenuItem"; title = "Custom"; ObjectID = "gcJ-GN-9gz"; */ 402 | "gcJ-GN-9gz.title" = "自定义"; 403 | 404 | /* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ 405 | "hQb-2v-fYv.title" = "Smart Quotes"; 406 | 407 | /* Class = "NSTextFieldCell"; title = "MB"; ObjectID = "hdG-Me-bAt"; */ 408 | "hdG-Me-bAt.title" = "MB"; 409 | 410 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "hlg-9B-vKO"; */ 411 | "hlg-9B-vKO.title" = "Text Cell"; 412 | 413 | /* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ 414 | "hz2-CU-CR7.title" = "Check Document Now"; 415 | 416 | /* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ 417 | "hz9-B4-Xy5.title" = "服务"; 418 | 419 | /* Class = "NSMenuItem"; title = "Smaller"; ObjectID = "i1d-Er-qST"; */ 420 | "i1d-Er-qST.title" = "Smaller"; 421 | 422 | /* Class = "NSMenu"; title = "Baseline"; ObjectID = "ijk-EB-dga"; */ 423 | "ijk-EB-dga.title" = "Baseline"; 424 | 425 | /* Class = "NSTextFieldCell"; placeholderString = "Comma separated folder names"; ObjectID = "ini-oz-Wpw"; */ 426 | "ini-oz-Wpw.placeholderString" = "逗号分隔的文件夹名称"; 427 | 428 | /* Class = "NSMenuItem"; title = "Kern"; ObjectID = "jBQ-r6-VK2"; */ 429 | "jBQ-r6-VK2.title" = "Kern"; 430 | 431 | /* Class = "NSTextFieldCell"; title = "Sparkle Framework"; ObjectID = "jFH-29-tnu"; */ 432 | "jFH-29-tnu.title" = "Sparkle Framework"; 433 | 434 | /* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "jFq-tB-4Kx"; */ 435 | "jFq-tB-4Kx.title" = "\tRight to Left"; 436 | 437 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "jWq-m5-D4C"; */ 438 | "jWq-m5-D4C.title" = "Text Cell"; 439 | 440 | /* Class = "NSWindow"; title = "Create New TmpDisk"; ObjectID = "jhY-SC-DO1"; */ 441 | "jhY-SC-DO1.title" = "创建新的 TmpDisk"; 442 | 443 | /* Class = "NSMenuItem"; title = "Format"; ObjectID = "jxT-CU-nIS"; */ 444 | "jxT-CU-nIS.title" = "格式"; 445 | 446 | /* Class = "NSMenuItem"; title = "Show Sidebar"; ObjectID = "kIP-vf-haE"; */ 447 | "kIP-vf-haE.title" = "显示侧边栏"; 448 | 449 | /* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ 450 | "mK6-2p-4JG.title" = "检查语法和拼写"; 451 | 452 | /* Class = "NSTableColumn"; headerCell.title = "Warn"; ObjectID = "moE-oU-Xjj"; */ 453 | "moE-oU-Xjj.headerCell.title" = "警告"; 454 | 455 | /* Class = "NSMenuItem"; title = "50% of Ram"; ObjectID = "nXv-G3-d8b"; */ 456 | "nXv-G3-d8b.title" = "50% 内存"; 457 | 458 | /* Class = "NSTextFieldCell"; placeholderString = "0"; ObjectID = "nYC-7d-c8X"; */ 459 | "nYC-7d-c8X.placeholderString" = "0"; 460 | 461 | /* Class = "NSTextFieldCell"; title = "16"; ObjectID = "nYC-7d-c8X"; */ 462 | "nYC-7d-c8X.title" = "16"; 463 | 464 | /* Class = "NSMenuItem"; title = "Ligatures"; ObjectID = "o6e-r0-MWq"; */ 465 | "o6e-r0-MWq.title" = "Ligatures"; 466 | 467 | /* Class = "NSMenuItem"; title = "75% of Ram"; ObjectID = "oUd-0o-jwy"; */ 468 | "oUd-0o-jwy.title" = "75% 内存"; 469 | 470 | /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "oas-Oc-fiZ"; */ 471 | "oas-Oc-fiZ.title" = "打开最近的文件"; 472 | 473 | /* Class = "NSMenuItem"; title = "Loosen"; ObjectID = "ogc-rX-tC1"; */ 474 | "ogc-rX-tC1.title" = "Loosen"; 475 | 476 | /* Class = "NSTableColumn"; headerCell.title = "Indexed"; ObjectID = "oqx-VA-7Zr"; */ 477 | "oqx-VA-7Zr.headerCell.title" = "索引卷"; 478 | 479 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "pL4-3x-WFv"; */ 480 | "pL4-3x-WFv.title" = "Text Cell"; 481 | 482 | /* Class = "NSTextFieldCell"; title = "Warning: When a TmpDisk is ejected or the computer restarted all files on it will be permanently deleted. Please only store temporary files on a TmpDisk."; ObjectID = "pSH-sJ-D0k"; */ 483 | "pSH-sJ-D0k.title" = "警告:TmpDisk 被推出或电脑重新启动后,其中的文件将被永久删除。请务必只在 TmpDisk 中存放临时文件。"; 484 | 485 | /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ 486 | "pa3-QI-u2k.title" = "删除"; 487 | 488 | /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "pm4-Q2-gSs"; */ 489 | "pm4-Q2-gSs.title" = "Text Cell"; 490 | 491 | /* Class = "NSMenuItem"; title = "Save…"; ObjectID = "pxx-59-PXV"; */ 492 | "pxx-59-PXV.title" = "保存…"; 493 | 494 | /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; */ 495 | "q09-fT-Sye.title" = "Find Next"; 496 | 497 | /* Class = "NSTextFieldCell"; title = "TmpFS Root Folder"; ObjectID = "q71-xV-gGa"; */ 498 | "q71-xV-gGa.title" = "TmpFS 根文件夹"; 499 | 500 | /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "qIS-W8-SiK"; */ 501 | "qIS-W8-SiK.title" = "页面设置…"; 502 | 503 | /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "qXn-wF-aJV"; */ 504 | "qXn-wF-aJV.title" = "Table View Cell"; 505 | 506 | /* Class = "NSWindow"; title = "About"; ObjectID = "qkc-td-g2b"; */ 507 | "qkc-td-g2b.title" = "关于"; 508 | 509 | /* Class = "NSTextFieldCell"; title = "Folders"; ObjectID = "qw8-z7-vQx"; */ 510 | "qw8-z7-vQx.title" = "文件夹"; 511 | 512 | /* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ 513 | "rbD-Rh-wIN.title" = "Check Spelling While Typing"; 514 | 515 | /* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ 516 | "rgM-f4-ycn.title" = "Smart Dashes"; 517 | 518 | /* Class = "NSMenuItem"; title = "Show Toolbar"; ObjectID = "snW-S8-Cw5"; */ 519 | "snW-S8-Cw5.title" = "Show Toolbar"; 520 | 521 | /* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "t3z-IH-wJR"; */ 522 | "t3z-IH-wJR.title" = "Table View Cell"; 523 | 524 | /* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ 525 | "tRr-pd-1PS.title" = "Data Detectors"; 526 | 527 | /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "tXI-mr-wws"; */ 528 | "tXI-mr-wws.title" = "打开最近的文件"; 529 | 530 | /* Class = "NSMenu"; title = "Kern"; ObjectID = "tlD-Oa-oAM"; */ 531 | "tlD-Oa-oAM.title" = "Kern"; 532 | 533 | /* Class = "NSTextFieldCell"; title = "These volumes will be created automatically whenever TmpDisk starts"; ObjectID = "toS-Ay-uEq"; */ 534 | "toS-Ay-uEq.title" = "应用启动时自动创建"; 535 | 536 | /* Class = "NSMenu"; title = "TmpDisk"; ObjectID = "uQy-DD-JDr"; */ 537 | "uQy-DD-JDr.title" = "TmpDisk"; 538 | 539 | /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ 540 | "uRl-iY-unG.title" = "剪切"; 541 | 542 | /* Class = "NSMenuItem"; title = "25% of Ram"; ObjectID = "ud8-KO-JVf"; */ 543 | "ud8-KO-JVf.title" = "25% 内存"; 544 | 545 | /* Class = "NSMenuItem"; title = "Paste Style"; ObjectID = "vKC-jM-MkH"; */ 546 | "vKC-jM-MkH.title" = "Paste Style"; 547 | 548 | /* Class = "NSMenuItem"; title = "Show Ruler"; ObjectID = "vLm-3I-IUL"; */ 549 | "vLm-3I-IUL.title" = "Show Ruler"; 550 | 551 | /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "vNY-rz-j42"; */ 552 | "vNY-rz-j42.title" = "Clear Menu"; 553 | 554 | /* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ 555 | "vmV-6d-7jI.title" = "Make Upper Case"; 556 | 557 | /* Class = "NSMenu"; title = "Ligatures"; ObjectID = "w0m-vy-SC9"; */ 558 | "w0m-vy-SC9.title" = "Ligatures"; 559 | 560 | /* Class = "NSMenuItem"; title = "Align Right"; ObjectID = "wb2-vD-lq4"; */ 561 | "wb2-vD-lq4.title" = "Align Right"; 562 | 563 | /* Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd"; */ 564 | "wpr-3q-Mcd.title" = "帮助"; 565 | 566 | /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ 567 | "x3v-GG-iWU.title" = "复制"; 568 | 569 | /* Class = "NSButtonCell"; title = "Use TmpFS (Needs Root Access)"; ObjectID = "xBA-Ze-bf2"; */ 570 | "xBA-Ze-bf2.title" = "使用 TmpFS(需要根目录访问权限)"; 571 | 572 | /* Class = "NSMenuItem"; title = "Use All"; ObjectID = "xQD-1f-W4t"; */ 573 | "xQD-1f-W4t.title" = "Use All"; 574 | 575 | /* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ 576 | "xrE-MZ-jX0.title" = "Speech"; 577 | 578 | /* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ 579 | "z6F-FW-3nz.title" = "Show Substitutions"; 580 | 581 | /* Class = "NSTextFieldCell"; title = "Installing the TmpDiskCreator Helper can install TmpFS TmpDisks without asking for your root password. Requires Admin password to install"; ObjectID = "LLv-lK-dgg"; */ 582 | "LLv-lK-dgg.title" = "安装 TmpDiskHelper 后,无需输入 root 密码即可创建 TmpFS 临时磁盘。安装时需要管理员密码"; 583 | -------------------------------------------------------------------------------- /TmpDiskLauncher/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TmpDiskLauncher 4 | // 5 | // Created by @imothe on 12/19/21. 6 | // 7 | // This file is part of TmpDisk. 8 | // 9 | // TmpDisk is free software: you can redistribute it and/or modify 10 | // it under the terms of the GNU General Public License as published by 11 | // the Free Software Foundation, either version 3 of the License, or 12 | // (at your option) any later version. 13 | // 14 | // TmpDisk is distributed in the hope that it will be useful, 15 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | // GNU General Public License for more details. 18 | // 19 | // You should have received a copy of the GNU General Public License 20 | // along with TmpDisk. If not, see . 21 | 22 | import Cocoa 23 | 24 | extension Notification.Name { 25 | static let killLauncher = Notification.Name("killLauncher") 26 | } 27 | 28 | @main 29 | class AppDelegate: NSObject, NSApplicationDelegate { 30 | 31 | func applicationDidFinishLaunching(_ aNotification: Notification) { 32 | let mainAppIdentifier = "com.imothee.TmpDisk" 33 | let runningApps = NSWorkspace.shared.runningApplications 34 | let isRunning = !runningApps.filter { $0.bundleIdentifier == mainAppIdentifier }.isEmpty 35 | 36 | if !isRunning { 37 | DistributedNotificationCenter.default().addObserver(self, selector: #selector(self.terminate), name: .killLauncher, object: mainAppIdentifier) 38 | 39 | let path = Bundle.main.bundlePath as NSString 40 | var components = path.pathComponents 41 | components.removeLast() 42 | components.removeLast() 43 | components.removeLast() 44 | components.append("MacOS") 45 | components.append("TmpDisk") 46 | 47 | let newPath = NSString.path(withComponents: components) 48 | 49 | NSWorkspace.shared.launchApplication(newPath) 50 | } 51 | else { 52 | self.terminate() 53 | } 54 | } 55 | 56 | func applicationWillTerminate(_ aNotification: Notification) { 57 | // Insert code here to tear down your application 58 | } 59 | 60 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 61 | return true 62 | } 63 | 64 | @objc func terminate() { 65 | NSApp.terminate(nil) 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /TmpDiskLauncher/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 | -------------------------------------------------------------------------------- /TmpDiskLauncher/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "filename" : "icon.png", 40 | "idiom" : "mac", 41 | "scale" : "2x", 42 | "size" : "256x256" 43 | }, 44 | { 45 | "filename" : "icon-1.png", 46 | "idiom" : "mac", 47 | "scale" : "1x", 48 | "size" : "512x512" 49 | }, 50 | { 51 | "idiom" : "mac", 52 | "scale" : "2x", 53 | "size" : "512x512" 54 | } 55 | ], 56 | "info" : { 57 | "author" : "xcode", 58 | "version" : 1 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /TmpDiskLauncher/Assets.xcassets/AppIcon.appiconset/icon-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imothee/tmpdisk/c5db4f3f8567beaa5eac4a477c86a5591658ebf0/TmpDiskLauncher/Assets.xcassets/AppIcon.appiconset/icon-1.png -------------------------------------------------------------------------------- /TmpDiskLauncher/Assets.xcassets/AppIcon.appiconset/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imothee/tmpdisk/c5db4f3f8567beaa5eac4a477c86a5591658ebf0/TmpDiskLauncher/Assets.xcassets/AppIcon.appiconset/icon.png -------------------------------------------------------------------------------- /TmpDiskLauncher/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TmpDiskLauncher/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSBackgroundOnly 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TmpDiskLauncher/TmpDiskLauncher.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TmpDiskLauncher/es.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; */ 3 | "1UK-8n-QPP.title" = "Customize Toolbar…"; 4 | 5 | /* Class = "NSMenuItem"; title = "TmpDiskLauncher"; ObjectID = "1Xt-HY-uBw"; */ 6 | "1Xt-HY-uBw.title" = "TmpDiskLauncher"; 7 | 8 | /* Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; */ 9 | "1b7-l0-nxx.title" = "Find"; 10 | 11 | /* Class = "NSMenuItem"; title = "Lower"; ObjectID = "1tx-W0-xDw"; */ 12 | "1tx-W0-xDw.title" = "Lower"; 13 | 14 | /* Class = "NSMenuItem"; title = "Raise"; ObjectID = "2h7-ER-AoG"; */ 15 | "2h7-ER-AoG.title" = "Raise"; 16 | 17 | /* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ 18 | "2oI-Rn-ZJC.title" = "Transformations"; 19 | 20 | /* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ 21 | "3IN-sU-3Bg.title" = "Spelling"; 22 | 23 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "3Om-Ey-2VK"; */ 24 | "3Om-Ey-2VK.title" = "Use Default"; 25 | 26 | /* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ 27 | "3rS-ZA-NoH.title" = "Speech"; 28 | 29 | /* Class = "NSMenuItem"; title = "Tighten"; ObjectID = "46P-cB-AYj"; */ 30 | "46P-cB-AYj.title" = "Tighten"; 31 | 32 | /* Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; */ 33 | "4EN-yA-p0u.title" = "Find"; 34 | 35 | /* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ 36 | "4J7-dP-txa.title" = "Enter Full Screen"; 37 | 38 | /* Class = "NSMenuItem"; title = "Quit TmpDiskLauncher"; ObjectID = "4sb-4s-VLi"; */ 39 | "4sb-4s-VLi.title" = "Quit TmpDiskLauncher"; 40 | 41 | /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ 42 | "5QF-Oa-p0T.title" = "Edit"; 43 | 44 | /* Class = "NSMenuItem"; title = "Copy Style"; ObjectID = "5Vv-lz-BsD"; */ 45 | "5Vv-lz-BsD.title" = "Copy Style"; 46 | 47 | /* Class = "NSMenuItem"; title = "About TmpDiskLauncher"; ObjectID = "5kV-Vb-QxS"; */ 48 | "5kV-Vb-QxS.title" = "About TmpDiskLauncher"; 49 | 50 | /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ 51 | "6dh-zS-Vam.title" = "Redo"; 52 | 53 | /* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ 54 | "78Y-hA-62v.title" = "Correct Spelling Automatically"; 55 | 56 | /* Class = "NSMenu"; title = "Writing Direction"; ObjectID = "8mr-sm-Yjd"; */ 57 | "8mr-sm-Yjd.title" = "Writing Direction"; 58 | 59 | /* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ 60 | "9ic-FL-obx.title" = "Substitutions"; 61 | 62 | /* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ 63 | "9yt-4B-nSM.title" = "Smart Copy/Paste"; 64 | 65 | /* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ 66 | "AYu-sK-qS6.title" = "Main Menu"; 67 | 68 | /* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ 69 | "BOF-NM-1cW.title" = "Preferences…"; 70 | 71 | /* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "BgM-ve-c93"; */ 72 | "BgM-ve-c93.title" = "\tLeft to Right"; 73 | 74 | /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "Bw7-FT-i3A"; */ 75 | "Bw7-FT-i3A.title" = "Save As…"; 76 | 77 | /* Class = "NSMenuItem"; title = "Close"; ObjectID = "DVo-aG-piG"; */ 78 | "DVo-aG-piG.title" = "Close"; 79 | 80 | /* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ 81 | "Dv1-io-Yv7.title" = "Spelling and Grammar"; 82 | 83 | /* Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; */ 84 | "F2S-fz-NVQ.title" = "Help"; 85 | 86 | /* Class = "NSMenuItem"; title = "TmpDiskLauncher Help"; ObjectID = "FKE-Sm-Kum"; */ 87 | "FKE-Sm-Kum.title" = "TmpDiskLauncher Help"; 88 | 89 | /* Class = "NSMenuItem"; title = "Text"; ObjectID = "Fal-I4-PZk"; */ 90 | "Fal-I4-PZk.title" = "Text"; 91 | 92 | /* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ 93 | "FeM-D8-WVr.title" = "Substitutions"; 94 | 95 | /* Class = "NSMenuItem"; title = "Bold"; ObjectID = "GB9-OM-e27"; */ 96 | "GB9-OM-e27.title" = "Bold"; 97 | 98 | /* Class = "NSMenu"; title = "Format"; ObjectID = "GEO-Iw-cKr"; */ 99 | "GEO-Iw-cKr.title" = "Format"; 100 | 101 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "GUa-eO-cwY"; */ 102 | "GUa-eO-cwY.title" = "Use Default"; 103 | 104 | /* Class = "NSMenuItem"; title = "Font"; ObjectID = "Gi5-1S-RQB"; */ 105 | "Gi5-1S-RQB.title" = "Font"; 106 | 107 | /* Class = "NSMenuItem"; title = "Writing Direction"; ObjectID = "H1b-Si-o9J"; */ 108 | "H1b-Si-o9J.title" = "Writing Direction"; 109 | 110 | /* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ 111 | "H8h-7b-M4v.title" = "View"; 112 | 113 | /* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ 114 | "HFQ-gK-NFA.title" = "Text Replacement"; 115 | 116 | /* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ 117 | "HFo-cy-zxI.title" = "Show Spelling and Grammar"; 118 | 119 | /* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ 120 | "HyV-fh-RgO.title" = "View"; 121 | 122 | /* Class = "NSMenuItem"; title = "Subscript"; ObjectID = "I0S-gh-46l"; */ 123 | "I0S-gh-46l.title" = "Subscript"; 124 | 125 | /* Class = "NSMenuItem"; title = "Open…"; ObjectID = "IAo-SY-fd9"; */ 126 | "IAo-SY-fd9.title" = "Open…"; 127 | 128 | /* Class = "NSMenuItem"; title = "Justify"; ObjectID = "J5U-5w-g23"; */ 129 | "J5U-5w-g23.title" = "Justify"; 130 | 131 | /* Class = "NSMenuItem"; title = "Use None"; ObjectID = "J7y-lM-qPV"; */ 132 | "J7y-lM-qPV.title" = "Use None"; 133 | 134 | /* Class = "NSMenuItem"; title = "Revert to Saved"; ObjectID = "KaW-ft-85H"; */ 135 | "KaW-ft-85H.title" = "Revert to Saved"; 136 | 137 | /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ 138 | "Kd2-mp-pUS.title" = "Show All"; 139 | 140 | /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ 141 | "LE2-aR-0XJ.title" = "Bring All to Front"; 142 | 143 | /* Class = "NSMenuItem"; title = "Paste Ruler"; ObjectID = "LVM-kO-fVI"; */ 144 | "LVM-kO-fVI.title" = "Paste Ruler"; 145 | 146 | /* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "Lbh-J2-qVU"; */ 147 | "Lbh-J2-qVU.title" = "\tLeft to Right"; 148 | 149 | /* Class = "NSMenuItem"; title = "Copy Ruler"; ObjectID = "MkV-Pr-PK5"; */ 150 | "MkV-Pr-PK5.title" = "Copy Ruler"; 151 | 152 | /* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ 153 | "NMo-om-nkz.title" = "Services"; 154 | 155 | /* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "Nop-cj-93Q"; */ 156 | "Nop-cj-93Q.title" = "\tDefault"; 157 | 158 | /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ 159 | "OY7-WF-poV.title" = "Minimize"; 160 | 161 | /* Class = "NSMenuItem"; title = "Baseline"; ObjectID = "OaQ-X3-Vso"; */ 162 | "OaQ-X3-Vso.title" = "Baseline"; 163 | 164 | /* Class = "NSMenuItem"; title = "Hide TmpDiskLauncher"; ObjectID = "Olw-nP-bQN"; */ 165 | "Olw-nP-bQN.title" = "Hide TmpDiskLauncher"; 166 | 167 | /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; */ 168 | "OwM-mh-QMV.title" = "Find Previous"; 169 | 170 | /* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ 171 | "Oyz-dy-DGm.title" = "Stop Speaking"; 172 | 173 | /* Class = "NSMenuItem"; title = "Bigger"; ObjectID = "Ptp-SP-VEL"; */ 174 | "Ptp-SP-VEL.title" = "Bigger"; 175 | 176 | /* Class = "NSMenuItem"; title = "Show Fonts"; ObjectID = "Q5e-8K-NDq"; */ 177 | "Q5e-8K-NDq.title" = "Show Fonts"; 178 | 179 | /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ 180 | "R4o-n2-Eq4.title" = "Zoom"; 181 | 182 | /* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "RB4-Sm-HuC"; */ 183 | "RB4-Sm-HuC.title" = "\tRight to Left"; 184 | 185 | /* Class = "NSMenuItem"; title = "Superscript"; ObjectID = "Rqc-34-cIF"; */ 186 | "Rqc-34-cIF.title" = "Superscript"; 187 | 188 | /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ 189 | "Ruw-6m-B2m.title" = "Select All"; 190 | 191 | /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; */ 192 | "S0p-oC-mLd.title" = "Jump to Selection"; 193 | 194 | /* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ 195 | "Td7-aD-5lo.title" = "Window"; 196 | 197 | /* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ 198 | "UEZ-Bs-lqG.title" = "Capitalize"; 199 | 200 | /* Class = "NSMenuItem"; title = "Center"; ObjectID = "VIY-Ag-zcb"; */ 201 | "VIY-Ag-zcb.title" = "Center"; 202 | 203 | /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ 204 | "Vdr-fp-XzO.title" = "Hide Others"; 205 | 206 | /* Class = "NSMenuItem"; title = "Italic"; ObjectID = "Vjx-xi-njq"; */ 207 | "Vjx-xi-njq.title" = "Italic"; 208 | 209 | /* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ 210 | "W48-6f-4Dl.title" = "Edit"; 211 | 212 | /* Class = "NSMenuItem"; title = "Underline"; ObjectID = "WRG-CD-K1S"; */ 213 | "WRG-CD-K1S.title" = "Underline"; 214 | 215 | /* Class = "NSMenuItem"; title = "New"; ObjectID = "Was-JA-tGl"; */ 216 | "Was-JA-tGl.title" = "New"; 217 | 218 | /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; */ 219 | "WeT-3V-zwk.title" = "Paste and Match Style"; 220 | 221 | /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; */ 222 | "Xz5-n4-O0W.title" = "Find…"; 223 | 224 | /* Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; */ 225 | "YEy-JH-Tfz.title" = "Find and Replace…"; 226 | 227 | /* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "YGs-j5-SAR"; */ 228 | "YGs-j5-SAR.title" = "\tDefault"; 229 | 230 | /* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ 231 | "Ynk-f8-cLZ.title" = "Start Speaking"; 232 | 233 | /* Class = "NSMenuItem"; title = "Align Left"; ObjectID = "ZM1-6Q-yy1"; */ 234 | "ZM1-6Q-yy1.title" = "Align Left"; 235 | 236 | /* Class = "NSMenuItem"; title = "Paragraph"; ObjectID = "ZvO-Gk-QUH"; */ 237 | "ZvO-Gk-QUH.title" = "Paragraph"; 238 | 239 | /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "aTl-1u-JFS"; */ 240 | "aTl-1u-JFS.title" = "Print…"; 241 | 242 | /* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ 243 | "aUF-d1-5bR.title" = "Window"; 244 | 245 | /* Class = "NSMenu"; title = "Font"; ObjectID = "aXa-aM-Jaq"; */ 246 | "aXa-aM-Jaq.title" = "Font"; 247 | 248 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "agt-UL-0e3"; */ 249 | "agt-UL-0e3.title" = "Use Default"; 250 | 251 | /* Class = "NSMenuItem"; title = "Show Colors"; ObjectID = "bgn-CT-cEk"; */ 252 | "bgn-CT-cEk.title" = "Show Colors"; 253 | 254 | /* Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu"; */ 255 | "bib-Uj-vzu.title" = "File"; 256 | 257 | /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; */ 258 | "buJ-ug-pKt.title" = "Use Selection for Find"; 259 | 260 | /* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ 261 | "c8a-y6-VQd.title" = "Transformations"; 262 | 263 | /* Class = "NSMenuItem"; title = "Use None"; ObjectID = "cDB-IK-hbR"; */ 264 | "cDB-IK-hbR.title" = "Use None"; 265 | 266 | /* Class = "NSMenuItem"; title = "Selection"; ObjectID = "cqv-fj-IhA"; */ 267 | "cqv-fj-IhA.title" = "Selection"; 268 | 269 | /* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ 270 | "cwL-P1-jid.title" = "Smart Links"; 271 | 272 | /* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ 273 | "d9M-CD-aMd.title" = "Make Lower Case"; 274 | 275 | /* Class = "NSMenu"; title = "Text"; ObjectID = "d9c-me-L2H"; */ 276 | "d9c-me-L2H.title" = "Text"; 277 | 278 | /* Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ"; */ 279 | "dMs-cI-mzQ.title" = "File"; 280 | 281 | /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ 282 | "dRJ-4n-Yzg.title" = "Undo"; 283 | 284 | /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ 285 | "gVA-U4-sdL.title" = "Paste"; 286 | 287 | /* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ 288 | "hQb-2v-fYv.title" = "Smart Quotes"; 289 | 290 | /* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ 291 | "hz2-CU-CR7.title" = "Check Document Now"; 292 | 293 | /* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ 294 | "hz9-B4-Xy5.title" = "Services"; 295 | 296 | /* Class = "NSMenuItem"; title = "Smaller"; ObjectID = "i1d-Er-qST"; */ 297 | "i1d-Er-qST.title" = "Smaller"; 298 | 299 | /* Class = "NSMenu"; title = "Baseline"; ObjectID = "ijk-EB-dga"; */ 300 | "ijk-EB-dga.title" = "Baseline"; 301 | 302 | /* Class = "NSMenuItem"; title = "Kern"; ObjectID = "jBQ-r6-VK2"; */ 303 | "jBQ-r6-VK2.title" = "Kern"; 304 | 305 | /* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "jFq-tB-4Kx"; */ 306 | "jFq-tB-4Kx.title" = "\tRight to Left"; 307 | 308 | /* Class = "NSMenuItem"; title = "Format"; ObjectID = "jxT-CU-nIS"; */ 309 | "jxT-CU-nIS.title" = "Format"; 310 | 311 | /* Class = "NSMenuItem"; title = "Show Sidebar"; ObjectID = "kIP-vf-haE"; */ 312 | "kIP-vf-haE.title" = "Show Sidebar"; 313 | 314 | /* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ 315 | "mK6-2p-4JG.title" = "Check Grammar With Spelling"; 316 | 317 | /* Class = "NSMenuItem"; title = "Ligatures"; ObjectID = "o6e-r0-MWq"; */ 318 | "o6e-r0-MWq.title" = "Ligatures"; 319 | 320 | /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "oas-Oc-fiZ"; */ 321 | "oas-Oc-fiZ.title" = "Open Recent"; 322 | 323 | /* Class = "NSMenuItem"; title = "Loosen"; ObjectID = "ogc-rX-tC1"; */ 324 | "ogc-rX-tC1.title" = "Loosen"; 325 | 326 | /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ 327 | "pa3-QI-u2k.title" = "Delete"; 328 | 329 | /* Class = "NSMenuItem"; title = "Save…"; ObjectID = "pxx-59-PXV"; */ 330 | "pxx-59-PXV.title" = "Save…"; 331 | 332 | /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; */ 333 | "q09-fT-Sye.title" = "Find Next"; 334 | 335 | /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "qIS-W8-SiK"; */ 336 | "qIS-W8-SiK.title" = "Page Setup…"; 337 | 338 | /* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ 339 | "rbD-Rh-wIN.title" = "Check Spelling While Typing"; 340 | 341 | /* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ 342 | "rgM-f4-ycn.title" = "Smart Dashes"; 343 | 344 | /* Class = "NSMenuItem"; title = "Show Toolbar"; ObjectID = "snW-S8-Cw5"; */ 345 | "snW-S8-Cw5.title" = "Show Toolbar"; 346 | 347 | /* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ 348 | "tRr-pd-1PS.title" = "Data Detectors"; 349 | 350 | /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "tXI-mr-wws"; */ 351 | "tXI-mr-wws.title" = "Open Recent"; 352 | 353 | /* Class = "NSMenu"; title = "Kern"; ObjectID = "tlD-Oa-oAM"; */ 354 | "tlD-Oa-oAM.title" = "Kern"; 355 | 356 | /* Class = "NSMenu"; title = "TmpDiskLauncher"; ObjectID = "uQy-DD-JDr"; */ 357 | "uQy-DD-JDr.title" = "TmpDiskLauncher"; 358 | 359 | /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ 360 | "uRl-iY-unG.title" = "Cut"; 361 | 362 | /* Class = "NSMenuItem"; title = "Paste Style"; ObjectID = "vKC-jM-MkH"; */ 363 | "vKC-jM-MkH.title" = "Paste Style"; 364 | 365 | /* Class = "NSMenuItem"; title = "Show Ruler"; ObjectID = "vLm-3I-IUL"; */ 366 | "vLm-3I-IUL.title" = "Show Ruler"; 367 | 368 | /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "vNY-rz-j42"; */ 369 | "vNY-rz-j42.title" = "Clear Menu"; 370 | 371 | /* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ 372 | "vmV-6d-7jI.title" = "Make Upper Case"; 373 | 374 | /* Class = "NSMenu"; title = "Ligatures"; ObjectID = "w0m-vy-SC9"; */ 375 | "w0m-vy-SC9.title" = "Ligatures"; 376 | 377 | /* Class = "NSMenuItem"; title = "Align Right"; ObjectID = "wb2-vD-lq4"; */ 378 | "wb2-vD-lq4.title" = "Align Right"; 379 | 380 | /* Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd"; */ 381 | "wpr-3q-Mcd.title" = "Help"; 382 | 383 | /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ 384 | "x3v-GG-iWU.title" = "Copy"; 385 | 386 | /* Class = "NSMenuItem"; title = "Use All"; ObjectID = "xQD-1f-W4t"; */ 387 | "xQD-1f-W4t.title" = "Use All"; 388 | 389 | /* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ 390 | "xrE-MZ-jX0.title" = "Speech"; 391 | 392 | /* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ 393 | "z6F-FW-3nz.title" = "Show Substitutions"; 394 | -------------------------------------------------------------------------------- /TmpDiskTests/SMJobBlessUtil.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | # 3 | # File: SMJobBlessUtil.py 4 | # 5 | # Contains: Tool for checking and correcting apps that use SMJobBless. 6 | # 7 | # Written by: DTS 8 | # 9 | # Copyright: Copyright (c) 2012 Apple Inc. All Rights Reserved. 10 | # 11 | # Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. 12 | # ("Apple") in consideration of your agreement to the following 13 | # terms, and your use, installation, modification or 14 | # redistribution of this Apple software constitutes acceptance of 15 | # these terms. If you do not agree with these terms, please do 16 | # not use, install, modify or redistribute this Apple software. 17 | # 18 | # In consideration of your agreement to abide by the following 19 | # terms, and subject to these terms, Apple grants you a personal, 20 | # non-exclusive license, under Apple's copyrights in this 21 | # original Apple software (the "Apple Software"), to use, 22 | # reproduce, modify and redistribute the Apple Software, with or 23 | # without modifications, in source and/or binary forms; provided 24 | # that if you redistribute the Apple Software in its entirety and 25 | # without modifications, you must retain this notice and the 26 | # following text and disclaimers in all such redistributions of 27 | # the Apple Software. Neither the name, trademarks, service marks 28 | # or logos of Apple Inc. may be used to endorse or promote 29 | # products derived from the Apple Software without specific prior 30 | # written permission from Apple. Except as expressly stated in 31 | # this notice, no other rights or licenses, express or implied, 32 | # are granted by Apple herein, including but not limited to any 33 | # patent rights that may be infringed by your derivative works or 34 | # by other works in which the Apple Software may be incorporated. 35 | # 36 | # The Apple Software is provided by Apple on an "AS IS" basis. 37 | # APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING 38 | # WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, 39 | # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING 40 | # THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN 41 | # COMBINATION WITH YOUR PRODUCTS. 42 | # 43 | # IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, 44 | # INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 45 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 46 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY 47 | # OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION 48 | # OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY 49 | # OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR 50 | # OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF 51 | # SUCH DAMAGE. 52 | # 53 | 54 | import sys 55 | import os 56 | import getopt 57 | import subprocess 58 | import plistlib 59 | import operator 60 | import platform 61 | 62 | class UsageException (Exception): 63 | """ 64 | Raised when the progam detects a usage issue; the top-level code catches this 65 | and prints a usage message. 66 | """ 67 | pass 68 | 69 | class CheckException (Exception): 70 | """ 71 | Raised when the "check" subcommand detects a problem; the top-level code catches 72 | this and prints a nice error message. 73 | """ 74 | def __init__(self, message, path=None): 75 | self.message = message 76 | self.path = path 77 | 78 | def checkCodeSignature(programPath, programType): 79 | """Checks the code signature of the referenced program.""" 80 | 81 | # Use the codesign tool to check the signature. The second "-v" is required to enable 82 | # verbose mode, which causes codesign to do more checking. By default it does the minimum 83 | # amount of checking ("Is the program properly signed?"). If you enabled verbose mode it 84 | # does other sanity checks, which we definitely want. The specific thing I'd like to 85 | # detect is "Does the code satisfy its own designated requirement?" and I need to enable 86 | # verbose mode to get that. 87 | 88 | args = [ 89 | # "false", 90 | "codesign", 91 | "-v", 92 | "-v", 93 | programPath 94 | ] 95 | try: 96 | subprocess.check_call(args, stderr=open("/dev/null")) 97 | except subprocess.CalledProcessError as e: 98 | raise CheckException("%s code signature invalid" % programType, programPath) 99 | 100 | def readDesignatedRequirement(programPath, programType): 101 | """Returns the designated requirement of the program as a string.""" 102 | args = [ 103 | # "false", 104 | "codesign", 105 | "-d", 106 | "-r", 107 | "-", 108 | programPath 109 | ] 110 | try: 111 | req = subprocess.check_output(args, stderr=open("/dev/null"), encoding="utf-8") 112 | except subprocess.CalledProcessError as e: 113 | raise CheckException("%s designated requirement unreadable" % programType, programPath) 114 | 115 | reqLines = req.splitlines() 116 | if len(reqLines) != 1 or not req.startswith("designated => "): 117 | raise CheckException("%s designated requirement malformed" % programType, programPath) 118 | return reqLines[0][len("designated => "):] 119 | 120 | def readInfoPlistFromPath(infoPath): 121 | """Reads an "Info.plist" file from the specified path.""" 122 | try: 123 | with open(infoPath, 'rb') as fp: 124 | info = plistlib.load(fp) 125 | except: 126 | raise CheckException("'Info.plist' not readable", infoPath) 127 | if not isinstance(info, dict): 128 | raise CheckException("'Info.plist' root must be a dictionary", infoPath) 129 | return info 130 | 131 | def readPlistFromToolSection(toolPath, segmentName, sectionName): 132 | """Reads a dictionary property list from the specified section within the specified executable.""" 133 | 134 | # Run otool -s to get a hex dump of the section. 135 | 136 | args = [ 137 | # "false", 138 | "otool", 139 | "-V", 140 | "-arch", 141 | platform.machine(), 142 | "-s", 143 | segmentName, 144 | sectionName, 145 | toolPath 146 | ] 147 | try: 148 | plistDump = subprocess.check_output(args, encoding="utf-8") 149 | except subprocess.CalledProcessError as e: 150 | raise CheckException("tool %s / %s section unreadable" % (segmentName, sectionName), toolPath) 151 | 152 | # Convert that dump to an property list. 153 | 154 | plistLines = plistDump.strip().splitlines(keepends=True) 155 | 156 | if len(plistLines) < 3: 157 | raise CheckException("tool %s / %s section dump malformed (1)" % (segmentName, sectionName), toolPath) 158 | 159 | header = plistLines[1].strip() 160 | 161 | if not header.endswith("(%s,%s) section" % (segmentName, sectionName)): 162 | raise CheckException("tool %s / %s section dump malformed (2)" % (segmentName, sectionName), toolPath) 163 | 164 | del plistLines[0:2] 165 | 166 | try: 167 | 168 | if header.startswith('Contents of'): 169 | data = [] 170 | for line in plistLines: 171 | # line looks like this: 172 | # 173 | # '100000000 3c 3f 78 6d 6c 20 76 65 72 73 69 6f 6e 3d 22 31 |= 2 178 | del columns[0] 179 | for hexStr in columns: 180 | data.append(int(hexStr, 16)) 181 | data = bytes(data) 182 | else: 183 | data = bytes("".join(plistLines), encoding="utf-8") 184 | 185 | plist = plistlib.loads(data) 186 | except: 187 | raise CheckException("tool %s / %s section dump malformed (3)" % (segmentName, sectionName), toolPath) 188 | 189 | # Check the root of the property list. 190 | 191 | if not isinstance(plist, dict): 192 | raise CheckException("tool %s / %s property list root must be a dictionary" % (segmentName, sectionName), toolPath) 193 | 194 | return plist 195 | 196 | def checkStep1(appPath): 197 | """Checks that the app and the tool are both correctly code signed.""" 198 | 199 | if not os.path.isdir(appPath): 200 | raise CheckException("app not found", appPath) 201 | 202 | # Check the app's code signature. 203 | 204 | checkCodeSignature(appPath, "app") 205 | 206 | # Check the tool directory. 207 | 208 | toolDirPath = os.path.join(appPath, "Contents", "Library", "LaunchServices") 209 | if not os.path.isdir(toolDirPath): 210 | raise CheckException("tool directory not found", toolDirPath) 211 | 212 | # Check each tool's code signature. 213 | 214 | toolPathList = [] 215 | for toolName in os.listdir(toolDirPath): 216 | if toolName != ".DS_Store": 217 | toolPath = os.path.join(toolDirPath, toolName) 218 | if not os.path.isfile(toolPath): 219 | raise CheckException("tool directory contains a directory", toolPath) 220 | checkCodeSignature(toolPath, "tool") 221 | toolPathList.append(toolPath) 222 | 223 | # Check that we have at least one tool. 224 | 225 | if len(toolPathList) == 0: 226 | raise CheckException("no tools found", toolDirPath) 227 | 228 | return toolPathList 229 | 230 | def checkStep2(appPath, toolPathList): 231 | """Checks the SMPrivilegedExecutables entry in the app's "Info.plist".""" 232 | 233 | # Create a map from the tool name (not path) to its designated requirement. 234 | 235 | toolNameToReqMap = dict() 236 | for toolPath in toolPathList: 237 | req = readDesignatedRequirement(toolPath, "tool") 238 | toolNameToReqMap[os.path.basename(toolPath)] = req 239 | 240 | # Read the Info.plist for the app and extract the SMPrivilegedExecutables value. 241 | 242 | infoPath = os.path.join(appPath, "Contents", "Info.plist") 243 | info = readInfoPlistFromPath(infoPath) 244 | if "SMPrivilegedExecutables" not in info: 245 | raise CheckException("'SMPrivilegedExecutables' not found", infoPath) 246 | infoToolDict = info["SMPrivilegedExecutables"] 247 | if not isinstance(infoToolDict, dict): 248 | raise CheckException("'SMPrivilegedExecutables' must be a dictionary", infoPath) 249 | 250 | # Check that the list of tools matches the list of SMPrivilegedExecutables entries. 251 | 252 | if sorted(infoToolDict.keys()) != sorted(toolNameToReqMap.keys()): 253 | raise CheckException("'SMPrivilegedExecutables' and tools in 'Contents/Library/LaunchServices' don't match") 254 | 255 | # Check that all the requirements match. 256 | 257 | # This is an interesting policy choice. Technically the tool just needs to match 258 | # the requirement listed in SMPrivilegedExecutables, and we can check that by 259 | # putting the requirement into tmp.req and then running 260 | # 261 | # $ codesign -v -R tmp.req /path/to/tool 262 | # 263 | # However, for a Developer ID signed tool we really want to have the SMPrivilegedExecutables 264 | # entry contain the tool's designated requirement because Xcode has built a 265 | # more complex DR that does lots of useful and important checks. So, as a matter 266 | # of policy we require that the value in SMPrivilegedExecutables match the tool's DR. 267 | 268 | for toolName in infoToolDict: 269 | if infoToolDict[toolName] != toolNameToReqMap[toolName]: 270 | raise CheckException("tool designated requirement (%s) doesn't match entry in 'SMPrivilegedExecutables' (%s)" % (toolNameToReqMap[toolName], infoToolDict[toolName])) 271 | 272 | def checkStep3(appPath, toolPathList): 273 | """Checks the "Info.plist" embedded in each helper tool.""" 274 | 275 | # First get the app's designated requirement. 276 | 277 | appReq = readDesignatedRequirement(appPath, "app") 278 | 279 | # Then check that the tool's SMAuthorizedClients value matches it. 280 | 281 | for toolPath in toolPathList: 282 | info = readPlistFromToolSection(toolPath, "__TEXT", "__info_plist") 283 | 284 | if "CFBundleInfoDictionaryVersion" not in info or info["CFBundleInfoDictionaryVersion"] != "6.0": 285 | raise CheckException("'CFBundleInfoDictionaryVersion' in tool __TEXT / __info_plist section must be '6.0'", toolPath) 286 | 287 | if "CFBundleIdentifier" not in info or info["CFBundleIdentifier"] != os.path.basename(toolPath): 288 | raise CheckException("'CFBundleIdentifier' in tool __TEXT / __info_plist section must match tool name", toolPath) 289 | 290 | if "SMAuthorizedClients" not in info: 291 | raise CheckException("'SMAuthorizedClients' in tool __TEXT / __info_plist section not found", toolPath) 292 | infoClientList = info["SMAuthorizedClients"] 293 | if not isinstance(infoClientList, list): 294 | raise CheckException("'SMAuthorizedClients' in tool __TEXT / __info_plist section must be an array", toolPath) 295 | if len(infoClientList) != 1: 296 | raise CheckException("'SMAuthorizedClients' in tool __TEXT / __info_plist section must have one entry", toolPath) 297 | 298 | # Again, as a matter of policy we require that the SMAuthorizedClients entry must 299 | # match exactly the designated requirement of the app. 300 | 301 | if infoClientList[0] != appReq: 302 | raise CheckException("app designated requirement (%s) doesn't match entry in 'SMAuthorizedClients' (%s)" % (appReq, infoClientList[0]), toolPath) 303 | 304 | def checkStep4(appPath, toolPathList): 305 | """Checks the "launchd.plist" embedded in each helper tool.""" 306 | 307 | for toolPath in toolPathList: 308 | launchd = readPlistFromToolSection(toolPath, "__TEXT", "__launchd_plist") 309 | 310 | if "Label" not in launchd or launchd["Label"] != os.path.basename(toolPath): 311 | raise CheckException("'Label' in tool __TEXT / __launchd_plist section must match tool name", toolPath) 312 | 313 | # We don't need to check that the label matches the bundle identifier because 314 | # we know it matches the tool name and step 4 checks that the tool name matches 315 | # the bundle identifier. 316 | 317 | def checkStep5(appPath): 318 | """There's nothing to do here; we effectively checked for this is steps 1 and 2.""" 319 | pass 320 | 321 | def check(appPath): 322 | """Checks the SMJobBless setup of the specified app.""" 323 | 324 | # Each of the following steps matches a bullet point in the SMJobBless header doc. 325 | 326 | toolPathList = checkStep1(appPath) 327 | 328 | checkStep2(appPath, toolPathList) 329 | 330 | checkStep3(appPath, toolPathList) 331 | 332 | checkStep4(appPath, toolPathList) 333 | 334 | checkStep5(appPath) 335 | 336 | def setreq(appPath, appInfoPlistPath, toolInfoPlistPaths): 337 | """ 338 | Reads information from the built app and uses it to set the SMJobBless setup 339 | in the specified app and tool Info.plist source files. 340 | """ 341 | 342 | if not os.path.isdir(appPath): 343 | raise CheckException("app not found", appPath) 344 | 345 | if not os.path.isfile(appInfoPlistPath): 346 | raise CheckException("app 'Info.plist' not found", appInfoPlistPath) 347 | for toolInfoPlistPath in toolInfoPlistPaths: 348 | if not os.path.isfile(toolInfoPlistPath): 349 | raise CheckException("app 'Info.plist' not found", toolInfoPlistPath) 350 | 351 | # Get the designated requirement for the app and each of the tools. 352 | 353 | appReq = readDesignatedRequirement(appPath, "app") 354 | 355 | toolDirPath = os.path.join(appPath, "Contents", "Library", "LaunchServices") 356 | if not os.path.isdir(toolDirPath): 357 | raise CheckException("tool directory not found", toolDirPath) 358 | 359 | toolNameToReqMap = {} 360 | for toolName in os.listdir(toolDirPath): 361 | req = readDesignatedRequirement(os.path.join(toolDirPath, toolName), "tool") 362 | toolNameToReqMap[toolName] = req 363 | 364 | if len(toolNameToReqMap) > len(toolInfoPlistPaths): 365 | raise CheckException("tool directory has more tools (%d) than you've supplied tool 'Info.plist' paths (%d)" % (len(toolNameToReqMap), len(toolInfoPlistPaths)), toolDirPath) 366 | if len(toolNameToReqMap) < len(toolInfoPlistPaths): 367 | raise CheckException("tool directory has fewer tools (%d) than you've supplied tool 'Info.plist' paths (%d)" % (len(toolNameToReqMap), len(toolInfoPlistPaths)), toolDirPath) 368 | 369 | # Build the new value for SMPrivilegedExecutables. 370 | 371 | appToolDict = {} 372 | toolInfoPlistPathToToolInfoMap = {} 373 | for toolInfoPlistPath in toolInfoPlistPaths: 374 | toolInfo = readInfoPlistFromPath(toolInfoPlistPath) 375 | toolInfoPlistPathToToolInfoMap[toolInfoPlistPath] = toolInfo 376 | if "CFBundleIdentifier" not in toolInfo: 377 | raise CheckException("'CFBundleIdentifier' not found", toolInfoPlistPath) 378 | bundleID = toolInfo["CFBundleIdentifier"] 379 | if not isinstance(bundleID, str): 380 | raise CheckException("'CFBundleIdentifier' must be a string", toolInfoPlistPath) 381 | appToolDict[bundleID] = toolNameToReqMap[bundleID] 382 | 383 | # Set the SMPrivilegedExecutables value in the app "Info.plist". 384 | 385 | appInfo = readInfoPlistFromPath(appInfoPlistPath) 386 | needsUpdate = "SMPrivilegedExecutables" not in appInfo 387 | if not needsUpdate: 388 | oldAppToolDict = appInfo["SMPrivilegedExecutables"] 389 | if not isinstance(oldAppToolDict, dict): 390 | raise CheckException("'SMPrivilegedExecutables' must be a dictionary", appInfoPlistPath) 391 | appToolDictSorted = sorted(appToolDict.items(), key=operator.itemgetter(0)) 392 | oldAppToolDictSorted = sorted(oldAppToolDict.items(), key=operator.itemgetter(0)) 393 | needsUpdate = (appToolDictSorted != oldAppToolDictSorted) 394 | 395 | if needsUpdate: 396 | appInfo["SMPrivilegedExecutables"] = appToolDict 397 | with open(appInfoPlistPath, 'wb') as fp: 398 | plistlib.dump(appInfo, fp) 399 | print ("%s: updated" % appInfoPlistPath, file = sys.stdout) 400 | 401 | # Set the SMAuthorizedClients value in each tool's "Info.plist". 402 | 403 | toolAppListSorted = [ appReq ] # only one element, so obviously sorted (-: 404 | for toolInfoPlistPath in toolInfoPlistPaths: 405 | toolInfo = toolInfoPlistPathToToolInfoMap[toolInfoPlistPath] 406 | 407 | needsUpdate = "SMAuthorizedClients" not in toolInfo 408 | if not needsUpdate: 409 | oldToolAppList = toolInfo["SMAuthorizedClients"] 410 | if not isinstance(oldToolAppList, list): 411 | raise CheckException("'SMAuthorizedClients' must be an array", toolInfoPlistPath) 412 | oldToolAppListSorted = sorted(oldToolAppList) 413 | needsUpdate = (toolAppListSorted != oldToolAppListSorted) 414 | 415 | if needsUpdate: 416 | toolInfo["SMAuthorizedClients"] = toolAppListSorted 417 | with open(toolInfoPlistPath, 'wb') as f: 418 | plistlib.dump(toolInfo, f) 419 | print("%s: updated" % toolInfoPlistPath, file = sys.stdout) 420 | 421 | def main(): 422 | options, appArgs = getopt.getopt(sys.argv[1:], "d") 423 | 424 | debug = False 425 | for opt, val in options: 426 | if opt == "-d": 427 | debug = True 428 | else: 429 | raise UsageException() 430 | 431 | if len(appArgs) == 0: 432 | raise UsageException() 433 | command = appArgs[0] 434 | if command == "check": 435 | if len(appArgs) != 2: 436 | raise UsageException() 437 | check(appArgs[1]) 438 | elif command == "setreq": 439 | if len(appArgs) < 4: 440 | raise UsageException() 441 | setreq(appArgs[1], appArgs[2], appArgs[3:]) 442 | else: 443 | raise UsageException() 444 | 445 | if __name__ == "__main__": 446 | try: 447 | main() 448 | except CheckException as e: 449 | if e.path is None: 450 | print("%s: %s" % (os.path.basename(sys.argv[0]), e.message), file = sys.stderr) 451 | else: 452 | path = e.path 453 | if path.endswith("/"): 454 | path = path[:-1] 455 | print("%s: %s" % (path, e.message), file = sys.stderr) 456 | sys.exit(1) 457 | except UsageException as e: 458 | print("usage: %s check /path/to/app" % os.path.basename(sys.argv[0]), file = sys.stderr) 459 | print(" %s setreq /path/to/app /path/to/app/Info.plist /path/to/tool/Info.plist..." % os.path.basename(sys.argv[0]), file = sys.stderr) 460 | sys.exit(1) 461 | -------------------------------------------------------------------------------- /TmpDiskTests/TmpDiskTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TmpDiskTests.swift 3 | // TmpDiskTests 4 | // 5 | // Created by Tim on 12/11/21. 6 | // 7 | 8 | import XCTest 9 | @testable import TmpDisk 10 | 11 | class TmpDiskTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | TmpDiskManager.shared.ejectAllTmpDisks(recreate: false) 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | TmpDiskManager.shared.ejectAllTmpDisks(recreate: false) 19 | } 20 | 21 | func testCreateTmpDiskNoName() throws { 22 | let volume = TmpDiskVolume() 23 | TmpDiskManager.shared.createTmpDisk(volume: volume) {error in 24 | XCTAssertNotNil(error) 25 | XCTAssertEqual(error!, TmpDiskError.noName) 26 | } 27 | } 28 | 29 | // MARK: - TmpDisk 30 | 31 | func testCreateTmpDiskSucceedsAndEjects() throws { 32 | let volume = TmpDiskVolume(name: "testvolume") 33 | let expectation = self.expectation(description: "Creating") 34 | TmpDiskManager.shared.createTmpDisk(volume: volume) {error in 35 | XCTAssertNil(error) 36 | XCTAssertTrue(TmpDiskManager.shared.exists(volume: volume), "Volume should exist") 37 | 38 | TmpDiskManager.shared.ejectTmpDisksWithName(names: [volume.name], recreate: false) 39 | XCTAssertFalse(TmpDiskManager.shared.exists(volume: volume), "Volume should not exist") 40 | expectation.fulfill() 41 | } 42 | waitForExpectations(timeout: 5, handler: nil) 43 | } 44 | 45 | func testCreateFolders() throws { 46 | let volume = TmpDiskVolume(name: "testwithfolders", folders: ["folder1", "folder2/subfolder1"]) 47 | let expectation = self.expectation(description: "Creating") 48 | 49 | TmpDiskManager.shared.createTmpDisk(volume: volume) {error in 50 | XCTAssertNil(error) 51 | XCTAssertTrue(TmpDiskManager.shared.exists(volume: volume), "Volume should exist") 52 | 53 | XCTAssertTrue(FileManager.default.fileExists(atPath: "\(volume.path())/folder1"), "Volume should exist") 54 | XCTAssertTrue(FileManager.default.fileExists(atPath: "\(volume.path())/folder2/subfolder1"), "Volume should exist") 55 | expectation.fulfill() 56 | } 57 | waitForExpectations(timeout: 5, handler: nil) 58 | } 59 | 60 | func testCreateExists() throws { 61 | let volume = TmpDiskVolume(name: "testexists") 62 | let expectation = self.expectation(description: "Creating") 63 | 64 | TmpDiskManager.shared.createTmpDisk(volume: volume) {error in 65 | XCTAssertNil(error) 66 | XCTAssertTrue(TmpDiskManager.shared.exists(volume: volume), "Volume should exist") 67 | 68 | TmpDiskManager.shared.createTmpDisk(volume: volume) {error in 69 | XCTAssertNotNil(error) 70 | XCTAssertEqual(error!, TmpDiskError.exists) 71 | expectation.fulfill() 72 | } 73 | } 74 | waitForExpectations(timeout: 5, handler: nil) 75 | } 76 | 77 | // MARK: - Tmpfs 78 | 79 | func testCreateTmpfsSucceedsAndEjects() throws { 80 | let volume = TmpDiskVolume(name: "testtmpfsvolume", tmpFs: true) 81 | let expectation = self.expectation(description: "Creating") 82 | 83 | TmpDiskManager.shared.createTmpDisk(volume: volume) {error in 84 | XCTAssertNil(error) 85 | XCTAssertTrue(TmpDiskManager.shared.exists(volume: volume), "Volume should exist") 86 | 87 | TmpDiskManager.shared.ejectTmpDisksWithName(names: [volume.name], recreate: false) 88 | XCTAssertFalse(TmpDiskManager.shared.exists(volume: volume), "Volume should not exist") 89 | expectation.fulfill() 90 | } 91 | waitForExpectations(timeout: 25, handler: nil) 92 | } 93 | 94 | func testCreateTmpfsFolders() throws { 95 | let volume = TmpDiskVolume(name: "testwithfolders", tmpFs: true, folders: ["folder1", "folder2/subfolder1"]) 96 | let expectation = self.expectation(description: "Creating") 97 | 98 | TmpDiskManager.shared.createTmpDisk(volume: volume) {error in 99 | XCTAssertNil(error) 100 | XCTAssertTrue(TmpDiskManager.shared.exists(volume: volume), "Volume should exist") 101 | 102 | XCTAssertTrue(FileManager.default.fileExists(atPath: "\(volume.path())/folder1"), "Volume should exist") 103 | XCTAssertTrue(FileManager.default.fileExists(atPath: "\(volume.path())/folder2/subfolder1"), "Volume should exist") 104 | expectation.fulfill() 105 | } 106 | waitForExpectations(timeout: 5, handler: nil) 107 | } 108 | 109 | func testCreateTmpfsExists() throws { 110 | let volume = TmpDiskVolume(name: "testexists", tmpFs: true) 111 | let expectation = self.expectation(description: "Creating") 112 | 113 | TmpDiskManager.shared.createTmpDisk(volume: volume) {error in 114 | XCTAssertNil(error) 115 | XCTAssertTrue(TmpDiskManager.shared.exists(volume: volume), "Volume should exist") 116 | 117 | TmpDiskManager.shared.createTmpDisk(volume: volume) {error in 118 | XCTAssertNotNil(error) 119 | XCTAssertEqual(error!, TmpDiskError.exists) 120 | expectation.fulfill() 121 | } 122 | } 123 | waitForExpectations(timeout: 5, handler: nil) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /TmpDiskUITests/TmpDiskUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TmpDiskUITests.swift 3 | // TmpDiskUITests 4 | // 5 | // Created by Tim on 12/11/21. 6 | // 7 | 8 | import XCTest 9 | 10 | class TmpDiskUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TmpDiskUITests/TmpDiskUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TmpDiskUITestsLaunchTests.swift 3 | // TmpDiskUITests 4 | // 5 | // Created by Tim on 12/11/21. 6 | // 7 | 8 | import XCTest 9 | 10 | class TmpDiskUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /appdmg.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "TmpDisk", 3 | "icon": "TmpDisk/icon.icns", 4 | "background": "dmgbg.png", 5 | "icon-size": 80, 6 | "contents": [ 7 | { "x": 480, "y": 244, "type": "link", "path": "/Applications" }, 8 | { "x": 160, "y": 244, "type": "file", "path": "TmpDisk.app" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /com.imothee.TmpDiskHelper/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.imothee.TmpDiskHelper 7 | CFBundleInfoDictionaryVersion 8 | 6.0 9 | SMAuthorizedClients 10 | 11 | anchor apple generic and identifier com.imothee.TmpDisk and certificate leaf[subject.OU] = AGZ3AP53DM 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /com.imothee.TmpDiskHelper/NSXPCConnection+AuditToken.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSXPCConnection+AuditToken.h 3 | // TmpDisk 4 | // 5 | // Created by Tim on 2/28/24. 6 | // 7 | 8 | @import Foundation; 9 | 10 | @interface NSXPCConnection (AuditToken) 11 | 12 | // Apple uses this property internally to verify XPC connections. 13 | // There is no safe pulicly available alternative (check by client pid, for example, is racy) 14 | @property (nonatomic, readonly) audit_token_t auditToken; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /com.imothee.TmpDiskHelper/TmpDiskCreator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TmpDiskCreator.swift 3 | // TmpDiskHelper 4 | // 5 | // Created by Tim on 2/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | class TmpDiskCreatorImpl: NSObject, TmpDiskCreator { 11 | 12 | func createTmpDisk(_ command: String, onCreate: @escaping (Bool) -> Void) { 13 | NSLog("[SMJBS]: \(#function)") 14 | 15 | let task = Process() 16 | task.launchPath = "/bin/sh" 17 | task.arguments = ["-c", command] 18 | task.launch() 19 | task.waitUntilExit() 20 | let created = (task.terminationStatus == 0) 21 | 22 | onCreate(created) 23 | } 24 | 25 | func uninstall() { 26 | try? FileManager.default.removeItem(atPath: "/Library/PrivilegedHelperTools/com.imothee.TmpDiskHelper") 27 | try? FileManager.default.removeItem(atPath: "/Library/LaunchDaemons/com.imothee.TmpDiskHelper.plist") 28 | 29 | let task = Process() 30 | task.launchPath = "/bin/launchctl" 31 | task.arguments = ["bootout", "system/com.imothee.TmpDiskHelper"] 32 | task.waitUntilExit() 33 | try? task.run() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /com.imothee.TmpDiskHelper/XPCServer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XPCServer.swift 3 | // TmpDiskHelper 4 | // 5 | // Created by Tim on 2/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | class XPCServer: NSObject { 11 | 12 | internal static let shared = XPCServer() 13 | 14 | private var listener: NSXPCListener? 15 | 16 | internal func start() { 17 | listener = NSXPCListener(machServiceName: Constant.helperMachLabel) 18 | listener?.delegate = self 19 | listener?.resume() 20 | } 21 | 22 | private func connetionInterruptionHandler() { 23 | NSLog("[SMJBS]: \(#function)") 24 | } 25 | 26 | private func connectionInvalidationHandler() { 27 | NSLog("[SMJBS]: \(#function)") 28 | } 29 | 30 | private func isValidClient(forConnection connection: NSXPCConnection) -> Bool { 31 | 32 | var token = connection.auditToken; 33 | let tokenData = Data(bytes: &token, count: MemoryLayout.size(ofValue:token)) 34 | let attributes = [kSecGuestAttributeAudit : tokenData] 35 | 36 | // Check which flags you need 37 | let flags: SecCSFlags = [] 38 | var code: SecCode? = nil 39 | var status = SecCodeCopyGuestWithAttributes(nil, attributes as CFDictionary, flags, &code) 40 | 41 | if status != errSecSuccess { 42 | return false 43 | } 44 | 45 | guard let dynamicCode = code else { 46 | return false 47 | } 48 | 49 | let authedClients = Bundle.main.infoDictionary?["SMAuthorizedClients"] as? [String?] 50 | guard let entitlements = authedClients?[0] else { 51 | return false 52 | } 53 | 54 | var requirement: SecRequirement? 55 | 56 | status = SecRequirementCreateWithString(entitlements as CFString, flags, &requirement) 57 | 58 | if status != errSecSuccess { 59 | return false 60 | } 61 | 62 | status = SecCodeCheckValidity(dynamicCode, flags, requirement) 63 | 64 | return status == errSecSuccess 65 | } 66 | } 67 | 68 | extension XPCServer: NSXPCListenerDelegate { 69 | 70 | func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { 71 | NSLog("[SMJBS]: \(#function)") 72 | 73 | if (!isValidClient(forConnection: newConnection)) { 74 | NSLog("[SMJBS]: Client is not valid") 75 | return false 76 | } 77 | 78 | NSLog("[SMJBS]: Client is valid") 79 | 80 | let creator = TmpDiskCreatorImpl() 81 | 82 | newConnection.exportedInterface = NSXPCInterface(with: TmpDiskCreator.self) 83 | newConnection.exportedObject = creator 84 | 85 | newConnection.interruptionHandler = connetionInterruptionHandler 86 | newConnection.invalidationHandler = connectionInvalidationHandler 87 | 88 | newConnection.resume() 89 | 90 | return true 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /com.imothee.TmpDiskHelper/com.imothee.TmpDiskHelper-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // com.imothee.TmpDiskHelper-Bridging-Header.h 3 | // TmpDisk 4 | // 5 | // Created by Tim on 2/28/24. 6 | // 7 | 8 | #import "NSXPCConnection+AuditToken.h" 9 | -------------------------------------------------------------------------------- /com.imothee.TmpDiskHelper/launchd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /com.imothee.TmpDiskHelper/launchd.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Build 6 | 4 7 | Label 8 | com.imothee.TmpDiskHelper 9 | MachServices 10 | 11 | com.imothee.TmpDiskHelper 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /com.imothee.TmpDiskHelper/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // TmpDiskHelper 4 | // 5 | // Created by Tim on 2/27/24. 6 | // 7 | 8 | import Foundation 9 | 10 | NSLog("[SMJBS]: Privileged TmpDiskHelper has started") 11 | 12 | XPCServer.shared.start() 13 | 14 | CFRunLoopRun() 15 | -------------------------------------------------------------------------------- /dmgbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imothee/tmpdisk/c5db4f3f8567beaa5eac4a477c86a5591658ebf0/dmgbg.png -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Ensure the build directory exists 4 | [ ! -d "./build" ] && mkdir "./build" 5 | 6 | # Ensure we have a TmpDisk.app in the folder 7 | if [ ! -d "./TmpDisk.app" ]; then 8 | echo "Error: TmpDisk.app not found in the current directory" 9 | exit 1 10 | fi 11 | 12 | # Package the TmpDisk.app into the dmg 13 | npx appdmg@latest ./appdmg.json ./build/TmpDisk.dmg 14 | 15 | # Find the sparkle-project folder 16 | sparkle=$(find ~/Library/Developer/Xcode/DerivedData -type d -maxdepth 1 -print | grep -m1 'TmpDisk-') 17 | 18 | # Sign the dmg 19 | "$sparkle"/SourcePackages/artifacts/sparkle/bin/generate_appcast -f ./keys/dsa_priv.pem ./build 20 | --------------------------------------------------------------------------------