Working Python and Go versions

This commit is contained in:
IamTheFij 2020-11-26 08:03:23 -08:00
parent d60f07ef60
commit fcc132e72f
7 changed files with 550 additions and 0 deletions

3
.gitignore vendored
View File

@ -15,3 +15,6 @@
# Dependency directories (remove the comment below to include it)
# vendor/
# Application specific
nohup.out
browser-ruler

25
Makefile Normal file
View 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

View File

@ -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
View 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
View 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
View 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
View 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)
}
}
}