~conses/nx-router

Easy to define URL routes in Nyxt
README: Update with examples section.
router.lisp: Fix string external rules.
LICENSE: Change author

refs

master
browse  log 

clone

read-only
https://git.sr.ht/~conses/nx-router
read/write
git@git.sr.ht:~conses/nx-router

You can also use your local clone with git send-email.

1. nx-router

nx-router is a URL routing extension for Nyxt. It lets you define fine-grained routes so you can enhance the browsing experience without getting your attention sucked away. You can set up URL redirects, block lists, open resources with external applications, all in a cohesive configuration language.

Note that the extension might not work consistently with click events, as these are obscured by WebkitGTK, and there is currently no way to redirect iframes, which is why this extension is currently aimed at handling top-level requests. For a consistent behavior, you should consider following hints via follow-hint or open links in a new buffer through the middle-click button or the context menu.

1.1. Installation

To install the extension, you need to download the source and place it in Nyxt's extensions path, given by the value of nyxt-source-registry (by default ~/.local/share/nyxt/extensions).

git clone https://git.sr.ht/~conses/nx-router ~/.local/share/nyxt/extensions/nx-router

The extension works with Nyxt 3 onward but it's encouraged to use it with the latest version of Nyxt master for the time being.

If you want to place the extension elsewhere in the system, such as for development purposes, you can configure so via the ASDF source registry mechanism. For this, you'll need to create a file in the source registry directory, ~/.config/common-lisp/source-registry.conf.d/, and then put the following contents into it, replacing the path with the desired system path.

(:tree "/path/to/user/location")

Then, make sure to refresh the ASDF cache via asdf:clear-source-registry. ASDF will now be able to find the extension on the custom path. For more information on this utility, please refer to the ASDF manual.

By default, Nyxt won't read the custom source registry path we provided, so ensure to include a reset-asdf-registries invocation in Nyxt's configuration file too.

In your Nyxt configuration file, place the following.

(define-nyxt-user-system-and-load nyxt-user/router
  :depends-on (nx-router)
  :components ("router.lisp"))

Where router.lisp is a custom file that should you should create relative to Nyxt's configuration directory (*config-file*'s pathname by default) to provide the extension settings after the nx-router system has been successfully loaded. Inside this file, place the following.

(define-configuration web-buffer
  ((default-modes `(router:router-mode ,@%slot-value%))))

In addition, you should add the extension options, explained in the following section.

1.2. Configuration

You can configure nx-router by customizing the mode's slots as follows.

(import 'router:make-route)

(define-configuration router:router-mode
  ((router:enforce-p t)
   (router:media-enabled-p t)
   (router:banner-p t)
   (router:routes '())))

Where router:router-mode slots include:

enforce-p
if non-nil, this prevents you from disabling the mode.
media-enabled-p
specifies whether to show media in sites. This can be overridden on a per-router:route basis later in the configuration.
banner-p
if non-nil, displays a Nyxt internal page upon visiting blocked triggers.
routes
list of router:route's. See the below documentation on how to build a router:route object.

router:route slots include a mini-DSL that specifies what URL part to block, redirect or invoke externally via a comparison type. Personally, I believe this is a bit more straightforward than having to fiddle around with complex regular expressions and it allows you to define many site behaviors within a single point. The following is a detailed description of all the available slots. If this is not clear to you, feel free to skip to the next section, where many example routes are provided with their various forms.

redirect
takes a URL host to redirect to as a string or as a quri:uri object. For more complex redirection logic, you can supply a router:redirect object (see its class definition for the available slots), an arbitrary function to compute a redirect host, or a cons of the form (REDIRECT-URI . TYPES), where TYPES is a property list of the form (TYPE . RULES) that can currently only take :path for TYPE. RULES is an alist of the form (REPLACEMENT-PATH . ORIGINAL-PATHS), where ORIGINAL-PATHS is a single string or list of paths of the original URL which will be redirected to REPLACEMENT-PATH. If you want to redirect all paths except ORIGINAL-PATHS to REPLACEMENT-PATH, prefix this list with not.
blocklist
takes a router:blocklist object (see its class definition for the available slots), the value t to block the entire route, or a property list of blocking conditions in the form of (TYPE VALUE), where TYPE can be one of :path or :host, and VALUE is either another property list of the form (TYPE PATHNAMES), where TYPE is either :starts, :ends, or :contains to denote the URL comparison and PATHNAMES is a simple string or a list of URL pathnames to draw the comparison against. You can also pass an integer as VALUE to indicate the number of URL sections (e.g. https://example.com/section1/section2) to block in case the blocking condition value is not known. If PATHNAMES is prefixed with not, all paths will be blocked except for the specified list.
external
used to open resources externally. If it's a function, it takes a single parameter REQUEST-DATA and can invoke arbitrary Lisp forms within it. If provided as a string, it will run the specified command via uiop:run-program with the current URL as its argument in a format-like syntax.
original
the route's original hostname, which can be used for storage purposes (bookmarks, history, etc.) so that the original URL is recorded instead of the redirect's URL.
media-p
whether to show media in the resource or not. This is useful if you want to block all media via the router:media-enabled-p slot, but only override it for certain resources.
instances
you can provide a custom function to compute a list of instances, which will be added to the route's triggers automatically. This is useful if a service provides an official endpoint where these are stored.

1.3. Examples

Set up all Instagram requests to redirect to the hostname www.picuki.com and additionally redirect all the paths that don't start with /, /p/, or /tv/ to /profile/ paths, and all paths that do start with /p/ to /media/, as this is what the alternative front-end uses for its URL structure.

(make-route (match-regex "https://(www.)?insta.*")
            :redirect (make-instance
                       'router:redirect
                       :to "www.picuki.com"
                       :rules '(("/profile/" . (not "/" "/p/" "/tv/"))
                                ("/media/" . "/p/")))
            :blocklist (make-instance
                        'router:blocklist
                        :rules '(:contains (not "/video"))))

Redirect all TikTok requests except the index path to /@placeholder/video/ since this is what ProxiTok uses for its video paths.

(make-route (match-domain "tiktok.com")
            :redirect '("proxitok.herokuapp.com" .
                        (:path (("/@placeholder/video/" . (not "/" "/@"))))))

The above uses the cons form of :redirect that can be argued to be terser, but you can re-write the redirect to use a router:redirect object if it seems clearer to you.

(make-route (match-domain "tiktok.com")
            :redirect (make-instance
                       'router:redirect
                       :to "proxitok.herokuapp.com"
                       :rules '(("/@placeholder/video/" . (not "/" "/@"))))

Redirect all Reddit requests to the teddit.namazso.eu host and additionally block all of the paths belonging to this trigger except those that contain the /comments path. This would essentially limit the user to only being able to access Reddit publications instead of sections like its main feed.

(make-route (match-domain "reddit.com")
            :redirect "teddit.namazso.eu"
            :original "www.reddit.com"
            :blocklist '(:path (:contains (not "/comments"))))

You can pass an :original slot to the route, so that if you wrap Nyxt internal methods like shown below, history entries will get recorded with the original URL, meaning an inverse redirection will be applied to figure out the original URL structure.

(defmethod nyxt:on-signal-load-finished :around ((mode nyxt/history-mode:history-mode) url)
  (call-next-method mode (router:trace-url url)))

Matches on YouTube video URLs, videos hosted on its alternative front-ends such as Invidious, as well as MP3 files, redirecting all of these requests to youtube.com, and dispatching a rule which invokes an external program with the current request data, in this case launching an mpv player IPC client process through mpv.el to control the player from Emacs. You could also pass a one-placeholder format string such as mpv --video=no ~a to the :external slot if you'd rather not use a Lisp form, where ~a represents the current route URL. Note how a route's trigger can also consist of a list of predicates for which to match URLs, which means on the route below, it will match either URLs that contain the video Regexp or URLs that contain the .mp3 file extension.

(make-route '((match-regex ".*/watch\\?v=.*")
              (match-file-extension "mp3"))
            :redirect "youtube.com"
            :external (lambda (data)
                        (eval-in-emacs
                         `(mpv-start
                           ,(quri:render-uri (url data))))))

