Event Horizon Shell plagarism

The Event Horizon Shell is a wayland shell designed to be used with various wayland compositors and developed by the content creator Mattscreative.

It initially started out as a closed source projects which was a mix of dot files to preconfigure supported compositors and a work-in-progress shell with releases being pushed into a dedicated release repository

I personally only learned about this project by people who were already suspiscious of it and I could use my limited reverse engineering skills to help determine what the deal was, though to be honest I did go a bit overboard and ended up disecting the arch, deb and rpm releases to see if there were any meaningful differences (spoiler alert: there were none and they were all compiled on Fedora).

The first thing I wanted to take a look at is the compositor support, supporting Niri, Hyprland, Sway and more is not a simple undertaking so I was interested how this was achieved so I searched for “Niri” and found a function which returns a string for what was probably an enum in the original source code:

 1char * compositor_kind_cstr(undefined1 param_1)
 2
 3{
 4  switch(param_1) {
 5  default:
 6    return "Unknown";
 7  case 1:
 8    return "Niri";
 9  case 2:
10    return "Hyprland";
11  case 3:
12    return "Sway";
13  case 4:
14    return "Mango";
15  case 5:
16    return "Labwc";
17  case 6:
18    return "Triad";
19  }
20}

What is weird is that Triad support was never advertised, is this an in-progress feature that simple wasn’t advertised or was it inherited from somewhere?

Searching for “Hyprland” in the strings gave me actually useful symbol names, for example _ZN24HyprlandWorkspaceBackend23recomputeWorkspaceFlagsEv which is the mangled form of HyprlandWorkspaceBackend::recomputeWorkspaceFlags() which could be traced back to Noctalia[1]

The compositor_kind_cstr function was also taken from Noctalia directly[2] and their enum order[3] matches the order of the decompiled function, the likelyhood for this to happen by chance is so low that it is more likely that this was just copied.

I then realized that the release repo actually contained the Noctalia Shell License[4] so while it is a bit morally dubious no license was violated except Matt decided that it was trash and removed the references to it, if this was just an innocent mistake or Matt based his work off of Noctalia then why would he go out and react like this?

Everyone who should be aware of this had been given the same information written here up to this point and I simply decided to move on but keep an eye on the situation during which Matt had decided to mock people accusing him of AI and plagarism in his discord status and in release notes using AI generated images [5].

Some time later the Event Horizon Shell was open sourced[6] so now I can actually look at the real code and it became very obvious that at least parts of the shell were just directly copied from Noctalia without proper attribution which violates their license.

for example:

Event Horizon

 1void HyprlandWorkspaceManager::jumpTo(const std::string& id) {
 2  auto wsId = findIdForName(id);
 3  if (wsId <= 0) { return; }
 4  auto cmd = m_backend.isLuaConfig()
 5    ? std::format("dispatch hl.dsp.focus({{workspace = {}}})", wsId)
 6    : std::format("dispatch workspace {}", wsId);
 7  if (!m_backend.sendCommand(cmd)) {
 8    debug_log("hyprland", "workspace jumpTo failed: %s no response", cmd.c_str());
 9  }
10}  

Noctalia

 1void HyprlandWorkspaceBackend::activate(const std::string& id) {
 2  if (id.empty() || !m_runtime.available()) {
 3    return;
 4  }
 5
 6  std::string target = id;
 7  if (const auto* workspace = findWorkspaceByKey(id); workspace != nullptr) {
 8    target = workspace->identity.selector;
 9  } else if (const auto parsed = parseInt(id); parsed.has_value() && *parsed < 0) {
10    // Legacy named workspace IDs are not valid absolute dispatcher targets.
11    return;
12  }
13
14  if (m_runtime.configIsLua()) {
15    (void)m_runtime.request(std::format("dispatch hl.dsp.focus({{workspace = \"{}\"}})", target));
16  } else {
17    (void)m_runtime.request(std::format("dispatch workspace {}", target));
18  }
19}

this is functionally the same code with method names changed, some logic inlined and reformatted and this is done across every single compositor implementation and maybe more.

