7 changed files with 550 additions and 0 deletions
@ -0,0 +1,25 @@
|
||||
# BIN = browser_ruler.py
|
||||
BIN = browser-ruler
|
||||
SCRIPT_INSTALL_PATH ?= $(HOME)/.local/bin
|
||||
DESKTOP_INSTALL_PATH ?= $(HOME)/.local/share/applications
|
||||
|
||||
.PHONY: default all clean |
||||
default: test |
||||
|
||||
.PHONY: test |
||||
test: $(BIN) |
||||
./$(BIN) https://duck.com/
|
||||
|
||||
$(BIN): *.go |
||||
go build .
|
||||
|
||||
.PHONY: install |
||||
install: $(BIN) |
||||
mkdir -p "$(SCRIPT_INSTALL_PATH)"
|
||||
mkdir -p "$(DESKTOP_INSTALL_PATH)"
|
||||
cp $(BIN) "$(SCRIPT_INSTALL_PATH)/$(BIN)"
|
||||
sed "s|{SCRIPT}|$(SCRIPT_INSTALL_PATH)/$(BIN)|" ./browserRuler.desktop > "$(DESKTOP_INSTALL_PATH)/browserRuler.desktop"
|
||||
|
||||
.PHONY: set-default |
||||
set-default: |
||||
xdg-settings set default-web-browser browserRuler.desktop
|
@ -1,2 +1,18 @@
|
||||
# browser-ruler |
||||
|
||||
A small program that allows writing rules to determine which browser to lauch based on the URL |
||||
|
||||
This is tested on an Ubuntu system but should work on anything that supports xdg |
||||
|
||||
## Configuration |
||||
|
||||
There is no configuration, persay. Intead, the code must be updated. |
||||
|
||||
Edit `main.go` and edit the list of `BrowserRules` in a similar fashion as the example and then run `make install` again. |
||||
|
||||
There is also a Python version as `browser_ruler.py`. To use that instead, you can edit that and then update the Makefile to make the `BIN` variable `BIN = browser_ruler.py`. |
||||
|
||||
## Installation |
||||
|
||||
make install set-default |
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
[Desktop Entry] |
||||
Name=Browser Ruler |
||||
Comment=Automatically opens urls in a particular browser |
||||
Exec={SCRIPT} %U |
||||
Terminal=false |
||||
Type=Application |
||||
MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https; |
||||
Categories=Network;WebBrowser; |
||||
StartupNotify=false |
@ -0,0 +1,201 @@
|
||||
#! /usr/bin/env python3 |
||||
import sys |
||||
from re import match |
||||
from subprocess import Popen |
||||
from typing import Any |
||||
from typing import Callable |
||||
from typing import List |
||||
from typing import Iterable |
||||
from typing import NamedTuple |
||||
from urllib.parse import urlparse |
||||
|
||||
|
||||
class Intent(NamedTuple): |
||||
url: str |
||||
referrer: str |
||||
|
||||
|
||||
class BrowserRule(NamedTuple): |
||||
match_func: Callable[[Intent], bool] |
||||
command: str |
||||
args: List[str] = [] |
||||
|
||||
def matched(self, intent: Intent) -> bool: |
||||
"""Checks if the intent matches the rule using match_func""" |
||||
return self.match_func(intent) |
||||
|
||||
def launch(self, intent: Intent) -> None: |
||||
"""Launch the command""" |
||||
Popen(["nohup", self.command, *self.args, intent.url], shell=False) |
||||
|
||||
def maybe_launch(self, intent: Intent) -> bool: |
||||
if self.matched(intent): |
||||
self.launch(intent) |
||||
return True |
||||
|
||||
return False |
||||
|
||||
|
||||
# Matching functions and factories |
||||
MatchFunc = Callable[[Intent], bool] |
||||
|
||||
|
||||
def match_regex(rules: List[str]) -> MatchFunc: |
||||
"""Returns a function to handle urls and match them using regex""" |
||||
def match_func(intent: Intent) -> bool: |
||||
for rule in rules: |
||||
if match(rule, intent.url): |
||||
return True |
||||
return False |
||||
|
||||
return match_func |
||||
|
||||
|
||||
def match_host_regex(rules: List[str]) -> MatchFunc: |
||||
"""Returns a function to handle urls and match them using regex""" |
||||
def match_func(intent: Intent) -> bool: |
||||
url = urlparse(intent.url) |
||||
for rule in rules: |
||||
if match(rule, url.hostname): |
||||
return True |
||||
return False |
||||
|
||||
return match_func |
||||
|
||||
|
||||
def match_hostname(hostnames: List[str]) -> MatchFunc: |
||||
"""Returns a matching function that parses URLs and checks their hosts""" |
||||
def match_func(intent: Intent) -> bool: |
||||
url = urlparse(intent.url) |
||||
for hostname in hostnames: |
||||
if url.hostname == hostname: |
||||
return True |
||||
return False |
||||
|
||||
return match_func |
||||
|
||||
|
||||
def any_match(rules: Iterable[MatchFunc]) -> MatchFunc: |
||||
def match_func(intent: Intent) -> bool: |
||||
return any(r(intent) for r in rules) |
||||
|
||||
return match_func |
||||
|
||||
|
||||
def default(x: Any) -> bool: |
||||
"""Always returns True""" |
||||
return True |
||||
|
||||
|
||||
def noop(x: Any) -> bool: |
||||
"""Always returns False""" |
||||
return False |
||||
|
||||
|
||||
browser_rules = [ |
||||
# Self-hosted sites |
||||
BrowserRule( |
||||
match_host_regex([ |
||||
r".*\.iamthefij\.com$", |
||||
r".*\.thefij\.rocks$", |
||||
r".*\.thefij$", |
||||
]), |
||||
"firefox", |
||||
), |
||||
# Work domains |
||||
BrowserRule( |
||||
any_match(( |
||||
match_hostname([ |
||||
"app.signalfx.com", |
||||
"lever.co", |
||||
"work.grubhub.com", |
||||
"y", |
||||
"yelp.rimeto.io", |
||||
"yelp.slack.com", |
||||
"yelplove.appspot.com", |
||||
]), |
||||
match_host_regex([ |
||||
r".*\.lifesize\.com$", |
||||
r".*\.myworkday\.com$", |
||||
r".*\.salesforce\.com$", |
||||
r".*\.yelpcorp\.com$", |
||||
]), |
||||
)), |
||||
"chromium-browser" |
||||
), |
||||
# Other, generally Googly things |
||||
BrowserRule( |
||||
any_match(( |
||||
match_hostname([ |
||||
"google.com", |
||||
"youtube.com", |
||||
]), |
||||
match_host_regex([ |
||||
r".*\.google\.com$", |
||||
r".*\.youtube\.com$", |
||||
]), |
||||
)), |
||||
"chromium-browser", |
||||
), |
||||
# Fall back to firefox as the default |
||||
BrowserRule( |
||||
default, |
||||
"firefox", |
||||
), |
||||
|
||||
# Sample browsers |
||||
|
||||
# Firefox new tab |
||||
BrowserRule( |
||||
noop, |
||||
"firefox", |
||||
), |
||||
# Firefox new window |
||||
BrowserRule( |
||||
noop, |
||||
"firefox", |
||||
["--new-window"], |
||||
), |
||||
# Firefox private window |
||||
BrowserRule( |
||||
noop, |
||||
"firefox", |
||||
["--private-window"], |
||||
), |
||||
# Chromium new tab |
||||
BrowserRule( |
||||
noop, |
||||
"chromium-browser", |
||||
), |
||||
# Chromium new window |
||||
BrowserRule( |
||||
noop, |
||||
"chromium-browser", |
||||
["--new-window"], |
||||
), |
||||
# Chromium incongnito |
||||
BrowserRule( |
||||
noop, |
||||
"chromium-browser", |
||||
["--incognito"], |
||||
), |
||||
] |
||||
|
||||
|
||||
def open_url(url: str, referrer=None) -> bool: |
||||
intent = Intent(url, referrer) |
||||
return any(rule.maybe_launch(intent) for rule in browser_rules) |
||||
|
||||
|
||||
def main(urls: List[str], referrer=None): |
||||
failed_urls: List[str] = [] |
||||
for url in urls: |
||||
if not open_url(url): |
||||
failed_urls.append(url) |
||||
|
||||
if failed_urls: |
||||
raise ValueError(f"No rules present for {failed_urls}. Add a default rule") |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
main(sys.argv[1:]) |
@ -0,0 +1,114 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
) |
||||
|
||||
// TODO: This is a total work in progress. It generally works execept it's ugly and regex strings aren't parsed correctly
|
||||
|
||||
// Config is the full set of configuration required
|
||||
type Config struct { |
||||
DefaultBrowserCommand []string |
||||
Rules []BrowserRuleConfig |
||||
} |
||||
|
||||
// BrowserRuleConfig represents a BrowserRule
|
||||
type BrowserRuleConfig struct { |
||||
BrowserCommand []string |
||||
Matchers []MatcherConfig |
||||
} |
||||
|
||||
// MatcherConfig represents a single MatchFunc
|
||||
type MatcherConfig struct { |
||||
Kind string |
||||
Targets []string |
||||
} |
||||
|
||||
func parseConfig(jsonBytes []byte) (Config, error) { |
||||
var c Config |
||||
err := json.Unmarshal(jsonBytes, &c) |
||||
return c, err |
||||
} |
||||
|
||||
// GetRules returns all rules parsed from a JSON string
|
||||
func GetRules(jsonBytes []byte) (rules []BrowserRule, err error) { |
||||
c, err := parseConfig(jsonBytes) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
for _, rule := range c.Rules { |
||||
if len(rule.BrowserCommand) < 1 { |
||||
err = fmt.Errorf("Must provide a browser command") |
||||
return |
||||
} |
||||
browserCommand := rule.BrowserCommand[0] |
||||
args := rule.BrowserCommand[1:] |
||||
|
||||
matchers := []MatchFunc{} |
||||
for _, matcher := range rule.Matchers { |
||||
switch matcher.Kind { |
||||
case "matchHostname": |
||||
fmt.Println("Match hostname for ", matcher.Targets) |
||||
matchers = append(matchers, matchHostname(matcher.Targets...)) |
||||
case "matchHostRegexp": |
||||
matchers = append(matchers, matchHostRegexp(matcher.Targets...)) |
||||
case "matchRegexp": |
||||
matchers = append(matchers, matchRegexp(matcher.Targets...)) |
||||
default: |
||||
err = fmt.Errorf("Unknown matcher kind %s", matcher.Kind) |
||||
return |
||||
} |
||||
} |
||||
var matcher MatchFunc |
||||
if len(matchers) == 1 { |
||||
matcher = matchers[0] |
||||
} else { |
||||
matcher = matchAny(matchers...) |
||||
} |
||||
|
||||
rules = append( |
||||
rules, |
||||
BrowserRule{ |
||||
matcher: matcher, |
||||
command: browserCommand, |
||||
args: args, |
||||
}, |
||||
BrowserRule{ |
||||
matcher: defaultBrowser, |
||||
command: c.DefaultBrowserCommand[0], |
||||
args: c.DefaultBrowserCommand[1:], |
||||
}, |
||||
) |
||||
|
||||
} |
||||
|
||||
return |
||||
} |
||||
|
||||
func GetTestRules() ([]BrowserRule, error) { |
||||
jsonBytes := []byte(`{ |
||||
"DefaultBrowserCommand": ["chromium-browser"], |
||||
"Rules": [ |
||||
{ |
||||
"BrowserCommand": ["firefox"], |
||||
"Matchers": [ |
||||
{ |
||||
"Kind": "matchHostname", |
||||
"Targets": [ |
||||
"iamthefij.com" |
||||
] |
||||
}, |
||||
{ |
||||
"Kind": "matchHostRegexp", |
||||
"Targets": [ |
||||
".*\.iamthefij\.com$", |
||||
] |
||||
} |
||||
] |
||||
} |
||||
] |
||||
}`) |
||||
return GetRules(jsonBytes) |
||||
} |
@ -0,0 +1,182 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"net/url" |
||||
"os/exec" |
||||
"regexp" |
||||
) |
||||
|
||||
// MatchFunc is a signature for a function to match a URL
|
||||
type MatchFunc = func(url.URL) bool |
||||
|
||||
// A BrowserRule is a rule that will launch a browser, if matched
|
||||
type BrowserRule struct { |
||||
matcher MatchFunc |
||||
command string |
||||
args []string |
||||
} |
||||
|
||||
// IsMatched will check to see the rule matches the provied URL
|
||||
func (r BrowserRule) IsMatched(dest url.URL) bool { |
||||
return r.matcher(dest) |
||||
} |
||||
|
||||
// Launch will launch the browser with the provided URL
|
||||
func (r BrowserRule) Launch(dest url.URL) error { |
||||
args := append(r.args, "--", dest.String()) |
||||
cmd := exec.Command(r.command, args...) |
||||
return cmd.Start() |
||||
} |
||||
|
||||
// MaybeLaunch will lauch the browser with the provided URL if it matches the rule
|
||||
func (r BrowserRule) MaybeLaunch(dest url.URL) (bool, error) { |
||||
if r.IsMatched(dest) { |
||||
return true, r.Launch(dest) |
||||
} |
||||
return false, nil |
||||
} |
||||
|
||||
func matchHostname(hostnames ...string) MatchFunc { |
||||
return func(dest url.URL) bool { |
||||
for _, host := range hostnames { |
||||
if host == dest.Hostname() { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
} |
||||
|
||||
func matchHostRegexp(hostRegexp ...string) MatchFunc { |
||||
matchers := []*regexp.Regexp{} |
||||
for _, s := range hostRegexp { |
||||
matchers = append(matchers, regexp.MustCompile(s)) |
||||
} |
||||
return func(dest url.URL) bool { |
||||
for _, matcher := range matchers { |
||||
if matcher.MatchString(dest.Hostname()) { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
} |
||||
|
||||
func matchRegexp(urlRegexp ...string) MatchFunc { |
||||
matchers := []*regexp.Regexp{} |
||||
for _, s := range urlRegexp { |
||||
matchers = append(matchers, regexp.MustCompile(s)) |
||||
} |
||||
return func(dest url.URL) bool { |
||||
for _, matcher := range matchers { |
||||
if matcher.MatchString(dest.String()) { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
} |
||||
|
||||
func matchAny(matchFuncs ...MatchFunc) MatchFunc { |
||||
return func(dest url.URL) bool { |
||||
for _, f := range matchFuncs { |
||||
if f(dest) { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
} |
||||
|
||||
// Always returns true
|
||||
func defaultBrowser(dest url.URL) bool { |
||||
return true |
||||
} |
||||
|
||||
// Never returns true
|
||||
func noop(dest url.URL) bool { |
||||
return false |
||||
} |
||||
|
||||
// Rules to evaluate
|
||||
var rules = []BrowserRule{ |
||||
// Personal domains
|
||||
BrowserRule{ |
||||
matcher: matchHostRegexp( |
||||
`.*\.iamthefij\.com$`, |
||||
`.*\.thefij\.rocks$`, |
||||
`.*\.thefij$`, |
||||
), |
||||
command: "firefox", |
||||
}, |
||||
// Work domains
|
||||
BrowserRule{ |
||||
matcher: matchAny( |
||||
matchHostname( |
||||
"app.signalfx.com", |
||||
"lever.co", |
||||
"work.grubhub.com", |
||||
"y", |
||||
"yelp.rimeto.io", |
||||
"yelp.slack.com", |
||||
"yelplove.appspot.com", |
||||
), |
||||
matchHostRegexp( |
||||
`.*\.lifesize\.com$`, |
||||
`.*\.myworkday\.com$`, |
||||
`.*\.salesforce\.com$`, |
||||
`.*\.yelpcorp\.com$`, |
||||
), |
||||
), |
||||
command: "chromium-browser", |
||||
}, |
||||
// Googly domains
|
||||
BrowserRule{ |
||||
matcher: matchAny( |
||||
matchHostname( |
||||
"google.com", |
||||
"youtube.com", |
||||
), |
||||
matchHostRegexp( |
||||
`.*\.google\.com$`, |
||||
`.*\.youtube\.com$`, |
||||
), |
||||
), |
||||
command: "chromium-browser", |
||||
}, |
||||
// Default fallback browser
|
||||
BrowserRule{ |
||||
matcher: defaultBrowser, |
||||
command: "firefox", |
||||
}, |
||||
} |
||||
|
||||
func handleUrl(urlString string) error { |
||||
dest, err := url.Parse(urlString) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
var matched bool |
||||
for _, rule := range rules { |
||||
matched, err = rule.MaybeLaunch(*dest) |
||||
if matched { |
||||
break |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func main() { |
||||
flag.Parse() |
||||
urls := flag.Args() |
||||
|
||||
for _, urlString := range urls { |
||||
err := handleUrl(urlString) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue