Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 136 additions & 5 deletions Sources/Pesty/AppController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ final class AppController: NSObject, NSApplicationDelegate {

let store = ClipboardStore.shared
let monitor = ClipboardMonitor()
let pasteSequence = PasteSequence.shared

private var barController: BarWindowController?
private var statusItem: NSStatusItem?
private var settingsWindow: NSWindow?
private var pasteStackController: PasteStackWindowController?
private var keyMonitor: Any?

private(set) var previousApp: NSRunningApplication?
private(set) var lastActiveApp: NSRunningApplication?
private var pasteStackTargetApp: NSRunningApplication?

var suppressAutoHide = false

Expand All @@ -29,6 +32,7 @@ final class AppController: NSObject, NSApplicationDelegate {
monitor.start()

HotKeyCenter.shared.onTrigger = { [weak self] in self?.toggleBar() }
HotKeyCenter.shared.onSequenceTrigger = { [weak self] in self?.pasteNextInSequence() }
HotKeyCenter.shared.start()

setupStatusItem()
Expand Down Expand Up @@ -129,14 +133,14 @@ final class AppController: NSObject, NSApplicationDelegate {
}
}

func showBar() {
func showBar(source requestedSource: BarSource? = nil) {
let front = NSWorkspace.shared.frontmostApplication
if front?.bundleIdentifier != Bundle.main.bundleIdentifier {
previousApp = front
}
store.searchText = ""
store.source = .history
store.selectFirst()
store.source = requestedSource ?? .history
if store.source != .pasteStack { store.selectFirst() }

if barController == nil {
barController = BarWindowController()
Expand Down Expand Up @@ -167,6 +171,108 @@ final class AppController: NSObject, NSApplicationDelegate {
hideBar()
}

func beginPasteSequence() {
pasteStackTargetApp = previousApp ?? lastActiveApp
pasteSequence.begin()
showPasteStack()
hideBar()

let target = pasteStackTargetApp
DispatchQueue.main.async {
target?.activate(options: [])
}
}

func showPasteStack() {
if pasteStackController == nil {
pasteStackController = PasteStackWindowController()
}
pasteStackController?.show()
}

func hidePasteStack() {
pasteStackController?.hide()
}

func showPasteStackTab() {
store.searchText = ""
store.source = .pasteStack
pasteSequence.selectFirst()
pasteStackController?.hide()
if barController?.window?.isVisible != true {
showBar(source: .pasteStack)
}
}

func cancelPasteSequence() {
pasteSequence.cancel()
pasteStackController?.hide()
pasteStackTargetApp = nil
}

func newPasteStack() {
pasteStackTargetApp = previousApp ?? lastActiveApp
pasteSequence.newStack()
showPasteStack()
hideBar()

let target = pasteStackTargetApp
DispatchQueue.main.async {
target?.activate(options: [])
}
}

func pausePasteSequence() {
pasteSequence.pause()
}

func clearPasteStack() {
cancelPasteSequence()
}

func capturePasteStackItem(_ item: ClipItem) {
_ = pasteSequence.addIfNeeded(item)
}

func removePasteStackEntry(_ entry: PasteStackEntry) {
pasteSequence.remove(entry)
}

func reAddPasteStackEntry(_ entry: PasteStackEntry) {
pasteSequence.reAdd(entry)
}

func resetPasteStackProgress() {
pasteSequence.resetProgress()
}

func pasteNextInSequence() {
#if !MAS
guard !Settings.shared.pasteDirectly || PasteService.ensureAccessibility(prompt: true) else { return }
#endif

guard let entry = pasteSequence.next() else { return }
performPasteStackEntry(entry)
}

func pasteStackEntry(_ entry: PasteStackEntry) {
guard let entry = pasteSequence.next(entryID: entry.id) else { return }
performPasteStackEntry(entry)
}

func pasteSelectedStackEntry() {
guard let entry = pasteSequence.selectedEntry else { return }
pasteStackEntry(entry)
}

private func performPasteStackEntry(_ entry: PasteStackEntry) {
hideBar()
PasteService.paste(entry.item,
into: pasteStackTargetApp ?? previousApp,
monitor: monitor,
imageOverride: entry.imagePreview)
}

func showSettings() {
NSApp.activate(ignoringOtherApps: true)
if let win = settingsWindow {
Expand Down Expand Up @@ -204,7 +310,11 @@ final class AppController: NSObject, NSApplicationDelegate {
let ctrl = flags.contains(.control)
let opt = flags.contains(.option)

if cmd, let chars = event.charactersIgnoringModifiers, let n = Int(chars), (1...9).contains(n) {
if store.source != .pasteStack,
cmd,
let chars = event.charactersIgnoringModifiers,
let n = Int(chars),
(1...9).contains(n) {
let items = store.visibleItems
if n <= items.count { pasteItem(items[n - 1]) }
return nil
Expand All @@ -216,25 +326,46 @@ final class AppController: NSObject, NSApplicationDelegate {
else { hideBar() }
return nil
case kVK_Return, kVK_ANSI_KeypadEnter:
if store.source == .pasteStack {
pasteSelectedStackEntry()
return nil
}
pasteSelected(); return nil
case kVK_LeftArrow, kVK_UpArrow:
if store.source == .pasteStack {
pasteSequence.moveSelection(by: -1)
return nil
}
store.moveSelection(by: -1); return nil
case kVK_RightArrow, kVK_DownArrow:
if store.source == .pasteStack {
pasteSequence.moveSelection(by: 1)
return nil
}
store.moveSelection(by: 1); return nil
case kVK_Delete:
if store.source == .pasteStack, let entry = pasteSequence.selectedEntry {
removePasteStackEntry(entry)
return nil
}
if cmd, let sel = store.selectedItem { store.delete(sel); return nil }
if !store.searchText.isEmpty {
store.searchText.removeLast(); store.selectFirst(); return nil
}
return nil
case kVK_ForwardDelete:
if store.source == .pasteStack, let entry = pasteSequence.selectedEntry {
removePasteStackEntry(entry)
return nil
}
if let sel = store.selectedItem { store.delete(sel) }
return nil
default:
break
}

if !cmd && !ctrl && !opt,
if store.source != .pasteStack,
!cmd && !ctrl && !opt,
let chars = event.characters, chars.count == 1,
let scalar = chars.unicodeScalars.first,
scalar.value >= 32, scalar.value != 127 {
Expand Down
41 changes: 33 additions & 8 deletions Sources/Pesty/Hotkey/HotKeyCenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ final class HotKeyCenter {
static let shared = HotKeyCenter()

var onTrigger: (() -> Void)?
var onSequenceTrigger: (() -> Void)?

private var hotKeyRef: EventHotKeyRef?
private var sequenceHotKeyRef: EventHotKeyRef?
private var handlerRef: EventHandlerRef?
private let signature: OSType = 0x50535459

Expand All @@ -22,25 +24,48 @@ final class HotKeyCenter {
guard handlerRef == nil else { return }
var spec = EventTypeSpec(eventClass: OSType(kEventClassKeyboard),
eventKind: OSType(kEventHotKeyPressed))
InstallEventHandler(GetApplicationEventTarget(), { _, _, _ -> OSStatus in
DispatchQueue.main.async { HotKeyCenter.shared.onTrigger?() }
InstallEventHandler(GetApplicationEventTarget(), { _, event, _ -> OSStatus in
var id = EventHotKeyID()
GetEventParameter(event,
EventParamName(kEventParamDirectObject),
EventParamType(typeEventHotKeyID),
nil,
MemoryLayout<EventHotKeyID>.size,
nil,
&id)
DispatchQueue.main.async {
if id.id == 2 {
HotKeyCenter.shared.onSequenceTrigger?()
} else {
HotKeyCenter.shared.onTrigger?()
}
}
return noErr
}, 1, &spec, nil, &handlerRef)
}

func reload() {
unregister()
let keyCode = UInt32(Settings.shared.hotkeyKeyCode)
let modifiers = UInt32(Settings.shared.hotkeyModifiers)
guard keyCode != 0 else { return }
let id = EventHotKeyID(signature: signature, id: 1)
hotKeyRef = register(keyCode: Settings.shared.hotkeyKeyCode,
modifiers: Settings.shared.hotkeyModifiers,
id: 1)
sequenceHotKeyRef = register(keyCode: Settings.shared.sequenceHotkeyKeyCode,
modifiers: Settings.shared.sequenceHotkeyModifiers,
id: 2)
}

private func register(keyCode: Int, modifiers: Int, id: UInt32) -> EventHotKeyRef? {
guard keyCode != 0 else { return nil }
let hotKeyID = EventHotKeyID(signature: signature, id: id)
var ref: EventHotKeyRef?
let status = RegisterEventHotKey(keyCode, modifiers, id, GetApplicationEventTarget(), 0, &ref)
if status == noErr { hotKeyRef = ref }
let status = RegisterEventHotKey(UInt32(keyCode), UInt32(modifiers), hotKeyID,
GetApplicationEventTarget(), 0, &ref)
return status == noErr ? ref : nil
}

private func unregister() {
if let ref = hotKeyRef { UnregisterEventHotKey(ref); hotKeyRef = nil }
if let ref = sequenceHotKeyRef { UnregisterEventHotKey(ref); sequenceHotKeyRef = nil }
}

static func describe(keyCode: Int, modifiers: Int) -> String {
Expand Down
3 changes: 2 additions & 1 deletion Sources/Pesty/Monitor/ClipboardMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ final class ClipboardMonitor {
lastChangeCount = current
if current == suppressUntilChangeCount { return }
guard let item = makeItem() else { return }
ClipboardStore.shared.addCaptured(item)
let storedItem = ClipboardStore.shared.addCaptured(item)
AppController.shared.capturePasteStackItem(storedItem)
}

private func makeItem() -> ClipItem? {
Expand Down
11 changes: 7 additions & 4 deletions Sources/Pesty/Monitor/PasteService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import Carbon.HIToolbox
enum PasteService {

@discardableResult
static func copy(_ item: ClipItem, to pasteboard: NSPasteboard = .general) -> Int {
static func copy(_ item: ClipItem,
to pasteboard: NSPasteboard = .general,
imageOverride: NSImage? = nil) -> Int {
if item.type == .image {
guard let img = ClipboardStore.shared.loadImage(for: item) else {
guard let img = imageOverride ?? ClipboardStore.shared.loadImage(for: item) else {
return pasteboard.changeCount
}
pasteboard.clearContents()
Expand Down Expand Up @@ -38,8 +40,9 @@ enum PasteService {

static func paste(_ item: ClipItem,
into targetApp: NSRunningApplication?,
monitor: ClipboardMonitor) {
let change = copy(item)
monitor: ClipboardMonitor,
imageOverride: NSImage? = nil) {
let change = copy(item, imageOverride: imageOverride)
monitor.suppressUntilChangeCount = change
if Settings.shared.playSound { NSSound(named: "Pop")?.play() }

Expand Down
14 changes: 10 additions & 4 deletions Sources/Pesty/Settings/HotkeyRecorderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@ import AppKit
import Carbon.HIToolbox

struct HotkeyRecorderView: View {
@Bindable private var settings = Settings.shared
@Binding private var keyCode: Int
@Binding private var modifiers: Int
@State private var recording = false
@State private var monitor: Any?

init(keyCode: Binding<Int>, modifiers: Binding<Int>) {
_keyCode = keyCode
_modifiers = modifiers
}

var body: some View {
Button {
recording ? stop() : start()
} label: {
Text(recording ? "Press keys…" : settings.hotkeyDisplay)
Text(recording ? "Press keys…" : HotKeyCenter.describe(keyCode: keyCode, modifiers: modifiers))
.font(.system(size: 13, weight: .medium, design: .rounded))
.frame(minWidth: 90)
.padding(.horizontal, 12).padding(.vertical, 5)
Expand All @@ -34,8 +40,8 @@ struct HotkeyRecorderView: View {
if mods & (cmdKey | controlKey | optionKey) == 0 {
NSSound.beep(); return nil
}
settings.hotkeyKeyCode = Int(event.keyCode)
settings.hotkeyModifiers = mods
keyCode = Int(event.keyCode)
modifiers = mods
stop()
return nil
}
Expand Down
Loading