Hammerspoon: hs.urldirector

Created on 3 Apr 2015  Â·  28Comments  Â·  Source: Hammerspoon/hammerspoon

I want to be able to click on URLs and choose which browser they open with. We could do this with the various features of https://developer.apple.com/library/mac/documentation/Carbon/Reference/LaunchServicesReference/#//apple_ref/c/func/LSCopyApplicationURLsForURL and some (optional) UI.

extension suggestion

Most helpful comment

All tested and done, there is now enough API to write a pretty URL director. Here is my quick Lua implementation of such a thing. Perhaps @lowne can sprinkle some magic on it :D

-- URL director
-- This makes Hammerspoon take over as the default http/https handler
-- Whenever a URL is opened, Hammerspoon will draw all of the app icons which can handle URLs and let the user choose where to direct the URL
hs.urlevent.httpCallback = function(scheme, host, params, fullURL)
    print("URL Director: "..fullURL)

    local screen = hs.screen.mainScreen():frame()
    local handlers = hs.urlevent.getAllHandlersForScheme(scheme)
    local numHandlers = #handlers
    local modalKeys = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
                       "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P",
                       "A", "S", "D", "F", "G", "H", "J", "K", "L",
                       "Z", "X", "C", "V", "B", "N", "M"}

    local boxBorder = 10
    local iconSize = 96

    if numHandlers > 0 then
        local appIcons = {}
        local appNames = {}
        local modalDirector = hs.hotkey.modal.new()
        local x = screen.x + (screen.w / 2) - (numHandlers * iconSize / 2)
        local y = screen.y + (screen.h / 2) - (iconSize / 2)
        local box = hs.drawing.rectangle(hs.geometry.rect(x - boxBorder, y - boxBorder, (numHandlers * iconSize) + (boxBorder * 2), iconSize + (boxBorder * 4)))
        box:setFillColor({["red"]=0,["blue"]=0,["green"]=0,["alpha"]=0.5}):setFill(true):show()

        local exitDirector = function(bundleID, url)
            if (bundleID and url) then
                hs.urlevent.openURLWithBundle(url, bundleID)
            end
            for _,icon in pairs(appIcons) do
                icon:delete()
            end
            for _,name in pairs(appNames) do
                name:delete()
            end
            box:delete()
            modalDirector:exit()
        end

        for num,handler in pairs(handlers) do
            local appIcon = hs.drawing.appImage(hs.geometry.size(iconSize, iconSize), handler)
            if appIcon then
                local appName = hs.drawing.text(hs.geometry.size(iconSize, boxBorder), modalKeys[num].." "..hs.application.nameForBundleID(handler))

                table.insert(appIcons, appIcon)
                table.insert(appNames, appName)

                appIcon:setTopLeft(hs.geometry.point(x + ((num - 1) * iconSize), y))
                appIcon:setClickCallback(function() exitDirector(handler, fullURL) end)
                appIcon:orderAbove(box)
                appIcon:show()

                appName:setTopLeft(hs.geometry.point(x + ((num - 1) * iconSize), y + iconSize))
                appName:setTextStyle({["size"]=10,["color"]={["red"]=1,["blue"]=1,["green"]=1,["alpha"]=1},["alignment"]="center",["lineBreak"]="truncateMiddle"})
                appName:orderAbove(box)
                appName:show()

                modalDirector:bind({}, modalKeys[num], function() exitDirector(handler, fullURL) end)
            end
        end

        modalDirector:bind({}, "Escape", exitDirector)
        modalDirector:enter()
    end
end
hs.urlevent.setDefaultHandler('http')

All 28 comments

So the idea would be that we choose to register as the default handler for one or more URL schemes of the user's choice, then when we are invoked with one of those URLs, we do one of two things:

  • Call out to a user provided callback which decides what to do
  • Look up the applications which can handle that URL scheme and present a nice UI with a choice of their icons and a quick keyboard shortcut for each

The building blocks for the second thing would be available to the first thing, so the user's callback could include some rules of their choosing, e.g. unconditionally opening URLs containing google.com with Chrome, with the ability to fall back on looking up the applications registered for the URL scheme and presenting the nice UI.

Ohh very Androidy. i like it :)

Seems like this may not actually be practicable, simply because we can't register URL handlers at runtime, they have to be hard coded in the app's Info.plist, and I don't think we should be in the business of rewriting that file dynamically :(

