├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .swiftlint.yml ├── .travis.yml ├── CHANGELOG.md ├── Cartfile ├── Cartfile.private ├── Cartfile.resolved ├── LICENSE ├── Makefile ├── Podfile ├── Podfile.lock ├── README.md ├── TySimulator.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── TySimulator.xcscheme ├── TySimulator.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── TySimulator ├── ActivationWindow.xib ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-128.png │ │ ├── Icon-128@2x.png │ │ ├── Icon-16.png │ │ ├── Icon-16@2x.png │ │ ├── Icon-256.png │ │ ├── Icon-256@2x.png │ │ ├── Icon-32.png │ │ ├── Icon-32@2x.png │ │ ├── Icon-512.png │ │ └── Icon-512@2x.png │ ├── Contents.json │ ├── MenuIcon.imageset │ │ ├── Contents.json │ │ ├── Icon-18.png │ │ ├── Icon-18@2x.png │ │ └── Icon-18@3x.png │ ├── empty.imageset │ │ ├── Contents.json │ │ ├── empty@2x.png │ │ └── empty@3x.png │ ├── finder.imageset │ │ ├── Contents.json │ │ ├── finder@2x.png │ │ └── finder@3x.png │ ├── icon-off.imageset │ │ ├── Contents.json │ │ ├── icon-off@2x.png │ │ └── icon-off@3x.png │ ├── icon-on.imageset │ │ ├── Contents.json │ │ ├── icon-on@2x.png │ │ └── icon-on@3x.png │ ├── settings.imageset │ │ ├── Contents.json │ │ ├── settings@2x.png │ │ └── settings@3x.png │ └── tmp-logo.imageset │ │ ├── Contents.json │ │ ├── tmp-logo@2x.png │ │ └── tmp-logo@3x.png ├── Base.lproj │ └── Localizable.strings ├── BaseDetailCollectionItem.xib ├── CommandTransformer.swift ├── DirectoryWatcher.swift ├── Info.plist ├── LRU-K │ ├── LRU.swift │ ├── LRUCache.swift │ └── LRUK.swift ├── MainMenu.storyboard ├── MainMenuController.swift ├── MainViewController.swift ├── MainViewController.xib ├── Model │ ├── AppGroupModel.swift │ ├── ApplicationModel.swift │ ├── CommandModel.swift │ ├── DeviceModel.swift │ └── MediaModel.swift ├── Preferences │ ├── CommandViewController.swift │ ├── CommandViewController.xib │ ├── GeneralPreferencesViewController.swift │ ├── GeneralPreferencesViewController.xib │ ├── KeyBindingsPreferencesViewController.swift │ ├── KeyBindingsPreferencesViewController.xib │ ├── Preference.swift │ └── ShortcutTableCellView.swift ├── Resource │ ├── dsa_pub.pem │ └── tmp-logo.png ├── Script.swift ├── Simulator.swift ├── TySimulator-Bridging-Header.h ├── TySimulator.entitlements ├── Utils │ ├── Application.swift │ ├── FileManager.swift │ ├── MD5.swift │ ├── Process.swift │ ├── ShortcutMonitor.swift │ ├── TextView.swift │ ├── URL.swift │ └── ViewController.swift ├── main.swift ├── zh-Hans.lproj │ └── Localizable.strings └── zh-Hant.lproj │ └── Localizable.strings ├── TySimulatorTests ├── Info.plist ├── RecentTests.swift ├── Resource │ └── script-format.txt ├── ScriptTests.swift ├── SimulatorTests.swift ├── TestFinder │ ├── Empty │ ├── SubFinder │ │ ├── Empty │ │ └── SubSubFinder │ │ │ └── .gitkeep │ └── SubFinder2 │ │ └── .gitkeep └── UtilTests.swift ├── codecov.yml ├── resources ├── DMG Canvas.dmgCanvas │ ├── Disk Image │ └── QuickLook │ │ └── Preview.jpg ├── dmg-background.png ├── icon-off.png ├── icon-on.png ├── logo.png ├── menu-icon.png ├── tysimulator-logo.png └── tysimulator-logo.psd └── scripts ├── ExportOptions.plist ├── build ├── export_archive ├── generate_app_icon ├── generate_menu_icon ├── i18n └── pack /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: macOS-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Soft Env 13 | run: | 14 | carthage version 15 | pod --version 16 | xcodebuild -version 17 | xcpretty -v 18 | - name: Install Dependencies 19 | run: | 20 | carthage bootstrap --platform osx 21 | pod install --repo-update 22 | - name: Test 23 | run: xcodebuild -workspace TySimulator.xcworkspace -scheme TySimulator -sdk macosx ONLY_ACTIVE_ARCH=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -enableCodeCoverage YES test 24 | - uses: codecov/codecov-action@v1.0.3 25 | with: 26 | token: ${{secrets.CODECOV_TOKEN}} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 35 | # 36 | Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | Carthage/Checkouts 42 | 43 | Carthage/Build 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 51 | 52 | fastlane/report.xml 53 | fastlane/Preview.html 54 | fastlane/screenshots 55 | fastlane/test_output 56 | 57 | # Code Injection 58 | # 59 | # After new code Injection tools there's a generated folder /iOSInjectionProject 60 | # https://github.com/johnno1962/injectionforxcode 61 | 62 | iOSInjectionProject/ 63 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - trailing_whitespace 3 | - force_cast 4 | - force_try 5 | # - colon 6 | # - comma 7 | # - control_statement 8 | # opt_in_rules: # some rules are only opt-in 9 | # - empty_count 10 | # Find all the available rules by running: 11 | # swiftlint rules 12 | included: # paths to include during linting. `--path` is ignored if present. 13 | - TySimulator 14 | excluded: # paths to ignore during linting. Takes precedence over `included`. 15 | - Carthage 16 | - Pods 17 | # - Source/ExcludedFolder 18 | # - Source/ExcludedFile.swift 19 | 20 | # configurable rules can be customized from this configuration file 21 | # binary rules can set their severity level 22 | # force_cast: warning # implicitly 23 | # force_try: 24 | # severity: warning # explicitly 25 | # rules that have both warning and error levels, can set just the warning level 26 | # implicitly 27 | line_length: 500 28 | # they can set both implicitly with an array 29 | # type_body_length: 30 | # - 300 # warning 31 | # - 400 # error 32 | # or they can set both explicitly 33 | file_length: 34 | warning: 500 35 | error: 1200 36 | # naming rules can set warnings/errors for min_length and max_length 37 | # additionally they can set excluded names 38 | type_name: 39 | min_length: 3 40 | max_length: 41 | warning: 40 42 | error: 50 43 | excluded: 44 | - OS 45 | identifier_name: 46 | # min_length: # only min_length 47 | # error: 4 # only error 48 | excluded: 49 | - id 50 | - os 51 | - kq 52 | - _defaults 53 | - _defaultsArray 54 | # - GlobalAPIKey 55 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji) 56 | 57 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode11.2 3 | cache: 4 | cocoapods: true 5 | before_install: 6 | - gem install cocoapods 7 | - brew update 8 | - brew outdated carthage || brew upgrade carthage 9 | before_script: 10 | - carthage bootstrap --platform osx 11 | - pod install --repo-update 12 | script: 13 | - xcodebuild -workspace TySimulator.xcworkspace -scheme TySimulator -sdk macosx ONLY_ACTIVE_ARCH=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -enableCodeCoverage YES test 14 | after_success: 15 | - bash <(curl -s https://codecov.io/bash) -cF osx 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.6.0 2 | === 3 | 去除 License, TySimulator 定为免费 4 | 5 | 0.7.0 6 | === 7 | #### Enhancement 8 | 1. 使用新的logo 9 | 2. 添加 i18n, 支持 简繁英 三种语言 10 | 3. 没有icon的App, 将显示默认的空logo 11 | 12 | 0.7.1 13 | === 14 | #### Fix Bug 15 | 1. [#1 在Mac上使用暗色菜单栏时, 看不到图标](https://github.com/luckytianyiyan/TySimulator/issues/1) 16 | 2. 自定义脚本解析错误 17 | 18 | 0.8.0 19 | === 20 | #### New Feature 21 | 1. 新增"最近使用的App" 22 | 23 | #### Enhancement 24 | 1. App菜单项的样式 25 | 26 | 0.8.1 27 | === 28 | #### Fix Bug 29 | 1. Dock Icon 显示错误 30 | 31 | 0.9.0 32 | === 33 | 最低支持 macOS 10.11 34 | 35 | #### New Feature 36 | 1. 支持 Mojave 37 | 2. 新的UI 38 | 39 | 0.10.0 40 | === 41 | 最低支持 macOS 10.12 42 | 43 | #### New Feature 44 | 1. 支持 Catalina 45 | 2. 支持 XCode 11 46 | 47 | #### Fix Bug 48 | 1. 菜单栏中, "设置"无法点击 -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "SwiftyBeaver/SwiftyBeaver" 2 | github "radex/SwiftyUserDefaults" 3 | github "SwiftyJSON/SwiftyJSON" 4 | github "SnapKit/SnapKit" 5 | -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "Quick/Quick" 2 | github "Quick/Nimble" -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" "v8.0.4" 2 | github "Quick/Quick" "v2.2.0" 3 | github "SnapKit/SnapKit" "5.0.1" 4 | github "SwiftyBeaver/SwiftyBeaver" "1.8.3" 5 | github "SwiftyJSON/SwiftyJSON" "4.2.0" 6 | github "radex/SwiftyUserDefaults" "4.0.0" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 luckytianyiyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help build 2 | 3 | help: ## show this help message and exit 4 | @echo "usage: make [target]" 5 | @echo 6 | @echo "targets:" 7 | @egrep '^(.+)\:\ ##\ (.+)' ${MAKEFILE_LIST} | column -t -c 2 -s ':#' 8 | 9 | icon: ## generate app icons 10 | @scripts/generate_app_icon resources/logo.png TySimulator/Assets.xcassets/AppIcon.appiconset 11 | 12 | menu-icon: ## generate menu icons 13 | @scripts/generate_menu_icon resources/menu-icon.png TySimulator/Assets.xcassets/MenuIcon.imageset 14 | 15 | bootstrap: 16 | @pod install 17 | @carthage bootstrap --platform osx 18 | 19 | build: ## build TySimulator 20 | @./scripts/build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO 21 | @open build 22 | 23 | archive: ## archive TySimulator 24 | @rm -rf build 25 | @./scripts/build 26 | @./scripts/export_archive 27 | @open build 28 | 29 | clean: ## clean 30 | @rm -rf build 31 | 32 | pack: 33 | @./scripts/pack 34 | 35 | i18n: 36 | @./scripts/i18n 37 | 38 | %: 39 | @: 40 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :osx, '10.10' 3 | 4 | target 'TySimulator' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for TySimulator 9 | pod 'MASShortcut' 10 | pod 'MASPreferences' 11 | pod 'DevMateKit' 12 | pod 'ACEViewSwift', :git => 'https://github.com/ty0x2333/ACEViewSwift.git', :branch => 'fix_jsCall_error', :submodules => true 13 | pod 'Fabric' 14 | pod 'Crashlytics' 15 | 16 | target 'TySimulatorTests' do 17 | inherit! :search_paths 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - ACEViewSwift (2.1.0) 3 | - Crashlytics (3.14.0): 4 | - Fabric (~> 1.10.2) 5 | - DevMateKit (1.9.3) 6 | - Fabric (1.10.2) 7 | - MASPreferences (1.3) 8 | - MASShortcut (2.4.0) 9 | 10 | DEPENDENCIES: 11 | - ACEViewSwift (from `https://github.com/ty0x2333/ACEViewSwift.git`, branch `fix_jsCall_error`) 12 | - Crashlytics 13 | - DevMateKit 14 | - Fabric 15 | - MASPreferences 16 | - MASShortcut 17 | 18 | SPEC REPOS: 19 | trunk: 20 | - Crashlytics 21 | - DevMateKit 22 | - Fabric 23 | - MASPreferences 24 | - MASShortcut 25 | 26 | EXTERNAL SOURCES: 27 | ACEViewSwift: 28 | :branch: fix_jsCall_error 29 | :git: https://github.com/ty0x2333/ACEViewSwift.git 30 | :submodules: true 31 | 32 | CHECKOUT OPTIONS: 33 | ACEViewSwift: 34 | :commit: a126c3c9185103e9fbb3ae011cdfadbb137f78e3 35 | :git: https://github.com/ty0x2333/ACEViewSwift.git 36 | :submodules: true 37 | 38 | SPEC CHECKSUMS: 39 | ACEViewSwift: e9488695c68807a24a872296cb9dba40bd444694 40 | Crashlytics: 540b7e5f5da5a042647227a5e3ac51d85eed06df 41 | DevMateKit: a7a46f498d4c549125109d826e858a49d508cbb8 42 | Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74 43 | MASPreferences: c08b8622dd17b47da87669e741efd7c92e970e8c 44 | MASShortcut: d9e4909e878661cc42877cc9d6efbe638273ab57 45 | 46 | PODFILE CHECKSUM: 8308debeef5349374b5b7b3aea075efaba8cf466 47 | 48 | COCOAPODS: 1.8.4 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | TySimulator 3 |

