From 292392873ea49b445c0618ab384aff11752ae290 Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Thu, 27 Jan 2022 22:07:41 -0800 Subject: [PATCH] Refactor to read from an hcl config file and expr rules --- Makefile | 3 +- README.md | 48 +++++++++-- browser_ruler.py | 208 ---------------------------------------------- browsers.go | 60 ------------- config.go | 191 +++++++++++++++++++----------------------- go.mod | 5 ++ go.sum | 82 ++++++++++++++++++ main.go | 29 ++++++- rules.go | 54 ------------ sample-config.hcl | 64 ++++++++++++++ 10 files changed, 306 insertions(+), 438 deletions(-) delete mode 100755 browser_ruler.py delete mode 100644 browsers.go create mode 100644 go.sum delete mode 100644 rules.go create mode 100644 sample-config.hcl diff --git a/Makefile b/Makefile index e947e97..6dab0a8 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,3 @@ -# BIN = browser_ruler.py BIN = browser-ruler SCRIPT_INSTALL_PATH ?= $(HOME)/.local/bin DESKTOP_INSTALL_PATH ?= $(HOME)/.local/share/applications @@ -8,7 +7,7 @@ default: test .PHONY: test test: $(BIN) - ./$(BIN) https://duck.com/ + ./$(BIN) -config ./sample-config.hcl https://duck.com/ $(BIN): *.go go build . diff --git a/README.md b/README.md index a1fffa1..66c3268 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,55 @@ 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 -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 make install set-default - diff --git a/browser_ruler.py b/browser_ruler.py deleted file mode 100755 index b659f5b..0000000 --- a/browser_ruler.py +++ /dev/null @@ -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:]) diff --git a/browsers.go b/browsers.go deleted file mode 100644 index 4c84a7b..0000000 --- a/browsers.go +++ /dev/null @@ -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, - } -} diff --git a/config.go b/config.go index 4e14e7e..63305d8 100644 --- a/config.go +++ b/config.go @@ -1,129 +1,112 @@ package main import ( - "encoding/json" "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 type Config struct { - DefaultBrowserCommand []string - Rules []BrowserRuleConfig -} - -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 + DefaultBrowserCommand []string `hcl:"default_browser_cmd"` + Rules []BrowserRuleConfig `hcl:"rule,block"` } // BrowserRuleConfig represents a BrowserRule type BrowserRuleConfig struct { - BrowserCommand []string - Matchers []MatcherConfig + Name string `hcl:"name,label"` + BrowserCommand []string `hcl:"browser_cmd"` + MatchExpr string `hcl:"match"` } -// MatcherConfig represents a single MatchFunc -type MatcherConfig struct { - Kind string - Targets []string +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 + } + }, + "fullUrl": dest.String(), + "hostname": dest.Hostname(), + "url": dest, + } + matchFunc, err := expr.Eval(rule, env) + if err != nil { + panic(err) + } + + return matchFunc.(MatchFunc)(dest) + } } -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) +func LoadConfig(path string) ([]BrowserRule, error) { + rules := []BrowserRule{} + var config Config + err := hclsimple.DecodeFile(path, nil, &config) if err != nil { - return + return nil, err } - 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:], - }, - ) - + for _, rule := range config.Rules { + rules = append(rules, BrowserRule{ + command: rule.BrowserCommand[0], + args: rule.BrowserCommand[1:], + matcher: MakeMatchFunc(rule.MatchExpr), + }) } - return + // Add default browser rule to the end + rules = append(rules, BrowserRule{ + command: config.DefaultBrowserCommand[0], + args: config.DefaultBrowserCommand[1:], + matcher: defaultBrowser, + }) + + return rules, nil } -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) +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 + } + } + + 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) + } + + 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") } diff --git a/go.mod b/go.mod index e2cd197..e227333 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ 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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aaee386 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 9f24385..0d177f0 100644 --- a/main.go +++ b/main.go @@ -96,18 +96,18 @@ func defaultBrowser(dest url.URL) bool { } // Never returns true -func noop(dest url.URL) bool { +func matchNever(dest url.URL) bool { return false } -func handleURL(urlString string) error { +func handleURL(browserRules []BrowserRule, 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 rules() { + for _, rule := range browserRules { matched, err = rule.MaybeLaunch(*dest) if err != nil { return fmt.Errorf("failed launching browser: %w", err) @@ -122,11 +122,32 @@ func handleURL(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(urlString) + err := handleURL(browserRules, urlString) if err != nil { panic(err) } diff --git a/rules.go b/rules.go deleted file mode 100644 index d49d9c9..0000000 --- a/rules.go +++ /dev/null @@ -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), - } -} diff --git a/sample-config.hcl b/sample-config.hcl new file mode 100644 index 0000000..79c8211 --- /dev/null +++ b/sample-config.hcl @@ -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 = <