diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-16 15:23:35 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-16 15:23:35 +0200 |
| commit | 13a10999f53802858f87883db381842d741eab69 (patch) | |
| tree | 7354e5d8c3b34a84ddb50a3906c47638d189550c | |
| parent | 6dcf62fbdf2446cf163923d5de0676a6b3ad3019 (diff) | |
| download | quickshell-13a10999f53802858f87883db381842d741eab69.tar.gz quickshell-13a10999f53802858f87883db381842d741eab69.zip | |
feat(assistant): file kinds and dropped URLs
Files.js decides what the card offers for a staged file; the daemon decides what chat accepts, so the text and image lists match its own. Dropped URLs become paths only for file:// ones.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
| -rw-r--r-- | desktop/assistant/Files.js | 55 | ||||
| -rw-r--r-- | desktop/assistant/test_files.js | 40 |
2 files changed, 95 insertions, 0 deletions
diff --git a/desktop/assistant/Files.js b/desktop/assistant/Files.js new file mode 100644 index 0000000..b7f8c2b --- /dev/null +++ b/desktop/assistant/Files.js @@ -0,0 +1,55 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +.pragma library + +// What a staged file is, for its chip and the actions offered on it. The +// text and image lists match desktop-assistant's, which decides what chat +// accepts; this only decides what the card offers. +// ponytail: the daemon also takes any text/* mime type; a file of such a +// type with a suffix not listed here reads as "other" and is not offered +// chat. Add the suffix when one shows up. +const TEXT = [".py", ".c", ".h", ".cpp", ".hpp", ".rs", ".go", ".js", ".ts", ".jsx", + ".tsx", ".sh", ".bash", ".zsh", ".lua", ".rb", ".pl", ".php", ".java", + ".kt", ".swift", ".sql", ".toml", ".yaml", ".yml", ".json", ".ini", + ".cfg", ".conf", ".md", ".rst", ".txt", ".csv", ".xml", ".html", ".css", + ".diff", ".patch", ".slackbuild", ".info", ".desktop", ".service", ".qml"]; +const IMAGE = [".png", ".jpg", ".jpeg", ".gif", ".webp"]; +const VIDEO = [".mp4", ".mkv", ".webm", ".mov", ".avi", ".m4v"]; +const ARCHIVE = [".zip", ".tar", ".gz", ".tgz", ".xz", ".txz", ".bz2", ".zst", ".7z", ".rar"]; + +// Kinds chat takes; the rest only get local actions. +const CHAT = ["text", "image", "pdf"]; + +function name(path) { return path.slice(path.lastIndexOf("/") + 1); } + +function kind(path) { + const n = name(path).toLowerCase(); + const dot = n.lastIndexOf("."); + const ext = dot > 0 ? n.slice(dot) : ""; + if (ext === ".pdf") return "pdf"; + if (IMAGE.includes(ext)) return "image"; + if (VIDEO.includes(ext)) return "video"; + if (ARCHIVE.includes(ext)) return "archive"; + if (TEXT.includes(ext)) return "text"; + return "other"; +} + +function chats(k) { return CHAT.includes(k); } + +// A dropped URL to a local path, or "" for anything that is not a file. +function fromUrl(url) { + const s = String(url); + if (!s.startsWith("file://")) return ""; + try { return decodeURIComponent(s.slice(7)); } catch (e) { return ""; } +} + +function file(path) { return { path: path, name: name(path), kind: kind(path) }; } diff --git a/desktop/assistant/test_files.js b/desktop/assistant/test_files.js new file mode 100644 index 0000000..a15495b --- /dev/null +++ b/desktop/assistant/test_files.js @@ -0,0 +1,40 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// Run: node desktop/assistant/test_files.js +// Files.js is a QML library, so its .pragma line is stripped before node +// evaluates it. + +const fs = require("fs"); +const assert = require("assert"); + +const src = fs.readFileSync(__dirname + "/Files.js", "utf8") + .replace(/^\.pragma library$/m, ""); +const F = new Function(src + "\nreturn { kind, chats, fromUrl, file, name };")(); + +const cases = { + "/a/report.pdf": "pdf", "/a/Report.PDF": "pdf", "/a/x.png": "image", "/a/x.JPEG": "image", + "/a/clip.mp4": "video", "/a/c.MKV": "video", "/a/src.tar.gz": "archive", "/a/b.7z": "archive", + "/a/main.py": "text", "/a/notes.md": "text", "/a/pkg.SlackBuild": "text", + "/a/Makefile": "other", "/a/.bashrc": "other", "/a/x.bin": "other", "/a/dir.d/noext": "other", +}; +for (const [p, k] of Object.entries(cases)) assert.strictEqual(F.kind(p), k, p); + +assert.ok(F.chats("pdf") && F.chats("image") && F.chats("text")); +assert.ok(!F.chats("video") && !F.chats("archive") && !F.chats("other")); + +assert.strictEqual(F.fromUrl("file:///home/you/My%20Docs/caf%C3%A9.pdf"), "/home/you/My Docs/café.pdf"); +assert.strictEqual(F.fromUrl("file:///tmp/a.txt"), "/tmp/a.txt"); +assert.strictEqual(F.fromUrl("https://example.org/a.pdf"), ""); +assert.strictEqual(F.fromUrl("file:///bad%E0%A4%A"), ""); // malformed escape +assert.deepStrictEqual(F.file("/x/y/a b.pdf"), { path: "/x/y/a b.pdf", name: "a b.pdf", kind: "pdf" }); + +console.log("ok"); |
