7 changed files with 108 additions and 17 deletions

No files matched your search

+3 -3
View File
@@ -10,9 +10,9 @@ Repository material and both Tentacles are licensed under GPL-3.0-or-later; see
Tentacles, each in a direct top-level folder with its own `tentacle.toml`. Tentacles, each in a direct top-level folder with its own `tentacle.toml`.
The catalog lists native packages for Linux amd64 and arm64. Hello World 0.1.0 is The catalog lists native packages for Linux amd64 and arm64. Hello World 0.1.0 is
available from the [release page](https://git.stormux.org/storm/cthulhu-tentacles/releases/tag/hello-world-v0.1.0). available from the [release page](https://git.stormux.org/storm/cthulhu-tentacles/releases/tag/hello-world-v0.1.0).
Wormhole 0.1.1 requires the matching reader with interactive Tentacle support. Wormhole 0.1.2 requires the matching reader with interactive Tentacle settings support.
Its release archives are prepared for upload to the Wormhole release page; Native packages are available from the
installation becomes available once both archives have been uploaded. [Wormhole release page](https://git.stormux.org/storm/cthulhu-tentacles/releases/tag/wormhole-v0.1.2).
Local build artifacts live in the ignored `dist/` directory. Local build artifacts live in the ignored `dist/` directory.
The discovery source URL is The discovery source URL is
`https://git.stormux.org/storm/cthulhu-tentacles`. `https://git.stormux.org/storm/cthulhu-tentacles`.
+2 -2
View File
@@ -24,10 +24,10 @@ manifest = "wormhole/tentacle.toml"
platform = "linux" platform = "linux"
architecture = "amd64" architecture = "amd64"
url = "https://git.stormux.org/storm/cthulhu-tentacles/releases/download/wormhole-v0.1.1/wormhole-0.1.1-linux-amd64.tar.gz" url = "https://git.stormux.org/storm/cthulhu-tentacles/releases/download/wormhole-v0.1.1/wormhole-0.1.1-linux-amd64.tar.gz"
sha256 = "fbad678824f7ea003231197e6d131ab9588d9b896297d4da152b191efe2f77f3" sha256 = "4e1e58885c20bfcb6e274db392e6a765476bce7bde08494f71e38065359d2968"
[[tentacles.releases]] [[tentacles.releases]]
platform = "linux" platform = "linux"
architecture = "arm64" architecture = "arm64"
url = "https://git.stormux.org/storm/cthulhu-tentacles/releases/download/wormhole-v0.1.1/wormhole-0.1.1-linux-arm64.tar.gz" url = "https://git.stormux.org/storm/cthulhu-tentacles/releases/download/wormhole-v0.1.1/wormhole-0.1.1-linux-arm64.tar.gz"
sha256 = "d6783f8f003b59ea7d81f5a9ff741f53155c4eaea7e97f8b56c86529cde5d8bd" sha256 = "035d18a049c93b0fabeca8424d12b3f788e15f3fd0cd119766fc598900798326"
+16 -4
View File
@@ -36,7 +36,7 @@ def check_metadata(repoRoot):
require(item["manifest"] == f"{name}/tentacle.toml", "Manifest path must match ID") require(item["manifest"] == f"{name}/tentacle.toml", "Manifest path must match ID")
manifest = read_toml(repoRoot / item["manifest"]) manifest = read_toml(repoRoot / item["manifest"])
require(manifest["format-version"] == 1 and manifest["id"] == name require(manifest["format-version"] == 1 and manifest["id"] == name
and manifest["name"] and manifest["version"] == ("0.1.1" if name == "wormhole" else "0.1.0"), "Invalid Tentacle metadata") and manifest["name"] and manifest["version"] == ("0.1.2" if name == "wormhole" else "0.1.0"), "Invalid Tentacle metadata")
expectedGrants = ["presentation"] + (["interaction"] if name == "wormhole" else []) expectedGrants = ["presentation"] + (["interaction"] if name == "wormhole" else [])
expectedProgram = "wormhole-tentacle" if name == "wormhole" else name expectedProgram = "wormhole-tentacle" if name == "wormhole" else name
require(manifest["entry-point"] == [f"bin/{expectedProgram}"] require(manifest["entry-point"] == [f"bin/{expectedProgram}"]
@@ -81,11 +81,14 @@ def check_registration(programPath, name):
require(registration["id"] == "1" and registration["result"]["session"] == "check", require(registration["id"] == "1" and registration["result"]["session"] == "check",
"Invalid registration response") "Invalid registration response")
commands = registration["result"]["commands"] commands = registration["result"]["commands"]
require(len(commands) == (7 if name == "wormhole" else 1) and commands[0]["id"] == name require(len(commands) == (9 if name == "wormhole" else 1) and commands[0]["id"] == name
and commands[0]["presentation"] is True, "Unexpected commands or grant") and commands[0]["presentation"] is True, "Unexpected commands or grant")
require(commands[0]["bindings"] == [ require(commands[0]["bindings"] == [
{"layout": layout, "scope": "global", "key": ("Cthulhu+Alt+Shift+W" if name == "wormhole" else "Cthulhu+Alt+Shift+H")} {"layout": layout, "scope": "global", "key": ("Cthulhu+Ctrl+Shift+W" if name == "wormhole" else "Cthulhu+Alt+Shift+H")}
for layout in ("desktop", "laptop")], "Wrong suggested shortcut") for layout in ("desktop", "laptop")], "Wrong suggested shortcut")
if name == "wormhole":
require([command["id"] for command in commands if command.get("settings")]
== ["settings"], "Wrong registered settings entry point")
process.stdin.close() process.stdin.close()
require(process.wait(timeout=5) == 0, "Tentacle did not stop cleanly on EOF") require(process.wait(timeout=5) == 0, "Tentacle did not stop cleanly on EOF")
finally: finally:
@@ -109,6 +112,15 @@ def check_host(repoRoot, programPath, name):
or value == {"speech": "Magic wormhole is not installed", or value == {"speech": "Magic wormhole is not installed",
"braille": "Magic wormhole is not installed", "uninterruptible": True}, "braille": "Magic wormhole is not installed", "uninterruptible": True},
"Host returned an unexpected Wormhole menu") "Host returned an unexpected Wormhole menu")
result = subprocess.run([
"go", "run", "./cmd/tentacle-host", "-presentation", "-interaction",
"-command", "settings", str(programPath),
], cwd=readerRoot, env=environment, capture_output=True, text=True,
timeout=60, check=True)
value = json.loads(result.stdout)
require(value.get("form", {}).get("title") == "Wormhole preferences"
and value.get("next") == "save-settings",
"Host returned an unexpected Wormhole settings form")
return return
require(json.loads(result.stdout) == { require(json.loads(result.stdout) == {
"speech": "Hello world!", "braille": "Hello world!", "uninterruptible": True, "speech": "Hello world!", "braille": "Hello world!", "uninterruptible": True,
@@ -123,7 +135,7 @@ def main():
programPath = repoRoot / name / "bin" / binary programPath = repoRoot / name / "bin" / binary
check_registration(programPath, name) check_registration(programPath, name)
check_host(repoRoot, programPath, name) check_host(repoRoot, programPath, name)
print("Repository metadata, shortcut registration and actual-host greeting/menu checks passed.") print("Repository metadata, shortcut/settings registration and actual-host greeting/menu/settings checks passed.")
if __name__ == "__main__": if __name__ == "__main__":
+12 -5
View File
@@ -1,9 +1,17 @@
# Wormhole # Wormhole
An experimental Magic Wormhole Tentacle for Cthulhu-Go. Suggested shortcut: An experimental Magic Wormhole Tentacle for Cthulhu-Go. Suggested shortcut:
**Cthulhu+Alt+Shift+W**, on desktop and laptop layouts. It opens the reader's **Cthulhu+Ctrl+Shift+W**, on desktop and laptop layouts. It opens the reader's
internal **Wormhole menu**, preserving the underlying application's focus. internal **Wormhole menu**, preserving the underlying application's focus.
With a reader supporting Tentacle settings registration, global Preferences →
Tentacles → select Wormhole → Settings opens **Wormhole preferences** directly.
Save persists the two checkboxes and returns to reader preferences; Escape
discards the form's edits. This entry point works without the Wormhole CLI.
Use Wormhole 0.1.2 and rebuild the reader to obtain settings registration and the
Ctrl+Shift shortcut; the published 0.1.1 binary uses Alt+Shift and has no
registered settings entry point.
Install the `magic-wormhole` CLI using your distribution's package manager. Install the `magic-wormhole` CLI using your distribution's package manager.
Clipboard operations require `xclip` on Xorg. Missing `wormhole` does not stop Clipboard operations require `xclip` on Xorg. Missing `wormhole` does not stop
reader startup: the shortcut speaks and brailles **Magic wormhole is not reader startup: the shortcut speaks and brailles **Magic wormhole is not
@@ -102,9 +110,8 @@ interaction = true
The normal installation path is global Preferences → Tentacles → Add repository The normal installation path is global Preferences → Tentacles → Add repository
with `https://git.stormux.org/storm/cthulhu-tentacles`, followed by selecting with `https://git.stormux.org/storm/cthulhu-tentacles`, followed by selecting
Wormhole and Install and enable. Version 0.1.1 downloads use the normal Wormhole and Install and enable. Version 0.1.2 downloads use the normal
`wormhole-v0.1.1` release attachments. Installation becomes available after the [`wormhole-v0.1.2` release attachments](https://git.stormux.org/storm/cthulhu-tentacles/releases/tag/wormhole-v0.1.2). The manifest
prepared archives are uploaded. The manifest
requests presentation and interaction permissions. Local metadata above is an requests presentation and interaction permissions. Local metadata above is an
optional development path. optional development path.
Prepare amd64/arm64 archives with Prepare amd64/arm64 archives with
@@ -117,7 +124,7 @@ After configuring the local entry:
1. Quit Cthulhu with Cthulhu+Q. Press Ctrl+Escape, then Shift+G. These are 1. Quit Cthulhu with Cthulhu+Q. Press Ctrl+Escape, then Shift+G. These are
separate key presses. Shift+G rebuilds the reader and braille helper. separate key presses. Shift+G rebuilds the reader and braille helper.
2. Focus an ordinary multiline text field. Open Cthulhu+Alt+Shift+W. Expect 2. Focus an ordinary multiline text field. Open Cthulhu+Ctrl+Shift+W. Expect
“Wormhole menu. Send focused item” in speech and the menu option on braille. “Wormhole menu. Send focused item” in speech and the menu option on braille.
Native application focus must stay on the original field. Send it to a Native application focus must stay on the original field. Send it to a
second Wormhole endpoint; the recipient must get exactly the current line. second Wormhole endpoint; the recipient must get exactly the current line.
+22 -1
View File
@@ -90,6 +90,25 @@ func (w *wormhole) preferencesForm() tentacle.Interaction {
{ID: "save", Label: "Save", Kind: tentacle.Button}, {ID: "save", Label: "Save", Kind: tentacle.Button},
}}, Next: "preferences"} }}, Next: "preferences"}
} }
func (w *wormhole) openPreferences(ctx context.Context, _ json.RawMessage) (any, error) {
w.mutex.Lock()
defer w.mutex.Unlock()
if err := ctx.Err(); err != nil {
return nil, err
}
result := w.preferencesForm()
result.Next = "save-settings"
return result, nil
}
func (w *wormhole) saveSettings(ctx context.Context, raw json.RawMessage) (any, error) {
value, err := w.savePreferences(ctx, raw)
if err != nil {
return nil, err
}
result := value.(tentacle.Interaction)
result.Form, result.Next, result.Done = nil, "", true
return result, nil
}
func (w *wormhole) savePreferences(ctx context.Context, raw json.RawMessage) (any, error) { func (w *wormhole) savePreferences(ctx context.Context, raw json.RawMessage) (any, error) {
value, err := submission(raw) value, err := submission(raw)
if err != nil { if err != nil {
@@ -146,11 +165,13 @@ func (w *wormhole) commands() []tentacle.Command {
{"wormhole", "Wormhole menu", w.open}, {"menu", "Wormhole menu selection", w.choose}, {"wormhole", "Wormhole menu", w.open}, {"menu", "Wormhole menu selection", w.choose},
{"send", "Send Wormhole item", w.send}, {"receive", "Receive Wormhole item", w.receive}, {"send", "Send Wormhole item", w.send}, {"receive", "Receive Wormhole item", w.receive},
{"preferences", "Save Wormhole preferences", w.savePreferences}, {"control", "Wormhole transfer controls", w.control}, {"poll", "Wormhole transfer status", w.poll}, {"preferences", "Save Wormhole preferences", w.savePreferences}, {"control", "Wormhole transfer controls", w.control}, {"poll", "Wormhole transfer status", w.poll},
{"settings", "Wormhole preferences", w.openPreferences}, {"save-settings", "Save Wormhole settings", w.saveSettings},
} { } {
command := tentacle.Command{ID: entry.id, Title: entry.title, Description: entry.title, Presentation: true, Interaction: true, Handle: entry.handler} command := tentacle.Command{ID: entry.id, Title: entry.title, Description: entry.title, Presentation: true, Interaction: true, Handle: entry.handler}
if entry.id == "wormhole" { if entry.id == "wormhole" {
command.Bindings = []tentacle.Binding{{Layout: "desktop", Scope: "global", Key: "Cthulhu+Alt+Shift+W"}, {Layout: "laptop", Scope: "global", Key: "Cthulhu+Alt+Shift+W"}} command.Bindings = []tentacle.Binding{{Layout: "desktop", Scope: "global", Key: "Cthulhu+Ctrl+Shift+W"}, {Layout: "laptop", Scope: "global", Key: "Cthulhu+Ctrl+Shift+W"}}
} }
command.Settings = entry.id == "settings"
commands = append(commands, command) commands = append(commands, command)
} }
return commands return commands
+51
View File
@@ -89,6 +89,57 @@ func TestMenuPreferencesAndMissingDependency(t *testing.T) {
t.Fatal(result, err) t.Fatal(result, err)
} }
} }
func TestRegisteredSettingsAndDefaultShortcut(t *testing.T) {
w := testWormhole(t, "exit 0\n")
w.program = filepath.Join(t.TempDir(), "missing")
settings := 0
for _, command := range w.commands() {
if command.ID == "wormhole" {
if len(command.Bindings) != 2 {
t.Fatal(command.Bindings)
}
for index, layout := range []string{"desktop", "laptop"} {
binding := command.Bindings[index]
if binding.Layout != layout || binding.Scope != "global" || binding.Key != "Cthulhu+Ctrl+Shift+W" {
t.Fatal(binding)
}
}
}
if !command.Settings {
continue
}
settings++
value, err := command.Handle(context.Background(), nil)
if err != nil {
t.Fatal(err)
}
form := value.(tentacle.Interaction)
if !command.Interaction || !command.Presentation || form.Form.Title != "Wormhole preferences" || form.Next != "save-settings" {
t.Fatal(command, form)
}
}
if settings != 1 {
t.Fatalf("settings entry points: %d", settings)
}
value, err := w.saveSettings(context.Background(), arguments(tentacle.Submission{Activated: "save", Values: map[string]tentacle.Value{"copy-code": {Checked: true}}}))
if err != nil {
t.Fatal(err)
}
result := value.(tentacle.Interaction)
if !result.Done || result.Form != nil || result.Next != "" || !w.preferences.CopySendCode {
t.Fatal(result)
}
data, err := os.ReadFile(w.settingsPath)
var saved preferences
if err != nil || json.Unmarshal(data, &saved) != nil || !saved.CopySendCode {
t.Fatal("settings did not persist", err)
}
_, err = w.saveSettings(context.Background(), arguments(tentacle.Submission{Cancelled: true}))
if err != nil || !w.preferences.CopySendCode {
t.Fatal("Cancel changed saved settings", err)
}
}
func TestSendPreservesClipboardTextAndCodePreference(t *testing.T) { func TestSendPreservesClipboardTextAndCodePreference(t *testing.T) {
for _, copyCode := range []bool{false, true} { for _, copyCode := range []bool{false, true} {
t.Run(map[bool]string{false: "read-only", true: "copy-code"}[copyCode], func(t *testing.T) { t.Run(map[bool]string{false: "read-only", true: "copy-code"}[copyCode], func(t *testing.T) {
+2 -2
View File
@@ -2,7 +2,7 @@ format-version = 1
id = "wormhole" id = "wormhole"
name = "Wormhole" name = "Wormhole"
description = "Send a focused line or clipboard item and receive files, folders, and text with Magic Wormhole." description = "Send a focused line or clipboard item and receive files, folders, and text with Magic Wormhole."
version = "0.1.1" version = "0.1.2"
authors = ["Storm Dragon"] authors = ["Storm Dragon"]
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
source = "https://git.stormux.org/storm/cthulhu-tentacles" source = "https://git.stormux.org/storm/cthulhu-tentacles"
@@ -18,6 +18,6 @@ major = 1
minor = 0 minor = 0
[requirements] [requirements]
reader = "Cthulhu-Go with interactive Tentacle support" reader = "Cthulhu-Go with interactive Tentacle settings support"
runtime = "Magic Wormhole CLI (wormhole); xclip for clipboard operations" runtime = "Magic Wormhole CLI (wormhole); xclip for clipboard operations"
build = "Go 1.24 or newer and the adjacent cthulhu-go source checkout" build = "Go 1.24 or newer and the adjacent cthulhu-go source checkout"