├── .gitignore ├── Makefile ├── README.md └── pbv.swift /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | INSTALL ?= install 2 | prefix ?= /usr/local 3 | bindir ?= $(prefix)/bin 4 | CFLAGS := -O 5 | 6 | all: bin/pbv 7 | 8 | bin/pbv: $(wildcard *.swift) 9 | @mkdir -p $(@D) 10 | xcrun -sdk macosx swiftc $+ $(CFLAGS) -o $@ 11 | 12 | install: bin/pbv 13 | $(INSTALL) $< $(DESTDIR)$(bindir) 14 | 15 | uninstall: 16 | rm -f $(DESTDIR)$(bindir)/pbv 17 | 18 | clean: 19 | rm -f bin/pbv 20 | 21 | test: 22 | swiftformat --lint *.swift 23 | swiftlint lint *.swift 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # macos-pasteboard 2 | 3 | Like macOS's built-in `pbpaste` but more flexible and raw. 4 | 5 | 6 | ## Install 7 | 8 | ```sh 9 | git clone https://github.com/chbrown/macos-pasteboard 10 | cd macos-pasteboard 11 | make install 12 | ``` 13 | 14 | 15 | ## Example 16 | 17 | In a web browser (I'm using Safari), highlight and copy the "macos-pasteboard" heading at the top of this README. 18 | 19 | `pbv --help` lists the types supported by the current contents of the clipboard. The available types depend not only on the type of object you copied, but also on which program you copied it within. 20 | 21 | ```console 22 | $ pbv --help 23 | Usage: pbv [-h|--help] 24 | pbv [dataType [dataType [...]]] [-s|--stream] 25 | 26 | Read contents of pasteboard as 'dataType'. If multiple types are specified, 27 | tries each from left to right, stopping at first success. If omitted, 28 | defaults to 'public.utf8-plain-text'. 29 | 30 | Options: 31 | -h|--help Show this help and exit 32 | -s|--stream Start an infinite loop polling the Pasteboard 'changeCount', 33 | running as usual whenever it changes 34 | 35 | Available types for the 'Apple CFPasteboard general' pasteboard: 36 | dyn.ah62d4rv4gu8y63n2nuuhg5pbsm4ca6dbsr4gnkduqf31k3pcr7u1e3basv61a3k 37 | NeXT smart paste pasteboard type 38 | com.apple.webarchive 39 | Apple Web Archive pasteboard type 40 | public.rtf 41 | NeXT Rich Text Format v1.0 pasteboard type 42 | public.html 43 | Apple HTML pasteboard type 44 | public.utf8-plain-text 45 | NSStringPboardType 46 | com.apple.WebKit.custom-pasteboard-data 47 | public.utf16-external-plain-text 48 | CorePasteboardFlavorType 0x75743136 49 | dyn.ah62d4rv4gk81n65yru 50 | CorePasteboardFlavorType 0x7573746C 51 | com.apple.traditional-mac-plain-text 52 | CorePasteboardFlavorType 0x54455854 53 | dyn.ah62d4rv4gk81g7d3ru 54 | CorePasteboardFlavorType 0x7374796C 55 | ``` 56 | 57 | If you don't pass a data type, `pbv` will default to outputting the plain text version (the `public.utf8-plain-text` type): 58 | 59 | ```console 60 | $ pbv 61 | macos-pasteboard 62 | ``` 63 | 64 | But in this example we want the HTML version of what we copied: 65 | 66 | ```console 67 | $ pbv public.html 68 |

macos-pasteboard

