Add code block execution to the GUI outline view

Each executable source block now shows a "Run" button in the outline.
Clicking it runs the block server-side and renders the inserted
#+RESULTS: in place, with an exit-status indicator.

DocumentModel gains executeBlock(row, blockIndex), which runs
/ctl execute <doc> <id> <n>, refreshes the section body, and emits
dataChanged(BodyRole). isLanguageSupported(lang) backs button visibility
via /ctl languages (loaded once, cached).

OutlineView tags each parsed code part with its src-block ordinal so the
GUI index matches the server's block indexing, and the code block header
carries the Run control plus running/ok/error status.
This commit is contained in:
Levi Neely 2026-10-01 15:26:05 +02:00
parent d073c8fa5e
commit 36a9607576
3 changed files with 152 additions and 10 deletions

View File

@ -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,16 +371,26 @@ 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
// Header row: language label + run control
RowLayout {
Layout.fillWidth: true
spacing: outline.baseFontSize * 0.5
// Language label
Label {
visible: (modelData.language ?? "") !== ""
@ -387,6 +400,70 @@ ListView {
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
Label {
Layout.fillWidth: true

View File

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

View File

@ -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 <doc> <id> <n>`,
// 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;
};