Working Python and Go versions
This commit is contained in:
parent
d60f07ef60
commit
fcc132e72f
3
.gitignore
vendored
3
.gitignore
vendored
@ -15,3 +15,6 @@
|
|||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
# vendor/
|
||||||
|
|
||||||
|
# Application specific
|
||||||
|
nohup.out
|
||||||
|
browser-ruler
|
||||||
|
25
Makefile
Normal file
25
Makefile
Normal file
@ -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
|
16
README.md
16
README.md
@ -1,2 +1,18 @@
|
|||||||
# browser-ruler
|
# 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
|
||||||
|
|
||||||
|
9
browserRuler.desktop
Normal file
9
browserRuler.desktop
Normal file
@ -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
|
201
browser_ruler.py
Executable file
201
browser_ruler.py
Executable file
@ -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:])
|
114
config.go
Normal file
114
config.go
Normal file
@ -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)
|
||||||
|
}
|
182
main.go
Normal file
182
main.go
Normal file
@ -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
Block a user