Send arbitrary messages to arbitrary entities
This commit is contained in:
parent
2ab8dd5fb9
commit
f94473d4a6
25
README.md
25
README.md
|
@ -1,21 +1,14 @@
|
||||||
# xdg-open-ng
|
# 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.
|
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> |
|
|
||||||
|
|
23
exec.prs
23
exec.prs
|
@ -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
|
|
||||||
.
|
|
|
@ -4,4 +4,4 @@ XdgOpen = <xdg-open @uris [string ...]> .
|
||||||
|
|
||||||
UriRunnerConfig = ListenOn / ActionHandler .
|
UriRunnerConfig = ListenOn / ActionHandler .
|
||||||
ListenOn = <listen-on @dataspace #!any> .
|
ListenOn = <listen-on @dataspace #!any> .
|
||||||
ActionHandler = <action-handler @pat string @cmd [string ...]> .
|
ActionHandler = <action-handler @pat string @entity #!any @action any> .
|
||||||
|
|
32
src/exec.nim
32
src/exec.nim
|
@ -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))
|
|
|
@ -14,24 +14,26 @@ type
|
||||||
`listenon`*: ListenOn[E]
|
`listenon`*: ListenOn[E]
|
||||||
|
|
||||||
of UriRunnerConfigKind.`ActionHandler`:
|
of UriRunnerConfigKind.`ActionHandler`:
|
||||||
`actionhandler`*: ActionHandler
|
`actionhandler`*: ActionHandler[E]
|
||||||
|
|
||||||
|
|
||||||
ListenOn*[E] {.preservesRecord: "listen-on".} = ref object
|
ListenOn*[E] {.preservesRecord: "listen-on".} = ref object
|
||||||
`dataspace`*: Preserve[E]
|
`dataspace`*: Preserve[E]
|
||||||
|
|
||||||
ActionHandler* {.preservesRecord: "action-handler".} = object
|
ActionHandler*[E] {.preservesRecord: "action-handler".} = ref object
|
||||||
`pat`*: string
|
`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))
|
`$`(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))
|
encode(toPreserve(x, E))
|
||||||
|
|
||||||
proc `$`*(x: XdgOpen | ActionHandler): string =
|
proc `$`*(x: XdgOpen): string =
|
||||||
`$`(toPreserve(x))
|
`$`(toPreserve(x))
|
||||||
|
|
||||||
proc encode*(x: XdgOpen | ActionHandler): seq[byte] =
|
proc encode*(x: XdgOpen): seq[byte] =
|
||||||
encode(toPreserve(x))
|
encode(toPreserve(x))
|
||||||
|
|
|
@ -3,52 +3,39 @@
|
||||||
|
|
||||||
import std/[asyncdispatch, re]
|
import std/[asyncdispatch, re]
|
||||||
import preserves, syndicate
|
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):
|
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)
|
connectStdio(root, turn)
|
||||||
|
|
||||||
onPublish(turn, root, ?ActionHandler) do (pat: string; cmd: seq[Assertion]):
|
onPublish(turn, root, ?ActionHandler[Ref]) do (pat: string; entity: Ref; response: Assertion):
|
||||||
if cmd.len < 2:
|
handlers.add (re(pat, {reIgnoreCase, reStudy}), entity, response,)
|
||||||
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]
|
|
||||||
|
|
||||||
during(turn, root, ?ListenOn[Ref]) do (a: Assertion):
|
during(turn, root, ?ListenOn[Ref]) do (ds: Ref):
|
||||||
let ds = unembed a
|
|
||||||
onMessage(turn, ds, ?XdgOpen) do (uris: seq[string]):
|
onMessage(turn, ds, ?XdgOpen) do (uris: seq[string]):
|
||||||
for uri in uris:
|
for uri in uris:
|
||||||
var matched: bool
|
var matched: bool
|
||||||
for act in actions:
|
for handler in handlers:
|
||||||
if match(uri, act.regex):
|
if match(uri, handler.regex):
|
||||||
matched = true
|
matched = true
|
||||||
var args = newSeq[string](act.args.len)
|
var response = handler.action
|
||||||
for i, arg in act.args:
|
rewrite(response, uri, handler.regex)
|
||||||
if arg.isString:
|
message(turn, handler.entity, response)
|
||||||
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))
|
|
||||||
if not matched:
|
if not matched:
|
||||||
stderr.writeLine "no actions matched for ", uri
|
stderr.writeLine "no actions matched for ", uri
|
||||||
do:
|
do:
|
||||||
# The Syndicate server retracts all assertions when
|
# The Syndicate server retracts all assertions when
|
||||||
# the config is rewritten.
|
# the config is rewritten.
|
||||||
actions.setLen 0
|
handlers.setLen 0
|
||||||
|
|
||||||
runForever()
|
runForever()
|
||||||
|
|
|
@ -1,50 +1,68 @@
|
||||||
; Expose a dataspace over a unix socket
|
; Expose a dataspace over a unix socket
|
||||||
let ?root_ds = dataspace
|
let ?socketspace = dataspace
|
||||||
<require-service <relay-listener <unix "/run/user/1000/dataspace"> $gatekeeper>>
|
<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>>
|
<require-service <daemon uri_runner>>
|
||||||
|
|
||||||
<daemon uri_runner {
|
<daemon uri_runner {
|
||||||
argv: "uri_runner"
|
argv: "uri_runner"
|
||||||
protocol: text/syndicate
|
protocol: text/syndicate
|
||||||
}>
|
}>
|
||||||
|
|
||||||
? <service-object <daemon uri_runner> ?cap> [
|
? <service-object <daemon uri_runner> ?cap> $cap [
|
||||||
|
|
||||||
; send configuration to uri_runner
|
; send configuration to uri_runner
|
||||||
$cap [
|
<listen-on $socketspace>
|
||||||
<listen-on $root_ds>
|
|
||||||
|
|
||||||
; Here the "0" argument is replaced with the whole URI asserted by xdg-open.
|
; When http* is matched send a message to $execspace
|
||||||
<action-handler "http://.*|https://.*|.*html", ["/run/current-system/sw/bin/librewolf" 0]>
|
; 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.
|
; Capture a pattern within a pattern
|
||||||
<action-handler "(tox:.*)|uri:(tox:.*)", ["/run/current-system/sw/bin/qtox" 1]>
|
<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.
|
; Local file-system paths should always be prefixed
|
||||||
<action-handler "https://twitter.com/(.*)" ["/run/current-system/sw/bin/librewolf" "https://nitter.net/$1"]>
|
; 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 "(magnet:?.*xt=urn:btih.*)" $execspace
|
||||||
<action-handler ".*\\.avi|.*\\.mkv|.*\\.mp4|.*\\.ogg|.*\\.opus", ["/run/current-system/sw/bin/mpv" 0]>
|
<exec ["/bin/transmission-remote-gtk" "$1"]>>
|
||||||
|
|
||||||
; filesystem paths are always prefixed with file://
|
<action-handler "(tox:.*)|uri:(tox:.*)" $execspace
|
||||||
<action-handler "file://(.*.pdf)" ["/run/current-system/sw/bin/mupdf" 1]>
|
<exec ["/bin/qtox" "$1"]>>
|
||||||
]
|
|
||||||
|
|
||||||
; uri_runner sends messages to the server to start handler applications
|
<action-handler "(gemini://.*|file:///.*.gmi)" $execspace
|
||||||
$cap ?? <exec ?argv ?restartPolicy> [
|
<exec ["/bin/lagrange" "$1"]>>
|
||||||
let ?id = timestamp
|
|
||||||
let ?facet = facet
|
<action-handler "(https://.*\\.twitch.tv/.*)" $execspace
|
||||||
let ?d = <uri_runner-exec $id $argv>
|
<exec ["/bin/streamlink" "-p" "mpv" "$1"]>>
|
||||||
$config <run-service <daemon $d>>
|
|
||||||
$config <daemon $d {
|
; when the mpv-transator is available send it commands directly
|
||||||
argv: $argv,
|
$config ? <service-object <daemon mpv-translator> ?mpv> $mpv [
|
||||||
readyOnStart: #f,
|
$cap <action-handler "(.*\\.avi|.*\\.mkv|.*mp4|.*ogg|.*youtu.*|.*\\.m3u8|.*webm)" $mpv
|
||||||
restart: $restartPolicy,
|
<mpv 1 { "command": ["loadfile" "$1" "append-play"] }>>
|
||||||
}>
|
|
||||||
$config ? <service-state <daemon $d> complete> [$facet ! stop]
|
|
||||||
$config ? <service-state <daemon $d> failed> [$facet ! stop]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "0.4.0"
|
version = "99999999"
|
||||||
author = "Emery"
|
author = "Emery"
|
||||||
description = "A better xdg-open"
|
description = "A better xdg-open"
|
||||||
license = "Unlicense"
|
license = "Unlicense"
|
||||||
|
|
Loading…
Reference in New Issue