From 8b81f41e52b55835aaaa7e4962af23a00188cbdc Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Mon, 27 Feb 2023 12:32:38 +0000 Subject: Plugin System (#1590) --------- Co-authored-by: Mihai Fufezan --- .github/workflows/ci.yaml | 4 +- .gitignore | 1 + .gitmodules | 3 + CMakeLists.txt | 4 +- Makefile | 13 ++ example/examplePlugin/Makefile | 8 + example/examplePlugin/customDecoration.cpp | 74 ++++++++ example/examplePlugin/customDecoration.hpp | 29 +++ example/examplePlugin/customLayout.cpp | 80 +++++++++ example/examplePlugin/customLayout.hpp | 32 ++++ example/examplePlugin/globals.hpp | 5 + example/examplePlugin/main.cpp | 92 ++++++++++ flake.nix | 3 + hyprctl/main.cpp | 3 + meson.build | 4 + nix/default.nix | 2 + nix/meson-build.patch | 44 +++-- nix/udis86.nix | 32 ++++ src/Compositor.cpp | 15 ++ src/Compositor.hpp | 2 + src/config/ConfigManager.cpp | 41 ++++- src/config/ConfigManager.hpp | 43 +++-- src/debug/CrashReporter.cpp | 13 ++ src/debug/HyprCtl.cpp | 50 ++++++ src/debug/HyprCtl.hpp | 6 +- src/debug/HyprNotificationOverlay.cpp | 155 ++++++++++++++++ src/debug/HyprNotificationOverlay.hpp | 40 +++++ src/defines.hpp | 2 +- src/events/Monitors.cpp | 13 +- src/helpers/Color.hpp | 4 +- src/helpers/Monitor.hpp | 3 + src/managers/HookSystemManager.cpp | 57 ++++-- src/managers/HookSystemManager.hpp | 27 ++- src/managers/KeybindManager.hpp | 1 + src/managers/LayoutManager.cpp | 58 ++++-- src/managers/LayoutManager.hpp | 16 +- src/meson.build | 1 + src/plugins/HookSystem.cpp | 151 ++++++++++++++++ src/plugins/HookSystem.hpp | 48 +++++ src/plugins/PluginAPI.cpp | 193 ++++++++++++++++++++ src/plugins/PluginAPI.hpp | 217 +++++++++++++++++++++++ src/plugins/PluginSystem.cpp | 139 +++++++++++++++ src/plugins/PluginSystem.hpp | 44 +++++ src/render/decorations/IHyprWindowDecoration.hpp | 3 +- subprojects/udis86 | 1 + 45 files changed, 1691 insertions(+), 85 deletions(-) create mode 100644 example/examplePlugin/Makefile create mode 100644 example/examplePlugin/customDecoration.cpp create mode 100644 example/examplePlugin/customDecoration.hpp create mode 100644 example/examplePlugin/customLayout.cpp create mode 100644 example/examplePlugin/customLayout.hpp create mode 100644 example/examplePlugin/globals.hpp create mode 100644 example/examplePlugin/main.cpp create mode 100644 nix/udis86.nix create mode 100644 src/debug/HyprNotificationOverlay.cpp create mode 100644 src/debug/HyprNotificationOverlay.hpp create mode 100644 src/plugins/HookSystem.cpp create mode 100644 src/plugins/HookSystem.hpp create mode 100644 src/plugins/PluginAPI.cpp create mode 100644 src/plugins/PluginAPI.hpp create mode 100644 src/plugins/PluginSystem.cpp create mode 100644 src/plugins/PluginSystem.hpp create mode 160000 subprojects/udis86 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 68154929..d5b9d368 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: run: | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf pacman --noconfirm --noprogressbar -Syyu - pacman --noconfirm --noprogressbar -Sy glslang libepoxy libfontenc libxcvt libxfont2 libxkbfile vulkan-headers vulkan-validation-layers xcb-util-errors xcb-util-renderutil xcb-util-wm xorg-fonts-encodings xorg-server-common xorg-setxkbmap xorg-xkbcomp xorg-xwayland git cmake go clang lld libc++ pkgconf meson ninja wayland wayland-protocols libinput libxkbcommon pixman glm libdrm libglvnd cairo pango systemd scdoc base-devel seatd + pacman --noconfirm --noprogressbar -Sy glslang libepoxy libfontenc libxcvt libxfont2 libxkbfile vulkan-headers vulkan-validation-layers xcb-util-errors xcb-util-renderutil xcb-util-wm xorg-fonts-encodings xorg-server-common xorg-setxkbmap xorg-xkbcomp xorg-xwayland git cmake go clang lld libc++ pkgconf meson ninja wayland wayland-protocols libinput libxkbcommon pixman glm libdrm libglvnd cairo pango systemd scdoc base-devel seatd python - name: Set up user run: | useradd -m githubuser @@ -62,7 +62,7 @@ jobs: run: | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf pacman --noconfirm --noprogressbar -Syyu - pacman --noconfirm --noprogressbar -Sy glslang libepoxy libfontenc libxcvt libxfont2 libxkbfile vulkan-headers vulkan-validation-layers git go clang lld libc++ pkgconf meson ninja wayland wayland-protocols libinput libxkbcommon pixman glm libdrm libglvnd cairo pango systemd scdoc base-devel seatd cmake jq + pacman --noconfirm --noprogressbar -Sy glslang libepoxy libfontenc libxcvt libxfont2 libxkbfile vulkan-headers vulkan-validation-layers git go clang lld libc++ pkgconf meson ninja wayland wayland-protocols libinput libxkbcommon pixman glm libdrm libglvnd cairo pango systemd scdoc base-devel seatd cmake jq python - name: Checkout Hyprland uses: actions/checkout@v3 with: diff --git a/.gitignore b/.gitignore index 95a1b6f0..96fd6c87 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ result* *-protocol.c *-protocol.h .ccls-cache +*.so hyprctl/hyprctl diff --git a/.gitmodules b/.gitmodules index ed443f60..26dbceeb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "subprojects/hyprland-protocols"] path = subprojects/hyprland-protocols url = https://github.com/hyprwm/hyprland-protocols +[submodule "subprojects/udis86"] + path = subprojects/udis86 + url = https://github.com/canihavesomecoffee/udis86 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6eec94b0..a5ea06a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,9 +52,10 @@ ENDIF(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) include_directories(. PRIVATE "subprojects/wlroots/include/") include_directories(. PRIVATE "subprojects/wlroots/build/include/") +include_directories(. PRIVATE "subprojects/udis86/") set(CMAKE_CXX_STANDARD 23) add_compile_options(-DWLR_USE_UNSTABLE) -add_compile_options(-Wall -Wextra -Wno-unused-parameter -Wno-unused-value -Wno-missing-field-initializers -Wno-narrowing) +add_compile_options(-Wall -Wextra -Wno-unused-parameter -Wno-unused-value -Wno-missing-field-initializers -Wno-narrowing -Wno-pointer-arith) ADD_LINK_OPTIONS( -rdynamic ) SET(CMAKE_ENABLE_EXPORTS TRUE) @@ -139,4 +140,5 @@ target_link_libraries(Hyprland ${CMAKE_SOURCE_DIR}/wlr-foreign-toplevel-management-unstable-v1-protocol.o ${CMAKE_SOURCE_DIR}/hyprland-toplevel-export-v1-protocol.o ${CMAKE_SOURCE_DIR}/fractional-scale-v1-protocol.o + ${CMAKE_SOURCE_DIR}/subprojects/udis86/build/libudis86/liblibudis86.a ) diff --git a/Makefile b/Makefile index 47a27d00..6b375912 100644 --- a/Makefile +++ b/Makefile @@ -157,6 +157,7 @@ all: make clear make fixwlr cd ./subprojects/wlroots && meson build/ --buildtype=release && ninja -C build/ && cp ./build/libwlroots.so.12032 /usr/lib/ && cd ../.. + cd subprojects/udis86 && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -H./ -B./build -G Ninja && cmake --build ./build --config Release --target all -j$(shell nproc) make protocols make release cd hyprctl && make all && cd .. @@ -211,6 +212,18 @@ config: cd subprojects/wlroots && ninja -C build/ install + cd subprojects/udis86 && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -H./ -B./build -G Ninja && cmake --build ./build --config Release --target all -j$(shell nproc) + +pluginenv: + make protocols + + cd subprojects/udis86 && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -H./ -B./build -G Ninja && cmake --build ./build --config Release --target all -j$(shell nproc) + + make fixwlr + + cd subprojects/wlroots && meson ./build --prefix=/usr --buildtype=release -Dwerror=false -Dexamples=false + cd subprojects/wlroots && ninja -C build/ + configdebug: make protocols diff --git a/example/examplePlugin/Makefile b/example/examplePlugin/Makefile new file mode 100644 index 00000000..3ccc2930 --- /dev/null +++ b/example/examplePlugin/Makefile @@ -0,0 +1,8 @@ +# compile with HYPRLAND_HEADERS= make all +# make sure that the path above is to the root hl repo directory, NOT src/ +# and that you have ran `make protocols` in the hl dir. + +all: + g++ -shared -fPIC --no-gnu-unique main.cpp customLayout.cpp customDecoration.cpp -o examplePlugin.so -g -I "/usr/include/pixman-1" -I "/usr/include/libdrm" -I "${HYPRLAND_HEADERS}" -std=c++23 +clean: + rm ./examplePlugin.so diff --git a/example/examplePlugin/customDecoration.cpp b/example/examplePlugin/customDecoration.cpp new file mode 100644 index 00000000..d336d4e8 --- /dev/null +++ b/example/examplePlugin/customDecoration.cpp @@ -0,0 +1,74 @@ +#include "customDecoration.hpp" +#include "../../src/Window.hpp" +#include "../../src/Compositor.hpp" +#include "globals.hpp" + +CCustomDecoration::CCustomDecoration(CWindow* pWindow) { + m_pWindow = pWindow; + m_vLastWindowPos = pWindow->m_vRealPosition.vec(); + m_vLastWindowSize = pWindow->m_vRealSize.vec(); +} + +CCustomDecoration::~CCustomDecoration() { + damageEntire(); +} + +SWindowDecorationExtents CCustomDecoration::getWindowDecorationExtents() { + return m_seExtents; +} + +void CCustomDecoration::draw(CMonitor* pMonitor, float a, const Vector2D& offset) { + if (!g_pCompositor->windowValidMapped(m_pWindow)) + return; + + if (!m_pWindow->m_sSpecialRenderData.decorate) + return; + + static auto* const PCOLOR = &HyprlandAPI::getConfigValue(PHANDLE, "plugin:example:border_color")->intValue; + static auto* const PROUNDING = &HyprlandAPI::getConfigValue(PHANDLE, "decoration:rounding")->intValue; + static auto* const PBORDERSIZE = &HyprlandAPI::getConfigValue(PHANDLE, "general:border_size")->intValue; + + const auto ROUNDING = !m_pWindow->m_sSpecialRenderData.rounding ? + 0 : + (m_pWindow->m_sAdditionalConfigData.rounding.toUnderlying() == -1 ? *PROUNDING : m_pWindow->m_sAdditionalConfigData.rounding.toUnderlying()); + + // draw the border + wlr_box fullBox = {(int)(m_vLastWindowPos.x - *PBORDERSIZE), (int)(m_vLastWindowPos.y - *PBORDERSIZE), (int)(m_vLastWindowSize.x + 2.0 * *PBORDERSIZE), + (int)(m_vLastWindowSize.y + 2.0 * *PBORDERSIZE)}; + + fullBox.x -= pMonitor->vecPosition.x; + fullBox.y -= pMonitor->vecPosition.y; + + m_seExtents = {{m_vLastWindowPos.x - fullBox.x - pMonitor->vecPosition.x + 2, m_vLastWindowPos.y - fullBox.y - pMonitor->vecPosition.y + 2}, + {fullBox.x + fullBox.width + pMonitor->vecPosition.x - m_vLastWindowPos.x - m_vLastWindowSize.x + 2, + fullBox.y + fullBox.height + pMonitor->vecPosition.y - m_vLastWindowPos.y - m_vLastWindowSize.y + 2}}; + + fullBox.x += offset.x; + fullBox.y += offset.y; + + if (fullBox.width < 1 || fullBox.height < 1) + return; // don't draw invisible shadows + + g_pHyprOpenGL->scissor((wlr_box*)nullptr); + + scaleBox(&fullBox, pMonitor->scale); + g_pHyprOpenGL->renderBorder(&fullBox, CColor(*PCOLOR), *PROUNDING * pMonitor->scale + *PBORDERSIZE * 2, a); +} + +eDecorationType CCustomDecoration::getDecorationType() { + return DECORATION_CUSTOM; +} + +void CCustomDecoration::updateWindow(CWindow* pWindow) { + + m_vLastWindowPos = pWindow->m_vRealPosition.vec(); + m_vLastWindowSize = pWindow->m_vRealSize.vec(); + + damageEntire(); +} + +void CCustomDecoration::damageEntire() { + wlr_box dm = {(int)(m_vLastWindowPos.x - m_seExtents.topLeft.x), (int)(m_vLastWindowPos.y - m_seExtents.topLeft.y), + (int)(m_vLastWindowSize.x + m_seExtents.topLeft.x + m_seExtents.bottomRight.x), (int)m_seExtents.topLeft.y}; + g_pHyprRenderer->damageBox(&dm); +} \ No newline at end of file diff --git a/example/examplePlugin/customDecoration.hpp b/example/examplePlugin/customDecoration.hpp new file mode 100644 index 00000000..a08b48bf --- /dev/null +++ b/example/examplePlugin/customDecoration.hpp @@ -0,0 +1,29 @@ +#pragma once + +#define WLR_USE_UNSTABLE + +#include "../../src/render/decorations/IHyprWindowDecoration.hpp" + +class CCustomDecoration : public IHyprWindowDecoration { + public: + CCustomDecoration(CWindow*); + virtual ~CCustomDecoration(); + + virtual SWindowDecorationExtents getWindowDecorationExtents(); + + virtual void draw(CMonitor*, float a, const Vector2D& offset); + + virtual eDecorationType getDecorationType(); + + virtual void updateWindow(CWindow*); + + virtual void damageEntire(); + + private: + SWindowDecorationExtents m_seExtents; + + CWindow* m_pWindow = nullptr; + + Vector2D m_vLastWindowPos; + Vector2D m_vLastWindowSize; +}; \ No newline at end of file diff --git a/example/examplePlugin/customLayout.cpp b/example/examplePlugin/customLayout.cpp new file mode 100644 index 00000000..fcc51394 --- /dev/null +++ b/example/examplePlugin/customLayout.cpp @@ -0,0 +1,80 @@ +#include "customLayout.hpp" +#include "../../src/Compositor.hpp" +#include "globals.hpp" + +void CHyprCustomLayout::onWindowCreatedTiling(CWindow* pWindow) { + const auto PMONITOR = g_pCompositor->getMonitorFromID(pWindow->m_iMonitorID); + const auto SIZE = PMONITOR->vecSize; + + // these are used for focus and move calculations, and are *required* to touch for moving focus to work properly. + pWindow->m_vPosition = Vector2D{(SIZE.x / 2.0) * (m_vWindowData.size() % 2), (SIZE.y / 2.0) * (int)(m_vWindowData.size() > 1)}; + pWindow->m_vSize = SIZE / 2.0; + + // this is the actual pos and size of the window (where it's rendered) + pWindow->m_vRealPosition = pWindow->m_vPosition + Vector2D{10, 10}; + pWindow->m_vRealSize = pWindow->m_vSize - Vector2D{20, 20}; + + const auto PDATA = &m_vWindowData.emplace_back(); + PDATA->pWindow = pWindow; +} + +void CHyprCustomLayout::onWindowRemovedTiling(CWindow* pWindow) { + std::erase_if(m_vWindowData, [&](const auto& other) { return other.pWindow == pWindow; }); +} + +bool CHyprCustomLayout::isWindowTiled(CWindow* pWindow) { + return std::find_if(m_vWindowData.begin(), m_vWindowData.end(), [&](const auto& other) { return other.pWindow == pWindow; }) != m_vWindowData.end(); +} + +void CHyprCustomLayout::recalculateMonitor(const int& eIdleInhibitMode) { + ; // empty +} + +void CHyprCustomLayout::recalculateWindow(CWindow* pWindow) { + ; // empty +} + +void CHyprCustomLayout::resizeActiveWindow(const Vector2D& delta, CWindow* pWindow) { + ; // empty +} + +void CHyprCustomLayout::fullscreenRequestForWindow(CWindow* pWindow, eFullscreenMode mode, bool on) { + ; // empty +} + +std::any CHyprCustomLayout::layoutMessage(SLayoutMessageHeader header, std::string content) { + return ""; +} + +SWindowRenderLayoutHints CHyprCustomLayout::requestRenderHints(CWindow* pWindow) { + return {}; +} + +void CHyprCustomLayout::switchWindows(CWindow* pWindowA, CWindow* pWindowB) { + ; // empty +} + +void CHyprCustomLayout::alterSplitRatio(CWindow* pWindow, float delta, bool exact) { + ; // empty +} + +std::string CHyprCustomLayout::getLayoutName() { + return "custom"; +} + +void CHyprCustomLayout::replaceWindowDataWith(CWindow* from, CWindow* to) { + ; // empty +} + +void CHyprCustomLayout::onEnable() { + for (auto& w : g_pCompositor->m_vWindows) { + if (w->isHidden() || !w->m_bIsMapped || w->m_bFadingOut || w->m_bIsFloating) + continue; + + onWindowCreatedTiling(w.get()); + } +} + +void CHyprCustomLayout::onDisable() { + m_vWindowData.clear(); +} \ No newline at end of file diff --git a/example/examplePlugin/customLayout.hpp b/example/examplePlugin/customLayout.hpp new file mode 100644 index 00000000..974882c2 --- /dev/null +++ b/example/examplePlugin/customLayout.hpp @@ -0,0 +1,32 @@ +#pragma once + +#define WLR_USE_UNSTABLE + +#include "../../src/layout/IHyprLayout.hpp" + +struct SWindowData { + CWindow* pWindow = nullptr; +}; + +class CHyprCustomLayout : public IHyprLayout { + public: + virtual void onWindowCreatedTiling(CWindow*); + virtual void onWindowRemovedTiling(CWindow*); + virtual bool isWindowTiled(CWindow*); + virtual void recalculateMonitor(const int&); + virtual void recalculateWindow(CWindow*); + virtual void resizeActiveWindow(const Vector2D&, CWindow* pWindow = nullptr); + virtual void fullscreenRequestForWindow(CWindow*, eFullscreenMode, bool); + virtual std::any layoutMessage(SLayoutMessageHeader, std::string); + virtual SWindowRenderLayoutHints requestRenderHints(CWindow*); + virtual void switchWindows(CWindow*, CWindow*); + virtual void alterSplitRatio(CWindow*, float, bool); + virtual std::string getLayoutName(); + virtual void replaceWindowDataWith(CWindow* from, CWindow* to); + + virtual void onEnable(); + virtual void onDisable(); + + private: + std::vector m_vWindowData; +}; \ No newline at end of file diff --git a/example/examplePlugin/globals.hpp b/example/examplePlugin/globals.hpp new file mode 100644 index 00000000..37e8363b --- /dev/null +++ b/example/examplePlugin/globals.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +inline HANDLE PHANDLE = nullptr; \ No newline at end of file diff --git a/example/examplePlugin/main.cpp b/example/examplePlugin/main.cpp new file mode 100644 index 00000000..49364f59 --- /dev/null +++ b/example/examplePlugin/main.cpp @@ -0,0 +1,92 @@ +#define WLR_USE_UNSTABLE + +#include "globals.hpp" + +#include +#include +#include "customLayout.hpp" +#include "customDecoration.hpp" + +#include +#include + +// Methods +inline std::unique_ptr g_pCustomLayout; +inline CFunctionHook* g_pFocusHook = nullptr; +inline CFunctionHook* g_pMotionHook = nullptr; +inline CFunctionHook* g_pMouseDownHook = nullptr; +typedef void (*origFocusWindow)(void*, CWindow*, wlr_surface*); +typedef void (*origMotion)(wlr_seat*, uint32_t, double, double); +typedef void (*origMouseDownNormal)(void*, wlr_pointer_button_event*); + +// Do NOT change this function. +APICALL EXPORT std::string PLUGIN_API_VERSION() { + return HYPRLAND_API_VERSION; +} + +static void onActiveWindowChange(void* self, std::any data) { + try { + auto* const PWINDOW = std::any_cast(data); + + HyprlandAPI::addNotification(PHANDLE, "[ExamplePlugin] Active window: " + (PWINDOW ? PWINDOW->m_szTitle : "None"), CColor{0.f, 0.5f, 1.f, 1.f}, 5000); + } catch (std::bad_any_cast& e) { HyprlandAPI::addNotification(PHANDLE, "[ExamplePlugin] Active window: None", CColor{0.f, 0.5f, 1.f, 1.f}, 5000); } +} + +static void onNewWindow(void* self, std::any data) { + auto* const PWINDOW = std::any_cast(data); + + HyprlandAPI::addWindowDecoration(PHANDLE, PWINDOW, new CCustomDecoration(PWINDOW)); +} + +void hkFocusWindow(void* thisptr, CWindow* pWindow, wlr_surface* pSurface) { + // HyprlandAPI::addNotification(PHANDLE, getFormat("FocusWindow with %lx %lx", pWindow, pSurface), CColor{0.f, 1.f, 1.f, 1.f}, 5000); + (*(origFocusWindow)g_pFocusHook->m_pOriginal)(thisptr, pWindow, pSurface); +} + +void hkNotifyMotion(wlr_seat* wlr_seat, uint32_t time_msec, double sx, double sy) { + // HyprlandAPI::addNotification(PHANDLE, getFormat("NotifyMotion with %lf %lf", sx, sy), CColor{0.f, 1.f, 1.f, 1.f}, 5000); + (*(origMotion)g_pMotionHook->m_pOriginal)(wlr_seat, time_msec, sx, sy); +} + +void hkProcessMouseDownNormal(void* thisptr, wlr_pointer_button_event* e) { + // HyprlandAPI::addNotification(PHANDLE, "Mouse down normal!", CColor{0.8f, 0.2f, 0.5f, 1.0f}, 5000); + (*(origMouseDownNormal)g_pMouseDownHook->m_pOriginal)(thisptr, e); +} + +APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { + PHANDLE = handle; + + HyprlandAPI::addNotification(PHANDLE, "Hello World from an example plugin!", CColor{0.f, 1.f, 1.f, 1.f}, 5000); + + HyprlandAPI::registerCallbackDynamic(PHANDLE, "activeWindow", [&](void* self, std::any data) { onActiveWindowChange(self, data); }); + HyprlandAPI::registerCallbackDynamic(PHANDLE, "openWindow", [&](void* self, std::any data) { onNewWindow(self, data); }); + + g_pCustomLayout = std::make_unique(); + + HyprlandAPI::addLayout(PHANDLE, "custom", g_pCustomLayout.get()); + + HyprlandAPI::addConfigValue(PHANDLE, "plugin:example:border_color", SConfigValue{.intValue = configStringToInt("rgb(44ee44)")}); + + HyprlandAPI::addDispatcher(PHANDLE, "example", [](std::string arg) { HyprlandAPI::addNotification(PHANDLE, "Arg passed: " + arg, CColor{0.5f, 0.5f, 0.7f, 1.0f}, 5000); }); + + // Hook a public member + g_pFocusHook = HyprlandAPI::createFunctionHook(PHANDLE, (void*)&CCompositor::focusWindow, (void*)&hkFocusWindow); + // Hook a public non-member + g_pMotionHook = HyprlandAPI::createFunctionHook(PHANDLE, (void*)&wlr_seat_pointer_notify_motion, (void*)&hkNotifyMotion); + // Hook a private member (!WARNING: the signature may differ in clang. This one is for gcc ONLY.) + g_pMouseDownHook = HyprlandAPI::createFunctionHook( + PHANDLE, HyprlandAPI::getFunctionAddressFromSignature(PHANDLE, "_ZN13CInputManager22processMouseDownNormalEP24wlr_pointer_button_event"), (void*)&hkProcessMouseDownNormal); + + // Enable our hooks + g_pFocusHook->hook(); + g_pMotionHook->hook(); + g_pMouseDownHook->hook(); + + HyprlandAPI::reloadConfig(); + + return {"ExamplePlugin", "An example plugin", "Vaxry", "1.0"}; +} + +APICALL EXPORT void PLUGIN_EXIT() { + HyprlandAPI::invokeHyprctlCommand("seterror", "disable"); +} \ No newline at end of file diff --git a/flake.nix b/flake.nix index 3f2b6940..973a6fb9 100644 --- a/flake.nix +++ b/flake.nix @@ -52,11 +52,14 @@ version = props.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); wlroots = wlroots-hyprland; inherit (inputs.hyprland-protocols.packages.${prev.stdenv.hostPlatform.system}) hyprland-protocols; + inherit udis86; }; hyprland-debug = hyprland.override {debug = true;}; hyprland-no-hidpi = hyprland.override {hidpiXWayland = false;}; hyprland-nvidia = hyprland.override {nvidiaPatches = true;}; + udis86 = prev.callPackage ./nix/udis86.nix {}; + waybar-hyprland = prev.waybar.overrideAttrs (oldAttrs: { postPatch = '' # use hyprctl to switch workspaces diff --git a/hyprctl/main.cpp b/hyprctl/main.cpp index 81343526..a5bfda65 100644 --- a/hyprctl/main.cpp +++ b/hyprctl/main.cpp @@ -41,6 +41,7 @@ commands: switchxkblayout seterror setprop + plugin flags: -j -> output in JSON @@ -349,6 +350,8 @@ int main(int argc, char** argv) { request(fullRequest, 1); else if (fullRequest.contains("/setprop")) request(fullRequest, 3); + else if (fullRequest.contains("/plugin")) + request(fullRequest, 1); else if (fullRequest.contains("/output")) exitStatus = outputRequest(argc, argv); else if (fullRequest.contains("/setcursor")) diff --git a/meson.build b/meson.build index 0ae3d518..f3802553 100644 --- a/meson.build +++ b/meson.build @@ -43,6 +43,10 @@ wlroots = subproject('wlroots', default_options: ['examples=false']) have_xwlr = wlroots.get_variable('features').get('xwayland') xcb_dep = dependency('xcb', required: get_option('xwayland')) +cmake = import('cmake') +udis = cmake.subproject('udis86') +udis86 = udis.dependency('libudis86') + if get_option('xwayland').enabled() and not have_xwlr error('Cannot enable Xwayland in Hyprland: wlroots has been built without Xwayland support') endif diff --git a/nix/default.nix b/nix/default.nix index 89037960..78e103d4 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -18,6 +18,7 @@ mount, pciutils, systemd, + udis86, wayland, wayland-protocols, wayland-scanner, @@ -72,6 +73,7 @@ in libinput libxkbcommon mesa + udis86 wayland wayland-protocols wayland-scanner diff --git a/nix/meson-build.patch b/nix/meson-build.patch index 53f11969..075cb6be 100644 --- a/nix/meson-build.patch +++ b/nix/meson-build.patch @@ -1,34 +1,50 @@ diff --git a/meson.build b/meson.build -index 22ee4bf..5528613 100644 +index f380255..abd7cd3 100644 --- a/meson.build +++ b/meson.build -@@ -2,16 +2,10 @@ project('Hyprland', 'cpp', 'c', - version : '0.1', - default_options : ['warning_level=3', 'cpp_std=c++20', 'default_library=static']) - +@@ -39,23 +39,8 @@ add_project_arguments( + ], + language: 'cpp') + -wlroots = subproject('wlroots', default_options: ['examples=false']) -have_xwlr = wlroots.get_variable('features').get('xwayland') -+wlroots = dependency('wlroots', version: '>=0.16.0') xcb_dep = dependency('xcb', required: get_option('xwayland')) - + +-cmake = import('cmake') +-udis = cmake.subproject('udis86') +-udis86 = udis.dependency('libudis86') +- -if get_option('xwayland').enabled() and not have_xwlr - error('Cannot enable Xwayland in Hyprland: wlroots has been built without Xwayland support') -endif -have_xwayland = xcb_dep.found() and have_xwlr - -if not have_xwayland -+if not xcb_dep.found() - add_project_arguments('-DNO_XWAYLAND', language: 'cpp') - endif - +-add_project_arguments('-DNO_XWAYLAND', language: 'cpp') +-endif +- + backtrace_dep = cpp_compiler.find_library('execinfo', required: false) + systemd_dep = dependency('libsystemd', required: get_option('systemd')) + diff --git a/src/meson.build b/src/meson.build -index 5d64188..a676333 100644 +index 7b658d3..da8baa5 100644 --- a/src/meson.build +++ b/src/meson.build -@@ -7,5 +7,5 @@ executable('Hyprland', src, +@@ -7,7 +7,7 @@ executable('Hyprland', src, server_protos, dependency('wayland-server'), dependency('wayland-client'), - wlroots.get_variable('wlroots'), -+ wlroots, ++ dependency('wlroots'), dependency('cairo'), + dependency('libdrm'), + dependency('egl'), +@@ -16,7 +16,7 @@ executable('Hyprland', src, + xcb_dep, + backtrace_dep, + systemd_dep, +- udis86, ++ dependency('udis86'), + + dependency('pixman-1'), + dependency('gl', 'opengl'), diff --git a/nix/udis86.nix b/nix/udis86.nix new file mode 100644 index 00000000..d5e92afc --- /dev/null +++ b/nix/udis86.nix @@ -0,0 +1,32 @@ +{ + lib, + stdenv, + fetchFromGitHub, + autoreconfHook, + python3, +}: +stdenv.mkDerivation { + pname = "udis86"; + version = "unstable-2022-10-13"; + + src = fetchFromGitHub { + owner = "canihavesomecoffee"; + repo = "udis86"; + rev = "5336633af70f3917760a6d441ff02d93477b0c86"; + hash = "sha256-HifdUQPGsKQKQprByeIznvRLONdOXeolOsU5nkwIv3g="; + }; + + nativeBuildInputs = [autoreconfHook python3]; + + configureFlags = ["--enable-shared"]; + + outputs = ["bin" "out" "dev" "lib"]; + + meta = with lib; { + homepage = "https://udis86.sourceforge.net"; + license = licenses.bsd2; + mainProgram = "udcli"; + description = "Easy-to-use, minimalistic x86 disassembler library (libudis86)"; + platforms = platforms.all; + }; +} diff --git a/src/Compositor.cpp b/src/Compositor.cpp index 8ab741da..a7d48a15 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -18,6 +18,12 @@ int handleCritSignal(int signo, void* data) { } void handleSegv(int sig) { + + if (g_pHookSystem->m_bCurrentEventPlugin) { + longjmp(g_pHookSystem->m_jbHookFaultJumpBuf, 1); + return; + } + CrashReporter::createAndSaveCrash(); abort(); } @@ -364,6 +370,12 @@ void CCompositor::startCompositor() { Debug::log(LOG, "Creating the HyprDebugOverlay!"); g_pDebugOverlay = std::make_unique(); + + Debug::log(LOG, "Creating the HyprNotificationOverlay!"); + g_pHyprNotificationOverlay = std::make_unique(); + + Debug::log(LOG, "Creating the PluginSystem!"); + g_pPluginSystem = std::make_unique(); // // @@ -2012,6 +2024,9 @@ void CCompositor::scheduleFrameForMonitor(CMonitor* pMonitor) { if (!pMonitor->m_bEnabled) return; + if (pMonitor->renderingActive) + pMonitor->pendingFrame = true; + wlr_output_schedule_frame(pMonitor->output); } diff --git a/src/Compositor.hpp b/src/Compositor.hpp index 8008acc3..534f2487 100644 --- a/src/Compositor.hpp +++ b/src/Compositor.hpp @@ -19,12 +19,14 @@ #include "managers/SessionLockManager.hpp" #include "managers/HookSystemManager.hpp" #include "debug/HyprDebugOverlay.hpp" +#include "debug/HyprNotificationOverlay.hpp" #include "helpers/Monitor.hpp" #include "helpers/Workspace.hpp" #include "Window.hpp" #include "render/Renderer.hpp" #include "render/OpenGL.hpp" #include "hyprerror/HyprError.hpp" +#include "plugins/PluginSystem.hpp" class CCompositor { public: diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index c2d8a3ca..0931a6f8 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -271,7 +271,7 @@ void CConfigManager::init() { void CConfigManager::configSetValueSafe(const std::string& COMMAND, const std::string& VALUE) { if (configValues.find(COMMAND) == configValues.end()) { - if (COMMAND.find("device:") != 0 /* devices parsed later */) { + if (COMMAND.find("device:") != 0 /* devices parsed later */ && COMMAND.find("plugin:") != 0 /* plugins parsed later */) { if (COMMAND[0] == '$') { // register a dynamic var Debug::log(LOG, "Registered dynamic var \"%s\" -> %s", COMMAND.c_str(), VALUE.c_str()); @@ -306,6 +306,18 @@ void CConfigManager::configSetValueSafe(const std::string& COMMAND, const std::s } CONFIGENTRY = &it->second.at(CONFIGVAR); + } else if (COMMAND.find("plugin:") == 0) { + for (auto& [handle, pMap] : pluginConfigs) { + auto it = std::find_if(pMap->begin(), pMap->end(), [&](const auto& other) { return other.first == COMMAND; }); + if (it == pMap->end()) { + return; // plugin vars do not err, so we silently ignore. + } + + CONFIGENTRY = &it->second; + } + + if (!CONFIGENTRY) + return; // silent ignore } else { CONFIGENTRY = &configValues.at(COMMAND); } @@ -1649,8 +1661,17 @@ SConfigValue* CConfigManager::getConfigValuePtr(const std::string& val) { SConfigValue* CConfigManager::getConfigValuePtrSafe(const std::string& val) { const auto IT = configValues.find(val); - if (IT == configValues.end()) + if (IT == configValues.end()) { + // maybe plugin + for (auto& [pl, pMap] : pluginConfigs) { + const auto PLIT = pMap->find(val); + + if (PLIT != pMap->end()) + return &PLIT->second; + } + return nullptr; + } return &(IT->second); } @@ -1800,3 +1821,19 @@ ICustomConfigValueData::~ICustomConfigValueData() { std::unordered_map CConfigManager::getAnimationConfig() { return animationConfig; } + +void CConfigManager::addPluginConfigVar(HANDLE handle, const std::string& name, const SConfigValue& value) { + auto CONFIGMAPIT = std::find_if(pluginConfigs.begin(), pluginConfigs.end(), [&](const auto& other) { return other.first == handle; }); + + if (CONFIGMAPIT == pluginConfigs.end()) { + pluginConfigs.emplace( + std::pair>>(handle, std::make_unique>())); + CONFIGMAPIT = std::find_if(pluginConfigs.begin(), pluginConfigs.end(), [&](const auto& other) { return other.first == handle; }); + } + + (*CONFIGMAPIT->second)[name] = value; +} + +void CConfigManager::removePluginConfig(HANDLE handle) { + std::erase_if(pluginConfigs, [&](const auto& other) { return other.first == handle; }); +} \ No newline at end of file diff --git a/src/config/ConfigManager.hpp b/src/config/ConfigManager.hpp index e246d506..742f9fe8 100644 --- a/src/config/ConfigManager.hpp +++ b/src/config/ConfigManager.hpp @@ -21,6 +21,8 @@ #define INITANIMCFG(name) animationConfig[name] = {} #define CREATEANIMCFG(name, parent) animationConfig[name] = {false, "", "", 0.f, -1, &animationConfig["global"], &animationConfig[parent]} +#define HANDLE void* + struct SConfigValue { int64_t intValue = -INT64_MAX; float floatValue = -__FLT_MAX__; @@ -159,6 +161,9 @@ class CConfigManager { std::unordered_map getAnimationConfig(); + void addPluginConfigVar(HANDLE handle, const std::string& name, const SConfigValue& value); + void removePluginConfig(HANDLE handle); + // no-op when done. void dispatchExecOnce(); @@ -180,33 +185,35 @@ class CConfigManager { std::string configCurrentPath; private: - std::deque configPaths; // stores all the config paths - std::unordered_map configModifyTimes; // stores modify times - std::unordered_map configDynamicVars; // stores dynamic vars declared by the user - std::unordered_map configValues; - std::unordered_map> deviceConfigs; // stores device configs + std::deque configPaths; // stores all the config paths + std::unordered_map configModifyTimes; // stores modify times + std::unordered_map configDynamicVars; // stores dynamic vars declared by the user + std::unordered_map configValues; + std::unordered_map> deviceConfigs; // stores device configs + + std::unordered_map animationConfig; // stores all the animations with their set values - std::unordered_map animationConfig; // stores all the animations with their set values + std::string currentCategory = ""; // For storing the category of the current item - std::string currentCategory = ""; // For storing the category of the current item + std::string parseError = ""; // For storing a parse error to display later - std::string parseError = ""; // For storing a parse error to display later + std::string m_szCurrentSubmap = ""; // For storing the current keybind submap - std::string m_szCurrentSubmap = ""; // For storing the current keybind submap + std::vector> boundWorkspaces; - std::vector> boundWorkspaces; + std::vector execRequestedRules; // rules requested with exec, e.g. [workspace 2] kitty - std::vector execRequestedRules; // rules requested with exec, e.g. [workspace 2] kitty + std::unordered_map>> pluginConfigs; // stores plugin configs - bool isFirstLaunch = true; // For exec-once + bool isFirstLaunch = true; // For exec-once - std::deque m_dMonitorRules; - std::deque m_dWindowRules; - std::deque m_dLayerRules; - std::deque m_dBlurLSNamespaces; + std::deque m_dMonitorRules; + std::deque m_dWindowRules; + std::deque m_dLayerRules; + std::deque m_dBlurLSNamespaces; - bool firstExecDispatched = false; - std::deque firstExecRequests; + bool firstExecDispatched = false; + std::deque firstExecRequests; // internal methods void setDefaultVars(); diff --git a/src/debug/CrashReporter.cpp b/src/debug/CrashReporter.cpp index 5738f180..084e44ad 100644 --- a/src/debug/CrashReporter.cpp +++ b/src/debug/CrashReporter.cpp @@ -4,10 +4,13 @@ #include #include +#include "../plugins/PluginSystem.hpp" + #if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) #include #endif + std::string getRandomMessage() { const std::vector MESSAGES = {"Sorry, didn't mean to...", @@ -43,6 +46,16 @@ void CrashReporter::createAndSaveCrash() { finalCrashReport += "Hyprland received signal 11 (SIGSEGV): Segmentation Fault\n\n"; + if (!g_pPluginSystem->getAllPlugins().empty()) { + finalCrashReport += "Hyprland seems to be running with plugins. This crash might not be Hyprland's fault.\nPlugins:\n"; + + for (auto& p : g_pPluginSystem->getAllPlugins()) { + finalCrashReport += getFormat("\t%s (%s) %s\n", p->name.c_str(), p->author.c_str(), p->version.c_str()); + } + + finalCrashReport += "\n\n"; + } + finalCrashReport += "System info:\n"; struct utsname unameInfo; diff --git a/src/debug/HyprCtl.cpp b/src/debug/HyprCtl.cpp index 91af3938..f6ed285b 100644 --- a/src/debug/HyprCtl.cpp +++ b/src/debug/HyprCtl.cpp @@ -1087,6 +1087,50 @@ std::string dispatchOutput(std::string request) { return "ok"; } +std::string dispatchPlugin(std::string request) { + CVarList vars(request, 0, ' '); + + if (vars.size() < 2) + return "not enough args"; + + const auto OPERATION = vars[1]; + const auto PATH = vars[2]; + + if (OPERATION == "load") { + if (vars.size() < 3) + return "not enough args"; + + const auto PLUGIN = g_pPluginSystem->loadPlugin(PATH); + + if (!PLUGIN) + return "error in loading plugin"; + } else if (OPERATION == "unload") { + if (vars.size() < 3) + return "not enough args"; + + const auto PLUGIN = g_pPluginSystem->getPluginByPath(PATH); + + if (!PLUGIN) + return "plugin not loaded"; + + g_pPluginSystem->unloadPlugin(PLUGIN); + } else if (OPERATION == "list") { + const auto PLUGINS = g_pPluginSystem->getAllPlugins(); + + std::string list = ""; + for (auto& p : PLUGINS) { + list += getFormat("\nPlugin %s by %s:\n\tHandle: %lx\n\tVersion: %s\n\tDescription: %s\n", p->name.c_str(), p->author.c_str(), p->m_pHandle, p->version.c_str(), + p->description.c_str()); + } + + return list; + } else { + return "unknown opt"; + } + + return "ok"; +} + std::string getReply(std::string request) { auto format = HyprCtl::FORMAT_NORMAL; @@ -1134,6 +1178,8 @@ std::string getReply(std::string request) { return bindsRequest(format); else if (request == "animations") return animationsRequest(format); + else if (request.find("plugin") == 0) + return dispatchPlugin(request); else if (request.find("setprop") == 0) return dispatchSetProp(request); else if (request.find("seterror") == 0) @@ -1156,6 +1202,10 @@ std::string getReply(std::string request) { return "unknown request"; } +std::string HyprCtl::makeDynamicCall(const std::string& input) { + return getReply(input); +} + int hyprCtlFDTick(int fd, uint32_t mask, void* data) { if (mask & WL_EVENT_ERROR || mask & WL_EVENT_HANGUP) return 0; diff --git a/src/debug/HyprCtl.hpp b/src/debug/HyprCtl.hpp index 26859692..00cf7c5b 100644 --- a/src/debug/HyprCtl.hpp +++ b/src/debug/HyprCtl.hpp @@ -5,7 +5,8 @@ #include "../helpers/MiscFunctions.hpp" namespace HyprCtl { - void startHyprCtlSocket(); + void startHyprCtlSocket(); + std::string makeDynamicCall(const std::string& input); // very simple thread-safe request method inline bool requestMade = false; @@ -18,8 +19,7 @@ namespace HyprCtl { inline int iSocketFD = -1; - enum eHyprCtlOutputFormat - { + enum eHyprCtlOutputFormat { FORMAT_NORMAL = 0, FORMAT_JSON }; diff --git a/src/debug/HyprNotificationOverlay.cpp b/src/debug/HyprNotificationOverlay.cpp new file mode 100644 index 00000000..94cb6a6c --- /dev/null +++ b/src/debug/HyprNotificationOverlay.cpp @@ -0,0 +1,155 @@ +#include "HyprNotificationOverlay.hpp" +#include "../Compositor.hpp" + +CHyprNotificationOverlay::CHyprNotificationOverlay() { + g_pHookSystem->hookDynamic("focusedMon", [&](void* self, std::any param) { + if (m_dNotifications.size() == 0) + return; + + g_pHyprRenderer->damageBox(&m_bLastDamage); + }); +} + +void CHyprNotificationOverlay::addNotification(const std::string& text, const CColor& color, const float timeMs) { + const auto PNOTIF = m_dNotifications.emplace_back(std::make_unique()).get(); + + PNOTIF->text = text; + PNOTIF->color = color; + PNOTIF->started.reset(); + PNOTIF->timeMs = timeMs; +} + +wlr_box CHyprNotificationOverlay::drawNotifications(CMonitor* pMonitor) { + static constexpr auto ANIM_DURATION_MS = 600.0; + static constexpr auto ANIM_LAG_MS = 100.0; + static constexpr auto NOTIF_LEFTBAR_SIZE = 5.0; + + float offsetY = 10; + float maxWidth = 0; + + const auto SCALE = pMonitor->scale; + const auto FONTSIZE = std::clamp((int)(10.f * ((pMonitor->vecPixelSize.x * SCALE) / 1920.f)), 8, 40); + + const auto MONSIZE = pMonitor->vecPixelSize; + + cairo_select_font_face(m_pCairo, "Noto Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); + cairo_set_font_size(m_pCairo, FONTSIZE); + + cairo_text_extents_t cairoExtents; + + const auto PBEZIER = g_pAnimationManager->getBezier("default"); + + for (auto& notif : m_dNotifications) { + // first rect (bg, col) + const float FIRSTRECTANIMP = + (notif->started.getMillis() > (ANIM_DURATION_MS - ANIM_LAG_MS) ? + (notif->started.getMillis() > notif->timeMs - (ANIM_DURATION_MS - ANIM_LAG_MS) ? notif->timeMs - notif->started.getMillis() : (ANIM_DURATION_MS - ANIM_LAG_MS)) : + notif->started.getMillis()) / + (ANIM_DURATION_MS - ANIM_LAG_MS); + + const float FIRSTRECTPERC = FIRSTRECTANIMP >= 0.99f ? 1.f : PBEZIER->getYForPoint(FIRSTRECTANIMP); + + // second rect (fg, black) + const float SECONDRECTANIMP = (notif->started.getMillis() > ANIM_DURATION_MS ? + (notif->started.getMillis() > notif->timeMs - ANIM_DURATION_MS ? notif->timeMs - notif->started.getMillis() : ANIM_DURATION_MS) : + notif->started.getMillis()) / + ANIM_DURATION_MS; + + const float SECONDRECTPERC = SECONDRECTANIMP >= 0.99f ? 1.f : PBEZIER->getYForPoint(SECONDRECTANIMP); + + // third rect (horiz, col) + const float THIRDRECTPERC = notif->started.getMillis() / notif->timeMs; + + // get text size + cairo_text_extents(m_pCairo, notif->text.c_str(), &cairoExtents); + + cairo_set_source_rgba(m_pCairo, notif->color.r, notif->color.g, notif->color.b, notif->color.a); + + const auto NOTIFSIZE = Vector2D{cairoExtents.width + 20, cairoExtents.height + 10}; + + // draw rects + cairo_rectangle(m_pCairo, MONSIZE.x - (NOTIFSIZE.x + NOTIF_LEFTBAR_SIZE) * FIRSTRECTPERC, offsetY, (NOTIFSIZE.x + NOTIF_LEFTBAR_SIZE) * FIRSTRECTPERC, NOTIFSIZE.y); + cairo_fill(m_pCairo); + + cairo_set_source_rgb(m_pCairo, 0.f, 0.f, 0.f); + + cairo_rectangle(m_pCairo, MONSIZE.x - NOTIFSIZE.x * SECONDRECTPERC, offsetY, NOTIFSIZE.x * SECONDRECTPERC, NOTIFSIZE.y); + cairo_fill(m_pCairo); + + cairo_set_source_rgba(m_pCairo, notif->color.r, notif->color.g, notif->color.b, notif->color.a); + + cairo_rectangle(m_pCairo, MONSIZE.x - NOTIFSIZE.x * SECONDRECTPERC + 3, offsetY + NOTIFSIZE.y - 4, THIRDRECTPERC * (NOTIFSIZE.x - 6), 2); + cairo_fill(m_pCairo); + + // draw text + cairo_set_source_rgb(m_pCairo, 1.f, 1.f, 1.f); + cairo_move_to(m_pCairo, MONSIZE.x - NOTIFSIZE.x * SECONDRECTPERC + NOTIF_LEFTBAR_SIZE, offsetY + FONTSIZE + (FONTSIZE / 10.0)); + cairo_show_text(m_pCairo, notif->text.c_str()); + + // adjust offset and move on + offsetY += NOTIFSIZE.y + 10; + + if (maxWidth < NOTIFSIZE.x) + maxWidth = NOTIFSIZE.x; + } + + // cleanup notifs + std::erase_if(m_dNotifications, [](const auto& notif) { return notif->started.getMillis() > notif->timeMs; }); + + return wlr_box{(int)(pMonitor->vecPosition.x + pMonitor->vecSize.x - maxWidth - 20), (int)pMonitor->vecPosition.y, (int)maxWidth + 20, (int)offsetY + 10}; +} + +void CHyprNotificationOverlay::draw(CMonitor* pMonitor) { + + if (m_pLastMonitor != pMonitor || !m_pCairo || !m_pCairoSurface) { + + if (m_pCairo && m_pCairoSurface) { + cairo_destroy(m_pCairo); + cairo_surface_destroy(m_pCairoSurface); + } + + m_pCairoSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, pMonitor->vecPixelSize.x, pMonitor->vecPixelSize.y); + m_pCairo = cairo_create(m_pCairoSurface); + m_pLastMonitor = pMonitor; + } + + // Draw the notifications + if (m_dNotifications.size() == 0) + return; + + // Render to the monitor + + // clear the pixmap + cairo_save(m_pCairo); + cairo_set_operator(m_pCairo, CAIRO_OPERATOR_CLEAR); + cairo_paint(m_pCairo); + cairo_restore(m_pCairo); + + cairo_surface_flush(m_pCairoSurface); + + wlr_box damage = drawNotifications(pMonitor); + + g_pHyprRenderer->damageBox(&damage); + g_pHyprRenderer->damageBox(&m_bLastDamage); + + g_pCompositor->scheduleFrameForMonitor(pMonitor); + + m_bLastDamage = damage; + + // copy the data to an OpenGL texture we have + const auto DATA = cairo_image_surface_get_data(m_pCairoSurface); + m_tTexture.allocate(); + glBindTexture(GL_TEXTURE_2D, m_tTexture.m_iTexID); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + +#ifndef GLES2 + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED); +#endif + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, pMonitor->vecPixelSize.x, pMonitor->vecPixelSize.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA); + + wlr_box pMonBox = {0, 0, pMonitor->vecPixelSize.x, pMonitor->vecPixelSize.y}; + g_pHyprOpenGL->renderTexture(m_tTexture, &pMonBox, 1.f); +} \ No newline at end of file diff --git a/src/debug/HyprNotificationOverlay.hpp b/src/debug/HyprNotificationOverlay.hpp new file mode 100644 index 00000000..12ad8e38 --- /dev/null +++ b/src/debug/HyprNotificationOverlay.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "../defines.hpp" +#include "../helpers/Timer.hpp" +#include "../helpers/Monitor.hpp" +#include "../render/Texture.hpp" + +#include + +#include + +struct SNotification { + std::string text = ""; + CColor color; + CTimer started; + float timeMs = 0; +}; + +class CHyprNotificationOverlay { + public: + CHyprNotificationOverlay(); + + void draw(CMonitor* pMonitor); + void addNotification(const std::string& text, const CColor& color, const float timeMs); + + private: + wlr_box drawNotifications(CMonitor* pMonitor); + wlr_box m_bLastDamage; + + std::deque> m_dNotifications; + + cairo_surface_t* m_pCairoSurface = nullptr; + cairo_t* m_pCairo = nullptr; + + CMonitor* m_pLastMonitor = nullptr; + + CTexture m_tTexture; +}; + +inline std::unique_ptr g_pHyprNotificationOverlay; \ No newline at end of file diff --git a/src/defines.hpp b/src/defines.hpp index cad434fd..b7047552 100644 --- a/src/defines.hpp +++ b/src/defines.hpp @@ -89,4 +89,4 @@ #define SPECIAL_WORKSPACE_START (-99) -#define PI 3.14159265358979 +#define PI 3.14159265358979 \ No newline at end of file diff --git a/src/events/Monitors.cpp b/src/events/Monitors.cpp index 799669a1..271b7d81 100644 --- a/src/events/Monitors.cpp +++ b/src/events/Monitors.cpp @@ -197,6 +197,8 @@ void Events::listener_monitorFrame(void* owner, void* data) { return; } + PMONITOR->renderingActive = true; + // we need to cleanup fading out when rendering the appropriate context g_pCompositor->cleanupFadingOut(PMONITOR->ID); @@ -207,6 +209,8 @@ void Events::listener_monitorFrame(void* owner, void* data) { if (*PDAMAGEBLINK || *PVFR == 0) g_pCompositor->scheduleFrameForMonitor(PMONITOR); + PMONITOR->renderingActive = false; + return; } @@ -260,6 +264,9 @@ void Events::listener_monitorFrame(void* owner, void* data) { if (PMONITOR == g_pCompositor->m_vMonitors.front().get()) g_pHyprError->draw(); + if (PMONITOR == g_pCompositor->m_pLastMonitor) + g_pHyprNotificationOverlay->draw(PMONITOR); + // for drawing the debug overlay if (PMONITOR == g_pCompositor->m_vMonitors.front().get() && *PDEBUGOVERLAY == 1) { startRenderOverlay = std::chrono::high_resolution_clock::now(); @@ -306,12 +313,16 @@ void Events::listener_monitorFrame(void* owner, void* data) { pixman_region32_fini(&frameDamage); pixman_region32_fini(&damage); + PMONITOR->renderingActive = false; + if (!wlr_output_commit(PMONITOR->output)) return; - if (*PDAMAGEBLINK || *PVFR == 0) + if (*PDAMAGEBLINK || *PVFR == 0 || PMONITOR->pendingFrame) g_pCompositor->scheduleFrameForMonitor(PMONITOR); + PMONITOR->pendingFrame = false; + if (*PDEBUGOVERLAY == 1) { const float µs = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - startRender).count() / 1000.f; g_pDebugOverlay->renderData(PMONITOR, µs); diff --git a/src/helpers/Color.hpp b/src/helpers/Color.hpp index 184087ac..ffd6d2d8 100644 --- a/src/helpers/Color.hpp +++ b/src/helpers/Color.hpp @@ -1,6 +1,6 @@ #pragma once -#include "../includes.hpp" +#include class CColor { public: @@ -13,7 +13,7 @@ class CColor { uint64_t getAsHex(); CColor operator-(const CColor& c2) const { - return CColor(r - c2.r, g - c2.g, b - c2.b, a - c2.a); + return CColor(r - c2.r, g - c2.g, b - c2.b, a - c2.a); } CColor operator+(const CColor& c2) const { diff --git a/src/helpers/Monitor.hpp b/src/helpers/Monitor.hpp index 6582cbfc..9ad6d917 100644 --- a/src/helpers/Monitor.hpp +++ b/src/helpers/Monitor.hpp @@ -42,6 +42,9 @@ class CMonitor { bool enabled10bit = false; // as above, this can be TRUE even if 10 bit failed. bool createdByUser = false; + bool pendingFrame = false; // if we schedule a frame during rendering, reschedule it after + bool renderingActive = false; + // mirroring CMonitor* pMirrorOf = nullptr; std::vector mirrors; diff --git a/src/managers/HookSystemManager.cpp b/src/managers/HookSystemManager.cpp index 02775a24..c5e4cdd8 100644 --- a/src/managers/HookSystemManager.cpp +++ b/src/managers/HookSystemManager.cpp @@ -1,44 +1,79 @@ #include "HookSystemManager.hpp" +#include "../plugins/PluginSystem.hpp" + CHookSystemManager::CHookSystemManager() { ; // } // returns the pointer to the function -HOOK_CALLBACK_FN* CHookSystemManager::hookDynamic(const std::string& event, HOOK_CALLBACK_FN fn) { +HOOK_CALLBACK_FN* CHookSystemManager::hookDynamic(const std::string& event, HOOK_CALLBACK_FN fn, HANDLE handle) { const auto PVEC = getVecForEvent(event); const auto PFN = &m_lCallbackFunctions.emplace_back(fn); - PVEC->emplace_back(PFN); + PVEC->emplace_back(SCallbackFNPtr{PFN, handle}); return PFN; } -void CHookSystemManager::hookStatic(const std::string& event, HOOK_CALLBACK_FN* fn) { +void CHookSystemManager::hookStatic(const std::string& event, HOOK_CALLBACK_FN* fn, HANDLE handle) { const auto PVEC = getVecForEvent(event); - PVEC->emplace_back(fn); + PVEC->emplace_back(SCallbackFNPtr{fn, handle}); } void CHookSystemManager::unhook(HOOK_CALLBACK_FN* fn) { std::erase_if(m_lCallbackFunctions, [&](const auto& other) { return &other == fn; }); for (auto& [k, v] : m_lpRegisteredHooks) { - std::erase_if(v, [&](const auto& other) { return other == fn; }); + std::erase_if(v, [&](const auto& other) { return other.fn == fn; }); } } -void CHookSystemManager::emit(const std::vector* callbacks, std::any data) { +void CHookSystemManager::emit(const std::vector* callbacks, std::any data) { if (callbacks->empty()) return; - for (auto& cb : *callbacks) - (*cb)(cb, data); + std::vector faultyHandles; + + for (auto& cb : *callbacks) { + + m_bCurrentEventPlugin = false; + + if (!cb.handle) { + // we don't guard hl hooks + (*cb.fn)(cb.fn, data); + continue; + } + + m_bCurrentEventPlugin = true; + + if (std::find(faultyHandles.begin(), faultyHandles.end(), cb.handle) != faultyHandles.end()) + continue; + + try { + if (!setjmp(m_jbHookFaultJumpBuf)) + (*cb.fn)(cb.fn, data); + else { + // this module crashed. + throw std::exception(); + } + } catch (std::exception& e) { + // TODO: this works only once...? + faultyHandles.push_back(cb.handle); + Debug::log(ERR, " [hookSystem] Hook from plugin %lx caused a SIGSEGV, queueing for unloading.", cb.handle); + } + } + + if (!faultyHandles.empty()) { + for (auto& h : faultyHandles) + g_pPluginSystem->unloadPlugin(g_pPluginSystem->getPluginByHandle(h), true); + } } -std::vector* CHookSystemManager::getVecForEvent(const std::string& event) { +std::vector* CHookSystemManager::getVecForEvent(const std::string& event) { auto IT = std::find_if(m_lpRegisteredHooks.begin(), m_lpRegisteredHooks.end(), [&](const auto& other) { return other.first == event; }); if (IT != m_lpRegisteredHooks.end()) return &IT->second; - Debug::log(LOG, "[hookSystem] New hook event registered: %s", event.c_str()); + Debug::log(LOG, " [hookSystem] New hook event registered: %s", event.c_str()); - return &m_lpRegisteredHooks.emplace_back(std::make_pair<>(event, std::vector{})).second; + return &m_lpRegisteredHooks.emplace_back(std::make_pair<>(event, std::vector{})).second; } \ No newline at end of file diff --git a/src/managers/HookSystemManager.hpp b/src/managers/HookSystemManager.hpp index 57f1e58b..66edd1c0 100644 --- a/src/managers/HookSystemManager.hpp +++ b/src/managers/HookSystemManager.hpp @@ -4,11 +4,21 @@ #include #include +#include #include +#include + +#include "../plugins/PluginAPI.hpp" + // global typedef for hooked functions. Passes itself as a ptr when called, and `data` additionally. typedef std::function HOOK_CALLBACK_FN; +struct SCallbackFNPtr { + HOOK_CALLBACK_FN* fn = nullptr; + HANDLE handle = nullptr; +}; + #define EMIT_HOOK_EVENT(name, param) \ { \ static auto* const PEVENTVEC = g_pHookSystem->getVecForEvent(name); \ @@ -20,17 +30,20 @@ class CHookSystemManager { CHookSystemManager(); // returns the pointer to the function - HOOK_CALLBACK_FN* hookDynamic(const std::string& event, HOOK_CALLBACK_FN fn); - void hookStatic(const std::string& event, HOOK_CALLBACK_FN* fn); - void unhook(HOOK_CALLBACK_FN* fn); + HOOK_CALLBACK_FN* hookDynamic(const std::string& event, HOOK_CALLBACK_FN fn, HANDLE handle = nullptr); + void hookStatic(const std::string& event, HOOK_CALLBACK_FN* fn, HANDLE handle = nullptr); + void unhook(HOOK_CALLBACK_FN* fn); + + void emit(const std::vector* callbacks, std::any data = 0); + std::vector* getVecForEvent(const std::string& event); - void emit(const std::vector* callbacks, std::any data = 0); - std::vector* getVecForEvent(const std::string& event); + bool m_bCurrentEventPlugin = false; + jmp_buf m_jbHookFaultJumpBuf; private: // todo: this is slow. Maybe static ptrs should be somehow allowed. unique ptr for vec? - std::list>> m_lpRegisteredHooks; - std::list m_lCallbackFunctions; + std::list>> m_lpRegisteredHooks; + std::list m_lCallbackFunctions; }; inline std::unique_ptr g_pHookSystem; \ No newline at end of file diff --git a/src/managers/KeybindManager.hpp b/src/managers/KeybindManager.hpp index 0b8fa4e8..6c773a8c 100644 --- a/src/managers/KeybindManager.hpp +++ b/src/managers/KeybindManager.hpp @@ -8,6 +8,7 @@ class CInputManager; class CConfigManager; +class CPluginSystem; struct SKeybind { std::string key = ""; diff --git a/src/managers/LayoutManager.cpp b/src/managers/LayoutManager.cpp index 86e45010..c219c25b 100644 --- a/src/managers/LayoutManager.cpp +++ b/src/managers/LayoutManager.cpp @@ -1,29 +1,51 @@ #include "LayoutManager.hpp" -IHyprLayout* CLayoutManager::getCurrentLayout() { - switch (m_iCurrentLayoutID) { - case LAYOUT_DWINDLE: return &m_cDwindleLayout; - case LAYOUT_MASTER: return &m_cMasterLayout; - } +CLayoutManager::CLayoutManager() { + m_vLayouts.emplace_back(std::make_pair<>("dwindle", &m_cDwindleLayout)); + m_vLayouts.emplace_back(std::make_pair<>("master", &m_cMasterLayout)); +} - // fallback - return &m_cDwindleLayout; +IHyprLayout* CLayoutManager::getCurrentLayout() { + return m_vLayouts[m_iCurrentLayoutID].second; } void CLayoutManager::switchToLayout(std::string layout) { - if (layout == "dwindle") { - if (m_iCurrentLayoutID != LAYOUT_DWINDLE) { + for (size_t i = 0; i < m_vLayouts.size(); ++i) { + if (m_vLayouts[i].first == layout) { getCurrentLayout()->onDisable(); - m_iCurrentLayoutID = LAYOUT_DWINDLE; + m_iCurrentLayoutID = i; getCurrentLayout()->onEnable(); + return; } - } else if (layout == "master") { - if (m_iCurrentLayoutID != LAYOUT_MASTER) { - getCurrentLayout()->onDisable(); - m_iCurrentLayoutID = LAYOUT_MASTER; - getCurrentLayout()->onEnable(); - } - } else { - Debug::log(ERR, "Unknown layout %s!", layout.c_str()); } + + Debug::log(ERR, "Unknown layout!"); +} + +bool CLayoutManager::addLayout(const std::string& name, IHyprLayout* layout) { + if (std::find_if(m_vLayouts.begin(), m_vLayouts.end(), [&](const auto& other) { return other.first == name || other.second == layout; }) != m_vLayouts.end()) + return false; + + m_vLayouts.emplace_back(std::make_pair<>(name, layout)); + + Debug::log(LOG, "Added new layout %s at %lx", name.c_str(), layout); + + return true; +} + +bool CLayoutManager::removeLayout(IHyprLayout* layout) { + const auto IT = std::find_if(m_vLayouts.begin(), m_vLayouts.end(), [&](const auto& other) { return other.second == layout; }); + + if (IT == m_vLayouts.end() || IT->first == "dwindle" || IT->first == "master") + return false; + + if (m_iCurrentLayoutID == IT - m_vLayouts.begin()) { + switchToLayout("dwindle"); + } + + Debug::log(LOG, "Removed a layout %s at %lx", IT->first.c_str(), layout); + + std::erase(m_vLayouts, *IT); + + return true; } diff --git a/src/managers/LayoutManager.hpp b/src/managers/LayoutManager.hpp index b14b989f..1ebe5711 100644 --- a/src/managers/LayoutManager.hpp +++ b/src/managers/LayoutManager.hpp @@ -5,20 +5,28 @@ class CLayoutManager { public: + CLayoutManager(); + IHyprLayout* getCurrentLayout(); void switchToLayout(std::string); + bool addLayout(const std::string& name, IHyprLayout* layout); + bool removeLayout(IHyprLayout* layout); + private: - enum HYPRLAYOUTS { + enum HYPRLAYOUTS + { LAYOUT_DWINDLE = 0, LAYOUT_MASTER }; - HYPRLAYOUTS m_iCurrentLayoutID = LAYOUT_DWINDLE; + int m_iCurrentLayoutID = LAYOUT_DWINDLE; + + CHyprDwindleLayout m_cDwindleLayout; + CHyprMasterLayout m_cMasterLayout; - CHyprDwindleLayout m_cDwindleLayout; - CHyprMasterLayout m_cMasterLayout; + std::vector> m_vLayouts; }; inline std::unique_ptr g_pLayoutManager; \ No newline at end of file diff --git a/src/meson.build b/src/meson.build index 6c018e12..7b658d31 100644 --- a/src/meson.build +++ b/src/meson.build @@ -16,6 +16,7 @@ executable('Hyprland', src, xcb_dep, backtrace_dep, systemd_dep, + udis86, dependency('pixman-1'), dependency('gl', 'opengl'), diff --git a/src/plugins/HookSystem.cpp b/src/plugins/HookSystem.cpp new file mode 100644 index 00000000..a8ddd90e --- /dev/null +++ b/src/plugins/HookSystem.cpp @@ -0,0 +1,151 @@ +#include "HookSystem.hpp" + +#define register +#include +#undef register +#include +#include +#include + +CFunctionHook::CFunctionHook(HANDLE owner, void* source, void* destination) { + m_pSource = source; + m_pDestination = destination; + m_pOwner = owner; +} + +CFunctionHook::~CFunctionHook() { + if (m_bActive) { + unhook(); + } +} + +size_t getInstructionLenAt(void* start) { + ud_t udis; + + ud_init(&udis); + ud_set_mode(&udis, 64); + ud_set_syntax(&udis, UD_SYN_INTEL); + + size_t curOffset = 1; + size_t insSize = 0; + while (true) { + ud_set_input_buffer(&udis, (uint8_t*)start, curOffset); + insSize = ud_disassemble(&udis); + if (insSize != curOffset) + break; + curOffset++; + } + + return insSize; +} + +size_t probeMinimumJumpSize(void* start, size_t min) { + + size_t size = 0; + + while (size <= min) { + // find info about this instruction + size_t insLen = getInstructionLenAt(start + size); + size += insLen; + } + + return size; +} + +bool CFunctionHook::hook() { + + // check for unsupported platforms +#if !defined(__x86_64__) + return false; +#endif + + // movabs $0,%rax | jmpq *%rax + static constexpr uint8_t ABSOLUTE_JMP_ADDRESS[] = {0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0}; + // pushq %rax + static constexpr uint8_t PUSH_RAX[] = {0x50}; + // popq %rax + static constexpr uint8_t POP_RAX[] = {0x58}; + // nop + static constexpr uint8_t NOP = 0x90; + + // get minimum size to overwrite + const auto HOOKSIZE = probeMinimumJumpSize(m_pSource, sizeof(ABSOLUTE_JMP_ADDRESS) + sizeof(PUSH_RAX) + sizeof(POP_RAX)); + + // alloc trampoline + m_pTrampolineAddr = mmap(NULL, sizeof(ABSOLUTE_JMP_ADDRESS) + HOOKSIZE + sizeof(PUSH_RAX), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + + // populate trampoline + memcpy(m_pTrampolineAddr, m_pSource, HOOKSIZE); // first, original func bytes + memcpy(m_pTrampolineAddr + HOOKSIZE, PUSH_RAX, sizeof(PUSH_RAX)); // then, pushq %rax + memcpy(m_pTrampolineAddr + HOOKSIZE + sizeof(PUSH_RAX), ABSOLUTE_JMP_ADDRESS, sizeof(ABSOLUTE_JMP_ADDRESS)); // then, jump to source + + // fixup trampoline addr + *(uint64_t*)(m_pTrampolineAddr + HOOKSIZE + 2 + sizeof(PUSH_RAX)) = (uint64_t)(m_pSource + sizeof(ABSOLUTE_JMP_ADDRESS)); + + // make jump to hk + mprotect(m_pSource - ((uint64_t)m_pSource) % sysconf(_SC_PAGE_SIZE), sysconf(_SC_PAGE_SIZE), PROT_READ | PROT_WRITE | PROT_EXEC); + memcpy(m_pSource, ABSOLUTE_JMP_ADDRESS, sizeof(ABSOLUTE_JMP_ADDRESS)); + + // make popq %rax and NOP all remaining + memcpy(m_pSource + sizeof(ABSOLUTE_JMP_ADDRESS), POP_RAX, sizeof(POP_RAX)); + size_t currentOp = sizeof(ABSOLUTE_JMP_ADDRESS) + sizeof(POP_RAX); + memset(m_pSource + currentOp, NOP, HOOKSIZE - currentOp); + + // fixup jump addr + *(uint64_t*)(m_pSource + 2) = (uint64_t)(m_pDestination); + + // revert mprot + mprotect(m_pSource - ((uint64_t)m_pSource) % sysconf(_SC_PAGE_SIZE), sysconf(_SC_PAGE_SIZE), PROT_READ | PROT_EXEC); + + // set original addr to trampo addr + m_pOriginal = m_pTrampolineAddr; + + m_bActive = true; + m_iHookLen = HOOKSIZE; + m_iTrampoLen = HOOKSIZE + sizeof(ABSOLUTE_JMP_ADDRESS); + + return true; +} + +bool CFunctionHook::unhook() { + // check for unsupported platforms +#if !defined(__x86_64__) + return false; +#endif + + if (!m_bActive) + return false; + + // allow write to src + mprotect(m_pSource - ((uint64_t)m_pSource) % sysconf(_SC_PAGE_SIZE), sysconf(_SC_PAGE_SIZE), PROT_READ | PROT_WRITE | PROT_EXEC); + + // write back original bytes + memcpy(m_pSource, m_pTrampolineAddr, m_iHookLen); + + // revert mprot + mprotect(m_pSource - ((uint64_t)m_pSource) % sysconf(_SC_PAGE_SIZE), sysconf(_SC_PAGE_SIZE), PROT_READ | PROT_EXEC); + + // unmap + munmap(m_pTrampolineAddr, m_iTrampoLen); + + // reset vars + m_bActive = false; + m_iHookLen = 0; + m_iTrampoLen = 0; + m_pTrampolineAddr = nullptr; + + return true; +} + +CFunctionHook* CHookSystem::initHook(HANDLE owner, void* source, void* destination) { + return m_vHooks.emplace_back(std::make_unique(owner, source, destination)).get(); +} + +bool CHookSystem::removeHook(CFunctionHook* hook) { + std::erase_if(m_vHooks, [&](const auto& other) { return other.get() == hook; }); + return true; // todo: make false if not found +} + +void CHookSystem::removeAllHooksFrom(HANDLE handle) { + std::erase_if(m_vHooks, [&](const auto& other) { return other->m_pOwner == handle; }); +} \ No newline at end of file diff --git a/src/plugins/HookSystem.hpp b/src/plugins/HookSystem.hpp new file mode 100644 index 00000000..56227449 --- /dev/null +++ b/src/plugins/HookSystem.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +#define HANDLE void* + +class CFunctionHook { + public: + CFunctionHook(HANDLE owner, void* source, void* destination); + ~CFunctionHook(); + + bool hook(); + bool unhook(); + + CFunctionHook(const CFunctionHook&) = delete; + CFunctionHook(CFunctionHook&&) = delete; + CFunctionHook& operator=(const CFunctionHook&) = delete; + CFunctionHook& operator=(CFunctionHook&&) = delete; + + void* m_pOriginal = nullptr; + + private: + void* m_pSource = nullptr; + void* m_pFunctionAddr = nullptr; + void* m_pTrampolineAddr = nullptr; + void* m_pDestination = nullptr; + size_t m_iHookLen = 0; + size_t m_iTrampoLen = 0; + HANDLE m_pOwner = nullptr; + bool m_bActive = false; + + friend class CHookSystem; +}; + +class CHookSystem { + public: + CFunctionHook* initHook(HANDLE handle, void* source, void* destination); + bool removeHook(CFunctionHook* hook); + + void removeAllHooksFrom(HANDLE handle); + + private: + std::vector> m_vHooks; +}; + +inline std::unique_ptr g_pFunctionHookSystem; \ No newline at end of file diff --git a/src/plugins/PluginAPI.cpp b/src/plugins/PluginAPI.cpp new file mode 100644 index 00000000..dc8c9707 --- /dev/null +++ b/src/plugins/PluginAPI.cpp @@ -0,0 +1,193 @@ +#include "PluginAPI.hpp" +#include "../Compositor.hpp" +#include "../debug/HyprCtl.hpp" +#include + +APICALL bool HyprlandAPI::registerCallbackStatic(HANDLE handle, const std::string& event, HOOK_CALLBACK_FN* fn) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return false; + + g_pHookSystem->hookStatic(event, fn, handle); + PLUGIN->registeredCallbacks.emplace_back(std::make_pair<>(event, fn)); + + return true; +} + +APICALL HOOK_CALLBACK_FN* HyprlandAPI::registerCallbackDynamic(HANDLE handle, const std::string& event, HOOK_CALLBACK_FN fn) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return nullptr; + + auto* const PFN = g_pHookSystem->hookDynamic(event, fn, handle); + PLUGIN->registeredCallbacks.emplace_back(std::make_pair<>(event, PFN)); + return PFN; +} + +APICALL bool HyprlandAPI::unregisterCallback(HANDLE handle, HOOK_CALLBACK_FN* fn) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return false; + + g_pHookSystem->unhook(fn); + std::erase_if(PLUGIN->registeredCallbacks, [&](const auto& other) { return other.second == fn; }); + + return true; +} + +APICALL std::string HyprlandAPI::invokeHyprctlCommand(const std::string& call, const std::string& args, const std::string& format) { + std::string COMMAND = format + "/" + call + " " + args; + return HyprCtl::makeDynamicCall(COMMAND); +} + +APICALL bool HyprlandAPI::addLayout(HANDLE handle, const std::string& name, IHyprLayout* layout) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return false; + + PLUGIN->registeredLayouts.push_back(layout); + + return g_pLayoutManager->addLayout(name, layout); +} + +APICALL bool HyprlandAPI::removeLayout(HANDLE handle, IHyprLayout* layout) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return false; + + std::erase(PLUGIN->registeredLayouts, layout); + + return g_pLayoutManager->removeLayout(layout); +} + +APICALL bool HyprlandAPI::reloadConfig() { + g_pConfigManager->m_bForceReload = true; + return true; +} + +APICALL bool HyprlandAPI::addNotification(HANDLE handle, const std::string& text, const CColor& color, const float timeMs) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return false; + + g_pHyprNotificationOverlay->addNotification(text, color, timeMs); + + return true; +} + +APICALL CFunctionHook* HyprlandAPI::createFunctionHook(HANDLE handle, const void* source, const void* destination) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return nullptr; + + return g_pFunctionHookSystem->initHook(handle, (void*)source, (void*)destination); +} + +APICALL bool HyprlandAPI::removeFunctionHook(HANDLE handle, CFunctionHook* hook) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return false; + + return g_pFunctionHookSystem->removeHook(hook); +} + +APICALL bool HyprlandAPI::addWindowDecoration(HANDLE handle, CWindow* pWindow, IHyprWindowDecoration* pDecoration) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return false; + + if (!g_pCompositor->windowValidMapped(pWindow)) + return false; + + PLUGIN->registeredDecorations.push_back(pDecoration); + + pWindow->m_dWindowDecorations.emplace_back(pDecoration); + + return true; +} + +APICALL bool HyprlandAPI::removeWindowDecoration(HANDLE handle, IHyprWindowDecoration* pDecoration) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return false; + + for (auto& w : g_pCompositor->m_vWindows) { + for (auto& d : w->m_dWindowDecorations) { + if (d.get() == pDecoration) { + std::erase(w->m_dWindowDecorations, d); + return true; + } + } + } + + return false; +} + +APICALL bool HyprlandAPI::addConfigValue(HANDLE handle, const std::string& name, const SConfigValue& value) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!g_pPluginSystem->m_bAllowConfigVars) + return false; + + if (!PLUGIN) + return false; + + if (name.find("plugin:") != 0) + return false; + + g_pConfigManager->addPluginConfigVar(handle, name, value); + return true; +} + +APICALL SConfigValue* HyprlandAPI::getConfigValue(HANDLE handle, const std::string& name) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return nullptr; + + return g_pConfigManager->getConfigValuePtrSafe(name); +} + +APICALL void* HyprlandAPI::getFunctionAddressFromSignature(HANDLE handle, const std::string& sig) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return nullptr; + + return dlsym(nullptr, sig.c_str()); +} + +APICALL bool HyprlandAPI::addDispatcher(HANDLE handle, const std::string& name, std::function handler) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return false; + + PLUGIN->registeredDispatchers.push_back(name); + + g_pKeybindManager->m_mDispatchers[name] = handler; + + return true; +} + +APICALL bool HyprlandAPI::removeDispatcher(HANDLE handle, const std::string& name) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return false; + + std::erase_if(g_pKeybindManager->m_mDispatchers, [&](const auto& other) { return other.first == name; }); + std::erase_if(PLUGIN->registeredDispatchers, [&](const auto& other) { return other == name; }); + + return true; +} \ No newline at end of file diff --git a/src/plugins/PluginAPI.hpp b/src/plugins/PluginAPI.hpp new file mode 100644 index 00000000..756a5631 --- /dev/null +++ b/src/plugins/PluginAPI.hpp @@ -0,0 +1,217 @@ +#pragma once + +/* + +Hyprland Plugin API. + +Most documentation will be made with comments in this code, but more info can be also found on the wiki. + +!WARNING! +The Hyprland API passes C++ objects over, so no ABI compatibility is guaranteed. +Make sure to compile your plugins with the same compiler as Hyprland, and ideally, +on the same machine. + +See examples/examplePlugin for an example plugin + +*/ + +#define HYPRLAND_API_VERSION "0.1" + +#include "../helpers/Color.hpp" +#include "HookSystem.hpp" + +#include +#include +#include + +typedef std::function HOOK_CALLBACK_FN; +typedef struct { + std::string name; + std::string description; + std::string author; + std::string version; +} PLUGIN_DESCRIPTION_INFO; + +#define APICALL extern "C" +#define EXPORT __attribute__((visibility("default"))) +#define REQUIRED +#define OPTIONAL +#define HANDLE void* + +class IHyprLayout; +class CWindow; +class IHyprWindowDecoration; +struct SConfigValue; + +/* + These methods are for the plugin to implement + Methods marked with REQUIRED are required. +*/ + +/* + called pre-plugin init. + In case of a version mismatch, will eject the .so. + + This function should not be modified, see the example plugin. +*/ +typedef REQUIRED std::string (*PPLUGIN_API_VERSION_FUNC)(); +#define PLUGIN_API_VERSION pluginAPIVersion +#define PLUGIN_API_VERSION_FUNC_STR "pluginAPIVersion" + +/* + called on plugin init. Passes a handle as the parameter, which the plugin should keep for identification later. + The plugin should return a PLUGIN_DESCRIPTION_INFO struct with information about itself. + + Keep in mind this is executed synchronously, and as such any blocking calls to hyprland might hang. (e.g. system("hyprctl ...")) +*/ +typedef REQUIRED PLUGIN_DESCRIPTION_INFO (*PPLUGIN_INIT_FUNC)(HANDLE); +#define PLUGIN_INIT pluginInit +#define PLUGIN_INIT_FUNC_STR "pluginInit" + +/* + called on plugin unload, if that was a user action. If the plugin is being unloaded by an error, + this will not be called. + + Hooks are unloaded after exit. +*/ +typedef OPTIONAL void (*PPLUGIN_EXIT_FUNC)(void); +#define PLUGIN_EXIT pluginExit +#define PLUGIN_EXIT_FUNC_STR "pluginExit" + +/* + End plugin methods +*/ + +namespace HyprlandAPI { + + /* + Add a config value. + All config values MUST be in the plugin: namespace + This method may only be called in "pluginInit" + + After you have registered ALL of your config values, you may call `getConfigValue` + + returns: true on success, false on fail + */ + APICALL bool addConfigValue(HANDLE handle, const std::string& name, const SConfigValue& value); + + /* + Get a config value. + + returns: a pointer to the config value struct, which is guaranteed to be valid for the life of this plugin, unless another `addConfigValue` is called afterwards. + nullptr on error. + */ + APICALL SConfigValue* getConfigValue(HANDLE handle, const std::string& name); + + /* + Register a static (pointer) callback to a selected event. + Pointer must be kept valid until unregisterCallback() is called. + + returns: true on success, false on fail + */ + APICALL bool registerCallbackStatic(HANDLE handle, const std::string& event, HOOK_CALLBACK_FN* fn); + + /* + Register a dynamic (function) callback to a selected event. + Pointer will be free'd by Hyprland on unregisterCallback(). + + returns: a pointer to the newly allocated function. nullptr on fail. + */ + APICALL HOOK_CALLBACK_FN* registerCallbackDynamic(HANDLE handle, const std::string& event, HOOK_CALLBACK_FN fn); + + /* + Unregisters a callback. If the callback was dynamic, frees the memory. + + returns: true on success, false on fail + */ + APICALL bool unregisterCallback(HANDLE handle, HOOK_CALLBACK_FN* fn); + + /* + Calls a hyprctl command. + + returns: the output (as in hyprctl) + */ + APICALL std::string invokeHyprctlCommand(const std::string& call, const std::string& args, const std::string& format = ""); + + /* + Adds a layout to Hyprland. + + returns: true on success. False otherwise. + */ + APICALL bool addLayout(HANDLE handle, const std::string& name, IHyprLayout* layout); + + /* + Removes an added layout from Hyprland. + + returns: true on success. False otherwise. + */ + APICALL bool removeLayout(HANDLE handle, IHyprLayout* layout); + + /* + Queues a config reload. Does not take effect immediately. + + returns: true on success. False otherwise. + */ + APICALL bool reloadConfig(); + + /* + Adds a notification. + + returns: true on success. False otherwise. + */ + APICALL bool addNotification(HANDLE handle, const std::string& text, const CColor& color, const float timeMs); + + /* + Creates a trampoline function hook to an internal hl func. + + returns: CFunctionHook* + + !WARNING! Hooks are *not* guaranteed any API stability. Internal methods may be removed, added, or renamed. Consider preferring the API whenever possible. + */ + APICALL CFunctionHook* createFunctionHook(HANDLE handle, const void* source, const void* destination); + + /* + Removes a trampoline function hook. Will unhook if still hooked. + + returns: true on success. False otherwise. + + !WARNING! Hooks are *not* guaranteed any API stability. Internal methods may be removed, added, or renamed. Consider preferring the API whenever possible. + */ + APICALL bool removeFunctionHook(HANDLE handle, CFunctionHook* hook); + + /* + Gets a function address from a signature. + This is useful for hooking private functions. + + returns: function address, or nullptr on fail. + */ + APICALL void* getFunctionAddressFromSignature(HANDLE handle, const std::string& sig); + + /* + Adds a window decoration to a window + + returns: true on success. False otherwise. + */ + APICALL bool addWindowDecoration(HANDLE handle, CWindow* pWindow, IHyprWindowDecoration* pDecoration); + + /* + Removes a window decoration + + returns: true on success. False otherwise. + */ + APICALL bool removeWindowDecoration(HANDLE handle, IHyprWindowDecoration* pDecoration); + + /* + Adds a keybind dispatcher. + + returns: true on success. False otherwise. + */ + APICALL bool addDispatcher(HANDLE handle, const std::string& name, std::function handler); + + /* + Removes a keybind dispatcher. + + returns: true on success. False otherwise. + */ + APICALL bool removeDispatcher(HANDLE handle, const std::string& name); +}; \ No newline at end of file diff --git a/src/plugins/PluginSystem.cpp b/src/plugins/PluginSystem.cpp new file mode 100644 index 00000000..5b15782d --- /dev/null +++ b/src/plugins/PluginSystem.cpp @@ -0,0 +1,139 @@ +#include "PluginSystem.hpp" + +#include +#include "../Compositor.hpp" + +CPluginSystem::CPluginSystem() { + g_pFunctionHookSystem = std::make_unique(); +} + +CPlugin* CPluginSystem::loadPlugin(const std::string& path) { + + if (getPluginByPath(path)) { + Debug::log(ERR, " [PluginSystem] Cannot load a plugin twice!"); + return nullptr; + } + + auto* const PLUGIN = m_vLoadedPlugins.emplace_back(std::make_unique()).get(); + + PLUGIN->path = path; + + HANDLE MODULE = dlopen(path.c_str(), RTLD_LAZY); + + if (!MODULE) { + Debug::log(ERR, " [PluginSystem] Plugin %s could not be loaded: %s", path.c_str(), dlerror()); + m_vLoadedPlugins.pop_back(); + return nullptr; + } + + PLUGIN->m_pHandle = MODULE; + + PPLUGIN_API_VERSION_FUNC apiVerFunc = (PPLUGIN_API_VERSION_FUNC)dlsym(MODULE, PLUGIN_API_VERSION_FUNC_STR); + PPLUGIN_INIT_FUNC initFunc = (PPLUGIN_INIT_FUNC)dlsym(MODULE, PLUGIN_INIT_FUNC_STR); + + if (!apiVerFunc || !initFunc) { + Debug::log(ERR, " [PluginSystem] Plugin %s could not be loaded. (No apiver/init func)", path.c_str()); + dlclose(MODULE); + m_vLoadedPlugins.pop_back(); + return nullptr; + } + + const std::string PLUGINAPIVER = apiVerFunc(); + + if (PLUGINAPIVER != HYPRLAND_API_VERSION) { + Debug::log(ERR, " [PluginSystem] Plugin %s could not be loaded. (API version mismatch)", path.c_str()); + dlclose(MODULE); + m_vLoadedPlugins.pop_back(); + return nullptr; + } + + PLUGIN_DESCRIPTION_INFO PLUGINDATA; + + try { + if (!setjmp(m_jbPluginFaultJumpBuf)) { + m_bAllowConfigVars = true; + PLUGINDATA = initFunc(MODULE); + } else { + // this module crashed. + throw std::exception(); + } + } catch (std::exception& e) { + m_bAllowConfigVars = false; + Debug::log(ERR, " [PluginSystem] Plugin %s (Handle %lx) crashed in init. Unloading.", path.c_str(), MODULE); + unloadPlugin(PLUGIN, true); // Plugin could've already hooked/done something + return nullptr; + } + + m_bAllowConfigVars = false; + + PLUGIN->author = PLUGINDATA.author; + PLUGIN->description = PLUGINDATA.description; + PLUGIN->version = PLUGINDATA.version; + PLUGIN->name = PLUGINDATA.name; + + Debug::log(LOG, " [PluginSystem] Plugin %s loaded. Handle: %lx, path: \"%s\", author: \"%s\", description: \"%s\", version: \"%s\"", PLUGINDATA.name.c_str(), MODULE, + path.c_str(), PLUGINDATA.author.c_str(), PLUGINDATA.description.c_str(), PLUGINDATA.version.c_str()); + + return PLUGIN; +} + +void CPluginSystem::unloadPlugin(const CPlugin* plugin, bool eject) { + if (!plugin) + return; + + if (!eject) { + PPLUGIN_EXIT_FUNC exitFunc = (PPLUGIN_EXIT_FUNC)dlsym(plugin->m_pHandle, PLUGIN_EXIT_FUNC_STR); + if (exitFunc) + exitFunc(); + } + + for (auto& [k, v] : plugin->registeredCallbacks) + g_pHookSystem->unhook(v); + + const auto ls = plugin->registeredLayouts; + for (auto& l : ls) + g_pLayoutManager->removeLayout(l); + + g_pFunctionHookSystem->removeAllHooksFrom(plugin->m_pHandle); + + const auto rd = plugin->registeredDecorations; + for (auto& d : rd) + HyprlandAPI::removeWindowDecoration(plugin->m_pHandle, d); + + const auto rdi = plugin->registeredDispatchers; + for (auto& d : rdi) + HyprlandAPI::removeDispatcher(plugin->m_pHandle, d); + + g_pConfigManager->removePluginConfig(plugin->m_pHandle); + + dlclose(plugin->m_pHandle); + + Debug::log(LOG, " [PluginSystem] Plugin %s unloaded.", plugin->name.c_str()); + + std::erase_if(m_vLoadedPlugins, [&](const auto& other) { return other->m_pHandle == plugin->m_pHandle; }); +} + +CPlugin* CPluginSystem::getPluginByPath(const std::string& path) { + for (auto& p : m_vLoadedPlugins) { + if (p->path == path) + return p.get(); + } + + return nullptr; +} + +CPlugin* CPluginSystem::getPluginByHandle(HANDLE handle) { + for (auto& p : m_vLoadedPlugins) { + if (p->m_pHandle == handle) + return p.get(); + } + + return nullptr; +} + +std::vector CPluginSystem::getAllPlugins() { + std::vector results(m_vLoadedPlugins.size()); + for (size_t i = 0; i < m_vLoadedPlugins.size(); ++i) + results[i] = m_vLoadedPlugins[i].get(); + return results; +} \ No newline at end of file diff --git a/src/plugins/PluginSystem.hpp b/src/plugins/PluginSystem.hpp new file mode 100644 index 00000000..041ee47e --- /dev/null +++ b/src/plugins/PluginSystem.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include "../defines.hpp" +#include "PluginAPI.hpp" +#include + +class IHyprWindowDecoration; + +class CPlugin { + public: + std::string name = ""; + std::string description = ""; + std::string author = ""; + std::string version = ""; + + std::string path = ""; + + HANDLE m_pHandle = nullptr; + + std::vector registeredLayouts; + std::vector registeredDecorations; + std::vector> registeredCallbacks; + std::vector registeredDispatchers; +}; + +class CPluginSystem { + public: + CPluginSystem(); + + CPlugin* loadPlugin(const std::string& path); + void unloadPlugin(const CPlugin* plugin, bool eject = false); + CPlugin* getPluginByPath(const std::string& path); + CPlugin* getPluginByHandle(HANDLE handle); + std::vector getAllPlugins(); + + bool m_bAllowConfigVars = false; + + private: + std::vector> m_vLoadedPlugins; + + jmp_buf m_jbPluginFaultJumpBuf; +}; + +inline std::unique_ptr g_pPluginSystem; \ No newline at end of file diff --git a/src/render/decorations/IHyprWindowDecoration.hpp b/src/render/decorations/IHyprWindowDecoration.hpp index 093b99cf..4cd52cba 100644 --- a/src/render/decorations/IHyprWindowDecoration.hpp +++ b/src/render/decorations/IHyprWindowDecoration.hpp @@ -6,7 +6,8 @@ enum eDecorationType { DECORATION_NONE = -1, DECORATION_GROUPBAR, - DECORATION_SHADOW + DECORATION_SHADOW, + DECORATION_CUSTOM }; struct SWindowDecorationExtents { diff --git a/subprojects/udis86 b/subprojects/udis86 new file mode 160000 index 00000000..5336633a --- /dev/null +++ b/subprojects/udis86 @@ -0,0 +1 @@ +Subproject commit 5336633af70f3917760a6d441ff02d93477b0c86 -- cgit v1.2.3