Are you certain that's the only way?

I still have RCDefaultApp installed which is supposed to let you change the default apps for extensions and url types... haven't really used it in ages, it has sort of followed my home directory on my laptop since about Snow Leopard, so all I can really say about it is that opening it up doesn't crash Yosemite...

But I sincerely doubt it was rewriting info lists across the system... a cache or database combination of them, maybe... but...

At any rate, you might give it a look and see if it sparks any ideas:

http://www.rubicode.com/Software/RCDefaultApp/ http://www.rubicode.com/Software/RCDefaultApp/

On Jun 23, 2015, at 2:33 AM, Chris Jones [email protected] wrote:

Seems like this may not actually be practicable, simply because we can't register URL handlers at runtime, they have to be hard coded in the app's Info.plist, and I don't think we should be in the business of rewriting that file dynamically :(

—
Reply to this email directly or view it on GitHub https://github.com/Hammerspoon/hammerspoon/issues/257#issuecomment-114391114.

@asmagill That is setting the default app for a given URL/Mime/etc type. My thought was that we'd dynamically register ourselves as a handler for arbitrary URL types and let the user code up a dispatch mechanism to the apps that actually consume those URL types.

I suppose we could just register ourselves for a selection of typical default URL types and try to avoid becoming the default for any of them, unless the user specifically asks for that.

I see the distinction...

A first step would be to allow a user to select a file in the finder and select Hammerspoon via "Open with..."... having done absolutely zero research into this so far, i'm assuming Hammerspoon would have to register some sort of handler to accept external files at the application level (similar to how notifications that start up a currently closed application work... I forget the exact message sent to NSApplication or it's delegate for this, but I was looking into it at one point)... this could then trigger a lua callback maybe with a path to the file... i can think of a few things I might like to do with that... of course setting up URL/MimeTypes to make HS the preferred opener would be useful too, but that's secondary to being able to do it at all.

Just my thoughts... I don't think we should throw this idea out yet... maybe morph it a bit, but... it has potential in my opinion.

On Jun 23, 2015, at 3:53 AM, Chris Jones [email protected] wrote:

@asmagill https://github.com/asmagill That is setting the default app for a given URL/Mime/etc type. My thought was that we'd dynamically register ourselves as a handler for arbitrary URL types and let the user code up a dispatch mechanism to the apps that actually consume those URL types.

I suppose we could just register ourselves for a selection of typical default URL types and try to avoid becoming the default for any of them, unless the user specifically asks for that.

—
Reply to this email directly or view it on GitHub https://github.com/Hammerspoon/hammerspoon/issues/257#issuecomment-114410689.

So I think what we'd want to do is have API that lets people choose to make Hammerspoon the default handler for a particular type of URL and what their fallback choice of app is. The reason for that being that if we're not running, we won't be able to respond to the URL opening event, so something like:

hs.urldirector.registerCallbackWithFallback("http", function(url) blah end, "com.apple.Safari")

that way, in __gc we can revert the default http:// handler to Safari. We could also then have hs.urldirector.unregister("http") if the user wants to use that, and we would force them to think about the "what happens when Hammerspoon isn't set up to respond" situation.

This extension would also present the challenge of the user being able to select an appropriate handler with some UI. I'm somewhat tempted to have an hs.choose extension which copies the behaviour of https://github.com/sdegutis/choose

(because @sdegutis writes all the best things :)

@asmagill yeah I think file types is somewhat easier because we can register more globally for them. My primary interest is for URLs, because I often have two browsers open and have no way to click on a link in some other app and have it go to a browser of my choice :)

I've been playing with some basic user input via hs.hotkey and hs.drawing... it's _very_ basic right now... (check https://github.com/asmagill/hammerspoon-config/blob/master/utils/fontTables.lua https://github.com/asmagill/hammerspoon-config/blob/master/utils/fontTables.lua, https://github.com/asmagill/hammerspoon-config/blob/master/utils/typee.lua https://github.com/asmagill/hammerspoon-config/blob/master/utils/typee.lua and https://github.com/asmagill/hammerspoon-config/blob/master/utils/_keys/fonts.lua https://github.com/asmagill/hammerspoon-config/blob/master/utils/_keys/fonts.lua, if you're interested)... hence my interest in redoing hs.hotkey right now :-)

I think with some adjustments and additions to hs.drawing, or maybe a more generic module that uses hs.drawing but wraps multiple objects into one container object, we can get some basic ui elements... I'm not against porting his chooser, but I hate to see something that narrowly focused when something a little more generic and powerful can probably be done... Of course, I only started looking at his chooser a few minutes ago, so I reserve the right to change my mind!

It's kind of like hs.hints... love what it does, but I would also love a more generic way to get/use/place application icons than writing a whole new module to do it... we didn't have hs.drawing before, so hints being single and wrapped up like it is made sense... now that we maybe have a place for graphic elements, pulling out bits like the icons, or at least exposing them in the hints module, and making them more generally available means others wouldn't have to re-invent the wheel or go through his code and replicate it... just to throw out an example, I've been mulling over something like having multiple pop-up docks or drawers containing application and document icons and will likely reuse some of the code from hints... less wasteful if we both have one place to go to.

I'll break this out into multiple issues later today... it seems I've strayed a bit! And I'll look closer at his chooser -- the functionality is needed, but I wonder if a standalone module or a module using existing building blocks will be better.

On Jun 23, 2015, at 4:17 AM, Chris Jones [email protected] wrote:

So I think what we'd want to do is have API that lets people choose to make Hammerspoon the default handler for a particular type of URL and what their fallback choice of app is. The reason for that being that if we're not running, we won't be able to respond to the URL opening event, so something like:

hs.urldirector.registerCallbackWithFallback("http", function(url) blah end, "com.apple.Safari")

that way, in __gc we can revert the default http:// handler to Safari. We could also then have hs.urldirector.unregister("http") if the user wants to use that, and we would force them to think about the "what happens when Hammerspoon isn't set up to respond" situation.

This extension would also present the challenge of the user being able to select an appropriate handler with some UI. I'm somewhat tempted to have an hs.choose extension which copies the behaviour of https://github.com/sdegutis/choose https://github.com/sdegutis/choose
—
Reply to this email directly or view it on GitHub https://github.com/Hammerspoon/hammerspoon/issues/257#issuecomment-114416835.

@asmagill I was actually just thinking a few minutes ago that we could pretty easily make hs.drawing.appImage() that would take a bundle ID, fetch the icon and make an hs.drawing.image() with it :)

Hi, I just wanted to chime in that I would love this feature. I recently started using hammerspoon and I've already replaced a lot of other apps with it. One that I haven't quite been able to get rid of is ControlPlane, and that's mostly because ControlPlane can be registered as a default web browser. Then whenever a link is clicked in another application, ControlPlane runs the rules I've set up for it and that determines which actual web browser will open the link. I'd love to be able to do this with hammerspoon instead, using a custom function.

Rather than set this up in a separate hs.urldirector extension (mainly for technical reasons), I've got an initial implementation of the required API in hs.urlevent. You can make Hammerspoon take over the default handling of http/https URLs and supply a callback function that gets called when URLs are opened. There is then also a function that lets you open an arbitrary URL with a specified application.

I also implemented the code that @asmagill mentioned somewhere, to cache a URL used to launch Hammerspoon.

It needs a bit more testing, and I want to implement an example Lua setup that mimics hs.hints in terms of showing some UI for forwarding a URL, then I'll push the code up for the next release :)

That's excellent news! I'm looking forward to the next release! :)

All tested and done, there is now enough API to write a pretty URL director. Here is my quick Lua implementation of such a thing. Perhaps @lowne can sprinkle some magic on it :D

