├── Capture ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── icon-16.png │ │ ├── icon-32.png │ │ ├── icon-128.png │ │ ├── icon-16@2x.png │ │ ├── icon-256.png │ │ ├── icon-32@2x.png │ │ ├── icon-512.png │ │ ├── icon-128@2x.png │ │ ├── icon-256@2x.png │ │ ├── icon-512@2x.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── BridgingHeader.h ├── Capture.entitlements ├── Controllers │ ├── PrefsWindowController.swift │ ├── CaptureWindowController.swift │ ├── PrefsWindowController.xib │ └── CaptureWindowController.xib ├── AppDelegate.swift ├── Info.plist ├── Extensions │ ├── Shared │ │ ├── Extensions-Geometry.swift │ │ └── Extensions-Shared.swift │ └── macOS │ │ └── Extensions-macOS.swift ├── CaptureThing.swift └── Base.lproj │ └── MainMenu.xib ├── README.md ├── Capture.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── project.pbxproj ├── Podfile ├── Capture.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Podfile.lock ├── LICENSE └── .gitignore /Capture/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # capture-thing 2 | 3 | Please see [the accompanying blog post](https://tyler.io/capture-thing/) for details. 4 | -------------------------------------------------------------------------------- /Capture/Assets.xcassets/AppIcon.appiconset/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-16.png -------------------------------------------------------------------------------- /Capture/Assets.xcassets/AppIcon.appiconset/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-32.png -------------------------------------------------------------------------------- /Capture/Assets.xcassets/AppIcon.appiconset/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-128.png -------------------------------------------------------------------------------- /Capture/Assets.xcassets/AppIcon.appiconset/icon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-16@2x.png -------------------------------------------------------------------------------- /Capture/Assets.xcassets/AppIcon.appiconset/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-256.png -------------------------------------------------------------------------------- /Capture/Assets.xcassets/AppIcon.appiconset/icon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-32@2x.png -------------------------------------------------------------------------------- /Capture/Assets.xcassets/AppIcon.appiconset/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-512.png -------------------------------------------------------------------------------- /Capture/Assets.xcassets/AppIcon.appiconset/icon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-128@2x.png -------------------------------------------------------------------------------- /Capture/Assets.xcassets/AppIcon.appiconset/icon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-256@2x.png -------------------------------------------------------------------------------- /Capture/Assets.xcassets/AppIcon.appiconset/icon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-512@2x.png -------------------------------------------------------------------------------- /Capture.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Capture/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Capture/BridgingHeader.h: -------------------------------------------------------------------------------- 1 | // 2 | // BridgingHeader.h 3 | // Capture 4 | // 5 | // Created by Tyler Hall on 4/19/21. 6 | // 7 | 8 | #ifndef BridgingHeader_h 9 | #define BridgingHeader_h 10 | 11 | #import 12 | 13 | #endif /* BridgingHeader_h */ 14 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'Capture' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for Capture 9 | pod 'MASShortcut' 10 | end 11 | -------------------------------------------------------------------------------- /Capture/Capture.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Capture.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Capture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Capture.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - MASShortcut (2.4.0) 3 | 4 | DEPENDENCIES: 5 | - MASShortcut 6 | 7 | SPEC REPOS: 8 | trunk: 9 | - MASShortcut 10 | 11 | SPEC CHECKSUMS: 12 | MASShortcut: d9e4909e878661cc42877cc9d6efbe638273ab57 13 | 14 | PODFILE CHECKSUM: 24a9bc608f460c195484110ae123abdace82d66e 15 | 16 | COCOAPODS: 1.11.2 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tyler Hall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Capture/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon-16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon-32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon-128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon-256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon-256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon-512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon-512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Capture/Controllers/PrefsWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsWindowController.swift 3 | // Capture 4 | // 5 | // Created by Tyler Hall on 4/19/21. 6 | // 7 | 8 | import AppKit 9 | 10 | let kShowShortcut = "kShowShortcut" 11 | let kCaptureScreenshot = "kCaptureScreenshot" 12 | let kCaptureActiveTab = "kCaptureActiveTab" 13 | let kCaptureAllTabs = "kCaptureAllTabs" 14 | let kCapturePath = "kCapturePath" 15 | let kBrowser = "kBrowser" 16 | 17 | class PrefsWindowController: NSWindowController { 18 | 19 | static let shared = PrefsWindowController(windowNibName: String(describing: PrefsWindowController.self)) 20 | 21 | @IBOutlet weak var showShortcutKeyView: MASShortcutView! 22 | @IBOutlet weak var screenshotCheckbox: NSButton! 23 | @IBOutlet weak var activeBrowserTabCheckbox: NSButton! 24 | @IBOutlet weak var allBrowserTabsCheckbox: NSButton! 25 | @IBOutlet weak var captureFolderPathControl: NSPathControl! 26 | 27 | override func windowDidLoad() { 28 | super.windowDidLoad() 29 | showShortcutKeyView.associatedUserDefaultsKey = kShowShortcut 30 | captureFolderPathControl.url = UserDefaults.standard.url(forKey: kCapturePath) 31 | } 32 | 33 | @IBAction func activeBrowserCheckboxClicked(_ sender: AnyObject?) { 34 | if activeBrowserTabCheckbox.state == .on { 35 | allBrowserTabsCheckbox.state = .off 36 | UserDefaults.standard.set(false, forKey: kCaptureAllTabs) 37 | } 38 | } 39 | 40 | @IBAction func allBrowsersCheckboxClicked(_ sender: AnyObject?) { 41 | if allBrowserTabsCheckbox.state == .on { 42 | activeBrowserTabCheckbox.state = .off 43 | UserDefaults.standard.set(false, forKey: kCaptureActiveTab) 44 | } 45 | } 46 | 47 | @IBAction func chooseCaptureFolder(_ sender: AnyObject?) { 48 | let panel = NSOpenPanel() 49 | panel.canChooseFiles = false 50 | panel.canChooseDirectories = true 51 | panel.runModal() 52 | if let url = panel.url { 53 | captureFolderPathControl.url = url 54 | UserDefaults.standard.set(url, forKey: kCapturePath) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .DS_Store 92 | /Pods 93 | -------------------------------------------------------------------------------- /Capture/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Capture 4 | // 5 | // Created by Tyler Hall on 4/18/21. 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | var isLaunching = true 14 | 15 | func applicationDidFinishLaunching(_ aNotification: Notification) { 16 | UserDefaults.standard.addObserver(self, forKeyPath: kShowShortcut, options: NSKeyValueObservingOptions.init(), context: nil) 17 | shortcutsDidChange() 18 | 19 | // Always show the prefs window until we have picked a capture folder... 20 | if UserDefaults.standard.url(forKey: kCapturePath) == nil { 21 | PrefsWindowController.shared.showWindow(nil) 22 | } 23 | } 24 | 25 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 26 | if keyPath == kShowShortcut { 27 | shortcutsDidChange() 28 | } 29 | } 30 | 31 | func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool { 32 | guard !isLaunching else { 33 | isLaunching = false 34 | return false 35 | } 36 | 37 | showCaptureWindow() 38 | return false 39 | } 40 | 41 | @objc func shortcutsDidChange() { 42 | MASShortcutBinder.shared()?.breakBinding(withDefaultsKey: kShowShortcut) 43 | MASShortcutBinder.shared()?.bindShortcut(withDefaultsKey: kShowShortcut, toAction: { [weak self] in 44 | self?.handleHotkey() 45 | }) 46 | } 47 | 48 | @IBAction func showPrefs(_ sender: AnyObject?) { 49 | PrefsWindowController.shared.showWindow(nil) 50 | } 51 | 52 | @IBAction func newCapture(_ sender: AnyObject?) { 53 | showCaptureWindow() 54 | } 55 | 56 | @IBAction func openCaptureFolder(_ sender: AnyObject?) { 57 | if let url = CaptureThing.shared.currentFileURL?.deletingLastPathComponent() { 58 | NSWorkspace.shared.activateFileViewerSelecting([url]) 59 | } 60 | } 61 | 62 | func handleHotkey() { 63 | let windowIsVisible = (CaptureWindowController.shared.window?.isVisible ?? false) 64 | 65 | if windowIsVisible && NSApp.isActive { 66 | CaptureWindowController.shared.takeQuickScreenshot() 67 | } else { 68 | showCaptureWindow() 69 | NSApp.activate(ignoringOtherApps: true) 70 | } 71 | } 72 | 73 | func showCaptureWindow() { 74 | // Always show the prefs window until we have picked a capture folder... 75 | guard UserDefaults.standard.url(forKey: kCapturePath) != nil else { 76 | PrefsWindowController.shared.showWindow(nil) 77 | return 78 | } 79 | 80 | CaptureWindowController.shared.showWindow(nil) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Capture/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0.0 21 | CFBundleVersion 22 | 2 23 | LSApplicationCategoryType 24 | public.app-category.productivity 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSAppleEventsUsageDescription 28 | Allow Capture to integrate with other applications. 29 | NSHumanReadableCopyright 30 | Copyright © 2021 Retina Studio. All rights reserved. 31 | NSPrincipalClass 32 | NSApplication 33 | NSServices 34 | 35 | 36 | NSKeyEquivalent 37 | 38 | default 39 | B 40 | 41 | NSMenuItem 42 | 43 | default 44 | Process with TextBuddy 45 | 46 | NSMessage 47 | processTextServiceHandler 48 | NSPortName 49 | TextBuddy 50 | NSRequiredContext 51 | 52 | NSSendTypes 53 | 54 | NSStringPboardType 55 | NSRTFPboardType 56 | NSRTFDPboardType 57 | 58 | NSServiceDescription 59 | Processes the selected text with TextBuddy. 60 | 61 | 62 | NSMenuItem 63 | 64 | default 65 | Send to TextBuddy 66 | 67 | NSMessage 68 | textServiceHandler 69 | NSPortName 70 | TextBuddy 71 | NSRequiredContext 72 | 73 | NSSendTypes 74 | 75 | NSStringPboardType 76 | NSRTFPboardType 77 | NSRTFDPboardType 78 | 79 | NSServiceDescription 80 | Sends the selected text to TextBuddy to be edited. 81 | 82 | 83 | NSMenuItem 84 | 85 | default 86 | Send to TextBuddy 87 | 88 | NSMessage 89 | fileServiceHandler 90 | NSPortName 91 | TextBuddy 92 | NSRequiredContext 93 | 94 | NSSendTypes 95 | 96 | NSURLPboardType 97 | 98 | NSServiceDescription 99 | Sends the selected file to TextBuddy to process. 100 | 101 | 102 | NSMainNibFile 103 | MainMenu 104 | 105 | 106 | -------------------------------------------------------------------------------- /Capture/Extensions/Shared/Extensions-Geometry.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | extension CGPoint { 4 | 5 | func scaled(_ size: CGSize) -> CGPoint { 6 | return CGPoint(x: x * size.width, y: y * size.height) 7 | } 8 | 9 | func offsetBy(_ dx: CGFloat, dy: CGFloat) -> CGPoint { 10 | return CGPoint(x: x + dx, y: y + dy) 11 | } 12 | 13 | func scaledAroundCenterPoint(_ scale: CGFloat, centerPoint: CGPoint) -> CGPoint { 14 | let xNew = (scale * (x - centerPoint.x)) + centerPoint.x 15 | let yNew = (scale * (y - centerPoint.y)) + centerPoint.y 16 | return CGPoint(x: xNew, y: yNew) 17 | } 18 | 19 | func rotatedAroundCenterPoint(_ radians: CGFloat, centerPoint: CGPoint) -> CGPoint { 20 | let s = sin(radians) 21 | let c = cos(radians) 22 | 23 | var px = self.x - centerPoint.x 24 | var py = self.y - centerPoint.y 25 | 26 | let newX = px * c - py * s 27 | let newY = px * s + py * c 28 | 29 | px = newX + centerPoint.x 30 | py = newY + centerPoint.y 31 | 32 | return CGPoint(x: px, y: py) 33 | } 34 | } 35 | 36 | extension CGFloat { 37 | var valueOrZero: CGFloat { 38 | return CGFloat.maximum(0, self) 39 | } 40 | } 41 | 42 | extension Array where Element == CGPoint { 43 | 44 | func pointsAdjacentTo(index: Int) -> (Int, Int)? { 45 | guard count >= 3 else { return nil } 46 | 47 | if index == 0 { 48 | return (count - 1, 1) 49 | } else if index == (count - 1) { 50 | return (index - 1, 0) 51 | } else { 52 | return (index - 1, index + 1) 53 | } 54 | } 55 | 56 | // This only works on arrays with 4 points 57 | func oppositeVertexIndex(vertexIndex: Int) -> Int { 58 | guard count == 4 else { fatalError() } 59 | switch vertexIndex { 60 | case 0: 61 | return 2 62 | case 1: 63 | return 3 64 | case 2: 65 | return 0 66 | case 3: 67 | return 1 68 | default: 69 | fatalError() 70 | } 71 | } 72 | } 73 | 74 | struct Line { 75 | var p1: CGPoint 76 | var p2: CGPoint 77 | 78 | var midPoint: CGPoint { 79 | return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2) 80 | } 81 | 82 | var length: CGFloat { 83 | return sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)) 84 | } 85 | 86 | var slope: CGFloat? { 87 | guard p2.x - p1.x != 0 else { return nil } 88 | return (p2.y - p1.y) / (p2.x - p1.x) 89 | } 90 | 91 | var yIntercept: CGFloat? { 92 | if let slope = slope { 93 | return p1.y - slope * p1.x 94 | } 95 | return nil 96 | } 97 | 98 | func pointAt(x: CGFloat) -> CGPoint? { 99 | if let slope = slope, let yIntercept = yIntercept { 100 | return CGPoint(x: x, y: slope * x + yIntercept) 101 | } 102 | return nil 103 | } 104 | 105 | func intercetionWith(line: Line) -> CGPoint? { 106 | // y = ax + c 107 | // y = bx + d 108 | // ax + c = bx + d 109 | // ax - bx = d - c 110 | // x = (d - c) / (a - b) 111 | 112 | guard let a = slope, let c = yIntercept else { 113 | return nil 114 | } 115 | 116 | guard let b = line.slope, let d = line.yIntercept else { 117 | return nil 118 | } 119 | 120 | // Check for parallel lines 121 | if a == b { 122 | return nil 123 | } 124 | 125 | if a - b == 0 { 126 | return nil 127 | } 128 | 129 | if d - c == 0 { 130 | return nil 131 | } 132 | 133 | let x = (d - c) / (a - b) 134 | let y = a * x + c 135 | 136 | let p = CGPoint(x: x, y: y) 137 | if hasXCoord(x: p.x) && hasYCoord(y: p.y) && line.hasXCoord(x: p.x) && line.hasYCoord(y: p.y) { 138 | return p 139 | } 140 | 141 | return nil 142 | } 143 | 144 | func hasXCoord(x: CGFloat) -> Bool { 145 | let xMin = min(p1.x, p2.x) 146 | let xMax = max(p1.x, p2.x) 147 | return (xMin <= x) && (x <= xMax) 148 | } 149 | 150 | func hasYCoord(y: CGFloat) -> Bool { 151 | let yMin = min(p1.y, p2.y) 152 | let yMax = max(p1.y, p2.y) 153 | return (yMin <= y) && (y <= yMax) 154 | } 155 | 156 | func hasPoint(point: CGPoint) -> Bool { 157 | return hasXCoord(x: point.x) && hasYCoord(y: point.y) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Capture/Extensions/macOS/Extensions-macOS.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | extension NSEvent { 4 | var isRightClick: Bool { 5 | let rightClick = (self.type == .rightMouseDown) 6 | let controlClick = self.modifierFlags.contains(.control) 7 | return rightClick || controlClick 8 | } 9 | } 10 | 11 | extension NSFont { 12 | func withTraits(_ traits: NSFontDescriptor.SymbolicTraits) -> NSFont { 13 | let fd = fontDescriptor.withSymbolicTraits(traits) 14 | if let font = NSFont(descriptor: fd, size: pointSize) { 15 | return font 16 | } else { 17 | return self 18 | } 19 | } 20 | 21 | func italics() -> NSFont { 22 | return withTraits(.italic) 23 | } 24 | 25 | func bold() -> NSFont { 26 | return withTraits(.bold) 27 | } 28 | 29 | func boldItalics() -> NSFont { 30 | return withTraits([.bold, .italic]) 31 | } 32 | } 33 | 34 | extension NSImage { 35 | func writeJPGToURL(_ url: URL, quality: Float = 0.85) -> Bool { 36 | let properties = [NSBitmapImageRep.PropertyKey.compressionFactor: quality] 37 | guard let imageData = self.tiffRepresentation else { return false } 38 | guard let imageRep = NSBitmapImageRep(data: imageData) else { return false } 39 | guard let fileData = imageRep.representation(using: .jpeg, properties: properties) else { return false } 40 | 41 | do { 42 | try fileData.write(to: url) 43 | } catch { 44 | return false 45 | } 46 | 47 | return true 48 | } 49 | 50 | func writePNGToURL(_ url: URL) -> Bool { 51 | guard let imageData = self.tiffRepresentation else { return false } 52 | guard let imageRep = NSBitmapImageRep(data: imageData) else { return false } 53 | guard let fileData = imageRep.representation(using: .png, properties: [:]) else { return false } 54 | 55 | do { 56 | try fileData.write(to: url) 57 | } catch { 58 | return false 59 | } 60 | 61 | return true 62 | } 63 | 64 | func tint(color: NSColor) -> NSImage { 65 | let image = self.copy() as! NSImage 66 | image.lockFocus() 67 | 68 | color.set() 69 | 70 | let imageRect = NSRect(origin: NSZeroPoint, size: image.size) 71 | imageRect.fill(using: .sourceAtop) 72 | 73 | image.unlockFocus() 74 | 75 | return image 76 | } 77 | 78 | func scaleBy(factor: CGFloat = 0.5) -> NSImage { 79 | let newSize = NSMakeSize(size.width * factor, size.height * factor) 80 | let scaledImage = NSImage(size: newSize) 81 | scaledImage.lockFocus() 82 | draw(in: NSMakeRect(0, 0, newSize.width, newSize.height)) 83 | scaledImage.unlockFocus() 84 | return scaledImage 85 | } 86 | } 87 | 88 | extension NSTableView { 89 | func reloadOnMainThread(_ complete: (() -> ())? = nil) { 90 | DispatchQueue.main.async { 91 | self.reloadData() 92 | complete?() 93 | } 94 | } 95 | 96 | func reloadMaintainingSelection(_ complete: (() -> ())? = nil) { 97 | let oldSelectedRowIndexes = selectedRowIndexes 98 | reloadOnMainThread { 99 | if oldSelectedRowIndexes.count == 0 { 100 | if self.numberOfRows > 0 { 101 | self.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) 102 | } 103 | } else { 104 | self.selectRowIndexes(oldSelectedRowIndexes, byExtendingSelection: false) 105 | } 106 | } 107 | } 108 | 109 | func selectFirstPossibleRow() { 110 | for i in 0..= 0) && (selectedRow < numberOfRows) { 140 | return item(atRow: selectedRow) 141 | } else { 142 | return nil 143 | } 144 | } 145 | } 146 | 147 | var selectedView: NSView? { 148 | get { 149 | if (selectedRow >= 0) && (selectedRow < numberOfRows) { 150 | return view(atColumn: 0, row: self.selectedRow, makeIfNecessary: false) 151 | } else { 152 | return nil 153 | } 154 | } 155 | } 156 | 157 | func expandAll() { 158 | DispatchQueue.main.async { 159 | self.expandItem(nil, expandChildren: true) 160 | } 161 | } 162 | } 163 | 164 | extension NSView { 165 | func pinEdges(to other: NSView, offset: CGFloat = 0, animate: Bool = false) { 166 | if animate { 167 | animator().leadingAnchor.constraint(equalTo: other.leadingAnchor, constant: offset).isActive = true 168 | } else { 169 | leadingAnchor.constraint(equalTo: other.leadingAnchor, constant: offset).isActive = true 170 | trailingAnchor.constraint(equalTo: other.trailingAnchor).isActive = true 171 | topAnchor.constraint(equalTo: other.topAnchor).isActive = true 172 | bottomAnchor.constraint(equalTo: other.bottomAnchor).isActive = true 173 | } 174 | } 175 | } 176 | 177 | extension NSWindow { 178 | func toolbarHeight() -> CGFloat { 179 | if let windowFrameHeight = contentView?.frame.height { 180 | let contentLayoutRectHeight = contentLayoutRect.height 181 | let fullSizeContentViewNoContentAreaHeight = windowFrameHeight - contentLayoutRectHeight 182 | return fullSizeContentViewNoContentAreaHeight 183 | } 184 | 185 | return 0 186 | } 187 | } 188 | 189 | extension String { 190 | // I should really just stop using these and switch to one of the better, full-featured attributed string 191 | // libraries, but meh. This stuff works for now. 192 | func boldString(textColor: NSColor = NSColor.textColor) -> NSMutableAttributedString { 193 | let attrStr = NSMutableAttributedString(string: self) 194 | attrStr.addAttribute(NSAttributedString.Key.foregroundColor, value: textColor, range: NSRange(self.startIndex..., in: self)) 195 | attrStr.addAttribute(NSAttributedString.Key.font, value: NSFont.boldSystemFont(ofSize: NSFont.systemFontSize), range: NSRange(self.startIndex..., in: self)) 196 | return attrStr 197 | } 198 | 199 | func coloredAttributedString(textColor: NSColor = NSColor.textColor) -> NSMutableAttributedString { 200 | let attrStr = NSMutableAttributedString(string: self) 201 | attrStr.addAttribute(NSAttributedString.Key.foregroundColor, value: textColor, range: NSRange(self.startIndex..., in: self)) 202 | attrStr.addAttribute(NSAttributedString.Key.font, value: NSFont.systemFont(ofSize: NSFont.systemFontSize), range: NSRange(self.startIndex..., in: self)) 203 | return attrStr 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Capture/Controllers/CaptureWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptureWindowController.swift 3 | // Capture 4 | // 5 | // Created by Tyler Hall on 4/18/21. 6 | // 7 | 8 | import AppKit 9 | import CoreWLAN 10 | 11 | class CaptureWindowController: NSWindowController { 12 | 13 | static let shared = CaptureWindowController(windowNibName: String(describing: CaptureWindowController.self)) 14 | 15 | @IBOutlet weak var summaryTextField: NSTextField! 16 | @IBOutlet weak var detailsTextView: NSTextView! 17 | @IBOutlet weak var dropView: DroppableView! 18 | @IBOutlet weak var attachmentsTableView: NSTableView! 19 | @IBOutlet weak var screenshotCheckbox: NSButton! 20 | @IBOutlet weak var activeBrowserTabCheckbox: NSButton! 21 | @IBOutlet weak var allBrowserTabsCheckbox: NSButton! 22 | 23 | var attachments = [URL]() 24 | 25 | override func windowDidLoad() { 26 | super.windowDidLoad() 27 | 28 | detailsTextView.font = NSFont.systemFont(ofSize: 16) 29 | detailsTextView.textContainerInset = NSSize(width: 10, height: 10) 30 | 31 | setupDragging() 32 | } 33 | 34 | override func showWindow(_ sender: Any?) { 35 | super.showWindow(sender) 36 | restoreDefaults() 37 | } 38 | 39 | func restoreDefaults() { 40 | summaryTextField.stringValue = "" 41 | detailsTextView.string = "" 42 | screenshotCheckbox.state = UserDefaults.standard.bool(forKey: kCaptureScreenshot) ? .on : .off 43 | activeBrowserTabCheckbox.state = UserDefaults.standard.bool(forKey: kCaptureActiveTab) ? .on : .off 44 | allBrowserTabsCheckbox.state = UserDefaults.standard.bool(forKey: kCaptureAllTabs) ? .on : .off 45 | attachments.removeAll() 46 | attachmentsTableView.reloadData() 47 | window?.makeFirstResponder(summaryTextField) 48 | } 49 | 50 | @IBAction func focusSummaryTextField(_ sender: AnyObject?) { 51 | window?.makeFirstResponder(summaryTextField) 52 | } 53 | 54 | @IBAction func checkScreenshot(_ sender: AnyObject?) { 55 | screenshotCheckbox.state = (screenshotCheckbox.state == .on) ? .off : .on 56 | } 57 | 58 | @IBAction func checkActiveBrowserTab(_ sender: AnyObject?) { 59 | if activeBrowserTabCheckbox.state == .on { 60 | activeBrowserTabCheckbox.state = .off 61 | } else { 62 | activeBrowserTabCheckbox.state = .on 63 | allBrowserTabsCheckbox.state = .off 64 | } 65 | } 66 | 67 | @IBAction func checkAllBrowserTabs(_ sender: AnyObject?) { 68 | if allBrowserTabsCheckbox.state == .on { 69 | allBrowserTabsCheckbox.state = .off 70 | } else { 71 | allBrowserTabsCheckbox.state = .on 72 | activeBrowserTabCheckbox.state = .off 73 | } 74 | } 75 | 76 | @IBAction func activeBrowserCheckboxClicked(_ sender: AnyObject?) { 77 | if activeBrowserTabCheckbox.state == .on { 78 | allBrowserTabsCheckbox.state = .off 79 | } 80 | } 81 | 82 | @IBAction func allBrowsersCheckboxClicked(_ sender: AnyObject?) { 83 | if allBrowserTabsCheckbox.state == .on { 84 | activeBrowserTabCheckbox.state = .off 85 | } 86 | } 87 | 88 | @IBAction func saveButtonClicked(_ sender: AnyObject?) { 89 | window?.close() 90 | NSApp.hide(nil) 91 | 92 | let settings = CaptureThing.Settings(summary: summaryTextField.stringValue, 93 | details: detailsTextView.string, 94 | takeScreenshot: (screenshotCheckbox.state == .on), 95 | activeBrowserTab: (activeBrowserTabCheckbox.state == .on), 96 | allBrowserTabs: (allBrowserTabsCheckbox.state == .on), 97 | attachments: attachments) 98 | 99 | // Delay to the next run loop so the window closes before screenshots are taken. 100 | DispatchQueue.main.async { 101 | CaptureThing.shared.capture(settings: settings) 102 | } 103 | } 104 | 105 | @IBAction func cancelButtonClicked(_ sender: AnyObject?) { 106 | window?.close() 107 | NSApp.hide(nil) 108 | } 109 | 110 | func takeQuickScreenshot() { 111 | window?.close() 112 | NSApp.hide(nil) 113 | 114 | let settings = CaptureThing.Settings(summary: "Quick Screenshot", 115 | details: "", 116 | takeScreenshot: true, 117 | activeBrowserTab: false, 118 | allBrowserTabs: false, 119 | attachments: []) 120 | 121 | // Delay to the next run loop so the window closes before screenshots are taken. 122 | DispatchQueue.main.async { 123 | CaptureThing.shared.capture(settings: settings) 124 | } 125 | } 126 | } 127 | 128 | protocol DragDelegate: AnyObject { 129 | func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation 130 | func draggingExited(_ sender: NSDraggingInfo?) 131 | func draggingEnded(_ sender: NSDraggingInfo) 132 | func performDragOperation(_ sender: NSDraggingInfo) -> Bool 133 | } 134 | 135 | extension CaptureWindowController: DragDelegate { 136 | 137 | func setupDragging() { 138 | dropView.dragDelegate = self 139 | } 140 | 141 | func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { 142 | return .copy 143 | } 144 | 145 | func draggingExited(_ sender: NSDraggingInfo?) { 146 | 147 | } 148 | 149 | func draggingEnded(_ sender: NSDraggingInfo) { 150 | 151 | } 152 | 153 | func performDragOperation(_ sender: NSDraggingInfo) -> Bool { 154 | let supportedClasses = [NSFilePromiseReceiver.self, NSURL.self] 155 | let searchOptions: [NSPasteboard.ReadingOptionKey: Any] = [.urlReadingFileURLsOnly: true] 156 | 157 | sender.enumerateDraggingItems(options: [], for: nil, classes: supportedClasses, searchOptions: searchOptions) { [weak self] (draggingItem, _, _) in 158 | guard let self = self else { return } 159 | switch draggingItem.item { 160 | case let filePromiseReceiver as NSFilePromiseReceiver: 161 | filePromiseReceiver.receivePromisedFiles(atDestination: CaptureThing.shared.dropDestinationURL, options: [:], 162 | operationQueue: CaptureThing.shared.workQueue) { (fileURL, error) in 163 | if error == nil { 164 | self.attachments.append(fileURL) 165 | DispatchQueue.main.async { [weak self] in 166 | self?.attachmentsTableView.reloadData() 167 | } 168 | } 169 | } 170 | case let fileURL as URL: 171 | do { 172 | let newURL = CaptureThing.shared.dropDestinationURL.appendingPathComponent(fileURL.lastPathComponent) 173 | try FileManager.default.copyItem(at: fileURL, to: newURL) 174 | self.attachments.append(newURL) 175 | DispatchQueue.main.async { [weak self] in 176 | self?.attachmentsTableView.reloadData() 177 | } 178 | } catch { 179 | return 180 | } 181 | default: 182 | break 183 | } 184 | } 185 | 186 | return true 187 | } 188 | } 189 | 190 | extension CaptureWindowController: NSTableViewDataSource, NSTableViewDelegate { 191 | 192 | func numberOfRows(in tableView: NSTableView) -> Int { 193 | return attachments.count 194 | } 195 | 196 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 197 | guard attachments.indices.contains(row) else { return nil } 198 | guard let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("cell"), owner: nil) as? NSTableCellView else { return nil } 199 | cell.textField?.stringValue = attachments[row].lastPathComponent 200 | return cell 201 | } 202 | } 203 | 204 | 205 | class DroppableView: NSView { 206 | weak var dragDelegate: DragDelegate? 207 | 208 | override func awakeFromNib() { 209 | super.awakeFromNib() 210 | registerForDraggedTypes([.fileURL]) 211 | } 212 | 213 | override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { 214 | return dragDelegate?.draggingEntered(sender) ?? NSDragOperation.init() 215 | } 216 | 217 | override func draggingExited(_ sender: NSDraggingInfo?) { 218 | dragDelegate?.draggingExited(sender) 219 | } 220 | 221 | override func draggingEnded(_ sender: NSDraggingInfo) { 222 | dragDelegate?.draggingEnded(sender) 223 | } 224 | 225 | override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { 226 | dragDelegate?.performDragOperation(sender) ?? false 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /Capture/CaptureThing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptureThing.swift 3 | // Capture 4 | // 5 | // Created by Tyler Hall on 4/18/21. 6 | // 7 | 8 | import Foundation 9 | import CoreWLAN 10 | import CoreGraphics 11 | import AVFoundation 12 | import AppKit 13 | 14 | class CaptureThing { 15 | 16 | static let shared = CaptureThing() 17 | 18 | lazy var getAllTabsScriptSafari: NSAppleScript = { 19 | let script = """ 20 | set outList to {} 21 | tell application "Safari" 22 | repeat with w in windows 23 | repeat with t in (tabs of w) 24 | set theTitle to name of t 25 | copy theTitle to end of outList 26 | set theURL to URL of t 27 | copy theURL to end of outList 28 | end repeat 29 | end repeat 30 | end tell 31 | 32 | return outList 33 | """ 34 | let appleScript = NSAppleScript(source: script) 35 | appleScript?.compileAndReturnError(nil) 36 | return appleScript! 37 | }() 38 | 39 | lazy var getActiveTabsScriptSafari: NSAppleScript = { 40 | let script = """ 41 | set outList to {} 42 | tell application "Safari" 43 | set theTitle to the name of current tab of first window 44 | copy theTitle to end of outList 45 | set theURL to the URL of current tab of first window 46 | copy theURL to end of outList 47 | end tell 48 | 49 | return outList 50 | """ 51 | let appleScript = NSAppleScript(source: script) 52 | appleScript?.compileAndReturnError(nil) 53 | return appleScript! 54 | }() 55 | 56 | lazy var getAllTabsScriptChrome: NSAppleScript = { 57 | let script = """ 58 | set outList to {} 59 | 60 | tell application "Google Chrome" 61 | repeat with w in windows 62 | repeat with t in (tabs of w) 63 | set theTitle to title of t 64 | copy theTitle to end of outList 65 | set theURL to URL of t 66 | copy theURL to end of outList 67 | end repeat 68 | end repeat 69 | end tell 70 | 71 | return outList 72 | """ 73 | let appleScript = NSAppleScript(source: script) 74 | appleScript?.compileAndReturnError(nil) 75 | return appleScript! 76 | }() 77 | 78 | lazy var getActiveTabsScriptChrome: NSAppleScript = { 79 | let script = """ 80 | set outList to {} 81 | 82 | tell application "Google Chrome" 83 | set theTitle to title of active tab of first window 84 | copy theTitle to end of outList 85 | set theURL to URL of active tab of first window 86 | copy theURL to end of outList 87 | end tell 88 | 89 | return outList 90 | """ 91 | let appleScript = NSAppleScript(source: script) 92 | appleScript?.compileAndReturnError(nil) 93 | return appleScript! 94 | }() 95 | 96 | lazy var getAllTabsScriptBrave: NSAppleScript = { 97 | let script = """ 98 | set outList to {} 99 | 100 | tell application "Brave Browser" 101 | repeat with w in windows 102 | repeat with t in (tabs of w) 103 | set theTitle to title of t 104 | copy theTitle to end of outList 105 | set theURL to URL of t 106 | copy theURL to end of outList 107 | end repeat 108 | end repeat 109 | end tell 110 | 111 | return outList 112 | """ 113 | let appleScript = NSAppleScript(source: script) 114 | appleScript?.compileAndReturnError(nil) 115 | return appleScript! 116 | }() 117 | 118 | lazy var getActiveTabScriptBrave: NSAppleScript = { 119 | let script = """ 120 | set outList to {} 121 | 122 | tell application "Brave Browser" 123 | set theTitle to title of active tab of first window 124 | copy theTitle to end of outList 125 | set theURL to URL of active tab of first window 126 | copy theURL to end of outList 127 | end tell 128 | 129 | return outList 130 | """ 131 | let appleScript = NSAppleScript(source: script) 132 | appleScript?.compileAndReturnError(nil) 133 | return appleScript! 134 | }() 135 | 136 | enum Browser: Int { 137 | case Safari = 0 138 | case Brave = 1 139 | case Chrome = 2 140 | } 141 | 142 | struct Settings { 143 | var summary: String 144 | var details: String 145 | var takeScreenshot: Bool 146 | var activeBrowserTab: Bool 147 | var allBrowserTabs: Bool 148 | var attachments: [URL] 149 | } 150 | 151 | var hostname: String? 152 | 153 | var currentFileURL: URL? { 154 | guard let captureFolderURL = UserDefaults.standard.url(forKey: kCapturePath) else { return nil } 155 | 156 | let dfDir = DateFormatter() 157 | dfDir.dateFormat = "yyyy/MM/" 158 | let dateDir = dfDir.string(from: Date()) 159 | 160 | let dfFilename = DateFormatter() 161 | dfFilename.dateFormat = "yyyy-MM-dd EEEE" 162 | let dateFilename = dfFilename.string(from: Date()) 163 | 164 | var url = captureFolderURL.appendingPathComponent(dateDir) 165 | 166 | if !FileManager.default.fileExists(atPath: url.path) { 167 | do { 168 | try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) 169 | } catch { 170 | return nil 171 | } 172 | } 173 | 174 | url.appendPathComponent(dateFilename) 175 | url.appendPathExtension("md") 176 | if !FileManager.default.fileExists(atPath: url.path) { 177 | do { 178 | try ("" as NSString).write(to: url, atomically: false, encoding: String.Encoding.utf8.rawValue) 179 | } catch { 180 | return nil 181 | } 182 | } 183 | 184 | return url 185 | } 186 | 187 | var attachmentsDir: URL? { 188 | guard let currentFileURL = currentFileURL else { return nil } 189 | 190 | var dir = currentFileURL.deletingLastPathComponent() 191 | dir.appendPathComponent("attachments") 192 | 193 | if !FileManager.default.fileExists(atPath: dir.path) { 194 | do { 195 | try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: false, attributes: nil) 196 | } catch { 197 | return nil 198 | } 199 | } 200 | 201 | return dir 202 | } 203 | 204 | var dropDestinationURL: URL { 205 | let destinationURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 206 | try? FileManager.default.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil) 207 | return destinationURL 208 | } 209 | 210 | var workQueue: OperationQueue = { 211 | let providerQueue = OperationQueue() 212 | providerQueue.qualityOfService = .userInitiated 213 | return providerQueue 214 | }() 215 | 216 | init() { 217 | DispatchQueue.global(qos: .userInitiated).async { [weak self] in 218 | self?.hostname = Host.current().name 219 | } 220 | } 221 | } 222 | 223 | extension CaptureThing { 224 | 225 | func capture(settings: Settings) { 226 | var outStr = makeHeader(summary: settings.summary, details: settings.details) 227 | 228 | if settings.takeScreenshot { 229 | outStr += makeScreenshot() 230 | } 231 | 232 | outStr += makeAttachments(attachments: settings.attachments) 233 | 234 | if settings.allBrowserTabs { 235 | outStr += makeAllBrowserTabs() 236 | } else if settings.activeBrowserTab { 237 | outStr += makeActiveBrowserTab() 238 | } 239 | 240 | outStr += "### Misc Info\n" 241 | outStr += makeTimestamp() 242 | outStr += makeWifi() 243 | outStr += makeHostname() 244 | 245 | outStr += "\n* * * * *\n\n" 246 | 247 | writeToFile(str: outStr) 248 | } 249 | 250 | func makeHeader(summary: String, details: String) -> String { 251 | let df = DateFormatter() 252 | df.dateStyle = .none 253 | df.timeStyle = .short 254 | let dateStr = df.string(from: Date()) 255 | 256 | let trimmedSummary = summary.trimmingCharacters(in: .whitespacesAndNewlines) 257 | 258 | var outStr = "# \(dateStr)\n" 259 | outStr += trimmedSummary + "\n" 260 | outStr += "----------\n" 261 | 262 | if details.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 { 263 | outStr += details + "\n\n" 264 | } 265 | 266 | return outStr 267 | } 268 | 269 | func makeScreenshot() -> String { 270 | guard let attachmentsDir = attachmentsDir else { return "" } 271 | 272 | var outStr = "" 273 | if let filenames = takeScreenshots(folderURL: attachmentsDir), filenames.count > 0 { 274 | var i = 1 275 | for filename in filenames { 276 | if let escaped = filename.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) { 277 | outStr += "![Screenshot \(i)](attachments/\(escaped))\n" 278 | i += 1 279 | } 280 | } 281 | outStr += "\n" 282 | } 283 | return outStr 284 | } 285 | 286 | func makeAttachments(attachments: [URL]) -> String { 287 | guard let attachmentsDir = attachmentsDir else { return "" } 288 | guard attachments.count > 0 else { return "" } 289 | 290 | let unixTimestamp = Int32(Date().timeIntervalSince1970) 291 | let sortingDF = DateFormatter() 292 | sortingDF.dateFormat = "yyyy-MM-dd" 293 | let sortingKey = sortingDF.string(from: Date()) 294 | 295 | var outStr = "### Attachments\n" 296 | for srcURL in attachments { 297 | do { 298 | let fn = ("\(sortingKey) \(unixTimestamp)" + srcURL.lastPathComponent) 299 | if let escaped = fn.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) { 300 | var destURL = attachmentsDir 301 | destURL.appendPathComponent(fn) 302 | try FileManager.default.copyItem(at: srcURL, to: destURL) 303 | outStr += "* [\(srcURL.lastPathComponent)](attachments/\(escaped))\n" 304 | } 305 | } catch { 306 | 307 | } 308 | } 309 | 310 | outStr += "\n" 311 | 312 | return outStr 313 | } 314 | 315 | func makeWifi() -> String { 316 | if let ssid = CWWiFiClient.shared().interface()?.ssid() { 317 | return "*WiFi: \(ssid)*\n" 318 | } else { 319 | return "" 320 | } 321 | } 322 | 323 | func makeHostname() -> String { 324 | if let hostname = hostname { 325 | return "*Computer: \(hostname)*\n" 326 | } else { 327 | return "" 328 | } 329 | } 330 | 331 | func makeTimestamp() -> String { 332 | let df = ISO8601DateFormatter() 333 | let dateStr = df.string(from: Date()) 334 | return "*Timestamp: \(dateStr)*\n" 335 | } 336 | 337 | func makeActiveBrowserTab() -> String { 338 | guard let browser = Browser(rawValue: UserDefaults.standard.integer(forKey: kBrowser)) else { return "" } 339 | 340 | var eventDescriptor: NSAppleEventDescriptor 341 | switch browser { 342 | case .Safari: 343 | eventDescriptor = getActiveTabsScriptSafari.executeAndReturnError(nil) 344 | case .Brave: 345 | eventDescriptor = getActiveTabScriptBrave.executeAndReturnError(nil) 346 | case .Chrome: 347 | eventDescriptor = getActiveTabsScriptChrome.executeAndReturnError(nil) 348 | } 349 | 350 | guard let listDescriptor = eventDescriptor.coerce(toDescriptorType: typeAEList) else { return "" } 351 | guard listDescriptor.numberOfItems > 0 else { return "" } 352 | 353 | var outStr = "### Active Browser Tab\n" 354 | for i in stride(from: 1, through: listDescriptor.numberOfItems, by: 2) { 355 | if let title = listDescriptor.atIndex(i)?.stringValue, let url = listDescriptor.atIndex(i + 1)?.stringValue { 356 | outStr += "* [\(title)](\(url))\n" 357 | } 358 | } 359 | 360 | outStr += "\n" 361 | 362 | return outStr 363 | } 364 | 365 | func makeAllBrowserTabs() -> String { 366 | guard let browser = Browser(rawValue: UserDefaults.standard.integer(forKey: kBrowser)) else { return "" } 367 | 368 | var eventDescriptor: NSAppleEventDescriptor 369 | switch browser { 370 | case .Safari: 371 | eventDescriptor = getAllTabsScriptSafari.executeAndReturnError(nil) 372 | case .Brave: 373 | eventDescriptor = getAllTabsScriptBrave.executeAndReturnError(nil) 374 | case .Chrome: 375 | eventDescriptor = getAllTabsScriptChrome.executeAndReturnError(nil) 376 | } 377 | 378 | guard let listDescriptor = eventDescriptor.coerce(toDescriptorType: typeAEList) else { return "" } 379 | guard listDescriptor.numberOfItems > 0 else { return "" } 380 | 381 | var outStr = "### All Browser Tabs\n" 382 | for i in stride(from: 1, through: listDescriptor.numberOfItems, by: 2) { 383 | if let title = listDescriptor.atIndex(i)?.stringValue, let url = listDescriptor.atIndex(i + 1)?.stringValue { 384 | outStr += "* [\(title)](\(url))\n" 385 | } 386 | } 387 | 388 | outStr += "\n" 389 | 390 | return outStr 391 | } 392 | 393 | func writeToFile(str: String) { 394 | guard let fileURL = currentFileURL else { return } 395 | 396 | do { 397 | let fileHandle = try FileHandle(forWritingTo: fileURL) 398 | try fileHandle.seekToEnd() 399 | if let data = str.data(using: .utf8) { 400 | fileHandle.write(data) 401 | } 402 | try fileHandle.close() 403 | } catch { 404 | 405 | } 406 | } 407 | } 408 | 409 | extension CaptureThing { 410 | 411 | func takeScreenshots(folderURL: URL) -> [String]? { 412 | let sortingDF = DateFormatter() 413 | sortingDF.dateFormat = "yyyy-MM-dd" 414 | let sortingKey = sortingDF.string(from: Date()) 415 | 416 | var filenames = [String]() 417 | 418 | var displayCount: UInt32 = 0; 419 | var result = CGGetActiveDisplayList(0, nil, &displayCount) 420 | if (result != CGError.success) { 421 | print("error: \(result)") 422 | return nil 423 | } 424 | 425 | let allocated = Int(displayCount) 426 | let activeDisplays = UnsafeMutablePointer.allocate(capacity: allocated) 427 | result = CGGetActiveDisplayList(displayCount, activeDisplays, &displayCount) 428 | 429 | if (result != CGError.success) { 430 | print("error: \(result)") 431 | return nil 432 | } 433 | 434 | for i in 1...displayCount { 435 | let unixTimestamp = Int32(Date().timeIntervalSince1970) 436 | 437 | let baseFilename = "\(sortingKey) - Screenshot \(unixTimestamp)" + "_" + "\(i)" 438 | let filename = baseFilename + ".jpg" 439 | 440 | var fileURL = folderURL.appendingPathComponent(baseFilename) 441 | fileURL.appendPathExtension("jpg") 442 | 443 | let screenShot:CGImage = CGDisplayCreateImage(activeDisplays[Int(i - 1)])! 444 | let bitmapRep = NSBitmapImageRep(cgImage: screenShot) 445 | let jpegData = bitmapRep.representation(using: NSBitmapImageRep.FileType.jpeg, properties: [:])! 446 | 447 | do { 448 | try jpegData.write(to: fileURL, options: .atomic) 449 | filenames.append(filename) 450 | } 451 | catch { 452 | print("error: \(error)") 453 | } 454 | } 455 | 456 | return filenames 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /Capture/Controllers/PrefsWindowController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 104 | 105 | 106 | 107 | 118 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /Capture/Extensions/Shared/Extensions-Shared.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CryptoKit 3 | import CommonCrypto 4 | import NaturalLanguage 5 | 6 | extension Array { 7 | func shuffled() -> [Element] { 8 | if count < 2 { return self } 9 | var list = self 10 | for i in 0..<(list.count - 1) { 11 | let j = Int(arc4random_uniform(UInt32(list.count - i))) + i 12 | if i != j { 13 | list.swapAt(i, j) 14 | } 15 | } 16 | return list 17 | } 18 | 19 | // Creates an array containing all combinations of two arrays. 20 | static func createAllCombinations(from lhs: Array, and rhs: Array) -> Array<(T, U)> { 21 | let result: [(T, U)] = lhs.reduce([]) { (accum, t) in 22 | let innerResult: [(T, U)] = rhs.reduce([]) { (innerAccum, u) in 23 | return innerAccum + [(t, u)] 24 | } 25 | return accum + innerResult 26 | } 27 | return result 28 | } 29 | 30 | mutating func move(from oldIndex: Index, to newIndex: Index) { 31 | // Don't work for free and use swap when indices are next to each other - this 32 | // won't rebuild array and will be super efficient. 33 | if oldIndex == newIndex { return } 34 | if abs(newIndex - oldIndex) == 1 { return self.swapAt(oldIndex, newIndex) } 35 | self.insert(self.remove(at: oldIndex), at: newIndex) 36 | } 37 | } 38 | 39 | extension Array where Element: Equatable { 40 | mutating func move(_ element: Element, to newIndex: Index) { 41 | if let oldIndex: Int = self.firstIndex(of: element) { self.move(from: oldIndex, to: newIndex) } 42 | } 43 | } 44 | 45 | extension Collection { 46 | subscript(optional i: Index) -> Iterator.Element? { 47 | return self.indices.contains(i) ? self[i] : nil 48 | } 49 | } 50 | 51 | extension Data { 52 | var hexString: String { 53 | let hexString = map { String(format: "%02.2hhx", $0) }.joined() 54 | return hexString 55 | } 56 | } 57 | 58 | extension Date { 59 | static var yesterday: Date { return Date().dayBefore } 60 | static var tomorrow: Date { return Date().dayAfter } 61 | 62 | var dayBefore: Date { 63 | return Calendar.current.date(byAdding: .day, value: -1, to: noon)! 64 | } 65 | 66 | var dayAfter: Date { 67 | return Calendar.current.date(byAdding: .day, value: 1, to: noon)! 68 | } 69 | 70 | var noon: Date { 71 | return Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: self)! 72 | } 73 | 74 | // let d = Date() -> Mar 12, 2020 at 1:51 PM 75 | // d.stringify() -> "1584039099.486827" 76 | func stringify() -> String { 77 | return timeIntervalSince1970.stringValue() 78 | } 79 | 80 | // Date.unstringify("1584039099.486827") -> Mar 12, 2020 at 1:51 PM 81 | static func unstringify(_ ts: String) -> Date? { 82 | if let dbl = Double(ts) { 83 | return Date(timeIntervalSince1970: dbl) 84 | } 85 | return nil 86 | } 87 | 88 | func addMonth(n: Int) -> Date? { 89 | return Calendar.current.date(byAdding: .month, value: n, to: self) 90 | } 91 | 92 | func addDay(n: Int) -> Date? { 93 | return Calendar.current.date(byAdding: .day, value: n, to: self) 94 | } 95 | 96 | var month: Int { 97 | return Calendar.current.component(.month, from: self) 98 | } 99 | 100 | var year: Int { 101 | return Calendar.current.component(.year, from: self) 102 | } 103 | 104 | var day: Int { 105 | return Calendar.current.component(.day, from: self) 106 | } 107 | 108 | var startOfDay: Date { 109 | return Calendar.current.startOfDay(for: self) 110 | } 111 | 112 | var endOfDay: Date { 113 | var components = DateComponents() 114 | components.day = 1 115 | components.second = -1 116 | return Calendar.current.date(byAdding: components, to: startOfDay)! 117 | } 118 | 119 | var startOfMonth: Date { 120 | let components = Calendar.current.dateComponents([.year, .month], from: startOfDay) 121 | return Calendar.current.date(from: components)! 122 | } 123 | 124 | var endOfMonth: Date { 125 | var components = DateComponents() 126 | components.month = 1 127 | components.second = -1 128 | return Calendar.current.date(byAdding: components, to: startOfMonth)! 129 | } 130 | 131 | func numberOfDaysInMonth() -> Int { 132 | let range = Calendar.current.range(of: .day, in: .month, for: self)! 133 | return range.count 134 | } 135 | 136 | var isLastDayOfMonth: Bool { 137 | return dayAfter.month != month 138 | } 139 | 140 | var midnight: Date { 141 | return Calendar.current.startOfDay(for: self) 142 | } 143 | 144 | var startOfWeek: Date { 145 | return Calendar.current.date(from: Calendar.current.dateComponents([.yearForWeekOfYear, .weekOfYear], from: self))! 146 | } 147 | } 148 | 149 | extension Dictionary { 150 | // Combines self with another dictionary. 151 | mutating func merge(dict: [Key: Value]){ 152 | for (k, v) in dict { 153 | updateValue(v, forKey: k) 154 | } 155 | } 156 | } 157 | 158 | extension Double { 159 | // It's dumb, but I swear I end up having to dump a number into some type 160 | // of storage that only accepts a String way more often than I care to think about. 161 | func stringValue() -> String { 162 | return String(format:"%f", self) 163 | } 164 | 165 | func formatAsCurrency() -> String { 166 | let nf = NumberFormatter() 167 | nf.numberStyle = .currency 168 | return nf.string(from: NSNumber(floatLiteral: self))! 169 | } 170 | } 171 | 172 | extension FileManager { 173 | // Given a basename such as "My Picture" and fileExtension "jpg", 174 | // it will produce a unique, seqential filename such as "My Picuture 1.jpg" 175 | // that does not exist in directoryURL. If the directory already contained 176 | // "My Picuture.jpg", "My Picuture 1.jpg", "My Picuture 2.jpg", this will 177 | // return "My Picuture 3.jpg". 178 | func uniqueFileURL(directoryURL: URL, basename: String, fileExtension: String?) -> URL { 179 | var fullPathURL = directoryURL.appendingPathComponent(basename) 180 | if let ext = fileExtension { 181 | fullPathURL = fullPathURL.appendingPathExtension(ext) 182 | } 183 | 184 | var i = 0 185 | while FileManager.default.fileExists(atPath: fullPathURL.path) { 186 | i += 1 187 | let newBasename = "\(basename) \(i)" 188 | fullPathURL = directoryURL.appendingPathComponent(newBasename) 189 | if let ext = fileExtension { 190 | fullPathURL = fullPathURL.appendingPathExtension(ext) 191 | } 192 | } 193 | 194 | return fullPathURL 195 | } 196 | } 197 | 198 | extension NSAttributedString { 199 | // Haphazard solution that returns the range of a line of text at a given position, 200 | // where a line is delimited by newlines. 201 | func rangeOfLineAtLocation(_ location: Int) -> NSRange { 202 | if string.character(location).isNewline { 203 | var start = location 204 | while(start > 0 && !string.character(start - 1).isNewline) { 205 | start -= 1 206 | } 207 | return NSMakeRange(start, location-start) 208 | } 209 | 210 | var start: Int = location 211 | while(start > 0 && !string.character(start - 1).isNewline) { 212 | start -= 1 213 | } 214 | 215 | var end = location 216 | while(end < string.count - 1 && !string.character(end + 1).isNewline) { 217 | end += 1 218 | } 219 | 220 | return NSMakeRange(start, end - start) 221 | } 222 | 223 | func attributedStringByTrimmingCharacterSet(charSet: CharacterSet) -> NSAttributedString { 224 | let modifiedString = NSMutableAttributedString(attributedString: self) 225 | modifiedString.trimCharactersInSet(charSet: charSet) 226 | return NSAttributedString(attributedString: modifiedString) 227 | } 228 | } 229 | 230 | extension NSMutableAttributedString { 231 | func trimCharactersInSet(charSet: CharacterSet) { 232 | var range = (string as NSString).rangeOfCharacter(from: charSet as CharacterSet) 233 | 234 | // Trim leading characters from character set. 235 | while range.length != 0 && range.location == 0 { 236 | replaceCharacters(in: range, with: "") 237 | range = (string as NSString).rangeOfCharacter(from: charSet) 238 | } 239 | 240 | // Trim trailing characters from character set. 241 | range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards) 242 | while range.length != 0 && NSMaxRange(range) == length { 243 | replaceCharacters(in: range, with: "") 244 | range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards) 245 | } 246 | } 247 | } 248 | 249 | extension NSNotification.Name { 250 | func post(_ object: Any? = nil, userInfo: [AnyHashable: Any]? = nil) { 251 | NotificationCenter.default.post(name: self, object: object, userInfo: userInfo) 252 | } 253 | } 254 | 255 | extension NSRange { 256 | init?(string: String, lowerBound: String.Index, upperBound: String.Index) { 257 | let utf16 = string.utf16 258 | 259 | if let lowerBound = lowerBound.samePosition(in: utf16), let upperBound = upperBound.samePosition(in: utf16) { 260 | let location = utf16.distance(from: utf16.startIndex, to: lowerBound) 261 | let length = utf16.distance(from: lowerBound, to: upperBound) 262 | 263 | self.init(location: location, length: length) 264 | } 265 | return nil 266 | } 267 | 268 | init?(range: Range, in string: String) { 269 | self.init(string: string, lowerBound: range.lowerBound, upperBound: range.upperBound) 270 | } 271 | 272 | init?(range: ClosedRange, in string: String) { 273 | self.init(string: string, lowerBound: range.lowerBound, upperBound: range.upperBound) 274 | } 275 | } 276 | 277 | extension String { 278 | // An incredibly lenient and forgiving way to get a numeric String 279 | // out of another string - typically one provided by the user. 280 | // You shouldn't rely on this for anything truly mission critical. 281 | func numberString() -> String? { 282 | let strippedStr = trimmingCharacters(in: .whitespacesAndNewlines) 283 | let isNegative = strippedStr.hasPrefix("-") 284 | let allowedCharSet = CharacterSet(charactersIn: ".,0123456789") 285 | let filteredStr = components(separatedBy: allowedCharSet.inverted).joined() 286 | if (count(of: ".") + count(of: ",")) > 1 { return nil } 287 | return (isNegative ? "-" : "") + filteredStr 288 | } 289 | 290 | // Number of times a character occurs within a string. 291 | func count(of needle: Character) -> Int { 292 | return reduce(0) { 293 | $1 == needle ? $0 + 1 : $0 294 | } 295 | } 296 | 297 | // Returns an array of substrings delimited by whitespace - and also 298 | // combines tokens inside matching quotes into a single token. I don't 299 | // claim this to be pefect in every edge case - but I haven't encountered 300 | // a bug yet 🤷‍♀️. 301 | // "My name is Tim Apple".tokenize() -> ["My", "name", "is", "Tim", "Apple"] 302 | // "I hope the \"SF Giants\" have a \"better season\" this year" -> ["I", "hope", "the", "SF Giants", "have", "a", "better season", "this", "year"] 303 | @available(OSX 10.15, iOS 13, *) 304 | func tokenize() -> [String] { 305 | enum State { 306 | case Normal 307 | case InsideAQuote 308 | } 309 | 310 | let theString = self.trimmingCharacters(in: .whitespacesAndNewlines) 311 | 312 | var tokens = [String]() 313 | var state = State.Normal 314 | let delimeters = CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: "\"")) 315 | let quote = CharacterSet(charactersIn: "\"") 316 | 317 | let scanner = Scanner(string: theString) 318 | scanner.charactersToBeSkipped = .none 319 | 320 | while !scanner.isAtEnd { 321 | if state == .Normal { 322 | if let token = scanner.scanCharacters(from: delimeters.inverted) { 323 | tokens.append(token.trimmingCharacters(in: .whitespacesAndNewlines)) 324 | } else if let delims = scanner.scanCharacters(from: delimeters) { 325 | if delims.hasSuffix("\"") { 326 | state = .InsideAQuote 327 | } 328 | } 329 | } else { 330 | if let token = scanner.scanCharacters(from: quote.inverted) { 331 | tokens.append(token.trimmingCharacters(in: .whitespacesAndNewlines)) 332 | state = .Normal 333 | } 334 | } 335 | } 336 | 337 | return tokens 338 | } 339 | 340 | func number() -> NSNumber? { 341 | let amountStr = self.trimmingCharacters(in: .whitespacesAndNewlines) 342 | 343 | let nf = NumberFormatter() 344 | 345 | nf.numberStyle = .currency 346 | if let num = nf.number(from: amountStr) { 347 | return num 348 | } 349 | 350 | nf.numberStyle = .none 351 | if let num = nf.number(from: amountStr) { 352 | return num 353 | } 354 | 355 | return nil 356 | } 357 | 358 | func formatAsCurrency() -> String? { 359 | return number()?.doubleValue.formatAsCurrency() 360 | } 361 | 362 | func substring(to : Int) -> String { 363 | let toIndex = self.index(self.startIndex, offsetBy: to) 364 | return String(self[...toIndex]) 365 | } 366 | 367 | func substring(from : Int) -> String { 368 | let fromIndex = self.index(self.startIndex, offsetBy: from) 369 | return String(self[fromIndex...]) 370 | } 371 | 372 | func substring(_ r: Range) -> String { 373 | let fromIndex = self.index(self.startIndex, offsetBy: r.lowerBound) 374 | let toIndex = self.index(self.startIndex, offsetBy: r.upperBound) 375 | let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) 376 | return String(self[indexRange]) 377 | } 378 | 379 | func character(_ at: Int) -> Character { 380 | return self[self.index(self.startIndex, offsetBy: at)] 381 | } 382 | 383 | func lastIndexOfCharacter(_ c: Character) -> Int? { 384 | guard let index = range(of: String(c), options: .backwards)?.lowerBound else { return nil } 385 | return distance(from: startIndex, to: index) 386 | } 387 | 388 | func deletingPrefix(_ prefix: String) -> String { 389 | guard self.hasPrefix(prefix) else { return self } 390 | return String(self.dropFirst(prefix.count)) 391 | } 392 | 393 | func deletingSuffix(_ suffix: String) -> String { 394 | guard self.hasSuffix(suffix) else { return self } 395 | return String(self.dropLast(suffix.count)) 396 | } 397 | 398 | func lowerTrimmed() -> String { 399 | return self.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) 400 | } 401 | 402 | func keyVal() -> (String, String?)? { 403 | if let colonIndex = self.firstIndex(of: ":") { 404 | let key = self[.. [[String]] { 419 | guard let regex = try? NSRegularExpression(pattern: regex, options: [.dotMatchesLineSeparators]) else { return [] } 420 | let nsString = self as NSString 421 | let results = regex.matches(in: self, options: [.withoutAnchoringBounds], range: NSMakeRange(0, nsString.length)) 422 | return results.map { result in 423 | (0.. String? { 433 | guard let leftRange = range(of: left) else { return nil } 434 | guard let rightRange = range(of: right, options: .backwards) else { return nil } 435 | guard leftRange.upperBound <= rightRange.lowerBound else { return nil } 436 | 437 | let sub = self[leftRange.upperBound...] 438 | let closestToLeftRange = sub.range(of: right)! 439 | return String(sub[..) -> NSRange { 443 | guard let lower = UTF16View.Index(range.lowerBound, within: utf16) else { return .init() } 444 | guard let upper = UTF16View.Index(range.upperBound, within: utf16) else { return .init() } 445 | return NSRange(location: utf16.distance(from: utf16.startIndex, to: lower), length: utf16.distance(from: lower, to: upper)) 446 | } 447 | 448 | func lemmatize() -> [String] { 449 | let tagger = NSLinguisticTagger(tagSchemes: [.lemma], options: 0) 450 | tagger.string = self 451 | let range = NSMakeRange(0, self.utf16.count) 452 | let options: NSLinguisticTagger.Options = [.omitWhitespace, .omitPunctuation] 453 | 454 | var results = [String]() 455 | tagger.enumerateTags(in: range, unit: .word, scheme: .lemma, options: options) { (tag, tokenRange, stop) in 456 | if let lemma = tag?.rawValue { 457 | results.append(lemma) 458 | } 459 | } 460 | 461 | return (results.count > 0) ? [self] : results 462 | } 463 | } 464 | 465 | extension String { 466 | struct Summary { 467 | var characters = 0 468 | var nonWhitespaceCharacters = 0 469 | var words = 0 470 | var sentences = 0 471 | var lines = 0 472 | var averageReadingTime: TimeInterval = 0 473 | var aloudReadingTime: TimeInterval = 0 474 | } 475 | 476 | var summary: Summary { 477 | var s = Summary(characters: countCharacters(), 478 | nonWhitespaceCharacters: countNonWhitespaceCharacters(), 479 | words: countWords(), 480 | sentences: countSentences(), 481 | lines: countLines()) 482 | s.averageReadingTime = calculateAverageReadingTime(words: s.words) 483 | s.aloudReadingTime = calculateAloudReadingTime(words: s.words) 484 | return s 485 | } 486 | 487 | // https://digest.bps.org.uk/2019/06/13/most-comprehensive-review-to-date-suggests-the-average-persons-reading-speed-is-slower-than-commonly-thought/ 488 | func calculateAverageReadingTime(words: Int) -> TimeInterval { 489 | return TimeInterval(words) / 238.0 * 60.0 490 | } 491 | 492 | // https://www.visualthesaurus.com/cm/wc/seven-ways-to-write-a-better-speech/ 493 | func calculateAloudReadingTime(words: Int) -> TimeInterval { 494 | return TimeInterval(words) / 125.0 * 60.0 495 | } 496 | 497 | func countCharacters() -> Int { 498 | return count 499 | } 500 | 501 | func countNonWhitespaceCharacters() -> Int { 502 | components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().count 503 | } 504 | 505 | func countWords() -> Int { 506 | let words = components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty } 507 | return words.count 508 | } 509 | 510 | func countSentences() -> Int { 511 | let s = self 512 | var r = [Range]() 513 | let t = s.linguisticTags(in: s.startIndex.. Int { 526 | return components(separatedBy: .newlines).count 527 | } 528 | } 529 | 530 | extension TimeInterval { 531 | // let foo: TimeInterval = 6227 532 | // foo.durationString() -> "2h" 533 | // foo.durationString(2) -> "1h 44m" 534 | // foo.durationString(3) -> "1h 43m 47s" 535 | func durationString() -> String { 536 | let formatter = DateComponentsFormatter() 537 | formatter.allowedUnits = [.day, .hour, .minute, .second] 538 | formatter.unitsStyle = .abbreviated 539 | formatter.maximumUnitCount = 1 540 | return formatter.string(from: self)! 541 | } 542 | } 543 | 544 | extension URL { 545 | var isAccessibleFile: Bool { 546 | var isDir: ObjCBool = false 547 | let exists = FileManager.default.fileExists(atPath: self.path, isDirectory: &isDir) 548 | if !exists || isDir.boolValue { 549 | return false 550 | } 551 | return FileManager.default.isReadableFile(atPath: self.path) 552 | } 553 | 554 | var isAccessibleDirectory: Bool { 555 | var isDir: ObjCBool = false 556 | let exists = FileManager.default.fileExists(atPath: self.path, isDirectory: &isDir) 557 | if !exists || !isDir.boolValue { 558 | return false 559 | } 560 | return FileManager.default.isReadableFile(atPath: self.path) 561 | } 562 | 563 | func dumbGET(_ results: ((Data?) -> ())? = nil) { 564 | DispatchQueue.global(qos: .userInitiated).async { 565 | do { 566 | let data = try Data(contentsOf: self) 567 | results?(data) 568 | } catch { 569 | results?(nil) 570 | } 571 | } 572 | } 573 | } 574 | -------------------------------------------------------------------------------- /Capture.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 57C7AC60AA9E46BA1643DF39 /* Pods_Capture.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AC90FC257387B5EC28985B9 /* Pods_Capture.framework */; }; 11 | C636F6BD2747D53A00731E9F /* Extensions-macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C636F6B92747D53A00731E9F /* Extensions-macOS.swift */; }; 12 | C636F6BE2747D53A00731E9F /* Extensions-Geometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C636F6BB2747D53A00731E9F /* Extensions-Geometry.swift */; }; 13 | C636F6BF2747D53A00731E9F /* Extensions-Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = C636F6BC2747D53A00731E9F /* Extensions-Shared.swift */; }; 14 | C636F6FD2747ED6A00731E9F /* PrefsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C636F6FC2747ED6A00731E9F /* PrefsWindowController.swift */; }; 15 | C636F6FF2747ED8700731E9F /* PrefsWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C636F6FE2747ED8700731E9F /* PrefsWindowController.xib */; }; 16 | C6672AF927475FB000D616B6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6672AF827475FB000D616B6 /* AppDelegate.swift */; }; 17 | C6672AFB27475FB100D616B6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C6672AFA27475FB100D616B6 /* Assets.xcassets */; }; 18 | C6672AFE27475FB100D616B6 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = C6672AFC27475FB100D616B6 /* MainMenu.xib */; }; 19 | C6672B0E2747605500D616B6 /* CaptureWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6672B0D2747605500D616B6 /* CaptureWindowController.swift */; }; 20 | C6672B102747607600D616B6 /* CaptureWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C6672B0F2747607600D616B6 /* CaptureWindowController.xib */; }; 21 | C6672B12274767E700D616B6 /* CaptureThing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6672B11274767E700D616B6 /* CaptureThing.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 4AC90FC257387B5EC28985B9 /* Pods_Capture.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Capture.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | 6606ACB6A864F72E01DA4F21 /* Pods-Capture.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Capture.release.xcconfig"; path = "Target Support Files/Pods-Capture/Pods-Capture.release.xcconfig"; sourceTree = ""; }; 27 | 9CF0C42B8EE8BB2B4E5C7006 /* Pods-Capture.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Capture.debug.xcconfig"; path = "Target Support Files/Pods-Capture/Pods-Capture.debug.xcconfig"; sourceTree = ""; }; 28 | C636F6B92747D53A00731E9F /* Extensions-macOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Extensions-macOS.swift"; sourceTree = ""; }; 29 | C636F6BB2747D53A00731E9F /* Extensions-Geometry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Extensions-Geometry.swift"; sourceTree = ""; }; 30 | C636F6BC2747D53A00731E9F /* Extensions-Shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Extensions-Shared.swift"; sourceTree = ""; }; 31 | C636F6FA2747D7CA00731E9F /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; 32 | C636F6FC2747ED6A00731E9F /* PrefsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsWindowController.swift; sourceTree = ""; }; 33 | C636F6FE2747ED8700731E9F /* PrefsWindowController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PrefsWindowController.xib; sourceTree = ""; }; 34 | C6672AF527475FB000D616B6 /* Capture.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Capture.app; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | C6672AF827475FB000D616B6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 36 | C6672AFA27475FB100D616B6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | C6672AFD27475FB100D616B6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 38 | C6672AFF27475FB100D616B6 /* Capture.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Capture.entitlements; sourceTree = ""; }; 39 | C6672B0D2747605500D616B6 /* CaptureWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureWindowController.swift; sourceTree = ""; }; 40 | C6672B0F2747607600D616B6 /* CaptureWindowController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CaptureWindowController.xib; sourceTree = ""; }; 41 | C6672B11274767E700D616B6 /* CaptureThing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureThing.swift; sourceTree = ""; }; 42 | C6672B132747774D00D616B6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXFrameworksBuildPhase section */ 46 | C6672AF227475FB000D616B6 /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | 57C7AC60AA9E46BA1643DF39 /* Pods_Capture.framework in Frameworks */, 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | /* End PBXFrameworksBuildPhase section */ 55 | 56 | /* Begin PBXGroup section */ 57 | 661301BB148B2A98123B6650 /* Pods */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 9CF0C42B8EE8BB2B4E5C7006 /* Pods-Capture.debug.xcconfig */, 61 | 6606ACB6A864F72E01DA4F21 /* Pods-Capture.release.xcconfig */, 62 | ); 63 | path = Pods; 64 | sourceTree = ""; 65 | }; 66 | C24EC9F6EC71B867B6012736 /* Frameworks */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | 4AC90FC257387B5EC28985B9 /* Pods_Capture.framework */, 70 | ); 71 | name = Frameworks; 72 | sourceTree = ""; 73 | }; 74 | C636F6B82747D53A00731E9F /* macOS */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | C636F6B92747D53A00731E9F /* Extensions-macOS.swift */, 78 | ); 79 | path = macOS; 80 | sourceTree = ""; 81 | }; 82 | C636F6BA2747D53A00731E9F /* Shared */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | C636F6BB2747D53A00731E9F /* Extensions-Geometry.swift */, 86 | C636F6BC2747D53A00731E9F /* Extensions-Shared.swift */, 87 | ); 88 | path = Shared; 89 | sourceTree = ""; 90 | }; 91 | C6672AEC27475FB000D616B6 = { 92 | isa = PBXGroup; 93 | children = ( 94 | C6672AF727475FB000D616B6 /* Capture */, 95 | C6672AF627475FB000D616B6 /* Products */, 96 | 661301BB148B2A98123B6650 /* Pods */, 97 | C24EC9F6EC71B867B6012736 /* Frameworks */, 98 | ); 99 | sourceTree = ""; 100 | }; 101 | C6672AF627475FB000D616B6 /* Products */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | C6672AF527475FB000D616B6 /* Capture.app */, 105 | ); 106 | name = Products; 107 | sourceTree = ""; 108 | }; 109 | C6672AF727475FB000D616B6 /* Capture */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | C6672AF827475FB000D616B6 /* AppDelegate.swift */, 113 | C6672B11274767E700D616B6 /* CaptureThing.swift */, 114 | C6EC9C5A274BF1A1001765F9 /* Controllers */, 115 | C6EC9C5B274BF1B7001765F9 /* Extensions */, 116 | C6672AFA27475FB100D616B6 /* Assets.xcassets */, 117 | C6672AFC27475FB100D616B6 /* MainMenu.xib */, 118 | C6672AFF27475FB100D616B6 /* Capture.entitlements */, 119 | C6672B132747774D00D616B6 /* Info.plist */, 120 | C636F6FA2747D7CA00731E9F /* BridgingHeader.h */, 121 | ); 122 | path = Capture; 123 | sourceTree = ""; 124 | }; 125 | C6EC9C5A274BF1A1001765F9 /* Controllers */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | C6672B0D2747605500D616B6 /* CaptureWindowController.swift */, 129 | C6672B0F2747607600D616B6 /* CaptureWindowController.xib */, 130 | C636F6FC2747ED6A00731E9F /* PrefsWindowController.swift */, 131 | C636F6FE2747ED8700731E9F /* PrefsWindowController.xib */, 132 | ); 133 | path = Controllers; 134 | sourceTree = ""; 135 | }; 136 | C6EC9C5B274BF1B7001765F9 /* Extensions */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | C636F6B82747D53A00731E9F /* macOS */, 140 | C636F6BA2747D53A00731E9F /* Shared */, 141 | ); 142 | path = Extensions; 143 | sourceTree = ""; 144 | }; 145 | /* End PBXGroup section */ 146 | 147 | /* Begin PBXNativeTarget section */ 148 | C6672AF427475FB000D616B6 /* Capture */ = { 149 | isa = PBXNativeTarget; 150 | buildConfigurationList = C6672B0227475FB100D616B6 /* Build configuration list for PBXNativeTarget "Capture" */; 151 | buildPhases = ( 152 | 3A9AE67943FE16424E35D42C /* [CP] Check Pods Manifest.lock */, 153 | C6672AF127475FB000D616B6 /* Sources */, 154 | C6672AF227475FB000D616B6 /* Frameworks */, 155 | C6672AF327475FB000D616B6 /* Resources */, 156 | BFE9C85B6A0E6D8E40BE105D /* [CP] Embed Pods Frameworks */, 157 | ); 158 | buildRules = ( 159 | ); 160 | dependencies = ( 161 | ); 162 | name = Capture; 163 | productName = Capture; 164 | productReference = C6672AF527475FB000D616B6 /* Capture.app */; 165 | productType = "com.apple.product-type.application"; 166 | }; 167 | /* End PBXNativeTarget section */ 168 | 169 | /* Begin PBXProject section */ 170 | C6672AED27475FB000D616B6 /* Project object */ = { 171 | isa = PBXProject; 172 | attributes = { 173 | BuildIndependentTargetsInParallel = 1; 174 | LastSwiftUpdateCheck = 1310; 175 | LastUpgradeCheck = 1310; 176 | TargetAttributes = { 177 | C6672AF427475FB000D616B6 = { 178 | CreatedOnToolsVersion = 13.1; 179 | }; 180 | }; 181 | }; 182 | buildConfigurationList = C6672AF027475FB000D616B6 /* Build configuration list for PBXProject "Capture" */; 183 | compatibilityVersion = "Xcode 13.0"; 184 | developmentRegion = en; 185 | hasScannedForEncodings = 0; 186 | knownRegions = ( 187 | en, 188 | Base, 189 | de, 190 | "zh-Hans", 191 | ja, 192 | es, 193 | it, 194 | sv, 195 | cs, 196 | ko, 197 | "zh-Hant", 198 | pl, 199 | ru, 200 | fr, 201 | nl, 202 | pt, 203 | ); 204 | mainGroup = C6672AEC27475FB000D616B6; 205 | productRefGroup = C6672AF627475FB000D616B6 /* Products */; 206 | projectDirPath = ""; 207 | projectRoot = ""; 208 | targets = ( 209 | C6672AF427475FB000D616B6 /* Capture */, 210 | ); 211 | }; 212 | /* End PBXProject section */ 213 | 214 | /* Begin PBXResourcesBuildPhase section */ 215 | C6672AF327475FB000D616B6 /* Resources */ = { 216 | isa = PBXResourcesBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | C6672B102747607600D616B6 /* CaptureWindowController.xib in Resources */, 220 | C6672AFB27475FB100D616B6 /* Assets.xcassets in Resources */, 221 | C636F6FF2747ED8700731E9F /* PrefsWindowController.xib in Resources */, 222 | C6672AFE27475FB100D616B6 /* MainMenu.xib in Resources */, 223 | ); 224 | runOnlyForDeploymentPostprocessing = 0; 225 | }; 226 | /* End PBXResourcesBuildPhase section */ 227 | 228 | /* Begin PBXShellScriptBuildPhase section */ 229 | 3A9AE67943FE16424E35D42C /* [CP] Check Pods Manifest.lock */ = { 230 | isa = PBXShellScriptBuildPhase; 231 | buildActionMask = 2147483647; 232 | files = ( 233 | ); 234 | inputFileListPaths = ( 235 | ); 236 | inputPaths = ( 237 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 238 | "${PODS_ROOT}/Manifest.lock", 239 | ); 240 | name = "[CP] Check Pods Manifest.lock"; 241 | outputFileListPaths = ( 242 | ); 243 | outputPaths = ( 244 | "$(DERIVED_FILE_DIR)/Pods-Capture-checkManifestLockResult.txt", 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | shellPath = /bin/sh; 248 | 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# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 249 | showEnvVarsInLog = 0; 250 | }; 251 | BFE9C85B6A0E6D8E40BE105D /* [CP] Embed Pods Frameworks */ = { 252 | isa = PBXShellScriptBuildPhase; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | ); 256 | inputFileListPaths = ( 257 | "${PODS_ROOT}/Target Support Files/Pods-Capture/Pods-Capture-frameworks-${CONFIGURATION}-input-files.xcfilelist", 258 | ); 259 | name = "[CP] Embed Pods Frameworks"; 260 | outputFileListPaths = ( 261 | "${PODS_ROOT}/Target Support Files/Pods-Capture/Pods-Capture-frameworks-${CONFIGURATION}-output-files.xcfilelist", 262 | ); 263 | runOnlyForDeploymentPostprocessing = 0; 264 | shellPath = /bin/sh; 265 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Capture/Pods-Capture-frameworks.sh\"\n"; 266 | showEnvVarsInLog = 0; 267 | }; 268 | /* End PBXShellScriptBuildPhase section */ 269 | 270 | /* Begin PBXSourcesBuildPhase section */ 271 | C6672AF127475FB000D616B6 /* Sources */ = { 272 | isa = PBXSourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | C6672AF927475FB000D616B6 /* AppDelegate.swift in Sources */, 276 | C636F6BD2747D53A00731E9F /* Extensions-macOS.swift in Sources */, 277 | C636F6FD2747ED6A00731E9F /* PrefsWindowController.swift in Sources */, 278 | C636F6BF2747D53A00731E9F /* Extensions-Shared.swift in Sources */, 279 | C636F6BE2747D53A00731E9F /* Extensions-Geometry.swift in Sources */, 280 | C6672B12274767E700D616B6 /* CaptureThing.swift in Sources */, 281 | C6672B0E2747605500D616B6 /* CaptureWindowController.swift in Sources */, 282 | ); 283 | runOnlyForDeploymentPostprocessing = 0; 284 | }; 285 | /* End PBXSourcesBuildPhase section */ 286 | 287 | /* Begin PBXVariantGroup section */ 288 | C6672AFC27475FB100D616B6 /* MainMenu.xib */ = { 289 | isa = PBXVariantGroup; 290 | children = ( 291 | C6672AFD27475FB100D616B6 /* Base */, 292 | ); 293 | name = MainMenu.xib; 294 | sourceTree = ""; 295 | }; 296 | /* End PBXVariantGroup section */ 297 | 298 | /* Begin XCBuildConfiguration section */ 299 | C6672B0027475FB100D616B6 /* Debug */ = { 300 | isa = XCBuildConfiguration; 301 | buildSettings = { 302 | ALWAYS_SEARCH_USER_PATHS = NO; 303 | CLANG_ANALYZER_NONNULL = YES; 304 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 305 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 306 | CLANG_CXX_LIBRARY = "libc++"; 307 | CLANG_ENABLE_MODULES = YES; 308 | CLANG_ENABLE_OBJC_ARC = YES; 309 | CLANG_ENABLE_OBJC_WEAK = YES; 310 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 311 | CLANG_WARN_BOOL_CONVERSION = YES; 312 | CLANG_WARN_COMMA = YES; 313 | CLANG_WARN_CONSTANT_CONVERSION = YES; 314 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 315 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 316 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 317 | CLANG_WARN_EMPTY_BODY = YES; 318 | CLANG_WARN_ENUM_CONVERSION = YES; 319 | CLANG_WARN_INFINITE_RECURSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 323 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 325 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 326 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 327 | CLANG_WARN_STRICT_PROTOTYPES = YES; 328 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 329 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 330 | CLANG_WARN_UNREACHABLE_CODE = YES; 331 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 332 | COPY_PHASE_STRIP = NO; 333 | DEBUG_INFORMATION_FORMAT = dwarf; 334 | ENABLE_STRICT_OBJC_MSGSEND = YES; 335 | ENABLE_TESTABILITY = YES; 336 | GCC_C_LANGUAGE_STANDARD = gnu11; 337 | GCC_DYNAMIC_NO_PIC = NO; 338 | GCC_NO_COMMON_BLOCKS = YES; 339 | GCC_OPTIMIZATION_LEVEL = 0; 340 | GCC_PREPROCESSOR_DEFINITIONS = ( 341 | "DEBUG=1", 342 | "$(inherited)", 343 | ); 344 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 345 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 346 | GCC_WARN_UNDECLARED_SELECTOR = YES; 347 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 348 | GCC_WARN_UNUSED_FUNCTION = YES; 349 | GCC_WARN_UNUSED_VARIABLE = YES; 350 | MACOSX_DEPLOYMENT_TARGET = 12.0; 351 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 352 | MTL_FAST_MATH = YES; 353 | ONLY_ACTIVE_ARCH = YES; 354 | SDKROOT = macosx; 355 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 356 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 357 | }; 358 | name = Debug; 359 | }; 360 | C6672B0127475FB100D616B6 /* Release */ = { 361 | isa = XCBuildConfiguration; 362 | buildSettings = { 363 | ALWAYS_SEARCH_USER_PATHS = NO; 364 | CLANG_ANALYZER_NONNULL = YES; 365 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 366 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 367 | CLANG_CXX_LIBRARY = "libc++"; 368 | CLANG_ENABLE_MODULES = YES; 369 | CLANG_ENABLE_OBJC_ARC = YES; 370 | CLANG_ENABLE_OBJC_WEAK = YES; 371 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 372 | CLANG_WARN_BOOL_CONVERSION = YES; 373 | CLANG_WARN_COMMA = YES; 374 | CLANG_WARN_CONSTANT_CONVERSION = YES; 375 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 376 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 377 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 378 | CLANG_WARN_EMPTY_BODY = YES; 379 | CLANG_WARN_ENUM_CONVERSION = YES; 380 | CLANG_WARN_INFINITE_RECURSION = YES; 381 | CLANG_WARN_INT_CONVERSION = YES; 382 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 383 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 384 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 385 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 386 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 387 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 388 | CLANG_WARN_STRICT_PROTOTYPES = YES; 389 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 390 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 391 | CLANG_WARN_UNREACHABLE_CODE = YES; 392 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 393 | COPY_PHASE_STRIP = NO; 394 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 395 | ENABLE_NS_ASSERTIONS = NO; 396 | ENABLE_STRICT_OBJC_MSGSEND = YES; 397 | GCC_C_LANGUAGE_STANDARD = gnu11; 398 | GCC_NO_COMMON_BLOCKS = YES; 399 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 400 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 401 | GCC_WARN_UNDECLARED_SELECTOR = YES; 402 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 403 | GCC_WARN_UNUSED_FUNCTION = YES; 404 | GCC_WARN_UNUSED_VARIABLE = YES; 405 | MACOSX_DEPLOYMENT_TARGET = 12.0; 406 | MTL_ENABLE_DEBUG_INFO = NO; 407 | MTL_FAST_MATH = YES; 408 | SDKROOT = macosx; 409 | SWIFT_COMPILATION_MODE = wholemodule; 410 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 411 | }; 412 | name = Release; 413 | }; 414 | C6672B0327475FB100D616B6 /* Debug */ = { 415 | isa = XCBuildConfiguration; 416 | baseConfigurationReference = 9CF0C42B8EE8BB2B4E5C7006 /* Pods-Capture.debug.xcconfig */; 417 | buildSettings = { 418 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 419 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 420 | CODE_SIGN_ENTITLEMENTS = Capture/Capture.entitlements; 421 | CODE_SIGN_STYLE = Automatic; 422 | COMBINE_HIDPI_IMAGES = YES; 423 | CURRENT_PROJECT_VERSION = 2; 424 | DEVELOPMENT_TEAM = 3A6K89K388; 425 | ENABLE_HARDENED_RUNTIME = YES; 426 | GENERATE_INFOPLIST_FILE = NO; 427 | INFOPLIST_FILE = Capture/Info.plist; 428 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; 429 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 430 | INFOPLIST_KEY_NSMainNibFile = MainMenu; 431 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 432 | LD_RUNPATH_SEARCH_PATHS = ( 433 | "$(inherited)", 434 | "@executable_path/../Frameworks", 435 | ); 436 | MARKETING_VERSION = 1.0; 437 | PRODUCT_BUNDLE_IDENTIFIER = studio.retina.Capture; 438 | PRODUCT_NAME = "$(TARGET_NAME)"; 439 | SWIFT_EMIT_LOC_STRINGS = YES; 440 | SWIFT_OBJC_BRIDGING_HEADER = Capture/BridgingHeader.h; 441 | SWIFT_VERSION = 5.0; 442 | }; 443 | name = Debug; 444 | }; 445 | C6672B0427475FB100D616B6 /* Release */ = { 446 | isa = XCBuildConfiguration; 447 | baseConfigurationReference = 6606ACB6A864F72E01DA4F21 /* Pods-Capture.release.xcconfig */; 448 | buildSettings = { 449 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 450 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 451 | CODE_SIGN_ENTITLEMENTS = Capture/Capture.entitlements; 452 | CODE_SIGN_STYLE = Automatic; 453 | COMBINE_HIDPI_IMAGES = YES; 454 | CURRENT_PROJECT_VERSION = 2; 455 | DEVELOPMENT_TEAM = 3A6K89K388; 456 | ENABLE_HARDENED_RUNTIME = YES; 457 | GENERATE_INFOPLIST_FILE = NO; 458 | INFOPLIST_FILE = Capture/Info.plist; 459 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; 460 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 461 | INFOPLIST_KEY_NSMainNibFile = MainMenu; 462 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 463 | LD_RUNPATH_SEARCH_PATHS = ( 464 | "$(inherited)", 465 | "@executable_path/../Frameworks", 466 | ); 467 | MARKETING_VERSION = 1.0; 468 | PRODUCT_BUNDLE_IDENTIFIER = studio.retina.Capture; 469 | PRODUCT_NAME = "$(TARGET_NAME)"; 470 | SWIFT_EMIT_LOC_STRINGS = YES; 471 | SWIFT_OBJC_BRIDGING_HEADER = Capture/BridgingHeader.h; 472 | SWIFT_VERSION = 5.0; 473 | }; 474 | name = Release; 475 | }; 476 | /* End XCBuildConfiguration section */ 477 | 478 | /* Begin XCConfigurationList section */ 479 | C6672AF027475FB000D616B6 /* Build configuration list for PBXProject "Capture" */ = { 480 | isa = XCConfigurationList; 481 | buildConfigurations = ( 482 | C6672B0027475FB100D616B6 /* Debug */, 483 | C6672B0127475FB100D616B6 /* Release */, 484 | ); 485 | defaultConfigurationIsVisible = 0; 486 | defaultConfigurationName = Release; 487 | }; 488 | C6672B0227475FB100D616B6 /* Build configuration list for PBXNativeTarget "Capture" */ = { 489 | isa = XCConfigurationList; 490 | buildConfigurations = ( 491 | C6672B0327475FB100D616B6 /* Debug */, 492 | C6672B0427475FB100D616B6 /* Release */, 493 | ); 494 | defaultConfigurationIsVisible = 0; 495 | defaultConfigurationName = Release; 496 | }; 497 | /* End XCConfigurationList section */ 498 | }; 499 | rootObject = C6672AED27475FB000D616B6 /* Project object */; 500 | } 501 | -------------------------------------------------------------------------------- /Capture/Controllers/CaptureWindowController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 89 | 90 | 91 | 92 | 93 | 94 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 164 | 168 | 169 | 170 | 171 | 172 | 179 | 180 | 181 | 182 | 192 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /Capture/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | --------------------------------------------------------------------------------