4 | 5 | [![Build Status](https://travis-ci.org/ty0x2333/TySimulator.svg?branch=master)](https://travis-ci.org/ty0x2333/TySimulator) 6 | [![Financial Contributors on Open Collective](https://opencollective.com/TySimulator/all/badge.svg?label=financial+contributors)](https://opencollective.com/TySimulator) [![codecov](https://codecov.io/gh/ty0x2333/TySimulator/branch/master/graph/badge.svg?token=m2rZatAaPl)](https://codecov.io/gh/ty0x2333/TySimulator) 7 | [![codebeat badge](https://codebeat.co/badges/fa3da73d-90d2-4614-be71-4a2b5f21771c)](https://codebeat.co/projects/github-com-ty0x2333-tysimulator-master) 8 | [![GitHub release](https://img.shields.io/github/release/ty0x2333/TySimulator.svg)]() 9 | [![Swift Version](https://img.shields.io/badge/swift-4.2-orange.svg)]() 10 | 11 | --- 12 | 13 | Website: [https://tysimulator.com](https://tysimulator.com) 14 | 15 | Features 16 | === 17 | - Quick access to app's **Documents** directory 18 | - Quick access to simulator's **Media** directory 19 | - Manipulating current running or specified simulator 20 | - Customized command 21 | - Global hotkey 22 | - Recent apps 23 | 24 | Prerequisites 25 | === 26 | - macOS 10.12 or later 27 | - XCode 10 or later 28 | 29 | Installation 30 | === 31 | Install TySimulator from Website 32 | [https://tysimulator.com](https://tysimulator.com) 33 | 34 | or via [homebrew cask](https://github.com/caskroom/homebrew-cask) 35 | 36 | ```shell 37 | $ brew update 38 | $ brew cask install tysimulator 39 | ``` 40 | 41 | Building 42 | === 43 | 44 | Requirements 45 | --- 46 | - XCode 11.2 or later 47 | - [CocoaPods](https://github.com/CocoaPods/CocoaPods) 48 | - [Carthage](https://github.com/Carthage/Carthage) 49 | 50 | Optionals 51 | --- 52 | - [xcpretty](https://github.com/supermarin/xcpretty) 0.3.0 or later 53 | 54 | Instructions 55 | --- 56 | ```shell 57 | $ git clone https://github.com/ty0x2333/TySimulator.git 58 | $ cd TySimulator 59 | $ make bootstrap 60 | $ make build 61 | ``` 62 | 63 | Inspiration and UX Reference 64 | --- 65 | TySimulator is inspired by [Simulator](https://github.com/hyperoslo/Simulator), [SimPholders](https://simpholders.com), [macdown](https://github.com/MacDownApp/macdown), but with an independent legal copyright. If you don't like the implementation, please consider [Simulator](https://github.com/hyperoslo/Simulator), [SimPholders](https://simpholders.com). 66 | 67 | License 68 | === 69 | 70 | **TySimulator** is available under the MIT license. See the LICENSE file for more info. 71 | 72 | ## Contributors 73 | 74 | ### Code Contributors 75 | 76 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. 77 | 78 | 79 | ### Financial Contributors 80 | 81 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/TySimulator/contribute)] 82 | 83 | #### Individuals 84 | 85 | 86 | 87 | #### Organizations 88 | 89 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/TySimulator/contribute)] 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /TySimulator.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TySimulator.xcodeproj/xcshareddata/xcschemes/TySimulator.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 78 | 79 | 80 | 81 | 87 | 89 | 95 | 96 | 97 | 98 | 100 | 101 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /TySimulator.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TySimulator.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TySimulator.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Original 7 | PreviewsEnabled 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TySimulator/ActivationWindow.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /TySimulator/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/13. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Fabric 11 | import Crashlytics 12 | 13 | class AppDelegate: NSObject, NSApplicationDelegate, DevMateKitDelegate { 14 | @IBOutlet weak var mainMenuController: MainMenuController! 15 | 16 | func applicationDidFinishLaunching(_ aNotification: Notification) { 17 | NSViewController.awake() 18 | NSTextView.awake() 19 | 20 | Fabric.with([Crashlytics.self]) 21 | #if DEBUG 22 | Fabric.sharedSDK().debug = true 23 | #endif 24 | UserDefaults.standard.register(defaults: ["NSApplicationCrashOnExceptions": true]) 25 | 26 | NSApplication.toggleDockIcon(showIcon: false) 27 | DevMateKit.sendTrackingReport(nil, delegate: nil) 28 | DM_SUUpdater.shared().delegate = self 29 | 30 | DispatchQueue.global().async { 31 | Simulator.shared.updateDeivces() 32 | } 33 | } 34 | 35 | func applicationWillTerminate(_ aNotification: Notification) { 36 | LRUCache.shared.save() 37 | } 38 | 39 | // MARK: SUUpdaterDelegate_DevMateInteraction 40 | 41 | public func updaterDidNotFindUpdate(_ updater: DM_SUUpdater) { 42 | log.warning("not found update: \(updater)") 43 | } 44 | 45 | @nonobjc public func updaterShouldCheck(forBetaUpdates updater: DM_SUUpdater) -> Bool { 46 | #if DEBUG 47 | return true 48 | #else 49 | return false 50 | #endif 51 | } 52 | 53 | @objc public func isUpdater(inTestMode updater: DM_SUUpdater) -> Bool { 54 | #if DEBUG 55 | return true 56 | #else 57 | return false 58 | #endif 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "Icon-16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "Icon-16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "Icon-32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "Icon-32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "Icon-128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "Icon-128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "Icon-256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "Icon-256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "Icon-512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "Icon-512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-128.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-128@2x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-16.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-16@2x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-256.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-256@2x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-32.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-32@2x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-512.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/AppIcon.appiconset/Icon-512@2x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/MenuIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Icon-18.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Icon-18@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Icon-18@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/MenuIcon.imageset/Icon-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/MenuIcon.imageset/Icon-18.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/MenuIcon.imageset/Icon-18@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/MenuIcon.imageset/Icon-18@2x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/MenuIcon.imageset/Icon-18@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/MenuIcon.imageset/Icon-18@3x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/empty.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "empty@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "empty@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/empty.imageset/empty@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/empty.imageset/empty@2x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/empty.imageset/empty@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/empty.imageset/empty@3x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/finder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "finder@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "finder@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/finder.imageset/finder@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/finder.imageset/finder@2x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/finder.imageset/finder@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/finder.imageset/finder@3x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/icon-off.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icon-off@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "icon-off@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/icon-off.imageset/icon-off@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/icon-off.imageset/icon-off@2x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/icon-off.imageset/icon-off@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/icon-off.imageset/icon-off@3x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/icon-on.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icon-on@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "icon-on@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/icon-on.imageset/icon-on@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/icon-on.imageset/icon-on@2x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/icon-on.imageset/icon-on@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/icon-on.imageset/icon-on@3x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/settings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "settings@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "settings@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/settings.imageset/settings@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/settings.imageset/settings@2x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/settings.imageset/settings@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/settings.imageset/settings@3x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/tmp-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "tmp-logo@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "tmp-logo@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/tmp-logo.imageset/tmp-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/tmp-logo.imageset/tmp-logo@2x.png -------------------------------------------------------------------------------- /TySimulator/Assets.xcassets/tmp-logo.imageset/tmp-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Assets.xcassets/tmp-logo.imageset/tmp-logo@3x.png -------------------------------------------------------------------------------- /TySimulator/Base.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Base.lproj/Localizable.strings -------------------------------------------------------------------------------- /TySimulator/BaseDetailCollectionItem.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /TySimulator/CommandTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandTransformer.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/24. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MASShortcut 11 | 12 | class CommandTransformer: ValueTransformer { 13 | 14 | override class func allowsReverseTransformation() -> Bool { 15 | return true 16 | } 17 | 18 | override func reverseTransformedValue(_ value: Any?) -> Any? { 19 | let transformer = MASDictionaryTransformer() 20 | guard let value = value as? CommandModel, 21 | let shortcut = transformer.reverseTransformedValue(value.key) else { 22 | return [String: Any]() 23 | } 24 | 25 | return ["id": value.id, "name": value.name, "script": value.script, "shortcut": shortcut] 26 | } 27 | 28 | override func transformedValue(_ value: Any?) -> Any? { 29 | guard let value = value as? [String: Any] else { 30 | return nil 31 | } 32 | 33 | let transformer = MASDictionaryTransformer() 34 | var command: CommandModel 35 | if let id = value["id"] as? String { 36 | command = CommandModel(id: id) 37 | } else { 38 | command = CommandModel() 39 | } 40 | 41 | command.name = (value["name"] as? String) ?? "" 42 | if let shortcut = value["shortcut"] as? [String: Any] { 43 | command.key = transformer.transformedValue(shortcut) as? MASShortcut 44 | } 45 | command.script = value["script"] as! String 46 | 47 | return command 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /TySimulator/DirectoryWatcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DirectoryWatcher.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2017/7/20. 6 | // Copyright © 2017年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// https://developer.apple.com/library/content/samplecode/DocInteraction/Listings/ReadMe_txt.html 12 | class DirectoryWatcher { 13 | var dirFD: Int32 = -1 14 | var kq: Int32 = -1 15 | var dirKQRef: CFFileDescriptor? 16 | 17 | private var didChange: (() -> Void)? 18 | 19 | deinit { 20 | invalidate() 21 | } 22 | 23 | class func watchFolder(path: String, didChange:(() -> Void)?) -> DirectoryWatcher? { 24 | let result: DirectoryWatcher = DirectoryWatcher() 25 | result.didChange = didChange 26 | if result.startMonitoring(directory: path as NSString) { 27 | return result 28 | } 29 | return nil 30 | } 31 | 32 | func invalidate() { 33 | if dirKQRef != nil { 34 | CFFileDescriptorInvalidate(dirKQRef) 35 | dirKQRef = nil 36 | // We don't need to close the kq, CFFileDescriptorInvalidate closed it instead. 37 | // Change the value so no one thinks it's still live. 38 | kq = -1 39 | } 40 | 41 | if dirFD != -1 { 42 | close(dirFD) 43 | dirFD = -1 44 | } 45 | } 46 | 47 | // MARK: Private 48 | 49 | func kqueueFired() { 50 | assert(kq >= 0) 51 | 52 | var event: kevent = kevent() 53 | var timeout: timespec = timespec(tv_sec: 0, tv_nsec: 0) 54 | let eventCount = kevent(kq, nil, 0, &event, 1, &timeout) 55 | 56 | assert((eventCount >= 0) && (eventCount < 2)) 57 | 58 | didChange?() 59 | 60 | CFFileDescriptorEnableCallBacks(dirKQRef, kCFFileDescriptorReadCallBack) 61 | } 62 | 63 | func startMonitoring(directory: NSString) -> Bool { 64 | // Double initializing is not going to work... 65 | if dirKQRef == nil && dirFD == -1 && kq == -1 { 66 | // Open the directory we're going to watch 67 | dirFD = open(directory.fileSystemRepresentation, O_EVTONLY) 68 | if dirFD >= 0 { 69 | // Create a kqueue for our event messages... 70 | kq = kqueue() 71 | if kq >= 0 { 72 | var eventToAdd: kevent = kevent() 73 | eventToAdd.ident = UInt(dirFD) 74 | eventToAdd.filter = Int16(EVFILT_VNODE) 75 | eventToAdd.flags = UInt16(EV_ADD | EV_CLEAR) 76 | eventToAdd.fflags = UInt32(NOTE_WRITE) 77 | eventToAdd.data = 0 78 | eventToAdd.udata = nil 79 | 80 | let errNum = kevent(kq, &eventToAdd, 1, nil, 0, nil) 81 | if errNum == 0 { 82 | var context = CFFileDescriptorContext(version: 0, info: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()), retain: nil, release: nil, copyDescription: nil) 83 | 84 | let callback: CFFileDescriptorCallBack = { (kqRef: CFFileDescriptor?, callBackTypes: CFOptionFlags, info: UnsafeMutableRawPointer?) in 85 | guard let info = info else { 86 | return 87 | } 88 | let obj: DirectoryWatcher = Unmanaged.fromOpaque(info).takeUnretainedValue() 89 | assert(callBackTypes == kCFFileDescriptorReadCallBack) 90 | 91 | obj.kqueueFired() 92 | } 93 | 94 | // Passing true in the third argument so CFFileDescriptorInvalidate will close kq. 95 | dirKQRef = CFFileDescriptorCreate(nil, kq, true, callback, &context) 96 | if dirKQRef != nil { 97 | if let rls = CFFileDescriptorCreateRunLoopSource(nil, dirKQRef, 0) { 98 | CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, CFRunLoopMode.defaultMode) 99 | CFFileDescriptorEnableCallBacks(dirKQRef, kCFFileDescriptorReadCallBack) 100 | 101 | // If everything worked, return early and bypass shutting things down 102 | return true 103 | } 104 | // Couldn't create a runloop source, invalidate and release the CFFileDescriptorRef 105 | CFFileDescriptorInvalidate(dirKQRef) 106 | dirKQRef = nil 107 | } 108 | } 109 | // kq is active, but something failed, close the handle... 110 | close(kq) 111 | kq = -1 112 | } 113 | // file handle is open, but something failed, close the handle... 114 | close(dirFD) 115 | dirFD = -1 116 | } 117 | } 118 | return false 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /TySimulator/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | Fabric 24 | 25 | APIKey 26 | b8abdb192ffac1475217f74ad9b22a062de1c4ec 27 | Kits 28 | 29 | 30 | KitInfo 31 | 32 | KitName 33 | Crashlytics 34 | 35 | 36 | 37 | LSApplicationCategoryType 38 | public.app-category.developer-tools 39 | LSMinimumSystemVersion 40 | $(MACOSX_DEPLOYMENT_TARGET) 41 | LSUIElement 42 | 43 | NSHumanReadableCopyright 44 | Copyright © 2016年 ty0x2333. All rights reserved. 45 | NSMainStoryboardFile 46 | MainMenu 47 | NSPrincipalClass 48 | NSApplication 49 | SUPublicDSAKeyFile 50 | dsa_pub.pem 51 | 52 | 53 | -------------------------------------------------------------------------------- /TySimulator/LRU-K/LRU.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LRU.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2017/7/1. 6 | // Copyright © 2017年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class LRU { 12 | private let capacity: Int 13 | fileprivate var list = [K]() 14 | fileprivate var hashtable: [K: V] 15 | private let semaphore: DispatchSemaphore = DispatchSemaphore(value: 1) 16 | 17 | public var count: Int { 18 | return list.count 19 | } 20 | 21 | public var datas: [(key: K, value: V)] { 22 | var results: [(key: K, value: V)] = [] 23 | for key in list.reversed() { 24 | if let value = hashtable[key] { 25 | results.append((key: key, value: value)) 26 | } 27 | } 28 | return results 29 | } 30 | 31 | init(capacity: Int, datas: [(key: K, value: V)]? = nil) { 32 | self.capacity = capacity 33 | hashtable = [K: V](minimumCapacity: capacity) 34 | 35 | if let datas = datas { 36 | for (key, value) in datas.reversed() { 37 | list.append(key) 38 | hashtable[key] = value 39 | } 40 | } 41 | } 42 | 43 | subscript (key: K) -> V? { 44 | get { 45 | _ = semaphore.wait(timeout: .distantFuture) 46 | guard let elem = hashtable[key] else { 47 | semaphore.signal() 48 | return nil 49 | } 50 | list.remove(at: list.index(of: key)!) 51 | list.append(key) 52 | semaphore.signal() 53 | return elem 54 | } 55 | 56 | set(value) { 57 | _ = semaphore.wait(timeout: .distantFuture) 58 | repeat { 59 | if hashtable[key] != nil { 60 | list.remove(at: list.index(of: key)!) 61 | } 62 | 63 | hashtable[key] = value 64 | 65 | if value == nil { 66 | break 67 | } 68 | 69 | list.append(key) 70 | 71 | if list.count > capacity { 72 | let deletedKey = list.removeFirst() 73 | hashtable.removeValue(forKey: deletedKey) 74 | } 75 | } while false 76 | 77 | semaphore.signal() 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /TySimulator/LRU-K/LRUCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LRUCache.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2017/7/2. 6 | // Copyright © 2017年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | extension Notification.Name { 13 | public struct LRUCache { 14 | public static let DidRecord = Notification.Name(rawValue: "com.tianyiyan.notification.lru.didRecord") 15 | } 16 | } 17 | 18 | class LRUCache { 19 | fileprivate var lruk: LRUK 20 | var datas: [String] { 21 | return lruk.datas.map { $0.value } 22 | } 23 | public static let shared = LRUCache(file: LRUCache.filePath) 24 | 25 | fileprivate init() { 26 | lruk = LRUK(capacity: 5, bufferSize: 10, threshold: 2) 27 | } 28 | 29 | func record(app bundleIdentifier: String) { 30 | lruk[bundleIdentifier] = bundleIdentifier 31 | NotificationCenter.default.post(name: Notification.Name.LRUCache.DidRecord, object: nil) 32 | } 33 | } 34 | 35 | extension LRUCache { 36 | static var filePath: URL { 37 | let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! 38 | let bundleID = Bundle.main.bundleIdentifier! 39 | return appSupportURL.appendingPathComponent(bundleID, isDirectory: true).appendingPathComponent("LRUCache.json") 40 | } 41 | 42 | func jsonDescription() -> JSON { 43 | let history = lruk.history.datas.map { ["hits": $0.value.hits, "bundle_id": $0.value.value] } 44 | let datas = lruk.datas.map { $0.value } 45 | 46 | return JSON(["datas": datas, "history": history]) 47 | } 48 | 49 | convenience init(json: JSON) { 50 | self.init() 51 | let datas = json["datas"].arrayValue.map { (key: $0.stringValue, value: $0.stringValue) } 52 | let history = json["history"].arrayValue.map { 53 | (key: $0["bundle_id"].stringValue, value: (hits: $0["hits"].intValue, value: $0["bundle_id"].stringValue)) 54 | } 55 | lruk = LRUK(capacity: 3, bufferSize: 10, threshold: 2, datas: datas, history: history) 56 | } 57 | 58 | convenience init(file: URL) { 59 | if let content = try? String(contentsOf: LRUCache.filePath, encoding: .utf8) { 60 | self.init(json: JSON(parseJSON: content)) 61 | } else { 62 | self.init() 63 | } 64 | } 65 | 66 | func save() { 67 | do { 68 | try jsonDescription().rawString([.castNilToNSNull: true])?.write(to: LRUCache.filePath, atomically: true, encoding: .utf8) 69 | log.info("save LRU cache to \(LRUCache.filePath)") 70 | } catch { 71 | log.error("save LRU cache error: \(error)") 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /TySimulator/LRU-K/LRUK.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LRUK.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2017/7/1. 6 | // Copyright © 2017年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class LRUK { 12 | private let cache: LRU 13 | private let bufferSize: Int 14 | let history: LRU 15 | private let threshold: Int 16 | private let semaphore: DispatchSemaphore = DispatchSemaphore(value: 1) 17 | 18 | public var count: Int { 19 | return cache.count 20 | } 21 | 22 | public var datas: [(key: K, value: V)] { 23 | return cache.datas 24 | } 25 | 26 | init(capacity: Int, bufferSize: Int, threshold: Int = 2, datas: [(key: K, value: V)]? = nil, history: [(key: K, value: (hits: Int, value: V))]? = nil) { 27 | cache = LRU(capacity: capacity, datas: datas) 28 | self.history = LRU(capacity: bufferSize, datas: history) 29 | self.bufferSize = bufferSize 30 | self.threshold = threshold 31 | } 32 | 33 | subscript (key: K) -> V? { 34 | get { 35 | return cache[key] 36 | } 37 | 38 | // if value is nil, it will do nothing 39 | set(value) { 40 | guard let newValue = value else { 41 | return 42 | } 43 | _ = semaphore.wait(timeout: .distantFuture) 44 | 45 | if let his = history[key] { 46 | let hits = his.hits + 1 47 | 48 | if hits < threshold { 49 | history[key] = (hits: hits, value: newValue) 50 | } else { 51 | history[key] = nil 52 | cache[key] = newValue 53 | } 54 | } else if threshold < 2 { 55 | cache[key] = newValue 56 | } else { 57 | history[key] = (hits: 1, value: newValue) 58 | } 59 | 60 | semaphore.signal() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /TySimulator/MainMenu.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | -------------------------------------------------------------------------------- /TySimulator/MainMenuController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainMenuController.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/13. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class MainMenuController: NSObject { 12 | let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) 13 | let popover: NSPopover = NSPopover() 14 | let quitMenuItem: NSMenuItem = NSMenuItem(title: NSLocalizedString("menu.quit", comment: "menu"), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") 15 | let aboutItem: NSMenuItem = NSMenuItem(title: NSLocalizedString("menu.about", comment: "menu"), action: #selector(NSApplication.showAboutWindow), keyEquivalent: "") 16 | let preferenceItem: NSMenuItem = NSMenuItem(title: NSLocalizedString("menu.preference", comment: "menu"), action: #selector(NSApplication.showPreferencesWindow), keyEquivalent: ",") 17 | lazy var menu: NSMenu = { 18 | let menu = NSMenu() 19 | menu.autoenablesItems = false 20 | menu.addItem(preferenceItem) 21 | menu.addItem(aboutItem) 22 | menu.addItem(NSMenuItem.separator()) 23 | menu.addItem(quitMenuItem) 24 | return menu 25 | }() 26 | 27 | var monitor: Any? 28 | 29 | override func awakeFromNib() { 30 | super.awakeFromNib() 31 | 32 | popover.contentViewController = MainViewController(nibName: "MainViewController", bundle: nil) 33 | 34 | if let button = statusItem.button { 35 | button.image = NSImage(named: "MenuIcon") 36 | button.target = self 37 | button.action = #selector(MainMenuController.togglePopver(_:)) 38 | } 39 | } 40 | 41 | // MARK: Actions 42 | 43 | @IBAction func onClickMenuPreferences(_ sender: Any) { 44 | NSApplication.shared.showPreferencesWindow() 45 | } 46 | 47 | @objc func togglePopver(_ sender: Any?) { 48 | if popover.isShown { 49 | closePopover(sender: sender) 50 | } else { 51 | showPopover(sender: sender) 52 | } 53 | } 54 | 55 | // MARK: Private 56 | private func showPopover(sender: Any?) { 57 | guard let button = statusItem.button else { 58 | return 59 | } 60 | log.info("show Popover") 61 | popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) 62 | guard monitor == nil else { 63 | return 64 | } 65 | monitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] event in 66 | guard let weakSelf = self, 67 | weakSelf.popover.isShown else { 68 | return 69 | } 70 | weakSelf.closePopover(sender: event) 71 | } 72 | } 73 | 74 | func closePopover(sender: Any?) { 75 | log.info("close Popover") 76 | popover.performClose(sender) 77 | if let monitor = self.monitor { 78 | NSEvent.removeMonitor(monitor) 79 | self.monitor = nil 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /TySimulator/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2018/10/15. 6 | // Copyright © 2018 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SnapKit 11 | 12 | class MainViewController: NSViewController { 13 | @IBOutlet weak var deviceTableView: NSTableView! 14 | @IBOutlet weak var infoCollectionView: NSCollectionView! 15 | @IBOutlet weak var splitView: NSSplitView! 16 | 17 | @IBOutlet weak var progressView: NSProgressIndicator! 18 | @IBOutlet weak var recentView: NSView! 19 | @IBOutlet weak var recentCollectionView: NSCollectionView! 20 | 21 | @IBOutlet weak var emptyView: NSView! 22 | @IBOutlet var splitBottomConstraint: NSLayoutConstraint! 23 | @IBOutlet weak var recentContainerView: NSScrollView! 24 | 25 | private static let headerItemIdentifier = NSUserInterfaceItemIdentifier(rawValue: "infoSectionHeader") 26 | private static let appItemIdentifier = NSUserInterfaceItemIdentifier(rawValue: "defaultItem") 27 | private static let recentItemIdentifier = NSUserInterfaceItemIdentifier(rawValue: "recentItem") 28 | private var devices: [DeviceModel] = [] { 29 | didSet { 30 | recentCollectionView.reloadData() 31 | updateRecentView() 32 | } 33 | } 34 | private var recentApplications: [ApplicationModel] = [] 35 | private var selectedDeviceUDID: String? 36 | 37 | private var selectedDevice: DeviceModel? { 38 | guard let udid = selectedDeviceUDID else { 39 | return nil 40 | } 41 | return devices.first(where: { $0.udid == udid }) 42 | } 43 | 44 | override func viewDidLoad() { 45 | super.viewDidLoad() 46 | splitView.delegate = self 47 | deviceTableView.selectionHighlightStyle = .none 48 | deviceTableView.delegate = self 49 | deviceTableView.dataSource = self 50 | 51 | infoCollectionView.register(NSNib(nibNamed: "MainViewController", bundle: nil), forItemWithIdentifier: MainViewController.appItemIdentifier) 52 | infoCollectionView.register(InfoSectionHeaderView.self, forSupplementaryViewOfKind: NSCollectionView.elementKindSectionHeader, withIdentifier: MainViewController.headerItemIdentifier) 53 | infoCollectionView.delegate = self 54 | infoCollectionView.dataSource = self 55 | 56 | recentCollectionView.register(NSNib(nibNamed: "BaseDetailCollectionItem", bundle: nil), forItemWithIdentifier: MainViewController.recentItemIdentifier) 57 | recentCollectionView.dataSource = self 58 | recentCollectionView.delegate = self 59 | 60 | for subview in splitView.subviews { 61 | subview.wantsLayer = true 62 | subview.layer?.cornerRadius = 4.0 63 | subview.layer?.masksToBounds = true 64 | } 65 | 66 | recentContainerView.wantsLayer = true 67 | recentContainerView.layer?.cornerRadius = 4.0 68 | recentContainerView.layer?.masksToBounds = true 69 | 70 | NotificationCenter.default.addObserver(self, selector: #selector(devicesChangedNotification(sender:)), name: Notification.Name.Device.DidChange, object: nil) 71 | NotificationCenter.default.addObserver(self, selector: #selector(updateRecentApplications), name: Notification.Name.LRUCache.DidRecord, object: nil) 72 | 73 | devices = Simulator.shared.devices 74 | reloadInfos() 75 | updateRecentApplications() 76 | } 77 | 78 | deinit { 79 | NotificationCenter.default.removeObserver(self) 80 | } 81 | 82 | override func viewWillAppear() { 83 | super.viewWillAppear() 84 | progressView.startAnimation(nil) 85 | DispatchQueue.global().async { 86 | Simulator.shared.updateDeivces() 87 | DispatchQueue.main.async { 88 | self.progressView.stopAnimation(nil) 89 | } 90 | } 91 | } 92 | 93 | private func updateDeviceMenus() { 94 | log.verbose("update devices") 95 | 96 | devices = Simulator.shared.devices 97 | log.info("load devices: \(devices.count)") 98 | 99 | deviceTableView.reloadData() 100 | } 101 | 102 | private func reloadInfos() { 103 | infoCollectionView.reloadData() 104 | emptyView.isHidden = numberOfSections(in: infoCollectionView) > 0 105 | } 106 | 107 | @objc private func updateRecentApplications() { 108 | let datas = LRUCache.shared.datas 109 | guard datas.count > 0 else { 110 | recentApplications = [] 111 | return 112 | } 113 | var apps: [ApplicationModel] = [] 114 | let bootedDevices = Simulator.shared.bootedDevices 115 | for bundleID in datas { 116 | for device in bootedDevices { 117 | if let app = device.application(bundleIdentifier: bundleID) { 118 | apps.append(app) 119 | break 120 | } 121 | } 122 | } 123 | 124 | guard apps.count > 0 else { 125 | recentApplications = [] 126 | return 127 | } 128 | 129 | log.verbose("update recent apps") 130 | 131 | var models: [ApplicationModel] = [] 132 | for (idx, app) in apps.enumerated() { 133 | if idx > 2 { 134 | break 135 | } 136 | models.append(app) 137 | } 138 | recentApplications = models 139 | } 140 | 141 | @IBAction func onMenuClick(_ sender: NSButton) { 142 | guard let menu = (NSApp.delegate as? AppDelegate)?.mainMenuController?.menu, 143 | let event = NSApp.currentEvent else { 144 | return 145 | } 146 | NSMenu.popUpContextMenu(menu, with: event, for: sender) 147 | } 148 | 149 | // MARK: Notification 150 | @objc func devicesChangedNotification(sender: Notification) { 151 | log.verbose("devicesChangedNotification updateDeviceMenus") 152 | updateDeviceMenus() 153 | updateRecentApplications() 154 | } 155 | 156 | // MARK: Helper 157 | 158 | private func updateRecentView() { 159 | let isVisible = recentApplications.count > 0 160 | splitBottomConstraint.isActive = isVisible 161 | recentView.isHidden = !isVisible 162 | } 163 | } 164 | 165 | extension MainViewController: NSTableViewDelegate { 166 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 167 | let result: AppMenuTableCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "defaultRow"), owner: self) as! AppMenuTableCellView 168 | if devices.indices.contains(row) { 169 | let device = devices[row] 170 | result.name = device.displayName 171 | result.isAvailable = device.isOpen 172 | result.isHighlight = device.udid == selectedDeviceUDID 173 | } 174 | return result 175 | } 176 | 177 | func tableViewSelectionDidChange(_ notification: Notification) { 178 | if devices.indices.contains(deviceTableView.selectedRow) { 179 | selectedDeviceUDID = devices[deviceTableView.selectedRow].udid 180 | } 181 | deviceTableView.deselectAll(nil) 182 | deviceTableView.reloadData() 183 | reloadInfos() 184 | } 185 | } 186 | 187 | extension MainViewController: NSTableViewDataSource { 188 | func numberOfRows(in tableView: NSTableView) -> Int { 189 | return devices.count 190 | } 191 | } 192 | 193 | extension MainViewController: NSSplitViewDelegate { 194 | func splitViewWillResizeSubviews(_ notification: Notification) { 195 | infoCollectionView.collectionViewLayout?.invalidateLayout() 196 | } 197 | } 198 | 199 | extension MainViewController: NSCollectionViewDataSource { 200 | 201 | func numberOfSections(in collectionView: NSCollectionView) -> Int { 202 | if collectionView == recentCollectionView { 203 | return 1 204 | } 205 | guard let device = selectedDevice else { 206 | return 0 207 | } 208 | if device.applications.isEmpty, device.appGroups.isEmpty, device.medias.isEmpty { 209 | return 0 210 | } 211 | return 3 212 | } 213 | 214 | func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { 215 | guard collectionView == infoCollectionView else { 216 | return recentApplications.count 217 | } 218 | guard let device = selectedDevice else { 219 | return 0 220 | } 221 | switch section { 222 | case 0: 223 | return device.applications.count 224 | case 1: 225 | return device.appGroups.count 226 | case 2: 227 | return device.medias.count 228 | default: 229 | return 0 230 | } 231 | } 232 | 233 | func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { 234 | guard collectionView == infoCollectionView else { 235 | let item = collectionView.makeItem(withIdentifier: MainViewController.recentItemIdentifier, for: indexPath) as! BaseDetailCollectionItem 236 | if recentApplications.indices.contains(indexPath.item) { 237 | let app = recentApplications[indexPath.item] 238 | if let imageView = item.imageView { 239 | imageView.wantsLayer = true 240 | imageView.layer?.cornerRadius = 10.0 241 | imageView.layer?.masksToBounds = true 242 | } 243 | item.icon = app.bundle.appIcon 244 | item.name = app.bundle.appName 245 | item.location = app.dataPath 246 | } 247 | return item 248 | } 249 | let item = collectionView.makeItem(withIdentifier: MainViewController.appItemIdentifier, for: indexPath) as! DetailCollectionItem 250 | switch indexPath.section { 251 | case 0: 252 | if let applications = selectedDevice?.applications, applications.indices.contains(indexPath.item) { 253 | let app = applications[indexPath.item] 254 | item.icon = app.bundle.appIcon 255 | item.name = app.bundle.appName 256 | item.type = .app 257 | item.location = app.dataPath 258 | } 259 | case 1: 260 | if let groups = selectedDevice?.appGroups, groups.indices.contains(indexPath.item) { 261 | let group = groups[indexPath.item] 262 | item.name = group.bundleIdentifier 263 | item.type = .group 264 | item.location = group.location 265 | } 266 | case 2: 267 | if let medias = selectedDevice?.medias, medias.indices.contains(indexPath.item) { 268 | let media = medias[indexPath.item] 269 | item.name = media.name 270 | item.type = .assets 271 | item.location = media.location 272 | } 273 | default: 274 | break 275 | } 276 | return item 277 | } 278 | 279 | func collectionView(_ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, at indexPath: IndexPath) -> NSView { 280 | if collectionView == infoCollectionView, kind == NSCollectionView.elementKindSectionHeader { 281 | let headerView = collectionView.makeSupplementaryView(ofKind: kind, withIdentifier: MainViewController.headerItemIdentifier, for: indexPath) 282 | var text = "" 283 | switch indexPath.section { 284 | case 0: 285 | text = NSLocalizedString("menu.application", comment: "menu") 286 | case 1: 287 | text = NSLocalizedString("menu.app.group", comment: "menu") 288 | case 2: 289 | text = NSLocalizedString("menu.media", comment: "menu") 290 | default: 291 | break 292 | } 293 | (headerView as? InfoSectionHeaderView)?.title = text 294 | return headerView 295 | } 296 | return NSView() 297 | } 298 | 299 | func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> NSSize { 300 | return collectionView == infoCollectionView ? CGSize(width: collectionView.bounds.width, height: InfoSectionHeaderView.defaultHeight) : .zero 301 | } 302 | } 303 | 304 | extension MainViewController: NSCollectionViewDelegateFlowLayout { 305 | func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize { 306 | return collectionView == infoCollectionView ? CGSize(width: collectionView.bounds.width, height: 49) : CGSize(width: 63.0, height: 65.0) 307 | } 308 | 309 | func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { 310 | guard let indexPath = indexPaths.first else { 311 | return 312 | } 313 | 314 | var location: URL? 315 | if collectionView == infoCollectionView { 316 | location = detailItemLocation(indexPath: indexPath) 317 | } else if recentApplications.indices.contains(indexPath.item) { 318 | location = recentApplications[indexPath.item].dataPath 319 | } 320 | 321 | if let loc = location { 322 | DispatchQueue.main.async { 323 | NSWorkspace.shared.open(loc) 324 | } 325 | } 326 | (NSApp.delegate as? AppDelegate)?.mainMenuController?.closePopover(sender: nil) 327 | } 328 | 329 | private func detailItemLocation(indexPath: IndexPath) -> URL? { 330 | switch indexPath.section { 331 | case 0: 332 | if let applications = selectedDevice?.applications, applications.indices.contains(indexPath.item) { 333 | let app = applications[indexPath.item] 334 | LRUCache.shared.record(app: app.bundle.bundleID) 335 | return app.dataPath 336 | } 337 | case 1: 338 | if let groups = selectedDevice?.appGroups, groups.indices.contains(indexPath.item) { 339 | return groups[indexPath.item].location 340 | } 341 | case 2: 342 | if let medias = selectedDevice?.medias, medias.indices.contains(indexPath.item) { 343 | let media = medias[indexPath.item] 344 | return media.location 345 | } 346 | default: 347 | break 348 | } 349 | return nil 350 | } 351 | } 352 | 353 | class AppMenuTableCellView: NSTableCellView { 354 | var name: String? { 355 | set { 356 | textField?.stringValue = newValue ?? "" 357 | } 358 | get { 359 | return textField?.stringValue 360 | } 361 | } 362 | var isAvailable: Bool = false { 363 | didSet { 364 | imageView?.image = isAvailable ? NSImage(named: "icon-on") : NSImage(named: "icon-off") 365 | } 366 | } 367 | 368 | var isHighlight: Bool = false { 369 | didSet { 370 | needsDisplay = true 371 | } 372 | } 373 | 374 | override func prepareForReuse() { 375 | super.prepareForReuse() 376 | isAvailable = false 377 | } 378 | 379 | override func draw(_ dirtyRect: NSRect) { 380 | super.draw(dirtyRect) 381 | if isHighlight { 382 | NSColor.selectedControlColor.set() 383 | dirtyRect.fill() 384 | } 385 | } 386 | } 387 | 388 | class BaseDetailCollectionItem: NSCollectionViewItem { 389 | var icon: NSImage? { 390 | set { 391 | newValue?.isTemplate = false 392 | imageView?.image = newValue ?? NSImage(named: "tmp-logo") 393 | } 394 | get { 395 | return imageView?.image 396 | } 397 | } 398 | var name: String? { 399 | set { 400 | textField?.stringValue = newValue ?? "" 401 | } 402 | get { 403 | return textField?.stringValue 404 | } 405 | } 406 | 407 | var location: URL? 408 | } 409 | 410 | class DetailCollectionItem: BaseDetailCollectionItem { 411 | enum ItemType { 412 | case app 413 | case assets 414 | case group 415 | } 416 | var directoryWatcher: DirectoryWatcher? 417 | @IBOutlet private weak var sizeTextField: NSTextField! 418 | 419 | var type: ItemType = .app { 420 | didSet { 421 | if type == .app { 422 | imageView?.layer?.cornerRadius = 21.0 423 | } else { 424 | icon = NSImage(named: "finder") 425 | imageView?.layer?.cornerRadius = 0 426 | } 427 | } 428 | } 429 | 430 | override var location: URL? { 431 | didSet { 432 | directoryWatcher?.invalidate() 433 | guard let url = location?.appendingPathComponent("Documents", isDirectory: true) else { 434 | return 435 | } 436 | directoryWatcher = DirectoryWatcher.watchFolder(path: url.path, didChange: { [weak self] in 437 | log.verbose("\(url) did change") 438 | self?.updateSizeText() 439 | }) 440 | updateSizeText() 441 | } 442 | } 443 | 444 | override func awakeFromNib() { 445 | super.awakeFromNib() 446 | imageView?.wantsLayer = true 447 | imageView?.layer?.cornerRadius = 21.0 448 | imageView?.layer?.masksToBounds = true 449 | } 450 | 451 | override func prepareForReuse() { 452 | super.prepareForReuse() 453 | location = nil 454 | directoryWatcher = nil 455 | icon = nil 456 | name = nil 457 | } 458 | 459 | private func updateSizeText() { 460 | guard let url = location, 461 | let numerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.fileSizeKey], options: [], errorHandler: nil) else { 462 | sizeTextField.isHidden = true 463 | return 464 | } 465 | var total: Int64 = 0 466 | for object in numerator { 467 | var fileSizeResource: AnyObject? 468 | guard let fileURL = object as? NSURL else { 469 | continue 470 | } 471 | try? fileURL.getResourceValue(&fileSizeResource, forKey: .fileSizeKey) 472 | guard let fileSize = fileSizeResource as? NSNumber else { 473 | continue 474 | } 475 | total += fileSize.int64Value 476 | } 477 | sizeTextField.isHidden = false 478 | sizeTextField.stringValue = ByteCountFormatter.string(fromByteCount: total, countStyle: .file) 479 | } 480 | } 481 | 482 | class InfoSectionHeaderView: NSView { 483 | static let defaultHeight: CGFloat = 40.0 484 | private var textField: NSTextField = NSTextField() 485 | var title: String? { 486 | set { 487 | textField.stringValue = newValue ?? "" 488 | } 489 | get { 490 | return textField.stringValue 491 | } 492 | } 493 | 494 | override init(frame frameRect: NSRect) { 495 | super.init(frame: frameRect) 496 | 497 | textField.isBezeled = false 498 | textField.isEditable = false 499 | textField.isSelectable = false 500 | textField.font = NSFont.boldSystemFont(ofSize: 13.0) 501 | addSubview(textField) 502 | 503 | textField.snp.makeConstraints { (make) in 504 | make.left.equalToSuperview().offset(16.0) 505 | make.centerY.right.equalToSuperview() 506 | } 507 | } 508 | 509 | required init?(coder decoder: NSCoder) { 510 | fatalError("init(coder:) has not been implemented") 511 | } 512 | 513 | override func draw(_ dirtyRect: NSRect) { 514 | super.draw(dirtyRect) 515 | NSColor.placeholderTextColor.setFill() 516 | NSRect(x: 16.0, y: 8.0, width: bounds.width - 16.0, height: 0.5).fill() 517 | } 518 | } 519 | -------------------------------------------------------------------------------- /TySimulator/Model/AppGroupModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppGroupModel.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/13. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class AppGroupModel: NSObject { 12 | 13 | var bundleIdentifier: String = "" 14 | var location: URL? 15 | } 16 | -------------------------------------------------------------------------------- /TySimulator/Model/ApplicationModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApplicationModel.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/13. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ApplicationBundle { 12 | let bundleID: String 13 | let appName: String 14 | let appIcon: NSImage? 15 | init(bundleID: String, appName: String, appIcon: NSImage?) { 16 | self.bundleID = bundleID 17 | self.appName = appName 18 | self.appIcon = appIcon 19 | } 20 | } 21 | 22 | class ApplicationModel { 23 | let bundle: ApplicationBundle 24 | let uuid: String 25 | let deviceUDID: String 26 | let dataPath: URL 27 | 28 | init?(deviceUDID: String, uuid: String, bundle: ApplicationBundle, dataPath: URL) { 29 | self.deviceUDID = deviceUDID 30 | self.uuid = uuid 31 | self.bundle = bundle 32 | self.dataPath = dataPath 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /TySimulator/Model/CommandModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandModel.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/18. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MASShortcut 11 | 12 | class CommandModel: NSObject { 13 | private(set) var id: String 14 | @objc var name: String = "" 15 | @objc var script: String = "" 16 | @objc var key: MASShortcut? 17 | 18 | convenience override init() { 19 | let timeStamp = NSDate().timeIntervalSince1970 20 | self.init(id: String(timeStamp).md5()) 21 | } 22 | 23 | @objc init(id: String) { 24 | self.id = id 25 | super.init() 26 | } 27 | 28 | override var description: String { 29 | return "<\(name): \(String(describing: key))>" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /TySimulator/Model/DeviceModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceModel.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/13. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum OS: String { 12 | case tvOS 13 | case iOS 14 | case watchOS 15 | case unknown 16 | 17 | var order: Int { 18 | switch self { 19 | case .iOS: 20 | return 0 21 | case .tvOS: 22 | return 1 23 | case .watchOS: 24 | return 2 25 | default: 26 | return 3 27 | } 28 | } 29 | } 30 | 31 | class DeviceModel { 32 | 33 | let name: String 34 | let udid: String 35 | let osInfo: String 36 | 37 | let os: OS 38 | let isOpen: Bool 39 | let isAvailable: Bool 40 | let version: String 41 | let hasContent: Bool 42 | 43 | let applications: [ApplicationModel] 44 | let medias: [MediaModel] 45 | let appGroups: [AppGroupModel] 46 | let location: URL 47 | 48 | var displayName: String { 49 | return "\(name) (\(osInfo))" 50 | } 51 | 52 | init(osInfo: String, json: [String: Any]) { 53 | name = (json["name"] as? String) ?? "" 54 | udid = (json["udid"] as? String) ?? "" 55 | if let isAvailable = (json["availability"] as? String)?.contains("(available)") { 56 | self.isAvailable = isAvailable 57 | } else { 58 | self.isAvailable = (json["isAvailable"] as? Bool) ?? false 59 | } 60 | isOpen = (json["state"] as? String)?.contains("Booted") ?? false 61 | self.osInfo = osInfo 62 | if osInfo.components(separatedBy: "-").count >= 3 { 63 | os = OS(rawValue: osInfo.components(separatedBy: "-").first ?? "") ?? .unknown 64 | var osInfoArr = osInfo.components(separatedBy: "-") 65 | osInfoArr.remove(at: 0) 66 | version = osInfoArr.joined(separator: ".") 67 | } else { 68 | os = OS(rawValue: osInfo.components(separatedBy: " ").first ?? "") ?? .unknown 69 | version = osInfo.components(separatedBy: " ").last ?? "" 70 | } 71 | location = Simulator.devicesDirectory.appendingPathComponent("\(udid)") 72 | applications = Simulator.applications(deviceUDID: udid) 73 | appGroups = Simulator.appGroups(deviceUDID: udid) 74 | medias = Simulator.medias(path: location) 75 | hasContent = !applications.isEmpty 76 | } 77 | 78 | func application(bundleIdentifier: String) -> ApplicationModel? { 79 | return applications.first(where: { $0.bundle.bundleID == bundleIdentifier }) 80 | } 81 | } 82 | 83 | extension DeviceModel: Equatable { 84 | public static func == (lhs: DeviceModel, rhs: DeviceModel) -> Bool { 85 | return lhs.udid == rhs.udid 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /TySimulator/Model/MediaModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaModel.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/13. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class MediaModel { 12 | 13 | let name: String 14 | let location: URL 15 | 16 | init(name: String, location: URL) { 17 | self.name = name 18 | self.location = location 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TySimulator/Preferences/CommandViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandViewController.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/21. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import MASShortcut 11 | import ACEViewSwift 12 | 13 | class CommandViewController: NSViewController, ACEViewDelegate { 14 | @objc var command: CommandModel! 15 | @IBOutlet weak var nameTextField: NSTextField! 16 | @IBOutlet weak var shortcutView: MASShortcutView! 17 | @IBOutlet weak var aceView: ACEView! 18 | var save: ((CommandModel) -> Void)? 19 | 20 | init(_ command: CommandModel) { 21 | super.init(nibName: nil, bundle: nil) 22 | self.command = command 23 | } 24 | override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) { 25 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 26 | command = CommandModel() 27 | } 28 | 29 | required init?(coder: NSCoder) { 30 | super.init(coder: coder) 31 | } 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | title = "Command Editor" 36 | nameTextField.bind(NSBindingName(rawValue: "value"), to: command, withKeyPath: #keyPath(CommandModel.name), options: [NSBindingOption.continuouslyUpdatesValue: true]) 37 | shortcutView.bind(NSBindingName(rawValue: "shortcutValue"), to: command, withKeyPath: #keyPath(CommandModel.key), options: [NSBindingOption.continuouslyUpdatesValue: true]) 38 | 39 | aceView.onReady = { [unowned self] in 40 | self.aceView.bind(NSBindingName(rawValue: "string"), to: self.command, withKeyPath: #keyPath(CommandModel.script), options: [NSBindingOption.continuouslyUpdatesValue: true]) 41 | self.aceView.delegate = self 42 | if self.command.script.isEmpty { 43 | let (deviceIdentifier, applicationIdentifier) = self.placeholderDevice() 44 | self.aceView.string = "open " + Script.transformedValue(deviceIdentifier: deviceIdentifier, applicationIdentifier: applicationIdentifier) 45 | } else { 46 | self.aceView.string = self.command.script 47 | } 48 | self.aceView.mode = .sh 49 | self.aceView.theme = .xcode 50 | self.aceView.keyboardHandler = .ace 51 | self.aceView.showPrintMargin = false 52 | self.aceView.showInvisibles = true 53 | self.aceView.basicAutoCompletion = true 54 | self.aceView.liveAutocompletion = true 55 | self.aceView.snippets = true 56 | self.aceView.emmet = true 57 | } 58 | } 59 | 60 | func placeholderDevice() -> (String, String) { 61 | var deviceIdentifier = "booted" 62 | 63 | // try booted device 64 | if let device = Simulator.shared.bootedDevices.first { 65 | if device.applications.count > 0 { 66 | return (deviceIdentifier, (device.applications.first?.bundle.bundleID)!) 67 | } 68 | } 69 | 70 | // try other device 71 | for device in Simulator.shared.devices { 72 | deviceIdentifier = device.udid 73 | if device.applications.count > 0 { 74 | return (deviceIdentifier, (device.applications.first?.bundle.bundleID)!) 75 | } 76 | } 77 | return (deviceIdentifier, "your_app_bundle_identifier") 78 | } 79 | 80 | @IBAction func onSaveButtonClick(_ sender: NSButton) { 81 | // TODO: log 82 | save?(command) 83 | dismiss(self) 84 | } 85 | 86 | @IBAction func onCancelButtonClick(_ sender: NSButton) { 87 | // TODO: log 88 | dismiss(self) 89 | } 90 | 91 | // MARK: ACEViewDelegate 92 | func textDidChange(_ notification: Notification) { 93 | command.script = aceView.string 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /TySimulator/Preferences/CommandViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 70 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /TySimulator/Preferences/GeneralPreferencesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeneralPreferencesViewController.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/17. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import MASPreferences 11 | 12 | class GeneralPreferencesViewController: NSViewController, MASPreferencesViewController { 13 | 14 | @IBOutlet weak var checkForUpdatesButton: NSButton! 15 | @IBOutlet weak var feedbackButton: NSButton! 16 | @IBOutlet weak var applicationBox: NSBox! 17 | @IBOutlet weak var menuBox: NSBox! 18 | @IBOutlet weak var isOnlyHasContentDevices: NSButton! 19 | @IBOutlet weak var isOnlyAvailableDevices: NSButton! 20 | @IBOutlet weak var isLaunchAtStartup: NSButton! 21 | @IBOutlet weak var isAutomaticallyChecksForUpdates: NSButton! 22 | 23 | override func awakeFromNib() { 24 | super.awakeFromNib() 25 | let preference = Preference.shared 26 | 27 | checkForUpdatesButton.title = NSLocalizedString("preference.general.check.update", comment: "preference") 28 | feedbackButton.title = NSLocalizedString("preference.general.feedback", comment: "preference") 29 | 30 | applicationBox.title = NSLocalizedString("preference.general.application", comment: "preference") 31 | menuBox.title = NSLocalizedString("preference.general.menu", comment: "preference") 32 | 33 | isLaunchAtStartup.title = NSLocalizedString("preference.general.launch.startup", comment: "preference") 34 | isLaunchAtStartup.state = NSApplication.isLaunchAtStartup ? .on : .off 35 | 36 | isOnlyAvailableDevices.title = NSLocalizedString("preference.general.only.available.device", comment: "preference") 37 | isOnlyAvailableDevices.state = preference.onlyAvailableDevices ? .on : .off 38 | 39 | isOnlyHasContentDevices.title = NSLocalizedString("preference.general.only.has.application", comment: "preference") 40 | isOnlyHasContentDevices.state = preference.onlyHasContentDevices ? .on : .off 41 | 42 | isAutomaticallyChecksForUpdates.title = NSLocalizedString("preference.general.auto.check.update", comment: "preference") 43 | isAutomaticallyChecksForUpdates.state = DM_SUUpdater.shared().automaticallyChecksForUpdates ? .on : .off 44 | } 45 | @IBAction func onLaunchAtStartupButtonClick(_ sender: NSButton) { 46 | NSApplication.isLaunchAtStartup = sender.state == .on 47 | } 48 | 49 | @IBAction func onOnlyAvailableDevicesButtonClick(_ sender: NSButton) { 50 | Preference.shared.onlyAvailableDevices = sender.state == .on 51 | } 52 | 53 | @IBAction func onOnlyHasContentDevicesButtonClick(_ sender: NSButton) { 54 | Preference.shared.onlyHasContentDevices = sender.state == .on 55 | } 56 | 57 | @IBAction func onAutomaticallyChecksForUpdatesButtonClick(_ sender: NSButton) { 58 | DM_SUUpdater.shared().automaticallyChecksForUpdates = sender.state == .on 59 | } 60 | 61 | @IBAction func onCheckForUpdatesButtonClick(_ sender: NSButton) { 62 | NSApp.checkForUpdates() 63 | } 64 | 65 | @IBAction func onFeedbackButtonClick(_ sender: NSButton) { 66 | NSApp.showFeedbackWindow() 67 | } 68 | 69 | // MARK: MASPreferencesViewController 70 | var viewIdentifier: String { 71 | return "GeneralPreferences" 72 | } 73 | 74 | var toolbarItemImage: NSImage? = NSImage(named: NSImage.preferencesGeneralName) 75 | 76 | var toolbarItemLabel: String? = NSLocalizedString("preference.general", comment: "preference") 77 | } 78 | -------------------------------------------------------------------------------- /TySimulator/Preferences/GeneralPreferencesViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 43 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 83 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 113 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /TySimulator/Preferences/KeyBindingsPreferencesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyBindingsPreferencesViewController.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 16/11/17. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import MASPreferences 11 | import MASShortcut 12 | 13 | class KeyBindingsPreferencesViewController: NSViewController, MASPreferencesViewController, NSTableViewDelegate, NSTableViewDataSource, NSTextFieldDelegate { 14 | 15 | @IBOutlet weak var tableView: NSTableView! 16 | 17 | var commandController: NSArrayController = NSArrayController() 18 | 19 | override func awakeFromNib() { 20 | super.awakeFromNib() 21 | tableView.doubleAction = #selector(onTableViewDoubleClick(_:)) 22 | } 23 | 24 | // MARK: Actions 25 | @objc func onTableViewDoubleClick(_ sender: NSTableView) { 26 | log.verbose("click row: \(sender.clickedRow)") 27 | let command = Preference.shared.commands[sender.clickedRow] 28 | let commandViewController = CommandViewController(command) 29 | let oldKey = command.key 30 | commandViewController.save = { [weak self] (command) in 31 | log.verbose("save command: \(command)") 32 | MASShortcutMonitor.shared().unregisterShortcut(oldKey) 33 | Preference.shared.setCommand(id: command.id, command: command) 34 | MASShortcutMonitor.shared().register(command: command) 35 | self?.tableView.reloadData() 36 | } 37 | presentAsSheet(commandViewController) 38 | } 39 | 40 | @IBAction func onAddCommandButtonClick(_ sender: NSButton) { 41 | let commandViewController = CommandViewController() 42 | commandViewController.save = { [weak self] (command) in 43 | log.verbose("save command: \(command)") 44 | MASShortcutMonitor.shared().register(command: command) 45 | Preference.shared.append(command) 46 | self?.tableView.reloadData() 47 | } 48 | presentAsSheet(commandViewController) 49 | } 50 | 51 | @IBAction func onRemoveButtonClick(_ sender: NSButton) { 52 | if tableView.numberOfSelectedRows < 1 { 53 | log.warning("no row selected") 54 | return 55 | } 56 | let preference = Preference.shared 57 | let command = preference.commands[tableView.selectedRow] 58 | MASShortcutMonitor.shared().unregister(command: command) 59 | Preference.shared.remove(at: tableView.selectedRow) 60 | tableView.reloadData() 61 | } 62 | 63 | // MARK: NSTableViewDataSource 64 | func numberOfRows(in tableView: NSTableView) -> Int { 65 | return Preference.shared.commands.count 66 | } 67 | 68 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 69 | let command = Preference.shared.commands[row] 70 | 71 | if tableColumn == tableView.tableColumns[0] { 72 | if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "NameTableCellViewIdentifier"), owner: nil) as? NSTableCellView { 73 | cell.textField?.stringValue = command.name 74 | return cell 75 | } 76 | } else if tableColumn == tableView.tableColumns[1] { 77 | if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ShortcutTableCellViewIdentifier"), owner: nil) as? ShortcutTableCellView { 78 | cell.shortcutView?.shortcutValue = command.key 79 | cell.shortcutView?.shortcutValueChange = {(sender: MASShortcutView?) in 80 | MASShortcutMonitor.shared().unregister(command: command) 81 | command.key = sender?.shortcutValue 82 | Preference.shared.setCommand(id: command.id, command: command) 83 | MASShortcutMonitor.shared().register(command: command) 84 | log.info("row: \(row), shortcut changed: \(String(describing: sender?.shortcutValue))") 85 | } 86 | return cell 87 | } 88 | } 89 | 90 | return nil 91 | } 92 | 93 | // MARK: MASPreferencesViewController 94 | var viewIdentifier: String { 95 | return "KeyBindingsPreferences" 96 | } 97 | 98 | var toolbarItemImage: NSImage? = NSImage(named: NSImage.advancedName) 99 | 100 | var toolbarItemLabel: String? = NSLocalizedString("preference.key.binding", comment: "preference") 101 | } 102 | -------------------------------------------------------------------------------- /TySimulator/Preferences/KeyBindingsPreferencesViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 113 | 117 | 118 | 119 | 120 | 121 | 122 | 132 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /TySimulator/Preferences/Preference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Preference.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/17. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SwiftyUserDefaults 11 | import MASPreferences 12 | import MASShortcut 13 | 14 | public typealias CommandRaw = [String: Any] 15 | 16 | extension Array: DefaultsSerializable where Element == [CommandRaw] { 17 | public static var _defaults: DefaultsKeyedArchiverBridge { 18 | return DefaultsKeyedArchiverBridge() 19 | } 20 | public static var _defaultsArray: DefaultsKeyedArchiverBridge<[Element]> { 21 | fatalError("Multidimensional arrays are not supported yet") 22 | } 23 | } 24 | 25 | extension DefaultsKeys { 26 | static let preferences = DefaultsKey<[String: Any]?>("com.tianyiyan.preferences") 27 | static let onlyAvailableDevices = DefaultsKey("onlyAvailableDevices") 28 | static let onlyHasContentDevices = DefaultsKey("onlyHasContentDevices", defaultValue: false) 29 | static let commands = DefaultsKey<[CommandRaw]>("commands", defaultValue: []) 30 | } 31 | 32 | class Preference: NSObject { 33 | static let sharedWindowController: MASPreferencesWindowController = preferencesWindowController() 34 | static let shared: Preference = Preference() 35 | private(set) var commands: [CommandModel] = [] 36 | @objc dynamic var onlyAvailableDevices: Bool { 37 | didSet { 38 | Defaults[.onlyAvailableDevices] = onlyAvailableDevices 39 | } 40 | } 41 | @objc dynamic var onlyHasContentDevices: Bool { 42 | didSet { 43 | Defaults[.onlyHasContentDevices] = onlyHasContentDevices 44 | } 45 | } 46 | 47 | override init() { 48 | 49 | // init commands 50 | commands = [] 51 | let transformer = CommandTransformer() 52 | for data in Defaults[.commands] { 53 | if let command = transformer.transformedValue(data) as? CommandModel { 54 | MASShortcutMonitor.shared().register(command: command) 55 | commands.append(command) 56 | } 57 | } 58 | 59 | // init switchs 60 | onlyAvailableDevices = Defaults[.onlyAvailableDevices] ?? true 61 | onlyHasContentDevices = Defaults[.onlyHasContentDevices] 62 | 63 | super.init() 64 | } 65 | 66 | func append(_ command: CommandModel) { 67 | append(commands: [command]) 68 | } 69 | 70 | func append(commands: [CommandModel]) { 71 | self.commands.append(contentsOf: commands) 72 | synchronize() 73 | } 74 | 75 | func remove(at index: Int) { 76 | commands.remove(at: index) 77 | synchronize() 78 | } 79 | 80 | func setCommand(id: String, command: CommandModel) { 81 | if let idx = commands.index(where: { $0.id == id }) { 82 | commands[idx] = command 83 | synchronize() 84 | } 85 | } 86 | 87 | func synchronize() { 88 | let transformer = CommandTransformer() 89 | Defaults[.commands] = commands.map { transformer.reverseTransformedValue($0) as! [String: Any] } 90 | UserDefaults.standard.synchronize() 91 | } 92 | 93 | private class func preferencesWindowController() -> MASPreferencesWindowController { 94 | let generalViewController = GeneralPreferencesViewController() 95 | let keyBindingViewController = KeyBindingsPreferencesViewController() 96 | let preferencesWindow = MASPreferencesWindowController(viewControllers: [generalViewController, keyBindingViewController], title: NSLocalizedString("preference.title", comment: "preference")) 97 | preferencesWindow.window?.level = .floating 98 | return preferencesWindow 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /TySimulator/Preferences/ShortcutTableCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShortcutTableCellView.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/20. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import MASShortcut 11 | 12 | class ShortcutTableCellView: NSTableCellView { 13 | @IBOutlet open var shortcutView: MASShortcutView? 14 | } 15 | -------------------------------------------------------------------------------- /TySimulator/Resource/dsa_pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBtjCCASsGByqGSM44BAEwggEeAoGBAJ4rd/Egp/agelXDdTxRcw8LFOdmW4r7 3 | nzufoqw/be/dUFNG3GOzQtrztk/cYHf4W0xG0mRM2Zc/B4guQzU08PI2P122T7X0 4 | DZm6s7u/oJvBqfoTXpcrRryJESqb40nxA8+/MQ4wJZojUhKUps4DpK43GR3iRUY4 5 | iPKFljXhYLnHAhUAtgdgMSUP1tm20F6uiRyDL822jycCgYAJcXKUCshHulPk1iKL 6 | fa/BeDZK5NHhB0VCqmC8JfASOFHhtP/Ax95vzi4vKZIErch1wKk8fNMnMNxvl1Nk 7 | yD6ilT74ST2YARofJ3ouSF8aH1+Wezqc9G+j9s1bm9O5wkna1BIZDuwbnC4m4EvB 8 | rn16rrS4M5BLWtzparSwppZSzwOBhAACgYBDwG+LgJqATzu2J9BvoMEttyWK5H1j 9 | nXhUtO2eTvl3p8ZsGJ0fGu5jAe5in4eJLI/7/92t7kPEA7QunIEsUtyWX80QBUzO 10 | K2iSv5YoXrM72KRaWFpNomumCGvEfVH7Jd08h94zAYQN3JUeGaI4fKpOLs8wQ3Sf 11 | vyTjNkTb4xeDHg== 12 | -----END PUBLIC KEY----- 13 | -------------------------------------------------------------------------------- /TySimulator/Resource/tmp-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/Resource/tmp-logo.png -------------------------------------------------------------------------------- /TySimulator/Script.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Script.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 16/12/29. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | public class Script { 13 | public class func transformedScript(_ script: String) -> String { 14 | var result = script 15 | var res: [NSTextCheckingResult] = [] 16 | do { 17 | let regex = try NSRegularExpression(pattern: "\\$\\{\\{[\\s\\S]*?\\}\\}", options: NSRegularExpression.Options.caseInsensitive) 18 | res = regex.matches(in: script, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSRange(location: 0, length: script.count)) 19 | } catch { 20 | log.error(error) 21 | } 22 | guard res.count > 0 else { 23 | return script 24 | } 25 | for checkingRes in res { 26 | var commandValue = (script as NSString).substring(with: checkingRes.range) 27 | commandValue = (commandValue as NSString).substring(with: NSRange(location: 2, length: commandValue.count - 3) 28 | ) 29 | let command = JSON(parseJSON: commandValue) 30 | let deviceId = command["device"].stringValue 31 | 32 | guard !deviceId.isEmpty else { 33 | log.warning("device id is empty") 34 | continue 35 | } 36 | 37 | guard let device = (deviceId != "booted") ? Simulator.shared.device(udid: deviceId) : Simulator.shared.bootedDevices.first else { 38 | log.warning("no device: \(deviceId)") 39 | continue 40 | } 41 | 42 | if let bundleIdentifier = command["application"].string, 43 | let application = device.application(bundleIdentifier: bundleIdentifier), 44 | let location = application.dataPath.removeTrailingSlash?.absoluteString { 45 | 46 | result = (result as NSString).replacingCharacters(in: checkingRes.range, with: location) 47 | } 48 | } 49 | return result 50 | } 51 | 52 | public class func transformedValue(deviceIdentifier: String, applicationIdentifier: String) -> String { 53 | return "${{\"device\": \"\(deviceIdentifier)\", \"application\": \"\(applicationIdentifier)\"}}" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /TySimulator/Simulator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Simulator.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2018/10/8. 6 | // Copyright © 2018 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Notification.Name { 12 | public struct Device { 13 | public static let DidChange = Notification.Name(rawValue: "com.tianyiyan.notification.device.didChange") 14 | public static let BootedDidChange = Notification.Name(rawValue: "com.tianyiyan.notification.device.booted.didChange") 15 | } 16 | } 17 | 18 | class Simulator { 19 | static let shared = Simulator() 20 | private(set) var devices: [DeviceModel] = [] 21 | private(set) var bootedDevices: [DeviceModel] = [] 22 | var deviceContentToken: NSKeyValueObservation? 23 | var deviceAvailableToken: NSKeyValueObservation? 24 | 25 | init() { 26 | let changeHandler: (Preference, NSKeyValueObservedChange) -> Void = { [weak self] _, _ in 27 | DispatchQueue.global().async { 28 | self?.updateDeivces() 29 | } 30 | } 31 | deviceContentToken = Preference.shared.observe(\.onlyHasContentDevices, options: [.new], changeHandler: changeHandler) 32 | deviceAvailableToken = Preference.shared.observe(\.onlyAvailableDevices, options: [.new], changeHandler: changeHandler) 33 | } 34 | 35 | deinit { 36 | deviceContentToken?.invalidate() 37 | deviceAvailableToken?.invalidate() 38 | } 39 | 40 | func updateDeivces() { 41 | let allDevices = Simulator.listDevices() 42 | let preference = Preference.shared 43 | var filter: [DeviceFilter] = [] 44 | if preference.onlyAvailableDevices { 45 | filter.append(.onlyAvailableDevices) 46 | } 47 | if preference.onlyHasContentDevices { 48 | filter.append(.onlyHasContentDevices) 49 | } 50 | devices = Simulator.listSortedDevices(filter: filter, descending: false) 51 | 52 | let bootedDevices = allDevices.filter { 53 | $0.hasContent && $0.isAvailable && $0.os != .unknown && $0.isOpen 54 | }.sorted { 55 | $0.displayName.compare($1.displayName) == .orderedAscending 56 | } 57 | 58 | DispatchQueue.main.async { 59 | NotificationCenter.default.post(name: Notification.Name.Device.DidChange, object: nil) 60 | } 61 | 62 | let areInIncreasingOrder: ((DeviceModel, DeviceModel) -> Bool) = { (lhs, rhs) -> Bool in 63 | return lhs.udid > rhs.udid 64 | } 65 | 66 | let hasChanged = bootedDevices.sorted(by: areInIncreasingOrder) != self.bootedDevices.sorted(by: areInIncreasingOrder) 67 | self.bootedDevices = bootedDevices 68 | if hasChanged { 69 | DispatchQueue.main.async { 70 | NotificationCenter.default.post(name: Notification.Name.Device.BootedDidChange, object: nil) 71 | } 72 | } 73 | } 74 | 75 | static var devicesDirectory: URL { 76 | let path = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first ?? "" 77 | return URL(fileURLWithPath: path).appendingPathComponent("Developer/CoreSimulator/Devices") 78 | } 79 | 80 | func device(udid: String) -> DeviceModel? { 81 | return devices.first(where: { $0.udid == udid }) 82 | } 83 | } 84 | 85 | extension Simulator { 86 | struct DeviceFilter: OptionSet { 87 | let rawValue: Int 88 | 89 | static let onlyAvailableDevices = DeviceFilter(rawValue: 1 << 1) 90 | static let onlyHasContentDevices = DeviceFilter(rawValue: 1 << 2) 91 | } 92 | 93 | // MARK: Device 94 | 95 | class func listDevices(filter: [DeviceFilter] = []) -> [DeviceModel] { 96 | let data = Process.outputData(launchPath: "/usr/bin/xcrun", arguments: ["simctl", "list", "-j", "devices"]) 97 | 98 | let deviceObjs = try? JSONSerialization.jsonObject(with: data, options: [.allowFragments]) 99 | 100 | var devices: [DeviceModel] = [] 101 | 102 | guard let dic = deviceObjs as? [String: [String: Any]], let deviceDic = dic["devices"] else { 103 | return [] 104 | } 105 | deviceDic.forEach { (key, value) in 106 | let osInfo = key.replacingOccurrences(of: "com.apple.CoreSimulator.SimRuntime.", with: "") 107 | if let arrayValue = value as? [[String: Any]] { 108 | let models = arrayValue.map { DeviceModel(osInfo: osInfo, json: $0) } 109 | devices.append(contentsOf: models) 110 | } 111 | } 112 | 113 | devices = devices.filter { 114 | var result = $0.os != .unknown 115 | if filter.contains(.onlyAvailableDevices) { 116 | result = result && $0.isAvailable 117 | } 118 | if filter.contains(.onlyHasContentDevices) { 119 | result = result && $0.hasContent 120 | } 121 | return result 122 | } 123 | 124 | return devices 125 | } 126 | 127 | class func listSortedDevices(filter: [DeviceFilter] = [], descending: Bool) -> [DeviceModel] { 128 | return listDevices(filter: filter).sorted { 129 | $0.osInfo.compare($1.osInfo) == (descending ? .orderedDescending : .orderedAscending) 130 | } 131 | } 132 | 133 | class func devicePath(udid: String) -> URL { 134 | return Simulator.devicesDirectory.appendingPathComponent("\(udid)") 135 | } 136 | 137 | // MARK: Media 138 | 139 | class func medias(path: URL) -> [MediaModel] { 140 | let directory = path.appendingPathComponent("/data/Media/DCIM") 141 | 142 | return FileManager.directories(directory).map { 143 | let media = MediaModel(name: $0, location: directory.appendingPathComponent($0)) 144 | return media 145 | } 146 | } 147 | 148 | // MARK: Application 149 | 150 | class func applicationsDataPath(deviceUDID: String) -> URL { 151 | let devicePath = Simulator.devicePath(udid: deviceUDID) 152 | return devicePath.appendingPathComponent("data/Containers/Data/Application") 153 | } 154 | 155 | class func applicationsBundlePath(deviceUDID: String) -> URL { 156 | let devicePath = Simulator.devicesDirectory.appendingPathComponent("\(deviceUDID)") 157 | return devicePath.appendingPathComponent("data/Containers/Bundle/Application") 158 | } 159 | 160 | class func applications(deviceUDID: String) -> [ApplicationModel] { 161 | let bundlesDirectory = Simulator.applicationsBundlePath(deviceUDID: deviceUDID) 162 | let applicationDirectories = FileManager.directories(bundlesDirectory) 163 | var result: [ApplicationModel] = [] 164 | for uuid in applicationDirectories { 165 | if let bundle = applicationBundle(deviceUDID: deviceUDID, applicationUUID: uuid), 166 | let dataPath = findApplicationDataPath(deviceUDID: deviceUDID, bundleID: bundle.bundleID), 167 | let application = ApplicationModel(deviceUDID: deviceUDID, uuid: uuid, bundle: bundle, dataPath: dataPath) { 168 | result.append(application) 169 | } 170 | } 171 | return result 172 | } 173 | 174 | class func applicationBundle(deviceUDID: String, applicationUUID: String) -> ApplicationBundle? { 175 | let bundlesDirectory = Simulator.applicationsBundlePath(deviceUDID: deviceUDID) 176 | let bundlePath = bundlesDirectory.appendingPathComponent(applicationUUID) 177 | 178 | guard let appIAPDirectory = FileManager.directories(bundlePath).first, 179 | let json = NSDictionary(contentsOf: bundlePath.appendingPathComponent("\(appIAPDirectory)/Info.plist")), 180 | let bundleID = json["CFBundleIdentifier"] as? String else { 181 | return nil 182 | } 183 | 184 | let name = (json["CFBundleName"] as? String) ?? "Unknow" 185 | let icon: NSImage? 186 | if let bundleIcons = json["CFBundleIcons"] as? [String: Any], 187 | let primaryIcon = bundleIcons["CFBundlePrimaryIcon"] as? [String: Any], 188 | let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String], 189 | let iconFile = iconFiles.last { 190 | icon = NSImage(contentsOf: bundlePath.appendingPathComponent("\(appIAPDirectory)/\(iconFile)@3x.png")) 191 | } else { 192 | icon = nil 193 | } 194 | 195 | return ApplicationBundle(bundleID: bundleID, appName: name, appIcon: icon) 196 | } 197 | 198 | class func findApplicationDataPath(deviceUDID: String, bundleID: String) -> URL? { 199 | let directory = Simulator.applicationsDataPath(deviceUDID: deviceUDID) 200 | 201 | let plist = ".com.apple.mobile_container_manager.metadata.plist" 202 | for udid in FileManager.directories(directory) { 203 | let dataPath = directory.appendingPathComponent(udid) 204 | let plistPath = dataPath.appendingPathComponent(plist) 205 | guard let json = NSDictionary(contentsOf: plistPath), 206 | let metaDataIdentifier = json["MCMMetadataIdentifier"] as? String, 207 | metaDataIdentifier == bundleID else { 208 | continue 209 | } 210 | 211 | return dataPath 212 | } 213 | return nil 214 | } 215 | 216 | // MARK: AppGroup 217 | 218 | class func appGroups(deviceUDID: String) -> [AppGroupModel] { 219 | let devicePath = Simulator.devicesDirectory.appendingPathComponent("\(deviceUDID)") 220 | let directory = devicePath.appendingPathComponent("/data/Containers/Shared/AppGroup") 221 | return FileManager.directories(directory).map { 222 | let appGroup = AppGroupModel() 223 | appGroup.location = directory.appendingPathComponent($0) 224 | 225 | let plistPath = appGroup.location!.appendingPathComponent("/.com.apple.mobile_container_manager.metadata.plist") 226 | let json = NSDictionary(contentsOf: plistPath) 227 | 228 | appGroup.bundleIdentifier = json?["MCMMetadataIdentifier"] as? String ?? "" 229 | 230 | return appGroup 231 | }.filter { 232 | return !$0.bundleIdentifier.contains("com.apple") 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /TySimulator/TySimulator-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // TySimulator-Bridging-Header.h 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/24. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | #ifndef TySimulator_Bridging_Header_h 10 | #define TySimulator_Bridging_Header_h 11 | 12 | #import // required for MD5 13 | #import 14 | 15 | #endif /* TySimulator_Bridging_Header_h */ 16 | -------------------------------------------------------------------------------- /TySimulator/TySimulator.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | com.apple.security.cs.disable-library-validation 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TySimulator/Utils/Application.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Application.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/12/1. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSApplication { 12 | func showFeedbackWindow() { 13 | DevMateKit.showFeedbackDialog(nil, in: .modalMode) 14 | } 15 | 16 | @objc func showPreferencesWindow() { 17 | (NSApp.delegate as? AppDelegate)?.mainMenuController?.closePopover(sender: nil) 18 | let windowController = Preference.sharedWindowController 19 | windowController.select(at: 0) 20 | windowController.showWindow(nil) 21 | NSApp.activate(ignoringOtherApps: true) 22 | } 23 | 24 | @objc func showAboutWindow() { 25 | (NSApp.delegate as? AppDelegate)?.mainMenuController?.closePopover(sender: nil) 26 | NSApp.orderFrontStandardAboutPanel(nil) 27 | NSApp.activate(ignoringOtherApps: true) 28 | } 29 | 30 | func checkForUpdates() { 31 | DM_SUUpdater.shared().checkForUpdates(NSApp) 32 | } 33 | 34 | @discardableResult 35 | public class func toggleDockIcon(showIcon state: Bool) -> Bool { 36 | return NSApp.setActivationPolicy(state ? .regular : .accessory) 37 | } 38 | 39 | } 40 | 41 | // MARK: Launch 42 | extension NSApplication { 43 | 44 | class var isLaunchAtStartup: Bool { 45 | set { 46 | if newValue == isLaunchAtStartup { 47 | return 48 | } 49 | guard let loginItemsRef = LSSharedFileListCreate( nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil).takeRetainedValue() as LSSharedFileList? else { 50 | return 51 | } 52 | let itemReferences = itemReferencesInLoginItems() 53 | if newValue { 54 | let appUrl = NSURL(fileURLWithPath: Bundle.main.bundlePath) 55 | LSSharedFileListInsertItemURL(loginItemsRef, itemReferences.lastReference, nil, nil, appUrl, nil, nil) 56 | } else if let itemRef = itemReferences.existingReference { 57 | LSSharedFileListItemRemove(loginItemsRef, itemRef) 58 | } 59 | } 60 | 61 | get { 62 | return itemReferencesInLoginItems().existingReference != nil 63 | } 64 | } 65 | 66 | private class func itemReferencesInLoginItems() -> (existingReference: LSSharedFileListItem?, lastReference: LSSharedFileListItem?) { 67 | let appURL: NSURL = NSURL.fileURL(withPath: Bundle.main.bundlePath) as NSURL 68 | let loginItemsRef = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil).takeRetainedValue() 69 | let loginItems: NSArray = LSSharedFileListCopySnapshot(loginItemsRef, nil).takeRetainedValue() as NSArray 70 | 71 | let lastItemRef: LSSharedFileListItem = loginItems.lastObject as! LSSharedFileListItem 72 | 73 | for ref in loginItems.map({ $0 as! LSSharedFileListItem }) { 74 | if let itemURL = LSSharedFileListItemCopyResolvedURL(ref, 0, nil), 75 | (itemURL.takeRetainedValue() as NSURL).isEqual(appURL) { 76 | return (ref, lastItemRef) 77 | } 78 | } 79 | return (nil, lastItemRef) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /TySimulator/Utils/FileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManager.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/13. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension FileManager { 12 | 13 | class func directories(_ path: URL) -> [String] { 14 | let results = try? FileManager.default.contentsOfDirectory(atPath: path.path).filter { 15 | return isDirectory(path.appendingPathComponent("\($0)").path, name: $0) 16 | } 17 | 18 | return results ?? [] 19 | } 20 | 21 | class func isDirectory(_ path: String, name: String) -> Bool { 22 | var flag = ObjCBool(false) 23 | FileManager.default.fileExists(atPath: path, isDirectory: &flag) 24 | 25 | return flag.boolValue && !name.hasPrefix(".") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /TySimulator/Utils/MD5.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MD5.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/24. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | func md5() -> String { 13 | let str = cString(using: String.Encoding.utf8) 14 | let strLen = CUnsignedInt(lengthOfBytes(using: String.Encoding.utf8)) 15 | let digestLen = Int(CC_MD5_DIGEST_LENGTH) 16 | let result = UnsafeMutablePointer.allocate(capacity: digestLen) 17 | 18 | CC_MD5(str!, strLen, result) 19 | 20 | let hash = NSMutableString() 21 | for idx in 0 ..< digestLen { 22 | hash.appendFormat("%02x", result[idx]) 23 | } 24 | // https://stackoverflow.com/a/48570455/5701954 25 | // result.deinitialize() 26 | 27 | return String(format: hash as String) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /TySimulator/Utils/Process.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Process.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/13. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Process { 12 | class func output(launchPath: String, arguments: [String], directoryPath: URL? = nil) -> String { 13 | let data = outputData(launchPath: launchPath, arguments: arguments, directoryPath: directoryPath) 14 | return String(data: data, encoding: .utf8) ?? "" 15 | } 16 | 17 | class func outputData(launchPath: String, arguments: [String], directoryPath: URL? = nil) -> Data { 18 | let output = Pipe() 19 | 20 | let task = Process() 21 | task.launchPath = launchPath 22 | task.arguments = arguments 23 | task.standardOutput = output 24 | 25 | if let path = directoryPath?.removeTrailingSlash?.path { 26 | task.currentDirectoryPath = path 27 | } 28 | 29 | task.launch() 30 | 31 | // For some reason [task waitUntilExit]; does not return sometimes. Therefore this rather hackish solution: 32 | var count = 0 33 | while task.isRunning && count < 10 { 34 | Thread.sleep(forTimeInterval: 0.1) 35 | count += 1 36 | } 37 | 38 | return output.fileHandleForReading.readDataToEndOfFile() 39 | } 40 | 41 | @discardableResult 42 | class func execute(_ script: String) -> String { 43 | let scriptCLI = Script.transformedScript(script) 44 | log.info("run script: \(scriptCLI)") 45 | return output(launchPath: "/bin/sh", arguments: ["-c", scriptCLI]) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /TySimulator/Utils/ShortcutMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShortcutMonitor.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/24. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import MASShortcut 10 | 11 | extension MASShortcutMonitor { 12 | func register(command: CommandModel) { 13 | guard command.key != nil else { 14 | log.warning("register faild, command shortcut is nil") 15 | return 16 | } 17 | register(command.key, withAction: { 18 | NSSound(named: "Ping")?.play() 19 | log.debug("script: \(command.script)") 20 | Process.execute(command.script) 21 | }) 22 | } 23 | 24 | func unregister(command: CommandModel) { 25 | guard command.key != nil else { 26 | log.warning("unregister faild, command shortcut is nil") 27 | return 28 | } 29 | unregisterShortcut(command.key) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /TySimulator/Utils/TextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextView.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/30. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | private var placeHolderKey = 0 12 | extension NSTextView { 13 | static func awake() { 14 | let swizzlingClosure: () = { 15 | NSTextView.swizzleBecomeFirstResponder 16 | NSTextView.swizzleDraw 17 | }() 18 | 19 | swizzlingClosure 20 | } 21 | 22 | private var placeHolder: NSAttributedString? { 23 | get { 24 | return objc_getAssociatedObject(self, &placeHolderKey) as? NSAttributedString 25 | } 26 | 27 | set { 28 | objc_setAssociatedObject(self, &placeHolderKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC) 29 | } 30 | } 31 | 32 | var placeHolderString: String? { 33 | set { 34 | if let string = newValue { 35 | placeHolder = NSAttributedString(string: string, attributes: [NSAttributedString.Key.foregroundColor: NSColor.gray]) 36 | needsDisplay = true 37 | } 38 | } 39 | 40 | get { 41 | return placeHolder?.string 42 | } 43 | } 44 | 45 | private static let swizzleBecomeFirstResponder: Void = { 46 | let original = #selector(NSTextView.becomeFirstResponder) 47 | let replacement = #selector(NSTextView.swizzle_becomeFirstResponder) 48 | swizzleFunction(original: original, replacement: replacement) 49 | }() 50 | 51 | private static let swizzleDraw: Void = { 52 | let original = #selector(NSTextView.draw(_:)) 53 | let replacement = #selector(NSTextView.swizzle_draw(_:)) 54 | swizzleFunction(original: original, replacement: replacement) 55 | }() 56 | 57 | private class func swizzleFunction(original: Selector, replacement: Selector) { 58 | guard let originalMethod: Method = class_getInstanceMethod(NSTextView.self, original), 59 | let replacementMethod: Method = class_getInstanceMethod(NSTextView.self, replacement) else { 60 | return 61 | } 62 | 63 | let originalImplementation: IMP = method_getImplementation(originalMethod) 64 | let originalArgTypes = method_getTypeEncoding(originalMethod) 65 | 66 | let replacementImplementation: IMP = method_getImplementation(replacementMethod) 67 | let replacementArgTypes = method_getTypeEncoding(replacementMethod) 68 | 69 | if class_addMethod(NSTextView.self, original, replacementImplementation, replacementArgTypes) { 70 | class_replaceMethod(NSTextView.self, replacement, originalImplementation, originalArgTypes) 71 | } else { 72 | method_exchangeImplementations(originalMethod, replacementMethod) 73 | } 74 | } 75 | 76 | @objc func swizzle_becomeFirstResponder() -> Bool { 77 | needsDisplay = true 78 | return swizzle_becomeFirstResponder() 79 | } 80 | 81 | @objc func swizzle_draw(_ dirtyRect: NSRect) { 82 | swizzle_draw(dirtyRect) 83 | guard string.isEmpty else { 84 | return 85 | } 86 | if let placeHolder = placeHolder { 87 | placeHolder.draw(in: NSRect(x: 5, y: 0, width: bounds.size.width - 2 * 5, height: bounds.size.height)) 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /TySimulator/Utils/URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/13. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URL { 12 | var removeTrailingSlash: URL? { 13 | guard absoluteString.hasSuffix("/") else { 14 | return self 15 | } 16 | var urlString = absoluteString 17 | urlString.removeLast() 18 | return URL(string: urlString) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TySimulator/Utils/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/12/3. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension NSViewController { 12 | static func awake() { 13 | let swizzlingClosure: () = { 14 | NSViewController.swizzleViewDidAppear 15 | NSViewController.swizzleViewDidDisappear 16 | }() 17 | 18 | swizzlingClosure 19 | } 20 | 21 | private static let swizzleViewDidAppear: Void = { 22 | let original = #selector(NSViewController.viewDidAppear) 23 | let replacement = #selector(NSViewController.ty_viewDidAppear) 24 | swizzleFunction(original: original, replacement: replacement) 25 | }() 26 | 27 | private static let swizzleViewDidDisappear: Void = { 28 | let original = #selector(NSViewController.viewDidDisappear) 29 | let replacement = #selector(NSViewController.ty_viewDidDisappear) 30 | swizzleFunction(original: original, replacement: replacement) 31 | }() 32 | 33 | private class func swizzleFunction(original: Selector, replacement: Selector) { 34 | guard let originalMethod: Method = class_getInstanceMethod(NSViewController.self, original), 35 | let replacementMethod: Method = class_getInstanceMethod(NSViewController.self, replacement) else { 36 | return 37 | } 38 | 39 | let originalImplementation: IMP = method_getImplementation(originalMethod) 40 | let originalArgTypes = method_getTypeEncoding(originalMethod) 41 | 42 | let replacementImplementation: IMP = method_getImplementation(replacementMethod) 43 | let replacementArgTypes = method_getTypeEncoding(replacementMethod) 44 | 45 | if class_addMethod(NSViewController.self, original, replacementImplementation, replacementArgTypes) { 46 | class_replaceMethod(NSViewController.self, replacement, originalImplementation, originalArgTypes) 47 | } else { 48 | method_exchangeImplementations(originalMethod, replacementMethod) 49 | } 50 | } 51 | 52 | @objc func ty_viewDidAppear() { 53 | ty_viewDidAppear() 54 | 55 | guard className != "NSTouchBarViewController" else { 56 | return 57 | } 58 | if NSApp.windows.contains(where: { $0.isVisible }) { 59 | NSApplication.toggleDockIcon(showIcon: true) 60 | } 61 | } 62 | 63 | @objc func ty_viewDidDisappear() { 64 | ty_viewDidDisappear() 65 | 66 | guard className != "NSTouchBarViewController" else { 67 | return 68 | } 69 | if NSApp.windows.contains(where: { $0.isVisible && String(describing: type(of: $0)) != "NSStatusBarWindow" }) { 70 | return 71 | } 72 | NSApplication.toggleDockIcon(showIcon: false) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /TySimulator/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/13. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SwiftyBeaver 11 | 12 | let log = SwiftyBeaver.self 13 | let console = ConsoleDestination() 14 | log.addDestination(console) 15 | 16 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) 17 | -------------------------------------------------------------------------------- /TySimulator/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/zh-Hans.lproj/Localizable.strings -------------------------------------------------------------------------------- /TySimulator/zh-Hant.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulator/zh-Hant.lproj/Localizable.strings -------------------------------------------------------------------------------- /TySimulatorTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TySimulatorTests/RecentTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecentTests.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2017/7/1. 6 | // Copyright © 2017年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | @testable import TySimulator 12 | 13 | class RecentTests: QuickSpec { 14 | override func spec() { 15 | describe("LRU", closure: { 16 | let datas: [Int] = Array(0..<10) 17 | let capacity = 5 18 | context("append datas", { 19 | let lru = LRU(capacity: capacity) 20 | for data in datas { 21 | lru[data] = 0 22 | } 23 | it("count should be equal capacity", closure: { 24 | expect(lru.count).to(equal(capacity)) 25 | }) 26 | 27 | it("data", closure: { 28 | expect(lru.datas.map{ $0.key }).to(equal(Array(5..<10).reversed())) 29 | }) 30 | }) 31 | 32 | context("get value", { 33 | let lru = LRU(capacity: capacity) 34 | for data in datas { 35 | lru[data] = data 36 | } 37 | it("value should be equal seted", closure: { 38 | for data in 5..<10 { 39 | expect(lru[data]).to(equal(data)) 40 | } 41 | }) 42 | }) 43 | 44 | context("change value", { 45 | let lru = LRU(capacity: capacity) 46 | lru[10] = 10 47 | lru[10] = 20 48 | it("count", closure: { 49 | expect(lru.count).to(equal(1)) 50 | }) 51 | it("value should be changed", closure: { 52 | expect(lru[10]).to(equal(20)) 53 | }) 54 | }) 55 | 56 | }) 57 | 58 | describe("LRU-K", closure: { 59 | let datas: [Int] = Array(0..<10) 60 | let capacity = 5 61 | context("append datas", { 62 | let lruk = LRUK(capacity: capacity, bufferSize: 10) 63 | for data in datas { 64 | lruk[data] = 0 65 | } 66 | it("should be empty", closure: { 67 | expect(lruk.count).to(equal(0)) 68 | }) 69 | 70 | it("has cache", closure: { 71 | lruk[0] = 3 72 | expect(lruk.count).to(equal(1)) 73 | expect(lruk[0]).to(equal(3)) 74 | }) 75 | }) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /TySimulatorTests/Resource/script-format.txt: -------------------------------------------------------------------------------- 1 | open ${{ 2 | "device": "booted", 3 | "application": "com.tianyiyan.note" 4 | }} -------------------------------------------------------------------------------- /TySimulatorTests/ScriptTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScriptTests.swift 3 | // TySimulator 4 | // 5 | // Created by ty0x2333 on 2016/11/25. 6 | // Copyright © 2016年 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | @testable import TySimulator 12 | 13 | func content(of filename: String) -> String { 14 | let path = Bundle(for: ScriptTests.self).path(forResource: filename, ofType: "txt")! 15 | return try! String(contentsOfFile: path) 16 | } 17 | 18 | class ScriptTests: QuickSpec { 19 | override func spec() { 20 | describe("parsing script", closure: { 21 | 22 | // context("input valid command", { 23 | // let script = content(of: "script-format") 24 | // it("should be parsed", closure: { 25 | // let transformed = Script.transformedScript(script) 26 | // expect(transformed).toNot(equal(script)) 27 | // }) 28 | // }) 29 | 30 | context("input not valid command", { 31 | afterEach { 32 | print("after not") 33 | } 34 | let command = "${{xxxx}}" 35 | let script = "open \(command)" 36 | it("should be parsed", closure: { 37 | let transformed = Script.transformedScript(script) 38 | expect(transformed).to(equal(script)) 39 | }) 40 | }) 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /TySimulatorTests/SimulatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimulatorTests.swift 3 | // TySimulatorTests 4 | // 5 | // Created by ty0x2333 on 2018/10/8. 6 | // Copyright © 2018 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | @testable import TySimulator 12 | 13 | class SimulatorTests: QuickSpec { 14 | override func spec() { 15 | describe("Simulator", closure: { 16 | it("list Devices", closure: { 17 | 18 | }) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TySimulatorTests/TestFinder/Empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulatorTests/TestFinder/Empty -------------------------------------------------------------------------------- /TySimulatorTests/TestFinder/SubFinder/Empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulatorTests/TestFinder/SubFinder/Empty -------------------------------------------------------------------------------- /TySimulatorTests/TestFinder/SubFinder/SubSubFinder/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulatorTests/TestFinder/SubFinder/SubSubFinder/.gitkeep -------------------------------------------------------------------------------- /TySimulatorTests/TestFinder/SubFinder2/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/TySimulatorTests/TestFinder/SubFinder2/.gitkeep -------------------------------------------------------------------------------- /TySimulatorTests/UtilTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UtilTests.swift 3 | // TySimulatorTests 4 | // 5 | // Created by ty0x2333 on 2018/10/8. 6 | // Copyright © 2018 ty0x2333. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | @testable import TySimulator 12 | 13 | class UtilTests: QuickSpec { 14 | override func spec() { 15 | describe("URL", closure: { 16 | context("removeTrailingSlash", { 17 | let result = "https://tyy.sh" 18 | it("removed trailing slash", closure: { 19 | expect(URL(string: "https://tyy.sh/")?.removeTrailingSlash?.absoluteString).to(equal(result)) 20 | }) 21 | 22 | it("do nothing", closure: { 23 | expect(URL(string: "https://tyy.sh")?.removeTrailingSlash?.absoluteString).to(equal(result)) 24 | }) 25 | }) 26 | }) 27 | 28 | describe("String", closure: { 29 | context("md5", { 30 | it("had generated", closure: { 31 | expect("https://tyy.sh".md5()).to(equal("651949dc4ffadf4d933ac565796de4cd")) 32 | }) 33 | }) 34 | }) 35 | 36 | describe("FileManager", closure: { 37 | context("directories", { 38 | let bundle = Bundle(for: UtilTests.self) 39 | it("find directories", closure: { 40 | let path = bundle.path(forResource: "TestFinder", ofType: nil) 41 | let directories = FileManager.directories(URL(string: path!)!) 42 | expect(directories.contains("SubFinder")).to(beTrue()) 43 | expect(directories.contains("SubFinder2")).to(beTrue()) 44 | expect(directories.count).to(equal(2)) 45 | }) 46 | }) 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | token: 3f211234-cd1c-409e-8d35-9f9787cf714f 3 | -------------------------------------------------------------------------------- /resources/DMG Canvas.dmgCanvas/Disk Image: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/resources/DMG Canvas.dmgCanvas/Disk Image -------------------------------------------------------------------------------- /resources/DMG Canvas.dmgCanvas/QuickLook/Preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/resources/DMG Canvas.dmgCanvas/QuickLook/Preview.jpg -------------------------------------------------------------------------------- /resources/dmg-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/resources/dmg-background.png -------------------------------------------------------------------------------- /resources/icon-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/resources/icon-off.png -------------------------------------------------------------------------------- /resources/icon-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/resources/icon-on.png -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/resources/logo.png -------------------------------------------------------------------------------- /resources/menu-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/resources/menu-icon.png -------------------------------------------------------------------------------- /resources/tysimulator-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/resources/tysimulator-logo.png -------------------------------------------------------------------------------- /resources/tysimulator-logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ty0x2333/TySimulator/92719fffc648133d4c68627e62ff97af377c59fc/resources/tysimulator-logo.psd -------------------------------------------------------------------------------- /scripts/ExportOptions.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | destination 6 | export 7 | method 8 | developer-id 9 | provisioningProfiles 10 | 11 | com.tianyiyan.TySimulator 12 | TySimulator Develop ID 13 | 14 | signingCertificate 15 | Developer ID Application 16 | signingStyle 17 | manual 18 | teamID 19 | 9KYD7S7AZY 20 | 21 | 22 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | CUR_DIR=$(cd `dirname $0`; pwd) 4 | 5 | WORKSPACE_PATH=$CUR_DIR/../TySimulator.xcworkspace 6 | BUILD_PATH=$CUR_DIR/../build 7 | ARCHIVE_PATH=$BUILD_PATH/TySimulator.xcarchive 8 | DST_DSYMS_PATH=$BUILD_PATH/dSYMs 9 | XCODE_BUILD="xcodebuild archive ONLY_ACTIVE_ARCH=NO -workspace $WORKSPACE_PATH -scheme TySimulator -archivePath $ARCHIVE_PATH -configuration Release $*" 10 | 11 | if [ ! -d $BUILD_PATH ]; then 12 | mkdir $BUILD_PATH 13 | fi 14 | if [ ! -d $DST_DSYMS_PATH ]; then 15 | mkdir $DST_DSYMS_PATH 16 | fi 17 | 18 | if [ ! $(command -v xcpretty) ]; then 19 | $XCODE_BUILD 20 | else 21 | set -o pipefail && $XCODE_BUILD | xcpretty 22 | fi 23 | 24 | cp -r $BUILD_PATH/TySimulator.xcarchive/Products/Applications/TySimulator.app $BUILD_PATH/TySimulator-unsign.app 25 | 26 | cd $ARCHIVE_PATH/dSYMs 27 | for filename in $(ls $ARCHIVE_PATH/dSYMs); do 28 | zip -r $BUILD_PATH/dSYMs/$filename.zip $filename 29 | done 30 | -------------------------------------------------------------------------------- /scripts/export_archive: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | CUR_DIR=$(cd `dirname $0`; pwd) 4 | BUILD_PATH=$CUR_DIR/../build 5 | ARCHIVE_PATH=$BUILD_PATH/TySimulator.xcarchive 6 | 7 | cp $CUR_DIR/ExportOptions.plist $BUILD_PATH/ExportOptions.plist 8 | 9 | xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportOptionsPlist $BUILD_PATH/ExportOptions.plist -exportPath $BUILD_PATH 10 | 11 | codesign -vvvv $BUILD_PATH/TySimulator.app 12 | -------------------------------------------------------------------------------- /scripts/generate_app_icon: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SRC_FILE=$1 3 | DST_PATH=$2 4 | 5 | if [ -z "$DST_PATH" ]; then 6 | DST_PATH=$(dirname $0) 7 | fi 8 | 9 | error() { 10 | local red="\033[1;31m" 11 | local normal="\033[0m" 12 | echo "[${red}ERROR${normal}] $1" 13 | } 14 | 15 | # Check ImageMagick 16 | command -v convert >/dev/null 2>&1 || { error >&2 "The ImageMagick is not installed. Please install it first.see http://www.imagemagick.org/"; exit -1; } 17 | 18 | if [ -z $SRC_FILE ] 19 | then 20 | echo "No argument given" 21 | else 22 | convert "$SRC_FILE" -resize 16x16 "$DST_PATH/Icon-16.png" 23 | convert "$SRC_FILE" -resize 32x32 "$DST_PATH/Icon-16@2x.png" 24 | convert "$SRC_FILE" -resize 32x32 "$DST_PATH/Icon-32.png" 25 | convert "$SRC_FILE" -resize 64x64 "$DST_PATH/Icon-32@2x.png" 26 | convert "$SRC_FILE" -resize 128x128 "$DST_PATH/Icon-128.png" 27 | convert "$SRC_FILE" -resize 256x256 "$DST_PATH/Icon-128@2x.png" 28 | convert "$SRC_FILE" -resize 256x256 "$DST_PATH/Icon-256.png" 29 | convert "$SRC_FILE" -resize 512x512 "$DST_PATH/Icon-256@2x.png" 30 | convert "$SRC_FILE" -resize 512x512 "$DST_PATH/Icon-512.png" 31 | convert "$SRC_FILE" -resize 1024x1024 "$DST_PATH/Icon-512@2x.png" 32 | fi 33 | -------------------------------------------------------------------------------- /scripts/generate_menu_icon: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SRC_FILE=$1 3 | DST_PATH=$2 4 | 5 | if [ -z "$DST_PATH" ]; then 6 | DST_PATH=$(dirname $0) 7 | fi 8 | 9 | error() { 10 | local red="\033[1;31m" 11 | local normal="\033[0m" 12 | echo "[${red}ERROR${normal}] $1" 13 | } 14 | 15 | # Check ImageMagick 16 | command -v convert >/dev/null 2>&1 || { error >&2 "The ImageMagick is not installed. Please install it first.see http://www.imagemagick.org/"; exit -1; } 17 | 18 | if [ -z $SRC_FILE ] 19 | then 20 | echo "No argument given" 21 | else 22 | convert "$SRC_FILE" -resize 18 "$DST_PATH/Icon-18.png" 23 | convert "$SRC_FILE" -resize 36 "$DST_PATH/Icon-18@2x.png" 24 | convert "$SRC_FILE" -resize 54 "$DST_PATH/Icon-18@3x.png" 25 | fi 26 | -------------------------------------------------------------------------------- /scripts/i18n: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | required_ver="1.2.0" 3 | 4 | if [ ! $(command -v tystrings) ]; then 5 | echo "❗️ tystrings command not found. https://github.com/luckytianyiyan/TyStrings" 6 | echo "sudo pip install tystrings" 7 | exit 1 8 | elif [ ! `python -c "import tystrings; from pkg_resources import parse_version; print 1 if parse_version(tystrings.__version__) >= parse_version(\"${required_ver}\") else 0;"` ] 9 | then 10 | echo "❗️ require tystrings version $required_ver" 11 | echo "sudo pip install tystrings --upgrade" 12 | exit 1 13 | fi 14 | 15 | CUR_DIR=$(dirname $0) 16 | PROJ_DIR=$CUR_DIR/../TySimulator 17 | tystrings generate $(find $PROJ_DIR -name \*.m -o -name \*.swift) -o $PROJ_DIR/Base.lproj $PROJ_DIR/zh-Hans.lproj $PROJ_DIR/zh-Hant.lproj 18 | echo "Press any key to continue" 19 | read -n 1 -s -p " " 20 | tystrings translate $PROJ_DIR/zh-Hans.lproj/Localizable.strings $PROJ_DIR/zh-Hant.lproj/Localizable.strings --src-lang zh --dst-lang cht --appid 20160709000024959 --secret ke4UYwwvvgV9iQEIVjrC 21 | -------------------------------------------------------------------------------- /scripts/pack: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | VERSION=`sed -n '/MARKETING_VERSION/{s/MARKETING_VERSION = //;s/;//;s/^[[:space:]]*//;p;q;}' ./TySimulator.xcodeproj/project.pbxproj` 3 | dmgcanvas resources/DMG\ Canvas.dmgCanvas build/TySimulator\ $VERSION.dmg -volumeName TySimulator\ $VERSION -setTextString textObject3 $VERSION --------------------------------------------------------------------------------