Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@nathankerr
Last active December 19, 2023 08:47
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save nathankerr/38d8b0d45590741b57f5f79be336f07c to your computer and use it in GitHub Desktop.
Save nathankerr/38d8b0d45590741b57f5f79be336f07c to your computer and use it in GitHub Desktop.
Registering a Go app as a protocol handler under Mac OS X
#import <Foundation/Foundation.h>
extern void HandleURL(char*);
@interface GoPasser : NSObject
+ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event;
@end
void StartURLHandler(void);
#include "handler.h"
@implementation GoPasser
+ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
{
HandleURL([[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]);
}
@end
void StartURLHandler(void) {
NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager];
[appleEventManager setEventHandler:[GoPasser class]
andSelector:@selector(handleGetURLEvent:)
forEventClass:kInternetEventClass andEventID:kAEGetURL];
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>myapp</string>
<key>CFBundleIdentifier</key>
<string>com.pocketgophers.myapp</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.pocketgophers.myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>MyApp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>20</string>
</dict>
</plist>
package main
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#include "handler.h"
*/
import "C"
import (
"log"
"github.com/andlabs/ui"
)
// Sources used to figure this out:
// Info.plist: https://gist.github.com/chrissnell/db95a3c5ad6ceca4c673e96cca0f7548
// custom url handler example: http://fredandrandall.com/blog/2011/07/30/how-to-launch-your-macios-app-with-a-custom-url/
// obj-c example: https://gist.github.com/leepro/016d7d6b61021dfc67daf61771c92b3c
// note: import .h, not .m
var labelText chan string
func main() {
log.SetFlags(log.Lshortfile)
labelText = make(chan string, 1) // the event handler blocks!, so buffer the channel at least once to get the first message
C.StartURLHandler()
err := ui.Main(func() {
greeting := ui.NewLabel("greeting\nline 2")
window := ui.NewWindow("Hello", 200, 100, true)
window.SetChild(greeting)
window.OnClosing(func(*ui.Window) bool {
ui.Quit()
return true
})
window.Show()
go func() {
for text := range labelText {
ui.QueueMain(func() {
greeting.SetText(text)
})
}
}()
})
if err != nil {
log.Fatal(err)
}
}
//export HandleURL
func HandleURL(u *C.char) {
labelText <- C.GoString(u)
}
myapp.app: *.go *.h *.m Makefile Info.plist
mkdir -p myapp.app/Contents/MacOS
go build -i -o myapp.app/Contents/MacOS/myapp
cp Info.plist myapp.app/Contents/Info.plist
.PHONY: open
open: myapp.app
open myapp.app
.PHONY: scheme
scheme: myapp.app
open myapp://hello/world
.PHONY: clean
clean:
rm -rf myapp.app
@monmohan
Copy link

monmohan commented Apr 26, 2020

Hello Nathan,
I was looking to build a URL handler in go and this is pretty useful and works well. Thanks!
However one of the thing that's not very clear to me is the communication model between the app and OS. If I replace your ui.Main() with just a channel that is consuming from the handler and logging it out to a file, the main() method block and the app keeps spinning. My understanding is that main() should not return and ideally should have a goroutine that is receiving from the labelText channel which the HandleURL() writes to. I have tried standard stuff like using a channel to wait, timer, sleeping the program but in all cases main blocks and program never gets ready for HandlerURL.. How does using the UI avoid this main blocks?
e.g. this blocks -

func main() {
	C.StartURLHandler()
	go func() {
		for text := range labelText {
			logToFile(text)
		}
	}()
	time.Sleep(20 * time.Second)// I don't do this or something else, main exits and all goroutines would too
}

or this or essentially anything that doesn't terminate main

ticks := time.Tick(1 * time.Second)
	for {
		select {
		case text := <-labelText:
	                     doSomething(text)
		case <-ticks:
			//one second elapsed
		}
	}

@ManuelEberhardinger
Copy link

Hello Nathan,
I was looking to build a URL handler in go and this is pretty useful and works well. Thanks!
However one of the thing that's not very clear to me is the communication model between the app and OS. If I replace your ui.Main() with just a channel that is consuming from the handler and logging it out to a file, the main() method block and the app keeps spinning. My understanding is that main() should not return and ideally should have a goroutine that is receiving from the labelText channel which the HandleURL() writes to. I have tried standard stuff like using a channel to wait, timer, sleeping the program but in all cases main blocks and program never gets ready for HandlerURL.. How does using the UI avoid this main blocks?
e.g. this blocks -

func main() {
	C.StartURLHandler()
	go func() {
		for text := range labelText {
			logToFile(text)
		}
	}()
	time.Sleep(20 * time.Second)// I don't do this or something else, main exits and all goroutines would too
}

or this or essentially anything that doesn't terminate main

ticks := time.Tick(1 * time.Second)
	for {
		select {
		case text := <-labelText:
	                     doSomething(text)
		case <-ticks:
			//one second elapsed
		}
	}

I have the same problem. Do you found any solution for this?

@xeijin
Copy link

xeijin commented Aug 29, 2021

@ManuelEberhardinger @monmohan (and more likely, anyone who stumbles across this later)

I used mainthread to workaround the issue by separating out the handler code into its own package, and calling its init function in a go routine.

import (
       "github.com/golang-design/mainthread"
       "example.com/module/ui"
       "example.com/module/handler"
)

func main() { mainthread.Init(fn) }

// fn is the actual main function
func fn() {

	// macos requires UI to be run on the main thread so schedule that here (in my case, a tray icon)
	mainthread.Go(ui.Init)

	// moved protocol handler out of main() and into its own package with an init function
	go handler.Init()

        // execute the rest of your main here
        restOfMain()
}

@gedw99
Copy link

gedw99 commented Mar 21, 2023

fails on mac for me.

on 11.7.4 ( Big Sur )

make
mkdir -p myapp.app/Contents/MacOS
go build -o myapp.app/Contents/MacOS/myapp .
# myapp
handler.m:7:12: warning: passing 'NS_RETURNS_INNER_POINTER const char *' to parameter of type 'char *' discards qualifiers [-Wincompatible-pointer-types-discards-qualifiers]
./handler.h:3:28: note: passing argument to parameter here
handler.m:15:40: error: use of undeclared identifier 'kInternetEventClass'; did you mean 'kCoreEventClass'?
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/CoreServices.framework/Frameworks/AE.framework/Headers/AppleEvents.h:65:3: note: 'kCoreEventClass' declared here
handler.m:15:71: error: use of undeclared identifier 'kAEGetURL'; did you mean 'kAEISGetURL'?
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/CoreServices.framework/Frameworks/AE.framework/Headers/AERegistry.h:829:3: note: 'kAEISGetURL' declared here

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