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)
|
||||
# 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
|
||||
|
||||
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