From c4dc18cbfc53a1538ec2ff14f35b07693c731b7b Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 2 Oct 2026 12:11:19 +0200 Subject: [PATCH] Add an org path setting with a directory picker to Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let the user change the server's scanned org directory from the GUI, backed by the server's /cfg file. The server expands a leading ~ and rescans, so the client only sends a path. OrkClient: - orgPath() reads /cfg and extracts the org_path value (basic TOML string, with escapes). - setOrgPath() writes org_path to /cfg with proper TOML escaping, and emits orgPathChanged() on success. On failure the server's message is left in error(). SettingsDialog: - New "Org Path" group: a text field prefilled from orgPath() when the dialog opens, a Browse… button opening a FolderDialog (QtQuick.Dialogs), and an Apply button. The folder picker's file:// URL is converted to a local path. A status line shows success or the server error. The field accepts a raw path too, so ~ and typed paths work. Main.qml: - On orgPathChanged, clear the selection and refresh the document list so the sidebar reflects the newly scanned directory. Verified over 9P against a live server using the exact TOML setApply writes: empty initial config, ~/org expanded to $HOME/org, and an absolute Browse-style path each switch the served documents. GUI builds clean (QML cache compiles SettingsDialog). --- ui/qml/Main.qml | 8 +++ ui/qml/SettingsDialog.qml | 108 +++++++++++++++++++++++++++++++++++++- ui/src/orkclient.cpp | 45 ++++++++++++++++ ui/src/orkclient.h | 11 ++++ 4 files changed, 171 insertions(+), 1 deletion(-) diff --git a/ui/qml/Main.qml b/ui/qml/Main.qml index 6f6e7b3..1d80a6d 100644 --- a/ui/qml/Main.qml +++ b/ui/qml/Main.qml @@ -83,6 +83,14 @@ ApplicationWindow { Connections { target: orkClient function onConnectedChanged() { root.refreshDocs() } + // When the org path changes via Settings, the server has rescanned a + // new directory: drop the current selection and reload the doc list. + function onOrgPathChanged() { + root.currentDoc = "" + docModel.document = "" + docModel.reload() + root.refreshDocs() + } } // Settings dialog diff --git a/ui/qml/SettingsDialog.qml b/ui/qml/SettingsDialog.qml index 78e9621..f6f609d 100644 --- a/ui/qml/SettingsDialog.qml +++ b/ui/qml/SettingsDialog.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import QtQuick.Dialogs Dialog { id: settingsDialog @@ -9,10 +10,46 @@ Dialog { standardButtons: Dialog.Close width: 450 height: 400 - + x: (parent.width - width) / 2 y: (parent.height - height) / 2 + // Load the current org path from the server whenever the dialog opens. + onOpened: { + orgPathField.text = orkClient.orgPath() + orgPathStatus.clear() + } + + // Native folder picker. Writes the chosen local path into the field; the + // user still presses Apply to send it to the server. + FolderDialog { + id: folderDialog + title: qsTr("Select Org Directory") + onAccepted: { + // selectedFolder is a file:// URL; convert to a local path. + orgPathField.text = decodeURIComponent( + selectedFolder.toString().replace(/^file:\/\//, "")) + } + } + + // Send the field's path to the server via /cfg. On success, refresh the + // document list; on failure, show the server's error message. + function applyOrgPath() { + var path = orgPathField.text.trim() + if (path.length === 0) { + orgPathStatus.ok = false + orgPathStatus.text = qsTr("Path is empty.") + return + } + if (orkClient.setOrgPath(path)) { + orgPathStatus.ok = true + orgPathStatus.text = qsTr("Applied. Scanning %1").arg(path) + } else { + orgPathStatus.ok = false + orgPathStatus.text = qsTr("Failed: %1").arg(orkClient.error) + } + } + background: Rectangle { color: theme.windowColor border.color: theme.midColor @@ -46,6 +83,75 @@ Dialog { contentItem: ColumnLayout { spacing: 16 + // --- Org Path --- + GroupBox { + title: qsTr("Org Path") + Layout.fillWidth: true + + background: Rectangle { + color: "transparent" + border.color: theme.midColor + border.width: 1 + radius: 4 + y: 12 + } + + label: Label { + x: 8 + text: parent.title + color: theme.textColor + font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11 + } + + ColumnLayout { + anchors.fill: parent + spacing: 8 + + Label { + text: qsTr("Directory the server scans for .org files. A leading ~ expands to your home directory.") + color: theme.placeholderTextColor + font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 10 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + TextField { + id: orgPathField + Layout.fillWidth: true + placeholderText: qsTr("e.g. ~/org") + selectByMouse: true + onAccepted: applyOrgPath() + } + + Button { + text: qsTr("Browse…") + onClicked: folderDialog.open() + } + + Button { + text: qsTr("Apply") + onClicked: applyOrgPath() + } + } + + Label { + id: orgPathStatus + Layout.fillWidth: true + wrapMode: Text.WordWrap + visible: text.length > 0 + font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 10 + // ok/err color set by applyOrgPath via property. + property bool ok: true + color: ok ? theme.doneColor : theme.deadlineColor + function clear() { text = "" } + } + } + } + // --- Color Scheme --- GroupBox { title: qsTr("Color Scheme") diff --git a/ui/src/orkclient.cpp b/ui/src/orkclient.cpp index da77124..b083711 100644 --- a/ui/src/orkclient.cpp +++ b/ui/src/orkclient.cpp @@ -61,6 +61,51 @@ QString OrkClient::rdwr(const QString &path, const QString &text) { return QString::fromUtf8(data); } +// Extract the org_path value from a TOML config body. Handles a basic +// `org_path = "..."` assignment with standard TOML string escapes. Returns an +// empty string if the key is absent. +static QString parseOrgPath(const QString &toml) { + const QStringList lines = toml.split('\n'); + for (QString line : lines) { + line = line.trimmed(); + if (line.startsWith('#')) continue; + if (!line.startsWith("org_path")) continue; + const int eq = line.indexOf('='); + if (eq < 0) continue; + QString val = line.mid(eq + 1).trimmed(); + if (val.size() < 2 || !val.startsWith('"') || !val.endsWith('"')) continue; + val = val.mid(1, val.size() - 2); + // Unescape the common TOML basic-string escapes we emit. + val.replace("\\\\", "\\"); + val.replace("\\\"", "\""); + return val; + } + return QString(); +} + +// Escape a string for a TOML basic string (double-quoted). +static QString tomlEscape(const QString &s) { + QString out; + out.reserve(s.size()); + for (const QChar c : s) { + if (c == '\\' || c == '"') out.append('\\'); + out.append(c); + } + return out; +} + +QString OrkClient::orgPath() { + return parseOrgPath(read("/cfg")); +} + +bool OrkClient::setOrgPath(const QString &path) { + const QString body = QStringLiteral("org_path = \"%1\"\n").arg(tomlEscape(path)); + const bool ok = write("/cfg", body); + syncError(); + if (ok) emit orgPathChanged(); + return ok; +} + QVariantList OrkClient::docs() { QVariantList out; const QString idx = read("/idx"); diff --git a/ui/src/orkclient.h b/ui/src/orkclient.h index 6530c34..7cdf7b9 100644 --- a/ui/src/orkclient.h +++ b/ui/src/orkclient.h @@ -45,6 +45,16 @@ public: // Write-then-read against a control/query file (/ctl, /query, .../new). Q_INVOKABLE QString rdwr(const QString &path, const QString &text); + // Read the server's org_path setting from /cfg (empty if unset). This is + // the directory the server scans for org files. + Q_INVOKABLE QString orgPath(); + + // Set the server's org_path in /cfg. The server validates and applies it + // (switching the scanned directory, expanding a leading ~), then persists + // the config. Returns true on success; on failure the reason is in error(). + // Emits orgPathChanged() when the write succeeds. + Q_INVOKABLE bool setOrgPath(const QString &path); + // Parse /idx into a list of {name, title, path} maps for the doc sidebar. Q_INVOKABLE QVariantList docs(); @@ -54,6 +64,7 @@ public: signals: void connectedChanged(); void errorChanged(); + void orgPathChanged(); private: void syncError();