Skip to content

Modernization to Swift & SwuiftUI#46

Open
juancavallotti wants to merge 43 commits intonaotaka:masterfrom
juancavallotti:master
Open

Modernization to Swift & SwuiftUI#46
juancavallotti wants to merge 43 commits intonaotaka:masterfrom
juancavallotti:master

Conversation

@juancavallotti
Copy link
Copy Markdown

Hello!

I have been using this app for many years and it finally broke with MacOSX 26, and I felt it was only fair to modernize it so I can continue using it but I also wanted to contribute it back to you as a thank you for all what you've helped me these last 14 years.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request modernizes ClipMenu by migrating the codebase from legacy Objective-C to Swift, utilizing SwiftUI for the UI and SwiftData for persistence. The changes include a new event-driven clipboard pipeline, global hotkey support, and a JavaScript-based action system. Reviewers identified several improvement opportunities: securing the script engine against code injection by avoiding string interpolation, resolving concurrency violations when accessing the ModelContext, and replacing fragile retry logic in the AppDelegate. Additionally, suggestions were made to use type-safe enums instead of magic numbers and to implement a more robust hashing strategy for image data to prevent collisions.

Comment on lines +30 to +34
let wrapped = """
function __wrapper(clipText, clip) {
try { \(script) } catch(e) { __scriptException = e.toString(); return; }
}
"""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using string interpolation to inject the script source into a JavaScript function wrapper is risky. If the script content contains characters that break the JavaScript syntax (like a closing brace }), it will cause a syntax error or potentially allow for code injection. It is safer to pass the script as a variable to the JS context and use eval() or JSValue.call(withArguments:) to execute it.

Suggested change
let wrapped = """
function __wrapper(clipText, clip) {
try { \(script) } catch(e) { __scriptException = e.toString(); return; }
}
"""
context.setObject(script, forKeyedSubscript: "__userScript" as NSString)
let wrapped = """
function __wrapper(clipText, clip) {
try { eval(__userScript) } catch(e) { __scriptException = e.toString(); return; }
}
"""