To further illustrate this point I’ve ran clang-format across both the Noctalia and Event Horizon codebase to diff some functions, red is what was in the original codebase and green is what changed, everything that isn’t explicitly colored is the same

 1@@ -1,20 +1,20 @@
 2-[[nodiscard]] std::optional<bool> jsonOptionalBool(const nlohmann::json& payload, const char* key) {
 3-  if (!payload.is_object()) {
 4+std::optional<bool> optBool(nlohmann::json const &p, const char *k) {
 5+  if (!p.is_object()) {
 6     return std::nullopt;
 7   }
 8-  const auto it = payload.find(key);
 9-  if (it == payload.end()) {
10+  auto it = p.find(k);
11+  if (it == p.end()) {
12     return std::nullopt;
13   }
14   if (it->is_boolean()) {
15     return it->get<bool>();
16   }
17   if (it->is_string()) {
18-    const auto value = it->get<std::string>();
19-    if (value == "open" || value == "opened" || value == "true") {
20+    auto v = it->get<std::string>();
21+    if (v == "open" || v == "opened" || v == "true") {
22       return true;
23     }
24-    if (value == "closed" || value == "false") {
25+    if (v == "closed" || v == "false") {
26       return false;
27     }
28   }
  1@@ -1,71 +1,72 @@
  2-void NiriWorkspaceBackend::apply(std::vector<Workspace>& workspaces, const std::string& outputName) const {
  3-  if (!m_runtime.available() || workspaces.empty() || m_workspaces.empty()) {
  4+void NiriWorkspaceManager::sync(std::vector<DeskRegion> &ws,
  5+                                const std::string &outName) const {
  6+  if (!m_backend.canConnect() || ws.empty() || m_workspaces.empty()) {
  7     return;
  8   }
  9
 10-  const std::vector<const WorkspaceState*> candidates = sortedWorkspaceCandidatesForOutput(outputName);
 11-
 12-  std::vector<const WorkspaceState*> matches(workspaces.size(), nullptr);
 13+  auto cand = sortedWorkspaceCandidates(outName);
 14+  std::vector<WsState const *> matches(ws.size(), nullptr);
 15   std::unordered_map<std::uint64_t, bool> used;
 16
 17-  for (std::size_t i = 0; i < workspaces.size(); ++i) {
 18-    const auto parsedId = parseUnsigned(workspaces[i].id);
 19-    std::optional<std::size_t> parsedIndex = parseLeadingNumber(workspaces[i].id);
 20-    if (!parsedIndex.has_value()) {
 21-      parsedIndex = parseLeadingNumber(workspaces[i].name);
 22+  for (std::size_t i = 0; i < ws.size(); ++i) {
 23+    auto pid = parseUnsigned(ws[i].id);
 24+    auto pidx = parseLeadingNumber(ws[i].id);
 25+    if (!pidx.has_value()) {
 26+      pidx = parseLeadingNumber(ws[i].name);
 27     }
 28
 29-    auto pickCandidate = [&](auto&& predicate) -> const WorkspaceState* {
 30-      for (const auto* candidate : candidates) {
 31-        if (used.contains(candidate->id) || !predicate(*candidate)) {
 32+    auto pick = [&](auto pred) -> WsState const * {
 33+      for (auto *c : cand) {
 34+        if (used.contains(c->id) || !pred(*c)) {
 35           continue;
 36         }
 37-        used.emplace(candidate->id, true);
 38-        return candidate;
 39+        used.emplace(c->id, true);
 40+        return c;
 41       }
 42       return nullptr;
 43     };
 44
 45-    if (parsedId.has_value()) {
 46-      matches[i] = pickCandidate([&](const WorkspaceState& candidate) { return candidate.id == *parsedId; });
 47+    if (pid.has_value()) {
 48+      matches[i] = pick([&](WsState const &c) { return c.id == *pid; });
 49     }
 50-    if (matches[i] == nullptr && !workspaces[i].name.empty()) {
 51-      matches[i] = pickCandidate([&](const WorkspaceState& candidate) { return candidate.name == workspaces[i].name; });
 52+    if (matches[i] == nullptr && !ws[i].name.empty()) {
 53+      matches[i] = pick([&](WsState const &c) { return c.name == ws[i].name; });
 54     }
 55-    if (matches[i] == nullptr && parsedIndex.has_value()) {
 56-      matches[i] = pickCandidate([&](const WorkspaceState& candidate) {
 57-        return static_cast<std::size_t>(candidate.idx) == *parsedIndex;
 58+    if (matches[i] == nullptr && pidx.has_value()) {
 59+      matches[i] = pick([&](WsState const &c) {
 60+        return static_cast<std::size_t>(c.idx) == *pidx;
 61       });
 62     }
 63   }
 64
 65-  if (!outputName.empty()) {
 66-    std::size_t nextCandidate = 0;
 67-    for (auto& match : matches) {
 68-      if (match != nullptr) {
 69+  if (!outName.empty()) {
 70+    std::size_t next = 0;
 71+    for (std::size_t i = 0; i < matches.size(); ++i) {
 72+      if (matches[i] != nullptr) {
 73         continue;
 74       }
 75-      while (nextCandidate < candidates.size() && used.contains(candidates[nextCandidate]->id)) {
 76-        ++nextCandidate;
 77+      while (next < cand.size() && used.contains(cand[next]->id)) {
 78+        ++next;
 79       }
 80-      if (nextCandidate >= candidates.size()) {
 81+      if (next >= cand.size()) {
 82         break;
 83       }
 84-      match = candidates[nextCandidate];
 85-      used.emplace(candidates[nextCandidate]->id, true);
 86-      ++nextCandidate;
 87+      matches[i] = cand[next];
 88+      used.emplace(cand[next]->id, true);
 89+      ++next;
 90     }
 91   }
 92
 93-  for (std::size_t i = 0; i < workspaces.size(); ++i) {
 94+  for (std::size_t i = 0; i < ws.size(); ++i) {
 95     if (matches[i] != nullptr) {
 96       if (matches[i]->idx > 0) {
 97-        workspaces[i].index = matches[i]->idx;
 98+        ws[i].index = matches[i]->idx;
 99       }
100-      workspaces[i].occupied = m_occupancy.contains(matches[i]->id) && m_occupancy.at(matches[i]->id) > 0;
101+      ws[i].occupied = m_occupancy.contains(matches[i]->id) &&
102+                       m_occupancy.at(matches[i]->id) > 0;
103     } else {
104-      workspaces[i].index = 0;
105-      workspaces[i].occupied = false;
106+      ws[i].index = 0;
107+      ws[i].occupied = false;
108     }
109   }
110 }
 1@@ -1,21 +1,20 @@
 2-bool NiriWorkspaceBackend::handleWindowLayoutsChanged(const nlohmann::json& payload) {
 3-  const auto* changes = arrayPayload(payload, "changes");
 4+bool NiriWorkspaceManager::handleWindowLayout(nlohmann::json const &p) {
 5+  auto *changes = asArray(p, "changes");
 6   if (changes == nullptr) {
 7     return false;
 8   }
 9
10   bool changed = false;
11-  for (const auto& item : *changes) {
12+  for (auto const &item : *changes) {
13     if (!item.is_array() || item.size() < 2) {
14       continue;
15     }
16-
17-    const auto idOpt = jsonUnsigned(item[0]);
18+    auto idOpt = jsonU64(item[0]);
19     if (!idOpt.has_value()) {
20       continue;
21     }
22-    const std::uint64_t id = *idOpt;
23-    const auto& layout = item[1];
24+    std::uint64_t id = *idOpt;
25+    auto const &layout = item[1];
26
27     auto it = m_windows.find(id);
28     if (it == m_windows.end()) {
29@@ -23,25 +22,20 @@
30     }
31
32     if (layout.contains("pos_in_scrolling_layout")) {
33-      const auto& pos = layout["pos_in_scrolling_layout"];
34+      auto const &pos = layout["pos_in_scrolling_layout"];
35       if (pos.is_array() && pos.size() >= 2) {
36-        const auto xOpt = jsonInt32(pos[0]);
37-        const auto yOpt = jsonInt32(pos[1]);
38-        if (!xOpt.has_value() || !yOpt.has_value()) {
39+        auto xo = jsonI32(pos[0]);
40+        auto yo = jsonI32(pos[1]);
41+        if (!xo.has_value() || !yo.has_value()) {
42           continue;
43         }
44-        const std::int32_t x = *xOpt;
45-        const std::int32_t y = *yOpt;
46-        if (it->second.x != x || it->second.y != y) {
47-          it->second.x = x;
48-          it->second.y = y;
49+        if (it->second.x != *xo || it->second.y != *yo) {
50+          it->second.x = *xo;
51+          it->second.y = *yo;
52           changed = true;
53         }
54       }
55     }
56   }
57-
58-  // Layout positions are useful when a taskbar asks for ordering, but they do
59-  // not affect workspace occupancy or the normal workspace indicators.
60   return changed;
61 }  

Personally this whole subject has been gnawing on me, I knew what Matt was doing but everyone else didn’t want to bother making a fuss about it or didn’t think it was that big of a deal so I’m writing this to finally get this out of my system.

I personally don’t really know Matt and only interacted with him a handful of times (which were not exactly positive) so I cannot judge his character however I consider trying to pass of other peoples open source work, especially as a content creator in that space, to be utterly despicable and a violation of the values behind open source.