diff --git a/ui/qml/OutlineView.qml b/ui/qml/OutlineView.qml index 28e684a..d5aadb7 100644 --- a/ui/qml/OutlineView.qml +++ b/ui/qml/OutlineView.qml @@ -68,6 +68,7 @@ ListView { let inCode = false; let codeLang = ""; let codeLines = []; + let codeCounter = 0; let inTable = false; let tableLines = []; @@ -87,7 +88,8 @@ ListView { codeLang = trimmed.substring(11).trim().split(/\s+/)[0] || ""; codeLines = []; } else if (inCode && trimmed.toUpperCase() === "#+END_SRC") { - parts.push({type: "code", language: codeLang, content: codeLines.join('\n')}); + parts.push({type: "code", language: codeLang, content: codeLines.join('\n'), codeIndex: codeCounter}); + codeCounter++; inCode = false; codeLang = ""; codeLines = []; @@ -134,7 +136,8 @@ ListView { // Flush remaining if (inCode && codeLines.length > 0) { - parts.push({type: "code", language: codeLang, content: codeLines.join('\n')}); + parts.push({type: "code", language: codeLang, content: codeLines.join('\n'), codeIndex: codeCounter}); + codeCounter++; } else if (inTable && tableLines.length > 0) { let rows = []; let hasHeader = false; @@ -368,23 +371,97 @@ ListView { Component { id: codeBlockComponent Rectangle { + id: codeBlock color: theme.alternateBaseColor radius: outline.baseFontSize / 3 implicitHeight: codeColumn.implicitHeight + outline.baseFontSize - + + // Last execution status for this block: "", "running", "ok", "error" + property string runState: "" + property int runStatus: 0 + ColumnLayout { id: codeColumn anchors.fill: parent anchors.margins: outline.baseFontSize * 0.5 spacing: outline.baseFontSize * 0.3 - // Language label - Label { - visible: (modelData.language ?? "") !== "" - text: modelData.language - color: theme.linkColor - font.pointSize: outline.baseFontSize - 2 - font.bold: true + // Header row: language label + run control + RowLayout { + Layout.fillWidth: true + spacing: outline.baseFontSize * 0.5 + + // Language label + Label { + visible: (modelData.language ?? "") !== "" + text: modelData.language + color: theme.linkColor + font.pointSize: outline.baseFontSize - 2 + font.bold: true + } + + Item { Layout.fillWidth: true } + + // Run status indicator + Label { + visible: codeBlock.runState !== "" + text: { + switch (codeBlock.runState) { + case "running": return "running…" + case "ok": return "✓ exit 0" + case "error": return "✗ exit " + codeBlock.runStatus + default: return "" + } + } + color: codeBlock.runState === "error" + ? theme.cancelledColor + : (codeBlock.runState === "ok" + ? theme.doneColor + : theme.placeholderTextColor) + font.pointSize: outline.baseFontSize - 2 + } + + // Run button — only for executable languages + Rectangle { + id: runButton + visible: docModel.isLanguageSupported(modelData.language ?? "") + radius: 3 + implicitWidth: runLabel.implicitWidth + 14 + implicitHeight: runLabel.implicitHeight + 6 + color: runMouse.containsMouse ? theme.highlightColor : "transparent" + border.color: theme.linkColor + border.width: 1 + + Label { + id: runLabel + anchors.centerIn: parent + text: "▶ Run" + color: theme.linkColor + font.pointSize: outline.baseFontSize - 2 + font.bold: true + } + + MouseArea { + id: runMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + codeBlock.runState = "running" + let out = docModel.executeBlock(del.index, modelData.codeIndex ?? 0) + // The report contains a "status: N" line per block. + let m = out.match(/status:\s*(\d+)/) + let hadError = /\berror:/.test(out) + if (m) { + codeBlock.runStatus = parseInt(m[1]) + codeBlock.runState = codeBlock.runStatus === 0 ? "ok" : "error" + } else { + codeBlock.runStatus = hadError ? 1 : 0 + codeBlock.runState = hadError ? "error" : "ok" + } + } + } + } } // Code content diff --git a/ui/src/documentmodel.cpp b/ui/src/documentmodel.cpp index 68ae0e2..e735da3 100644 --- a/ui/src/documentmodel.cpp +++ b/ui/src/documentmodel.cpp @@ -135,6 +135,57 @@ void DocumentModel::reload() loadDocument(); } +QString DocumentModel::executeBlock(int row, int blockIndex) +{ + if (!m_client || m_document.isEmpty()) + return QStringLiteral("error: not connected"); + if (row < 0 || row >= m_visibleRows.size()) + return QStringLiteral("error: invalid row"); + + const QString &id = m_visibleRows[row]; + if (!m_sections.contains(id)) + return QStringLiteral("error: unknown section"); + + // Document name without a leading slash for the ctl verb. + QString doc = m_document; + if (doc.startsWith('/')) + doc = doc.mid(1); + + const QString cmd = QStringLiteral("execute %1 %2 %3") + .arg(doc, id) + .arg(blockIndex); + const QString result = m_client->rdwr(QStringLiteral("/ctl"), cmd); + + // Refresh the section body so the inserted #+RESULTS: block is rendered. + Section &sec = m_sections[id]; + sec.bodyLoaded = false; + loadBody(sec); + + const QModelIndex idx = index(row); + emit dataChanged(idx, idx, {BodyRole}); + + return result; +} + +bool DocumentModel::isLanguageSupported(const QString &lang) +{ + if (lang.isEmpty()) + return false; + + if (!m_languagesLoaded) { + m_supportedLanguages.clear(); + if (m_client) { + const QString out = m_client->rdwr(QStringLiteral("/ctl"), + QStringLiteral("languages")); + for (const QString &line : out.split('\n', Qt::SkipEmptyParts)) + m_supportedLanguages.append(line.trimmed().toLower()); + } + m_languagesLoaded = true; + } + + return m_supportedLanguages.contains(lang.trimmed().toLower()); +} + void DocumentModel::loadDocument() { beginResetModel(); diff --git a/ui/src/documentmodel.h b/ui/src/documentmodel.h index bd6b820..e9968df 100644 --- a/ui/src/documentmodel.h +++ b/ui/src/documentmodel.h @@ -60,6 +60,16 @@ public: // Reload from the server Q_INVOKABLE void reload(); + // Execute the Nth source block (0-based, counting only src blocks) in the + // section at the given visible row. Runs `/ctl execute `, + // refreshes that section's body so the inserted #+RESULTS: becomes visible, + // and returns the server's execution report. + Q_INVOKABLE QString executeBlock(int row, int blockIndex); + + // Whether a source block language can be executed by the server. Backed by + // `/ctl languages` (loaded once and cached). Case-insensitive. + Q_INVOKABLE bool isLanguageSupported(const QString &lang); + signals: void documentChanged(); @@ -121,4 +131,8 @@ private: // Visible row order (section ids) QStringList m_visibleRows; + + // Supported src-block languages from `/ctl languages` (loaded once). + mutable QStringList m_supportedLanguages; + mutable bool m_languagesLoaded = false; };