WIP: add plain text paste mode
This commit is contained in:
@@ -25,18 +25,27 @@ final class PasteActionService {
|
|||||||
|
|
||||||
enum PasteActionResult: Equatable {
|
enum PasteActionResult: Equatable {
|
||||||
case pasted
|
case pasted
|
||||||
|
case pastedPlainText
|
||||||
case copied
|
case copied
|
||||||
|
case copiedPlainText
|
||||||
case copiedNeedsPermission
|
case copiedNeedsPermission
|
||||||
|
case copiedPlainTextNeedsPermission
|
||||||
case failed(String)
|
case failed(String)
|
||||||
|
|
||||||
var message: String {
|
var message: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .pasted:
|
case .pasted:
|
||||||
return "Pasted"
|
return "Pasted"
|
||||||
|
case .pastedPlainText:
|
||||||
|
return "Pasted Plain Text"
|
||||||
case .copied:
|
case .copied:
|
||||||
return "Copied"
|
return "Copied"
|
||||||
|
case .copiedPlainText:
|
||||||
|
return "Copied Plain Text"
|
||||||
case .copiedNeedsPermission:
|
case .copiedNeedsPermission:
|
||||||
return "Copied. Grant Accessibility access to paste automatically."
|
return "Copied. Grant Accessibility access to paste automatically."
|
||||||
|
case .copiedPlainTextNeedsPermission:
|
||||||
|
return "Copied Plain Text. Grant Accessibility access to paste automatically."
|
||||||
case .failed(let message):
|
case .failed(let message):
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
@@ -67,11 +76,40 @@ final class PasteActionService {
|
|||||||
return .pasted
|
return .pasted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pastePlainText(_ item: ClipboardItem, targetApp: NSRunningApplication?) -> PasteActionResult {
|
||||||
|
guard writePlainTextToPasteboard(item) else {
|
||||||
|
return .failed("Could not write plain text to clipboard.")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let targetApp,
|
||||||
|
!targetApp.isTerminated else {
|
||||||
|
return .copiedPlainText
|
||||||
|
}
|
||||||
|
|
||||||
|
guard accessibilityPermissionProvider() else {
|
||||||
|
return .copiedPlainTextNeedsPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
guard targetActivator(targetApp) else {
|
||||||
|
return .copiedPlainText
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardPasteScheduler { [weak self] in
|
||||||
|
self?.pasteViaKeyboard()
|
||||||
|
}
|
||||||
|
return .pastedPlainText
|
||||||
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func copy(_ item: ClipboardItem) -> PasteActionResult {
|
func copy(_ item: ClipboardItem) -> PasteActionResult {
|
||||||
writeToPasteboard(item) ? .copied : .failed("Could not write item to clipboard.")
|
writeToPasteboard(item) ? .copied : .failed("Could not write item to clipboard.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func copyPlainText(_ item: ClipboardItem) -> PasteActionResult {
|
||||||
|
writePlainTextToPasteboard(item) ? .copiedPlainText : .failed("Could not write plain text to clipboard.")
|
||||||
|
}
|
||||||
|
|
||||||
func pasteboardWriters(for item: ClipboardItem) -> [NSPasteboardWriting] {
|
func pasteboardWriters(for item: ClipboardItem) -> [NSPasteboardWriting] {
|
||||||
switch item.kind {
|
switch item.kind {
|
||||||
case .image:
|
case .image:
|
||||||
@@ -185,6 +223,37 @@ final class PasteActionService {
|
|||||||
return didWrite
|
return didWrite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func plainText(for item: ClipboardItem) -> String? {
|
||||||
|
switch item.kind {
|
||||||
|
case .text, .unknown:
|
||||||
|
return nonEmptyPlainText(item.payload) ?? nonEmptyPlainText(item.displayText)
|
||||||
|
case .url, .file:
|
||||||
|
return nonEmptyPlainText(item.payload) ?? nonEmptyPlainText(item.displayText)
|
||||||
|
case .richText:
|
||||||
|
if let data = cacheService.data(for: item.payload),
|
||||||
|
let text = richTextPlainString(from: data) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return nonEmptyPlainText(richTextFallbackPlainString(for: item))
|
||||||
|
case .image:
|
||||||
|
return nonEmptyPlainText(item.ocrText) ?? nonEmptyPlainText(item.displayText)
|
||||||
|
case .pdf, .audio:
|
||||||
|
return nonEmptyPlainText(item.displayText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func writePlainTextToPasteboard(_ item: ClipboardItem) -> Bool {
|
||||||
|
guard let text = plainText(for: item) else { return false }
|
||||||
|
let board = NSPasteboard.general
|
||||||
|
board.clearContents()
|
||||||
|
let didWrite = board.setString(text, forType: .string)
|
||||||
|
if didWrite {
|
||||||
|
ClipboardSelfWriteTracker.mark(changeCount: board.changeCount)
|
||||||
|
}
|
||||||
|
return didWrite
|
||||||
|
}
|
||||||
|
|
||||||
private func stringPasteboardItem(_ value: String) -> NSPasteboardItem {
|
private func stringPasteboardItem(_ value: String) -> NSPasteboardItem {
|
||||||
let pasteboardItem = NSPasteboardItem()
|
let pasteboardItem = NSPasteboardItem()
|
||||||
pasteboardItem.setString(value, forType: .string)
|
pasteboardItem.setString(value, forType: .string)
|
||||||
@@ -220,6 +289,11 @@ final class PasteActionService {
|
|||||||
return normalized
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func nonEmptyPlainText(_ value: String?) -> String? {
|
||||||
|
guard let value, !value.clipboardTrimmed.isEmpty else { return nil }
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
private func richTextPlainString(from data: Data) -> String? {
|
private func richTextPlainString(from data: Data) -> String? {
|
||||||
guard let attributed = NSAttributedString(rtf: data, documentAttributes: nil) else {
|
guard let attributed = NSAttributedString(rtf: data, documentAttributes: nil) else {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ struct ClipboardPanelReflowPlan {
|
|||||||
|
|
||||||
enum ClipboardPanelShortcutAction: Equatable {
|
enum ClipboardPanelShortcutAction: Equatable {
|
||||||
case copy
|
case copy
|
||||||
|
case copyPlainText
|
||||||
case open
|
case open
|
||||||
|
case pastePlainText
|
||||||
case preview
|
case preview
|
||||||
case reveal
|
case reveal
|
||||||
}
|
}
|
||||||
@@ -332,6 +334,11 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
|
|||||||
self.performShortcutAction(action)
|
self.performShortcutAction(action)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if self.shouldHandlePanelKeyEvent(event, allowSearchFieldEditing: true),
|
||||||
|
let action = Self.modifiedShortcutAction(forKeyCode: event.keyCode, modifiers: event.modifierFlags) {
|
||||||
|
self.performShortcutAction(action)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
guard self.shouldHandlePanelKeyEvent(event) else { return event }
|
guard self.shouldHandlePanelKeyEvent(event) else { return event }
|
||||||
switch event.keyCode {
|
switch event.keyCode {
|
||||||
case 53:
|
case 53:
|
||||||
@@ -365,8 +372,12 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
|
|||||||
switch action {
|
switch action {
|
||||||
case .copy:
|
case .copy:
|
||||||
viewModel.copySelected()
|
viewModel.copySelected()
|
||||||
|
case .copyPlainText:
|
||||||
|
viewModel.copySelectedPlainText()
|
||||||
case .open:
|
case .open:
|
||||||
viewModel.openSelected()
|
viewModel.openSelected()
|
||||||
|
case .pastePlainText:
|
||||||
|
viewModel.pasteSelectedPlainText()
|
||||||
case .preview:
|
case .preview:
|
||||||
previewSelected()
|
previewSelected()
|
||||||
case .reveal:
|
case .reveal:
|
||||||
@@ -430,6 +441,19 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func modifiedShortcutAction(forKeyCode keyCode: UInt16, modifiers: NSEvent.ModifierFlags) -> ClipboardPanelShortcutAction? {
|
||||||
|
let relevantModifiers = modifiers.intersection(.deviceIndependentFlagsMask)
|
||||||
|
guard relevantModifiers == [.command, .shift] else { return nil }
|
||||||
|
switch keyCode {
|
||||||
|
case 8:
|
||||||
|
return .copyPlainText
|
||||||
|
case 9:
|
||||||
|
return .pastePlainText
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
|
func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
|
||||||
quickLookURL == nil ? 0 : 1
|
quickLookURL == nil ? 0 : 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -528,6 +528,14 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|||||||
self?.viewModel.selectItem(at: selected)
|
self?.viewModel.selectItem(at: selected)
|
||||||
self?.viewModel.copySelected()
|
self?.viewModel.copySelected()
|
||||||
}
|
}
|
||||||
|
card.onPastePlainText = { [weak self] selected in
|
||||||
|
self?.viewModel.selectItem(at: selected)
|
||||||
|
self?.viewModel.pasteSelectedPlainText()
|
||||||
|
}
|
||||||
|
card.onCopyPlainText = { [weak self] selected in
|
||||||
|
self?.viewModel.selectItem(at: selected)
|
||||||
|
self?.viewModel.copySelectedPlainText()
|
||||||
|
}
|
||||||
card.onEditText = { [weak self] selected in
|
card.onEditText = { [weak self] selected in
|
||||||
self?.editText(at: selected)
|
self?.editText(at: selected)
|
||||||
}
|
}
|
||||||
@@ -1308,6 +1316,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
var onSelect: (Int) -> Void = { _ in }
|
var onSelect: (Int) -> Void = { _ in }
|
||||||
var onPaste: (Int) -> Void = { _ in }
|
var onPaste: (Int) -> Void = { _ in }
|
||||||
var onCopy: (Int) -> Void = { _ in }
|
var onCopy: (Int) -> Void = { _ in }
|
||||||
|
var onPastePlainText: (Int) -> Void = { _ in }
|
||||||
|
var onCopyPlainText: (Int) -> Void = { _ in }
|
||||||
var onEditText: (Int) -> Void = { _ in }
|
var onEditText: (Int) -> Void = { _ in }
|
||||||
var onPreview: (Int) -> Void = { _ in }
|
var onPreview: (Int) -> Void = { _ in }
|
||||||
var onPasteboardWriters: (Int) -> [NSPasteboardWriting] = { _ in [] }
|
var onPasteboardWriters: (Int) -> [NSPasteboardWriting] = { _ in [] }
|
||||||
@@ -1491,6 +1501,10 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
menu.autoenablesItems = false
|
menu.autoenablesItems = false
|
||||||
addMenuItem("Paste", action: #selector(pasteFromMenu), to: menu)
|
addMenuItem("Paste", action: #selector(pasteFromMenu), to: menu)
|
||||||
addMenuItem("Copy", action: #selector(copyFromMenu), to: menu)
|
addMenuItem("Copy", action: #selector(copyFromMenu), to: menu)
|
||||||
|
if canPlainText {
|
||||||
|
addMenuItem("Paste Plain Text", action: #selector(pastePlainTextFromMenu), to: menu)
|
||||||
|
addMenuItem("Copy Plain Text", action: #selector(copyPlainTextFromMenu), to: menu)
|
||||||
|
}
|
||||||
if canEditText {
|
if canEditText {
|
||||||
addMenuItem("Edit", action: #selector(editTextFromMenu), to: menu)
|
addMenuItem("Edit", action: #selector(editTextFromMenu), to: menu)
|
||||||
}
|
}
|
||||||
@@ -1584,6 +1598,15 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
itemKind == .text
|
itemKind == .text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var canPlainText: Bool {
|
||||||
|
switch itemKind {
|
||||||
|
case .url, .image, .richText, .file, .pdf, .audio:
|
||||||
|
return true
|
||||||
|
case .text, .unknown:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var canReveal: Bool {
|
private var canReveal: Bool {
|
||||||
switch itemKind {
|
switch itemKind {
|
||||||
case .file, .image, .pdf, .audio:
|
case .file, .image, .pdf, .audio:
|
||||||
@@ -1694,6 +1717,14 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
onCopy(index)
|
onCopy(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func pastePlainTextFromMenu() {
|
||||||
|
onPastePlainText(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func copyPlainTextFromMenu() {
|
||||||
|
onCopyPlainText(index)
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func editTextFromMenu() {
|
@objc private func editTextFromMenu() {
|
||||||
onEditText(index)
|
onEditText(index)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,20 @@ final class ClipboardPanelViewModel {
|
|||||||
settings.setPasteStatus(message: result.message)
|
settings.setPasteStatus(message: result.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pasteSelectedPlainText() {
|
||||||
|
guard let item = selectedItem else { return }
|
||||||
|
let result = pasteService.pastePlainText(item, targetApp: targetApplicationProvider())
|
||||||
|
if case .pastedPlainText = result {
|
||||||
|
willPasteToTarget()
|
||||||
|
}
|
||||||
|
if case .failed = result {} else {
|
||||||
|
store.markUsed(item.id)
|
||||||
|
selectedItemID = item.id
|
||||||
|
}
|
||||||
|
statusMessage = result.message
|
||||||
|
settings.setPasteStatus(message: result.message)
|
||||||
|
}
|
||||||
|
|
||||||
func copySelected() {
|
func copySelected() {
|
||||||
guard let item = selectedItem else { return }
|
guard let item = selectedItem else { return }
|
||||||
let result = pasteService.copy(item)
|
let result = pasteService.copy(item)
|
||||||
@@ -163,6 +177,17 @@ final class ClipboardPanelViewModel {
|
|||||||
settings.setPasteStatus(message: result.message)
|
settings.setPasteStatus(message: result.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copySelectedPlainText() {
|
||||||
|
guard let item = selectedItem else { return }
|
||||||
|
let result = pasteService.copyPlainText(item)
|
||||||
|
if case .failed = result {} else {
|
||||||
|
store.markUsed(item.id)
|
||||||
|
selectedItemID = item.id
|
||||||
|
}
|
||||||
|
statusMessage = result.message
|
||||||
|
settings.setPasteStatus(message: result.message)
|
||||||
|
}
|
||||||
|
|
||||||
func pasteboardWriters(forItemAt index: Int) -> [NSPasteboardWriting] {
|
func pasteboardWriters(forItemAt index: Int) -> [NSPasteboardWriting] {
|
||||||
guard index >= 0 && index < visibleItems.count else { return [] }
|
guard index >= 0 && index < visibleItems.count else { return [] }
|
||||||
return pasteService.pasteboardWriters(for: visibleItems[index])
|
return pasteService.pasteboardWriters(for: visibleItems[index])
|
||||||
|
|||||||
@@ -157,4 +157,15 @@ final class ClipboardPanelControllerTests: XCTestCase {
|
|||||||
XCTAssertNil(ClipboardPanelController.commandShortcutAction(forKeyCode: 8, modifiers: [.command, .shift]))
|
XCTAssertNil(ClipboardPanelController.commandShortcutAction(forKeyCode: 8, modifiers: [.command, .shift]))
|
||||||
XCTAssertNil(ClipboardPanelController.commandShortcutAction(forKeyCode: 9, modifiers: .command))
|
XCTAssertNil(ClipboardPanelController.commandShortcutAction(forKeyCode: 9, modifiers: .command))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testModifiedShortcutsMapToPlainTextActions() {
|
||||||
|
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 8, modifiers: [.command, .shift]), .copyPlainText)
|
||||||
|
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 9, modifiers: [.command, .shift]), .pastePlainText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testModifiedShortcutsRequireCommandShiftOnly() {
|
||||||
|
XCTAssertNil(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 8, modifiers: .command))
|
||||||
|
XCTAssertNil(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 8, modifiers: [.command, .option, .shift]))
|
||||||
|
XCTAssertNil(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 31, modifiers: [.command, .shift]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,6 +271,39 @@ final class ClipboardPanelViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(NSPasteboard.general.string(forType: .URL), item.payload)
|
XCTAssertEqual(NSPasteboard.general.string(forType: .URL), item.payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCopySelectedPlainTextWritesOnlyStringRepresentation() {
|
||||||
|
let settings = makeSettings()
|
||||||
|
let cacheService = makeCacheService()
|
||||||
|
let store = makeStore(settings: settings, cacheService: cacheService)
|
||||||
|
let item = ClipboardItem(
|
||||||
|
id: UUID(),
|
||||||
|
kind: .url,
|
||||||
|
displayText: "Example",
|
||||||
|
payload: "https://example.com",
|
||||||
|
payloadHash: hash("https://example.com"),
|
||||||
|
createdAt: Date(timeIntervalSince1970: 200),
|
||||||
|
lastUsedAt: Date(timeIntervalSince1970: 200),
|
||||||
|
useCount: 0,
|
||||||
|
sourceApp: nil,
|
||||||
|
imagePath: nil,
|
||||||
|
thumbnailPath: nil
|
||||||
|
)
|
||||||
|
store.upsert(item)
|
||||||
|
store.flushPersistenceForTesting()
|
||||||
|
|
||||||
|
let viewModel = ClipboardPanelViewModel(store: store, settings: settings, cacheService: cacheService)
|
||||||
|
waitForVisibleItems(in: viewModel, count: 1)
|
||||||
|
|
||||||
|
viewModel.copySelectedPlainText()
|
||||||
|
store.flushPersistenceForTesting()
|
||||||
|
|
||||||
|
XCTAssertEqual(viewModel.statusMessage, "Copied Plain Text")
|
||||||
|
XCTAssertEqual(NSPasteboard.general.string(forType: .string), item.payload)
|
||||||
|
XCTAssertNil(NSPasteboard.general.string(forType: .URL))
|
||||||
|
XCTAssertEqual(store.items.first?.id, item.id)
|
||||||
|
XCTAssertEqual(store.items.first?.useCount, 1)
|
||||||
|
}
|
||||||
|
|
||||||
func testUpdateSelectedTextRefreshesVisibleItemAndSearch() {
|
func testUpdateSelectedTextRefreshesVisibleItemAndSearch() {
|
||||||
let settings = makeSettings()
|
let settings = makeSettings()
|
||||||
let cacheService = makeCacheService()
|
let cacheService = makeCacheService()
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ final class ClipboardPanelViewTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
fixture.view.debugFirstCardMenuTitles,
|
fixture.view.debugFirstCardMenuTitles,
|
||||||
["Paste", "Copy", "Quick Look", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"]
|
["Paste", "Copy", "Paste Plain Text", "Copy Plain Text", "Quick Look", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,41 @@ final class PasteActionServiceTests: XCTestCase {
|
|||||||
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "Paste into target")
|
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "Paste into target")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAutomaticPlainTextPasteActivatesTargetAndSchedulesKeyboardPasteWhenPermissionGranted() throws {
|
||||||
|
var activatedProcessID: pid_t?
|
||||||
|
let targetApp = try makeRunningTargetApp()
|
||||||
|
var didScheduleKeyboardPaste = false
|
||||||
|
let service = PasteActionService(
|
||||||
|
accessibilityPermissionProvider: { true },
|
||||||
|
targetActivator: { app in
|
||||||
|
activatedProcessID = app.processIdentifier
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
keyboardPasteScheduler: { _ in
|
||||||
|
didScheduleKeyboardPaste = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
let item = ClipboardItem(
|
||||||
|
id: UUID(),
|
||||||
|
kind: .url,
|
||||||
|
displayText: "Apple",
|
||||||
|
payload: "https://apple.com",
|
||||||
|
payloadHash: "hash",
|
||||||
|
createdAt: Date(),
|
||||||
|
lastUsedAt: Date(),
|
||||||
|
useCount: 0,
|
||||||
|
sourceApp: nil,
|
||||||
|
imagePath: nil,
|
||||||
|
thumbnailPath: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(service.pastePlainText(item, targetApp: targetApp), .pastedPlainText)
|
||||||
|
XCTAssertEqual(activatedProcessID, targetApp.processIdentifier)
|
||||||
|
XCTAssertTrue(didScheduleKeyboardPaste)
|
||||||
|
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "https://apple.com")
|
||||||
|
XCTAssertNil(NSPasteboard.general.string(forType: .URL))
|
||||||
|
}
|
||||||
|
|
||||||
func testAutomaticPasteDoesNotPostShortcutWhenTargetActivationFails() throws {
|
func testAutomaticPasteDoesNotPostShortcutWhenTargetActivationFails() throws {
|
||||||
var didAttemptActivation = false
|
var didAttemptActivation = false
|
||||||
let targetApp = try makeRunningTargetApp()
|
let targetApp = try makeRunningTargetApp()
|
||||||
@@ -264,6 +299,37 @@ final class PasteActionServiceTests: XCTestCase {
|
|||||||
XCTAssertEqual(NSPasteboard.general.string(forType: .string), attributed.string)
|
XCTAssertEqual(NSPasteboard.general.string(forType: .string), attributed.string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCopyPlainTextStripsRichTextFormatting() throws {
|
||||||
|
let directory = try makeTempDirectory()
|
||||||
|
let cacheService = ClipboardCacheService(baseURL: directory, encryptionService: fixedEncryptionService())
|
||||||
|
let attributed = NSAttributedString(
|
||||||
|
string: "Styled clipboard text",
|
||||||
|
attributes: [.font: NSFont.boldSystemFont(ofSize: 15)]
|
||||||
|
)
|
||||||
|
let rtfData = try XCTUnwrap(
|
||||||
|
attributed.rtf(from: NSRange(location: 0, length: attributed.length), documentAttributes: [:])
|
||||||
|
)
|
||||||
|
let path = try XCTUnwrap(cacheService.cacheRichText(rtfData, id: UUID()))
|
||||||
|
let service = PasteActionService(cacheService: cacheService)
|
||||||
|
let item = ClipboardItem(
|
||||||
|
id: UUID(),
|
||||||
|
kind: .richText,
|
||||||
|
displayText: attributed.string,
|
||||||
|
payload: path,
|
||||||
|
payloadHash: "hash",
|
||||||
|
createdAt: Date(),
|
||||||
|
lastUsedAt: Date(),
|
||||||
|
useCount: 0,
|
||||||
|
sourceApp: nil,
|
||||||
|
imagePath: nil,
|
||||||
|
thumbnailPath: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(service.copyPlainText(item), .copiedPlainText)
|
||||||
|
XCTAssertEqual(NSPasteboard.general.string(forType: .string), attributed.string)
|
||||||
|
XCTAssertNil(NSPasteboard.general.data(forType: .rtf))
|
||||||
|
}
|
||||||
|
|
||||||
func testCopyLegacyRichTextWritesPlainPayloadWhenRTFCacheIsUnavailable() {
|
func testCopyLegacyRichTextWritesPlainPayloadWhenRTFCacheIsUnavailable() {
|
||||||
let service = PasteActionService()
|
let service = PasteActionService()
|
||||||
let item = ClipboardItem(
|
let item = ClipboardItem(
|
||||||
@@ -307,6 +373,28 @@ final class PasteActionServiceTests: XCTestCase {
|
|||||||
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "Readable rich text")
|
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "Readable rich text")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCopyPlainTextForURLOmitsURLPasteboardTypes() {
|
||||||
|
let service = PasteActionService()
|
||||||
|
let item = ClipboardItem(
|
||||||
|
id: UUID(),
|
||||||
|
kind: .url,
|
||||||
|
displayText: "Apple",
|
||||||
|
payload: "https://apple.com",
|
||||||
|
payloadHash: "hash",
|
||||||
|
createdAt: Date(),
|
||||||
|
lastUsedAt: Date(),
|
||||||
|
useCount: 0,
|
||||||
|
sourceApp: nil,
|
||||||
|
imagePath: nil,
|
||||||
|
thumbnailPath: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(service.copyPlainText(item), .copiedPlainText)
|
||||||
|
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "https://apple.com")
|
||||||
|
XCTAssertNil(NSPasteboard.general.string(forType: .URL))
|
||||||
|
XCTAssertNil(NSPasteboard.general.string(forType: NSPasteboard.PasteboardType(rawValue: "public.url-name")))
|
||||||
|
}
|
||||||
|
|
||||||
func testCopyWritesFileReferenceType() throws {
|
func testCopyWritesFileReferenceType() throws {
|
||||||
let fileURL = try makeTempFile(contents: "file contents")
|
let fileURL = try makeTempFile(contents: "file contents")
|
||||||
let service = PasteActionService()
|
let service = PasteActionService()
|
||||||
|
|||||||
Reference in New Issue
Block a user