Comment on lines +31 to +38
func rootActions() async -> [ActionNode] {
guard let context else { return [] }
let descriptor = FetchDescriptor<ActionNode>(
predicate: #Predicate<ActionNode> { $0.parent == nil },
sortBy: [SortDescriptor(\ActionNode.sortIndex)]
)
return (try? context.fetch(descriptor)) ?? []
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The ActionService actor is accessing the ModelContext (which is the mainContext from AppRuntime) directly on its own executor. Since mainContext is isolated to the MainActor, this is a concurrency violation. These fetches should be performed on the MainActor or, preferably, ActionService should use its own background ModelContext created from the ModelContainer.

    func rootActions() async -> [ActionNode] {
        guard let context else { return [] }
        return await MainActor.run {
            let descriptor = FetchDescriptor<ActionNode>(
                predicate: #Predicate<ActionNode> { $0.parent == nil },
                sortBy: [SortDescriptor(\ActionNode.sortIndex)]
            )
            return (try? context.fetch(descriptor)) ?? []
        }
    }

Comment on lines +27 to +35
private func startDataServicesWhenReady(retryCount: Int) {
guard let modelContext = runtime.modelContainer?.mainContext else {
guard retryCount > 0 else { return }
Task { @MainActor in
try? await Task.sleep(nanoseconds: 100_000_000)
startDataServicesWhenReady(retryCount: retryCount - 1)
}
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The retry logic for waiting for the ModelContainer is fragile and likely unnecessary. Since runtime.modelContainer is initialized synchronously in ClipMenuApp.init(), it should be available by the time applicationDidFinishLaunching is called. If there is a concern about initialization timing, consider using a more robust observation mechanism or ensuring the container is ready before the AppDelegate methods are invoked.

guard let files = entry.filenames, !files.isEmpty else { return }
let hfsPaths = files.compactMap { posixPath -> String? in
let url = URL(fileURLWithPath: posixPath) as CFURL
return CFURLCopyFileSystemPath(url, CFURLPathStyle(rawValue: 1)!) as String?
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using magic numbers for CFURLPathStyle. Use the named enum case .cfurlhfsPathStyle for better readability and type safety. Also, the force unwrap ! should be avoided if possible.

Suggested change
return CFURLCopyFileSystemPath(url, CFURLPathStyle(rawValue: 1)!) as String?
return CFURLCopyFileSystemPath(url, .cfurlhfsPathStyle) as String?

Comment on lines +41 to +43

if let imageData {
h ^= imageData.count
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using imageData.count as part of the contentHash is a very weak way to differentiate images and will lead to collisions for different images of the same byte size. Consider using a more robust hashing method, such as the hashValue of the Data object (if stability across restarts is not required) or a proper checksum/hash (like SHA256) for stable deduplication.

@Frazer
Copy link
Copy Markdown

Frazer commented Apr 5, 2026

Awesome update!
I was just in the process of doing the same, when I thought to check the PRs to see if anyone has already done it.

naotaka hasn't been active for many years on GitHub, so I wonder if there is some way to surface this if the PR is never merged?
Publish as a fork? Rebrand and publish?

I also want to add in a search/filter function. I'll send you a PR if i get it to work. (I dont know how to code in swift, so perhaps this is something you could do easily)

@juancavallotti
Copy link
Copy Markdown
Author

Oh you can probably and easily do it with Claude code, and I agree on the rebrand, that's what the license says we should do anyway if we continue working on it in my repo.

@Frazer
Copy link
Copy Markdown

Frazer commented Apr 14, 2026

I spent all week on this
juancavallotti#1

And 2 more features I've been trying to get working will come in a follow up PR:
adding the ability to filter/search your clips, while keyboard shortcuts still work to jump to clips
adding previews of long text and images, and improving submenus opening direction.

I've actually poured so much in to this - also because I've loved this app for so long and wondered what happened to Naotaka. How/Who publishes the new branded version? If you publish, I'd love to be included in the credits, and maybe the charity we run called United Visions. Or if I publish it, I have to credit you, so would love to know what you want it to say.

I signed up as an Apple developer just to work on this. Sorry the PR includes taking out your team signing, and setting it up for generic dev building. I'm not really sure how this is supposed to be handled in xcode projects.

Names, I was thinking
ClipMenu - Arm Edition
MChipClip
MClipper
MClip

@naotaka if you are around, or if any of Naotaka's friends are around, I'd love to hear your thoughts.

and Juan, this is my first bigger open source project, so I hope I did the question about publishing and building in the right spirit.

@tommyrharper
Copy link
Copy Markdown

@juancavallotti @Frazer I just got a notification from apple that with the latest update Rosetta will be depreciated (causing the loss of support for running intel based apps on apple silicon) and thus ClipMenu will be depreciated. I also have been a loyal ClipMenu user for many years, and would like to revive this project.

Where are you at with re-publishing it? Did you create a new repo? I would be happy to contribute in brining across apple silicon support so ClipMenu can survive.

In regards to naming could call it "Jiffy", because it helps you navigate your clipboard in a jiffy. It has a bit more pizazz to it, and might help with the branding in getting broader support for the tool.

@juancavallotti
Copy link
Copy Markdown
Author

@tommyrharper I do have a published version with the newer stack here https://github.com/juancavallotti/ClipMenu and @Frazer is working through a pull request with featues he added.

@Elmolesto
Copy link
Copy Markdown

Hi everyone — I find it really interesting that several long-time ClipMenu power users seem to be showing up here around the same time. It says a lot about how useful this little app still is after all these years.

I’m currently rewriting the app from scratch, trying to stay very faithful to the original feel: native menu bar app, contextual menu, clipboard history, snippets, and keyboard-first workflow, but updated for current macOS.

I know the original README asks derivatives not to use the “ClipMenu” name. That said, given that Naotaka Morimoto does not seem to have been active for many years, I wonder if “ClipMenu2” could be a reasonable and respectful name for a continuation/revival — clearly not the original app, but a successor inspired by it.

In any case, I’m really happy to see people working on keeping this workflow alive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants