├── .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 |
--------------------------------------------------------------------------------