View Issue Details

IDProjectCategoryView StatusLast Update
0017807ScribusScripterpublic2026-05-06 17:19
Reporteraewa Assigned To 
PrioritynormalSeverityfeatureReproducibilityalways
Status newResolutionopen 
Product Version1.7.3 
Summary0017807: [PATCH] Scripter: expose a primitive to run callables on the GUI thread
DescriptionVersion: Scribus 1.7.3 (Windows + Linux)

The problem

I'm writing a Python script that runs inside Scribus and listens on a network socket, so an external program can drive Scribus (create documents, add frames, etc.). The listener has to live on a background thread — if the main thread blocks on accept(), the GUI freezes.

But any Scripter call from that background thread crashes Scribus (segfault on Windows, 0000097:0000100% repro on scribus.newDocument). That's expected — Qt requires GUI-thread affinity. The real problem is that Scripter gives me no way to hand work back to the GUI thread. There's no scribus.callOnMainThread(fn).

Current workaround

Bundle PyQt6 inside Scribus's Python and run a QTimer on the GUI thread that drains a queue every 20 ms. It works, but it forces every user to install a PyQt6 build whose Qt minor version exactly matches the Qt that Scribus was built against. On Windows this is fragile: Scribus 1.7.3 ships Qt 6.10.3 but pip install PyQt6 gives 6.11.x, which fails to load against 6.10.3 with ImportError: DLL load failed … procedure could not be found. End users have to be hand-held through picking the right wheel — just to schedule a callback.

What would fix it

scribus.invokeLater(callable, *args)

Schedules callable(*args) on the GUI thread and returns immediately. The implementation already exists in your tree — cmdmisc.cpp:745 uses exactly this pattern to route the quit slot:
QMetaObject::invokeMethod(ScCore->primaryMainWindow(), "slotFileQuit", Qt::QueuedConnection);

A generic Python-callable wrapper around the same call (storing the PyObject*, posting a queued event, invoking it on the GUI thread under the GIL) would unblock every long-running Scripter integration — network bridges, file watchers, batch tools with progress UI — without requiring a third-party Qt binding.

Two related observations

  1. Read-only Scripter calls (haveDoc, pageCount, getColorNames) often appear to work from a background thread. They don't crash, so people assume their threading model is fine — until the first mutation segfaults. A Q_ASSERT(QThread::currentThread() == qApp->thread()) at the top of each Scripter binding (debug builds only) would catch this immediately and save users a confusing debugging session.
  2. scribus.processEvents() isn't exposed. I went looking for it after a tight Python loop on the GUI thread produced "Application not responding". The method table in scriptplugin.cpp doesn't include it; the only processEvents() calls in scriptplugin/ are internal pumps inside the progress-bar commands. The dormant scripter2 branch wraps it, but mainline Scripter doesn't. Either it's worth exposing as scribus.processEvents(), or the docs should say clearly that scripts must yield to Qt some other way (and right now there is no other way from Python — hence this ticket).

Why it's worth doing

Every non-trivial Scripter integration hits this same wall and reinvents the same QTimer-with-bundled-PyQt workaround. Exposing the primitive Scribus already uses internally turns a multi-day setup problem into three lines of plain Python.
Additional InformationRelated information : I have create a MCP for LLM (Claude) to make reproductible and editable documents.

https://github.com/caewa/scribus-mcp
TagsMCP
PatchYes

Activities

ale

2026-05-05 14:51

manager   ~0053654

would you mind creating a patch?

aewa

2026-05-06 10:53

reporter   ~0053660

I have tested the patch with my MCP.
If you want to test it too, use at least ther v1.3.0 of the MCP.
scripter-invokelater.patch (5,918 bytes)   
diff --git a/scribus/plugins/scriptplugin/cmdmisc.cpp b/scribus/plugins/scriptplugin/cmdmisc.cpp
index f057b2b33..320233ddc 100644
--- a/scribus/plugins/scriptplugin/cmdmisc.cpp
+++ b/scribus/plugins/scriptplugin/cmdmisc.cpp
@@ -10,7 +10,9 @@ for which a new license (GPL+exception) is in place.
 #include <string>
 
 #include <QBuffer>
