aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/assistant
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/assistant')
-rw-r--r--desktop/assistant/Assistant.qml32
-rw-r--r--desktop/assistant/Files.js24
-rw-r--r--desktop/assistant/README.md6
-rw-r--r--desktop/assistant/test_files.js12
4 files changed, 67 insertions, 7 deletions
diff --git a/desktop/assistant/Assistant.qml b/desktop/assistant/Assistant.qml
index 977b4dd..0139ef9 100644
--- a/desktop/assistant/Assistant.qml
+++ b/desktop/assistant/Assistant.qml
@@ -105,10 +105,40 @@ Scope {
root.staged = files;
root.historyOpen = root.pickerOpen = false;
root.open = true;
+ root.refine();
}
function stage(path) {
- if (!root.staged.some(f => f.path === path)) root.staged = root.staged.concat([Files.file(path)]);
+ if (root.staged.some(f => f.path === path)) return;
+ root.staged = root.staged.concat([Files.file(path)]);
+ root.refine();
+ }
+
+ // A file whose name says nothing (a script with no suffix) is "other"
+ // until file(1) has read it. One run for all of them; the daemon makes
+ // its own call on the content when the question is sent.
+ function refine() {
+ const unknown = root.staged.filter(f => f.kind === "other").map(f => f.path);
+ if (!unknown.length) return;
+ sniff.paths = unknown;
+ sniff.running = false; // a running Process ignores running = true
+ sniff.running = true;
+ }
+
+ Process {
+ id: sniff
+ property var paths: []
+ command: ["file", "-b", "--mime-type", "--"].concat(paths)
+ stdout: StdioCollector {
+ onStreamFinished: {
+ const mimes = text.trim().split("\n");
+ root.staged = root.staged.map(f => {
+ const i = sniff.paths.indexOf(f.path);
+ return i < 0 || f.kind !== "other" ? f
+ : Object.assign({}, f, { kind: Files.fromMime(mimes[i] ?? "") });
+ });
+ }
+ }
}
function unstage(i) {
const s = root.staged.slice();
diff --git a/desktop/assistant/Files.js b/desktop/assistant/Files.js
index 825aed1..a75e693 100644
--- a/desktop/assistant/Files.js
+++ b/desktop/assistant/Files.js
@@ -14,9 +14,8 @@
// 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.
+// A suffix not listed here reads as "other" at first; Assistant.qml then
+// asks file(1) and fromMime() gives the kind.
// Text splits into prose and data (text) and code or configuration (code),
// so each gets the presets that fit it; the daemon sees one text kind.
const TEXT = [".md", ".rst", ".txt", ".csv"];
@@ -49,6 +48,25 @@ function kind(path) {
function chats(k) { return CHAT.includes(k); }
+// For a file the name says nothing about (a script with no suffix), from
+// the mime type `file -b --mime-type` reports. Prose and data are text;
+// any other text type, scripts and source among them, is code.
+const ARCHIVE_MIMES = ["application/zip", "application/x-tar", "application/gzip",
+ "application/x-xz", "application/x-bzip2", "application/zstd",
+ "application/x-7z-compressed", "application/x-rar",
+ "application/vnd.rar"];
+
+function fromMime(m) {
+ if (["text/plain", "text/markdown", "text/csv"].includes(m)) return "text";
+ if (m.startsWith("text/") || ["application/json", "application/xml",
+ "application/javascript"].includes(m)) return "code";
+ if (["image/png", "image/jpeg", "image/gif", "image/webp"].includes(m)) return "image";
+ if (m === "application/pdf") return "pdf";
+ if (m.startsWith("video/")) return "video";
+ if (ARCHIVE_MIMES.includes(m)) return "archive";
+ return "other";
+}
+
// A dropped URL to a local path, or "" for anything that is not a local
// file: file:///path, or file://localhost/path; any other host is remote.
function fromUrl(url) {
diff --git a/desktop/assistant/README.md b/desktop/assistant/README.md
index 4715b12..8c74343 100644
--- a/desktop/assistant/README.md
+++ b/desktop/assistant/README.md
@@ -61,8 +61,10 @@ Notes:
markers literally.
- **File kinds are decided twice.** `Files.js` decides what the card
- offers; the daemon decides what chat accepts. The lists match, but the
- daemon also takes any `text/*` file, which the card may show as "other".
+ offers; the daemon decides what chat accepts. Both go by the name first
+ and by the content when the name says nothing: the card runs
+ `file -b --mime-type` on staged "other" files, the daemon reads the first
+ 4 KB. A script with no suffix is therefore code here and text there.
- **`socketPath`** is a property so a check can point the canvas at a
scratch daemon; the desktop uses the default.
diff --git a/desktop/assistant/test_files.js b/desktop/assistant/test_files.js
index 80a855d..f2a65ca 100644
--- a/desktop/assistant/test_files.js
+++ b/desktop/assistant/test_files.js
@@ -18,7 +18,7 @@ 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 F = new Function(src + "\nreturn { kind, chats, fromUrl, file, name, fromMime };")();
const cases = {
"/a/report.pdf": "pdf", "/a/Report.PDF": "pdf", "/a/x.png": "image", "/a/x.JPEG": "image",
@@ -40,4 +40,14 @@ assert.strictEqual(F.fromUrl("file://otherhost/tmp/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" });
+// A file the name says nothing about is told by its mime type (file -b).
+const mimes = {
+ "text/x-shellscript": "code", "text/x-script.python": "code", "text/x-c": "code",
+ "text/x-makefile": "code", "application/json": "code", "text/plain": "text",
+ "text/csv": "text", "text/markdown": "text", "image/png": "image", "application/pdf": "pdf",
+ "video/mp4": "video", "application/zip": "archive", "application/x-xz": "archive",
+ "application/octet-stream": "other", "application/x-executable": "other", "": "other",
+};
+for (const [m, k] of Object.entries(mimes)) assert.strictEqual(F.fromMime(m), k, m);
+
console.log("ok");