-- URL director
-- This makes Hammerspoon take over as the default http/https handler
-- Whenever a URL is opened, Hammerspoon will draw all of the app icons which can handle URLs and let the user choose where to direct the URL
hs.urlevent.httpCallback = function(scheme, host, params, fullURL)
    print("URL Director: "..fullURL)

    local screen = hs.screen.mainScreen():frame()
    local handlers = hs.urlevent.getAllHandlersForScheme(scheme)
    local numHandlers = #handlers
    local modalKeys = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
                       "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P",
                       "A", "S", "D", "F", "G", "H", "J", "K", "L",
                       "Z", "X", "C", "V", "B", "N", "M"}

    local boxBorder = 10
    local iconSize = 96

    if numHandlers > 0 then
        local appIcons = {}
        local appNames = {}
        local modalDirector = hs.hotkey.modal.new()
        local x = screen.x + (screen.w / 2) - (numHandlers * iconSize / 2)
        local y = screen.y + (screen.h / 2) - (iconSize / 2)
        local box = hs.drawing.rectangle(hs.geometry.rect(x - boxBorder, y - boxBorder, (numHandlers * iconSize) + (boxBorder * 2), iconSize + (boxBorder * 4)))
        box:setFillColor({["red"]=0,["blue"]=0,["green"]=0,["alpha"]=0.5}):setFill(true):show()

        local exitDirector = function(bundleID, url)
            if (bundleID and url) then
                hs.urlevent.openURLWithBundle(url, bundleID)
            end
            for _,icon in pairs(appIcons) do
                icon:delete()
            end
            for _,name in pairs(appNames) do
                name:delete()
            end
            box:delete()
            modalDirector:exit()
        end

        for num,handler in pairs(handlers) do
            local appIcon = hs.drawing.appImage(hs.geometry.size(iconSize, iconSize), handler)
            if appIcon then
                local appName = hs.drawing.text(hs.geometry.size(iconSize, boxBorder), modalKeys[num].." "..hs.application.nameForBundleID(handler))

                table.insert(appIcons, appIcon)
                table.insert(appNames, appName)

                appIcon:setTopLeft(hs.geometry.point(x + ((num - 1) * iconSize), y))
                appIcon:setClickCallback(function() exitDirector(handler, fullURL) end)
                appIcon:orderAbove(box)
                appIcon:show()

                appName:setTopLeft(hs.geometry.point(x + ((num - 1) * iconSize), y + iconSize))
                appName:setTextStyle({["size"]=10,["color"]={["red"]=1,["blue"]=1,["green"]=1,["alpha"]=1},["alignment"]="center",["lineBreak"]="truncateMiddle"})
                appName:orderAbove(box)
                appName:show()

                modalDirector:bind({}, modalKeys[num], function() exitDirector(handler, fullURL) end)
            end
        end

        modalDirector:bind({}, "Escape", exitDirector)
        modalDirector:enter()
    end
end
hs.urlevent.setDefaultHandler('http')

It looks like this:
screenshot 2015-09-03 20 52 32

I actually suspect that a more useful implementation would hard code a few bundle identifiers for the browsers the user wants to use. I was just exercising the API by displaying all matching applications. I have no idea why I would ever want to open a URL with iTerm or Parallels :)

That looks perfect! That URL Director with app icons is very cool, too. My own use case is much more modest. I'm just going to be using an ApplicationWatcher to switch the default browser whenever a browser becomes active. So basically, links open in the last browser I was using. Thanks for implementing this!

I left a comment on the code the other day but I'm not sure if you guys get notified by those. There's a small typo here that would prevent https links from working, I believe. Since I see you're getting ready for the next release I thought I'd mention it again.

@ventolin thanks. 0.9.40 is out with the updated URL handling and it seems to work even with that typo!

Well good! I wasn't sure.

@cmsj , small fix (admittedly due to messing with laptop migration) in your quick and dirty but totally awesome example which eliminated yet another application "MultiBrowser" for me.

local appIcon = hs.drawing.appImage(hs.geometry.size(iconSize, iconSize), handler)
if appIcon then
    local appName = hs.drawing.text(hs.geometry.size(iconSize, boxBorder), modalKeys[num].." "..hs.application.nameForBundleID(handler))

to

local appIcon = hs.drawing.appImage(hs.geometry.size(iconSize, iconSize), handler)
local name = hs.application.nameForBundleID(handler)
if appIcon and name then
    local appName = hs.drawing.text(hs.geometry.size(iconSize, boxBorder), modalKeys[num].." "..name)

@junkblocker nice :)

@ventolin any chance you could share that simpler URL switcher, as I've been looking for an application with that specific functionality.

@ventolin I've been trying to get your browser.lua working for months. And tried to figure out all it's dependencies too. But no luck, I even set hammerspoon as default browser through macOS. Anything you think I could be missing?

FWIW, that browser.lua would be perfect to port to being a Spoon so it can Just Work for everyone. It oughtn't to be too hard :)

I'd love that. so a +1 from here.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

latenitefilms picture latenitefilms  Â·  3Comments

asmagill picture asmagill  Â·  4Comments

reestr picture reestr  Â·  3Comments

agzam picture agzam  Â·  3Comments

aaronjensen picture aaronjensen  Â·  3Comments