+#include <QCoreApplication>
 #include <QList>
+#include <QMetaObject>
 #include <QPixmap>
 
 #include "prefsmanager.h"
@@ -799,6 +801,59 @@ PyObject *scribus_readpdfoptions(PyObject* /* self */, PyObject* args)
 	Py_RETURN_NONE;
 }
 
+PyObject *scribus_invokelater(PyObject* /* self */, PyObject* args)
+{
+	Py_ssize_t n = PyTuple_GET_SIZE(args);
+	if (n < 1)
+	{
+		PyErr_SetString(PyExc_TypeError, "invokeLater() requires a callable");
+		return nullptr;
+	}
+	PyObject* callable = PyTuple_GET_ITEM(args, 0);
+	if (!PyCallable_Check(callable))
+	{
+		PyErr_SetString(PyExc_TypeError, "invokeLater() first argument must be callable");
+		return nullptr;
+	}
+	PyObject* callArgs = PyTuple_GetSlice(args, 1, n);
+	if (!callArgs)
+		return nullptr;
+	Py_INCREF(callable);
+
+	QObject* target = ScCore ? ScCore->primaryMainWindow() : nullptr;
+	if (!target)
+		target = QCoreApplication::instance();
+	if (!target)
+	{
+		Py_DECREF(callable);
+		Py_DECREF(callArgs);
+		PyErr_SetString(ScribusException, "invokeLater(): no Qt application available");
+		return nullptr;
+	}
+
+	QMetaObject::invokeMethod(target, [callable, callArgs]() {
+		PyGILState_STATE gs = PyGILState_Ensure();
+		PyObject* res = PyObject_Call(callable, callArgs, nullptr);
+		if (!res)
+			PyErr_Print();
+		else
+			Py_DECREF(res);
+		Py_DECREF(callable);
+		Py_DECREF(callArgs);
+		PyGILState_Release(gs);
+	}, Qt::QueuedConnection);
+
+	Py_RETURN_NONE;
+}
+
+PyObject *scribus_processevents(PyObject* /* self */)
+{
+	Py_BEGIN_ALLOW_THREADS
+	QCoreApplication::processEvents();
+	Py_END_ALLOW_THREADS
+	Py_RETURN_NONE;
+}
+
 /*! HACK: this removes "warning: 'blah' defined but not used" compiler warnings
 with header files structure untouched (docstrings are kept near declarations)
 PV */
@@ -818,10 +873,12 @@ void cmdmiscdocwarnings()
 	  << scribus_islayerlocked__doc__
 	  << scribus_islayeroutlined__doc__
 	  << scribus_islayerprintable__doc__
+	  << scribus_invokelater__doc__
 	  << scribus_islayervisible__doc__
 	  << scribus_loweractivelayer__doc__
 	  << scribus_moveselectiontoback__doc__ 
 	  << scribus_moveselectiontofront__doc__
+	  << scribus_processevents__doc__
 	  << scribus_raiseactivelayer__doc__
 	  << scribus_readpdfoptions__doc__
 	  << scribus_renderfont__doc__
diff --git a/scribus/plugins/scriptplugin/cmdmisc.h b/scribus/plugins/scriptplugin/cmdmisc.h
index 610dc2249..6e7394595 100644
--- a/scribus/plugins/scriptplugin/cmdmisc.h
+++ b/scribus/plugins/scriptplugin/cmdmisc.h
@@ -373,6 +373,34 @@ Save PDF options to fileName.\n\
 "));
 PyObject *scribus_savepdfoptions(PyObject* /* self */, PyObject* args);
 
