Refactor to read from an hcl config file and expr rules

This commit is contained in:
IamTheFij 2022-01-27 22:07:41 -08:00
parent 6d8fcee025
commit 292392873e
10 changed files with 306 additions and 438 deletions

View File

@ -1,4 +1,3 @@
# BIN = browser_ruler.py
BIN = browser-ruler BIN = browser-ruler
SCRIPT_INSTALL_PATH ?= $(HOME)/.local/bin SCRIPT_INSTALL_PATH ?= $(HOME)/.local/bin
DESKTOP_INSTALL_PATH ?= $(HOME)/.local/share/applications DESKTOP_INSTALL_PATH ?= $(HOME)/.local/share/applications
@ -8,7 +7,7 @@ default: test
.PHONY: test .PHONY: test
test: $(BIN) test: $(BIN)
./$(BIN) https://duck.com/ ./$(BIN) -config ./sample-config.hcl https://duck.com/
$(BIN): *.go $(BIN): *.go
go build . go build .

View File

@ -2,19 +2,55 @@
A small program that allows writing rules to determine which browser to lauch based on the URL 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 Tested on an Ubuntu and Pop_OS! system but should work on anything that supports xdg
## Configuration ## Configuration
There is no configuration, persay. Intead, the code must be updated. When launched, configuration will be read first from the `-config` option, if provided, then from `$XDG_CONFIG_HOME/browser-ruler/config.hcl`, if found, and then from `$HOME/.browser-ruler.hcl`. If none of these are found, it will panic and ask you to provide it some configuration.
Edit `rules.go` and edit the list of `BrowserRules` in a similar fashion as the example and then run `make install` again. Config files are in the HCL config language with a schema defined below.
You can use any rule functions found in `main.go` and any browsers found in `browsers.go`. ```hcl
default_browser_cmd = list(string)
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`. rule [label] {
browser_cmd = list(string)
match = string
}
```
To see some detailed samples, check out `sample-config.hcl`.
### Writing a MatchFunc
The `match` expression in the config should be a `MatchFunc`. This can done by using any of the helper methods available:
```go
// accepts any number of hostnames and returns a match function that matches the hostname against any of them
matchHostname(...string) MatchFunc
// accepts any number of regexps and returns a match function that matches the hostname against any of them
matchHostRegexp(...string) MatchFunc
// accepts any number of regexps and returns a match function that matches the full url string against any of them
matchRegexp(...string) MatchFunc
// accepts any number of MatchFuncs and returns a new match function that matches against any of them
matchAny(...MatchFunc) MatchFunc
```
If you want more flexibility, you can write your own using the [`expr`](https://github.com/antonmedv/expr) language. Generally, this language gives you the power to write an expression that evaluates to a boolean using exposed variables. Additonally, a helper exists to convert a `bool` value into a `MatchFunc`. For example:
```
MatchFunc(hostname endsWith "google.com")
```
The following variables are avaiable for building custom match expressions.
```
hostname: the requested URL hostname
fullUrl: the full string of the requested URL
url: the Go url.URL struct requested URL
MatchFunc(bool) MatchFunc: constructs a match function from a bool value
```
## Installation ## Installation
make install set-default make install set-default

View File

@ -1,208 +0,0 @@
#! /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):
"""Intent contains a url requested to be opened"""
url: str
# referrer isn't possible to be captured at this time
referrer: str
class BrowserRule(NamedTuple):
"""BrowserRule is matches a url and launches it in a browser"""
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"""
with Popen(["nohup", self.command, *self.args, intent.url], shell=False):
pass
def maybe_launch(self, intent: Intent) -> bool:
"""Launch URL in a browser if it matches the rules"""
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:
return any(match(rule, intent.url) for rule in rules)
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)
if not url.hostname:
return False
return any(match(rule, url.hostname) for rule in rules)
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:
"""Returns a functino that will check against any match rules
Equivalent to using `any(...)`"""
def match_func(intent: Intent) -> bool:
return any(r(intent) for r in rules)
return match_func
def default(_: Any) -> bool:
"""Always returns True"""
return True
def noop(_: 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:
"""Open a given url based on rule set"""
intent = Intent(url, referrer)
return any(rule.maybe_launch(intent) for rule in browser_rules)
def main(urls: List[str]):
"""Recieves list of urls and opens them each with provided rule sets"""
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:])

View File

@ -1,60 +0,0 @@
package main
func Firefox(matcher MatchFunc) BrowserRule {
return BrowserRule{
command: "firefox",
args: nil,
matcher: matcher,
}
}
func FirefoxWindow(matcher MatchFunc) BrowserRule {
return BrowserRule{
matcher: matcher,
command: "firefox",
args: []string{"--new-window"},
}
}
func FirefoxPrivate(matcher MatchFunc) BrowserRule {
return BrowserRule{
matcher: matcher,
command: "firefox",
args: []string{"--private-window"},
}
}
func Chromium(matcher MatchFunc) BrowserRule {
return BrowserRule{
command: "chromium-browser",
args: nil,
matcher: matcher,
}
}
func ChromiumWindow(matcher MatchFunc) BrowserRule {
return BrowserRule{
command: "chromium-browser",
args: []string{"--new-window"},
matcher: matcher,
}
}
func ChromiumIncognito(matcher MatchFunc) BrowserRule {
return BrowserRule{
command: "chromium-browser",
args: []string{"--incognito"},
matcher: matcher,
}
}
func ChromiumFlatpak(matcher MatchFunc) BrowserRule {
return BrowserRule{
command: "flatpak",
args: []string{
"run",
"org.chromium.Chromium",
},
matcher: matcher,
}
}

181
config.go
View File

@ -1,129 +1,112 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"net/url"
"os"
"path/filepath"
"github.com/antonmedv/expr"
"github.com/hashicorp/hcl/v2/hclsimple"
) )
// TODO: This is a total work in progress. It generally works execept it's ugly and regex strings aren't parsed correctly func fileExists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
}
return true
}
// Config is the full set of configuration required // Config is the full set of configuration required
type Config struct { type Config struct {
DefaultBrowserCommand []string DefaultBrowserCommand []string `hcl:"default_browser_cmd"`
Rules []BrowserRuleConfig Rules []BrowserRuleConfig `hcl:"rule,block"`
}
func (r *BrowserRule) UnmarshalJSON(data []byte) error {
var values struct {
command string
args []string
matchHostname []string
matchHostRegexp []string
matchRegexp []string
}
if err := json.Unmarshal(data, &values); err != nil {
return err
}
return nil
} }
// BrowserRuleConfig represents a BrowserRule // BrowserRuleConfig represents a BrowserRule
type BrowserRuleConfig struct { type BrowserRuleConfig struct {
BrowserCommand []string Name string `hcl:"name,label"`
Matchers []MatcherConfig BrowserCommand []string `hcl:"browser_cmd"`
MatchExpr string `hcl:"match"`
} }
// MatcherConfig represents a single MatchFunc func MakeMatchFunc(rule string) MatchFunc {
type MatcherConfig struct { return func(dest url.URL) bool {
Kind string env := map[string]interface{}{
Targets []string // Prebuilt match functions
} "matchAny": matchAny,
"matchHostRegexp": matchHostRegexp,
"matchHostname": matchHostname,
"matchNever": matchNever,
"matchRegexp": matchRegexp,
func parseConfig(jsonBytes []byte) (Config, error) { // Helpers for building custom matchers
var c Config "MatchFunc": func(foundMatch bool) MatchFunc {
err := json.Unmarshal(jsonBytes, &c) return func(_ url.URL) bool {
return c, err return foundMatch
} }
},
// GetRules returns all rules parsed from a JSON string "fullUrl": dest.String(),
func GetRules(jsonBytes []byte) (rules []BrowserRule, err error) { "hostname": dest.Hostname(),
c, err := parseConfig(jsonBytes) "url": dest,
}
matchFunc, err := expr.Eval(rule, env)
if err != nil { if err != nil {
return panic(err)
} }
for _, rule := range c.Rules { return matchFunc.(MatchFunc)(dest)
if len(rule.BrowserCommand) < 1 {
err = fmt.Errorf("Must provide a browser command")
return
} }
browserCommand := rule.BrowserCommand[0] }
args := rule.BrowserCommand[1:]
matchers := []MatchFunc{} func LoadConfig(path string) ([]BrowserRule, error) {
for _, matcher := range rule.Matchers { rules := []BrowserRule{}
switch matcher.Kind { var config Config
case "matchHostname": err := hclsimple.DecodeFile(path, nil, &config)
fmt.Println("Match hostname for ", matcher.Targets) if err != nil {
matchers = append(matchers, matchHostname(matcher.Targets...)) return nil, err
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( for _, rule := range config.Rules {
rules, rules = append(rules, BrowserRule{
BrowserRule{ command: rule.BrowserCommand[0],
matcher: matcher, args: rule.BrowserCommand[1:],
command: browserCommand, matcher: MakeMatchFunc(rule.MatchExpr),
args: args, })
}, }
BrowserRule{
// Add default browser rule to the end
rules = append(rules, BrowserRule{
command: config.DefaultBrowserCommand[0],
args: config.DefaultBrowserCommand[1:],
matcher: defaultBrowser, matcher: defaultBrowser,
command: c.DefaultBrowserCommand[0], })
args: c.DefaultBrowserCommand[1:],
},
)
} return rules, nil
return
} }
func GetTestRules() ([]BrowserRule, error) { func DefaultConfigPath() (string, error) {
jsonBytes := []byte(`{ appName := "browser-ruler"
"DefaultBrowserCommand": ["chromium-browser"],
"Rules": [ // Check XDG_CONFIG_HOME
{ dir, err := os.UserConfigDir()
"BrowserCommand": ["firefox"], if err == nil {
"Matchers": [ configPath := filepath.Join(dir, appName, "config.hcl")
{ if fileExists(configPath) {
"Kind": "matchHostname", return configPath, nil
"Targets": [
"iamthefij.com"
]
},
{
"Kind": "matchHostRegexp",
"Targets": [
".*\.iamthefij\.com$",
]
} }
]
} }
]
}`) dir, err = os.UserHomeDir()
return GetRules(jsonBytes) if err != nil {
return "", fmt.Errorf("Could not find a config dir or home dir to look for config file. Specify one with -config: %w", err)
}
configPath := filepath.Join(dir, "."+appName+".hcl")
if fileExists(configPath) {
return configPath, nil
}
return "", fmt.Errorf("no config file found in config dir or home dir")
} }

5
go.mod
View File

@ -1,3 +1,8 @@
module git.iamthefij.com/iamthefij/browser-ruler module git.iamthefij.com/iamthefij/browser-ruler
go 1.16 go 1.16
require (
github.com/antonmedv/expr v1.9.0
github.com/hashicorp/hcl/v2 v2.11.1
)

82
go.sum Normal file
View File

@ -0,0 +1,82 @@
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU=
github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJbcc=
github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA=
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

29
main.go
View File

@ -96,18 +96,18 @@ func defaultBrowser(dest url.URL) bool {
} }
// Never returns true // Never returns true
func noop(dest url.URL) bool { func matchNever(dest url.URL) bool {
return false return false
} }
func handleURL(urlString string) error { func handleURL(browserRules []BrowserRule, urlString string) error {
dest, err := url.Parse(urlString) dest, err := url.Parse(urlString)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse url: %s, %w", urlString, err) return fmt.Errorf("failed to parse url: %s, %w", urlString, err)
} }
var matched bool var matched bool
for _, rule := range rules() { for _, rule := range browserRules {
matched, err = rule.MaybeLaunch(*dest) matched, err = rule.MaybeLaunch(*dest)
if err != nil { if err != nil {
return fmt.Errorf("failed launching browser: %w", err) return fmt.Errorf("failed launching browser: %w", err)
@ -122,11 +122,32 @@ func handleURL(urlString string) error {
} }
func main() { func main() {
configPath := flag.String("config", "", "Path to config file, otherwise it will default to $XDG_CONFIG_HOME/browser-ruler/config.hcl or $HOME/.browser-ruler.hcl")
flag.Parse() flag.Parse()
// Read config path from option or find default
if *configPath == "" {
defaultConfigPath, err := DefaultConfigPath()
if err != nil {
fmt.Println("Failed to get a path to any config files")
panic(err)
}
configPath = &defaultConfigPath
}
// Read rules from config path
browserRules, err := LoadConfig(*configPath)
if err != nil {
fmt.Printf("Could not read config from path %s\n", *configPath)
panic(err)
}
// Get urls from args
urls := flag.Args() urls := flag.Args()
// For each url, run matcher
for _, urlString := range urls { for _, urlString := range urls {
err := handleURL(urlString) err := handleURL(browserRules, urlString)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -1,54 +0,0 @@
package main
// Update this file with your rules
// You can either define a BrowserRule directly, or using one of the
// pre-defined browsers in browsers.go
func rules() []BrowserRule {
return []BrowserRule{
// Personal domains
Firefox(
matchHostRegexp(
`.*\.iamthefij\.com$`,
`.*\.thefij\.rocks$`,
`.*\.thefij$`,
),
),
// Work domains
ChromiumFlatpak(
matchAny(
matchHostname(
"app.signalfx.com",
"lever.co",
"work.grubhub.com",
"y",
"yelp.rimeto.io",
"yelp.slack.com",
"yelplove.appspot.com",
"yelp-shootie.appspot.com",
),
matchHostRegexp(
`.*\.lifesize\.com$`,
`.*\.myworkday\.com$`,
`.*\.salesforce\.com$`,
`.*\.yelpcorp\.com$`,
),
),
),
// Googly domains
ChromiumFlatpak(
matchAny(
matchHostname(
"google.com",
"youtube.com",
),
matchHostRegexp(
`.*\.google\.com$`,
`.*\.youtube\.com$`,
),
),
),
// Default fallback browser
Firefox(defaultBrowser),
}
}

64
sample-config.hcl Normal file
View File

@ -0,0 +1,64 @@
default_browser_cmd = ["firefox"]
# The rule label doesn't matter
rule "Flatpak Chromium" {
# Command and args, like with default
browser_cmd = ["flatpak", "run", "org.chromium.Chromium"]
# match evaluates an expr that should result in a MatchFunc where the signature
# is MatchFunc = func(url.URL) bool
# There are several functions provided that contain common rules and help construct this.
# provided are:
# matchHostname(...string) MatchFunc
# matchHostRegexp(...string) MatchFunc
# matchRegexp(...string) MatchFunc
# matchAny(...MatchFunc) MatchFunc
#
# Note that when using regex, any escapes need to be doubled. Once to escape the string
# and a second time to escape the regexp char. So a literal `.` is represented as `\\.`
#
# For writing custom rules using expr, you can use the following two variables:
# hostname: the URL hostname
# fullUrl: the full string of the URL
# url: the Go url.URL struct that is being called
#
# When using those, you can turn them into a MatchFunc using
# MatchFunc(bool) MatchFunc
#
# For example:
# MatchFunc(hostname endsWith "example.com")
# is equivalent to:
# matchHostRegexp(".*\\.example\\.com$")
match = "matchHostname('google.com', 'youtube.com')"
}
rule "Firefox Private" {
browser_cmd = ["firefox", "--private-window"]
# For more complex rules, rules can span multiple lines using EOF syntax
match = <<EOF
matchAny(
matchHostname('facebook.com'),
MatchFunc(hostname == 'instagram.com')
)
EOF
}
# Some additional sample browser commands with a match func that will never match
rule "Firefox New Window" {
browser_cmd = ["firefox", "--new-window"]
match = "MatchFunc(false)"
}
rule "Chromium" {
browser_cmd = ["chromium-browser"]
match = "MatchFunc(false)"
}
rule "Chromium New Window" {
browser_cmd = ["chromium-browser", "--new-window"]
match = "matchNever"
}
rule "Chromium Incognito Window" {
browser_cmd = ["chromium-browser", "--incognito"]
match = "matchNever"
}