From c6fc5bbef45bcfa4f8d5e50ca7401aa1f5b6bc7d Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sat, 17 Oct 2026 11:08:49 +0200 Subject: settings: qtmaildir's config, in four Mail pages qtmaildir is configured through qtmaildir.conf alone, which it reads at startup and never writes. Four settings pages under a new Mail group edit it: Mail ([general], [sync], [completion]), Compose, Accounts (a card per account, add and remove) and Tags & shortcuts ([tagcolors] and [keys]). Fields save on Enter and unset when emptied, with qtmaildir's defaults as placeholders and numbers range-checked; the pages say when qtmaildir is running and a change waits for its restart. Ini.js edits the file line by line so its comments survive. Headers are only whole [name] lines: a section is never cut at the next "[", since values such as [Gmail]/Bozze hold brackets, the trap AGENTS.md records. The shortcut actions are copied from qtmaildir's KeyMap::knownActions() rather than exported by it, since a CLI flag there would have meant translations and a changelog entry; the field also takes free text, and the copy is marked as a ponytail ceiling. The settings sidebar now clips, which a list long enough to scroll showed it did not: entries drew over the search box. Co-Authored-By: Claude Opus 5.5 --- desktop/modules/qtmaildir/test_ini.js | 93 +++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 desktop/modules/qtmaildir/test_ini.js (limited to 'desktop/modules/qtmaildir/test_ini.js') diff --git a/desktop/modules/qtmaildir/test_ini.js b/desktop/modules/qtmaildir/test_ini.js new file mode 100644 index 0000000..2e7fb03 --- /dev/null +++ b/desktop/modules/qtmaildir/test_ini.js @@ -0,0 +1,93 @@ +// Copyright (C) 2026 Danilo M. +// +// 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/modules/qtmaildir/test_ini.js +// Ini.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 + "/Ini.js", "utf8") + .replace(/^\.pragma library$/m, ""); +const I = new Function(src + "\nreturn { sections, entries, get, set, remove, addSection, removeSection };")(); + +const file = [ + "[general]", + "; startup_query = Unread", + "toolbar_icon_size = 32", + "date_format = dd/MM/yyyy hh:mm", + "", + "[account.gmail]", + "maildir = gmail ; relative to notmuch's mail root", + "drafts = [Gmail]/Bozze", + 'send_command = "/usr/bin/msmtp -a x ; y"', + "label = GM", + "", + "[tagcolors]", + "shopping/amazon = #ff9900", + "", +].join("\n"); + +// A value holding brackets is not a section header (AGENTS.md: three of +// five accounts once lost their label to exactly that). +assert.deepStrictEqual(I.sections(file), ["general", "account.gmail", "tagcolors"]); +assert.strictEqual(I.get(file, "account.gmail", "drafts"), "[Gmail]/Bozze"); +assert.strictEqual(I.get(file, "account.gmail", "label"), "GM"); +// An inline comment is not part of the value; a quoted ; is. +assert.strictEqual(I.get(file, "account.gmail", "maildir"), "gmail"); +assert.strictEqual(I.get(file, "account.gmail", "send_command"), "/usr/bin/msmtp -a x ; y"); +// A commented key is unset. +assert.strictEqual(I.get(file, "general", "startup_query"), undefined); +assert.deepStrictEqual(I.entries(file, "tagcolors"), [{ key: "shopping/amazon", value: "#ff9900" }]); + +// Replacing keeps the inline comment and every other line. +let t = I.set(file, "account.gmail", "maildir", "gm2"); +assert.strictEqual(t.split("\n")[6], "maildir = gm2 ; relative to notmuch's mail root"); +assert.strictEqual(t.split("\n").length, file.split("\n").length); +// Setting a commented key activates its line. +t = I.set(file, "general", "startup_query", "Inbox"); +assert.strictEqual(t.split("\n")[1], "startup_query = Inbox"); +// Unsetting comments a key out in place. +t = I.set(file, "general", "toolbar_icon_size", null); +assert.strictEqual(t.split("\n")[2], "; toolbar_icon_size = 32"); +assert.strictEqual(I.get(t, "general", "toolbar_icon_size"), undefined); +// A new key lands at the end of its section, before the blank line. +t = I.set(file, "general", "language", "it"); +assert.strictEqual(t.split("\n")[4], "language = it"); +assert.strictEqual(t.split("\n")[5], ""); +assert.strictEqual(I.get(t, "general", "language"), "it"); +// A value with a ; or a quote is quoted, and reads back the same. +t = I.set(file, "account.gmail", "send_command", 'a "b" ; c'); +assert.strictEqual(I.get(t, "account.gmail", "send_command"), 'a "b" ; c'); +// A missing section is appended. +t = I.set(file, "compose", "send_html", "false"); +assert.ok(t.endsWith("\n[compose]\nsend_html = false\n"), JSON.stringify(t)); +assert.strictEqual(I.get(t, "compose", "send_html"), "false"); +// The same key in another section is untouched. +t = I.set("[a]\nx = 1\n\n[b]\nx = 2\n", "b", "x", "3"); +assert.strictEqual(t, "[a]\nx = 1\n\n[b]\nx = 3\n"); + +// remove deletes the line, where set(null) would comment it out. +t = I.remove(file, "tagcolors", "shopping/amazon"); +assert.deepStrictEqual(I.entries(t, "tagcolors"), []); +assert.ok(!t.includes("amazon")); +assert.strictEqual(I.remove(file, "tagcolors", "nope"), file); + +// Whole sections, for accounts. +t = I.addSection(file, "account.new", [["maildir", "new"], ["trash", "Trash"]]); +assert.deepStrictEqual(I.sections(t), ["general", "account.gmail", "tagcolors", "account.new"]); +assert.strictEqual(I.get(t, "account.new", "trash"), "Trash"); +t = I.removeSection(file, "account.gmail"); +assert.deepStrictEqual(I.sections(t), ["general", "tagcolors"]); +assert.strictEqual(t, "[general]\n; startup_query = Unread\ntoolbar_icon_size = 32\ndate_format = dd/MM/yyyy hh:mm\n\n[tagcolors]\nshopping/amazon = #ff9900\n"); + +console.log("test_ini: ok"); -- cgit v1.2.3