Compare commits

..

1 Commits
master ... hcl

Author SHA1 Message Date
IamTheFij a546764845 WIP: Refactor to read from an hcl config file and expr rules
Need to write some documentation
2022-01-27 22:07:41 -08:00
10 changed files with 287 additions and 117 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
@ -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

208
browser_ruler.py Executable file
View File

@ -0,0 +1,208 @@
#! /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:])

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,
}
}

View File

@ -34,35 +34,25 @@ type BrowserRuleConfig struct {
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,
"matchHostRegexp": matchHostRegexp,
"matchRegexp": matchRegexp,
// Helpers for building custom matchers
"matchAny": matchAny,
// Building custom matchers
"MatchFunc": func(foundMatch bool) MatchFunc {
return func(_ url.URL) bool {
return foundMatch
}
},
"fullUrl": dest.String(),
"hostname": dest.Hostname(),
"url": dest,
"url": dest.String(),
}
matchFuncExpr, err := expr.Eval(rule, env)
matchFunc, err := expr.Eval(rule, env)
if err != nil {
fmt.Printf("Error evaluating rule %s: %v", rule, err)
return false
panic(err)
}
matchFunc, ok := matchFuncExpr.(MatchFunc)
if !ok {
fmt.Printf("Error evaluating rule %s. Did not evaluate to a MatchFunc.", rule)
return false
}
return matchFunc(dest)
return matchFunc.(MatchFunc)(dest)
}
}

4
go.mod
View File

@ -3,6 +3,6 @@ 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
github.com/antonmedv/expr v1.9.0 // indirect
github.com/hashicorp/hcl/v2 v2.11.1 // indirect
)

9
go.sum
View File

@ -10,11 +10,9 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6
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=
@ -23,12 +21,9 @@ 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=
@ -37,18 +32,15 @@ github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
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=
@ -78,5 +70,4 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
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=

View File

@ -96,7 +96,7 @@ func defaultBrowser(dest url.URL) bool {
}
// Never returns true
func matchNever(dest url.URL) bool {
func noop(dest url.URL) bool {
return false
}

View File

@ -13,13 +13,9 @@ rule "Flatpak Chromium" {
# 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
# url: the full string of the URL
#
# When using those, you can turn them into a MatchFunc using
# MatchFunc(bool) MatchFunc
@ -27,7 +23,7 @@ rule "Flatpak Chromium" {
# For example:
# MatchFunc(hostname endsWith "example.com")
# is equivalent to:
# matchHostRegexp(".*\\.example\\.com$")
# matchHostRegexp(".*\\example\\.com$")
match = "matchHostname('google.com', 'youtube.com')"
}
@ -41,24 +37,3 @@ rule "Firefox Private" {
)
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"
}