Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
dc46c90
Migrate to Swift 6 (#1532)
Sublio Oct 2, 2026
9165a3c
Update dependencies (#1574)
Sublio Oct 2, 2026
49f28db
Stop scanning the whole history to render the menu bar title (#1551)
ddelange Oct 3, 2026
b2a7c41
Fix HistoryItemDecorator leaking every history item (#1552)
ddelange Oct 3, 2026
d21175f
Edit pinned items using a modal sheet instead of in settings
weisJ Jul 23, 2026
509ef43
Remove PinsSettingsPane from settings
weisJ Jul 6, 2026
82cd643
Fix rounded corners for sheets
weisJ Jul 7, 2026
b0e0f9d
Implement custom pin ordering through drag and drop
weisJ Jul 23, 2026
0985cbb
Only update search query if text really changed
weisJ Jul 7, 2026
70be934
Make sort oder for pins configurable
weisJ Jul 23, 2026
af9002a
Remove setting for pin ordering
weisJ Aug 11, 2026
506869e
Extract moveElements into extensions folder
weisJ Aug 11, 2026
3d17672
Use History.togglePin in testClearingAll
weisJ Aug 22, 2026
a0fb0d1
Return focus to search field after editing item
weisJ Aug 22, 2026
80a7aff
Fix drag and drop preview appearing behind popup
weisJ Aug 22, 2026
a6816d2
Make sure moving pins updates the history view
weisJ Aug 22, 2026
c94a133
Make drang and drop behave more intuitively
weisJ Aug 22, 2026
b161231
Separate pinned and unpinned history storage
weisJ Aug 22, 2026
fbaeede
Avoid allocating visible item collections
weisJ Aug 22, 2026
cc01af0
Add concatenated item collections
weisJ Aug 22, 2026
aca784e
Adapt pin ordering value types to Swift 6 isolation
weisJ Oct 3, 2026
55e4417
Allow editing pin keys for image items
weisJ Oct 3, 2026
0fd3a87
Keep keyboard focus and actions in the item editor
weisJ Oct 3, 2026
74ac00b
Preserve rich content when only item metadata changes
weisJ Oct 3, 2026
9936c3b
Preserve numeric item shortcuts after unpinning
weisJ Oct 3, 2026
7bf6194
Label item editor controls for VoiceOver
weisJ Oct 3, 2026
c81b41d
Use a larger square-and-pencil edit button
weisJ Oct 3, 2026
9e6db5a
Reuse the large text view for editing with native change tracking
weisJ Oct 3, 2026
fe408a6
Avoid layout shift when editing
weisJ Oct 3, 2026
69ef58d
Don’t create preview view if slidout is not open
weisJ Oct 3, 2026
554ed2a
Cache text previews and cap paragraph length
weisJ Oct 3, 2026
2b5c40e
Disable saving unchanged items
weisJ Oct 3, 2026
14622ed
Remove the unused Pins settings pane
weisJ Oct 3, 2026
2ef27fc
Use native .reorderable() on macOS 27
weisJ Oct 3, 2026
c60a123
Improve unpin/edit icons
p0deje Oct 5, 2026
c95bb47
Allow pinning items even if there are no pin letters
p0deje Oct 5, 2026
19c3d82
Revert popup window level to .statusBar
p0deje Oct 5, 2026
f26dfbb
Properly set lead item when removing from selection
weisJ Feb 8, 2026
90980b0
Avoid allocating Array in enumerated ForEach Views
weisJ Jul 30, 2026
85c5c01
Hide selection indices for unselected history items
weisJ Oct 6, 2026
8388176
Use EnumeratedForEach for selectable history lists
weisJ Oct 6, 2026
78858b7
Draw selection backgrounds directly in their shape
weisJ Oct 6, 2026
c0fc7c7
Fix multi-item pin reordering
weisJ Oct 6, 2026
dd2738e
Remove native macOS 27 reordering
weisJ Oct 6, 2026
f64c6fc
Stop storing Safari WebArchives and other unselected pasteboard types…
KullAxel Oct 6, 2026
130f23a
Fix SwiftLint warnings
p0deje Oct 6, 2026
a2055c3
Allow ignoring apps outside /Applications (#1540)
anandghegde Oct 6, 2026
a92c11a
Restore accidentally deleted comment
weisJ Oct 6, 2026
0dd5478
Use accurate OCR with automatic language detection (#1586)
panozzaj Oct 12, 2026
d62d0bf
Fix Swift 6.4 concurrency warnings (#1585)
Sublio Oct 13, 2026
fe0d74d
Improve Hindi translation (#1546)
weblate Oct 14, 2026
0790189
Show image size in preview (#1589)
davidsanchez222 Oct 14, 2026
22bd466
Use special symbols in as pin keys
p0deje Oct 14, 2026
8360935
Don't rerun OCR for images when toggling special symbols (#1590)
Sublio Oct 15, 2026
50d2f1c
Wrap the hidden footer warning in long translations (#1592)
itssali Oct 15, 2026
7e14eb8
Fix building with Xcode 26.5 (#1593)
itssali Oct 15, 2026
c700182
Fix crash when watched app bundle changes
p0deje Oct 16, 2026
54fb9f0
Migrate unit tests from XCTest to Swift Testing (#1597)
Sublio Oct 23, 2026
c6b02ac
Merge upstream Maccy (origin/master 54fb9f0) into MaccyPlus
roypadina Oct 24, 2026
d7a017b
fix: Release build crash on Swift 6.3 (Xcode 26.x); skip renamed Clip…
roypadina Oct 24, 2026
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ jobs:
run: |
xcodebuild test -project Maccy.xcodeproj -scheme Maccy -destination 'platform=macOS' \
-only-testing:MaccyTests -skip-testing:MaccyTests/ClipboardTests \
-skip-testing:MaccyTests/SharedStateTests/ClipboardTests \
CODE_SIGN_IDENTITY=- CODE_SIGNING_ALLOWED=NO
96 changes: 73 additions & 23 deletions Maccy.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Maccy.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
"argument" : "enable-testing"
}
],
"environmentVariableEntries" : [
{
"key" : "CI",
"value" : "$(CI)"
}
],
"targetForVariableExpansion" : {
"containerPath" : "container:Maccy.xcodeproj",
"identifier" : "DAEE38421E3DBEB100DD2966",
Expand Down
3 changes: 2 additions & 1 deletion Maccy/Accessibility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ struct Accessibility {
// Show the system "grant Accessibility" prompt. This lets macOS register the
// correct (current) binary itself, instead of relying on a manual drag-in
// that TCC churn keeps invalidating.
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
// Literal value of kAXTrustedCheckOptionPrompt; Swift 6 rejects reading that global var.
let options = ["AXTrustedCheckOptionPrompt": true] as CFDictionary
_ = AXIsProcessTrustedWithOptions(options)
}
}
10 changes: 5 additions & 5 deletions Maccy/Actions/ActionRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Defaults
import Foundation

// MatchMode: unchanged contract.
enum MatchMode: String, Codable, CaseIterable, Identifiable {
nonisolated enum MatchMode: String, Codable, CaseIterable, Identifiable {
case all // AND
case any // OR

Expand All @@ -12,14 +12,14 @@ enum MatchMode: String, Codable, CaseIterable, Identifiable {
}

// A single condition referencing a provider by id.
struct RuleCondition: Codable, Identifiable, Hashable {
nonisolated struct RuleCondition: Codable, Identifiable, Hashable {
var id: UUID = UUID()
var provider: String // e.g. "builtin.kind", "com.maccyplus.soft-wrap"
var params: JSONValue = .object([:])
}

// Persisted configuration for one action within a rule.
struct ActionConfig: Codable, Identifiable, Hashable {
nonisolated struct ActionConfig: Codable, Identifiable, Hashable {
var id: UUID = UUID()
var provider: String // e.g. "builtin.openURL", "com.maccyplus.unwrap"
var params: JSONValue = .object([:])
Expand All @@ -37,7 +37,7 @@ struct ActionConfig: Codable, Identifiable, Hashable {

// A user-defined rule: when its conditions match, its (ordered) actions become
// available. The first action is the default.
struct ActionRule: Codable, Identifiable, Hashable, Defaults.Serializable {
nonisolated struct ActionRule: Codable, Identifiable, Hashable, Defaults.Serializable {
var id: UUID = UUID()
var schemaVersion: Int = 3
var name: String = "New rule"
Expand All @@ -47,7 +47,7 @@ struct ActionRule: Codable, Identifiable, Hashable, Defaults.Serializable {
var actions: [ActionConfig] = []
var autoRunDefault: Bool = false

static let presets: [ActionRule] = [
@MainActor static let presets: [ActionRule] = [
// Open links — kind == url → openURL or webSearch
ActionRule(
name: "Open links",
Expand Down
10 changes: 5 additions & 5 deletions Maccy/Actions/ActionsCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ enum ActionsCLI {
// Built from the LIVE provider registry so the catalog can't drift from the
// installed providers. `ProviderRegistry` is @MainActor; enter synchronously
// (the CLI has no run loop). `registerProviders()` ran in `run(_:)` first.
let catalog: [String: Any] = MainActor.assumeIsolated {
func encode(_ descriptors: [ProviderDescriptor]) -> [[String: Any]] {
let catalog: [String: Any] = {
@MainActor func encode(_ descriptors: [ProviderDescriptor]) -> [[String: Any]] {
descriptors.map { descriptor -> [String: Any] in
[
"id": descriptor.id,
Expand Down Expand Up @@ -235,7 +235,7 @@ enum ActionsCLI {
"runs that action unconditionally on the most recent clip.",
"defaultTerminalApps": TerminalApps.defaults
]
}
}()

return emitJSONObject(catalog)
}
Expand Down Expand Up @@ -303,7 +303,7 @@ enum ActionsCLI {
}

private static func pluginsList() -> Int32 {
let payload: [String: Any] = MainActor.assumeIsolated {
let payload: [String: Any] = {
var packages: [String: [String: Any]] = [:]
for d in ProviderRegistry.shared.descriptors() {
let pkgID = d.pluginID ?? "builtin"
Expand All @@ -324,7 +324,7 @@ enum ActionsCLI {
"localFolders": Defaults[.localMarketplaceFolders],
"marketplaces": MarketplaceStore.shared.registeredMarketplaceURLs().map(\.absoluteString)
]
}
}()
return emitJSONObject(payload)
}

Expand Down
2 changes: 1 addition & 1 deletion Maccy/Actions/KeyboardLayoutFixer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Foundation
// between US-QWERTY and Israeli SI-1452. Direction is auto-detected by script
// count. Ported from the standalone KeyLayoutSwitcher engine; dumb +
// deterministic, with no per-word plausibility scoring.
enum KeyboardLayoutFixer {
nonisolated enum KeyboardLayoutFixer {
// English-typed character -> Hebrew character the user intended.
static let enToHe: [Character: Character] = [
// letters lowercase
Expand Down
37 changes: 22 additions & 15 deletions Maccy/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Sparkle
import SwiftUI

class AppDelegate: NSObject, NSApplicationDelegate {
static let isTesting = CommandLine.arguments.contains("enable-testing")
nonisolated static let isTesting = CommandLine.arguments.contains("enable-testing")
var panel: FloatingPanel<ContentView>!

@objc
Expand All @@ -22,10 +22,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// dynamic `title` (recent copy text) so VoiceOver always announces something
// meaningful even when that preference is off or the app is disabled.
private func updateStatusItemAccessibilityLabel() {
let base = NSLocalizedString("status_item_accessibility_label", comment: "")
statusItem.button?.setAccessibilityLabel(
isStatusItemDisabled ? "\(base) — \(NSLocalizedString("status_item_disabled_accessibility_suffix", comment: ""))" : base
)
var accessibilityLabel = NSLocalizedString("status_item_accessibility_label", comment: "")
if isStatusItemDisabled {
accessibilityLabel += " — \(NSLocalizedString("status_item_disabled_accessibility_suffix", comment: ""))"
}
statusItem.button?.setAccessibilityLabel(accessibilityLabel)
}

private var isStatusItemDisabled: Bool {
Expand Down Expand Up @@ -192,6 +193,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
_ = try? Storage.shared.sanitizeTitles()
}

ensureMigration(key: "2026-09-15-remove-unpersistable-contents") {
_ = try? Storage.shared.removeUnpersistableContents()
}

// The following defaults are not used in Maccy 2.x
// and should be removed in 3.x.
// - LaunchAtLogin__hasMigrated
Expand All @@ -203,18 +208,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {

@objc
private func performStatusItemClick() {
if let event = NSApp.currentEvent {
let modifierFlags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
let modifierFlags = (NSApp.currentEvent?.modifierFlags ?? [])
.union(NSEvent.modifierFlags)
.intersection(.deviceIndependentFlagsMask)

if modifierFlags.contains(.option) {
Defaults[.ignoreEvents].toggle()
if modifierFlags.contains(.option) {
Defaults[.ignoreEvents].toggle()

if modifierFlags.contains(.shift) {
Defaults[.ignoreOnlyNextEvent] = Defaults[.ignoreEvents]
}

return
if modifierFlags.contains(.shift) {
Defaults[.ignoreOnlyNextEvent] = Defaults[.ignoreEvents]
}

return
}

panel.toggle(height: AppState.shared.popup.height, at: .statusItem)
Expand Down Expand Up @@ -242,7 +247,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
object: nil,
queue: nil
) { notification in
if let name = notification.userInfo?["name"] as? KeyboardShortcuts.Name, names.contains(name) {
guard let name = notification.userInfo?["name"] as? KeyboardShortcuts.Name,
names.contains(name) else { return }
MainActor.assumeIsolated {
KeyboardShortcuts.disable(name)
}
}
Expand Down
26 changes: 12 additions & 14 deletions Maccy/ApplicationImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,21 @@ class ApplicationImage {
let source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: descriptor,
eventMask: [.write, .delete],
queue: DispatchQueue.global()
queue: .main
)
eventSource = source
source.setEventHandler {
DispatchQueue.main.async {
let event = source.data
if event.contains(.delete) {
// File was deleted.
print("Deleted", appURL.path)
source.cancel()
self.image = nil
self.lastChecked = nil
} else if event.contains(.write) {
// File was modified. Fetch new icon
print("Modified", appURL.path)
self.image = NSWorkspace.shared.icon(forFile: appURL.path)
}
let event = source.data
if event.contains(.delete) {
// File was deleted.
print("Deleted", appURL.path)
source.cancel()
self.image = nil
self.lastChecked = nil
} else if event.contains(.write) {
// File was modified. Fetch new icon
print("Modified", appURL.path)
self.image = NSWorkspace.shared.icon(forFile: appURL.path)
}
}
source.setCancelHandler {
Expand Down
13 changes: 11 additions & 2 deletions Maccy/Clipboard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class Clipboard {
.concealed,
.transient
]
private let unpersistableTypes: Set<NSPasteboard.PasteboardType> = [
.appleWebArchive,
.safariWebArchve
]

private var enabledTypes: Set<NSPasteboard.PasteboardType> { Defaults[.enabledPasteboardTypes] }
private var disabledTypes: Set<NSPasteboard.PasteboardType> { supportedTypes.subtracting(enabledTypes) }
Expand Down Expand Up @@ -62,6 +66,12 @@ class Clipboard {
start()
}

func isPersistable(_ type: NSPasteboard.PasteboardType) -> Bool {
return !unpersistableTypes.contains(type) &&
!type.rawValue.hasPrefix(microsoftSourcePrefix) &&
!type.rawValue.hasPrefix(dynamicTypePrefix)
}

@MainActor
func copyInMaccy(_ string: String) {
pasteboard.clearContents()
Expand Down Expand Up @@ -198,8 +208,7 @@ class Clipboard {

types = types
.subtracting(disabledTypes)
.filter { !$0.rawValue.starts(with: dynamicTypePrefix) }
.filter { !$0.rawValue.starts(with: microsoftSourcePrefix) }
.filter(isPersistable)

// Avoid reading Microsoft Word links from bookmarks and cross-references.
// https://github.com/p0deje/Maccy/issues/613
Expand Down
45 changes: 45 additions & 0 deletions Maccy/ConcatenatedCollection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/// A random-access view over two arrays that does not allocate combined storage.
nonisolated struct ConcatenatedCollection<Element>: RandomAccessCollection {
typealias Index = Int

private let leading: [Element]
private let trailing: [Element]

init(_ leading: [Element], _ trailing: [Element]) {
self.leading = leading
self.trailing = trailing
}

var startIndex: Int { 0 }
var endIndex: Int { leading.count + trailing.count }

subscript(position: Int) -> Element {
precondition(position >= startIndex && position < endIndex, "Index out of bounds")
if position < leading.count {
return leading[position]
}
return trailing[position - leading.count]
}

func index(after index: Int) -> Int {
index + 1
}

func index(before index: Int) -> Int {
index - 1
}

func index(_ index: Int, offsetBy distance: Int) -> Int {
index + distance
}

func distance(from start: Int, to end: Int) -> Int {
end - start
}

func toArray() -> [Element] {
leading + trailing
}
}

extension ConcatenatedCollection: Equatable where Element: Equatable {}
Loading
Loading