69 | ``` 70 | 71 | Using [tidy](http://www.html-tidy.org/), 72 | [xmlstarlet](http://xmlstar.sourceforge.net/), and 73 | [kramdown](https://kramdown.gettalong.org/), 74 | we can convert this to Markdown without too much pain: 75 | 76 | ```console 77 | $ pbv public.html \ 78 | | tidy -quiet --show-warnings no -asxml -i -w 0 --quote-nbsp no \ 79 | | xmlstarlet ed -d //@id -d //@class -d //@rel -d //@style -d //@target -d //_:br -r //_:u -v span \ 80 | | xmlstarlet sel -i -t -c '/_:html/_:body/node()' \ 81 | | sed -e 's/ xmlns="[^[:space:]]*"//g' \ 82 | | kramdown -i html -o remove_html_tags,kramdown --line-width 9999 --remove-span-html-tags 83 | # macos-pasteboard 84 | ​ 85 | ``` 86 | 87 | 88 | ## License 89 | 90 | Copyright © 2016–2020 Christopher Brown. 91 | [MIT Licensed](https://chbrown.github.io/licenses/MIT/#2016-2020). 92 | -------------------------------------------------------------------------------- /pbv.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env swift 2 | import Cocoa 3 | import Foundation 4 | 5 | let newline = Data([0x0A] as [UInt8]) 6 | 7 | /** 8 | Write the given string to STDERR. 9 | 10 | - parameter str: native string to write encode in utf-8. 11 | - parameter appendNewline: whether or not to write a newline (U+000A) after the given string (defaults to true) 12 | */ 13 | func printErr(_ str: String, appendNewline: Bool = true) { 14 | // writing to STDERR takes a bit of boilerplate, compared to print() 15 | if let data = str.data(using: .utf8) { 16 | FileHandle.standardError.write(data) 17 | if appendNewline { 18 | FileHandle.standardError.write(newline) 19 | } 20 | } 21 | } 22 | 23 | /** 24 | Read Data from NSPasteboard. 25 | 26 | - parameter pasteboard: specific pasteboard to read from 27 | - parameter dataTypeName: name of pasteboard data type to read as 28 | 29 | - throws: `NSError` if data cannot be read as given dataType 30 | */ 31 | func pasteboardData(_ pasteboard: NSPasteboard, dataTypeName: String) throws -> Data { 32 | let dataType = NSPasteboard.PasteboardType(rawValue: dataTypeName) 33 | if let string = pasteboard.string(forType: dataType) { 34 | let data = string.data(using: .utf8)! // supposedly force-unwrapping using UTF-8 never fails 35 | return data 36 | } 37 | if let data = pasteboard.data(forType: dataType) { 38 | return data 39 | } 40 | throw NSError( 41 | domain: "pbv", 42 | code: 0, 43 | userInfo: [ 44 | NSLocalizedDescriptionKey: "Could not access pasteboard contents as String or Data for type: '\(dataTypeName)'", 45 | ] 46 | ) 47 | } 48 | 49 | /** 50 | Read Data from NSPasteboard, trying each dataType in turn. 51 | 52 | - parameter pasteboard: specific pasteboard to read from 53 | - parameter dataTypeNames: names of pasteboard data type to try reading as 54 | 55 | - throws: `NSError` if data cannot be read as _any_ of the given dataTypes 56 | */ 57 | func bestPasteboardData(_ pasteboard: NSPasteboard, dataTypeNames: [String]) throws -> Data { 58 | for dataTypeName in dataTypeNames { 59 | if let data = try? pasteboardData(pasteboard, dataTypeName: dataTypeName) { 60 | return data 61 | } 62 | } 63 | let dataTypeNamesJoined = dataTypeNames.map { "'\($0)'" }.joined(separator: ", ") 64 | throw NSError( 65 | domain: "pbv", 66 | code: 0, 67 | userInfo: [ 68 | NSLocalizedDescriptionKey: "Could not access pasteboard contents as String or Data for types: \(dataTypeNamesJoined)", 69 | ] 70 | ) 71 | } 72 | 73 | func streamBestPasteboardData(_ pasteboard: NSPasteboard, dataTypeNames: [String], timeInterval: TimeInterval = 0.1) -> Timer { 74 | var lastChangeCount = pasteboard.changeCount 75 | 76 | return Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true) { _ in 77 | let changeCount = pasteboard.changeCount 78 | if changeCount != lastChangeCount { 79 | printErr("#\(changeCount)") 80 | lastChangeCount = changeCount 81 | do { 82 | let data = try bestPasteboardData(pasteboard, dataTypeNames: dataTypeNames) 83 | FileHandle.standardOutput.write(data) 84 | } catch { 85 | printErr(error.localizedDescription) 86 | } 87 | } 88 | } 89 | } 90 | 91 | func printTypes(_ pasteboard: NSPasteboard) { 92 | printErr("Available types for the '\(pasteboard.name.rawValue)' pasteboard:") 93 | // Apple documentation says `types` "is an array NSString objects", 94 | // but that's wrong: they are PasteboardType structures. 95 | if let types = pasteboard.types { 96 | for type in types { 97 | printErr(" \(type.rawValue)") 98 | } 99 | } else { 100 | printErr(" (not available)") 101 | } 102 | } 103 | 104 | // CLI helpers 105 | 106 | private func basename(_ pathOption: String?) -> String? { 107 | if let path = pathOption { 108 | return URL(fileURLWithPath: path).lastPathComponent 109 | } 110 | return nil 111 | } 112 | 113 | private func printUsage(_ pasteboard: NSPasteboard) { 114 | let process = basename(CommandLine.arguments.first) ?? "pbv" 115 | printErr(""" 116 | Usage: \(process) [-h|--help] 117 | \(process) [dataType [dataType [...]]] [-s|--stream] 118 | 119 | Read contents of pasteboard as 'dataType'. If multiple types are specified, 120 | tries each from left to right, stopping at first success. If omitted, 121 | defaults to 'public.utf8-plain-text'. 122 | 123 | Options: 124 | -h|--help Show this help and exit 125 | -s|--stream Start an infinite loop polling the Pasteboard 'changeCount', 126 | running as usual whenever it changes 127 | 128 | """) 129 | printTypes(pasteboard) 130 | } 131 | 132 | // CLI entry point 133 | 134 | func main() { 135 | let pasteboard: NSPasteboard = .general 136 | 137 | // CommandLine.arguments[0] is the fullpath to this file 138 | // CommandLine.arguments[1+] should be the desired type(s) 139 | var args = Array(CommandLine.arguments.dropFirst()) 140 | if args.contains("-h") || args.contains("--help") { 141 | printUsage(pasteboard) 142 | exit(0) 143 | } 144 | 145 | let streamIndex = args.firstIndex { arg in arg == "-s" || arg == "--stream" } 146 | if let index = streamIndex { 147 | args.remove(at: index) 148 | } 149 | 150 | let types = args.isEmpty ? ["public.utf8-plain-text"] : args 151 | if streamIndex != nil { 152 | let timer = streamBestPasteboardData(pasteboard, dataTypeNames: types) 153 | 154 | let runLoop = RunLoop.main 155 | runLoop.add(timer, forMode: .default) 156 | runLoop.run() 157 | } else { 158 | do { 159 | let data = try bestPasteboardData(pasteboard, dataTypeNames: types) 160 | FileHandle.standardOutput.write(data) 161 | exit(0) 162 | } catch { 163 | printErr(error.localizedDescription) 164 | printTypes(pasteboard) 165 | exit(1) 166 | } 167 | } 168 | } 169 | 170 | main() 171 | --------------------------------------------------------------------------------