├── .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 | --------------------------------------------------------------------------------