├── .github ├── AppIcon.png └── screenshot.png ├── .gitignore ├── .swiftlint-version ├── .swiftlint.yml ├── .travis.yml ├── Brisk.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── Brisk.xcscheme ├── Brisk.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Brisk ├── AppDelegate.swift ├── Application.swift ├── Constants.swift ├── Controllers │ ├── AppleRadarPreferencesViewController.swift │ ├── AuthenticationViewController.swift │ ├── CascadingWindowController.swift │ ├── FileDuplicateViewController.swift │ ├── OpenRadarPreferencesViewController.swift │ ├── RadarViewController.swift │ ├── TabViewController.swift │ └── ViewController.swift ├── Extensions │ ├── Attachment+Serialization.swift │ ├── Data+Extension.swift │ ├── Dictionary+Extension.swift │ ├── NSDocumentController+Extension.swift │ ├── NSPopUpButton+Extension.swift │ ├── NSProgressIndicator+Extension.swift │ ├── NSStatusItem+Extension.swift │ ├── NSStoryboard+Extension.swift │ ├── NSTextField+Extension.swift │ ├── NSTextView+Extension.swift │ ├── Radar+OpenRadar.swift │ ├── Radar+Serialization.swift │ └── String+Extension.swift ├── GlobalHotKey.swift ├── Keychain.swift ├── Models │ ├── OpenRadar.swift │ ├── RadarDocument.swift │ ├── Result.swift │ └── Validatable.swift ├── Reproducability.swift ├── Resources │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── 1024.png │ │ │ ├── 128.png │ │ │ ├── 16.png │ │ │ ├── 256.png │ │ │ ├── 32.png │ │ │ ├── 512.png │ │ │ ├── 64.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── StatusItemIcon.imageset │ │ │ ├── Contents.json │ │ │ ├── StatusItemIcon.png │ │ │ └── StatusItemIcon@2x.png │ │ └── radar.imageset │ │ │ ├── Contents.json │ │ │ └── radar.png │ ├── Base.lproj │ │ └── Main.storyboard │ ├── Info.plist │ └── dsa_pub.pem ├── StoryboardRouter.swift └── Views │ ├── AttachmentDroppableView.swift │ └── TextView.swift ├── BriskTests ├── .swiftlint.yml ├── DictionaryExtensionTests.swift ├── OpenRadarTests.swift ├── RadarIDParsingTests.swift ├── RadarSerializationTests.swift ├── Resources │ ├── Info.plist │ ├── openradar.json │ ├── openradarstrings.json │ └── radar.json ├── ResultTests.swift └── StringExtensionTests.swift ├── BriskUITests ├── BriskUITests.swift └── Resources │ └── Info.plist ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── Graphics ├── AppIcon.psd └── StatusItemIcon.psd ├── LICENSE ├── Makefile ├── Podfile ├── Podfile.lock ├── README.md ├── RELEASING.md └── appcast.xml /.github/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/.github/AppIcon.png -------------------------------------------------------------------------------- /.github/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/.github/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | Pods 3 | swiftlint 4 | xcuserdata 5 | -------------------------------------------------------------------------------- /.swiftlint-version: -------------------------------------------------------------------------------- 1 | 0.24.2 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Brisk 3 | - BriskTests 4 | - BriskUITests 5 | disabled_rules: 6 | - force_cast 7 | - opening_brace 8 | - prohibited_super_call 9 | - type_name 10 | - variable_name 11 | opt_in_rules: 12 | - attributes 13 | - closure_end_indentation 14 | - closure_spacing 15 | - conditional_returns_on_newline 16 | - explicit_init 17 | - first_where 18 | - operator_usage_whitespace 19 | - overridden_super_call 20 | - private_outlet 21 | - prohibited_super_call 22 | - redundant_nil_coalescing 23 | - sorted_imports 24 | - switch_case_on_newline 25 | - trailing_comma 26 | line_length: 27 | - 110 28 | trailing_comma: 29 | mandatory_comma: true 30 | private_outlet: 31 | allow_private_set: true 32 | attributes: 33 | always_on_line_above: 34 | - "@discardableResult" 35 | - "@IBDesignable" 36 | - "@nonobjc" 37 | - "@objc" 38 | always_on_same_line: 39 | - "@IBAction" 40 | - "@IBInspectable" 41 | - "@IBOutlet" 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - os: osx 4 | env: ACTION=test 5 | - os: osx 6 | env: ACTION=lint 7 | 8 | language: swift 9 | osx_image: xcode9.3 10 | 11 | install: true 12 | script: 13 | - make ci 14 | -------------------------------------------------------------------------------- /Brisk.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1BA2CE444CBF0C961AEF74B3 /* Pods_Brisk.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C88425AC44C8AB4389F2A583 /* Pods_Brisk.framework */; }; 11 | 493890561F157F16008F9E94 /* AttachmentDroppableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493890551F157F16008F9E94 /* AttachmentDroppableView.swift */; }; 12 | BADCFD626E46B81044A115B2 /* Pods_BriskTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0018868F17477C7D609957AD /* Pods_BriskTests.framework */; }; 13 | C2042B531D17397900DEF594 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2042B521D17397900DEF594 /* ViewController.swift */; }; 14 | C20DDC121D16496E0086D304 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20DDC111D16496E0086D304 /* AuthenticationViewController.swift */; }; 15 | C20DDC141D1652C20086D304 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20DDC131D1652C20086D304 /* Keychain.swift */; }; 16 | C20DDC161D1673670086D304 /* NSStoryboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20DDC151D1673670086D304 /* NSStoryboard+Extension.swift */; }; 17 | C20DDC181D16771A0086D304 /* StoryboardRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20DDC171D16771A0086D304 /* StoryboardRouter.swift */; }; 18 | C20DDC1A1D167D5F0086D304 /* RadarDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20DDC191D167D5F0086D304 /* RadarDocument.swift */; }; 19 | C219CBD81D173602002F89FC /* NSStatusItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219CBD71D173602002F89FC /* NSStatusItem+Extension.swift */; }; 20 | C219CBDA1D1736EB002F89FC /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219CBD91D1736EB002F89FC /* Application.swift */; }; 21 | C21DAA171F00433C0016E8AA /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = C21DAA161F00433C0016E8AA /* dsa_pub.pem */; }; 22 | C23ED5061EFA14E1006988BA /* FileDuplicateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23ED5041EFA1174006988BA /* FileDuplicateViewController.swift */; }; 23 | C23ED5081EFA1FD1006988BA /* NSDocumentController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23ED5071EFA1FD1006988BA /* NSDocumentController+Extension.swift */; }; 24 | C23ED50A1EFA2362006988BA /* NSProgressIndicator+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23ED5091EFA2362006988BA /* NSProgressIndicator+Extension.swift */; }; 25 | C23ED50C1EFA2DA9006988BA /* RadarIDParsingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23ED50B1EFA2DA9006988BA /* RadarIDParsingTests.swift */; }; 26 | C255C4431DAB7A9C00383D0A /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C255C4421DAB7A9C00383D0A /* ResultTests.swift */; }; 27 | C27694501EFCD461009C3FA1 /* StringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C276944F1EFCD461009C3FA1 /* StringExtensionTests.swift */; }; 28 | C27694521EFD094F009C3FA1 /* OpenRadar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27694511EFD094F009C3FA1 /* OpenRadar.swift */; }; 29 | C27694541EFD097C009C3FA1 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27694531EFD097C009C3FA1 /* String+Extension.swift */; }; 30 | C27694561EFD0BD4009C3FA1 /* openradarstrings.json in Resources */ = {isa = PBXBuildFile; fileRef = C27694551EFD0BD4009C3FA1 /* openradarstrings.json */; }; 31 | C27A5A171E721BD300391E63 /* Attachment+Serialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27A5A161E721BD300391E63 /* Attachment+Serialization.swift */; }; 32 | C27DBF221EF8F039007666C3 /* OpenRadarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27DBF211EF8F039007666C3 /* OpenRadarTests.swift */; }; 33 | C27DBF241EF8F071007666C3 /* Radar+OpenRadar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27DBF231EF8F071007666C3 /* Radar+OpenRadar.swift */; }; 34 | C27DBF261EF8F12D007666C3 /* openradar.json in Resources */ = {isa = PBXBuildFile; fileRef = C27DBF251EF8F12D007666C3 /* openradar.json */; }; 35 | C27DBF281EF8F6E2007666C3 /* DictionaryExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27DBF271EF8F6E2007666C3 /* DictionaryExtensionTests.swift */; }; 36 | C27DBF2B1EF8F75C007666C3 /* Dictionary+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27DBF2A1EF8F75C007666C3 /* Dictionary+Extension.swift */; }; 37 | C27F3B721D8B239B00EA3B7D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27F3B711D8B239B00EA3B7D /* Constants.swift */; }; 38 | C28001D91D148AB800569A72 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28001D81D148AB800569A72 /* AppDelegate.swift */; }; 39 | C28001DD1D148AB800569A72 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C28001DC1D148AB800569A72 /* Assets.xcassets */; }; 40 | C28001E01D148AB800569A72 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C28001DE1D148AB800569A72 /* Main.storyboard */; }; 41 | C28001F61D148AB800569A72 /* BriskUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28001F51D148AB800569A72 /* BriskUITests.swift */; }; 42 | C28399CC1D148CDC00D240C4 /* RadarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28399CB1D148CDC00D240C4 /* RadarViewController.swift */; }; 43 | C2839A151D14F29B00D240C4 /* Validatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2839A141D14F29B00D240C4 /* Validatable.swift */; }; 44 | C2839A171D14F2B100D240C4 /* NSTextView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2839A161D14F2B100D240C4 /* NSTextView+Extension.swift */; }; 45 | C2839A191D14F2CF00D240C4 /* NSTextField+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2839A181D14F2CF00D240C4 /* NSTextField+Extension.swift */; }; 46 | C2839A1B1D14F2E100D240C4 /* NSPopUpButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2839A1A1D14F2E100D240C4 /* NSPopUpButton+Extension.swift */; }; 47 | C2839A241D14FC7000D240C4 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2839A231D14FC7000D240C4 /* Result.swift */; }; 48 | C2839A281D14FE8B00D240C4 /* Data+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2839A271D14FE8B00D240C4 /* Data+Extension.swift */; }; 49 | C2C042271EF6F781008BFC88 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2C042261EF6F781008BFC88 /* TextView.swift */; }; 50 | C2C360171D172A9A00217D22 /* AppleRadarPreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2C360161D172A9A00217D22 /* AppleRadarPreferencesViewController.swift */; }; 51 | C2C360191D172DB400217D22 /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2C360181D172DB400217D22 /* TabViewController.swift */; }; 52 | C2D0B2071DAB6D0E005E804F /* radar.json in Resources */ = {isa = PBXBuildFile; fileRef = C2D0B2061DAB6D0E005E804F /* radar.json */; }; 53 | C2D0B2091DAB6D23005E804F /* RadarSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D0B2081DAB6D23005E804F /* RadarSerializationTests.swift */; }; 54 | C2D0B20B1DAB6F00005E804F /* Radar+Serialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D0B20A1DAB6F00005E804F /* Radar+Serialization.swift */; }; 55 | C2E8AE981D168A9200A65DB0 /* CascadingWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E8AE971D168A9200A65DB0 /* CascadingWindowController.swift */; }; 56 | C2ED20581D172437002A54B0 /* OpenRadarPreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2ED20571D172437002A54B0 /* OpenRadarPreferencesViewController.swift */; }; 57 | E8D9E1C51E98636300D392B7 /* GlobalHotKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8D9E1C41E98636300D392B7 /* GlobalHotKey.swift */; }; 58 | /* End PBXBuildFile section */ 59 | 60 | /* Begin PBXContainerItemProxy section */ 61 | C28001E71D148AB800569A72 /* PBXContainerItemProxy */ = { 62 | isa = PBXContainerItemProxy; 63 | containerPortal = C28001CD1D148AB800569A72 /* Project object */; 64 | proxyType = 1; 65 | remoteGlobalIDString = C28001D41D148AB800569A72; 66 | remoteInfo = Brisk; 67 | }; 68 | C28001F21D148AB800569A72 /* PBXContainerItemProxy */ = { 69 | isa = PBXContainerItemProxy; 70 | containerPortal = C28001CD1D148AB800569A72 /* Project object */; 71 | proxyType = 1; 72 | remoteGlobalIDString = C28001D41D148AB800569A72; 73 | remoteInfo = Brisk; 74 | }; 75 | /* End PBXContainerItemProxy section */ 76 | 77 | /* Begin PBXFileReference section */ 78 | 0018868F17477C7D609957AD /* Pods_BriskTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BriskTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 79 | 45F9233FBE2D1D0D4ABF530C /* Pods-Brisk.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Brisk.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Brisk/Pods-Brisk.debug.xcconfig"; sourceTree = ""; }; 80 | 493890551F157F16008F9E94 /* AttachmentDroppableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentDroppableView.swift; sourceTree = ""; }; 81 | 7DD69D9F6507E9D3006DC976 /* Pods-BriskTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BriskTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-BriskTests/Pods-BriskTests.release.xcconfig"; sourceTree = ""; }; 82 | 9002A6E26E9C2D617DFB9460 /* Pods-Brisk.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Brisk.release.xcconfig"; path = "Pods/Target Support Files/Pods-Brisk/Pods-Brisk.release.xcconfig"; sourceTree = ""; }; 83 | C2042B521D17397900DEF594 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 84 | C20DDC111D16496E0086D304 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = ""; }; 85 | C20DDC131D1652C20086D304 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; 86 | C20DDC151D1673670086D304 /* NSStoryboard+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSStoryboard+Extension.swift"; sourceTree = ""; }; 87 | C20DDC171D16771A0086D304 /* StoryboardRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryboardRouter.swift; sourceTree = ""; }; 88 | C20DDC191D167D5F0086D304 /* RadarDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadarDocument.swift; sourceTree = ""; }; 89 | C219CBD71D173602002F89FC /* NSStatusItem+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSStatusItem+Extension.swift"; sourceTree = ""; }; 90 | C219CBD91D1736EB002F89FC /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 91 | C21DAA161F00433C0016E8AA /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dsa_pub.pem; sourceTree = ""; }; 92 | C23ED5041EFA1174006988BA /* FileDuplicateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileDuplicateViewController.swift; sourceTree = ""; }; 93 | C23ED5071EFA1FD1006988BA /* NSDocumentController+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSDocumentController+Extension.swift"; sourceTree = ""; }; 94 | C23ED5091EFA2362006988BA /* NSProgressIndicator+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSProgressIndicator+Extension.swift"; sourceTree = ""; }; 95 | C23ED50B1EFA2DA9006988BA /* RadarIDParsingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadarIDParsingTests.swift; sourceTree = ""; }; 96 | C255C4421DAB7A9C00383D0A /* ResultTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultTests.swift; sourceTree = ""; }; 97 | C276944F1EFCD461009C3FA1 /* StringExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensionTests.swift; sourceTree = ""; }; 98 | C27694511EFD094F009C3FA1 /* OpenRadar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenRadar.swift; sourceTree = ""; }; 99 | C27694531EFD097C009C3FA1 /* String+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 100 | C27694551EFD0BD4009C3FA1 /* openradarstrings.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = openradarstrings.json; sourceTree = ""; }; 101 | C27A5A161E721BD300391E63 /* Attachment+Serialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Attachment+Serialization.swift"; sourceTree = ""; }; 102 | C27DBF211EF8F039007666C3 /* OpenRadarTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenRadarTests.swift; sourceTree = ""; }; 103 | C27DBF231EF8F071007666C3 /* Radar+OpenRadar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Radar+OpenRadar.swift"; sourceTree = ""; }; 104 | C27DBF251EF8F12D007666C3 /* openradar.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = openradar.json; sourceTree = ""; }; 105 | C27DBF271EF8F6E2007666C3 /* DictionaryExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryExtensionTests.swift; sourceTree = ""; }; 106 | C27DBF2A1EF8F75C007666C3 /* Dictionary+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Extension.swift"; sourceTree = ""; }; 107 | C27F3B711D8B239B00EA3B7D /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 108 | C28001D51D148AB800569A72 /* Brisk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Brisk.app; sourceTree = BUILT_PRODUCTS_DIR; }; 109 | C28001D81D148AB800569A72 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 110 | C28001DC1D148AB800569A72 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 111 | C28001DF1D148AB800569A72 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 112 | C28001E11D148AB800569A72 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 113 | C28001E61D148AB800569A72 /* BriskTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BriskTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 114 | C28001EC1D148AB800569A72 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 115 | C28001F11D148AB800569A72 /* BriskUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BriskUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 116 | C28001F51D148AB800569A72 /* BriskUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BriskUITests.swift; sourceTree = ""; }; 117 | C28001F71D148AB800569A72 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 118 | C28399CB1D148CDC00D240C4 /* RadarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadarViewController.swift; sourceTree = ""; }; 119 | C2839A141D14F29B00D240C4 /* Validatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Validatable.swift; sourceTree = ""; }; 120 | C2839A161D14F2B100D240C4 /* NSTextView+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSTextView+Extension.swift"; sourceTree = ""; }; 121 | C2839A181D14F2CF00D240C4 /* NSTextField+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSTextField+Extension.swift"; sourceTree = ""; }; 122 | C2839A1A1D14F2E100D240C4 /* NSPopUpButton+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSPopUpButton+Extension.swift"; sourceTree = ""; }; 123 | C2839A231D14FC7000D240C4 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; 124 | C2839A271D14FE8B00D240C4 /* Data+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Extension.swift"; sourceTree = ""; }; 125 | C2C042261EF6F781008BFC88 /* TextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; 126 | C2C360161D172A9A00217D22 /* AppleRadarPreferencesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppleRadarPreferencesViewController.swift; sourceTree = ""; }; 127 | C2C360181D172DB400217D22 /* TabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabViewController.swift; sourceTree = ""; }; 128 | C2D0B2061DAB6D0E005E804F /* radar.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = radar.json; sourceTree = ""; }; 129 | C2D0B2081DAB6D23005E804F /* RadarSerializationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadarSerializationTests.swift; sourceTree = ""; }; 130 | C2D0B20A1DAB6F00005E804F /* Radar+Serialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Radar+Serialization.swift"; sourceTree = ""; }; 131 | C2E8AE971D168A9200A65DB0 /* CascadingWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CascadingWindowController.swift; sourceTree = ""; }; 132 | C2ED20571D172437002A54B0 /* OpenRadarPreferencesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenRadarPreferencesViewController.swift; sourceTree = ""; }; 133 | C88425AC44C8AB4389F2A583 /* Pods_Brisk.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Brisk.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 134 | E8D9E1C41E98636300D392B7 /* GlobalHotKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalHotKey.swift; sourceTree = ""; }; 135 | F4A33BC64FC74AD0621A14C7 /* Pods-BriskTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BriskTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-BriskTests/Pods-BriskTests.debug.xcconfig"; sourceTree = ""; }; 136 | /* End PBXFileReference section */ 137 | 138 | /* Begin PBXFrameworksBuildPhase section */ 139 | C28001D21D148AB800569A72 /* Frameworks */ = { 140 | isa = PBXFrameworksBuildPhase; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | 1BA2CE444CBF0C961AEF74B3 /* Pods_Brisk.framework in Frameworks */, 144 | ); 145 | runOnlyForDeploymentPostprocessing = 0; 146 | }; 147 | C28001E31D148AB800569A72 /* Frameworks */ = { 148 | isa = PBXFrameworksBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | BADCFD626E46B81044A115B2 /* Pods_BriskTests.framework in Frameworks */, 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | C28001EE1D148AB800569A72 /* Frameworks */ = { 156 | isa = PBXFrameworksBuildPhase; 157 | buildActionMask = 2147483647; 158 | files = ( 159 | ); 160 | runOnlyForDeploymentPostprocessing = 0; 161 | }; 162 | /* End PBXFrameworksBuildPhase section */ 163 | 164 | /* Begin PBXGroup section */ 165 | 126829078580050524ADC48E /* Pods */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | 45F9233FBE2D1D0D4ABF530C /* Pods-Brisk.debug.xcconfig */, 169 | 9002A6E26E9C2D617DFB9460 /* Pods-Brisk.release.xcconfig */, 170 | F4A33BC64FC74AD0621A14C7 /* Pods-BriskTests.debug.xcconfig */, 171 | 7DD69D9F6507E9D3006DC976 /* Pods-BriskTests.release.xcconfig */, 172 | ); 173 | name = Pods; 174 | sourceTree = ""; 175 | }; 176 | C255C4401DAB79C100383D0A /* Resources */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | C28001EC1D148AB800569A72 /* Info.plist */, 180 | C27DBF251EF8F12D007666C3 /* openradar.json */, 181 | C27694551EFD0BD4009C3FA1 /* openradarstrings.json */, 182 | C2D0B2061DAB6D0E005E804F /* radar.json */, 183 | ); 184 | path = Resources; 185 | sourceTree = ""; 186 | }; 187 | C255C4411DAB79CC00383D0A /* Resources */ = { 188 | isa = PBXGroup; 189 | children = ( 190 | C28001F71D148AB800569A72 /* Info.plist */, 191 | ); 192 | path = Resources; 193 | sourceTree = ""; 194 | }; 195 | C28001CC1D148AB800569A72 = { 196 | isa = PBXGroup; 197 | children = ( 198 | C28001D71D148AB800569A72 /* Brisk */, 199 | C28001E91D148AB800569A72 /* BriskTests */, 200 | C28001F41D148AB800569A72 /* BriskUITests */, 201 | C4CC83B6A8A6286F3AF47B9F /* Frameworks */, 202 | 126829078580050524ADC48E /* Pods */, 203 | C28001D61D148AB800569A72 /* Products */, 204 | ); 205 | sourceTree = ""; 206 | }; 207 | C28001D61D148AB800569A72 /* Products */ = { 208 | isa = PBXGroup; 209 | children = ( 210 | C28001D51D148AB800569A72 /* Brisk.app */, 211 | C28001E61D148AB800569A72 /* BriskTests.xctest */, 212 | C28001F11D148AB800569A72 /* BriskUITests.xctest */, 213 | ); 214 | name = Products; 215 | sourceTree = ""; 216 | }; 217 | C28001D71D148AB800569A72 /* Brisk */ = { 218 | isa = PBXGroup; 219 | children = ( 220 | C2839A211D14F5BF00D240C4 /* Controllers */, 221 | C2839A1E1D14F59400D240C4 /* Extensions */, 222 | C2839A201D14F5A900D240C4 /* Models */, 223 | C2839A221D14F5CC00D240C4 /* Resources */, 224 | C2C042251EF6F781008BFC88 /* Views */, 225 | C28001D81D148AB800569A72 /* AppDelegate.swift */, 226 | C219CBD91D1736EB002F89FC /* Application.swift */, 227 | E8D9E1C41E98636300D392B7 /* GlobalHotKey.swift */, 228 | C27F3B711D8B239B00EA3B7D /* Constants.swift */, 229 | C20DDC131D1652C20086D304 /* Keychain.swift */, 230 | C20DDC171D16771A0086D304 /* StoryboardRouter.swift */, 231 | ); 232 | path = Brisk; 233 | sourceTree = ""; 234 | }; 235 | C28001E91D148AB800569A72 /* BriskTests */ = { 236 | isa = PBXGroup; 237 | children = ( 238 | C27DBF271EF8F6E2007666C3 /* DictionaryExtensionTests.swift */, 239 | C27DBF211EF8F039007666C3 /* OpenRadarTests.swift */, 240 | C23ED50B1EFA2DA9006988BA /* RadarIDParsingTests.swift */, 241 | C2D0B2081DAB6D23005E804F /* RadarSerializationTests.swift */, 242 | C255C4401DAB79C100383D0A /* Resources */, 243 | C255C4421DAB7A9C00383D0A /* ResultTests.swift */, 244 | C276944F1EFCD461009C3FA1 /* StringExtensionTests.swift */, 245 | ); 246 | path = BriskTests; 247 | sourceTree = ""; 248 | }; 249 | C28001F41D148AB800569A72 /* BriskUITests */ = { 250 | isa = PBXGroup; 251 | children = ( 252 | C28001F51D148AB800569A72 /* BriskUITests.swift */, 253 | C255C4411DAB79CC00383D0A /* Resources */, 254 | ); 255 | path = BriskUITests; 256 | sourceTree = ""; 257 | }; 258 | C2839A1E1D14F59400D240C4 /* Extensions */ = { 259 | isa = PBXGroup; 260 | children = ( 261 | C27A5A161E721BD300391E63 /* Attachment+Serialization.swift */, 262 | C2839A271D14FE8B00D240C4 /* Data+Extension.swift */, 263 | C27DBF2A1EF8F75C007666C3 /* Dictionary+Extension.swift */, 264 | C23ED5071EFA1FD1006988BA /* NSDocumentController+Extension.swift */, 265 | C2839A1A1D14F2E100D240C4 /* NSPopUpButton+Extension.swift */, 266 | C23ED5091EFA2362006988BA /* NSProgressIndicator+Extension.swift */, 267 | C219CBD71D173602002F89FC /* NSStatusItem+Extension.swift */, 268 | C20DDC151D1673670086D304 /* NSStoryboard+Extension.swift */, 269 | C2839A181D14F2CF00D240C4 /* NSTextField+Extension.swift */, 270 | C2839A161D14F2B100D240C4 /* NSTextView+Extension.swift */, 271 | C27DBF231EF8F071007666C3 /* Radar+OpenRadar.swift */, 272 | C2D0B20A1DAB6F00005E804F /* Radar+Serialization.swift */, 273 | C27694531EFD097C009C3FA1 /* String+Extension.swift */, 274 | ); 275 | path = Extensions; 276 | sourceTree = ""; 277 | }; 278 | C2839A201D14F5A900D240C4 /* Models */ = { 279 | isa = PBXGroup; 280 | children = ( 281 | C27694511EFD094F009C3FA1 /* OpenRadar.swift */, 282 | C20DDC191D167D5F0086D304 /* RadarDocument.swift */, 283 | C2839A231D14FC7000D240C4 /* Result.swift */, 284 | C2839A141D14F29B00D240C4 /* Validatable.swift */, 285 | ); 286 | path = Models; 287 | sourceTree = ""; 288 | }; 289 | C2839A211D14F5BF00D240C4 /* Controllers */ = { 290 | isa = PBXGroup; 291 | children = ( 292 | C2C360161D172A9A00217D22 /* AppleRadarPreferencesViewController.swift */, 293 | C20DDC111D16496E0086D304 /* AuthenticationViewController.swift */, 294 | C2E8AE971D168A9200A65DB0 /* CascadingWindowController.swift */, 295 | C23ED5041EFA1174006988BA /* FileDuplicateViewController.swift */, 296 | C2ED20571D172437002A54B0 /* OpenRadarPreferencesViewController.swift */, 297 | C28399CB1D148CDC00D240C4 /* RadarViewController.swift */, 298 | C2C360181D172DB400217D22 /* TabViewController.swift */, 299 | C2042B521D17397900DEF594 /* ViewController.swift */, 300 | ); 301 | path = Controllers; 302 | sourceTree = ""; 303 | }; 304 | C2839A221D14F5CC00D240C4 /* Resources */ = { 305 | isa = PBXGroup; 306 | children = ( 307 | C28001DC1D148AB800569A72 /* Assets.xcassets */, 308 | C21DAA161F00433C0016E8AA /* dsa_pub.pem */, 309 | C28001E11D148AB800569A72 /* Info.plist */, 310 | C28001DE1D148AB800569A72 /* Main.storyboard */, 311 | ); 312 | path = Resources; 313 | sourceTree = ""; 314 | }; 315 | C2C042251EF6F781008BFC88 /* Views */ = { 316 | isa = PBXGroup; 317 | children = ( 318 | 493890551F157F16008F9E94 /* AttachmentDroppableView.swift */, 319 | C2C042261EF6F781008BFC88 /* TextView.swift */, 320 | ); 321 | path = Views; 322 | sourceTree = ""; 323 | }; 324 | C4CC83B6A8A6286F3AF47B9F /* Frameworks */ = { 325 | isa = PBXGroup; 326 | children = ( 327 | C88425AC44C8AB4389F2A583 /* Pods_Brisk.framework */, 328 | 0018868F17477C7D609957AD /* Pods_BriskTests.framework */, 329 | ); 330 | name = Frameworks; 331 | sourceTree = ""; 332 | }; 333 | /* End PBXGroup section */ 334 | 335 | /* Begin PBXNativeTarget section */ 336 | C28001D41D148AB800569A72 /* Brisk */ = { 337 | isa = PBXNativeTarget; 338 | buildConfigurationList = C28001FA1D148AB800569A72 /* Build configuration list for PBXNativeTarget "Brisk" */; 339 | buildPhases = ( 340 | 5044117728B28669F55893B3 /* [CP] Check Pods Manifest.lock */, 341 | C28001D11D148AB800569A72 /* Sources */, 342 | C28001D21D148AB800569A72 /* Frameworks */, 343 | C28001D31D148AB800569A72 /* Resources */, 344 | 0FE9F8AD31E59F6784A7B564 /* [CP] Embed Pods Frameworks */, 345 | CCDAF9227C8454928AD3CBF0 /* [CP] Copy Pods Resources */, 346 | ); 347 | buildRules = ( 348 | ); 349 | dependencies = ( 350 | ); 351 | name = Brisk; 352 | productName = Brisk; 353 | productReference = C28001D51D148AB800569A72 /* Brisk.app */; 354 | productType = "com.apple.product-type.application"; 355 | }; 356 | C28001E51D148AB800569A72 /* BriskTests */ = { 357 | isa = PBXNativeTarget; 358 | buildConfigurationList = C28001FD1D148AB800569A72 /* Build configuration list for PBXNativeTarget "BriskTests" */; 359 | buildPhases = ( 360 | 216322EDA7CE01E4C1AF627A /* [CP] Check Pods Manifest.lock */, 361 | C28001E21D148AB800569A72 /* Sources */, 362 | C28001E31D148AB800569A72 /* Frameworks */, 363 | C28001E41D148AB800569A72 /* Resources */, 364 | 0DE5F5B8CCFB9C76C40B6419 /* [CP] Embed Pods Frameworks */, 365 | 3E61C15B958FB16681C486D8 /* [CP] Copy Pods Resources */, 366 | ); 367 | buildRules = ( 368 | ); 369 | dependencies = ( 370 | C28001E81D148AB800569A72 /* PBXTargetDependency */, 371 | ); 372 | name = BriskTests; 373 | productName = BriskTests; 374 | productReference = C28001E61D148AB800569A72 /* BriskTests.xctest */; 375 | productType = "com.apple.product-type.bundle.unit-test"; 376 | }; 377 | C28001F01D148AB800569A72 /* BriskUITests */ = { 378 | isa = PBXNativeTarget; 379 | buildConfigurationList = C28002001D148AB800569A72 /* Build configuration list for PBXNativeTarget "BriskUITests" */; 380 | buildPhases = ( 381 | C28001ED1D148AB800569A72 /* Sources */, 382 | C28001EE1D148AB800569A72 /* Frameworks */, 383 | C28001EF1D148AB800569A72 /* Resources */, 384 | ); 385 | buildRules = ( 386 | ); 387 | dependencies = ( 388 | C28001F31D148AB800569A72 /* PBXTargetDependency */, 389 | ); 390 | name = BriskUITests; 391 | productName = BriskUITests; 392 | productReference = C28001F11D148AB800569A72 /* BriskUITests.xctest */; 393 | productType = "com.apple.product-type.bundle.ui-testing"; 394 | }; 395 | /* End PBXNativeTarget section */ 396 | 397 | /* Begin PBXProject section */ 398 | C28001CD1D148AB800569A72 /* Project object */ = { 399 | isa = PBXProject; 400 | attributes = { 401 | LastSwiftUpdateCheck = 0730; 402 | LastUpgradeCheck = 0920; 403 | ORGANIZATIONNAME = Brisk; 404 | TargetAttributes = { 405 | C28001D41D148AB800569A72 = { 406 | CreatedOnToolsVersion = 7.3.1; 407 | LastSwiftMigration = 0920; 408 | }; 409 | C28001E51D148AB800569A72 = { 410 | CreatedOnToolsVersion = 7.3.1; 411 | LastSwiftMigration = 0920; 412 | TestTargetID = C28001D41D148AB800569A72; 413 | }; 414 | C28001F01D148AB800569A72 = { 415 | CreatedOnToolsVersion = 7.3.1; 416 | LastSwiftMigration = 0920; 417 | TestTargetID = C28001D41D148AB800569A72; 418 | }; 419 | }; 420 | }; 421 | buildConfigurationList = C28001D01D148AB800569A72 /* Build configuration list for PBXProject "Brisk" */; 422 | compatibilityVersion = "Xcode 3.2"; 423 | developmentRegion = English; 424 | hasScannedForEncodings = 0; 425 | knownRegions = ( 426 | en, 427 | Base, 428 | ); 429 | mainGroup = C28001CC1D148AB800569A72; 430 | productRefGroup = C28001D61D148AB800569A72 /* Products */; 431 | projectDirPath = ""; 432 | projectRoot = ""; 433 | targets = ( 434 | C28001D41D148AB800569A72 /* Brisk */, 435 | C28001E51D148AB800569A72 /* BriskTests */, 436 | C28001F01D148AB800569A72 /* BriskUITests */, 437 | ); 438 | }; 439 | /* End PBXProject section */ 440 | 441 | /* Begin PBXResourcesBuildPhase section */ 442 | C28001D31D148AB800569A72 /* Resources */ = { 443 | isa = PBXResourcesBuildPhase; 444 | buildActionMask = 2147483647; 445 | files = ( 446 | C28001DD1D148AB800569A72 /* Assets.xcassets in Resources */, 447 | C21DAA171F00433C0016E8AA /* dsa_pub.pem in Resources */, 448 | C28001E01D148AB800569A72 /* Main.storyboard in Resources */, 449 | ); 450 | runOnlyForDeploymentPostprocessing = 0; 451 | }; 452 | C28001E41D148AB800569A72 /* Resources */ = { 453 | isa = PBXResourcesBuildPhase; 454 | buildActionMask = 2147483647; 455 | files = ( 456 | C27694561EFD0BD4009C3FA1 /* openradarstrings.json in Resources */, 457 | C27DBF261EF8F12D007666C3 /* openradar.json in Resources */, 458 | C2D0B2071DAB6D0E005E804F /* radar.json in Resources */, 459 | ); 460 | runOnlyForDeploymentPostprocessing = 0; 461 | }; 462 | C28001EF1D148AB800569A72 /* Resources */ = { 463 | isa = PBXResourcesBuildPhase; 464 | buildActionMask = 2147483647; 465 | files = ( 466 | ); 467 | runOnlyForDeploymentPostprocessing = 0; 468 | }; 469 | /* End PBXResourcesBuildPhase section */ 470 | 471 | /* Begin PBXShellScriptBuildPhase section */ 472 | 0DE5F5B8CCFB9C76C40B6419 /* [CP] Embed Pods Frameworks */ = { 473 | isa = PBXShellScriptBuildPhase; 474 | buildActionMask = 2147483647; 475 | files = ( 476 | ); 477 | inputPaths = ( 478 | ); 479 | name = "[CP] Embed Pods Frameworks"; 480 | outputPaths = ( 481 | ); 482 | runOnlyForDeploymentPostprocessing = 0; 483 | shellPath = /bin/sh; 484 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-BriskTests/Pods-BriskTests-frameworks.sh\"\n"; 485 | showEnvVarsInLog = 0; 486 | }; 487 | 0FE9F8AD31E59F6784A7B564 /* [CP] Embed Pods Frameworks */ = { 488 | isa = PBXShellScriptBuildPhase; 489 | buildActionMask = 2147483647; 490 | files = ( 491 | ); 492 | inputPaths = ( 493 | ); 494 | name = "[CP] Embed Pods Frameworks"; 495 | outputPaths = ( 496 | ); 497 | runOnlyForDeploymentPostprocessing = 0; 498 | shellPath = /bin/sh; 499 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Brisk/Pods-Brisk-frameworks.sh\"\n"; 500 | showEnvVarsInLog = 0; 501 | }; 502 | 216322EDA7CE01E4C1AF627A /* [CP] Check Pods Manifest.lock */ = { 503 | isa = PBXShellScriptBuildPhase; 504 | buildActionMask = 2147483647; 505 | files = ( 506 | ); 507 | inputPaths = ( 508 | ); 509 | name = "[CP] Check Pods Manifest.lock"; 510 | outputPaths = ( 511 | ); 512 | runOnlyForDeploymentPostprocessing = 0; 513 | shellPath = /bin/sh; 514 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 515 | showEnvVarsInLog = 0; 516 | }; 517 | 3E61C15B958FB16681C486D8 /* [CP] Copy Pods Resources */ = { 518 | isa = PBXShellScriptBuildPhase; 519 | buildActionMask = 2147483647; 520 | files = ( 521 | ); 522 | inputPaths = ( 523 | ); 524 | name = "[CP] Copy Pods Resources"; 525 | outputPaths = ( 526 | ); 527 | runOnlyForDeploymentPostprocessing = 0; 528 | shellPath = /bin/sh; 529 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-BriskTests/Pods-BriskTests-resources.sh\"\n"; 530 | showEnvVarsInLog = 0; 531 | }; 532 | 5044117728B28669F55893B3 /* [CP] Check Pods Manifest.lock */ = { 533 | isa = PBXShellScriptBuildPhase; 534 | buildActionMask = 2147483647; 535 | files = ( 536 | ); 537 | inputPaths = ( 538 | ); 539 | name = "[CP] Check Pods Manifest.lock"; 540 | outputPaths = ( 541 | ); 542 | runOnlyForDeploymentPostprocessing = 0; 543 | shellPath = /bin/sh; 544 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 545 | showEnvVarsInLog = 0; 546 | }; 547 | CCDAF9227C8454928AD3CBF0 /* [CP] Copy Pods Resources */ = { 548 | isa = PBXShellScriptBuildPhase; 549 | buildActionMask = 2147483647; 550 | files = ( 551 | ); 552 | inputPaths = ( 553 | ); 554 | name = "[CP] Copy Pods Resources"; 555 | outputPaths = ( 556 | ); 557 | runOnlyForDeploymentPostprocessing = 0; 558 | shellPath = /bin/sh; 559 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Brisk/Pods-Brisk-resources.sh\"\n"; 560 | showEnvVarsInLog = 0; 561 | }; 562 | /* End PBXShellScriptBuildPhase section */ 563 | 564 | /* Begin PBXSourcesBuildPhase section */ 565 | C28001D11D148AB800569A72 /* Sources */ = { 566 | isa = PBXSourcesBuildPhase; 567 | buildActionMask = 2147483647; 568 | files = ( 569 | C219CBDA1D1736EB002F89FC /* Application.swift in Sources */, 570 | C2ED20581D172437002A54B0 /* OpenRadarPreferencesViewController.swift in Sources */, 571 | C2839A1B1D14F2E100D240C4 /* NSPopUpButton+Extension.swift in Sources */, 572 | C27DBF241EF8F071007666C3 /* Radar+OpenRadar.swift in Sources */, 573 | C219CBD81D173602002F89FC /* NSStatusItem+Extension.swift in Sources */, 574 | 493890561F157F16008F9E94 /* AttachmentDroppableView.swift in Sources */, 575 | C2042B531D17397900DEF594 /* ViewController.swift in Sources */, 576 | C2E8AE981D168A9200A65DB0 /* CascadingWindowController.swift in Sources */, 577 | C27F3B721D8B239B00EA3B7D /* Constants.swift in Sources */, 578 | C23ED5081EFA1FD1006988BA /* NSDocumentController+Extension.swift in Sources */, 579 | C20DDC161D1673670086D304 /* NSStoryboard+Extension.swift in Sources */, 580 | C2839A281D14FE8B00D240C4 /* Data+Extension.swift in Sources */, 581 | C23ED5061EFA14E1006988BA /* FileDuplicateViewController.swift in Sources */, 582 | C2839A241D14FC7000D240C4 /* Result.swift in Sources */, 583 | E8D9E1C51E98636300D392B7 /* GlobalHotKey.swift in Sources */, 584 | C2C360191D172DB400217D22 /* TabViewController.swift in Sources */, 585 | C20DDC141D1652C20086D304 /* Keychain.swift in Sources */, 586 | C27694541EFD097C009C3FA1 /* String+Extension.swift in Sources */, 587 | C28001D91D148AB800569A72 /* AppDelegate.swift in Sources */, 588 | C2C042271EF6F781008BFC88 /* TextView.swift in Sources */, 589 | C28399CC1D148CDC00D240C4 /* RadarViewController.swift in Sources */, 590 | C2D0B20B1DAB6F00005E804F /* Radar+Serialization.swift in Sources */, 591 | C20DDC181D16771A0086D304 /* StoryboardRouter.swift in Sources */, 592 | C2839A151D14F29B00D240C4 /* Validatable.swift in Sources */, 593 | C20DDC121D16496E0086D304 /* AuthenticationViewController.swift in Sources */, 594 | C23ED50A1EFA2362006988BA /* NSProgressIndicator+Extension.swift in Sources */, 595 | C27A5A171E721BD300391E63 /* Attachment+Serialization.swift in Sources */, 596 | C2839A191D14F2CF00D240C4 /* NSTextField+Extension.swift in Sources */, 597 | C27DBF2B1EF8F75C007666C3 /* Dictionary+Extension.swift in Sources */, 598 | C2C360171D172A9A00217D22 /* AppleRadarPreferencesViewController.swift in Sources */, 599 | C2839A171D14F2B100D240C4 /* NSTextView+Extension.swift in Sources */, 600 | C20DDC1A1D167D5F0086D304 /* RadarDocument.swift in Sources */, 601 | C27694521EFD094F009C3FA1 /* OpenRadar.swift in Sources */, 602 | ); 603 | runOnlyForDeploymentPostprocessing = 0; 604 | }; 605 | C28001E21D148AB800569A72 /* Sources */ = { 606 | isa = PBXSourcesBuildPhase; 607 | buildActionMask = 2147483647; 608 | files = ( 609 | C27DBF221EF8F039007666C3 /* OpenRadarTests.swift in Sources */, 610 | C27DBF281EF8F6E2007666C3 /* DictionaryExtensionTests.swift in Sources */, 611 | C27694501EFCD461009C3FA1 /* StringExtensionTests.swift in Sources */, 612 | C255C4431DAB7A9C00383D0A /* ResultTests.swift in Sources */, 613 | C23ED50C1EFA2DA9006988BA /* RadarIDParsingTests.swift in Sources */, 614 | C2D0B2091DAB6D23005E804F /* RadarSerializationTests.swift in Sources */, 615 | ); 616 | runOnlyForDeploymentPostprocessing = 0; 617 | }; 618 | C28001ED1D148AB800569A72 /* Sources */ = { 619 | isa = PBXSourcesBuildPhase; 620 | buildActionMask = 2147483647; 621 | files = ( 622 | C28001F61D148AB800569A72 /* BriskUITests.swift in Sources */, 623 | ); 624 | runOnlyForDeploymentPostprocessing = 0; 625 | }; 626 | /* End PBXSourcesBuildPhase section */ 627 | 628 | /* Begin PBXTargetDependency section */ 629 | C28001E81D148AB800569A72 /* PBXTargetDependency */ = { 630 | isa = PBXTargetDependency; 631 | target = C28001D41D148AB800569A72 /* Brisk */; 632 | targetProxy = C28001E71D148AB800569A72 /* PBXContainerItemProxy */; 633 | }; 634 | C28001F31D148AB800569A72 /* PBXTargetDependency */ = { 635 | isa = PBXTargetDependency; 636 | target = C28001D41D148AB800569A72 /* Brisk */; 637 | targetProxy = C28001F21D148AB800569A72 /* PBXContainerItemProxy */; 638 | }; 639 | /* End PBXTargetDependency section */ 640 | 641 | /* Begin PBXVariantGroup section */ 642 | C28001DE1D148AB800569A72 /* Main.storyboard */ = { 643 | isa = PBXVariantGroup; 644 | children = ( 645 | C28001DF1D148AB800569A72 /* Base */, 646 | ); 647 | name = Main.storyboard; 648 | path = .; 649 | sourceTree = ""; 650 | }; 651 | /* End PBXVariantGroup section */ 652 | 653 | /* Begin XCBuildConfiguration section */ 654 | C28001F81D148AB800569A72 /* Debug */ = { 655 | isa = XCBuildConfiguration; 656 | buildSettings = { 657 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 658 | CLANG_WARN_BOOL_CONVERSION = YES; 659 | CLANG_WARN_COMMA = YES; 660 | CLANG_WARN_CONSTANT_CONVERSION = YES; 661 | CLANG_WARN_EMPTY_BODY = YES; 662 | CLANG_WARN_ENUM_CONVERSION = YES; 663 | CLANG_WARN_INFINITE_RECURSION = YES; 664 | CLANG_WARN_INT_CONVERSION = YES; 665 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 666 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 667 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 668 | CLANG_WARN_STRICT_PROTOTYPES = YES; 669 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 670 | CLANG_WARN_UNREACHABLE_CODE = YES; 671 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 672 | CODE_SIGN_IDENTITY = "-"; 673 | COPY_PHASE_STRIP = NO; 674 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 675 | ENABLE_STRICT_OBJC_MSGSEND = YES; 676 | ENABLE_TESTABILITY = YES; 677 | GCC_NO_COMMON_BLOCKS = YES; 678 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 679 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 680 | GCC_WARN_UNDECLARED_SELECTOR = YES; 681 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 682 | GCC_WARN_UNUSED_FUNCTION = YES; 683 | GCC_WARN_UNUSED_VARIABLE = YES; 684 | MACOSX_DEPLOYMENT_TARGET = 10.11; 685 | ONLY_ACTIVE_ARCH = YES; 686 | SDKROOT = macosx; 687 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 688 | }; 689 | name = Debug; 690 | }; 691 | C28001F91D148AB800569A72 /* Release */ = { 692 | isa = XCBuildConfiguration; 693 | buildSettings = { 694 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 695 | CLANG_WARN_BOOL_CONVERSION = YES; 696 | CLANG_WARN_COMMA = YES; 697 | CLANG_WARN_CONSTANT_CONVERSION = YES; 698 | CLANG_WARN_EMPTY_BODY = YES; 699 | CLANG_WARN_ENUM_CONVERSION = YES; 700 | CLANG_WARN_INFINITE_RECURSION = YES; 701 | CLANG_WARN_INT_CONVERSION = YES; 702 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 703 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 704 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 705 | CLANG_WARN_STRICT_PROTOTYPES = YES; 706 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 707 | CLANG_WARN_UNREACHABLE_CODE = YES; 708 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 709 | CODE_SIGN_IDENTITY = "-"; 710 | COPY_PHASE_STRIP = NO; 711 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 712 | ENABLE_STRICT_OBJC_MSGSEND = YES; 713 | GCC_NO_COMMON_BLOCKS = YES; 714 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 715 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 716 | GCC_WARN_UNDECLARED_SELECTOR = YES; 717 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 718 | GCC_WARN_UNUSED_FUNCTION = YES; 719 | GCC_WARN_UNUSED_VARIABLE = YES; 720 | MACOSX_DEPLOYMENT_TARGET = 10.11; 721 | SDKROOT = macosx; 722 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 723 | }; 724 | name = Release; 725 | }; 726 | C28001FB1D148AB800569A72 /* Debug */ = { 727 | isa = XCBuildConfiguration; 728 | baseConfigurationReference = 45F9233FBE2D1D0D4ABF530C /* Pods-Brisk.debug.xcconfig */; 729 | buildSettings = { 730 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 731 | COMBINE_HIDPI_IMAGES = YES; 732 | DSTROOT = /; 733 | INFOPLIST_FILE = Brisk/Resources/Info.plist; 734 | PRODUCT_BUNDLE_IDENTIFIER = com.brisk.Brisk; 735 | PRODUCT_NAME = "$(TARGET_NAME)"; 736 | SWIFT_VERSION = 4.0; 737 | }; 738 | name = Debug; 739 | }; 740 | C28001FC1D148AB800569A72 /* Release */ = { 741 | isa = XCBuildConfiguration; 742 | baseConfigurationReference = 9002A6E26E9C2D617DFB9460 /* Pods-Brisk.release.xcconfig */; 743 | buildSettings = { 744 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 745 | COMBINE_HIDPI_IMAGES = YES; 746 | DSTROOT = /; 747 | INFOPLIST_FILE = Brisk/Resources/Info.plist; 748 | PRODUCT_BUNDLE_IDENTIFIER = com.brisk.Brisk; 749 | PRODUCT_NAME = "$(TARGET_NAME)"; 750 | SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; 751 | SWIFT_VERSION = 4.0; 752 | }; 753 | name = Release; 754 | }; 755 | C28001FE1D148AB800569A72 /* Debug */ = { 756 | isa = XCBuildConfiguration; 757 | baseConfigurationReference = F4A33BC64FC74AD0621A14C7 /* Pods-BriskTests.debug.xcconfig */; 758 | buildSettings = { 759 | BUNDLE_LOADER = "$(TEST_HOST)"; 760 | COMBINE_HIDPI_IMAGES = YES; 761 | INFOPLIST_FILE = BriskTests/Resources/Info.plist; 762 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' '@loader_path/../Frameworks'"; 763 | PRODUCT_BUNDLE_IDENTIFIER = com.brisk.BriskTests; 764 | PRODUCT_NAME = "$(TARGET_NAME)"; 765 | SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; 766 | SWIFT_VERSION = 4.0; 767 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Brisk.app/Contents/MacOS/Brisk"; 768 | }; 769 | name = Debug; 770 | }; 771 | C28001FF1D148AB800569A72 /* Release */ = { 772 | isa = XCBuildConfiguration; 773 | baseConfigurationReference = 7DD69D9F6507E9D3006DC976 /* Pods-BriskTests.release.xcconfig */; 774 | buildSettings = { 775 | BUNDLE_LOADER = "$(TEST_HOST)"; 776 | COMBINE_HIDPI_IMAGES = YES; 777 | INFOPLIST_FILE = BriskTests/Resources/Info.plist; 778 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' '@loader_path/../Frameworks'"; 779 | PRODUCT_BUNDLE_IDENTIFIER = com.brisk.BriskTests; 780 | PRODUCT_NAME = "$(TARGET_NAME)"; 781 | SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; 782 | SWIFT_VERSION = 4.0; 783 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Brisk.app/Contents/MacOS/Brisk"; 784 | }; 785 | name = Release; 786 | }; 787 | C28002011D148AB800569A72 /* Debug */ = { 788 | isa = XCBuildConfiguration; 789 | buildSettings = { 790 | COMBINE_HIDPI_IMAGES = YES; 791 | INFOPLIST_FILE = BriskUITests/Resources/Info.plist; 792 | PRODUCT_BUNDLE_IDENTIFIER = com.brisk.BriskUITests; 793 | PRODUCT_NAME = "$(TARGET_NAME)"; 794 | SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; 795 | SWIFT_VERSION = 4.0; 796 | }; 797 | name = Debug; 798 | }; 799 | C28002021D148AB800569A72 /* Release */ = { 800 | isa = XCBuildConfiguration; 801 | buildSettings = { 802 | COMBINE_HIDPI_IMAGES = YES; 803 | INFOPLIST_FILE = BriskUITests/Resources/Info.plist; 804 | PRODUCT_BUNDLE_IDENTIFIER = com.brisk.BriskUITests; 805 | PRODUCT_NAME = "$(TARGET_NAME)"; 806 | SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; 807 | SWIFT_VERSION = 4.0; 808 | }; 809 | name = Release; 810 | }; 811 | /* End XCBuildConfiguration section */ 812 | 813 | /* Begin XCConfigurationList section */ 814 | C28001D01D148AB800569A72 /* Build configuration list for PBXProject "Brisk" */ = { 815 | isa = XCConfigurationList; 816 | buildConfigurations = ( 817 | C28001F81D148AB800569A72 /* Debug */, 818 | C28001F91D148AB800569A72 /* Release */, 819 | ); 820 | defaultConfigurationIsVisible = 0; 821 | defaultConfigurationName = Release; 822 | }; 823 | C28001FA1D148AB800569A72 /* Build configuration list for PBXNativeTarget "Brisk" */ = { 824 | isa = XCConfigurationList; 825 | buildConfigurations = ( 826 | C28001FB1D148AB800569A72 /* Debug */, 827 | C28001FC1D148AB800569A72 /* Release */, 828 | ); 829 | defaultConfigurationIsVisible = 0; 830 | defaultConfigurationName = Release; 831 | }; 832 | C28001FD1D148AB800569A72 /* Build configuration list for PBXNativeTarget "BriskTests" */ = { 833 | isa = XCConfigurationList; 834 | buildConfigurations = ( 835 | C28001FE1D148AB800569A72 /* Debug */, 836 | C28001FF1D148AB800569A72 /* Release */, 837 | ); 838 | defaultConfigurationIsVisible = 0; 839 | defaultConfigurationName = Release; 840 | }; 841 | C28002001D148AB800569A72 /* Build configuration list for PBXNativeTarget "BriskUITests" */ = { 842 | isa = XCConfigurationList; 843 | buildConfigurations = ( 844 | C28002011D148AB800569A72 /* Debug */, 845 | C28002021D148AB800569A72 /* Release */, 846 | ); 847 | defaultConfigurationIsVisible = 0; 848 | defaultConfigurationName = Release; 849 | }; 850 | /* End XCConfigurationList section */ 851 | }; 852 | rootObject = C28001CD1D148AB800569A72 /* Project object */; 853 | } 854 | -------------------------------------------------------------------------------- /Brisk.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Brisk.xcodeproj/xcshareddata/xcschemes/Brisk.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 62 | 63 | 64 | 65 | 76 | 78 | 84 | 85 | 86 | 87 | 90 | 91 | 92 | 93 | 94 | 95 | 101 | 103 | 109 | 110 | 111 | 112 | 114 | 115 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /Brisk.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Brisk.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Brisk/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | @NSApplicationMain 4 | final class AppDelegate: NSObject, NSApplicationDelegate { 5 | @IBOutlet private var statusMenu: NSMenu! 6 | @IBOutlet private var dupeRadarMenuItem: NSMenuItem! 7 | 8 | private var statusItem: NSStatusItem? 9 | 10 | func applicationWillFinishLaunching(_ notification: Notification) { 11 | self.registerDefaults() 12 | self.setupDockIcon() 13 | self.setupStatusItem() 14 | self.registerRadarURLs() 15 | } 16 | 17 | func applicationDidFinishLaunching(_ notification: Notification) { 18 | StoryboardRouter.reloadTopWindowController() 19 | GlobalHotKey.register() 20 | } 21 | 22 | func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool { 23 | return UserDefaults.standard.bool(forKey: Defaults.showDockIcon) 24 | } 25 | 26 | func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { 27 | if flag { 28 | return true 29 | } 30 | 31 | NSDocumentController.shared.newDocument(self) 32 | return false 33 | } 34 | 35 | func application(_ sender: NSApplication, openFiles filenames: [String]) { 36 | for filename in filenames { 37 | let url = URL(fileURLWithPath: filename) 38 | 39 | if let openDocument = NSDocumentController.shared.document(for: url) { 40 | openDocument.showWindows() 41 | } else if let document = NSDocumentController.shared.makeRadarDocument(withContentsOf: url) { 42 | NSDocumentController.shared.addDocument(document) 43 | document.showWindows() 44 | } 45 | } 46 | } 47 | 48 | func applicationWillTerminate(_ notification: Notification) { 49 | self.cleanupStatusItem() 50 | } 51 | 52 | // MARK: - Private Methods 53 | 54 | private func registerDefaults() { 55 | let defaults: [String: Any] = [ 56 | Defaults.showDockIcon: false, 57 | ] 58 | 59 | UserDefaults.standard.register(defaults: defaults) 60 | } 61 | 62 | private func setupDockIcon() { 63 | if !UserDefaults.standard.bool(forKey: Defaults.showDockIcon) { 64 | return 65 | } 66 | 67 | NSApp.setActivationPolicy(.regular) 68 | } 69 | 70 | private func setupStatusItem() { 71 | let image = NSImage(named: .init(rawValue: "StatusItemIcon"))! 72 | self.statusItem = NSStatusItem.create(image: image, menu: self.statusMenu) 73 | } 74 | 75 | private func cleanupStatusItem() { 76 | self.statusItem?.remove() 77 | self.statusItem = nil 78 | } 79 | 80 | private func registerRadarURLs() { 81 | let eventManager = NSAppleEventManager.shared() 82 | let selector = #selector(self.handleGetURLEvent(event:withReplyEvent:)) 83 | eventManager.setEventHandler(self, andSelector: selector, 84 | forEventClass: AEEventClass(kInternetEventClass), 85 | andEventID: AEEventID(kAEGetURL)) 86 | } 87 | 88 | @objc 89 | private func handleGetURLEvent(event: NSAppleEventDescriptor, withReplyEvent: NSAppleEventDescriptor) { 90 | let radarLink = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue 91 | if let radarID = radarLink.flatMap(radarID(from:)) { 92 | _ = self.dupeRadarMenuItem.target!.perform(self.dupeRadarMenuItem.action!) 93 | let viewController = NSApp.windows 94 | .compactMap { $0.contentViewController as? FileDuplicateViewController } 95 | .first! 96 | viewController.searchForOpenRadar(text: radarID) 97 | } else { 98 | let alert = NSAlert() 99 | alert.messageText = "Invalid Radar ID" 100 | alert.informativeText = "The link '\(radarLink ?? "")' doesn't contain a valid radar ID" 101 | alert.runModal() 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Brisk/Application.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | final class Application: NSApplication, NSSeguePerforming { 4 | override func orderFrontStandardAboutPanel(_ sender: Any?) { 5 | super.orderFrontStandardAboutPanel(sender) 6 | NSApp.activate(ignoringOtherApps: true) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Brisk/Constants.swift: -------------------------------------------------------------------------------- 1 | struct Defaults { 2 | static let showDockIcon = "showDockIcon" 3 | } 4 | -------------------------------------------------------------------------------- /Brisk/Controllers/AppleRadarPreferencesViewController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | final class AppleRadarPreferencesViewController: ViewController { 4 | @IBOutlet private var appleIDTextField: NSTextField! 5 | 6 | override func viewDidLoad() { 7 | super.viewDidLoad() 8 | 9 | if let (username, _) = Keychain.get(.radar) { 10 | self.appleIDTextField.stringValue = username 11 | } 12 | } 13 | 14 | @IBAction private func logOut(_ sender: Any) { 15 | Keychain.delete(.radar) 16 | self.view.window?.close() 17 | StoryboardRouter.reloadTopWindowController() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Brisk/Controllers/AuthenticationViewController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | final class AuthenticationViewController: ViewController { 4 | @IBOutlet private var loginButton: NSButton! 5 | @IBOutlet private var appleIDTextField: NSTextField! 6 | @IBOutlet private var passwordTextField: NSSecureTextField! 7 | 8 | private var validatables: [Validatable] { 9 | return [ 10 | self.appleIDTextField, 11 | self.passwordTextField, 12 | ] 13 | } 14 | 15 | var userDidLogin: (() -> Void)? 16 | 17 | override func viewWillAppear() { 18 | super.viewWillAppear() 19 | self.appleIDTextField.becomeFirstResponder() 20 | } 21 | 22 | // MARK: - Private Methods 23 | 24 | @IBAction private func login(_ sender: Any) { 25 | let username = self.appleIDTextField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) 26 | let password = self.passwordTextField.stringValue 27 | Keychain.set(username: username, password: password, forKey: .radar) 28 | StoryboardRouter.reloadTopWindowController() 29 | NSDocumentController.shared.newDocument(self) 30 | } 31 | 32 | private func enableInterface(enable: Bool) { 33 | self.loginButton.isEnabled = enable 34 | self.appleIDTextField.isEnabled = enable 35 | self.passwordTextField.isEnabled = enable 36 | } 37 | 38 | fileprivate func enableLoginIfValid() { 39 | let isValid = self.validatables.reduce(true) { valid, validatable in 40 | return valid && validatable.isValid 41 | } 42 | 43 | self.loginButton.isEnabled = isValid 44 | } 45 | } 46 | 47 | extension AuthenticationViewController { 48 | override func controlTextDidChange(_: Notification) { 49 | self.enableLoginIfValid() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Brisk/Controllers/CascadingWindowController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | final class CascadingWindowController: NSWindowController { 4 | override var shouldCascadeWindows: Bool { 5 | get { return true } 6 | set { super.shouldCascadeWindows = newValue } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Brisk/Controllers/FileDuplicateViewController.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | import AppKit 3 | import Sonar 4 | 5 | final class FileDuplicateViewController: ViewController { 6 | @IBOutlet private var progressIndicator: NSProgressIndicator! 7 | @IBOutlet private var radarIDTextField: NSTextField! 8 | @IBOutlet private var searchButton: NSButton! 9 | 10 | func searchForOpenRadar(text: String) { 11 | self.radarIDTextField.stringValue = text 12 | self.searchForOpenRadar(self.searchButton) 13 | } 14 | 15 | @IBAction private func searchForOpenRadar(_ sender: NSButton) { 16 | let setLoading: (Bool) -> Void = { [weak self] loading in 17 | self?.progressIndicator.isLoading = loading 18 | self?.radarIDTextField.isEnabled = !loading 19 | self?.searchButton.isEnabled = !loading 20 | } 21 | 22 | setLoading(true) 23 | let id = radarID(from: self.radarIDTextField.stringValue)! 24 | let url = URL(string: "https://openradar.appspot.com/api/radar?number=\(id)")! 25 | Alamofire.request(url) 26 | .validate() 27 | .responseJSON { [weak self] result in 28 | setLoading(false) 29 | 30 | if let error = result.error { 31 | self?.show(NSAlert(error: error)) 32 | return 33 | } 34 | 35 | guard let json = result.value as? [String: Any], 36 | let result = json["result"] as? [String: Any], !result.isEmpty else 37 | { 38 | self?.showError(title: "No OpenRadar found", 39 | message: "Couldn't find an OpenRadar with ID #\(id)") 40 | return 41 | } 42 | 43 | guard let radar = try? Radar(openRadar: json), 44 | let document = NSDocumentController.shared.makeRadarDocument() else 45 | { 46 | self?.showError(title: "Invalid OpenRadar", 47 | message: "OpenRadar is missing required fields") 48 | return 49 | } 50 | 51 | document.makeWindowControllers(for: radar) 52 | NSDocumentController.shared.addDocument(document) 53 | document.showWindows() 54 | 55 | self?.view.window?.windowController?.close() 56 | } 57 | } 58 | 59 | override func controlTextDidChange(_ notification: Notification) { 60 | assert(notification.object as? NSTextField === self.radarIDTextField) 61 | self.searchButton.isEnabled = radarID(from: self.radarIDTextField.stringValue) != nil 62 | } 63 | 64 | private func showError(title: String, message: String) { 65 | let alert = NSAlert() 66 | alert.messageText = title 67 | alert.informativeText = message 68 | self.show(alert) 69 | } 70 | 71 | private func show(_ alert: NSAlert) { 72 | alert.runModal() 73 | self.radarIDTextField.becomeFirstResponder() 74 | } 75 | } 76 | 77 | public func radarID(from string: String) -> String? { 78 | guard let text = string.components(separatedBy: "/").last?.strip(), !text.isEmpty else { 79 | return nil 80 | } 81 | 82 | if text.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted)?.isEmpty != false { 83 | return text 84 | } else { 85 | return nil 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Brisk/Controllers/OpenRadarPreferencesViewController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | private let kAPIKeyURL = URL(string: "https://openradar.appspot.com/apikey")! 4 | private let kOpenRadarUsername = "openradar" 5 | 6 | final class OpenRadarPreferencesViewController: ViewController { 7 | @IBOutlet private var APIKeyTextField: NSTextField! 8 | 9 | override func viewDidLoad() { 10 | super.viewDidLoad() 11 | 12 | if let (_, password) = Keychain.get(.openRadar) { 13 | self.APIKeyTextField.stringValue = password 14 | } 15 | 16 | self.APIKeyTextField.becomeFirstResponder() 17 | } 18 | 19 | @IBAction private func getAPIKey(_ sender: Any) { 20 | NSWorkspace.shared.open(kAPIKeyURL) 21 | } 22 | 23 | fileprivate func saveCurrentToken() { 24 | let token = self.APIKeyTextField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) 25 | if token.isEmpty { 26 | Keychain.delete(.openRadar) 27 | } else { 28 | Keychain.set(username: kOpenRadarUsername, password: token, forKey: .openRadar) 29 | } 30 | } 31 | } 32 | 33 | extension OpenRadarPreferencesViewController: NSTextFieldDelegate { 34 | override func controlTextDidEndEditing(_: Notification) { 35 | if self.view.window?.isKeyWindow == true { 36 | self.saveCurrentToken() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Brisk/Controllers/RadarViewController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Sonar 3 | 4 | final class RadarViewController: ViewController { 5 | @IBOutlet fileprivate var actualTextView: NSTextView! 6 | @IBOutlet fileprivate var descriptionTextView: NSTextView! 7 | @IBOutlet fileprivate var expectedTextView: NSTextView! 8 | @IBOutlet fileprivate var notesTextView: NSTextView! 9 | @IBOutlet fileprivate var stepsTextView: NSTextView! 10 | @IBOutlet private var areaPopUp: NSPopUpButton! 11 | @IBOutlet private var classificationPopUp: NSPopUpButton! 12 | @IBOutlet private var configurationTextField: NSTextField! 13 | @IBOutlet private var productPopUp: NSPopUpButton! 14 | @IBOutlet private var progressIndicator: NSProgressIndicator! 15 | @IBOutlet private var reproducibilityPopUp: NSPopUpButton! 16 | @IBOutlet private var submitButton: NSButton! 17 | @IBOutlet private var titleTextField: NSTextField! 18 | @IBOutlet private var versionTextField: NSTextField! 19 | @IBOutlet private var toggleAttachmentButton: NSButton! 20 | @IBOutlet private var attachmentTextField: NSTextField! 21 | @IBOutlet private var postToOpenRadarButton: NSButton! 22 | @IBOutlet private var attachmentDroppableView: AttachmentDroppableView! 23 | 24 | private var attachments: [Attachment] = [] { 25 | didSet { 26 | if oldValue != self.attachments { 27 | self.document?.updateChangeCount(.changeDone) 28 | } 29 | 30 | let attachment = self.attachments.first 31 | self.toggleAttachmentButton.title = attachment == nil ? "Add Attachment" : "Remove Attachment" 32 | self.attachmentTextField.stringValue = attachment?.filename ?? "No Attachment" 33 | } 34 | } 35 | 36 | private var validatables: [Validatable] { 37 | return [ 38 | self.actualTextView, 39 | self.classificationPopUp, 40 | self.descriptionTextView, 41 | self.expectedTextView, 42 | self.productPopUp, 43 | self.reproducibilityPopUp, 44 | self.stepsTextView, 45 | self.titleTextField, 46 | self.versionTextField, 47 | ] 48 | } 49 | 50 | fileprivate var document: RadarDocument? { 51 | return self.windowController?.document as? RadarDocument 52 | } 53 | 54 | private var windowController: NSWindowController? { 55 | return self.view.window?.windowController 56 | } 57 | 58 | override func viewDidLoad() { 59 | super.viewDidLoad() 60 | 61 | self.setupTextViewDelegates() 62 | self.classificationPopUp.setItems(titles: Classification.All.map { $0.name }) 63 | self.reproducibilityPopUp.setItems(titles: Reproducibility.All.map { $0.name }) 64 | self.productPopUp.set(items: Product.All, getTitle: { $0.name }, getGroup: { $0.category }) 65 | self.attachmentDroppableView.droppedAttachment = { [weak self] attachments in 66 | self?.attachments = [attachments] 67 | } 68 | 69 | let product = Product.All.first { $0.name == self.productPopUp.selectedTitle }! 70 | self.updateAreas(with: product) 71 | } 72 | 73 | override func viewWillAppear() { 74 | super.viewWillAppear() 75 | self.view.window?.delegate = self 76 | 77 | // Workaround for #30, #33 (filed as rdar://problem/34061891) 78 | // Apparently, resizing the window makes the text views start redrawing 79 | // themselves again 80 | self.view.window.flatMap { $0.setContentSize($0.contentMinSize) } 81 | } 82 | 83 | func restore(_ radar: Radar) { 84 | self.classificationPopUp.selectItem(withTitle: radar.classification.name) 85 | self.reproducibilityPopUp.selectItem(withTitle: radar.reproducibility.name) 86 | self.productPopUp.selectItem(withTitle: radar.product.name) 87 | self.updateAreas(with: radar.product) 88 | if let area = radar.area { 89 | self.areaPopUp.selectItem(withTitle: area.name) 90 | } 91 | 92 | self.titleTextField.stringValue = radar.title 93 | self.descriptionTextView.string = radar.description 94 | self.stepsTextView.string = radar.steps 95 | self.expectedTextView.string = radar.expected 96 | self.actualTextView.string = radar.actual 97 | self.configurationTextField.stringValue = radar.configuration 98 | self.versionTextField.stringValue = radar.version 99 | self.notesTextView.string = radar.notes 100 | self.attachments = radar.attachments 101 | 102 | self.enableSubmitIfValid() 103 | self.updateTitleFromDocument() 104 | self.postToOpenRadarButton.state = .off 105 | self.document?.updateChangeCount(.changeCleared) 106 | } 107 | 108 | func currentRadar() -> Radar { 109 | let product = Product.All.first { $0.name == self.productPopUp.selectedTitle }! 110 | let classification = Classification.All.first { $0.name == self.classificationPopUp.selectedTitle }! 111 | let reproducibility = Reproducibility.All 112 | .first { $0.name == self.reproducibilityPopUp.selectedTitle }! 113 | let area = Area.areas(for: product).first { $0.name == self.areaPopUp.selectedTitle } 114 | 115 | return Radar( 116 | classification: classification, product: product, reproducibility: reproducibility, 117 | title: self.titleTextField.stringValue, 118 | description: self.descriptionTextView.string, steps: self.stepsTextView.string, 119 | expected: self.expectedTextView.string, actual: self.actualTextView.string, 120 | configuration: self.configurationTextField.stringValue, 121 | version: self.versionTextField.stringValue, notes: self.notesTextView.string, 122 | attachments: self.attachments, area: area 123 | ) 124 | } 125 | 126 | // MARK: - Private Methods 127 | 128 | @IBAction private func submitRadar(_ sender: Any) { 129 | for field in self.validatables where !field.isValid { 130 | assertionFailure("Shouldn't be able to submit with invalid fields") 131 | return self.showError(message: "Validation failed") 132 | } 133 | 134 | guard let (username, password) = Keychain.get(.radar) else { 135 | assertionFailure("Shouldn't be able to submit a radar without credentials") 136 | return self.showError(message: "Submitting radar without username/password") 137 | } 138 | 139 | var radar = self.currentRadar() 140 | self.submitButton.isEnabled = false 141 | self.progressIndicator.startAnimation(self) 142 | 143 | let appleRadar = Sonar(service: .appleRadar(appleID: username, password: password)) 144 | appleRadar.loginThenCreate( 145 | radar: radar, 146 | getTwoFactorCode: { [weak self] closure in self?.askForTwoFactorCode(closure: closure) }, 147 | closure: { [weak self] result in 148 | switch result { 149 | case .success(let radarID): 150 | guard self?.postToOpenRadarButton.state == .on, 151 | let (_, token) = Keychain.get(.openRadar) else 152 | { 153 | self?.submitRadarCompletion(success: true) 154 | return 155 | } 156 | 157 | radar.ID = radarID 158 | let openRadar = Sonar(service: .openRadar(token: token)) 159 | openRadar.loginThenCreate( 160 | radar: radar, getTwoFactorCode: { closure in 161 | assertionFailure("Didn't handle Open Radar two factor") 162 | closure(nil) 163 | }, closure: { [weak self] result in 164 | switch result { 165 | case .success: 166 | self?.submitRadarCompletion(success: true) 167 | case .failure(let error): 168 | self?.showError(message: error.message) 169 | self?.submitRadarCompletion(success: false) 170 | } 171 | }) 172 | case .failure(let error): 173 | self?.showError(message: error.message) 174 | self?.submitRadarCompletion(success: false) 175 | } 176 | }) 177 | } 178 | 179 | @IBAction private func productChanged(_ sender: NSPopUpButton) { 180 | let product = Product.All.first { $0.name == sender.selectedTitle }! 181 | self.updateAreas(with: product) 182 | } 183 | 184 | @IBAction private func toggleAttachment(_ sender: NSButton) { 185 | if !self.attachments.isEmpty { 186 | self.attachments = [] 187 | return 188 | } 189 | 190 | guard let window = self.view.window else { 191 | assertionFailure("Adding attachment with no window") 192 | return 193 | } 194 | 195 | self.addAttachment(to: window) 196 | } 197 | 198 | private func addAttachment(to window: NSWindow) { 199 | let panel = NSOpenPanel() 200 | panel.beginSheetModal(for: window) { [weak panel] response in 201 | guard response == .OK, let url = panel?.urls.first else { 202 | return 203 | } 204 | 205 | do { 206 | let attachment = try Attachment(url: url) 207 | self.attachments = [attachment] 208 | } catch AttachmentError.invalidMimeType(let fileExtension) { 209 | self.showError(message: "Unknown MIME type for extension: '\(fileExtension)'") 210 | } catch let error { 211 | let alert = NSAlert(error: error) 212 | alert.beginSheetModal(for: window, completionHandler: nil) 213 | } 214 | } 215 | } 216 | 217 | private func submitRadarCompletion(success: Bool) { 218 | self.progressIndicator.stopAnimation(self) 219 | self.submitButton.isEnabled = true 220 | 221 | if success { 222 | if self.document?.fileURL != nil { 223 | self.document?.save(self) 224 | } 225 | 226 | self.document?.close() 227 | } 228 | } 229 | 230 | private func updateAreas(with product: Product) { 231 | let areaNames = Area.areas(for: product).map { $0.name } 232 | self.areaPopUp.setItems(titles: areaNames) 233 | self.areaPopUp.isEnabled = !areaNames.isEmpty 234 | } 235 | 236 | private func showError(message: String) { 237 | guard let window = self.view.window else { 238 | return 239 | } 240 | 241 | let alert = NSAlert() 242 | alert.messageText = message 243 | alert.beginSheetModal(for: window, completionHandler: nil) 244 | } 245 | 246 | private func askForTwoFactorCode(closure: @escaping (String?) -> Void) { 247 | guard let window = self.view.window else { 248 | return 249 | } 250 | 251 | let alert = NSAlert() 252 | alert.messageText = "Enter two factor auth code" 253 | alert.addButton(withTitle: "OK") 254 | alert.addButton(withTitle: "Cancel") 255 | let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 100, height: 22)) 256 | alert.accessoryView = field 257 | alert.beginSheetModal(for: window) { [weak self] response in 258 | if response == .alertFirstButtonReturn { 259 | if field.stringValue.isEmpty { 260 | self?.askForTwoFactorCode(closure: closure) 261 | } else { 262 | closure(field.stringValue) 263 | } 264 | } else { 265 | closure(nil) 266 | } 267 | } 268 | 269 | field.becomeFirstResponder() 270 | } 271 | 272 | fileprivate func updateOpenRadarButton() { 273 | let canPostToOpenRadar = Keychain.get(.openRadar) != nil 274 | self.postToOpenRadarButton.isEnabled = canPostToOpenRadar 275 | self.postToOpenRadarButton.toolTip = canPostToOpenRadar ? nil : "Open Preferences to add an account" 276 | 277 | if !canPostToOpenRadar { 278 | self.postToOpenRadarButton.state = .off 279 | } 280 | } 281 | 282 | fileprivate func enableSubmitIfValid() { 283 | let isValid = self.validatables.reduce(true) { valid, validatable in 284 | return valid && validatable.isValid 285 | } 286 | 287 | self.submitButton.isEnabled = isValid 288 | } 289 | 290 | fileprivate func updateTitleFromDocument() { 291 | let title = self.titleTextField.stringValue 292 | if title.isEmpty { 293 | return 294 | } 295 | 296 | self.document?.displayName = title 297 | self.windowController?.synchronizeWindowTitleWithDocumentName() 298 | } 299 | } 300 | 301 | extension RadarViewController: NSTextViewDelegate { 302 | private var textViews: [NSTextView] { 303 | return [ 304 | self.actualTextView, 305 | self.descriptionTextView, 306 | self.expectedTextView, 307 | self.notesTextView, 308 | self.stepsTextView, 309 | ] 310 | } 311 | 312 | override func controlTextDidChange(_ obj: Notification) { 313 | self.document?.updateChangeCount(.changeDone) 314 | self.textChanged() 315 | } 316 | 317 | func textDidChange(_ notification: Notification) { 318 | self.textChanged() 319 | } 320 | 321 | private func textChanged() { 322 | self.enableSubmitIfValid() 323 | self.updateTitleFromDocument() 324 | } 325 | 326 | fileprivate func setupTextViewDelegates() { 327 | for textView in self.textViews { 328 | textView.delegate = self 329 | } 330 | } 331 | } 332 | 333 | extension RadarViewController: NSWindowDelegate { 334 | func windowDidBecomeKey(_ notification: Notification) { 335 | self.updateOpenRadarButton() 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /Brisk/Controllers/TabViewController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | final class TabViewController: NSTabViewController { 4 | override func viewWillAppear() { 5 | super.viewWillAppear() 6 | NSApp.activate(ignoringOtherApps: true) 7 | self.setTitle(withItem: self.tabView.selectedTabViewItem) 8 | } 9 | 10 | override func tabView(_ tabView: NSTabView, willSelect tabViewItem: NSTabViewItem?) { 11 | super.tabView(tabView, didSelect: tabViewItem) 12 | self.setTitle(withItem: tabViewItem) 13 | } 14 | 15 | private func setTitle(withItem item: NSTabViewItem?) { 16 | let newTitle = item?.viewController?.title 17 | self.view.window?.title = newTitle ?? "" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Brisk/Controllers/ViewController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class ViewController: NSViewController { 4 | override func viewWillAppear() { 5 | super.viewWillAppear() 6 | NSApp.activate(ignoringOtherApps: true) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Brisk/Extensions/Attachment+Serialization.swift: -------------------------------------------------------------------------------- 1 | import Sonar 2 | 3 | public extension Attachment { 4 | func toJSON() -> [String: Any] { 5 | return [ 6 | "filename": self.filename, 7 | "mimetype": self.mimeType, 8 | "data": self.data.base64EncodedString(), 9 | ] 10 | } 11 | 12 | func toData() throws -> Data { 13 | return try JSONSerialization.data(withJSONObject: self.toJSON(), options: []) 14 | } 15 | 16 | init?(json: [String: Any]) { 17 | guard let filename = json["filename"] as? String, 18 | let mimeType = json["mimetype"] as? String, 19 | let encodedData = json["data"] as? String, 20 | let data = Data(base64Encoded: encodedData) else 21 | { 22 | return nil 23 | } 24 | 25 | self.init(filename: filename, mimeType: mimeType, data: data) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Brisk/Extensions/Data+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Data { 4 | public func toJSONDictionary() -> [String: Any]? { 5 | return (try? JSONSerialization.jsonObject(with: self, options: [])) as? [String: Any] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Brisk/Extensions/Dictionary+Extension.swift: -------------------------------------------------------------------------------- 1 | public extension Dictionary where Key == String { 2 | public func onlyStrings() -> [String: String] { 3 | var newDictionary = [String: String]() 4 | for (key, value) in self { 5 | newDictionary[key] = value as? String 6 | } 7 | 8 | return newDictionary 9 | } 10 | } 11 | 12 | public extension Dictionary where Key == String, Value == String { 13 | public func filterEmpty() -> [String: String] { 14 | var newDictionary = [String: String]() 15 | for (key, value) in self where !value.isEmpty { 16 | newDictionary[key] = value 17 | } 18 | 19 | return newDictionary 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Brisk/Extensions/NSDocumentController+Extension.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | private let kDocumentType = "com.brisk.radar" 4 | 5 | extension NSDocumentController { 6 | func makeRadarDocument() -> RadarDocument? { 7 | return (try? self.makeUntitledDocument(ofType: kDocumentType)) as? RadarDocument 8 | } 9 | 10 | func makeRadarDocument(withContentsOf url: URL) -> RadarDocument? { 11 | return (try? self.makeDocument(withContentsOf: url, ofType: kDocumentType)) as? RadarDocument 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Brisk/Extensions/NSPopUpButton+Extension.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | extension NSPopUpButton { 4 | var selectedTitle: String { 5 | return self.selectedItem?.title ?? "" 6 | } 7 | 8 | func setItems(titles: [String]) { 9 | let selectedTitle = self.selectedTitle 10 | self.removeAllItems() 11 | self.addItems(withTitles: titles) 12 | self.selectItem(withTitle: selectedTitle) 13 | if self.selectedItem == nil && self.numberOfItems > 0 { 14 | self.selectItem(at: 0) 15 | } 16 | } 17 | 18 | func set(items: [T], getTitle: (T) -> String, getGroup: (T) -> String) { 19 | let menu = NSMenu() 20 | menu.autoenablesItems = false 21 | menu.set(items: items, getTitle: getTitle, getGroup: getGroup) 22 | self.menu = menu 23 | if let index = menu.items.index(where: { $0.indentationLevel == 1 }) { 24 | self.selectItem(at: index) 25 | } 26 | } 27 | } 28 | 29 | extension NSPopUpButton: Validatable { 30 | var isValid: Bool { 31 | return !self.selectedTitle.isEmpty 32 | } 33 | } 34 | 35 | private extension NSMenu { 36 | func set(items: [T], getTitle: (T) -> String, getGroup: (T) -> String) { 37 | var groups = [String]() 38 | var titlesForGroup = [String: [String]]() 39 | 40 | for item in items { 41 | let group = getGroup(item) 42 | if !groups.contains(group) { 43 | groups.append(group) 44 | } 45 | 46 | let names = titlesForGroup[group] ?? [] 47 | titlesForGroup[group] = names + [getTitle(item)] 48 | } 49 | 50 | for group in groups { 51 | let titles = titlesForGroup[group]! 52 | let topLevelItem = NSMenuItem() 53 | topLevelItem.isEnabled = false 54 | topLevelItem.title = group 55 | self.addItem(topLevelItem) 56 | 57 | for title in titles { 58 | let item = NSMenuItem() 59 | item.title = title 60 | item.indentationLevel = 1 61 | self.addItem(item) 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Brisk/Extensions/NSProgressIndicator+Extension.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | extension NSProgressIndicator { 4 | var isLoading: Bool { 5 | set(loading) { 6 | if loading { 7 | self.startAnimation(nil) 8 | } else { 9 | self.stopAnimation(nil) 10 | } 11 | } 12 | 13 | get { 14 | assertionFailure("This value is bogus") 15 | return false 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Brisk/Extensions/NSStatusItem+Extension.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | extension NSStatusItem { 4 | static func create(image: NSImage, menu: NSMenu) -> NSStatusItem { 5 | let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 6 | statusItem.highlightMode = true 7 | statusItem.image = image 8 | statusItem.menu = menu 9 | 10 | return statusItem 11 | } 12 | 13 | func remove() { 14 | NSStatusBar.system.removeStatusItem(self) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Brisk/Extensions/NSStoryboard+Extension.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | private let kMainStoryboard = NSStoryboard(name: .init(rawValue: "Main"), bundle: nil) 4 | 5 | extension NSStoryboard { 6 | static var main: NSStoryboard { 7 | return kMainStoryboard 8 | } 9 | 10 | func instantiateWindowController(identifier: String) -> NSWindowController { 11 | return self.instantiateController(withIdentifier: .init(rawValue: identifier)) as! NSWindowController 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Brisk/Extensions/NSTextField+Extension.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | extension NSTextField: Validatable { 4 | var isValid: Bool { 5 | return !self.stringValue.isEmpty 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Brisk/Extensions/NSTextView+Extension.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | extension NSTextView: Validatable { 4 | var isValid: Bool { 5 | return !self.string.isEmpty 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Brisk/Extensions/Radar+OpenRadar.swift: -------------------------------------------------------------------------------- 1 | import Sonar 2 | 3 | public enum OpenRadarParsingError: Error { 4 | case noResult 5 | case missingRequiredFields 6 | case invalidFormat 7 | } 8 | 9 | public extension Radar { 10 | public init(openRadar json: [String: Any]) throws { 11 | guard let dictionary = json["result"] as? [String: Any] else { 12 | throw OpenRadarParsingError.noResult 13 | } 14 | 15 | let json = dictionary.onlyStrings().filterEmpty() 16 | guard let title = json["title"], let description = json["description"] else { 17 | throw OpenRadarParsingError.missingRequiredFields 18 | } 19 | 20 | let classificationString = json["classification"]?.lowercased() 21 | let classification = Classification.All.first { $0.name.lowercased() == classificationString } 22 | ?? Classification.All.first! 23 | let productString = json["product"]?.lowercased() 24 | let product = Product.All.first { $0.name.lowercased() == productString } ?? Product.All.first! 25 | 26 | let reproducibilityString = json["reproducible"]?.lowercased() 27 | let reproducibility = Reproducibility.All.first { $0.name.lowercased() == reproducibilityString } 28 | ?? Reproducibility.All.first! 29 | 30 | // Pick the last area (if there are any for the product) instead of defaulting to the first one from 31 | // the UI. Ideally we just wouldn't pick one in this case 32 | let lastArea = Area.areas(for: product).last 33 | 34 | let productVersion = json["product_version"] 35 | let radarID = json["number"] 36 | 37 | do { 38 | let openRadar = try description.openRadarFromSummary() 39 | let version = productVersion ?? openRadar.version ?? " " 40 | let updatedDescription = summary(for: radarID, description: openRadar.description ?? description) 41 | let area = Area.areas(for: product) 42 | .first { $0.name.lowercased() == openRadar.areaString?.lowercased() } 43 | 44 | self.init(classification: classification, product: product, reproducibility: reproducibility, 45 | title: title, description: updatedDescription, steps: openRadar.steps ?? " ", 46 | expected: openRadar.expected ?? " ", actual: openRadar.actual ?? " ", 47 | configuration: openRadar.configuration ?? "", version: version, 48 | notes: openRadar.notes ?? "", attachments: [], area: area ?? lastArea) 49 | 50 | } catch is OpenRadarParsingError { 51 | let updatedDescription = summary(for: radarID, description: description) 52 | let version = productVersion ?? " " 53 | 54 | self.init(classification: classification, product: product, reproducibility: reproducibility, 55 | title: title, description: updatedDescription, steps: " ", expected: " ", actual: " ", 56 | configuration: "", version: version, notes: "", attachments: [], area: lastArea) 57 | 58 | } catch let error { 59 | assertionFailure("Got unexpected error type \(error)") 60 | throw error 61 | } 62 | } 63 | } 64 | 65 | private func summary(for radarID: String?, description: String) -> String { 66 | return radarID.map { "This is a duplicate of radar #\($0)\n\n\(description)\n" } ?? description 67 | } 68 | -------------------------------------------------------------------------------- /Brisk/Extensions/Radar+Serialization.swift: -------------------------------------------------------------------------------- 1 | import Sonar 2 | 3 | public extension Radar { 4 | func toData() throws -> Data { 5 | var JSON: [String: Any] = [ 6 | "title": self.title, 7 | "description": self.description, 8 | "classification_id": self.classification.appleIdentifier, 9 | "product_id": self.product.appleIdentifier, 10 | "reproducibility_id": self.reproducibility.appleIdentifier, 11 | "steps": self.steps, 12 | "expected": self.expected, 13 | "actual": self.actual, 14 | "configuration": self.configuration, 15 | "version": self.version, 16 | "notes": self.notes, 17 | ] 18 | 19 | JSON["area_id"] = self.area?.appleIdentifier 20 | JSON["application_id"] = self.applicationID 21 | JSON["user_id"] = self.userID 22 | JSON["attachments"] = self.attachments.map { $0.toJSON() } 23 | 24 | return try JSONSerialization.data(withJSONObject: JSON, options: []) 25 | } 26 | 27 | public init?(json: [String: Any]) { 28 | guard let title = json["title"] as? String, 29 | let description = json["description"] as? String, 30 | let classificationID = json["classification_id"] as? Int, 31 | let productID = json["product_id"] as? Int, 32 | let reproducibilityID = json["reproducibility_id"] as? Int, 33 | let steps = json["steps"] as? String, 34 | let expected = json["expected"] as? String, 35 | let actual = json["actual"] as? String, 36 | let configuration = json["configuration"] as? String, 37 | let version = json["version"] as? String, 38 | let notes = json["notes"] as? String else 39 | { 40 | return nil 41 | } 42 | 43 | let areaID = json["area_id"] as? Int 44 | let applicationID = json["application_id"] as? String 45 | let userID = json["user_id"] as? String 46 | let attachments = (json["attachments"] as? [[String: Any]] ?? []).compactMap(Attachment.init) 47 | 48 | let classification = Classification.All.first { $0.appleIdentifier == classificationID } 49 | ?? Classification.All.first! 50 | let reproducibility = Reproducibility.All.first { $0.appleIdentifier == reproducibilityID } 51 | ?? Reproducibility.All.first! 52 | let product = Product.All.first { $0.appleIdentifier == productID } ?? Product.All.first! 53 | let area = Area.areas(for: product).first { $0.appleIdentifier == areaID } 54 | 55 | self.init(classification: classification, product: product, reproducibility: reproducibility, 56 | title: title, description: description, steps: steps, expected: expected, actual: actual, 57 | configuration: configuration, version: version, notes: notes, 58 | attachments: attachments, area: area, applicationID: applicationID, userID: userID) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Brisk/Extensions/String+Extension.swift: -------------------------------------------------------------------------------- 1 | public extension String { 2 | public func strip() -> String { 3 | return self.trimmingCharacters(in: .whitespacesAndNewlines) 4 | } 5 | } 6 | 7 | public func appendOrReturn(_ string1: String?, _ string2: String?) -> String? { 8 | if let string1 = string1, let string2 = string2 { 9 | return string1 + "\n" + string2 10 | } 11 | 12 | return string1 ?? string2 13 | } 14 | -------------------------------------------------------------------------------- /Brisk/GlobalHotKey.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Carbon 3 | 4 | struct GlobalHotKey { 5 | // ⌃⌥⌘-SPACE 6 | static func register() { 7 | // Register event handler 8 | var eventSpec = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), 9 | eventKind: UInt32(kEventHotKeyReleased)) 10 | InstallEventHandler(GetApplicationEventTarget(), { _, _, _ in 11 | return GlobalHotKey.hotKeyTriggered() 12 | }, 1, &eventSpec, nil, nil) 13 | 14 | // Register global hot key 15 | let htk1OSType = "htk1".utf16.reduce(0) { ($0 << 8) + OSType($1) } 16 | var carbonHotKey: EventHotKeyRef? 17 | // There's no reasonable way to handle this error, so just ignore it 18 | _ = RegisterEventHotKey(UInt32(kVK_Space), UInt32(controlKey | optionKey | cmdKey), 19 | EventHotKeyID(signature: htk1OSType, id: 1), 20 | GetEventDispatcherTarget(), 0, &carbonHotKey) 21 | } 22 | 23 | // Create new document when hot key is triggered 24 | private static func hotKeyTriggered() -> OSStatus { 25 | NSDocumentController.shared.newDocument(nil) 26 | return noErr 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Brisk/Keychain.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Security 3 | 4 | private let kAccessibilityLevel = kSecAttrAccessibleWhenUnlockedThisDeviceOnly 5 | private let kService = "Brisk" 6 | 7 | enum KeychainKey: String { 8 | case radar = "Radar Login" 9 | case openRadar = "Open Radar Token" 10 | } 11 | 12 | struct Keychain { 13 | static func get(_ key: KeychainKey) -> (String, String)? { 14 | let attributes: [CFString: Any] = [ 15 | kSecAttrAccessible: kAccessibilityLevel, 16 | kSecAttrLabel: key.rawValue, 17 | kSecAttrService: kService, 18 | kSecClass: kSecClassGenericPassword, 19 | kSecMatchLimit: kSecMatchLimitOne, 20 | kSecReturnAttributes: kCFBooleanTrue, 21 | kSecReturnData: kCFBooleanTrue, 22 | ] 23 | 24 | var result: CFTypeRef? 25 | let status = SecItemCopyMatching(attributes as CFDictionary, &result) 26 | 27 | guard let dictionary = result as? NSDictionary, status.success else { 28 | return nil 29 | } 30 | 31 | let username = dictionary[kSecAttrAccount as String] as? String 32 | let passwordData = dictionary[kSecValueData as String] as? Data 33 | let password = passwordData.flatMap { String(data: $0, encoding: String.Encoding.utf8) } 34 | if let username = username, let password = password { 35 | return (username, password) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | @discardableResult 42 | static func set(username: String, password: String, forKey key: KeychainKey) -> Bool { 43 | guard let passwordData = password.data(using: String.Encoding.utf8) else { 44 | return false 45 | } 46 | 47 | self.delete(key) 48 | 49 | let attributes: [CFString: Any] = [ 50 | kSecAttrAccessible: kAccessibilityLevel, 51 | kSecAttrAccount: username, 52 | kSecAttrLabel: key.rawValue, 53 | kSecAttrService: kService, 54 | kSecClass: kSecClassGenericPassword, 55 | kSecValueData: passwordData, 56 | ] 57 | 58 | let status = SecItemAdd(attributes as CFDictionary, nil) 59 | return status.success 60 | } 61 | 62 | @discardableResult 63 | static func delete(_ key: KeychainKey) -> Bool { 64 | let attributes: [CFString: Any] = [ 65 | kSecAttrAccessible: kAccessibilityLevel, 66 | kSecAttrLabel: key.rawValue, 67 | kSecAttrService: kService, 68 | kSecClass: kSecClassGenericPassword, 69 | ] 70 | 71 | let status = SecItemDelete(attributes as CFDictionary) 72 | return status.success 73 | } 74 | } 75 | 76 | extension CFString: Hashable { 77 | public var hashValue: Int { 78 | return (self as String).hashValue 79 | } 80 | } 81 | 82 | public func == (lhs: CFString, rhs: CFString) -> Bool { 83 | return lhs as String == rhs as String 84 | } 85 | 86 | private extension OSStatus { 87 | var success: Bool { 88 | return self == noErr 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Brisk/Models/OpenRadar.swift: -------------------------------------------------------------------------------- 1 | private let sectionToKeyPath: [String: WritableKeyPath] = [ 2 | "actual results": \.appendingToActual, 3 | "area": \.areaString, 4 | "configuration": \.configuration, 5 | "expected results": \.expected, 6 | "notes": \.notes, 7 | "observed results": \.appendingToActual, 8 | "steps to reproduce": \.steps, 9 | "summary": \.description, 10 | "version": \.version, 11 | ] 12 | 13 | public struct OpenRadar { 14 | public var actual: String? 15 | public var areaString: String? 16 | public var configuration: String? 17 | public var description: String? 18 | public var expected: String? 19 | public var notes: String? 20 | public var steps: String? 21 | public var version: String? 22 | fileprivate var appendingToActual: String? { 23 | get { return self.actual } 24 | set { self.actual = appendOrReturn(self.actual, newValue) } 25 | } 26 | 27 | fileprivate init() {} 28 | } 29 | 30 | public extension String { 31 | public func openRadarFromSummary() throws -> OpenRadar { 32 | let components = self.components(separatedBy: "\r\n") 33 | var openRadar = OpenRadar() 34 | var parts = [String]() 35 | var lastKeypath: WritableKeyPath? 36 | 37 | for component in components { 38 | guard component.last == ":", 39 | let keyPath = sectionToKeyPath[String(component.dropLast()).lowercased()] else 40 | { 41 | parts.append(component) 42 | continue 43 | } 44 | 45 | if !parts.isEmpty && lastKeypath == nil { 46 | throw OpenRadarParsingError.invalidFormat 47 | } 48 | 49 | if let lastKeypath = lastKeypath { 50 | openRadar[keyPath: lastKeypath] = parts.joined(separator: "\r\n").strip() 51 | } 52 | parts = [] 53 | lastKeypath = keyPath 54 | } 55 | 56 | if let keyPath = lastKeypath { 57 | if !parts.isEmpty { 58 | openRadar[keyPath: keyPath] = parts.joined(separator: "\r\n").strip() 59 | } 60 | } else { 61 | throw OpenRadarParsingError.invalidFormat 62 | } 63 | 64 | return openRadar 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Brisk/Models/RadarDocument.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Sonar 3 | 4 | private struct DocumentError: Error {} 5 | 6 | final class RadarDocument: NSDocument { 7 | override func makeWindowControllers() { 8 | self.makeWindowControllers(for: nil) 9 | } 10 | 11 | override func data(ofType typeName: String) throws -> Data { 12 | let viewController = self.windowControllers.first?.contentViewController as? RadarViewController 13 | if let radar = viewController?.currentRadar() { 14 | return try radar.toData() 15 | } 16 | 17 | throw DocumentError() 18 | } 19 | 20 | override func read(from data: Data, ofType typeName: String) throws { 21 | if let json = data.toJSONDictionary(), let radar = Radar(json: json) { 22 | self.makeWindowControllers(for: radar) 23 | } else { 24 | throw DocumentError() 25 | } 26 | } 27 | 28 | func makeWindowControllers(for radar: Radar?) { 29 | let windowController = NSStoryboard.main.instantiateWindowController(identifier: "Radar") 30 | self.addWindowController(windowController) 31 | 32 | if let radar = radar { 33 | let viewController = windowController.contentViewController as! RadarViewController 34 | viewController.restore(radar) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Brisk/Models/Result.swift: -------------------------------------------------------------------------------- 1 | public enum Result { 2 | case success(T) 3 | case failure(U) 4 | 5 | public init(value: T?, failWith error: @autoclosure () -> U) { 6 | if let value = value { 7 | self = .success(value) 8 | } else { 9 | self = .failure(error()) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Brisk/Models/Validatable.swift: -------------------------------------------------------------------------------- 1 | protocol Validatable { 2 | var isValid: Bool { get } 3 | } 4 | -------------------------------------------------------------------------------- /Brisk/Reproducability.swift: -------------------------------------------------------------------------------- 1 | struct Reproducability { 2 | let id: String 3 | let name: String 4 | } 5 | -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/Brisk/Resources/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/Brisk/Resources/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/Brisk/Resources/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/Brisk/Resources/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/Brisk/Resources/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/Brisk/Resources/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/Brisk/Resources/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/StatusItemIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "StatusItemIcon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "StatusItemIcon@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/StatusItemIcon.imageset/StatusItemIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/Brisk/Resources/Assets.xcassets/StatusItemIcon.imageset/StatusItemIcon.png -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/StatusItemIcon.imageset/StatusItemIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/Brisk/Resources/Assets.xcassets/StatusItemIcon.imageset/StatusItemIcon@2x.png -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/radar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "radar.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Brisk/Resources/Assets.xcassets/radar.imageset/radar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/Brisk/Resources/Assets.xcassets/radar.imageset/radar.png -------------------------------------------------------------------------------- /Brisk/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeExtensions 11 | 12 | brisk 13 | 14 | CFBundleTypeIconFile 15 | AppIcon 16 | CFBundleTypeName 17 | RadarDocument 18 | CFBundleTypeRole 19 | Editor 20 | LSItemContentTypes 21 | 22 | com.brisk.radar 23 | 24 | LSTypeIsPackage 25 | 0 26 | NSDocumentClass 27 | Brisk.RadarDocument 28 | 29 | 30 | CFBundleExecutable 31 | $(EXECUTABLE_NAME) 32 | CFBundleIdentifier 33 | $(PRODUCT_BUNDLE_IDENTIFIER) 34 | CFBundleInfoDictionaryVersion 35 | 6.0 36 | CFBundleName 37 | $(PRODUCT_NAME) 38 | CFBundlePackageType 39 | APPL 40 | CFBundleShortVersionString 41 | 1.2.0 42 | CFBundleSignature 43 | BRSK 44 | CFBundleURLTypes 45 | 46 | 47 | CFBundleURLName 48 | 49 | CFBundleURLSchemes 50 | 51 | rdar 52 | 53 | 54 | 55 | CFBundleVersion 56 | 1.2.0 57 | LSApplicationCategoryType 58 | public.app-category.developer-tools 59 | LSMinimumSystemVersion 60 | $(MACOSX_DEPLOYMENT_TARGET) 61 | LSUIElement 62 | 63 | NSHumanReadableCopyright 64 | Copyright © 2017 Brisk. All rights reserved. 65 | NSMainStoryboardFile 66 | Main 67 | NSPrincipalClass 68 | Brisk.Application 69 | SUFeedURL 70 | https://raw.githubusercontent.com/br1sk/brisk/master/appcast.xml 71 | SUPublicDSAKeyFile 72 | dsa_pub.pem 73 | UTExportedTypeDeclarations 74 | 75 | 76 | UTTypeConformsTo 77 | 78 | public.plain-text 79 | 80 | UTTypeIdentifier 81 | com.brisk.radar 82 | UTTypeTagSpecification 83 | 84 | public.filename-extension 85 | 86 | brisk 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Brisk/Resources/dsa_pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIGOzCCBC0GByqGSM44BAEwggQgAoICAQDVlSb+DNzvlK5STBsxh49uJra1Fyez 3 | pnNOU7zxvsVocw0tKvto6JkPDjhUlt8WudnU/QxP53Z41ZXDnKaghqP6LZ5CBAjp 4 | SkVhEK7qV3AsRGuMAaXCJ70EOQoIhhasAXQh37VDEjZfvL5yed6leLwS8/31IE6x 5 | VP3drGPrvEI3c5WaavSgR0ZQKphjn87KbOALAT+kzXmNubYocqkcfwB7HqdxJUCa 6 | cUVBr0BWtcDfjzXUzxeH8h1lnqiva4Z/59szDJgpg5RucO4ZCdes2QgNTs3VUSvd 7 | 0ak0l2x3gl/QH/ObNLQ/G5i1ITslRdIvnn3d9+/bBQugdzaOmTagDnQQ2qbpC9+k 8 | a5ofksoST65iD3fHt/dFF+nqJmeOpo5Ldn5ah3G1zRfxE5ii5EgsBOqJLuWBHBUY 9 | wt14vG+h4EHmY6j/ue4c8VKCZDnoU+rqMbqlDoRy9umW0A/ox+k88GM2+PP2+CDJ 10 | EtnRs4E1c+8wB+06KdS3wDV5qpGxNmBGX06tLK1vpxh4GATMAvAEYm1VEpTSs8CS 11 | H0VAWUTa4y9C9c1htsKiRnI27zvrVtXHYrJDg/fbQaoEfZAEYN21wkdmUzWNTJuc 12 | 9DwWqa6uQ1Beje7BetONYw1tQUszDN90ChtO6zHGj7Camvs9Y3Cq8d8sryAhA3sE 13 | FAvuLx297MVoiwIVAMHaQRtvJOM2bxyGZgJGFYOiZ0ifAoICAGa2uutzi7uf/3y0 14 | yNXM/OHCkNr0ckRcNNa9x1h5xHH1yyhG/gApDzYxGRR+pgDxKTVAkvA6vPhx2riX 15 | ELH1C9xPblJU4qNDsA2XI4lpQZXD1mIeifeOrMwXSpqSLCKrR85j6ytvmYZtDM5X 16 | XtcemJqd1nh6ImDrDp6JgUrbHVz1aVpkz/XIUX95Guwgrcd/cPh4LCseJpQ2wkqN 17 | fKwPardKs2BGla20dPpQUtB2BzaKUCXF1ZFKyLeLryi5mKgzMC0TvwG5IYXxHKpZ 18 | 7v95S7+JNPzTkAf11Eg/lhU4hZLjdktuUXUIWkBscgjzztW8xC7/1oeTsiRNYPck 19 | 7mi0hoF9xX+7xLGeedyP4rLyTA9u9+rOWCfpAnxUFLoHAraw2D/6gazU6B2UC8Y+ 20 | akvs7JpIkzSoDIU6CwIFN9h/WJdvLhC5P4fDowyIQEOez8pj6IO7dCCE9Qg8XauY 21 | T0lzTN8hElAZK/7yR4+5JjJV/UXaM9RWYBLs0k/kK9NuUMDM2JuVkUeyrTRphBLf 22 | HRh+4kXNVjW2/gmUrnr+aI9o8z8U5luZ+UMfwZ782pFxrXuBsc2QwoLKkLPamcXU 23 | H+JSb5lWWqrZzGzUhpLK8jk0iGtNN56BrYoyxwywvL+AXZBO1RlhKkEKPBG4qqVL 24 | jPkqR6CH3mEiIlA99y8pUL5xEQW0A4ICBgACggIBAIGlITJq+ThqGPgtKy578OeF 25 | cpfQ/yI8Er8qjyKREDJq7fdOcoTKyw3vwROKMBn3D/SH2YQ6OfRRBz3BIV8DsNtE 26 | qbd1EvtNMqMyi9Bqov/XxXYp1YlZFfWIPiS0+Hux5cqxyTb8UyXggH2pRKcIxOGL 27 | 3W3mzmbfSpkoUCIKN8TBmoAr6xIzv4PufgYvDrYj3Tdm3Sv0NMbZgPv79E6VWWTF 28 | VH+iCfRBC3Qfmx4z5wqwXa4q8dW2wXb7tXuefrp/NWUmE2CGX5zC3DrLS20gjUpi 29 | lWG3gx61WNRjFdDn5tFyYiWTcOMqSNwaBo19mIKpZqYGGWD2la7zamURWLvShODw 30 | sMqWZzhZvw89ReAG88nJBR2M+mOoH8F9Md0RLPIPGAcrQtF/RcGsEB7gNL7ymJT8 31 | dR7DFQ0YrrDj5iKIEvBzbqpQPmecguNYENfzouy+tc/3RYTUUXKSpYxx6ZElLr7J 32 | amuM3h3WgVsLXb74eCWXcBsN7BvpqhO5DnX8NLIkdNLsAD9ikpm0Tc7+SWF/7DEg 33 | MbShZNlL5A97qGUtMmWjw4EWoorTRqAEC/3e83vMjeem11GAKQJQQkJp7zCk6MbZ 34 | ms32aZdUFPpr6pcsIuOnj+xBRO84WSZsZzBInY0Dquw8iXJZTu2ohgLN8j3OuJSG 35 | oacTy7Gjmy9fopFy3xba 36 | -----END PUBLIC KEY----- 37 | -------------------------------------------------------------------------------- /Brisk/StoryboardRouter.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | final class StoryboardRouter: NSObject { 4 | fileprivate static let sharedRouter = StoryboardRouter() 5 | fileprivate var windowController: NSWindowController? 6 | 7 | static func reloadTopWindowController() { 8 | self.sharedRouter.reloadTopWindowController() 9 | } 10 | 11 | private func reloadTopWindowController() { 12 | self.windowController?.window?.delegate = nil 13 | self.windowController?.close() 14 | NSApp.stopModal() 15 | 16 | let radarLogin = Keychain.get(.radar) 17 | if radarLogin == nil { 18 | self.windowController = NSStoryboard.main.instantiateWindowController(identifier: "Login") 19 | NSApp.runModal(for: self.windowController!.window!) 20 | } 21 | 22 | self.windowController?.window?.delegate = self 23 | NSApp.activate(ignoringOtherApps: true) 24 | } 25 | } 26 | 27 | extension StoryboardRouter: NSWindowDelegate { 28 | func windowWillClose(_ notification: Notification) { 29 | self.windowController = nil 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Brisk/Views/AttachmentDroppableView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Sonar 3 | 4 | private enum AttachmentDropError: Error { 5 | case noAttachments 6 | } 7 | 8 | final class AttachmentDroppableView: NSView { 9 | /// Called with the dropped attachment after the drop has been completed 10 | var droppedAttachment: ((Attachment) -> Void)! 11 | 12 | override func awakeFromNib() { 13 | super.awakeFromNib() 14 | self.registerForDraggedTypes([makeFileNameType()]) 15 | } 16 | 17 | override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { 18 | do { 19 | _ = try sender.getAttachments() 20 | return .copy 21 | } catch { 22 | return [] 23 | } 24 | } 25 | 26 | override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { 27 | do { 28 | let attachments = try sender.getAttachments() 29 | self.droppedAttachment(attachments.first!) 30 | return true 31 | } catch { 32 | return false 33 | } 34 | } 35 | } 36 | 37 | fileprivate extension NSDraggingInfo { 38 | private var files: [URL] { 39 | let asStrings = self.draggingPasteboard().propertyList(forType: makeFileNameType()) as? [String] ?? [] 40 | return asStrings.map { URL(fileURLWithPath: $0) } 41 | } 42 | 43 | fileprivate func getAttachments() throws -> [Attachment] { 44 | if self.files.isEmpty { 45 | throw AttachmentDropError.noAttachments 46 | } 47 | 48 | return try self.files.map { try Attachment(url: $0) } 49 | } 50 | } 51 | 52 | private func makeFileNameType() -> NSPasteboard.PasteboardType { 53 | // in 10.13 there is more modern NSPasteboard.PasteboardType.fileURL or previously 54 | // NSPasteboard.PasteboardType("public.file-url"), but so far couldn't find a way 55 | // to read them from draggingPasteboard() 56 | return NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType") 57 | } 58 | -------------------------------------------------------------------------------- /Brisk/Views/TextView.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | final class TextView: NSTextView { 4 | @IBInspectable var placeholderString: String? 5 | 6 | // Show a placeholder using the private API from NSTextView 7 | // https://stackoverflow.com/a/43028577/902968 8 | @objc 9 | private var placeholderAttributedString: NSAttributedString? { 10 | return self.placeholderString.map(NSAttributedString.init) 11 | } 12 | 13 | required init?(coder: NSCoder) { 14 | super.init(coder: coder) 15 | 16 | self.font = NSFont.systemFont(ofSize: NSFont.systemFontSize) 17 | } 18 | 19 | override func doCommand(by selector: Selector) { 20 | switch selector { 21 | case #selector(TextView.insertTab(_:)): 22 | self.window?.selectNextKeyView(self) 23 | case #selector(TextView.insertBacktab(_:)): 24 | self.window?.selectPreviousKeyView(self) 25 | case #selector(NSWindow.selectNextKeyView(_:)): 26 | super.doCommand(by: #selector(TextView.insertTab(_:))) 27 | case #selector(NSWindow.selectPreviousKeyView(_:)): 28 | super.doCommand(by: #selector(TextView.insertBacktab(_:))) 29 | default: 30 | super.doCommand(by: selector) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BriskTests/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - force_try 3 | -------------------------------------------------------------------------------- /BriskTests/DictionaryExtensionTests.swift: -------------------------------------------------------------------------------- 1 | import Brisk 2 | import XCTest 3 | 4 | final class DictionaryExtensionTests: XCTestCase { 5 | func testFilteringExceptStrings() { 6 | let dictionary: [String: Any] = ["foo": "bar", "baz": 1] 7 | let newDictionary = dictionary.onlyStrings() 8 | 9 | XCTAssertEqual(newDictionary.count, 1) 10 | XCTAssertEqual(newDictionary["foo"], "bar") 11 | } 12 | 13 | func testFilteringEmptyStrings() { 14 | let dictionary: [String: String] = ["foo": "", "bar": "baz"] 15 | let newDictionary = dictionary.filterEmpty() 16 | 17 | XCTAssertEqual(newDictionary.count, 1) 18 | XCTAssertEqual(newDictionary["bar"], "baz") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /BriskTests/OpenRadarTests.swift: -------------------------------------------------------------------------------- 1 | import Brisk 2 | import Sonar 3 | import XCTest 4 | 5 | final class OpenRadarTests: XCTestCase { 6 | func testDeserializingJSON() { 7 | let json = loadOpenRadarJSON() 8 | guard let radar = try? Radar(openRadar: json) else { 9 | return XCTFail("Failed to deserializing open radar JSON") 10 | } 11 | 12 | XCTAssertEqual(radar.classification, .OtherBug) 13 | XCTAssertEqual(radar.product, .DeveloperTools) 14 | XCTAssertEqual(radar.reproducibility, .Always) 15 | XCTAssertEqual(radar.version, "Xcode 9.0") 16 | XCTAssertEqual(radar.configuration, "") 17 | XCTAssertEqual(radar.title, "Some title") 18 | XCTAssertEqual(radar.description, "This is a duplicate of radar #1234\n\nfoo\n\nbar\nbaz\n") 19 | XCTAssertEqual(radar.steps, "1. foo\n2. bar") 20 | XCTAssertEqual(radar.expected, "foo") 21 | XCTAssertEqual(radar.actual, "bar") 22 | XCTAssertEqual(radar.version, "Xcode 9.0") 23 | XCTAssertEqual(radar.notes, "some notes") 24 | } 25 | 26 | func testOpenRadarMissingResult() { 27 | do { 28 | _ = try Radar(openRadar: [:]) 29 | XCTFail("Radar shouldn't be valid") 30 | } catch let error as OpenRadarParsingError { 31 | XCTAssertEqual(error, .noResult) 32 | } catch { 33 | XCTFail("Got invalid error") 34 | } 35 | } 36 | 37 | func testOpenRadarMissingTitle() { 38 | do { 39 | _ = try Radar(openRadar: ["result": [:]]) 40 | XCTFail("Radar shouldn't be valid") 41 | } catch let error as OpenRadarParsingError { 42 | XCTAssertEqual(error, .missingRequiredFields) 43 | } catch { 44 | XCTFail("Got invalid error") 45 | } 46 | } 47 | 48 | func testObservedAndActualAppend() { 49 | let string = "Observed results:\r\nfoo\r\nActual Results:\r\nbar" 50 | guard let openRadar = try? string.openRadarFromSummary() else { 51 | return XCTFail("Failed to parse valid description") 52 | } 53 | 54 | XCTAssertEqual(openRadar.actual, "foo\nbar") 55 | } 56 | 57 | func testParsingOpenRadar() { 58 | guard let openRadar = try? loadOpenRadarString(.regular).openRadarFromSummary() else { 59 | return XCTFail("Failed to parse valid description") 60 | } 61 | 62 | XCTAssertEqual(openRadar.description, "foo\n\nbar\nbaz") 63 | XCTAssertEqual(openRadar.steps, "1. foo\n2. bar") 64 | XCTAssertEqual(openRadar.expected, "foo") 65 | XCTAssertEqual(openRadar.actual, "bar") 66 | XCTAssertEqual(openRadar.version, "Xcode 9.0") 67 | XCTAssertEqual(openRadar.notes, "some notes") 68 | XCTAssertEqual(openRadar.configuration, "some config") 69 | } 70 | 71 | func testParsingArea() { 72 | guard let openRadar = try? "Area:\r\nfoo".openRadarFromSummary() else { 73 | return XCTFail("Failed to parse valid description") 74 | } 75 | 76 | XCTAssertEqual(openRadar.areaString, "foo") 77 | } 78 | 79 | func testParsingLongerOpenRadar() { 80 | guard let openRadar = try? loadOpenRadarString(.long).openRadarFromSummary() else { 81 | return XCTFail("Failed to parse valid radar") 82 | } 83 | 84 | XCTAssertEqual(openRadar.description, "foo\n\nbar\nbaz\n\nqux\n\nfoo\nbar\n\nbaz\n\nqux") 85 | XCTAssertEqual(openRadar.steps, "foo") 86 | XCTAssertEqual(openRadar.expected, "bar") 87 | XCTAssertEqual(openRadar.actual, "baz") 88 | XCTAssertEqual(openRadar.version, "foo") 89 | XCTAssertNil(openRadar.notes) 90 | } 91 | 92 | func testParsingPartiallyValidRadar() { 93 | do { 94 | _ = try "foo.\r\n\r\nSummary:\r\nbar\r\nbaz".openRadarFromSummary() 95 | XCTFail("Open radar should be invalid") 96 | } catch let error as OpenRadarParsingError { 97 | XCTAssertEqual(error, .invalidFormat) 98 | } catch let error { 99 | XCTFail("Invalid error thrown: \(error)") 100 | } 101 | } 102 | 103 | func testParsingUnformattedOpenRadar() { 104 | do { 105 | _ = try "foo.\r\n\r\nbar\r\nbaz".openRadarFromSummary() 106 | XCTFail("Open radar should be invalid") 107 | } catch let error as OpenRadarParsingError { 108 | XCTAssertEqual(error, .invalidFormat) 109 | } catch let error { 110 | XCTFail("Invalid error thrown: \(error)") 111 | } 112 | } 113 | 114 | } 115 | 116 | private enum StringID: String { 117 | case regular 118 | case long 119 | } 120 | 121 | private func loadOpenRadarString(_ stringID: StringID) -> String { 122 | let url = Bundle(for: OpenRadarTests.self).url(forResource: "openradarstrings", withExtension: "json")! 123 | return try! Data(contentsOf: url).toJSONDictionary()!.onlyStrings()[stringID.rawValue]! 124 | } 125 | 126 | private func loadOpenRadarJSON() -> [String: Any] { 127 | let url = Bundle(for: OpenRadarTests.self).url(forResource: "openradar", withExtension: "json")! 128 | return try! Data(contentsOf: url).toJSONDictionary()! 129 | } 130 | -------------------------------------------------------------------------------- /BriskTests/RadarIDParsingTests.swift: -------------------------------------------------------------------------------- 1 | import Brisk 2 | import XCTest 3 | 4 | final class RadarIDParsingTests: XCTestCase { 5 | func testParsingNormalRadarID() { 6 | XCTAssertEqual(radarID(from: "1234"), "1234") 7 | } 8 | 9 | func testParsingRadarString() { 10 | XCTAssertNil(radarID(from: "foobar")) 11 | } 12 | 13 | func testParsingRadarURL() { 14 | XCTAssertEqual(radarID(from: "rdar://1234"), "1234") 15 | } 16 | 17 | func testParsingProblemURL() { 18 | XCTAssertEqual(radarID(from: "rdar://problem/1234"), "1234") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /BriskTests/RadarSerializationTests.swift: -------------------------------------------------------------------------------- 1 | import Brisk 2 | import Sonar 3 | import XCTest 4 | 5 | final class RadarSerializationTests: XCTestCase { 6 | func testSerializingRadar() { 7 | let attachment = Attachment(filename: "foo.png", mimeType: "image/png", 8 | data: Data(base64Encoded: "")!) 9 | let area = Area.areas(for: Product.iOS).first! 10 | let radar = Radar( 11 | classification: .Security, product: .iOS, reproducibility: .Always, title: "title", 12 | description: "description", steps: "steps", expected: "expected", actual: "actual", 13 | configuration: "config", version: "version", notes: "notes", attachments: [attachment], 14 | area: area, applicationID: "456", userID: "123" 15 | ) 16 | 17 | let json = try! radar.toData().toJSONDictionary() as NSDictionary? 18 | let realJson = loadRadarJSON() as NSDictionary 19 | XCTAssertEqual(json, realJson) 20 | } 21 | 22 | func testCreatingInvalidRadar() { 23 | let json = [String: Any]() 24 | let radar = Radar(json: json) 25 | XCTAssertNil(radar) 26 | } 27 | 28 | func testDeserializingRadar() { 29 | let radar = Radar(json: loadRadarJSON())! 30 | let attachment = radar.attachments.first! 31 | 32 | XCTAssertEqual(radar.actual, "actual") 33 | XCTAssertEqual(radar.applicationID, "456") 34 | XCTAssertEqual(radar.area?.appleIdentifier, 1) 35 | XCTAssertEqual(radar.classification.appleIdentifier, Classification.Security.appleIdentifier) 36 | XCTAssertEqual(radar.configuration, "config") 37 | XCTAssertEqual(radar.description, "description") 38 | XCTAssertEqual(radar.expected, "expected") 39 | XCTAssertEqual(radar.notes, "notes") 40 | XCTAssertEqual(radar.product.appleIdentifier, Product.iOS.appleIdentifier) 41 | XCTAssertEqual(radar.reproducibility.appleIdentifier, Reproducibility.Always.appleIdentifier) 42 | XCTAssertEqual(radar.steps, "steps") 43 | XCTAssertEqual(radar.title, "title") 44 | XCTAssertEqual(radar.userID, "123") 45 | XCTAssertEqual(radar.version, "version") 46 | XCTAssertEqual(radar.version, "version") 47 | XCTAssertEqual(attachment.filename, "foo.png") 48 | XCTAssertEqual(attachment.mimeType, "image/png") 49 | XCTAssertEqual(attachment.data, Data(base64Encoded: "")!) 50 | } 51 | } 52 | 53 | private func loadRadarJSON() -> [String: Any] { 54 | let url = Bundle(for: RadarSerializationTests.self).url(forResource: "radar", withExtension: "json")! 55 | return try! Data(contentsOf: url).toJSONDictionary()! 56 | } 57 | -------------------------------------------------------------------------------- /BriskTests/Resources/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /BriskTests/Resources/openradar.json: -------------------------------------------------------------------------------- 1 | { 2 | "result": { 3 | "classification": "Other Bug", 4 | "description": "Summary:\r\nfoo\n\nbar\nbaz\n\r\n\r\nSteps to Reproduce:\r\n1. foo\n2. bar\n\r\n\r\nExpected Results:\r\nfoo\n\r\n\r\nActual Results:\r\nbar\n\r\n\r\nVersion:\r\nXcode 9.0\r\n\r\nNotes:\r\n\r\nsome notes\n", 5 | "id": 123456, 6 | "number": "1234", 7 | "originated": "19-Jun-2017 21:22", 8 | "product": "Developer Tools", 9 | "product_version": "Xcode 9.0", 10 | "reproducible": "Always", 11 | "resolved": "", 12 | "status": "Open", 13 | "title": "Some title", 14 | "user": "foo@bar.com" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BriskTests/Resources/openradarstrings.json: -------------------------------------------------------------------------------- 1 | { 2 | "regular": "Summary:\r\nfoo\n\nbar\nbaz\n\r\n\r\nSteps to Reproduce:\r\n1. foo\n2. bar\n\r\n\r\nExpected Results:\r\nfoo\n\r\n\r\nActual Results:\r\nbar\n\r\n\r\nVersion:\r\nXcode 9.0\r\n\r\nNotes:\r\n\r\nsome notes\r\nConfiguration:\r\nsome config", 3 | "long": "Summary:\r\nfoo\n\nbar\nbaz\n\nqux\n\nfoo\nbar\n\nbaz\n\nqux\r\n\r\nSteps to Reproduce:\r\nfoo\r\n\r\nExpected Results:\r\nbar\r\n\r\nActual Results:\r\nbaz\r\n\r\nVersion:\r\nfoo\r\n\r\nNotes:" 4 | } 5 | -------------------------------------------------------------------------------- /BriskTests/Resources/radar.json: -------------------------------------------------------------------------------- 1 | { 2 | "actual": "actual", 3 | "application_id": "456", 4 | "area_id": 1, 5 | "classification_id": 1, 6 | "configuration": "config", 7 | "description": "description", 8 | "expected": "expected", 9 | "notes": "notes", 10 | "product_id": 579020, 11 | "reproducibility_id": 1, 12 | "steps": "steps", 13 | "title": "title", 14 | "user_id": "123", 15 | "version": "version", 16 | "attachments": [ 17 | { 18 | "filename": "foo.png", 19 | "mimetype": "image/png", 20 | "data": "" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /BriskTests/ResultTests.swift: -------------------------------------------------------------------------------- 1 | import Brisk 2 | import XCTest 3 | 4 | private struct TestError: Error {} 5 | 6 | final class ResultTests: XCTestCase { 7 | func testCreatingSuccessfully() { 8 | let result = Result(value: "hi", failWith: TestError()) 9 | if case .success(let value) = result { 10 | XCTAssertEqual(value, "hi") 11 | } else { 12 | XCTFail("Result should have been successful") 13 | } 14 | } 15 | 16 | func testCreatingError() { 17 | let result = Result(value: nil, failWith: TestError()) 18 | switch result { 19 | case .failure: 20 | break 21 | default: 22 | XCTFail("Didn't create error") 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /BriskTests/StringExtensionTests.swift: -------------------------------------------------------------------------------- 1 | import Brisk 2 | import XCTest 3 | 4 | final class StringExtensionTests: XCTestCase { 5 | func testAppendOrReturnBothExist() { 6 | XCTAssertEqual(appendOrReturn("foo", "bar"), "foo\nbar") 7 | } 8 | 9 | func testAppendOrReturnBothNil() { 10 | XCTAssertEqual(appendOrReturn(nil, nil), nil) 11 | } 12 | 13 | func testAppendOrReturnFirstString() { 14 | XCTAssertEqual(appendOrReturn("foo", nil), "foo") 15 | } 16 | 17 | func testAppendOrReturnSecondString() { 18 | XCTAssertEqual(appendOrReturn(nil, "bar"), "bar") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /BriskUITests/BriskUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class BriskUITests: XCTestCase {} 4 | -------------------------------------------------------------------------------- /BriskUITests/Resources/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # master 2 | 3 | ## Enhancements 4 | 5 | - None. 6 | 7 | ## Bug Fixes 8 | 9 | - None. 10 | 11 | # 1.2.0 12 | 13 | ## Enhancements 14 | 15 | - Update project to Swift 4.1 and Xcode 9.3 16 | [change](https://github.com/br1sk/brisk/pull/135) 17 | 18 | - Update project to Swift 4.0 and Xcode 9.2 19 | [change](https://github.com/br1sk/brisk/pull/126) 20 | 21 | ## Bug Fixes 22 | 23 | - Fix preferences window default height 24 | [change](https://github.com/br1sk/brisk/pull/141) 25 | 26 | - Update Sonar 27 | [issue 1](https://github.com/br1sk/brisk/issues/136) 28 | [issue 2](https://github.com/br1sk/brisk/issues/137) 29 | [issue 3](https://github.com/br1sk/brisk/issues/137) 30 | [change 1](https://github.com/br1sk/brisk/pull/138) 31 | [change 2](https://github.com/br1sk/brisk/pull/140) 32 | 33 | - Remove whitespace from Apple ID login 34 | [issue](https://github.com/br1sk/brisk/issues/133) 35 | [change](https://github.com/br1sk/brisk/pull/134) 36 | 37 | # 1.1.2 38 | 39 | ## Enhancements 40 | 41 | - None. 42 | 43 | ## Bug Fixes 44 | 45 | - Fix layout freezing issues 46 | [issue](https://github.com/br1sk/brisk/issues/33) 47 | [change](https://github.com/br1sk/brisk/pull/120) 48 | 49 | # 1.1.1 50 | 51 | ## Enhancements 52 | 53 | - None. 54 | 55 | ## Bug Fixes 56 | 57 | - Fix ambiguous layout when text fields are large 58 | [issue](https://github.com/br1sk/brisk/issues/113) 59 | [change](https://github.com/br1sk/brisk/pull/117) 60 | 61 | - Fix ambiguous version field layout 62 | [change](https://github.com/br1sk/brisk/pull/118) 63 | 64 | # 1.1.0 65 | 66 | ## Enhancements 67 | 68 | - Add checkbox to control crossposting to open radar 69 | [issue](https://github.com/br1sk/brisk/issues/4) 70 | [change](https://github.com/br1sk/brisk/pull/107) 71 | 72 | - Add drag and drop attachment support 73 | [issue](https://github.com/br1sk/brisk/issues/35) 74 | [change](https://github.com/br1sk/brisk/pull/109) 75 | 76 | - Default cross posting to false for duplicates 77 | [issue](https://github.com/br1sk/brisk/issues/106) 78 | [change](https://github.com/br1sk/brisk/issues/110) 79 | 80 | ## Bug Fixes 81 | 82 | - Don't register dup'd radars as dirty files 83 | [change](https://github.com/br1sk/brisk/pull/103) 84 | 85 | - Update the title when restoring a radar 86 | [change](https://github.com/br1sk/brisk/pull/104) 87 | 88 | - Remove unnecessary change count +1 89 | [change](https://github.com/br1sk/brisk/pull/108) 90 | 91 | # 1.0.1 92 | 93 | ## Enhancements 94 | 95 | - Update invalid rdar:// URL error message 96 | [change](https://github.com/br1sk/brisk/pull/95) 97 | 98 | ## Bug Fixes 99 | 100 | - None. 101 | 102 | # 1.0.0 103 | 104 | ## Enhancements 105 | 106 | - Make tab in text views jump between fields 107 | [issue](https://github.com/br1sk/brisk/issues/52) 108 | [change](https://github.com/br1sk/brisk/pull/78) 109 | 110 | - Add UI for duping radars from OpenRadar 111 | [issue](https://github.com/br1sk/brisk/issues/14) 112 | [change](https://github.com/br1sk/brisk/pull/75) 113 | 114 | - Respond to rdar:// URLs 115 | [issue](https://github.com/br1sk/brisk/issues/77) 116 | [change](https://github.com/br1sk/brisk/pull/79) 117 | 118 | - Set filetype icon to app icon 119 | [issue](https://github.com/br1sk/brisk/issues/47) 120 | [change](https://github.com/br1sk/brisk/pull/83) 121 | 122 | - Use User icon for Open Radar preferences 123 | [issue](https://github.com/br1sk/brisk/issues/15) 124 | [change](https://github.com/br1sk/brisk/pull/84) 125 | 126 | - Make Configuration and Notes optional 127 | [issue](https://github.com/br1sk/brisk/issues/46) 128 | [change](https://github.com/br1sk/brisk/pull/86) 129 | 130 | - Mark duplicate radars as dirty documents 131 | [change](https://github.com/br1sk/brisk/pull/88) 132 | 133 | - Add ability to remove attachment 134 | [issue](https://github.com/br1sk/brisk/issues/16) 135 | [change](https://github.com/br1sk/brisk/pull/89) 136 | 137 | - Combine `PlaceholderTextView` and `TextView` 138 | [change](https://github.com/br1sk/brisk/pull/90) 139 | 140 | - Make text view font size the normal system size 141 | [change](https://github.com/br1sk/brisk/pull/92) 142 | 143 | - Add hint text for text views 144 | [issue](https://github.com/br1sk/brisk/issues/54) 145 | [change](https://github.com/br1sk/brisk/issues/93) 146 | 147 | ## Bug Fixes 148 | 149 | - Typing emoji caused font to change 150 | [issue](https://github.com/br1sk/brisk/issues/55) 151 | [change](https://github.com/br1sk/brisk/pull/67) 152 | 153 | - Middle truncate long attachment names 154 | [change](https://github.com/br1sk/brisk/pull/69) 155 | 156 | - Remove ruler support from text views 157 | [issue](https://github.com/br1sk/brisk/issues/27) 158 | [change](https://github.com/br1sk/brisk/pull/70) 159 | 160 | - Remove unnecessary fields from `Info.plist` 161 | [change](https://github.com/br1sk/brisk/pull/72) 162 | 163 | - Remove App Transport Security exceptions 164 | [change](https://github.com/br1sk/brisk/pull/73) 165 | 166 | - Use Radar icon in Preferences 167 | [change](https://github.com/br1sk/brisk/pull/74) 168 | 169 | - Focus existing windows when reopening documents 170 | [issue](https://github.com/br1sk/brisk/issues/48) 171 | [change](https://github.com/br1sk/brisk/issues/80) 172 | 173 | - Only update window title on title text change 174 | [change](https://github.com/br1sk/brisk/pull/87) 175 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "cocoapods", "1.2.1" 4 | gem "psych" 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (2.3.5) 5 | activesupport (4.2.8) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | claide (1.0.1) 11 | cocoapods (1.2.1) 12 | activesupport (>= 4.0.2, < 5) 13 | claide (>= 1.0.1, < 2.0) 14 | cocoapods-core (= 1.2.1) 15 | cocoapods-deintegrate (>= 1.0.1, < 2.0) 16 | cocoapods-downloader (>= 1.1.3, < 2.0) 17 | cocoapods-plugins (>= 1.0.0, < 2.0) 18 | cocoapods-search (>= 1.0.0, < 2.0) 19 | cocoapods-stats (>= 1.0.0, < 2.0) 20 | cocoapods-trunk (>= 1.2.0, < 2.0) 21 | cocoapods-try (>= 1.1.0, < 2.0) 22 | colored2 (~> 3.1) 23 | escape (~> 0.0.4) 24 | fourflusher (~> 2.0.1) 25 | gh_inspector (~> 1.0) 26 | molinillo (~> 0.5.7) 27 | nap (~> 1.0) 28 | ruby-macho (~> 1.1) 29 | xcodeproj (>= 1.4.4, < 2.0) 30 | cocoapods-core (1.2.1) 31 | activesupport (>= 4.0.2, < 5) 32 | fuzzy_match (~> 2.0.4) 33 | nap (~> 1.0) 34 | cocoapods-deintegrate (1.0.1) 35 | cocoapods-downloader (1.1.3) 36 | cocoapods-plugins (1.0.0) 37 | nap 38 | cocoapods-search (1.0.0) 39 | cocoapods-stats (1.0.0) 40 | cocoapods-trunk (1.2.0) 41 | nap (>= 0.8, < 2.0) 42 | netrc (= 0.7.8) 43 | cocoapods-try (1.1.0) 44 | colored2 (3.1.2) 45 | escape (0.0.4) 46 | fourflusher (2.0.1) 47 | fuzzy_match (2.0.4) 48 | gh_inspector (1.0.3) 49 | i18n (0.8.1) 50 | minitest (5.10.1) 51 | molinillo (0.5.7) 52 | nanaimo (0.2.3) 53 | nap (1.1.0) 54 | netrc (0.7.8) 55 | psych (2.2.4) 56 | ruby-macho (1.1.0) 57 | thread_safe (0.3.6) 58 | tzinfo (1.2.10) 59 | thread_safe (~> 0.1) 60 | xcodeproj (1.4.4) 61 | CFPropertyList (~> 2.3.3) 62 | claide (>= 1.0.1, < 2.0) 63 | colored2 (~> 3.1) 64 | nanaimo (~> 0.2.3) 65 | 66 | PLATFORMS 67 | ruby 68 | 69 | DEPENDENCIES 70 | cocoapods (= 1.2.1) 71 | psych 72 | 73 | BUNDLED WITH 74 | 1.14.6 75 | -------------------------------------------------------------------------------- /Graphics/AppIcon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/Graphics/AppIcon.psd -------------------------------------------------------------------------------- /Graphics/StatusItemIcon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br1sk/brisk/145048228bc418c0864b5b81051cc80e0c02403d/Graphics/StatusItemIcon.psd -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Keith Smiley (http://keith.so) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the 'Software'), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | CONFIGURATION ?= Debug 3 | XCODEBUILD = xcodebuild \ 4 | -workspace Brisk.xcworkspace \ 5 | -scheme Brisk \ 6 | -configuration $(CONFIGURATION) \ 7 | DSTROOT=/ 8 | 9 | .DEFAULT_GOAL := build 10 | 11 | # Developer commands 12 | 13 | .PHONY: dependencies 14 | dependencies: ensure_pods 15 | 16 | .PHONY: build 17 | build: dependencies 18 | $(XCODEBUILD) build 19 | 20 | .PHONY: install 21 | install: CONFIGURATION = Release 22 | install: dependencies 23 | $(XCODEBUILD) clean install 24 | 25 | .PHONY: test 26 | test: ensure_pods 27 | $(XCODEBUILD) test 28 | 29 | .PHONY: lint 30 | lint: ensure_swiftlint 31 | ./swiftlint/swiftlint lint --strict 2> /dev/null 32 | 33 | # Internal commands 34 | 35 | .PHONY: ensure_pods 36 | ensure_pods: 37 | diff Podfile.lock Pods/Manifest.lock > /dev/null || $(MAKE) install_pods 38 | 39 | .PHONY: install_pods 40 | install_pods: 41 | bundle check || bundle install 42 | bundle exec pod install || bundle exec pod install --repo-update 43 | 44 | .PHONY: ensure_swiftlint 45 | ensure_swiftlint: 46 | diff <(./swiftlint/swiftlint version) <(cat .swiftlint-version) > /dev/null || $(MAKE) install_swiftlint 47 | 48 | .PHONY: install_swiftlint 49 | install_swiftlint: 50 | rm -rf swiftlint 51 | mkdir swiftlint 52 | curl --location --fail \ 53 | https://github.com/realm/SwiftLint/releases/download/"$(shell cat .swiftlint-version)"/portable_swiftlint.zip \ 54 | --output swiftlint/swiftlint.zip 55 | unzip -d swiftlint swiftlint/swiftlint.zip > /dev/null 56 | 57 | .PHONY: ci 58 | ci: 59 | ifndef ACTION 60 | $(error ACTION is not defined) 61 | endif 62 | make $(ACTION) 63 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, "10.11" 2 | 3 | use_frameworks! 4 | 5 | target :Brisk do 6 | pod "Sonar", :git => "https://github.com/br1sk/Sonar.git" 7 | pod "Sparkle" 8 | end 9 | 10 | target :BriskTests do 11 | pod "Sonar", :git => "https://github.com/br1sk/Sonar.git" 12 | end 13 | 14 | post_install do |installer| 15 | installer.pods_project.root_object.attributes["LastUpgradeCheck"] = "9999" 16 | 17 | installer.pods_project.targets.each do |target| 18 | target.build_configurations.each do |config| 19 | config.build_settings["SWIFT_VERSION"] = "4.0" 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (4.7.2) 3 | - Sonar (0.0.1): 4 | - Alamofire (~> 4.7.2) 5 | - Sparkle (1.19.0) 6 | 7 | DEPENDENCIES: 8 | - Sonar (from `https://github.com/br1sk/Sonar.git`) 9 | - Sparkle 10 | 11 | EXTERNAL SOURCES: 12 | Sonar: 13 | :git: https://github.com/br1sk/Sonar.git 14 | 15 | CHECKOUT OPTIONS: 16 | Sonar: 17 | :commit: 2fc402fbfc2a72c064918d0afc4a80f16d911b5e 18 | :git: https://github.com/br1sk/Sonar.git 19 | 20 | SPEC CHECKSUMS: 21 | Alamofire: e4fa87002c137ba2d8d634d2c51fabcda0d5c223 22 | Sonar: 96b87f9048b557dfbcd331c5d3598b8f92724d0d 23 | Sparkle: 8486d2493599665e466b5076e72b28849776e32f 24 | 25 | PODFILE CHECKSUM: 31638d14e4cbdaed3252aafe998baf86f9c87574 26 | 27 | COCOAPODS: 1.2.1 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brisk 2 | 3 | App Icon 4 | 5 | Brisk is a macOS app for filing [Radars](http://radar.apple.com) and 6 | optionally crossposting them to [Open Radar](http://www.openradar.me/). 7 | 8 | Brisk is written in Swift and uses 9 | [Sonar](https://github.com/br1sk/Sonar) to communicate with Apple's 10 | Radar web "APIs". 11 | 12 | Brisk supports two factor auth, attachments, and saving radars as 13 | drafts. 14 | 15 | ![screenshot](.github/screenshot.png) 16 | 17 | ## Installation 18 | 19 | There are a few different ways you can install Brisk: 20 | 21 | 1. Download the latest packaged `Brisk.app` from [the 22 | releases](https://github.com/br1sk/brisk/releases) and copy it to 23 | your Applications directory 24 | 1. Using [brew-cask](https://github.com/caskroom/homebrew-cask) with 25 | `brew install --cask brisk` 26 | 1. Clone the repo and run `make install` 27 | 28 | ## Development 29 | 30 | Build Brisk locally: 31 | 32 | ```sh 33 | $ make build 34 | ``` 35 | 36 | To just install the dependencies, and then build in Xcode: 37 | 38 | ```sh 39 | $ make dependencies 40 | $ open -a Xcode . 41 | ``` 42 | 43 | To build and install Brisk to your `/Applications` directory: 44 | 45 | ```sh 46 | $ make install 47 | ``` 48 | 49 | ## Credits 50 | 51 | Anteater icon by [Samantha Broccoli](http://www.samanthabroccoli.com) 52 | from [Raizlabs](https://www.raizlabs.com). 53 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | To release Brisk there are a few steps you need to take: 2 | 3 | 1. Update the Version and Build in the `Info.plist` to the version from 4 | the new release 5 | 1. Change the `master` section in the `CHANGELOG.md` to match the new 6 | release version. Don't add a new section for master changes yet 7 | 1. Commit both those changes on master with the message `Bump version 8 | $VERSION` 9 | 1. Tag the new commit with the version number of the release. Push the 10 | commit and the tag 11 | 1. Archive Brisk in Xcode 12 | 1. Create a `tar.gz` from the archived `Brisk.app` with `tar -pvczf 13 | Brisk.app.tar.gz Brisk.app` 14 | 1. Copy the output of `shasum -a 256 Brisk.app Brisk.app.tar.gz` for 15 | later 16 | 1. Draft a release on GitHub with the newly pushed tag 17 | 1. Title the release based on whatever feature / bugfix is "most 18 | important" 19 | 1. Fill the description with the contents of the changelog (you might 20 | need to join lines in the markdown to get it to render on a single 21 | line) 22 | 1. At the bottom of the description include the output of `shasum` from 23 | the previous step in triple backticks 24 | 1. Upload `Brisk.app.tar.gz` to the release 25 | 1. Save / create the release 26 | 1. In the `appcast.xml`, duplicate the top item, and paste it above 27 | 1. Change the title, enclosure url, `sparkle:version`, 28 | `releaseNotesLink` url to the new version 29 | 1. Get the size of the new `Brisk.app.tar.gz` with 30 | `stat --printf="%s" Brisk.app.tar.gz`. Replace `length` with that 31 | 1. Sign the release with `path/to/sparkle/sign_update Brisk.app.tar.gz 32 | path/to/dsa_priv.pem`. Replace the `sparkle:dsaSignature` with that 33 | 1. Update the `pubDate` with the output of 34 | `date +"%a, %d %b %G %H:%M:%S %z"` 35 | 1. Commit the appcast changes with the message `Update appcast for 36 | $VERSION`. Push the commit (note GitHub takes some time to propagate 37 | these changes) 38 | 1. Add a new `master` section in the `CHANGELOG.md` for future changes 39 | and commit it 40 | 1. Submit a PR to update the Brisk formula in homebrew-cask 41 | 1. Celebrate! 42 | -------------------------------------------------------------------------------- /appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Brisk Changelog 5 | https://raw.githubusercontent.com/br1sk/brisk/ks/sparkle/appcast.xml 6 | Most recent changes with links to updates. 7 | en 8 | 9 | Version 1.2.0 10 | 10.11 11 | https://raw.githubusercontent.com/br1sk/brisk/1.2.0/CHANGELOG.md 12 | Sat, 07 Jul 2018 16:50:32 -0700 13 | 14 | 15 | 16 | Version 1.1.2 17 | 10.11 18 | https://raw.githubusercontent.com/br1sk/brisk/1.1.2/CHANGELOG.md 19 | Mon, 28 Aug 2017 09:39:54 -0700 20 | 21 | 22 | 23 | Version 1.1.1 24 | 10.11 25 | https://raw.githubusercontent.com/br1sk/brisk/1.1.1/CHANGELOG.md 26 | Sun, 20 Aug 2017 15:26:28 -0700 27 | 28 | 29 | 30 | Version 1.1.0 31 | 10.11 32 | https://raw.githubusercontent.com/br1sk/brisk/master/CHANGELOG.md 33 | Sun, 30 Jul 2017 15:18:53 -0700 34 | 35 | 36 | 37 | Version 1.0.1 38 | 10.11 39 | https://raw.githubusercontent.com/br1sk/brisk/master/CHANGELOG.md 40 | Sun, 25 Jun 2017 13:49:59 -0700 41 | 42 | 43 | 44 | Version 1.0.0 45 | 10.11 46 | https://raw.githubusercontent.com/br1sk/brisk/master/CHANGELOG.md 47 | Sun, 25 Jun 2017 13:37:16 -0700 48 | 49 | 50 | 51 | 52 | --------------------------------------------------------------------------------