Skip to content

Commit 84fbe6d

Browse files
authored
Merge pull request #2770 from greenm01/triad-native-ipc-v5
Add Triad compositor backend
2 parents 1233c66 + 3ba38f5 commit 84fbe6d

17 files changed

Lines changed: 1448 additions & 4 deletions

meson.build

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,10 @@ _noctalia_sources = files(
654654
'src/compositors/niri/niri_output_backend.cpp',
655655
'src/compositors/niri/niri_keyboard_backend.cpp',
656656
'src/compositors/niri/niri_workspace_backend.cpp',
657+
'src/compositors/triad/triad_runtime.cpp',
658+
'src/compositors/triad/triad_output_backend.cpp',
659+
'src/compositors/triad/triad_keyboard_backend.cpp',
660+
'src/compositors/triad/triad_workspace_backend.cpp',
657661
'src/compositors/sway/sway_runtime.cpp',
658662
'src/compositors/sway/sway_keyboard_backend.cpp',
659663
'src/compositors/sway/sway_output_backend.cpp',

src/compositors/compositor_detect.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ namespace compositors {
3030
if (const char* v = std::getenv("LABWC_PID"); v != nullptr && v[0] != '\0') {
3131
return CompositorKind::Labwc;
3232
}
33+
if (const char* v = std::getenv("TRIAD_SOCKET"); v != nullptr && v[0] != '\0') {
34+
return CompositorKind::Triad;
35+
}
36+
if (const char* v = std::getenv("RIVER_WM"); v != nullptr && StringUtils::containsInsensitive(v, "triad")) {
37+
return CompositorKind::Triad;
38+
}
3339
if (const char* v = std::getenv("NIRI_SOCKET"); v != nullptr && v[0] != '\0') {
3440
return CompositorKind::Niri;
3541
}
@@ -42,6 +48,9 @@ namespace compositors {
4248

4349
// Fall back to the desktop env hint (covers dwl-style compositors that don't expose a socket var).
4450
const std::string hint = buildEnvHint();
51+
if (StringUtils::containsInsensitive(hint, "triad")) {
52+
return CompositorKind::Triad;
53+
}
4554
if (StringUtils::containsInsensitive(hint, "niri")) {
4655
return CompositorKind::Niri;
4756
}
@@ -69,6 +78,8 @@ namespace compositors {
6978

7079
std::string_view name(CompositorKind kind) {
7180
switch (kind) {
81+
case CompositorKind::Triad:
82+
return "Triad";
7283
case CompositorKind::Niri:
7384
return "Niri";
7485
case CompositorKind::Hyprland:

src/compositors/compositor_detect.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace compositors {
1212
Sway = 3,
1313
Mango = 4, // dwl-derived (mango, dwl)
1414
Labwc = 5,
15+
Triad = 6,
1516
};
1617

1718
// Detected once per process from env vars. Cached after the first call.
@@ -28,5 +29,6 @@ namespace compositors {
2829
[[nodiscard]] inline bool isSway() { return detect() == CompositorKind::Sway; }
2930
[[nodiscard]] inline bool isMango() { return detect() == CompositorKind::Mango; }
3031
[[nodiscard]] inline bool isLabwc() { return detect() == CompositorKind::Labwc; }
32+
[[nodiscard]] inline bool isTriad() { return detect() == CompositorKind::Triad; }
3133

3234
} // namespace compositors

src/compositors/compositor_platform.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
#include "compositors/niri/niri_workspace_backend.h"
1616
#include "compositors/sway/sway_keyboard_backend.h"
1717
#include "compositors/sway/sway_output_backend.h"
18+
#include "compositors/triad/triad_keyboard_backend.h"
19+
#include "compositors/triad/triad_output_backend.h"
20+
#include "compositors/triad/triad_runtime.h"
21+
#include "compositors/triad/triad_workspace_backend.h"
1822
#include "wayland/wayland_connection.h"
1923
#include "wayland/wayland_workspaces.h"
2024

@@ -191,6 +195,11 @@ namespace {
191195
(void)wayland;
192196
return compositors::sway::setOutputPower(runtime, on);
193197
});
198+
case compositors::CompositorKind::Triad:
199+
return std::make_unique<LambdaOutputPowerBackend>(
200+
[&runtime = runtimeRegistry.triad()](WaylandConnection& /*wayland*/, bool on) {
201+
return compositors::triad::setOutputPower(runtime, on);
202+
});
194203
case compositors::CompositorKind::Mango:
195204
return std::make_unique<LambdaOutputPowerBackend>(&setMangoOutputPower, true);
196205
case compositors::CompositorKind::Labwc:
@@ -209,6 +218,8 @@ namespace {
209218
return std::make_unique<FocusedOutputAdapter<NiriOutputBackend>>(runtimeRegistry.niri());
210219
case compositors::CompositorKind::Sway:
211220
return std::make_unique<FocusedOutputAdapter<SwayOutputBackend>>(runtimeRegistry.sway());
221+
case compositors::CompositorKind::Triad:
222+
return std::make_unique<FocusedOutputAdapter<TriadOutputBackend>>(runtimeRegistry.triad());
212223
case compositors::CompositorKind::Labwc:
213224
case compositors::CompositorKind::Mango:
214225
case compositors::CompositorKind::Unknown:
@@ -220,6 +231,8 @@ namespace {
220231
[[nodiscard]] std::unique_ptr<compositors::WorkspaceMetadataBackend>
221232
createWorkspaceMetadataBackend(compositors::CompositorRuntimeRegistry& runtimeRegistry) {
222233
switch (compositors::detect()) {
234+
case compositors::CompositorKind::Triad:
235+
return std::make_unique<TriadWorkspaceBackend>(runtimeRegistry.triad());
223236
case compositors::CompositorKind::Niri:
224237
return std::make_unique<NiriWorkspaceBackend>(runtimeRegistry.niri());
225238
case compositors::CompositorKind::Hyprland:
@@ -243,6 +256,8 @@ namespace {
243256
return std::make_unique<KeyboardLayoutBackendAdapter<MangoKeyboardBackend>>();
244257
case compositors::CompositorKind::Sway:
245258
return std::make_unique<KeyboardLayoutBackendAdapter<SwayKeyboardBackend>>(runtimeRegistry.sway());
259+
case compositors::CompositorKind::Triad:
260+
return std::make_unique<KeyboardLayoutBackendAdapter<TriadKeyboardBackend>>(runtimeRegistry.triad());
246261
case compositors::CompositorKind::Labwc:
247262
case compositors::CompositorKind::Unknown:
248263
break;

src/compositors/compositor_runtime.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
#include "compositors/hyprland/hyprland_runtime.h"
44
#include "compositors/niri/niri_runtime.h"
55
#include "compositors/sway/sway_runtime.h"
6+
#include "compositors/triad/triad_runtime.h"
67

78
namespace compositors {
89

910
CompositorRuntimeRegistry::CompositorRuntimeRegistry()
1011
: m_hyprland(std::make_unique<hyprland::HyprlandRuntime>()), m_niri(std::make_unique<niri::NiriRuntime>()),
11-
m_sway(std::make_unique<sway::SwayRuntime>()) {}
12+
m_sway(std::make_unique<sway::SwayRuntime>()), m_triad(std::make_unique<triad::TriadRuntime>()) {}
1213

1314
CompositorRuntimeRegistry::~CompositorRuntimeRegistry() = default;
1415

@@ -24,4 +25,8 @@ namespace compositors {
2425

2526
const sway::SwayRuntime& CompositorRuntimeRegistry::sway() const noexcept { return *m_sway; }
2627

28+
triad::TriadRuntime& CompositorRuntimeRegistry::triad() noexcept { return *m_triad; }
29+
30+
const triad::TriadRuntime& CompositorRuntimeRegistry::triad() const noexcept { return *m_triad; }
31+
2732
} // namespace compositors

src/compositors/compositor_runtime.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ namespace compositors {
1616
class SwayRuntime;
1717
} // namespace sway
1818

19+
namespace triad {
20+
class TriadRuntime;
21+
} // namespace triad
22+
1923
class CompositorRuntimeRegistry {
2024
public:
2125
CompositorRuntimeRegistry();
@@ -30,11 +34,14 @@ namespace compositors {
3034
[[nodiscard]] const niri::NiriRuntime& niri() const noexcept;
3135
[[nodiscard]] sway::SwayRuntime& sway() noexcept;
3236
[[nodiscard]] const sway::SwayRuntime& sway() const noexcept;
37+
[[nodiscard]] triad::TriadRuntime& triad() noexcept;
38+
[[nodiscard]] const triad::TriadRuntime& triad() const noexcept;
3339

3440
private:
3541
std::unique_ptr<hyprland::HyprlandRuntime> m_hyprland;
3642
std::unique_ptr<niri::NiriRuntime> m_niri;
3743
std::unique_ptr<sway::SwayRuntime> m_sway;
44+
std::unique_ptr<triad::TriadRuntime> m_triad;
3845
};
3946

4047
} // namespace compositors
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#include "compositors/triad/triad_keyboard_backend.h"
2+
3+
#include "compositors/triad/triad_runtime.h"
4+
5+
#include <json.hpp>
6+
7+
namespace {
8+
9+
[[nodiscard]] const nlohmann::json* triadState(const nlohmann::json& response) {
10+
if (!response.is_object()) {
11+
return nullptr;
12+
}
13+
const auto triadIt = response.find("triad");
14+
if (triadIt == response.end() || !triadIt->is_object()) {
15+
return nullptr;
16+
}
17+
const auto stateIt = triadIt->find("state");
18+
return stateIt != triadIt->end() && stateIt->is_object() ? &(*stateIt) : nullptr;
19+
}
20+
21+
[[nodiscard]] std::optional<KeyboardLayoutState> parseLayoutState(const nlohmann::json& response) {
22+
const auto* stateJson = triadState(response);
23+
if (stateJson == nullptr) {
24+
return std::nullopt;
25+
}
26+
27+
const auto namesIt = stateJson->find("keyboard_layouts");
28+
const auto currentIt = stateJson->find("current_keyboard_layout_idx");
29+
if (namesIt == stateJson->end() || !namesIt->is_array() || currentIt == stateJson->end() ||
30+
!currentIt->is_number_integer()) {
31+
return std::nullopt;
32+
}
33+
34+
KeyboardLayoutState state;
35+
state.currentIndex = currentIt->get<int>();
36+
state.names.reserve(namesIt->size());
37+
for (const auto& entry : *namesIt) {
38+
if (!entry.is_string()) {
39+
return std::nullopt;
40+
}
41+
state.names.push_back(entry.get<std::string>());
42+
}
43+
44+
if (state.currentIndex < 0 || state.currentIndex >= static_cast<int>(state.names.size())) {
45+
return std::nullopt;
46+
}
47+
return state;
48+
}
49+
50+
} // namespace
51+
52+
TriadKeyboardBackend::TriadKeyboardBackend(compositors::triad::TriadRuntime& runtime) : m_runtime(runtime) {}
53+
54+
bool TriadKeyboardBackend::isAvailable() const noexcept { return m_runtime.available(); }
55+
56+
bool TriadKeyboardBackend::cycleLayout() const {
57+
return isAvailable() && m_runtime.requestAction("switch-keyboard-layout", nlohmann::json{{"layout", "next"}});
58+
}
59+
60+
std::optional<KeyboardLayoutState> TriadKeyboardBackend::layoutState() const {
61+
if (!isAvailable()) {
62+
return std::nullopt;
63+
}
64+
const auto response = m_runtime.requestJson("state");
65+
return response.has_value() ? parseLayoutState(*response) : std::nullopt;
66+
}
67+
68+
std::optional<std::string> TriadKeyboardBackend::currentLayoutName() const {
69+
const auto state = layoutState();
70+
if (!state.has_value() || state->currentIndex < 0 || state->currentIndex >= static_cast<int>(state->names.size())) {
71+
return std::nullopt;
72+
}
73+
return state->names[static_cast<std::size_t>(state->currentIndex)];
74+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#pragma once
2+
3+
#include "compositors/keyboard_backend.h"
4+
5+
#include <optional>
6+
#include <string>
7+
8+
namespace compositors::triad {
9+
class TriadRuntime;
10+
} // namespace compositors::triad
11+
12+
class TriadKeyboardBackend {
13+
public:
14+
explicit TriadKeyboardBackend(compositors::triad::TriadRuntime& runtime);
15+
16+
[[nodiscard]] bool isAvailable() const noexcept;
17+
[[nodiscard]] bool cycleLayout() const;
18+
[[nodiscard]] std::optional<KeyboardLayoutState> layoutState() const;
19+
[[nodiscard]] std::optional<std::string> currentLayoutName() const;
20+
21+
private:
22+
compositors::triad::TriadRuntime& m_runtime;
23+
};
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#include "compositors/triad/triad_output_backend.h"
2+
3+
#include "compositors/triad/triad_runtime.h"
4+
#include "core/log.h"
5+
6+
#include <json.hpp>
7+
#include <optional>
8+
#include <string>
9+
10+
namespace {
11+
12+
constexpr Logger kLog("triad_output");
13+
14+
[[nodiscard]] const nlohmann::json* triadState(const nlohmann::json& response) {
15+
if (!response.is_object()) {
16+
return nullptr;
17+
}
18+
const auto triadIt = response.find("triad");
19+
if (triadIt == response.end() || !triadIt->is_object()) {
20+
return nullptr;
21+
}
22+
const auto stateIt = triadIt->find("state");
23+
return stateIt != triadIt->end() && stateIt->is_object() ? &(*stateIt) : nullptr;
24+
}
25+
26+
[[nodiscard]] std::optional<std::uint64_t> jsonUnsigned(const nlohmann::json& json) {
27+
if (json.is_number_unsigned()) {
28+
return json.get<std::uint64_t>();
29+
}
30+
if (json.is_number_integer()) {
31+
const auto value = json.get<std::int64_t>();
32+
if (value >= 0) {
33+
return static_cast<std::uint64_t>(value);
34+
}
35+
}
36+
return std::nullopt;
37+
}
38+
39+
[[nodiscard]] std::optional<std::string> activeOutputFromLayoutState(const nlohmann::json& state) {
40+
const auto activeIt = state.find("active_workspace_idx");
41+
const auto workspacesIt = state.find("workspaces");
42+
if (activeIt == state.end() || workspacesIt == state.end() || !workspacesIt->is_array()) {
43+
return std::nullopt;
44+
}
45+
const auto activeIdx = jsonUnsigned(*activeIt);
46+
if (!activeIdx.has_value()) {
47+
return std::nullopt;
48+
}
49+
for (const auto& workspace : *workspacesIt) {
50+
if (!workspace.is_object()) {
51+
continue;
52+
}
53+
const auto idxIt = workspace.find("workspace_idx");
54+
const auto outputIt = workspace.find("output");
55+
if (idxIt == workspace.end() || outputIt == workspace.end() || !outputIt->is_string()) {
56+
continue;
57+
}
58+
const auto workspaceIdx = jsonUnsigned(*idxIt);
59+
if (workspaceIdx.has_value() && *workspaceIdx == *activeIdx) {
60+
return outputIt->get<std::string>();
61+
}
62+
}
63+
return std::nullopt;
64+
}
65+
66+
} // namespace
67+
68+
TriadOutputBackend::TriadOutputBackend(compositors::triad::TriadRuntime& runtime) : m_runtime(runtime) {}
69+
70+
std::optional<std::string> TriadOutputBackend::focusedOutputName() const {
71+
if (auto response = m_runtime.requestJson("state"); response.has_value()) {
72+
if (const auto* state = triadState(*response); state != nullptr) {
73+
if (auto output = activeOutputFromLayoutState(*state); output.has_value() && !output->empty()) {
74+
return output;
75+
}
76+
const auto layoutIt = state->find("layout");
77+
if (layoutIt != state->end() && layoutIt->is_object()) {
78+
if (auto output = activeOutputFromLayoutState(*layoutIt); output.has_value() && !output->empty()) {
79+
return output;
80+
}
81+
}
82+
}
83+
}
84+
85+
kLog.debug("failed to resolve focused output from triad IPC");
86+
return std::nullopt;
87+
}
88+
89+
namespace compositors::triad {
90+
91+
bool setOutputPower(TriadRuntime& runtime, bool on) {
92+
return runtime.requestAction(on ? "power-on-monitors" : "power-off-monitors");
93+
}
94+
95+
} // namespace compositors::triad
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#pragma once
2+
3+
#include <optional>
4+
#include <string>
5+
6+
namespace compositors::triad {
7+
class TriadRuntime;
8+
} // namespace compositors::triad
9+
10+
class TriadOutputBackend {
11+
public:
12+
explicit TriadOutputBackend(compositors::triad::TriadRuntime& runtime);
13+
14+
[[nodiscard]] std::optional<std::string> focusedOutputName() const;
15+
16+
private:
17+
compositors::triad::TriadRuntime& m_runtime;
18+
};
19+
20+
namespace compositors::triad {
21+
22+
[[nodiscard]] bool setOutputPower(TriadRuntime& runtime, bool on);
23+
24+
} // namespace compositors::triad

0 commit comments

Comments
 (0)