The route below takes an :instances slot, which is a custom function like make-invidious-instances to be provided by the user that will compute a list of instances. This is useful if the service that is used as the :redirect slot offers a list of predefined instances, and these will also be added to the route's triggers on route instantiation. :redirect slots can also take an arbitrary function which will compute the redirect hostname to use.

(defun set-invidious-instance ()
  "Set the primary Invidious instance."
  (let ((instances
          (remove-if-not
           (lambda (instance)
             (and (string= (alex:assoc-value (second instance) :region)
                           "DE")
                  (string= (alex:assoc-value (second instance) :type)
                           "https")))
           (json:with-decoder-simple-list-semantics
             (json:decode-json-from-string
              (dex:get "https://api.invidious.io/instances.json"))))))
    (first (car instances))))

(defun make-invidious-instances ()
  "Return a list of Invidious instances."
  (mapcar 'first
          (json:with-decoder-simple-list-semantics
            (json:decode-json-from-string
             (dex:get "https://api.invidious.io/instances.json")))))

(make-route (match-domain "youtube.com" "youtu.be")
            :redirect 'set-invidious-instance
            :instances 'make-invidious-instances
            :blocklist '(:path (:starts "/c/")))

In case you'd like to specify a different URL scheme than HTTPS or a different port, you should supply a redirect in the form of a quri:uri object. For instance, the following sets up a route that redirects Google search results to a locally-running instance of Whoogle, and where these will appear as if they were searched in Google.

(make-route (match-regex "https://whoogle.*"
                         "https://.*google.com/search.*")
            :original (quri:uri "https://www.google.com")
            :redirect (quri:uri "http://localhost:5000"))

If you'd like to randomize your redirect hostname, you can use a service like Farside by including the following route, which redirects all Twitter URLs to the respective Farside endpoint.

(make-route (match-domain "twitter.com")
            :redirect (make-instance
                       'router:redirect
                       :to "farside.link"
                       :rules '(("/nitter/" . "/"))))

Showcases the use of a hostname blocklist, in this case preventing the user from accessing Amazon URLs unless they start with the smile hostname.

(make-route (match-domain "amazon.com")
            :blocklist '(:host (:starts (not "smile"))))

The above is the property list version of :blocklist that can be argued to be terser, but as shown in previous examples, we can rewrite it to use a router:blocklist object if it's clearer for us.

(make-route (match-domain "amazon.com")
            :blocklist (make-instance
                        'router:blocklist
                        :block-type :host
                        :rules '(:starts (not "smile"))))

Consists of a blocklist for certain paths of the lemmy.ml domain; namely, the blocked paths would be those that contain post on them or the ones that start with /u/, which would block all publications and user profiles on the site.

(make-route (match-domain "lemmy.ml")
            :blocklist '(:path (:contains "post" :starts "/u/")))

Provides a combined path rule for github.com requests. Combined rules (specified via or) in paths allow you to specify two or more predicates that you wish to draw the path comparison against. In this specific combination, the integer will first indicate that we want to block those paths that consist of a single sub-section (e.g. https://github.com/profile_name), or block all paths except the ones which contain pulls or search. This essentially allows you to specify a more general block rule and bypass it for certain scenarios. In this case, it would block all single sub-section paths on github.com (such as profiles, the marketplace and so on) but at the same time allow you to use GitHub's search engine and see your listed pull requests.

(make-route (match-domain "github.com")
            :blocklist '(:path (or 1 (:contains (not "pulls" "search")))))

Serves as a general blocklist trigger. To block an entire URL predicate or list of predicates, you can simply pass t to the blocklist slot.

(make-route (match-domain "timewastingsite1.com"
                          "timewastingsite2.com")
            :blocklist t)

1.4. Contributing

You can use the project's mailing list to send feedback, patches or open discussions. Bugs should be reported on the project's bug-tracker.