Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
37f75a5
core: give a document a role, and honour it
jcelerier Aug 6, 2026
9f4cfb8
tests: share the probe protocol fixture
jcelerier Aug 6, 2026
f3b1ea1
localtree: a terminal exposes nothing of its own
jcelerier Aug 6, 2026
c3a5ba4
engine: refuse a terminal's play before the transport, not after
jcelerier Aug 7, 2026
269d69d
device: length-delimit the protocol payload, like every other polymor…
jcelerier Aug 7, 2026
ffab27f
process: a command may name a factory this build does not have
jcelerier Aug 7, 2026
1914507
process: let a stand-in be given the state it never had
jcelerier Aug 7, 2026
196d70c
library: the panel follows whichever document is visible
jcelerier Aug 7, 2026
04035be
device: ask the document what may be added, not the machine
jcelerier Aug 7, 2026
762c176
library: browse the files of the machine the score runs on
jcelerier Aug 7, 2026
7c65318
device: show the connection state of the machine that has the device
jcelerier Aug 7, 2026
dc345c6
explorer: a device with no implementation must not throw
jcelerier Aug 7, 2026
cbd8f4b
process: ask for the state of anything built from another machine's data
jcelerier Aug 7, 2026
a26572c
library: do not rescan on every document change
jcelerier Aug 7, 2026
190cf6d
engine: a terminal's clock reads the position the host reports
jcelerier Aug 7, 2026
b5c1b0d
device: let a device say what it can be plugged into
jcelerier Aug 7, 2026
71ecef8
tests: the library test does not need a device
jcelerier Aug 7, 2026
6e8971d
library: ask for the environment each time, do not capture it
jcelerier Aug 7, 2026
5a2a81b
JS: let --script take a file, and report evaluation errors
jcelerier Aug 7, 2026
900623f
JS: do not import modules into a panel that does not exist
jcelerier Aug 7, 2026
99e5913
terminal: refresh port combo boxes when the peer reports a device
jcelerier Aug 7, 2026
367b685
engine: do not stop through a GUI action
jcelerier Aug 7, 2026
e047c9a
ports: accept a device dropped from the explorer
jcelerier Aug 7, 2026
9a3fe54
devices: keep the peer's hardware categories as categories
jcelerier Aug 7, 2026
f5d1e80
devices: let a document with no devices hand an edit to whoever has them
jcelerier Aug 7, 2026
862e28f
tests: give the probe protocol something deterministic to enumerate
jcelerier Aug 7, 2026
e150909
opaque: write a stand-in's ports in the binary format too
jcelerier Aug 7, 2026
18fb90a
devices: do not write into a device list that has moved on
jcelerier Aug 7, 2026
a636546
engine: stop what is playing, not what is in front
jcelerier Aug 7, 2026
60aa3cb
serialization: do not break into the debugger over a peer's bytes
jcelerier Aug 7, 2026
0667b61
tests: assert the behaviour, not the default that describes it
jcelerier Aug 7, 2026
f68b71f
tests: give the fixture a device that is really there
jcelerier Aug 7, 2026
619cf8f
comments: cut the rationale, keep what the code does
jcelerier Aug 7, 2026
19151a3
comments: second pass over the rationale blocks
jcelerier Aug 7, 2026
23206a7
devices: say what a device turned out to contain
jcelerier Aug 8, 2026
279cc6a
ThreadPool: stop joining the workers from whoever releases last
jcelerier Aug 8, 2026
6db7c3f
Sound: ask for a waveform once per view, not once per paint
jcelerier Aug 8, 2026
162db80
Environment: walk a tree, and name the inline transfer limit
jcelerier Aug 8, 2026
f0227b6
Devices: list presets through the document's environment
jcelerier Aug 8, 2026
14ab119
Import a file to where the score runs, not just to where it was dropped
jcelerier Aug 8, 2026
bbea094
Library: preview a shader from its bytes when the file is not here
jcelerier Aug 8, 2026
48e2185
Devices: let a terminal add what only the host can make
jcelerier Aug 8, 2026
c4c6402
Devices: hand the add command a device, not an empty node
jcelerier Aug 8, 2026
d595a20
Devices: keep the protocol's own "Name" through an opaque payload
jcelerier Aug 8, 2026
4c980d0
Devices: announce a device's tree when the device appears
jcelerier Aug 8, 2026
e6e4148
Devices: tell peers what a device reported
jcelerier Aug 8, 2026
f8c0df1
tests: run the shader sweeps, and let a test hide a factory
jcelerier Aug 8, 2026
e9fa85e
Let a document say where a script typed into it should run
jcelerier Aug 8, 2026
dc26823
JS: own the script evaluator instead of leaking a static one
jcelerier Aug 8, 2026
d7994ea
Let the library walk yield instead of holding the dialog shut
jcelerier Aug 8, 2026
8c8d6f5
ci: build the add-on branch this work is paired with
jcelerier Aug 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ci/common.deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ clone_addon() {
)
}