+/*! docstring */
+PyDoc_STRVAR(scribus_invokelater__doc__,
+QT_TR_NOOP("invokeLater(callable, *args)\n\
+\n\
+Schedules callable(*args) to run on the Scribus GUI thread and returns\n\
+immediately. Use this from background threads (e.g. socket listeners) to\n\
+hand mutating Scripter calls back to the main thread; calling Scripter\n\
+APIs that touch the document or GUI from another thread is unsafe and\n\
+will typically crash.\n\
+\n\
+The callable is invoked asynchronously via a queued Qt event, so by the\n\
+time invokeLater() returns the call has not yet happened. Exceptions\n\
+raised inside the callable are printed to the Scribus script error\n\
+stream; they cannot be propagated back to the caller.\n\
+"));
+/*! Schedule a Python callable on the GUI thread (queued). */
+PyObject *scribus_invokelater(PyObject * /*self*/, PyObject* args);
+
+/*! docstring */
+PyDoc_STRVAR(scribus_processevents__doc__,
+QT_TR_NOOP("processEvents()\n\
+\n\
+Process pending Qt events. Call this periodically from a long-running\n\
+Python loop running on the GUI thread to keep the interface responsive.\n\
+"));
+/*! Pump the Qt event loop. */
+PyObject *scribus_processevents(PyObject * /*self*/);
+
 PyDoc_STRVAR(scribus_readpdfoptions__doc__,
 QT_TR_NOOP("readPDFOptions(fileName)\n\
 \n\
diff --git a/scribus/plugins/scriptplugin/scriptplugin.cpp b/scribus/plugins/scriptplugin/scriptplugin.cpp
index 86e4bd6b9..5667ec169 100644
--- a/scribus/plugins/scriptplugin/scriptplugin.cpp
+++ b/scribus/plugins/scriptplugin/scriptplugin.cpp
@@ -457,6 +457,7 @@ PyMethodDef scribus_methods[] = {
 	{ "insertTableColumns", scribus_inserttablecolumns, METH_VARARGS, tr(scribus_inserttablecolumns__doc__)},
 	{ "insertTableRows", scribus_inserttablerows, METH_VARARGS, tr(scribus_inserttablerows__doc__)},
 	{ "insertText", scribus_inserttext, METH_VARARGS, tr(scribus_inserttext__doc__)},
+	{ "invokeLater", scribus_invokelater, METH_VARARGS, tr(scribus_invokelater__doc__)},
 	{ "isExportable", scribus_isexportable, METH_VARARGS, tr(scribus_isexportable__doc__)},
 	{ "isLayerFlow", scribus_islayerflow, METH_VARARGS, tr(scribus_islayerflow__doc__)},
 	{ "isLayerLocked", scribus_islayerlocked, METH_VARARGS, tr(scribus_islayerlocked__doc__)},
@@ -497,6 +498,7 @@ PyMethodDef scribus_methods[] = {
 	{ "placeSVG", scribus_placevec, METH_VARARGS, tr(scribus_placesvg__doc__)},
 	{ "placeSXD", scribus_placevec, METH_VARARGS, tr(scribus_placesxd__doc__)},
 	{ "placeVectorFile", scribus_placevec, METH_VARARGS, tr(scribus_placevec__doc__)},
+	{ "processEvents", (PyCFunction) scribus_processevents, METH_NOARGS, tr(scribus_processevents__doc__)},
 	{ "progressReset", (PyCFunction) scribus_progressreset, METH_NOARGS, tr(scribus_progressreset__doc__)},
 	{ "progressSet", scribus_progresssetprogress, METH_VARARGS, tr(scribus_progresssetprogress__doc__)},
 	{ "progressTotal", scribus_progresssettotalsteps, METH_VARARGS, tr(scribus_progresssettotalsteps__doc__)},
scripter-invokelater.patch (5,918 bytes)   

Issue History

Date Modified Username Field Change
2026-05-05 08:16 aewa New Issue
2026-05-05 08:16 aewa Tag Attached: MCP
2026-05-05 14:51 ale Note Added: 0053654
2026-05-06 10:53 aewa Note Added: 0053660
2026-05-06 10:53 aewa File Added: scripter-invokelater.patch
2026-05-06 17:19 ale Summary Scripter: expose a primitive to run callables on the GUI thread => [PATCH] Scripter: expose a primitive to run callables on the GUI thread
2026-05-06 17:19 ale Patch No => Yes