diff --git a/.gitignore b/.gitignore index d534044..a4b4a49 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output +.DS_Store diff --git a/README.md b/README.md index e84330d..e14105c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,13 @@ # macGist -Simple app to send pastboard items to GitHub's Gist +macGist is a simple app that sends your clipboard to GitHub's Gist and automatically copy the Gist URL into your Clipboard.. + +Be aware that it will replace your current clipboard with the Gist URL! + + +# Installation +You can download an unsigned version of the app [here](https://github.com/Bunn/macGist/releases/latest) + + +# Screenshots +![screenshot](./other/gist.gif) +![screenshot](./other/ss.png) diff --git a/macGist.xcodeproj/project.pbxproj b/macGist.xcodeproj/project.pbxproj new file mode 100644 index 0000000..c8042a6 --- /dev/null +++ b/macGist.xcodeproj/project.pbxproj @@ -0,0 +1,345 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 318087791EF5D9CA001712F4 /* GitHubAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318087761EF5D9CA001712F4 /* GitHubAPI.swift */; }; + 3180877A1EF5D9CA001712F4 /* GitHubCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318087771EF5D9CA001712F4 /* GitHubCredential.swift */; }; + 3180877B1EF5D9CA001712F4 /* GitHubRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318087781EF5D9CA001712F4 /* GitHubRouter.swift */; }; + 3180877D1EF5DA9F001712F4 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3180877C1EF5DA9F001712F4 /* Keychain.swift */; }; + 319D3B961ED501D4000D4245 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319D3B951ED501D4000D4245 /* AppDelegate.swift */; }; + 319D3B9A1ED501D4000D4245 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 319D3B991ED501D4000D4245 /* Assets.xcassets */; }; + 319D3B9D1ED501D4000D4245 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 319D3B9B1ED501D4000D4245 /* Main.storyboard */; }; + 319D3BA51ED50246000D4245 /* Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319D3BA41ED50246000D4245 /* Menu.swift */; }; + 31C2536B1EF5F17100D4FE6D /* PasteboardHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C2536A1EF5F17100D4FE6D /* PasteboardHelper.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 318087761EF5D9CA001712F4 /* GitHubAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHubAPI.swift; sourceTree = ""; }; + 318087771EF5D9CA001712F4 /* GitHubCredential.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHubCredential.swift; sourceTree = ""; }; + 318087781EF5D9CA001712F4 /* GitHubRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHubRouter.swift; sourceTree = ""; }; + 3180877C1EF5DA9F001712F4 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; + 319D3B921ED501D4000D4245 /* macGist.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = macGist.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 319D3B951ED501D4000D4245 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 319D3B991ED501D4000D4245 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 319D3B9C1ED501D4000D4245 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 319D3B9E1ED501D4000D4245 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 319D3BA41ED50246000D4245 /* Menu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Menu.swift; sourceTree = ""; }; + 31C2536A1EF5F17100D4FE6D /* PasteboardHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasteboardHelper.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 319D3B8F1ED501D4000D4245 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 318087751EF5D9C2001712F4 /* GitHub */ = { + isa = PBXGroup; + children = ( + 318087761EF5D9CA001712F4 /* GitHubAPI.swift */, + 318087771EF5D9CA001712F4 /* GitHubCredential.swift */, + 318087781EF5D9CA001712F4 /* GitHubRouter.swift */, + ); + name = GitHub; + sourceTree = ""; + }; + 319D3B891ED501D4000D4245 = { + isa = PBXGroup; + children = ( + 319D3B941ED501D4000D4245 /* macGist */, + 319D3B931ED501D4000D4245 /* Products */, + ); + sourceTree = ""; + }; + 319D3B931ED501D4000D4245 /* Products */ = { + isa = PBXGroup; + children = ( + 319D3B921ED501D4000D4245 /* macGist.app */, + ); + name = Products; + sourceTree = ""; + }; + 319D3B941ED501D4000D4245 /* macGist */ = { + isa = PBXGroup; + children = ( + 319D3B951ED501D4000D4245 /* AppDelegate.swift */, + 319D3BA41ED50246000D4245 /* Menu.swift */, + 31C2536C1EF5FD2A00D4FE6D /* Util */, + 31C2536D1EF5FD4000D4FE6D /* SupportFiles */, + 318087751EF5D9C2001712F4 /* GitHub */, + ); + path = macGist; + sourceTree = ""; + }; + 31C2536C1EF5FD2A00D4FE6D /* Util */ = { + isa = PBXGroup; + children = ( + 3180877C1EF5DA9F001712F4 /* Keychain.swift */, + 31C2536A1EF5F17100D4FE6D /* PasteboardHelper.swift */, + ); + name = Util; + sourceTree = ""; + }; + 31C2536D1EF5FD4000D4FE6D /* SupportFiles */ = { + isa = PBXGroup; + children = ( + 319D3B991ED501D4000D4245 /* Assets.xcassets */, + 319D3B9B1ED501D4000D4245 /* Main.storyboard */, + 319D3B9E1ED501D4000D4245 /* Info.plist */, + ); + name = SupportFiles; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 319D3B911ED501D4000D4245 /* macGist */ = { + isa = PBXNativeTarget; + buildConfigurationList = 319D3BA11ED501D4000D4245 /* Build configuration list for PBXNativeTarget "macGist" */; + buildPhases = ( + 319D3B8E1ED501D4000D4245 /* Sources */, + 319D3B8F1ED501D4000D4245 /* Frameworks */, + 319D3B901ED501D4000D4245 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = macGist; + productName = macGist; + productReference = 319D3B921ED501D4000D4245 /* macGist.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 319D3B8A1ED501D4000D4245 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0830; + LastUpgradeCheck = 0830; + ORGANIZATIONNAME = "Fernando Bunn"; + TargetAttributes = { + 319D3B911ED501D4000D4245 = { + CreatedOnToolsVersion = 8.3.2; + DevelopmentTeam = DNXBQ46VRB; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 319D3B8D1ED501D4000D4245 /* Build configuration list for PBXProject "macGist" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 319D3B891ED501D4000D4245; + productRefGroup = 319D3B931ED501D4000D4245 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 319D3B911ED501D4000D4245 /* macGist */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 319D3B901ED501D4000D4245 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 319D3B9A1ED501D4000D4245 /* Assets.xcassets in Resources */, + 319D3B9D1ED501D4000D4245 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 319D3B8E1ED501D4000D4245 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 319D3BA51ED50246000D4245 /* Menu.swift in Sources */, + 318087791EF5D9CA001712F4 /* GitHubAPI.swift in Sources */, + 319D3B961ED501D4000D4245 /* AppDelegate.swift in Sources */, + 3180877B1EF5D9CA001712F4 /* GitHubRouter.swift in Sources */, + 31C2536B1EF5F17100D4FE6D /* PasteboardHelper.swift in Sources */, + 3180877A1EF5D9CA001712F4 /* GitHubCredential.swift in Sources */, + 3180877D1EF5DA9F001712F4 /* Keychain.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 319D3B9B1ED501D4000D4245 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 319D3B9C1ED501D4000D4245 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 319D3B9F1ED501D4000D4245 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 319D3BA01ED501D4000D4245 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + }; + name = Release; + }; + 319D3BA21ED501D4000D4245 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = DNXBQ46VRB; + INFOPLIST_FILE = macGist/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.idevzilla.macGist; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 319D3BA31ED501D4000D4245 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = DNXBQ46VRB; + INFOPLIST_FILE = macGist/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.idevzilla.macGist; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 319D3B8D1ED501D4000D4245 /* Build configuration list for PBXProject "macGist" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 319D3B9F1ED501D4000D4245 /* Debug */, + 319D3BA01ED501D4000D4245 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 319D3BA11ED501D4000D4245 /* Build configuration list for PBXNativeTarget "macGist" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 319D3BA21ED501D4000D4245 /* Debug */, + 319D3BA31ED501D4000D4245 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 319D3B8A1ED501D4000D4245 /* Project object */; +} diff --git a/macGist.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/macGist.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..7056615 --- /dev/null +++ b/macGist.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macGist/AppDelegate.swift b/macGist/AppDelegate.swift new file mode 100644 index 0000000..69dc1a4 --- /dev/null +++ b/macGist/AppDelegate.swift @@ -0,0 +1,24 @@ +// +// AppDelegate.swift +// macGist +// +// Created by Fernando Bunn on 23/05/17. +// Copyright © 2017 Fernando Bunn. All rights reserved. +// + +import Cocoa + +@NSApplicationMain + +class AppDelegate: NSObject, NSApplicationDelegate { + + let menu = Menu() + + func applicationDidFinishLaunching(_ aNotification: Notification) { + menu.setupMenu() + } + + func applicationWillTerminate(_ aNotification: Notification) { + } +} + diff --git a/macGist/Assets.xcassets/AppIcon.appiconset/Contents.json b/macGist/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2db2b1c --- /dev/null +++ b/macGist/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/macGist/Assets.xcassets/Contents.json b/macGist/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/macGist/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/macGist/Assets.xcassets/check_white_icon.imageset/Contents.json b/macGist/Assets.xcassets/check_white_icon.imageset/Contents.json new file mode 100644 index 0000000..75eb3d9 --- /dev/null +++ b/macGist/Assets.xcassets/check_white_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "check_white_icon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "check_white_icon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/macGist/Assets.xcassets/check_white_icon.imageset/check_white_icon.png b/macGist/Assets.xcassets/check_white_icon.imageset/check_white_icon.png new file mode 100644 index 0000000..4107516 Binary files /dev/null and b/macGist/Assets.xcassets/check_white_icon.imageset/check_white_icon.png differ diff --git a/macGist/Assets.xcassets/check_white_icon.imageset/check_white_icon@2x.png b/macGist/Assets.xcassets/check_white_icon.imageset/check_white_icon@2x.png new file mode 100644 index 0000000..2bd583e Binary files /dev/null and b/macGist/Assets.xcassets/check_white_icon.imageset/check_white_icon@2x.png differ diff --git a/macGist/Assets.xcassets/icon.imageset/Contents.json b/macGist/Assets.xcassets/icon.imageset/Contents.json new file mode 100644 index 0000000..495aad5 --- /dev/null +++ b/macGist/Assets.xcassets/icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/macGist/Assets.xcassets/icon.imageset/icon.png b/macGist/Assets.xcassets/icon.imageset/icon.png new file mode 100644 index 0000000..c0bd85e Binary files /dev/null and b/macGist/Assets.xcassets/icon.imageset/icon.png differ diff --git a/macGist/Assets.xcassets/icon.imageset/icon@2x.png b/macGist/Assets.xcassets/icon.imageset/icon@2x.png new file mode 100644 index 0000000..e121d0f Binary files /dev/null and b/macGist/Assets.xcassets/icon.imageset/icon@2x.png differ diff --git a/macGist/Base.lproj/Main.storyboard b/macGist/Base.lproj/Main.storyboard new file mode 100644 index 0000000..ceb5dde --- /dev/null +++ b/macGist/Base.lproj/Main.storyboard @@ -0,0 +1,663 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macGist/GitHubAPI.swift b/macGist/GitHubAPI.swift new file mode 100644 index 0000000..1ebfa67 --- /dev/null +++ b/macGist/GitHubAPI.swift @@ -0,0 +1,160 @@ +// +// GitHubAPI.swift +// Xgist +// +// Created by Fernando Bunn on 27/03/17. +// Copyright © 2017 Fernando Bunn. All rights reserved. +// + +import Foundation + +struct GitHubAPI { + let twoFactorHeader = "X-GitHub-OTP" + + enum GitHubAPIError: Error { + case badHHTPStatus + case invalidRequest + case invalidJSON + case tokenNotFound + case twoFactorRequired + } + + + //MARK: - Variables + var isAuthenticated: Bool { + if let _ = token { + return true + } else { + return false + } + } + + var token: String? { + do { + let password = try Keychain().readPassword() + return password + } catch { + return nil + } + } + + + //MARK: - Public Functions + + func logout() { + do { + try Keychain().deleteItem() + } catch { + fatalError("Error deleting keychain item - \(error)") + } + } + + func post(gist: String, fileExtension: String, authenticated: Bool, completion: @escaping (Error?, String?) -> Void) { + var file = [String : Any]() + file["content"] = gist + + var files = [String : Any]() + files["macGist.\(fileExtension)"] = file + + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .medium + let dateString = formatter.string(from: Date()) + + var jsonDictionary = [String : Any]() + jsonDictionary["description"] = "Generated by macGist (https://github.com/Bunn/macGist) at \(dateString)" + jsonDictionary["public"] = false + jsonDictionary["files"] = files + + guard let request = GitHubRouter.gist(jsonDictionary, authenticated).request else { + completion(GitHubAPIError.invalidRequest, nil) + return + } + + //Setup Session + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + guard let data = data, error == nil else { + completion(error, nil) + return + } + + if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 201 { + completion(GitHubAPIError.badHHTPStatus, nil) + return + } + + if let json = try? JSONSerialization.jsonObject(with: data, options: []) as! [String : Any] { + if let htmlURL = json["html_url"] as? String { + completion(nil, htmlURL) + } + } + } + + task.resume() + } + + func authenticate (username: String, password: String, twoFactorCode: String? = nil, completion: @escaping (Error?) -> Void) { + let scopes = ["gist"] + let params = ["client_secret" : GitHubCredential.clientSecret.rawValue, + "scopes" : scopes, + "note" : "testNote"] as [String : Any] + + guard var request = GitHubRouter.auth(params).request else { + completion(GitHubAPIError.invalidRequest) + return + + } + if let code = twoFactorCode { + request.setValue(code, forHTTPHeaderField: twoFactorHeader) + } + let loginData = base64Login(username: username, password: password) + request.setValue("Basic \(loginData)", forHTTPHeaderField: "Authorization") + + //Setup Session + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + guard let responseData = data, error == nil, + let jsonObject = try? JSONSerialization.jsonObject(with: responseData, options: []) else { + completion(error) + return + } + + guard let json = jsonObject as? [String : Any] else { + completion(GitHubAPIError.invalidJSON) + return + } + + guard let httpStatus = response as? HTTPURLResponse else { + completion(GitHubAPIError.invalidRequest) + return + } + + guard httpStatus.allHeaderFields[self.twoFactorHeader] == nil else { + completion(GitHubAPIError.twoFactorRequired) + return + } + + guard let token = json["token"] as? String else { + completion(GitHubAPIError.tokenNotFound) + return + } + + do { + try Keychain().savePassword(token) + completion(nil) + } catch { + completion(error) + } + } + task.resume() + } + + + //MARK: - Private Functions + + fileprivate func base64Login(username: String, password: String) -> String { + let loginString = "\(username):\(password)" + let loginData = loginString.data(using: String.Encoding.utf8)! + let base64LoginString = loginData.base64EncodedString() + return base64LoginString + } +} diff --git a/macGist/GitHubCredential.swift b/macGist/GitHubCredential.swift new file mode 100644 index 0000000..120a7ec --- /dev/null +++ b/macGist/GitHubCredential.swift @@ -0,0 +1,14 @@ +// +// GitHubCredential.swift +// Xgist +// +// Created by Fernando Bunn on 28/03/17. +// Copyright © 2017 Fernando Bunn. All rights reserved. +// + +import Foundation + +enum GitHubCredential: String { + case clientSecret = "XXX" + case clientID = "XXX" +} diff --git a/macGist/GitHubRouter.swift b/macGist/GitHubRouter.swift new file mode 100644 index 0000000..a69de0e --- /dev/null +++ b/macGist/GitHubRouter.swift @@ -0,0 +1,61 @@ +// +// GitHubRouter.swift +// Xgist +// +// Created by Fernando Bunn on 27/03/17. +// Copyright © 2017 Fernando Bunn. All rights reserved. +// + +import Foundation + +enum GitHubRouter { + case auth([String : Any]) + case gist([String : Any], Bool) + + var basePath: String { + return "https://api.github.com" + } + + var path: String { + switch self { + case .auth: + return "authorizations/clients/\(GitHubCredential.clientID.rawValue)/\(UUID.init())" + case .gist( _ , let authenticated): + if authenticated { + guard let token = GitHubAPI().token else { fatalError("No Token") } + return "gists?access_token=\(token)" + } else { + return "gists" + } + } + } + + var method: String { + switch self { + case .auth: + return "PUT" + case .gist: + return "POST" + } + } + + var httpBody: Data? { + switch self { + case .auth(let params), .gist(let params, _): + let jsonData = try? JSONSerialization.data(withJSONObject: params) + return jsonData + } + } + + var request: URLRequest? { + guard let url = URL(string: "\(basePath)/\(path)") else { + print("Invalid URL") + return nil + } + + var request = URLRequest(url: url) + request.httpMethod = method + request.httpBody = httpBody + return request + } +} diff --git a/macGist/Info.plist b/macGist/Info.plist new file mode 100644 index 0000000..04bc7a8 --- /dev/null +++ b/macGist/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2017 Fernando Bunn. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + LSUIElement + + + diff --git a/macGist/Keychain.swift b/macGist/Keychain.swift new file mode 100644 index 0000000..a096683 --- /dev/null +++ b/macGist/Keychain.swift @@ -0,0 +1,111 @@ + +//https://developer.apple.com/library/content/samplecode/GenericKeychain/Introduction/Intro.html + +import Foundation + +struct KeychainConfiguration { + static let serviceName = "macGist" + static let accessGroup = "com.idevzilla.macGist" + static let account = "macGistAccount" +} + +struct Keychain { + + enum KeychainError: Error { + case noPassword + case unexpectedPasswordData + case unexpectedItemData + case unhandledError(status: OSStatus) + } + + func savePassword(_ password: String) throws { + // Encode the password into an Data object. + let encodedPassword = password.data(using: String.Encoding.utf8)! + + do { + // Check for an existing item in the keychain. + try _ = readPassword() + + // Update the existing item with the new password. + var attributesToUpdate = [String : AnyObject]() + attributesToUpdate[kSecValueData as String] = encodedPassword as AnyObject? + + let query = keychainQuery(withService: KeychainConfiguration.serviceName, account: KeychainConfiguration.account, accessGroup: KeychainConfiguration.accessGroup) + let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) + + // Throw an error if an unexpected status was returned. + guard status == noErr else { throw KeychainError.unhandledError(status: status) } + } + catch KeychainError.noPassword { + /* + No password was found in the keychain. Create a dictionary to save + as a new keychain item. + */ + var newItem = keychainQuery(withService: KeychainConfiguration.serviceName, account: KeychainConfiguration.account, accessGroup: KeychainConfiguration.accessGroup) + newItem[kSecValueData as String] = encodedPassword as AnyObject? + + // Add a the new item to the keychain. + let status = SecItemAdd(newItem as CFDictionary, nil) + + // Throw an error if an unexpected status was returned. + guard status == noErr else { throw KeychainError.unhandledError(status: status) } + } + } + + func readPassword() throws -> String { + /* + Build a query to find the item that matches the service, account and + access group. + */ + var query = keychainQuery(withService: KeychainConfiguration.serviceName, account: KeychainConfiguration.account, accessGroup: KeychainConfiguration.accessGroup) + + query[kSecMatchLimit as String] = kSecMatchLimitOne + query[kSecReturnAttributes as String] = kCFBooleanTrue + query[kSecReturnData as String] = kCFBooleanTrue + + // Try to fetch the existing keychain item that matches the query. + var queryResult: AnyObject? + let status = withUnsafeMutablePointer(to: &queryResult) { + SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) + } + + // Check the return status and throw an error if appropriate. + guard status != errSecItemNotFound else { throw KeychainError.noPassword } + guard status == noErr else { throw KeychainError.unhandledError(status: status) } + + // Parse the password string from the query result. + guard let existingItem = queryResult as? [String : AnyObject], + let passwordData = existingItem[kSecValueData as String] as? Data, + let password = String(data: passwordData, encoding: String.Encoding.utf8) + else { + throw KeychainError.unexpectedPasswordData + } + + return password + } + + func deleteItem() throws { + // Delete the existing item from the keychain. + let query = keychainQuery(withService: KeychainConfiguration.serviceName, account: KeychainConfiguration.account, accessGroup: KeychainConfiguration.accessGroup) + let status = SecItemDelete(query as CFDictionary) + + // Throw an error if an unexpected status was returned. + guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) } + } + + private func keychainQuery(withService service: String, account: String? = nil, accessGroup: String? = nil) -> [String : AnyObject] { + var query = [String : AnyObject]() + query[kSecClass as String] = kSecClassGenericPassword + query[kSecAttrService as String] = service as AnyObject? + + if let account = account { + query[kSecAttrAccount as String] = account as AnyObject? + } + + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup as String] = accessGroup as AnyObject? + } + + return query + } +} diff --git a/macGist/Menu.swift b/macGist/Menu.swift new file mode 100644 index 0000000..bf08516 --- /dev/null +++ b/macGist/Menu.swift @@ -0,0 +1,69 @@ +// +// Menu.swift +// macGist +// +// Created by Fernando Bunn on 23/05/17. +// Copyright © 2017 Fernando Bunn. All rights reserved. +// + +import Foundation +import AppKit + +enum Images { + case standardIcon + case checkmarkIcon + + var image: NSImage? { + var image: NSImage? + + switch self { + case .standardIcon: + image = NSImage(named: "icon") + case .checkmarkIcon: + image = NSImage(named: "check_white_icon") + } + image?.isTemplate = true + return image + } +} + +class Menu { + fileprivate let item = NSStatusBar.system().statusItem(withLength: NSVariableStatusItemLength) + + func setupMenu() { + item.image = Images.standardIcon.image + let menu = NSMenu() + + let gistMenuItem = NSMenuItem(title: "Create Gist", action: #selector(Menu.createGist), keyEquivalent: "g") + gistMenuItem.target = self + menu.addItem(gistMenuItem) + + let quitMenuItem = NSMenuItem(title: "Quit", action: #selector(Menu.quit), keyEquivalent: "q") + quitMenuItem.target = self + menu.addItem(quitMenuItem) + + item.menu = menu + } + + @objc fileprivate func quit() { + NSApplication.shared().terminate(self) + } + + fileprivate func displaySuccessIcon() { + item.image = Images.checkmarkIcon.image + let deadlineTime = DispatchTime.now() + .seconds(2) + DispatchQueue.main.asyncAfter(deadline: deadlineTime) { + self.item.image = Images.standardIcon.image } + } + + @objc fileprivate func createGist() { + guard let copiedItem = PasteboardHelper().getPasteboardString() else { return } + GitHubAPI().post(gist: copiedItem, fileExtension: "file", authenticated: false) { (error, string) in + if let value = string { + PasteboardHelper().save(string: value) + self.displaySuccessIcon() + } + } + } +} + diff --git a/macGist/PasteboardHelper.swift b/macGist/PasteboardHelper.swift new file mode 100644 index 0000000..e90244f --- /dev/null +++ b/macGist/PasteboardHelper.swift @@ -0,0 +1,34 @@ +// +// PasteboardHelper.swift +// macGist +// +// Created by Fernando Bunn on 17/06/17. +// Copyright © 2017 Fernando Bunn. All rights reserved. +// + +import Foundation +import AppKit + + +struct PasteboardHelper { + + func getPasteboardString() -> String? { + let pasteboard = NSPasteboard.general() + + if let copiedItems = pasteboard.readObjects(forClasses: [NSString.self], options: nil) { + for item in copiedItems { + if let stringItem = item as? String { + return stringItem; + } + } + } + return nil + } + + func save(string: String) { + let pasteboard = NSPasteboard.general() + pasteboard.clearContents() + pasteboard.writeObjects([string as NSPasteboardWriting]) + + } +} diff --git a/other/gist.gif b/other/gist.gif new file mode 100644 index 0000000..76325b3 Binary files /dev/null and b/other/gist.gif differ diff --git a/other/ss.png b/other/ss.png new file mode 100644 index 0000000..71b4eb4 Binary files /dev/null and b/other/ss.png differ