Compare commits

..

No commits in common. "master" and "non-config" have entirely different histories.

11 changed files with 418 additions and 334 deletions

View File

@ -1,15 +0,0 @@
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
hooks:
- id: check-added-large-files
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.4.0
hooks:
- id: go-fmt
- id: go-imports
- id: golangci-lint

View File

@ -1,3 +1,4 @@
# BIN = browser_ruler.py
BIN = browser-ruler
SCRIPT_INSTALL_PATH ?= $(HOME)/.local/bin
DESKTOP_INSTALL_PATH ?= $(HOME)/.local/share/applications
@ -7,7 +8,7 @@ default: test
.PHONY: test
test: $(BIN)
./$(BIN) -config ./sample-config.hcl https://duck.com/
./$(BIN) https://duck.com/
$(BIN): *.go
go build .
@ -22,7 +23,3 @@ install: $(BIN)
.PHONY: set-default
set-default:
xdg-settings set default-web-browser browserRuler.desktop
.PHONY: install-hooks
install-hooks:
pre-commit install --install-hooks

View File

@ -2,55 +2,19 @@
A small program that allows writing rules to determine which browser to lauch based on the URL
Tested on an Ubuntu and Pop_OS! system but should work on anything that supports xdg
This is tested on an Ubuntu system but should work on anything that supports xdg
## Configuration
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.
There is no configuration, persay. Intead, the code must be updated.
Config files are in the HCL config language with a schema defined below.
Edit `rules.go` and edit the list of `BrowserRules` in a similar fashion as the example and then run `make install` again.
```hcl
default_browser_cmd = list(string)
You can use any rule functions found in `main.go` and any browsers found in `browsers.go`.
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
```
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

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:])

60
browsers.go Normal file
View File

@ -0,0 +1,60 @@
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,
}
}

180
config.go
View File

@ -1,118 +1,114 @@
package main
import (
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"github.com/antonmedv/expr"
"github.com/hashicorp/hcl/v2/hclsimple"
)
func fileExists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
}
return true
}
// 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 `hcl:"default_browser_cmd"`
Rules []BrowserRuleConfig `hcl:"rule,block"`
DefaultBrowserCommand []string
Rules []BrowserRuleConfig
}
// BrowserRuleConfig represents a BrowserRule
type BrowserRuleConfig struct {
Name string `hcl:"name,label"`
BrowserCommand []string `hcl:"browser_cmd"`
MatchExpr string `hcl:"match"`
BrowserCommand []string
Matchers []MatcherConfig
}
func MakeMatchFunc(rule string) MatchFunc {
return func(dest url.URL) bool {
env := map[string]interface{}{
// Prebuilt match functions
"matchAny": matchAny,
"matchHostRegexp": matchHostRegexp,
"matchHostname": matchHostname,
"matchNever": matchNever,
"matchRegexp": matchRegexp,
// Helpers for building custom matchers
"MatchFunc": func(foundMatch bool) MatchFunc {
return func(_ url.URL) bool {
return foundMatch
// 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,
},
"fullUrl": dest.String(),
"hostname": dest.Hostname(),
"url": dest,
}
matchFuncExpr, err := expr.Eval(rule, env)
if err != nil {
fmt.Printf("Error evaluating rule %s: %v", rule, err)
return false
}
matchFunc, ok := matchFuncExpr.(MatchFunc)
if !ok {
fmt.Printf("Error evaluating rule %s. Did not evaluate to a MatchFunc.", rule)
return false
}
return matchFunc(dest)
}
}
func LoadConfig(path string) ([]BrowserRule, error) {
rules := []BrowserRule{}
var config Config
err := hclsimple.DecodeFile(path, nil, &config)
if err != nil {
return nil, err
}
for _, rule := range config.Rules {
rules = append(rules, BrowserRule{
command: rule.BrowserCommand[0],
args: rule.BrowserCommand[1:],
matcher: MakeMatchFunc(rule.MatchExpr),
})
}
// Add default browser rule to the end
rules = append(rules, BrowserRule{
command: config.DefaultBrowserCommand[0],
args: config.DefaultBrowserCommand[1:],
BrowserRule{
matcher: defaultBrowser,
})
command: c.DefaultBrowserCommand[0],
args: c.DefaultBrowserCommand[1:],
},
)
return rules, nil
}
func DefaultConfigPath() (string, error) {
appName := "browser-ruler"
// Check XDG_CONFIG_HOME
dir, err := os.UserConfigDir()
if err == nil {
configPath := filepath.Join(dir, appName, "config.hcl")
if fileExists(configPath) {
return configPath, nil
}
return
}
dir, err = os.UserHomeDir()
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)
func GetTestRules() ([]BrowserRule, error) {
jsonBytes := []byte(`{
"DefaultBrowserCommand": ["chromium-browser"],
"Rules": [
{
"BrowserCommand": ["firefox"],
"Matchers": [
{
"Kind": "matchHostname",
"Targets": [
"iamthefij.com"
]
},
{
"Kind": "matchHostRegexp",
"Targets": [
".*\.iamthefij\.com$",
]
}
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")
]
}`)
return GetRules(jsonBytes)
}

5
go.mod
View File

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

82
go.sum
View File

@ -1,82 +0,0 @@
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
func matchNever(dest url.URL) bool {
func noop(dest url.URL) bool {
return false
}
func handleURL(browserRules []BrowserRule, urlString string) error {
func handleURL(urlString string) error {
dest, err := url.Parse(urlString)
if err != nil {
return fmt.Errorf("failed to parse url: %s, %w", urlString, err)
}
var matched bool
for _, rule := range browserRules {
for _, rule := range rules() {
matched, err = rule.MaybeLaunch(*dest)
if err != nil {
return fmt.Errorf("failed launching browser: %w", err)
@ -122,32 +122,11 @@ func handleURL(browserRules []BrowserRule, urlString string) error {
}
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()
// 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()
// For each url, run matcher
for _, urlString := range urls {
err := handleURL(browserRules, urlString)
err := handleURL(urlString)
if err != nil {
panic(err)
}

53
rules.go Normal file
View File

@ -0,0 +1,53 @@
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",
),
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),
}
}

View File

@ -1,64 +0,0 @@
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"
}