clone_addon https://github.com/ossia/iscore-addon-network
# Paired with the session work on the add-on: the two repositories change
# together, so CI has to build the branch that matches rather than master.
clone_addon https://github.com/ossia/iscore-addon-network remote-control
clone_addon https://github.com/ossia/score-addon-synthimi
clone_addon https://github.com/ossia/score-addon-jk
clone_addon https://github.com/ossia/GBAP
Expand Down
2 changes: 2 additions & 0 deletions src/lib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ set(HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/score/application/ApplicationComponents.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/application/ApplicationContext.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/application/ApplicationServices.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/application/ScriptEvaluator.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/application/GUIApplicationContext.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/command/AggregateCommand.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/command/Command.hpp"
Expand Down Expand Up @@ -413,6 +414,7 @@ set(SRCS
"${CMAKE_CURRENT_SOURCE_DIR}/score/application/ApplicationContext.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/application/ApplicationComponents.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/application/ApplicationServices.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/application/ScriptEvaluator.cpp"

"${CMAKE_CURRENT_SOURCE_DIR}/score/actions/Action.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/actions/ActionManager.cpp"
Expand Down
42 changes: 42 additions & 0 deletions src/lib/core/application/ApplicationSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,56 @@
#include <QFileInfo>
#include <QObject>
#include <QString>
#include <QUrlQuery>

#if defined(__EMSCRIPTEN__)
#include <emscripten.h>
#endif

#include <score_git_info.hpp>

#include <thread>
namespace score
{
#if defined(__EMSCRIPTEN__)
namespace
{
//! A page has no command line: ?network-join=host:port&network-terminal
//! reaches score as --network-join=host:port --network-terminal.
//!
//! Passed through without an allow-list, so a link can ask a page to do
//! whatever a command line could.
QStringList argumentsFromUrl()
{
QStringList out;
const char* raw = emscripten_run_script_string("location.search");
if(!raw || !*raw)
return out;

QString search = QString::fromUtf8(raw);
if(search.startsWith('?'))
search.remove(0, 1);

const QUrlQuery query{search};
for(const auto& [key, value] : query.queryItems(QUrl::FullyDecoded))
{
if(key.isEmpty())
continue;

out += value.isEmpty() ? QStringLiteral("--%1").arg(key)
: QStringLiteral("--%1=%2").arg(key, value);
}
return out;
}
}
#endif

void ApplicationSettings::parse(QStringList cargs, int& argc, char** argv)
{
#if defined(__EMSCRIPTEN__)
cargs += argumentsFromUrl();
#endif

arguments = cargs;

opengl = false;
Expand Down
11 changes: 8 additions & 3 deletions src/lib/core/command/CommandStackSerialization.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ void loadCommandStack(
stack.updateStack([&]() {
stack.setSavedIndex(-1);

// A command we cannot read stops the history there rather than the load:
// what precedes it is consistent, what follows would undo against a state
// we never reached.
bool ok = true;
for(const auto& elt : undoStack)
{
auto cmd = components.instantiateUndoCommand(elt);
auto cmd = components.instantiateUndoCommandIfAvailable(elt);

if(redo_fun(cmd))
if(cmd && redo_fun(cmd))
{
stack.undoable().push(cmd);
}
Expand All @@ -44,7 +47,9 @@ void loadCommandStack(
{
for(const auto& elt : redoStack)
{
auto cmd = components.instantiateUndoCommand(elt);
auto cmd = components.instantiateUndoCommandIfAvailable(elt);
if(!cmd)
break;

stack.redoable().push(cmd);
}
Expand Down
15 changes: 15 additions & 0 deletions src/lib/core/document/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,26 @@ const std::vector<DocumentPlugin*>& DocumentContext::pluginModels() const
return document.model().pluginModels();
}

void Document::setScriptSink(ScriptSink s)
{
m_scriptSink = std::move(s);
}

const Document::ScriptSink& Document::scriptSink() const noexcept
{
return m_scriptSink;
}

score::Environment& DocumentContext::environment() const noexcept
{
return document.environment();
}

score::DocumentRole DocumentContext::role() const noexcept
{
return document.role();
}

Document::Document(
const QString& name, const Id<DocumentModel>& id, DocumentDelegateFactory& factory,
QWidget* parentview, QObject* parent)
Expand Down
23 changes: 22 additions & 1 deletion src/lib/core/document/Document.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once
#include <score/document/DocumentContext.hpp>
#include <score/document/DocumentRole.hpp>
#include <score/tools/Environment.hpp>
#include <score/locking/ObjectLocker.hpp>
#include <score/selection/FocusManager.hpp>
Expand All @@ -13,6 +14,8 @@

#include <QByteArray>
#include <QString>

#include <functional>
#include <QTimer>
#include <QVariant>

Expand Down Expand Up @@ -85,6 +88,21 @@ class SCORE_LIB_BASE_EXPORT Document final : public QObject
//! typically, once it is being edited through a session.
void setEnvironment(std::unique_ptr<score::Environment> env);

//! Where a script typed here should run. Set when this document is a view of
//! a score running elsewhere: the console then asks that machine instead of
//! answering from a document with no devices and no execution behind it.
//! `onResult` is called with what the other machine printed.
using ScriptSink
= std::function<void(const QString& code, std::function<void(QString)> onResult)>;
void setScriptSink(ScriptSink s);
const ScriptSink& scriptSink() const noexcept;

//! Whether this document may drive the hardware of the machine it is open on.
//!
//! Fixed at construction: devices connect while their plug-in deserializes,
//! so a document that must not claim them has to say so before it is read.
DocumentRole role() const noexcept { return m_role; }

DocumentModel& model() const noexcept { return *m_model; }

DocumentPresenter* presenter() const noexcept { return m_presenter; }
Expand Down Expand Up @@ -130,7 +148,8 @@ class SCORE_LIB_BASE_EXPORT Document final : public QObject

Document(
const QString& name, const QByteArray& data, SerializationIdentifier format,
DocumentDelegateFactory& type, QWidget* parentview, QObject* parent);
DocumentDelegateFactory& type, QWidget* parentview, QObject* parent,
DocumentRole role = DocumentRole::Local);

// Restore
Document(
Expand Down Expand Up @@ -163,6 +182,8 @@ class SCORE_LIB_BASE_EXPORT Document final : public QObject

DocumentContext m_context;
mutable std::unique_ptr<score::Environment> m_environment;
ScriptSink m_scriptSink;
DocumentRole m_role{DocumentRole::Local};

std::optional<score::RestorableDocument> m_initialData{};
bool m_virgin{false}; // Used to check if we can safely close it
Expand Down
5 changes: 3 additions & 2 deletions src/lib/core/document/DocumentBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,14 @@ Document* DocumentBuilder::loadDocument(
SCORE_LIB_BASE_EXPORT
Document* DocumentBuilder::loadDocument(
const score::GUIApplicationContext& ctx, QString filename, QByteArray data,
SerializationIdentifier format, DocumentDelegateFactory& doctype)
SerializationIdentifier format, DocumentDelegateFactory& doctype, DocumentRole role)
{
Document* doc = nullptr;
auto& doclist = ctx.documents.documents();
try
{
doc = new Document{filename, data, format, doctype, m_parentView, m_parentPresenter};
doc = new Document{
filename, data, format, doctype, m_parentView, m_parentPresenter, role};
for(auto& appPlug : ctx.guiApplicationPlugins())
{
appPlug->on_loadedDocument(*doc);
Expand Down
4 changes: 3 additions & 1 deletion src/lib/core/document/DocumentBuilder.hpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#pragma once
#include <score/document/DocumentRole.hpp>
#include <score/model/Identifier.hpp>
#include <score/serialization/VisitorInterface.hpp>

Expand Down Expand Up @@ -36,7 +37,8 @@ class SCORE_LIB_BASE_EXPORT DocumentBuilder
score::DocumentDelegateFactory& doctype);
Document* loadDocument(
const score::GUIApplicationContext& ctx, QString filename, QByteArray data,
SerializationIdentifier format, score::DocumentDelegateFactory& doctype);
SerializationIdentifier format, score::DocumentDelegateFactory& doctype,
DocumentRole role = DocumentRole::Local);
Document* restoreDocument(
const score::GUIApplicationContext& ctx, const score::RestorableDocument& doc,
score::DocumentDelegateFactory& doctype);
Expand Down
6 changes: 5 additions & 1 deletion src/lib/core/document/DocumentSerialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,17 @@ Document::Document(

Document::Document(
const QString& fileName, const QByteArray& data, SerializationIdentifier format,
DocumentDelegateFactory& factory, QWidget* parentview, QObject* parent)
DocumentDelegateFactory& factory, QWidget* parentview, QObject* parent,
DocumentRole role)
: QObject{parent}
, m_metadata{fileName}
, m_commandStack{*this}
, m_objectLocker{this}
, m_context{*this}
, m_role{role}
{
// Before loadModel: devices are instantiated as the device plug-in is read,
// and a terminal must not claim any of them.
loadModel(fileName, data, format, factory);

if(parentview)
Expand Down
7 changes: 2 additions & 5 deletions src/lib/core/presenter/DocumentManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -744,11 +744,8 @@ DocumentManager::Loadability DocumentManager::checkAndUpdateJson(
auto it = local_plugins.find(plug.plugin);
if(it == local_plugins.end())
{
// Not fatal. Refusing here used to make a document unopenable on any
// machine that did not have every plug-in it mentions -- which is every
// machine, once builds differ by platform -- and it could not see a
// factory missing inside a plug-in that *is* present anyway, so it never
// gave the guarantee it appeared to.
// Not fatal: it never gave the guarantee it looked like, since a
// factory missing inside a present plug-in is invisible to it.
res.missingPlugins.push_back(plug.plugin);
}
else
Expand Down
Empty file modified src/lib/core/view/QRecentFilesMenu.h
100755 → 100644
Empty file.
12 changes: 12 additions & 0 deletions src/lib/score/application/ScriptEvaluator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#include <score/application/ScriptEvaluator.hpp>

namespace score
{
ScriptEvaluator::~ScriptEvaluator() = default;

ScriptEvaluator*& scriptEvaluator() noexcept
{
static ScriptEvaluator* instance{};
return instance;
}
}
37 changes: 37 additions & 0 deletions src/lib/score/application/ScriptEvaluator.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#pragma once
#include <QString>

#include <score_lib_base_export.h>

namespace score
{
struct DocumentContext;

/**
* @brief Running a script on this machine, for somebody who is not here.
*
* A terminal's console edits a score that runs elsewhere. Commands happen to
* replicate, so `Score.createProcess` appears to work -- but everything that is
* not a command runs against the terminal's own document, where there are no
* devices, no execution and no hardware. `Score.device("x")` is null there and
* always will be.
*
* Rather than forwarding one call at a time, the script itself goes to the
* machine that can answer it. Which means the session layer has to run
* JavaScript, and it has no business knowing what JavaScript is: it looks up
* this interface, which the JS plug-in registers if it is loaded, and finds
* nothing if it is not.
*/
struct SCORE_LIB_BASE_EXPORT ScriptEvaluator
{
virtual ~ScriptEvaluator();

//! Evaluate `code` against `ctx`. The returned string is what a console
//! would have printed -- the value, or the error.
virtual QString evaluate(const score::DocumentContext& ctx, const QString& code) = 0;
};

//! The evaluator for this process, or null when nothing registered one.
//! Set once at startup by whichever plug-in can actually run scripts.
SCORE_LIB_BASE_EXPORT ScriptEvaluator*& scriptEvaluator() noexcept;
}
4 changes: 4 additions & 0 deletions src/lib/score/document/DocumentContext.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <score/command/CommandStackFacade.hpp>
#include <score/command/Dispatchers/OngoingCommandDispatcher.hpp>
#include <score/document/DocumentInterface.hpp>
#include <score/document/DocumentRole.hpp>
#include <score/selection/FocusManager.hpp>
class IdentifiedObjectAbstract;
class QTimer;
Expand Down Expand Up @@ -36,6 +37,9 @@ struct SCORE_LIB_BASE_EXPORT DocumentContext
//! the score. Ask rather than assuming a path can be opened.
score::Environment& environment() const noexcept;

//! Whether this document may claim the hardware of the machine it is open on.
score::DocumentRole role() const noexcept;

template <typename T>
T& model() const
{
Expand Down
34 changes: 34 additions & 0 deletions src/lib/score/document/DocumentRole.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#pragma once

namespace score
{
/**
* @brief What a document is allowed to do to the machine it is open on.
*
* A score names hardware: sound cards, MIDI ports, cameras, OSC sockets,
* render windows. Opening one has always meant claiming all of it, because the
* machine holding the document was the machine running the show.
*
* That stops being true once a score can be edited from somewhere else. A
* laptop driving a score that plays on a headless box must not open that box's
* MIDI ports on itself, nor put its render window on the wrong screen -- and
* the browser it might be a tab in has none of those things to offer anyway.
*
* Distinct from score::Environment, which answers where the *files* are: a
* peer in a multiplayer session reads its score from another machine and still
* plays it on its own hardware. Both answers are needed, and they differ.
*
* Known before the document is read rather than set afterwards: devices
* connect while their plug-in deserializes, so anything decided later is
* decided too late.
*/
enum class DocumentRole
{
//! Ordinary. Devices connect, execution runs here, windows open here.
Local,

//! The score runs elsewhere. This copy is for editing and watching it: no
//! ports, no hardware, no rendering, no executor.
Terminal
};
}
6 changes: 2 additions & 4 deletions src/lib/score/model/path/ObjectPath.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,8 @@ class SCORE_LIB_BASE_EXPORT ObjectPath
template <class T>
T& find(const score::DocumentContext& ctx) const
{
// Checked rather than assumed, for the reason given on try_find: an object
// of another type can be standing where this path points. safe_cast would
// abort in debug and cast blind in release; throwing lets the caller --
// a command being replayed from another peer, typically -- report it.
// Checked: another type can stand where this path points, and safe_cast
// aborts in debug and casts blind in release.
auto raw = m_cache.isNull() ? find_impl(ctx) : m_cache.data();
auto ptr = dynamic_cast<typename std::remove_const<T>::type*>(raw);
if(!ptr)
Expand Down
Loading
Loading