├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── assets
├── bowser.png
├── bowser.sketch
│ ├── Data
│ ├── QuickLook
│ │ ├── Preview.png
│ │ └── Thumbnail.png
│ ├── metadata
│ └── version
├── dmg-background.png
└── icon.icns
└── src
├── Info.plist
├── handler.h
├── handler.m
├── handleurl.go
├── listen.go
└── main.go
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | .DS_Store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 Net Gusto
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any
4 | purpose with or without fee is hereby granted, provided that the above
5 | copyright notice and this permission notice appear in all copies.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
13 | PERFORMANCE OF THIS SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | Bowser.app: clean Makefile src/*.go src/*.h src/*.m src/*.plist assets/*.icns assets/*.png
2 | mkdir -p dist/Bowser.app/Contents/MacOS dist/Bowser.app/Contents/Resources
3 | cd src && go build -i -o ../dist/Bowser.app/Contents/MacOS/bowser
4 | cp assets/bowser.png dist/Bowser.app/Contents/MacOS
5 | cp src/Info.plist dist/Bowser.app/Contents
6 | cp assets/icon.icns dist/Bowser.app/Contents/Resources
7 |
8 | .PHONY: install
9 | install: Bowser.app
10 | cp -Rf dist/Bowser.app /Applications
11 |
12 | .PHONY: clean
13 | clean:
14 | -rm -Rf dist/Bowser.app
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ⭐ Bowser - Dynamic Default Browser Switch
2 |
3 | Bowser is a MacOS application that selects the browser to open for a URL based on rules you define.
4 |
5 | My use case is to have 2 default browsers : one for development (Chrome), and one for surf (Safari).
6 |
7 | ## Install from binary releases
8 |
9 | Download the latest binary release (`.dmg`) here: https://github.com/netgusto/bowser/releases
10 |
11 | Open the `.dmg` image, drag Bowser to your `/Applications` folder.
12 |
13 | Then follow steps described in "Setup as default browser" below.
14 |
15 | ## Install from source
16 |
17 | The installation from source requires the Apple clang environment (XCode) and Go 1.8+
18 |
19 | ```sh
20 | $ git clone https://github.com/netgusto/bowser
21 | $ cd bowser
22 | $ make install
23 | ```
24 |
25 | Then follow steps described in "Setup as default browser" below.
26 |
27 | ## Setup as default browser
28 |
29 | Bowser has to be defined as default browser to operate.
30 |
31 | Once Bowser is installed in `/Applications`, go to your Mac **System Preferences** > **General** tab, and choose **Bowser** in the list of browsers.
32 |
33 | ## Configuration
34 |
35 | During first run, bowser will create a default config file at `~/.config/bowser/config.yml`.
36 |
37 | ```yml
38 | debug: false
39 | browsers:
40 | - alias: Default
41 | app: Safari
42 | ```
43 |
44 | This default config sets Safari as the default browser. You may now edit the file to add browsers and set regex rules corresponding to your likings.
45 |
46 | Example setup:
47 |
48 | ```yml
49 | debug: false
50 |
51 | browsers:
52 | - alias: Default
53 | app: Safari
54 |
55 | - alias: Dev
56 | app: Google Chrome
57 | match:
58 | - ^https?://127.0.0.1
59 | - ^https?://localhost
60 | ```
61 |
62 | **debug** set to true will forward debug messages to the syslog. Enable only for debugging purposes.
63 |
64 | ## License
65 |
66 | See the LICENSE file.
67 |
--------------------------------------------------------------------------------
/assets/bowser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netgusto/bowser/96ad2509578042eccf4719a14f9ae90fe72059f6/assets/bowser.png
--------------------------------------------------------------------------------
/assets/bowser.sketch/Data:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netgusto/bowser/96ad2509578042eccf4719a14f9ae90fe72059f6/assets/bowser.sketch/Data
--------------------------------------------------------------------------------
/assets/bowser.sketch/QuickLook/Preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netgusto/bowser/96ad2509578042eccf4719a14f9ae90fe72059f6/assets/bowser.sketch/QuickLook/Preview.png
--------------------------------------------------------------------------------
/assets/bowser.sketch/QuickLook/Thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netgusto/bowser/96ad2509578042eccf4719a14f9ae90fe72059f6/assets/bowser.sketch/QuickLook/Thumbnail.png
--------------------------------------------------------------------------------
/assets/bowser.sketch/metadata:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | app
6 | com.bohemiancoding.sketch
7 | build
8 | 5370
9 | commit
10 | 38aeabf36c76d40a8ed2d256f8f3b9492c3dac07
11 | fonts
12 |
13 | length
14 | 21163
15 | version
16 | 18
17 |
18 |
19 |
--------------------------------------------------------------------------------
/assets/bowser.sketch/version:
--------------------------------------------------------------------------------
1 | 18
--------------------------------------------------------------------------------
/assets/dmg-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netgusto/bowser/96ad2509578042eccf4719a14f9ae90fe72059f6/assets/dmg-background.png
--------------------------------------------------------------------------------
/assets/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netgusto/bowser/96ad2509578042eccf4719a14f9ae90fe72059f6/assets/icon.icns
--------------------------------------------------------------------------------
/src/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleExecutable
6 | bowser
7 | CFBundleIconFile
8 | icon.icns
9 | CFBundleIdentifier
10 | com.netgusto.bowser
11 | NSHighResolutionCapable
12 |
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | Bowser
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0.0
21 | CFBundleVersion
22 | 20
23 | LSUIElement
24 |
25 | LSBackgroundOnly
26 |
27 | CFBundleURLTypes
28 |
29 |
30 | CFBundleURLName
31 | http URL
32 | CFBundleURLSchemes
33 |
34 | http
35 |
36 |
37 |
38 | CFBundleURLName
39 | Secure http URL
40 | CFBundleURLSchemes
41 |
42 | https
43 |
44 |
45 |
46 | CFBundleURLName
47 | Local file URL
48 | CFBundleURLSchemes
49 |
50 | file
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/handler.h:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | extern void ReceiveURL(char*);
4 |
5 | @interface GoPasser : NSObject
6 | + (void)handleGetURLEvent:(NSAppleEventDescriptor *)event;
7 | @end
8 |
9 | void StartURLHandler(void);
10 |
--------------------------------------------------------------------------------
/src/handler.m:
--------------------------------------------------------------------------------
1 | #include "handler.h"
2 |
3 | @implementation GoPasser
4 | + (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
5 | {
6 | ReceiveURL([[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]);
7 | }
8 | @end
9 |
10 | void StartURLHandler(void) {
11 | NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager];
12 | [appleEventManager setEventHandler:[GoPasser class]
13 | andSelector:@selector(handleGetURLEvent:)
14 | forEventClass:kInternetEventClass andEventID:kAEGetURL];
15 | }
16 |
--------------------------------------------------------------------------------
/src/handleurl.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 | "path/filepath"
7 | "regexp"
8 |
9 | gosxnotifier "github.com/deckarep/gosx-notifier"
10 | "github.com/skratchdot/open-golang/open"
11 | )
12 |
13 | var devBrowser = "Google Chrome"
14 | var surfBrowser = "Safari"
15 |
16 | func handleURL(config Config, url string) chan interface{} {
17 | wait := make(chan interface{})
18 |
19 | go func() {
20 | log.Println("Received URL")
21 |
22 | var matchedBrowser *Browser
23 |
24 | for _, browser := range config.Browsers {
25 | for _, rgx := range browser.Match {
26 | success, _ := regexp.MatchString(rgx, url)
27 | if success {
28 | matchedBrowser = &browser
29 | break
30 | }
31 | }
32 |
33 | if matchedBrowser != nil {
34 | break
35 | }
36 | }
37 |
38 | if matchedBrowser == nil {
39 | // Looking for defautl browser
40 | for _, browser := range config.Browsers {
41 | if browser.Alias == "Default" {
42 | matchedBrowser = &browser
43 | break
44 | }
45 | }
46 | }
47 |
48 | if matchedBrowser == nil {
49 | // Cannot find default browser; fall back to Safari
50 | open.RunWith(url, "Safari")
51 | // open.RunWith("https://github.com/netgusto/bowser", "Safari")
52 | // open.RunWith("/tmp/hello.txt", "TextEdit")
53 |
54 | notifyProblem(
55 | "Cannot find a default browser - falling back to Safari\nSee https://github.com/netgusto/bowser",
56 | "Bowser - No default browser",
57 | )
58 |
59 | } else {
60 | err := open.RunWith(url, matchedBrowser.App)
61 | if err != nil {
62 | log.Println(err)
63 |
64 | if err2 := open.RunWith(url, "Safari"); err2 == nil {
65 | notifyProblem(
66 | "URL Matched \""+matchedBrowser.Alias+"\" but failed to open \""+matchedBrowser.App+"\" - Falling back to Safari",
67 | "Bowser - Problem with browser "+matchedBrowser.Alias,
68 | )
69 | } else {
70 | notifyProblem(
71 | "URL Matched \""+matchedBrowser.Alias+"\" but failed to open \""+matchedBrowser.App+"\" - Could not fall back to Safari",
72 | "Bowser - Problem with browser "+matchedBrowser.Alias,
73 | )
74 | }
75 | }
76 | }
77 |
78 | wait <- nil
79 | }()
80 |
81 | return wait
82 | }
83 |
84 | func notifyProblem(msg string, title string) {
85 | note := gosxnotifier.NewNotification(msg)
86 | note.Title = title
87 | note.AppIcon = getNotificationIconPath()
88 | note.Link = "https://github.com/netgusto/bowser#setup-default-browser"
89 | note.Push()
90 | }
91 |
92 | func getNotificationIconPath() string {
93 | dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
94 | if err != nil {
95 | log.Fatal(err)
96 | }
97 |
98 | return dir + "/" + "bowser.png"
99 | }
100 |
--------------------------------------------------------------------------------
/src/listen.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // From https://gist.github.com/nathankerr/38d8b0d45590741b57f5f79be336f07c
4 |
5 | /*
6 | #cgo CFLAGS: -x objective-c -Wno-incompatible-pointer-types-discards-qualifiers
7 | #cgo LDFLAGS: -framework Foundation
8 | #include "handler.h"
9 | */
10 | import "C"
11 |
12 | import (
13 | "github.com/andlabs/ui"
14 | )
15 |
16 | //export ReceiveURL
17 | func ReceiveURL(u *C.char) {
18 | urlStream <- C.GoString(u)
19 | }
20 |
21 | func listen(config Config) chan bool {
22 |
23 | urlStream = make(chan string, 1) // the event handler blocks!, so buffer the channel at least once to get the first message
24 | C.StartURLHandler()
25 |
26 | done := make(chan bool)
27 |
28 | ui.Main(func() {
29 | ui.QueueMain(func() { ui.Quit() })
30 |
31 | go func() {
32 | for url := range urlStream {
33 | <-handleURL(config, url)
34 | done <- true
35 | }
36 | }()
37 | })
38 |
39 | return done
40 | }
41 |
--------------------------------------------------------------------------------
/src/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "log/syslog"
6 | "time"
7 |
8 | "github.com/arkan/dotconfig"
9 | gosxnotifier "github.com/deckarep/gosx-notifier"
10 | "github.com/skratchdot/open-golang/open"
11 | )
12 |
13 | var urlStream chan string
14 | var appname = "bowser"
15 |
16 | func main() {
17 |
18 | config, err := getConfig()
19 | if err != nil {
20 | log.Panicln(err)
21 | }
22 |
23 | if config.Debug {
24 | // Configure logger to write to the syslog
25 | logwriter, err := syslog.New(syslog.LOG_NOTICE, appname)
26 | if err == nil {
27 | log.SetOutput(logwriter)
28 | }
29 | }
30 |
31 | select {
32 | case <-listen(config):
33 | if config.IsDefault {
34 | note := gosxnotifier.NewNotification("A default configuration has been set for Bowser in ~/.config/bowser/config.yml")
35 | note.Title = "Bowser - Please configure"
36 | note.AppIcon = getNotificationIconPath()
37 | note.Link = "https://github.com/netgusto/bowser#setup"
38 | note.Push()
39 | }
40 | log.Println("Done")
41 | case <-time.After(1000 * time.Millisecond):
42 | open.RunWith("https://github.com/netgusto/bowser", "Safari")
43 | log.Println("Not received any URL after 1s; exiting")
44 | }
45 | }
46 |
47 | // Config ...
48 | type Config struct {
49 | Debug bool `yaml:"debug"`
50 | Browsers []Browser `yaml:"browsers,omitempty"`
51 | IsDefault bool `yaml:"-"`
52 | }
53 |
54 | // Browser ...
55 | type Browser struct {
56 | Alias string `yaml:"alias"`
57 | App string `yaml:"app,omitempty"`
58 | Match []string `yaml:"match,omitempty"`
59 | }
60 |
61 | func getConfig() (Config, error) {
62 | config := Config{}
63 |
64 | if err := dotconfig.Load(appname, &config); err != nil {
65 | if err == dotconfig.ErrConfigNotFound {
66 | initDefaultConfig(&config)
67 | if err := dotconfig.Save(appname, config); err != nil {
68 | return config, err
69 | }
70 | }
71 | } else if err != nil {
72 | return config, err
73 | }
74 |
75 | return config, nil
76 | }
77 |
78 | func initDefaultConfig(config *Config) {
79 | config.IsDefault = true
80 | config.Debug = false
81 | config.Browsers = []Browser{
82 | Browser{
83 | Alias: "Default",
84 | App: "Safari",
85 | },
86 | // Browser{
87 | // Alias: "Dev",
88 | // App: "Google Chrome",
89 | // Match: []string{
90 | // "^https?://127.0.0.1",
91 | // "^https?://localhost",
92 | // },
93 | // },
94 | }
95 | }
96 |
--------------------------------------------------------------------------------