Send arbitrary messages to arbitrary entities

This commit is contained in:
Emery Hemingway 2022-06-25 13:53:51 -05:00
parent 2ab8dd5fb9
commit f94473d4a6
8 changed files with 87 additions and 142 deletions

View File

@ -1,21 +1,14 @@
# xdg-open-ng
An `xdg-open` replacement that uses Syndicate and PCRE pattern matching to open URIs.
An [xdg-open](https://portland.freedesktop.org/doc/xdg-open.html) replacement that uses [Syndicate](http://syndicate-lang.org/) and PCRE pattern matching to open URIs.
There are two utilites, `xdg-open` and `uri_runner`. The former connects to a shared Syndicate dataspace via a UNIX socket at `$SYNDICATE_SOCK` otherwise `$XDG_RUNTIME_DIR/dataspace` and as has no other configuration. The `uri_runner` component is intended to be managed by the [Syndicate server](https://git.syndicate-lang.org/syndicate-lang/syndicate-rs) thru which it receives configuration, see [uri_runner.pr](./uri_runner.pr) as an example.
There are two utilites, `xdg-open` and `uri_runner`. The former connects to a shared Syndicate dataspace via a UNIX socket at `$SYNDICATE_SOCK` otherwise `$XDG_RUNTIME_DIR/dataspace` and has no other configuration. The `uri_runner` component is intended to be managed by the [Syndicate server](https://git.syndicate-lang.org/syndicate-lang/syndicate-rs) thru which it receives configuration, see [uri_runner.pr](./uri_runner.pr) as an example.
Matching patterns to actions is done with `action-handler` records:
```preserves
<action-handler "foo://(.*):(.*)" $entity <krempel ["--host=$1" "--port=$2"]> >
```
In the preceding example the URI `foo://bar:42` would cause the message `<krempel ["--host=bar" "--port=42"]>` to be sent to `$entity`.
The [protocol.nim](./src/protocol.nim) file is generated from the [protocol.prs](./protocol.prs) schema, a [Tupfile](https://gittup.org/tup/) file is provided to do this.
## TODO
- Fallback commands?
## Examples
| Type | Rule |
|-|-|
| Bittorrent | <code><action-handler "magnet:?.*xt=urn:btih.*" ["transmission-remote-gtk" 0]></code> |
| Gemini | <code><action-handler "gemini://.*|file:///.*.gmi" ["/run/current-system/sw/bin/lagrange" 0]></code> |
| PDF | <code><action-handler "file://(.*.pdf)" ["mupdf" 1]></code> |
| Tox | <code><action-handler "tox:.*|uri:tox:.*", ["/nix/store/hix5jibhdzx0a2qbq5cqihac060zz10b-qtox-1.17.6/bin/qtox" 0]></code> |
| Twatter | <code><action-handler "https://twitter.com/(.*)" ["firefox" "--new-tab" "https://nitter.net/$1"]></code> |
| Video | <code><action-handler ".*\\.avi|.*\\.mkv|.*mp4|.*ogg|.*youtu.*|.*\\.m3u8|.*webm" ["mpv" "--no-terminal" "--force-window=immediate" "--loop-playlist" "--ytdl-format=best" 0]></code> |

View File

@ -1,23 +0,0 @@
; Copied from ../syndicate-rs/syndicate-server/protocols/schemas/externalServices.prs
version 1 .
Exec = <exec @argv CommandLine @restartPolicy RestartPolicy> .
CommandLine = @shell string / @full FullCommandLine .
FullCommandLine = [@program string @args string ...] .
RestartPolicy =
/ ; Whether the process terminates normally or abnormally, restart it
; without affecting any peer processes within the service.
=always
/ ; If the process terminates normally, leave everything alone; if it
; terminates abnormally, restart it without affecting peers.
@onError =on-error
/ ; If the process terminates normally, leave everything alone; if it
; terminates abnormally, restart the whole daemon (all processes
; within the daemon).
=all
/ ; Treat both normal and abnormal termination as normal termination; that is, never restart,
; and enter state "complete" even if the process fails.
=never
.

View File

@ -4,4 +4,4 @@ XdgOpen = <xdg-open @uris [string ...]> .
UriRunnerConfig = ListenOn / ActionHandler .
ListenOn = <listen-on @dataspace #!any> .
ActionHandler = <action-handler @pat string @cmd [string ...]> .
ActionHandler = <action-handler @pat string @entity #!any @action any> .

View File

@ -1,32 +0,0 @@
import
std/typetraits, preserves
type
CommandLineKind* {.pure.} = enum
`shell`, `full`
CommandLineShell* = string
`CommandLine`* {.preservesOr.} = object
case orKind*: CommandLineKind
of CommandLineKind.`shell`:
`shell`*: CommandLineShell
of CommandLineKind.`full`:
`full`*: FullCommandLine
Exec* {.preservesRecord: "exec".} = object
`argv`*: CommandLine
`restartPolicy`*: RestartPolicy
`RestartPolicy`* {.preservesOr, pure.} = enum
`always`, `onError`, `all`, `never`
FullCommandLine* {.preservesTuple.} = object
`program`*: string
`args`* {.preservesTupleTail.}: seq[string]
proc `$`*(x: CommandLine | Exec | FullCommandLine): string =
`$`(toPreserve(x))
proc encode*(x: CommandLine | Exec | FullCommandLine): seq[byte] =
encode(toPreserve(x))

View File

@ -14,24 +14,26 @@ type
`listenon`*: ListenOn[E]
of UriRunnerConfigKind.`ActionHandler`:
`actionhandler`*: ActionHandler
`actionhandler`*: ActionHandler[E]
ListenOn*[E] {.preservesRecord: "listen-on".} = ref object
`dataspace`*: Preserve[E]
ActionHandler* {.preservesRecord: "action-handler".} = object
ActionHandler*[E] {.preservesRecord: "action-handler".} = ref object
`pat`*: string
`cmd`*: seq[string]
`action`*: Preserve[E]
`entity`*: Preserve[E]
proc `$`*[E](x: UriRunnerConfig[E] | ListenOn[E]): string =
proc `$`*[E](x: UriRunnerConfig[E] | ListenOn[E] | ActionHandler[E]): string =
`$`(toPreserve(x, E))
proc encode*[E](x: UriRunnerConfig[E] | ListenOn[E]): seq[byte] =
proc encode*[E](x: UriRunnerConfig[E] | ListenOn[E] | ActionHandler[E]): seq[
byte] =
encode(toPreserve(x, E))
proc `$`*(x: XdgOpen | ActionHandler): string =
proc `$`*(x: XdgOpen): string =
`$`(toPreserve(x))
proc encode*(x: XdgOpen | ActionHandler): seq[byte] =
proc encode*(x: XdgOpen): seq[byte] =
encode(toPreserve(x))

View File

@ -3,52 +3,39 @@
import std/[asyncdispatch, re]
import preserves, syndicate
import ./protocol, ./exec
import ./protocol
type RegexAction = tuple[regex: Regex; entity: Ref; action: Assertion]
proc rewrite(result: var Assertion; uri: string; regex: Regex) =
proc op(pr: var Assertion) =
if pr.isString:
pr.string = replacef(uri, regex, pr.string)
apply(result, op)
bootDataspace("main") do (root: Ref; turn: var Turn):
var actions: seq[tuple[regex: Regex; cmd: string; args: seq[Assertion]]]
var handlers: seq[RegexAction]
connectStdio(root, turn)
onPublish(turn, root, ?ActionHandler) do (pat: string; cmd: seq[Assertion]):
if cmd.len < 2:
stderr.writeLine "ignoring ", $cmd, " for ", pat
else:
if cmd[0].isString:
var act = (re(pat, {reIgnoreCase, reStudy}), cmd[0].string, cmd[1..cmd.high],)
actions.add act
else:
stderr.writeLine "not a valid program specification: ", cmd[0]
onPublish(turn, root, ?ActionHandler[Ref]) do (pat: string; entity: Ref; response: Assertion):
handlers.add (re(pat, {reIgnoreCase, reStudy}), entity, response,)
during(turn, root, ?ListenOn[Ref]) do (a: Assertion):
let ds = unembed a
during(turn, root, ?ListenOn[Ref]) do (ds: Ref):
onMessage(turn, ds, ?XdgOpen) do (uris: seq[string]):
for uri in uris:
var matched: bool
for act in actions:
if match(uri, act.regex):
for handler in handlers:
if match(uri, handler.regex):
matched = true
var args = newSeq[string](act.args.len)
for i, arg in act.args:
if arg.isString:
args[i] = replacef(uri, act.regex, arg.string)
elif arg.isInteger:
if arg.int == 0:
args[i] = uri
else:
args[i] = replacef(uri, act.regex, "$" & $arg.int)
message(turn, root, Exec(
argv: CommandLine(
orKind: CommandLineKind.full,
full: FullCommandLine(
program: act.cmd,
args: args)),
restartPolicy: RestartPolicy.never))
var response = handler.action
rewrite(response, uri, handler.regex)
message(turn, handler.entity, response)
if not matched:
stderr.writeLine "no actions matched for ", uri
do:
# The Syndicate server retracts all assertions when
# the config is rewritten.
actions.setLen 0
handlers.setLen 0
runForever()

View File

@ -1,50 +1,68 @@
; Expose a dataspace over a unix socket
let ?root_ds = dataspace
let ?socketspace = dataspace
<require-service <relay-listener <unix "/run/user/1000/dataspace"> $gatekeeper>>
<bind "syndicate" #x"" $root_ds>
<bind "syndicate" #x"" $socketspace>
; Create a new dataspace for receiving "exec" messages
; from the uri_runner
let ?execspace = dataspace
$execspace ?? <exec ?argv> $config [
let ?id = timestamp
let ?facet = facet
let ?d = <uri_runner-exec $id $argv>
<run-service <daemon $d>>
<daemon $d {
argv: $argv,
env: { WAYLAND_DISPLAY: "wayland-1" DISPLAY: ":0" XDG_CACHE_HOME: "/home/cache" }
readyOnStart: #f,
restart: =never,
}>
? <service-state <daemon $d> complete> [$facet ! stop]
? <service-state <daemon $d> failed> [$facet ! stop]
]
; Start the uri_runner
<require-service <daemon uri_runner>>
<daemon uri_runner {
argv: "uri_runner"
protocol: text/syndicate
}>
? <service-object <daemon uri_runner> ?cap> [
? <service-object <daemon uri_runner> ?cap> $cap [
; send configuration to uri_runner
$cap [
<listen-on $root_ds>
<listen-on $socketspace>
; Here the "0" argument is replaced with the whole URI asserted by xdg-open.
<action-handler "http://.*|https://.*|.*html", ["/run/current-system/sw/bin/librewolf" 0]>
; When http* is matched send a message to $execspace
; that indicates the Syndicate server should start Firefox.
<action-handler "(http://.*|https://.*|.*html)" $execspace
<exec ["/bin/firefox" "--new-tab" "$1"]>>
; An argument can be a reference to a capture.
<action-handler "(tox:.*)|uri:(tox:.*)", ["/run/current-system/sw/bin/qtox" 1]>
; Capture a pattern within a pattern
<action-handler "https://twitter.com/(.*)" $execspace
<exec ["/bin/firefox" "--new-tab" "https://nitter.net/$1"]>>
; An argument can contain a reference to a capture using the $i notation.
<action-handler "https://twitter.com/(.*)" ["/run/current-system/sw/bin/librewolf" "https://nitter.net/$1"]>
; Local file-system paths should always be prefixed
; by file:// but that can be removed after matching
<action-handler "file://(.*.pdf)" $execspace
<exec ["/bin/mupdf" "$1"]>>
<action-handler "gemini://.*|file:///.*.gmi" ["/run/current-system/sw/bin/kristall" 0]>
<action-handler ".*\\.avi|.*\\.mkv|.*\\.mp4|.*\\.ogg|.*\\.opus", ["/run/current-system/sw/bin/mpv" 0]>
<action-handler "(magnet:?.*xt=urn:btih.*)" $execspace
<exec ["/bin/transmission-remote-gtk" "$1"]>>
; filesystem paths are always prefixed with file://
<action-handler "file://(.*.pdf)" ["/run/current-system/sw/bin/mupdf" 1]>
]
<action-handler "(tox:.*)|uri:(tox:.*)" $execspace
<exec ["/bin/qtox" "$1"]>>
; uri_runner sends messages to the server to start handler applications
$cap ?? <exec ?argv ?restartPolicy> [
let ?id = timestamp
let ?facet = facet
let ?d = <uri_runner-exec $id $argv>
$config <run-service <daemon $d>>
$config <daemon $d {
argv: $argv,
readyOnStart: #f,
restart: $restartPolicy,
}>
$config ? <service-state <daemon $d> complete> [$facet ! stop]
$config ? <service-state <daemon $d> failed> [$facet ! stop]
<action-handler "(gemini://.*|file:///.*.gmi)" $execspace
<exec ["/bin/lagrange" "$1"]>>
<action-handler "(https://.*\\.twitch.tv/.*)" $execspace
<exec ["/bin/streamlink" "-p" "mpv" "$1"]>>
; when the mpv-transator is available send it commands directly
$config ? <service-object <daemon mpv-translator> ?mpv> $mpv [
$cap <action-handler "(.*\\.avi|.*\\.mkv|.*mp4|.*ogg|.*youtu.*|.*\\.m3u8|.*webm)" $mpv
<mpv 1 { "command": ["loadfile" "$1" "append-play"] }>>
]
]

View File

@ -1,6 +1,6 @@
# Package
version = "0.4.0"
version = "99999999"
author = "Emery"
description = "A better xdg-open"
license = "Unlicense"