ace-support-mac-clipboard 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +61 -0
- data/LICENSE +21 -0
- data/README.md +29 -0
- data/Rakefile +14 -0
- data/lib/ace/support/mac_clipboard/content_parser.rb +166 -0
- data/lib/ace/support/mac_clipboard/content_type.rb +58 -0
- data/lib/ace/support/mac_clipboard/reader.rb +215 -0
- data/lib/ace/support/mac_clipboard/version.rb +9 -0
- data/lib/ace/support/mac_clipboard.rb +14 -0
- metadata +67 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c558611a5b584d6fcbfb84333bc45ac8cc0673af46f58608dcfa4b004d9d9e44
|
|
4
|
+
data.tar.gz: f09989c772c93bbbb5a19fae3ce2fcef7a26ab0473f2b3ffa1777bb2e0a3f3f2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 17c31bd56c10d82e28fe4ec291741e3452f331a6f0b9f0cf7ae3a558f551a13ed898db752200aed1a237ec5ab5fa1d778ce5670dd683167ea07f784f46bb0b57
|
|
7
|
+
data.tar.gz: 674909b85c99e490026d58b0ec6d8947254d3f1b3a4adc9326e88763b72608976612d513c1cc0b7b659386ea4993b868e5a40940cb71f4c04b87380c876b4b4b
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog][1], and this project adheres to [Semantic Versioning][2].
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-03-23
|
|
10
|
+
|
|
11
|
+
### Technical
|
|
12
|
+
- Removed phantom `handbook/**/*` glob from gemspec (no handbook directory exists).
|
|
13
|
+
|
|
14
|
+
## [0.2.3] - 2026-03-22
|
|
15
|
+
|
|
16
|
+
### Technical
|
|
17
|
+
- Corrected README integration guidance to reference `ace-idea` clipboard usage instead of `ace-taskflow`.
|
|
18
|
+
|
|
19
|
+
## [0.2.2] - 2026-03-22
|
|
20
|
+
|
|
21
|
+
### Technical
|
|
22
|
+
- Refreshed README structure with consistent tagline, overview, basic usage, and ACE project footer
|
|
23
|
+
|
|
24
|
+
## [0.2.1] - 2026-02-12
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Guard `require "ace/support/mac_clipboard"` behind platform check to prevent load errors on non-macOS
|
|
28
|
+
- Skip all tests gracefully on non-macOS platforms instead of failing
|
|
29
|
+
|
|
30
|
+
## [0.2.0] - 2026-01-03
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- **BREAKING**: Minimum Ruby version raised to 3.3.0 (was 3.1.0)
|
|
34
|
+
- Standardized gemspec file patterns with deterministic Dir.glob
|
|
35
|
+
- Added MIT LICENSE file
|
|
36
|
+
|
|
37
|
+
## [0.1.1] - 2025-11-11
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- Comprehensive test suite with smoke test pattern compatibility
|
|
41
|
+
- Test infrastructure for all core components (Error, ContentType, Reader, ContentParser)
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- Test discovery and execution issues with ace-test integration
|
|
45
|
+
- Module structure and constant loading verification tests
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- Update test structure to work with ace-test smoke pattern
|
|
49
|
+
- Improve test coverage for clipboard functionality
|
|
50
|
+
|
|
51
|
+
## 0.1.0 - 2025-10-13
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
- Initial release of ace-support-mac-clipboard
|
|
55
|
+
- Core clipboard functionality with FFI integration
|
|
56
|
+
- ContentType module for macOS clipboard type mappings
|
|
57
|
+
- Reader class for clipboard content access
|
|
58
|
+
- ContentParser class for data processing
|
|
59
|
+
|
|
60
|
+
[1]: https://keepachangelog.com/en/1.0.0/
|
|
61
|
+
[2]: https://semver.org/spec/v2.0.0.html
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Michal Czyz
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1> ACE - Support Mac Clipboard </h1>
|
|
3
|
+
|
|
4
|
+
macOS clipboard support for text, files, and image payloads used by ACE tools.
|
|
5
|
+
|
|
6
|
+
<img src="https://raw.githubusercontent.com/cs3b/ace/main/docs/brand/AgenticCodingEnvironment.Logo.XS.jpg" alt="ACE Logo" width="480">
|
|
7
|
+
<br><br>
|
|
8
|
+
|
|
9
|
+
<a href="https://rubygems.org/gems/ace-support-mac-clipboard"><img alt="Gem Version" src="https://img.shields.io/gem/v/ace-support-mac-clipboard.svg" /></a>
|
|
10
|
+
<a href="https://www.ruby-lang.org"><img alt="Ruby" src="https://img.shields.io/badge/Ruby-3.2+-CC342D?logo=ruby" /></a>
|
|
11
|
+
<a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg" /></a>
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
> Works with: Claude Code, Codex CLI, OpenCode, Gemini CLI, pi-agent, and more.
|
|
16
|
+
|
|
17
|
+
**macOS only.** `ace-support-mac-clipboard` integrates with macOS `NSPasteboard` so ACE tools can consume richer clipboard inputs than plain text. It handles screenshots, Finder file selections, and formatted content, presenting normalized Ruby structures to downstream packages.
|
|
18
|
+
|
|
19
|
+
## Use Cases
|
|
20
|
+
|
|
21
|
+
**Attach image and context content from clipboard** - support screenshot-based and file-based workflows in ACE tools like [ace-prompt-prep](../ace-prompt-prep) without manual file handling.
|
|
22
|
+
|
|
23
|
+
**Handle Finder selections and formatted text** - process files and rich content from macOS pasteboard without manual conversion steps.
|
|
24
|
+
|
|
25
|
+
**Keep platform details isolated** - encapsulate macOS-specific clipboard behavior in one package so the rest of ACE stays platform-neutral.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
Part of [ACE](https://github.com/cs3b/ace)
|
data/Rakefile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "minitest/test_task"
|
|
5
|
+
|
|
6
|
+
desc "Run tests using ace-test"
|
|
7
|
+
task :test do
|
|
8
|
+
sh "ace-test"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc "Run tests directly (CI mode)"
|
|
12
|
+
Minitest::TestTask.create(:ci)
|
|
13
|
+
|
|
14
|
+
task default: :test
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Support
|
|
7
|
+
module MacClipboard
|
|
8
|
+
class ContentParser
|
|
9
|
+
def self.parse(raw_result)
|
|
10
|
+
return {text: nil, attachments: []} unless raw_result[:success]
|
|
11
|
+
return {text: nil, attachments: []} if raw_result[:types].empty?
|
|
12
|
+
|
|
13
|
+
pasteboard = raw_result[:raw_pasteboard]
|
|
14
|
+
types = raw_result[:types]
|
|
15
|
+
|
|
16
|
+
text_parts = []
|
|
17
|
+
attachments = []
|
|
18
|
+
image_count = 0
|
|
19
|
+
|
|
20
|
+
# Classify all types
|
|
21
|
+
classified = types.map { |uti| [uti, ContentType.classify(uti)] }
|
|
22
|
+
|
|
23
|
+
# Process by priority
|
|
24
|
+
ContentType::PRIORITY_ORDER.each do |category|
|
|
25
|
+
relevant_utis = classified.select { |_uti, cat| cat == category }.map(&:first)
|
|
26
|
+
|
|
27
|
+
case category
|
|
28
|
+
when :files
|
|
29
|
+
# Read file URLs once (works for both public.file-url and NSFilenamesPboardType)
|
|
30
|
+
file_urls = Reader.read_file_urls(pasteboard)
|
|
31
|
+
file_urls.each do |path|
|
|
32
|
+
attachments << {
|
|
33
|
+
type: :file,
|
|
34
|
+
source_path: path,
|
|
35
|
+
filename: File.basename(path)
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
when :image
|
|
40
|
+
relevant_utis.each do |uti|
|
|
41
|
+
data = Reader.read_type(pasteboard, uti)
|
|
42
|
+
next unless data && data.bytesize > 0
|
|
43
|
+
|
|
44
|
+
image_count += 1
|
|
45
|
+
format = ContentType.image_format_from_uti(uti)
|
|
46
|
+
ext = ContentType::EXTENSIONS[:image][format]
|
|
47
|
+
|
|
48
|
+
attachments << {
|
|
49
|
+
type: :image,
|
|
50
|
+
format: format,
|
|
51
|
+
data: data,
|
|
52
|
+
filename: "clipboard-image-#{image_count}#{ext}"
|
|
53
|
+
}
|
|
54
|
+
break # Only take the first image format
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
when :rtf
|
|
58
|
+
relevant_utis.each do |uti|
|
|
59
|
+
data = Reader.read_type(pasteboard, uti)
|
|
60
|
+
next unless data && data.bytesize > 0
|
|
61
|
+
|
|
62
|
+
attachments << {
|
|
63
|
+
type: :rtf,
|
|
64
|
+
data: data,
|
|
65
|
+
filename: "clipboard-content.rtf"
|
|
66
|
+
}
|
|
67
|
+
break # Only take the first RTF format
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
when :html
|
|
71
|
+
relevant_utis.each do |uti|
|
|
72
|
+
data = Reader.read_type(pasteboard, uti)
|
|
73
|
+
next unless data && data.bytesize > 0
|
|
74
|
+
|
|
75
|
+
attachments << {
|
|
76
|
+
type: :html,
|
|
77
|
+
data: data,
|
|
78
|
+
filename: "clipboard-content.html"
|
|
79
|
+
}
|
|
80
|
+
break # Only take the first HTML format
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
when :text
|
|
84
|
+
relevant_utis.each do |uti|
|
|
85
|
+
text = Reader.read_string(pasteboard, uti)
|
|
86
|
+
next unless text && !text.empty?
|
|
87
|
+
|
|
88
|
+
text_parts << text
|
|
89
|
+
break # Only take the first text format
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Combine all text parts
|
|
95
|
+
combined_text = text_parts.join("\n\n").strip
|
|
96
|
+
combined_text = nil if combined_text.empty?
|
|
97
|
+
|
|
98
|
+
{
|
|
99
|
+
text: combined_text,
|
|
100
|
+
attachments: attachments
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.parse_text(data)
|
|
105
|
+
return nil unless data
|
|
106
|
+
|
|
107
|
+
data.force_encoding("UTF-8").strip
|
|
108
|
+
rescue
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.parse_file_urls(data)
|
|
113
|
+
return [] unless data
|
|
114
|
+
|
|
115
|
+
# Parse file URL data
|
|
116
|
+
urls = []
|
|
117
|
+
url_str = data.force_encoding("UTF-8").strip
|
|
118
|
+
|
|
119
|
+
# Handle file:// URLs
|
|
120
|
+
url_str = url_str.sub(%r{^file://}, "")
|
|
121
|
+
|
|
122
|
+
# URL decode
|
|
123
|
+
url_str = begin
|
|
124
|
+
URI.decode_www_form_component(url_str)
|
|
125
|
+
rescue
|
|
126
|
+
url_str
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
urls << url_str if File.exist?(url_str)
|
|
130
|
+
urls
|
|
131
|
+
rescue
|
|
132
|
+
[]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.parse_image(data, uti)
|
|
136
|
+
return nil unless data && data.bytesize > 0
|
|
137
|
+
|
|
138
|
+
format = ContentType.image_format_from_uti(uti)
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
format: format,
|
|
142
|
+
data: data
|
|
143
|
+
}
|
|
144
|
+
rescue
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def self.parse_rtf(data)
|
|
149
|
+
return nil unless data && data.bytesize > 0
|
|
150
|
+
|
|
151
|
+
data
|
|
152
|
+
rescue
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.parse_html(data)
|
|
157
|
+
return nil unless data && data.bytesize > 0
|
|
158
|
+
|
|
159
|
+
data.force_encoding("UTF-8")
|
|
160
|
+
rescue
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Support
|
|
5
|
+
module MacClipboard
|
|
6
|
+
module ContentType
|
|
7
|
+
# UTI (Uniform Type Identifier) mappings for macOS clipboard types
|
|
8
|
+
UTI_TYPES = {
|
|
9
|
+
# Text types
|
|
10
|
+
"public.utf8-plain-text" => :text,
|
|
11
|
+
"public.plain-text" => :text,
|
|
12
|
+
"NSStringPboardType" => :text,
|
|
13
|
+
|
|
14
|
+
# Image types
|
|
15
|
+
"public.png" => :image,
|
|
16
|
+
"public.jpeg" => :image,
|
|
17
|
+
"public.tiff" => :image,
|
|
18
|
+
"com.apple.icns" => :image,
|
|
19
|
+
|
|
20
|
+
# File URLs
|
|
21
|
+
"public.file-url" => :files,
|
|
22
|
+
"NSFilenamesPboardType" => :files,
|
|
23
|
+
|
|
24
|
+
# Rich text types
|
|
25
|
+
"public.rtf" => :rtf,
|
|
26
|
+
"com.apple.rtfd" => :rtf,
|
|
27
|
+
|
|
28
|
+
# HTML
|
|
29
|
+
"public.html" => :html,
|
|
30
|
+
"NSHTMLPboardType" => :html
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# Priority order for processing (higher priority first)
|
|
34
|
+
PRIORITY_ORDER = [:files, :image, :rtf, :html, :text].freeze
|
|
35
|
+
|
|
36
|
+
# File extensions for auto-generated filenames
|
|
37
|
+
EXTENSIONS = {
|
|
38
|
+
image: {png: ".png", jpeg: ".jpg", tiff: ".tiff"},
|
|
39
|
+
rtf: ".rtf",
|
|
40
|
+
html: ".html"
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
def self.classify(uti)
|
|
44
|
+
UTI_TYPES[uti] || :unknown
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.image_format_from_uti(uti)
|
|
48
|
+
case uti
|
|
49
|
+
when "public.png" then :png
|
|
50
|
+
when "public.jpeg" then :jpeg
|
|
51
|
+
when "public.tiff" then :tiff
|
|
52
|
+
else :png
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Support
|
|
7
|
+
module MacClipboard
|
|
8
|
+
class Reader
|
|
9
|
+
extend FFI::Library
|
|
10
|
+
|
|
11
|
+
# Load Objective-C runtime and AppKit framework
|
|
12
|
+
ffi_lib "/usr/lib/libobjc.dylib"
|
|
13
|
+
ffi_lib "/System/Library/Frameworks/AppKit.framework/AppKit"
|
|
14
|
+
|
|
15
|
+
# Objective-C runtime functions
|
|
16
|
+
attach_function :objc_getClass, [:string], :pointer
|
|
17
|
+
attach_function :sel_registerName, [:string], :pointer
|
|
18
|
+
attach_function :objc_msgSend, [:pointer, :pointer], :pointer
|
|
19
|
+
attach_function :objc_msgSend_id, :objc_msgSend, [:pointer, :pointer, :pointer], :pointer
|
|
20
|
+
attach_function :objc_msgSend_uint, :objc_msgSend, [:pointer, :pointer], :uint
|
|
21
|
+
attach_function :objc_msgSend_uint64, :objc_msgSend, [:pointer, :pointer, :uint64], :pointer
|
|
22
|
+
|
|
23
|
+
# Helper to send Objective-C messages
|
|
24
|
+
def self.objc_send(obj, selector, *args)
|
|
25
|
+
sel = sel_registerName(selector.to_s)
|
|
26
|
+
if args.empty?
|
|
27
|
+
objc_msgSend(obj, sel)
|
|
28
|
+
else
|
|
29
|
+
objc_msgSend_id(obj, sel, *args)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Read clipboard content
|
|
34
|
+
def self.read
|
|
35
|
+
return {success: false, error: "Not on macOS"} unless RUBY_PLATFORM.match?(/darwin/)
|
|
36
|
+
|
|
37
|
+
pasteboard = get_general_pasteboard
|
|
38
|
+
return {success: false, error: "Could not access pasteboard"} unless pasteboard
|
|
39
|
+
|
|
40
|
+
types = available_types(pasteboard)
|
|
41
|
+
return {success: true, types: [], text: nil, attachments: []} if types.empty?
|
|
42
|
+
|
|
43
|
+
{success: true, types: types, raw_pasteboard: pasteboard}
|
|
44
|
+
rescue => e
|
|
45
|
+
{success: false, error: e.message}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Get the general (system) pasteboard
|
|
49
|
+
def self.get_general_pasteboard
|
|
50
|
+
ns_pasteboard_class = objc_getClass("NSPasteboard")
|
|
51
|
+
objc_send(ns_pasteboard_class, "generalPasteboard")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get all available UTI types on the pasteboard
|
|
55
|
+
def self.available_types(pasteboard)
|
|
56
|
+
types_array = objc_send(pasteboard, "types")
|
|
57
|
+
return [] unless types_array && !types_array.null?
|
|
58
|
+
|
|
59
|
+
count = objc_msgSend_uint(types_array, sel_registerName("count"))
|
|
60
|
+
return [] if count.zero?
|
|
61
|
+
|
|
62
|
+
types = []
|
|
63
|
+
count.times do |i|
|
|
64
|
+
type_obj = objc_msgSend_uint64(types_array, sel_registerName("objectAtIndex:"), i)
|
|
65
|
+
next unless type_obj && !type_obj.null?
|
|
66
|
+
|
|
67
|
+
utf8_selector = sel_registerName("UTF8String")
|
|
68
|
+
type_cstr = objc_msgSend(type_obj, utf8_selector)
|
|
69
|
+
next if type_cstr.null?
|
|
70
|
+
|
|
71
|
+
types << type_cstr.read_string
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
types
|
|
75
|
+
rescue => e
|
|
76
|
+
warn "Error in available_types: #{e.message}"
|
|
77
|
+
warn e.backtrace.first(5)
|
|
78
|
+
[]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Read data for a specific UTI type
|
|
82
|
+
def self.read_type(pasteboard, uti)
|
|
83
|
+
# Create NSString for the UTI
|
|
84
|
+
objc_getClass("NSString")
|
|
85
|
+
uti_str = create_nsstring(uti)
|
|
86
|
+
return nil unless uti_str
|
|
87
|
+
|
|
88
|
+
# Get data from pasteboard
|
|
89
|
+
data = objc_msgSend_id(pasteboard, sel_registerName("dataForType:"), uti_str)
|
|
90
|
+
return nil unless data && !data.null?
|
|
91
|
+
|
|
92
|
+
# Get byte length
|
|
93
|
+
length = objc_msgSend_uint(data, sel_registerName("length"))
|
|
94
|
+
return nil if length.zero?
|
|
95
|
+
|
|
96
|
+
# Get bytes pointer
|
|
97
|
+
bytes = objc_msgSend(data, sel_registerName("bytes"))
|
|
98
|
+
return nil if bytes.null?
|
|
99
|
+
|
|
100
|
+
# Read binary data
|
|
101
|
+
bytes.read_bytes(length)
|
|
102
|
+
rescue
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Read string content for text types
|
|
107
|
+
def self.read_string(pasteboard, uti)
|
|
108
|
+
# Try to get as string first
|
|
109
|
+
objc_getClass("NSString")
|
|
110
|
+
uti_str = create_nsstring(uti)
|
|
111
|
+
return nil unless uti_str
|
|
112
|
+
|
|
113
|
+
string_obj = objc_msgSend_id(pasteboard, sel_registerName("stringForType:"), uti_str)
|
|
114
|
+
if string_obj && !string_obj.null?
|
|
115
|
+
utf8_selector = sel_registerName("UTF8String")
|
|
116
|
+
cstr = objc_msgSend(string_obj, utf8_selector)
|
|
117
|
+
return cstr.read_string unless cstr.null?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Fallback to reading as data
|
|
121
|
+
data = read_type(pasteboard, uti)
|
|
122
|
+
data&.force_encoding("UTF-8")
|
|
123
|
+
rescue
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Read file URLs from pasteboard
|
|
128
|
+
def self.read_file_urls(pasteboard)
|
|
129
|
+
# Try NSFilenamesPboardType first (Finder uses this for copied files)
|
|
130
|
+
file_paths = read_filenames_pboard_type(pasteboard)
|
|
131
|
+
return file_paths if file_paths.any?
|
|
132
|
+
|
|
133
|
+
# Fallback to public.file-url
|
|
134
|
+
file_paths = read_public_file_url(pasteboard)
|
|
135
|
+
return file_paths if file_paths.any?
|
|
136
|
+
|
|
137
|
+
[]
|
|
138
|
+
rescue => e
|
|
139
|
+
warn "Error reading file URLs: #{e.message}"
|
|
140
|
+
[]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Read file paths from NSFilenamesPboardType (used by Finder)
|
|
144
|
+
def self.read_filenames_pboard_type(pasteboard)
|
|
145
|
+
uti_str = create_nsstring("NSFilenamesPboardType")
|
|
146
|
+
return [] unless uti_str
|
|
147
|
+
|
|
148
|
+
# Get property list (NSArray of file path strings)
|
|
149
|
+
prop_list_sel = sel_registerName("propertyListForType:")
|
|
150
|
+
array_obj = objc_msgSend_id(pasteboard, prop_list_sel, uti_str)
|
|
151
|
+
return [] unless array_obj && !array_obj.null?
|
|
152
|
+
|
|
153
|
+
# Get count of files
|
|
154
|
+
count_sel = sel_registerName("count")
|
|
155
|
+
count = objc_msgSend_uint(array_obj, count_sel)
|
|
156
|
+
return [] if count.zero?
|
|
157
|
+
|
|
158
|
+
# Extract file paths
|
|
159
|
+
file_paths = []
|
|
160
|
+
count.times do |i|
|
|
161
|
+
obj_at_index_sel = sel_registerName("objectAtIndex:")
|
|
162
|
+
path_obj = objc_msgSend_uint64(array_obj, obj_at_index_sel, i)
|
|
163
|
+
next unless path_obj && !path_obj.null?
|
|
164
|
+
|
|
165
|
+
utf8_sel = sel_registerName("UTF8String")
|
|
166
|
+
cstr = objc_msgSend(path_obj, utf8_sel)
|
|
167
|
+
next if cstr.null?
|
|
168
|
+
|
|
169
|
+
path = cstr.read_string
|
|
170
|
+
file_paths << path if File.exist?(path)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
file_paths
|
|
174
|
+
rescue => e
|
|
175
|
+
warn "Error reading NSFilenamesPboardType: #{e.message}"
|
|
176
|
+
[]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Read file URL from public.file-url
|
|
180
|
+
def self.read_public_file_url(pasteboard)
|
|
181
|
+
data = read_type(pasteboard, "public.file-url")
|
|
182
|
+
return [] unless data
|
|
183
|
+
|
|
184
|
+
# Parse URL from data
|
|
185
|
+
url_str = data.force_encoding("UTF-8").strip
|
|
186
|
+
|
|
187
|
+
# Remove "file://" prefix if present
|
|
188
|
+
url_str = url_str.sub(%r{^file://}, "")
|
|
189
|
+
|
|
190
|
+
# URL decode
|
|
191
|
+
url_str = begin
|
|
192
|
+
URI.decode_www_form_component(url_str)
|
|
193
|
+
rescue
|
|
194
|
+
url_str
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
File.exist?(url_str) ? [url_str] : []
|
|
198
|
+
rescue
|
|
199
|
+
[]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Helper to create NSString from Ruby string
|
|
203
|
+
def self.create_nsstring(str)
|
|
204
|
+
ns_string_class = objc_getClass("NSString")
|
|
205
|
+
return nil unless ns_string_class
|
|
206
|
+
|
|
207
|
+
utf8_selector = sel_registerName("stringWithUTF8String:")
|
|
208
|
+
objc_msgSend_id(ns_string_class, utf8_selector, FFI::MemoryPointer.from_string(str))
|
|
209
|
+
rescue
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mac_clipboard/version"
|
|
4
|
+
require_relative "mac_clipboard/content_type"
|
|
5
|
+
require_relative "mac_clipboard/reader"
|
|
6
|
+
require_relative "mac_clipboard/content_parser"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Support
|
|
10
|
+
module MacClipboard
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ace-support-mac-clipboard
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Michal Czyz
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ffi
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.15'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.15'
|
|
26
|
+
description: Provides FFI-based access to macOS NSPasteboard for reading rich clipboard
|
|
27
|
+
content (images, files, RTF, HTML)
|
|
28
|
+
email:
|
|
29
|
+
- mc@cs3b.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- LICENSE
|
|
36
|
+
- README.md
|
|
37
|
+
- Rakefile
|
|
38
|
+
- lib/ace/support/mac_clipboard.rb
|
|
39
|
+
- lib/ace/support/mac_clipboard/content_parser.rb
|
|
40
|
+
- lib/ace/support/mac_clipboard/content_type.rb
|
|
41
|
+
- lib/ace/support/mac_clipboard/reader.rb
|
|
42
|
+
- lib/ace/support/mac_clipboard/version.rb
|
|
43
|
+
homepage: https://github.com/cs3b/ace
|
|
44
|
+
licenses:
|
|
45
|
+
- MIT
|
|
46
|
+
metadata:
|
|
47
|
+
homepage_uri: https://github.com/cs3b/ace
|
|
48
|
+
source_code_uri: https://github.com/cs3b/ace/tree/main/ace-support-mac-clipboard/
|
|
49
|
+
changelog_uri: https://github.com/cs3b/ace/blob/main/ace-support-mac-clipboard/CHANGELOG.md
|
|
50
|
+
rdoc_options: []
|
|
51
|
+
require_paths:
|
|
52
|
+
- lib
|
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: 3.2.0
|
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0'
|
|
63
|
+
requirements: []
|
|
64
|
+
rubygems_version: 3.6.9
|
|
65
|
+
specification_version: 4
|
|
66
|
+
summary: macOS NSPasteboard integration for ACE
|
|
67
|
+
test_files: []
|