Compare commits

...

16 Commits

20 changed files with 243 additions and 220 deletions

2
.envrc Normal file
View File

@ -0,0 +1,2 @@
source_env ..
use nix

3
.gitignore vendored
View File

@ -1,2 +1 @@
uri_runner
xdg_open
/nim.cfg

View File

@ -1,10 +1,15 @@
# xdg-open-ng
# Syndicated Open
An `xdg-open` replacement that uses Syndicate and PCRE pattern matching to open URIs.
An [open command](https://en.wikipedia.org/wiki/Open_(process)) implementation 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, `open` and `uri_runner`. The former connects to a shared Syndicate dataspace using a route at `$SYNDICATE_ROUTE` 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`.
See [handlers-example.pr](./handlers-example.pr) for more information.
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?

2
Tupfile Normal file
View File

@ -0,0 +1,2 @@
include_rules
: lock.json |> !nim_cfg |> | ./<lock>

3
Tuprules.tup Normal file
View File

@ -0,0 +1,3 @@
include ../syndicate-nim/depends.tup
NIM_FLAGS += --path:$(TUP_CWD)/../syndicate-nim/src
NIM_GROUPS += $(TUP_CWD)/<lock>

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
.

29
handlers-example.pr Normal file
View File

@ -0,0 +1,29 @@
; the <exec-space #!…> dataspace starts programs when it receives exec messages
? <exec-space ?execspace> [
<action-handler "file://(.*.pdf)" $execspace
<exec ["/run/current-system/sw/bin/mupdf" "$1"]>>
<action-handler "file://(.*.png)" $execspace
<exec ["/run/current-system/sw/bin/imv" "$1"]>>
<action-handler "(magnet:?.*xt=urn:btih.*)" $execspace
<exec ["/run/current-system/sw/bin/transmission-gtk" "$1"]>>
<action-handler "(tg://.*)" $execspace
<exec ["/run/current-system/sw/bin/telegram-desktop" "$1"]>>
<action-handler "https://twitter.com/(.*)" $execspace
<exec ["/run/current-system/sw/bin/firefox" "--new-tab" "https://nitter.net/$1"]>>
<action-handler "(file:///.*.html|http://.*|https://.*)" $execspace
<exec ["/run/current-system/sw/bin/firefox" "$#"]>>
<action-handler "mailto:(.*)" $execspace
<exec ["/run/current-system/sw/bin/astroid" "--mailto" "$#"]>>
]
; when mpv is available send it commands directly
? <mpv ?mpv> [
<action-handler "(.*mp4)|(.*mp3)" $mpv <play-file "$1">>
]

1
lock.json Normal file
View File

@ -0,0 +1 @@
{"depends":[{"method":"fetchzip","packages":["hashlib"],"path":"/nix/store/v03nzlpdgbfxd2zhcnkfbkq01d5kqxcl-source","rev":"84e0247555e4488594975900401baaf5bbbfb53","sha256":"1pfczsv8kl36qpv543f93d2y2vgz2acckssfap7l51s2x62m6qwx","srcDir":"","url":"https://github.com/khchen/hashlib/archive/84e0247555e4488594975900401baaf5bbbfb53.tar.gz"},{"method":"fetchzip","packages":["nimcrypto"],"path":"/nix/store/zyr8zwh7vaiycn1s4r8cxwc71f2k5l0h-source","ref":"traditional-api","rev":"602c5d20c69c76137201b5d41f788f72afb95aa8","sha256":"1dmdmgb6b9m5f8dyxk781nnd61dsk3hdxqks7idk9ncnpj9fng65","srcDir":"","url":"https://github.com/cheatfate/nimcrypto/archive/602c5d20c69c76137201b5d41f788f72afb95aa8.tar.gz"},{"method":"fetchzip","packages":["npeg"],"path":"/nix/store/ffkxmjmigfs7zhhiiqm0iw2c34smyciy-source","ref":"1.2.1","rev":"26d62fdc40feb84c6533956dc11d5ee9ea9b6c09","sha256":"0xpzifjkfp49w76qmaylan8q181bs45anmp46l4bwr3lkrr7bpwh","srcDir":"src","url":"https://github.com/zevv/npeg/archive/26d62fdc40feb84c6533956dc11d5ee9ea9b6c09.tar.gz"},{"method":"fetchzip","packages":["preserves"],"path":"/nix/store/nrcpzf9hx70kry3gwhrdzcs3qicjncjh-source","ref":"20231021","rev":"edece399be70818208bf2263c30cb2bcf435bbff","sha256":"0xmw35wmw3a4lja9q4qvlvpxv3xk0hnkjg4fwfw6f3inh6zfiqki","srcDir":"src","url":"https://git.syndicate-lang.org/ehmry/preserves-nim/archive/edece399be70818208bf2263c30cb2bcf435bbff.tar.gz"},{"method":"fetchzip","packages":["syndicate"],"path":"/nix/store/1y3nnpp2mhxqmdb3xh4c4k5k5l9hhqk3-source","ref":"20231019","rev":"57b99b20e7db1b97b1cb9c6df574bd13983c26fc","sha256":"1kgb3a78igs37xkmv8cbaxa17qdjf2h43vdmpda517c9086ggsn5","srcDir":"src","url":"https://git.syndicate-lang.org/ehmry/syndicate-nim/archive/57b99b20e7db1b97b1cb9c6df574bd13983c26fc.tar.gz"}]}

View File

@ -1,7 +1,16 @@
version 1 .
XdgOpen = <xdg-open @uris [string ...]> .
Authority = [ @username any @password any @host any @port any ] .
UriRunnerConfig = ListenOn / ActionHandler .
ListenOn = <listen-on @dataspace #!any> .
ActionHandler = <action-handler @pat string @cmd [string ...]> .
; Uniform Resource Identifier https://datatracker.ietf.org/doc/html/rfc3986
Uri = <uri @scheme string @auth Authority @path any @query any @fragment any> .
; Broadcast this to open a URI.
Open = <open @uri Uri>.
UriRunnerConfig = {
handlerspace: #!any
urispace: #!any
} .
ActionHandler = <action-handler @pat string @entity #!any @action any> .

7
shell.nix Normal file
View File

@ -0,0 +1,7 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.nim2Packages.buildNimPackage {
name = "dummy";
nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [ pkgs.pcre ];
}

View File

@ -1,2 +1,6 @@
include_rules
: foreach ../*.prs |> !preserves_schema_nim |> %B.nim
: foreach ../*.prs |> !preserves_schema_nim |> | {common}
: common.nim |> !nim_check |> {common}
: uri_runner.nim | $(SYNDICATE_PROTOCOL) {common} |> !nim_bin |> {bin}
: open.nim | $(SYNDICATE_PROTOCOL) {common} |> !nim_bin |> {bin}
: foreach {bin} |> !assert_built |>

48
src/common.nim Normal file
View File

@ -0,0 +1,48 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/uri
import preserves
proc toPreserveOrNull(s: string; E: typedesc): Preserve[E] =
if s != "": result = toPreserve(s, E)
proc toPreserveHook*(u: Uri; E: typedesc): Preserve[E] =
## Best-effort preservation of the `Uri` type.
runnableExamples:
from std/unittest import check
import std/uri
import preserves
var u = parseUri"https://bidstonobservatory.org"
check $(u.toPreserve) == """<uri "https" [#f #f "bidstonobservatory.org" #f] #f #f #f>"""
initRecord("uri",
u.scheme.toPreserve(E),
toPreserve([
u.username.toPreserveOrNull(E),
u.password.toPreserveOrNull(E),
u.hostname.toPreserveOrNull(E),
u.port.toPreserveOrNull(E),
]),
u.path.toPreserveOrNull(E),
u.query.toPreserveOrNull(E),
u.anchor.toPreserveOrNull(E),
)
proc fromPreserveHook*[E](u: var Uri; pr: Preserve[E]): bool =
## Best-effort preservation of the `Uri` type.
runnableExamples:
import std/uri
import preserves
var u: Uri
doAssert fromPreserveHook(u, parsePreserves"""<uri "https" [#f #f "bidstonobservatory.org" #f] #f #f #f>""")
if pr.isRecord("uri", 5):
result = fromPreserve(u.scheme, pr[0])
if result:
if pr[1].isSequence and pr[1].sequence.len == 4:
discard fromPreserve(u.username, pr[1][0])
discard fromPreserve(u.password, pr[1][1])
discard fromPreserve(u.hostname, pr[1][2])
discard fromPreserve(u.port, pr[1][3])
discard fromPreserve(u.path, pr[2])
discard fromPreserve(u.query, pr[3])
discard fromPreserve(u.anchor, pr[4])

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))

25
src/open.nim Normal file
View File

@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[asyncdispatch, os, uri]
import preserves, syndicate, syndicate/relays, syndicate/capabilities
import ./protocol, ./common
proc publishUri(turn: var Turn; ds: Cap) =
for arg in commandLineParams():
if fileExists(arg):
message(turn, ds, Open(uri: protocol.Uri(
scheme: "file", path: arg.absolutePath.toPreserve)))
else:
message(turn, ds, initRecord("open", arg.parseUri.toPreserve))
stop(turn)
proc main =
let route = envRoute()
discard bootDataspace("open") do (root: Cap; turn: var Turn):
resolve(turn, root, route, publishUri)
for i in 0..4: poll(20)
# A hack to exit
main()

View File

@ -1,37 +1,36 @@
import
std/typetraits, preserves
preserves
type
XdgOpen* {.preservesRecord: "xdg-open".} = object
`uris`*: seq[string]
Open* {.preservesRecord: "open".} = object
`uri`*: Uri
UriRunnerConfigKind* {.pure.} = enum
`ListenOn`, `ActionHandler`
`UriRunnerConfig`*[E] {.preservesOr.} = ref object
case orKind*: UriRunnerConfigKind
of UriRunnerConfigKind.`ListenOn`:
`listenon`*: ListenOn[E]
UriRunnerConfig* {.preservesDictionary.} = object
`handlerspace`* {.preservesEmbedded.}: Preserve[void]
`urispace`* {.preservesEmbedded.}: Preserve[void]
of UriRunnerConfigKind.`ActionHandler`:
`actionhandler`*: ActionHandler
Authority* {.preservesTuple.} = object
`username`*: Preserve[void]
`password`*: Preserve[void]
`host`*: Preserve[void]
`port`*: Preserve[void]
ListenOn*[E] {.preservesRecord: "listen-on".} = ref object
`dataspace`*: Preserve[E]
Uri* {.preservesRecord: "uri".} = object
`scheme`*: string
`auth`*: Authority
`path`*: Preserve[void]
`query`*: Preserve[void]
`fragment`*: Preserve[void]
ActionHandler* {.preservesRecord: "action-handler".} = object
`pat`*: string
`cmd`*: seq[string]
`entity`* {.preservesEmbedded.}: Preserve[void]
`action`*: Preserve[void]
proc `$`*[E](x: UriRunnerConfig[E] | ListenOn[E]): string =
`$`(toPreserve(x, E))
proc encode*[E](x: UriRunnerConfig[E] | ListenOn[E]): seq[byte] =
encode(toPreserve(x, E))
proc `$`*(x: XdgOpen | ActionHandler): string =
proc `$`*(x: Open | UriRunnerConfig | Authority | Uri | ActionHandler): string =
`$`(toPreserve(x))
proc encode*(x: XdgOpen | ActionHandler): seq[byte] =
proc encode*(x: Open | UriRunnerConfig | Authority | Uri | ActionHandler): seq[
byte] =
encode(toPreserve(x))

View File

@ -1,54 +1,49 @@
# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[asyncdispatch, re]
import preserves, syndicate
import ./protocol, ./exec
import std/[re, tables, uri]
import preserves, syndicate, syndicate/relays, syndicate/patterns
import ./protocol, ./common
bootDataspace("main") do (root: Ref; turn: var Turn):
var actions: seq[tuple[regex: Regex; cmd: string; args: seq[Assertion]]]
# importing std/re doesn't actually link against PCRE
{.passC: staticExec("pkg-config --cflags libpcre").}
{.passL: staticExec("pkg-config --libs libpcre").}
connectStdio(root, turn)
type RegexAction = tuple[regex: Regex, entity: Cap, action: Assertion]
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]
proc rewrite(result: var Assertion; uri: string; regex: Regex) {.gcsafe.} =
proc op(pr: var Assertion) {.gcsafe.} =
if pr.isString:
pr.string = replacef(uri, regex, pr.string)
apply(result, op)
during(turn, root, ?ListenOn[Ref]) do (a: Assertion):
let ds = unembed a
onMessage(turn, ds, ?XdgOpen) do (uris: seq[string]):
for uri in uris:
var matched: bool
for act in actions:
if match(uri, act.regex):
runActor("main") do (root: Cap; turn: var Turn):
connectStdio(turn, root)
let handlers = newTable[Handle, RegexAction]()
during(turn, root, ?UriRunnerConfig) do (handlerspace: Cap, urispace: Cap):
during(turn, handlerspace, dropType(ActionHandler)) do:
during(turn, handlerspace, ?ActionHandler) do (pat: string; entity: Cap; act: Assertion):
# `duringHandle` is a symbol exposed by the `during` macro
handlers[duringHandle] = (re(pat, {reIgnoreCase, reStudy}), entity, act,)
do:
del(handlers, duringHandle)
onMessage(turn, urispace, Open ? {0:grab()}) do (u: uri.Uri):
let uri = $u
assert len(handlers) > 0
var matched = false
for handler in handlers.values:
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 action = handler.action
try:
rewrite(action, uri, handler.regex)
message(turn, handler.entity, action)
except CatchableError:
stderr.writeLine "rewrite failed on ", action
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
runForever()
do:
clear(handlers)

View File

@ -1,28 +0,0 @@
# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[asyncdispatch, os]
from std/sequtils import map
import syndicate, syndicate/capabilities
import ./protocol
proc unixSocketPath: string =
result = getEnv("SYNDICATE_SOCK")
if result == "":
result = getEnv("XDG_RUNTIME_DIR", "/run/user/1000") / "dataspace"
proc mintCap: SturdyRef =
var key: array[16, byte]
mint(key, "syndicate")
bootDataspace("main") do (root: Ref; turn: var Turn):
connectUnix(turn, unixSocketPath(), mintCap()) do (turn: var Turn; ds: Ref):
var uris = commandLineParams().map do (param: string) -> string:
if fileExists param:
"file://" & absolutePath(param)
else:
param
message(turn, ds, XdgOpen(uris: uris))
for i in 0..7: poll(20)
# A hack to exit

13
syndicated_open.nimble Normal file
View File

@ -0,0 +1,13 @@
# Package
version = "20231021"
author = "Emery"
description = "Syndicated open command"
license = "Unlicense"
srcDir = "src"
bin = @[ "open", "uri_runner"]
# Dependencies
requires "nim >= 1.6.4", "syndicate >= 20231021"

View File

@ -1,50 +1,28 @@
; Expose a dataspace over a unix socket
let ?root_ds = dataspace
<require-service <relay-listener <unix "/run/user/1000/dataspace"> $gatekeeper>>
<bind "syndicate" #x"" $root_ds>
; configure the daemon when it is built
; (this is an artifact of the author's build system)
? <built uri_runner ?path ?hash> [
<daemon uri_runner {
argv: [$path]
protocol: application/syndicate
env: { BUILD_SUM: $hash }
}>
]
<require-service <daemon uri_runner>>
; grab a dataspace for observing <open …> messages
? <socketspace ?socketspace> [
<daemon uri_runner {
argv: "uri_runner"
protocol: text/syndicate
}>
? <service-object <daemon uri_runner> ?cap> [
; send configuration to uri_runner
$cap [
<listen-on $root_ds>
; 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]>
; An argument can be a reference to a capture.
<action-handler "(tox:.*)|uri:(tox:.*)", ["/run/current-system/sw/bin/qtox" 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"]>
<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]>
; filesystem paths are always prefixed with file://
<action-handler "file://(.*.pdf)" ["/run/current-system/sw/bin/mupdf" 1]>
; log open messages
$socketspace ?? <open ?stuff> [
$log ! <log "-" { line: <open $stuff> }>
]
; 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]
; configure the uri_runner
? <service-object <daemon uri_runner> ?cap> [
$cap {
; watch the config dataspace for handler configuration
handlerspace: $config
urispace: $socketspace
}
]
]

View File

@ -1,13 +0,0 @@
# Package
version = "0.4.0"
author = "Emery"
description = "A better xdg-open"
license = "Unlicense"
srcDir = "src"
bin = @[ "uri_runner", "xdg_open"]
# Dependencies
requires "nim >= 1.6.4", "syndicate >= 1.3.0"