Add an org path setting with a directory picker to Settings

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).
This commit is contained in:
Levi Neely 2026-10-02 12:11:19 +02:00
parent 7c9779b2fc
commit c4dc18cbfc
4 changed files with 171 additions and 1 deletions

View File

@ -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

View File

@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
Dialog {
id: settingsDialog
@ -13,6 +14,42 @@ Dialog {
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")

View File

@ -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");

View File

@ -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();