diff --git a/include/func_finder.hpp b/include/func_finder.hpp new file mode 100644 index 0000000..2e33376 --- /dev/null +++ b/include/func_finder.hpp @@ -0,0 +1,9 @@ +#include "globals.hpp" + +/* Find function pointer from the Hyprland binary by it's name and demangledName + * + * \param[in] name Simple name of the function (i.e. createGroup) + * \param[in] demangledName Demangled name of the function (i.e. CWindow::createGroup()) + * \return Raw pointer to the found function + */ +void* findHyprlandFunction(const std::string& name, const std::string& demangledName); diff --git a/include/globals.hpp b/include/globals.hpp index 66c4f04..b7b7ae1 100644 --- a/include/globals.hpp +++ b/include/globals.hpp @@ -1,5 +1,19 @@ #pragma once +#include +#include #include +const CColor s_notifyColor = {0x61 / 255.0f, 0xAF / 255.0f, 0xEF / 255.0f, 1.0f}; // RGBA +const PLUGIN_DESCRIPTION_INFO s_pluginDescription = {"dwindle-autogroup", "Dwindle Autogroup", "ItsDrike", "1.0"}; + inline HANDLE PHANDLE = nullptr; + +typedef void* (*createGroupFuncT)(CWindow*); +inline CFunctionHook* g_pCreateGroupHook = nullptr; + +typedef void* (*destroyGroupFuncT)(CWindow*); +inline CFunctionHook* g_pDestroyGroupHook = nullptr; + +typedef SDwindleNodeData* (*nodeFromWindowFuncT)(void*, CWindow*); +inline nodeFromWindowFuncT g_pNodeFromWindow = nullptr; diff --git a/include/plugin.hpp b/include/plugin.hpp new file mode 100644 index 0000000..82b0bb2 --- /dev/null +++ b/include/plugin.hpp @@ -0,0 +1,9 @@ +#include "globals.hpp" + +/* New custom function replacing the original CWindow::createGroup function + */ +void newCreateGroup(CWindow*); + +/* New custom function replacing the original CWindow::createGroup function + */ +void newDestroyGroup(CWindow*); diff --git a/src/func_finder.cpp b/src/func_finder.cpp new file mode 100644 index 0000000..ec4bf7e --- /dev/null +++ b/src/func_finder.cpp @@ -0,0 +1,62 @@ +#include "func_finder.hpp" + +/* Debug function for converting vector of strings to pretty-printed representation + * + * \param[in] Vector of strings + * \return Pretty printed single-line representation of the vector + */ +std::string vectorToString(const std::vector& vec) +{ + std::ostringstream oss; + oss << "["; + + for (size_t i = 0; i < vec.size(); ++i) { + oss << std::quoted(vec[i]); + + // Add a comma after each element except the last one + if (i < vec.size() - 1) { + oss << ", "; + } + } + + oss << "]"; + return oss.str(); +} + +void* findHyprlandFunction(const std::string& name, const std::string& demangledName) +{ + const auto METHODS = HyprlandAPI::findFunctionsByName(PHANDLE, name); + + if (METHODS.size() == 0) { + Debug::log( + ERR, "[dwindle-autogroup] Function {} wasn't found in Hyprland binary! This function's signature probably changed, report this", name); + HyprlandAPI::addNotification(PHANDLE, "[dwindle-autogroup] Initialization failed! " + name + " function wasn't found", s_notifyColor, 10000); + return nullptr; + } + + void* res = nullptr; + for (auto& method : METHODS) { + if (method.demangled == demangledName) { + res = method.address; + break; + } + } + // Use std::transform to extract the demangled strings from function matches + std::vector demangledStrings; + std::transform(METHODS.begin(), METHODS.end(), std::back_inserter(demangledStrings), [](const SFunctionMatch& match) { return match.demangled; }); + + if (res == nullptr) { + Debug::log(ERR, + "[dwindle-autogroup] Demangled function {} wasn't found in matches for function name {} in Hyprland binary! This function's " + "signature probably " + "changed, report this. Found matches: {}", + demangledName, + name, + vectorToString(demangledStrings)); + HyprlandAPI::addNotification( + PHANDLE, "[dwindle-autogroup] Initialization failed! " + name + " function (demangled) wasn't found", s_notifyColor, 10000); + return nullptr; + } + + return res; +} diff --git a/src/main.cpp b/src/main.cpp index 5001a7b..b836ddb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,191 +1,6 @@ +#include "func_finder.hpp" #include "globals.hpp" - -#include -#include -#include -#include -#include -#include -#include - -const CColor s_pluginColor = {0x61 / 255.0f, 0xAF / 255.0f, 0xEF / 255.0f, 1.0f}; - -inline std::function originalToggleGroup = nullptr; - -typedef SDwindleNodeData* (*nodeFromWindowT)(void*, CWindow*); -inline nodeFromWindowT g_pNodeFromWindow = nullptr; - -void collectChildNodes(std::deque* pDeque, SDwindleNodeData* node) -{ - if (node->isNode) { - collectChildNodes(pDeque, node->children[0]); - collectChildNodes(pDeque, node->children[1]); - } - else { - pDeque->emplace_back(node); - } -} - -/// This is partially from CKeybindManager::moveIntoGroup (dispatcher) function -/// but without making the new window focused. -void moveIntoGroup(CWindow* window, CWindow* groupRootWin) -{ - // Remove this window from being shown by layout (it will become a part of a group) - g_pLayoutManager->getCurrentLayout()->onWindowRemoved(window); - - // Create a group bar decoration for the window - // (if it's not already a group, in which case it should already have it) - if (!window->m_sGroupData.pNextWindow) - window->m_dWindowDecorations.emplace_back(std::make_unique(window)); - - groupRootWin->insertWindowToGroup(window); - - // Make sure to treat this window as hidden (will focus the group instead of this window - // on request activate). This is the behavior CWindow::setGroupCurrent uses. - window->setHidden(true); -} - -void collectGroupWindows(std::deque* pDeque, CWindow* pWindow) -{ - CWindow* curr = pWindow; - do { - const auto PLAST = curr; - pDeque->emplace_back(curr); - curr = curr->m_sGroupData.pNextWindow; - } while (curr != pWindow); -} - -void groupDissolve(const SDwindleNodeData* PNODE, CHyprDwindleLayout* layout) -{ - CWindow* PWINDOW = PNODE->pWindow; - - // This group only has this single winodw - if (PWINDOW->m_sGroupData.pNextWindow == PWINDOW) { - Debug::log(LOG, "Ignoring autogroup on single window dissolve"); - originalToggleGroup(""); - return; - } - - // We could just call originalToggleGroup function here, but that fucntion doesn't - // respect the dwindle layout, and would just place the newly ungroupped windows - // randomly throughout the worskapce, messing up the layout. So instead, we replicate - // it's behavior here manually, taking care to disolve the groups nicely. - Debug::log(LOG, "Dissolving group"); - - std::deque members; - collectGroupWindows(&members, PWINDOW); - - // If the group head window is in fullscreen, unfullscreen it. - // We need to have the window placed in the layout, to figure out where - // to ungroup the rest of the windows. - g_pCompositor->setWindowFullscreen(PWINDOW, false, FULLSCREEN_FULL); - - Debug::log(LOG, "Ungroupping Members"); - - const bool GROUPSLOCKEDPREV = g_pKeybindManager->m_bGroupsLocked; - - for (auto& w : members) { - w->m_sGroupData.pNextWindow = nullptr; - w->setHidden(false); - - // Ask layout to create a new window for all windows that were in the group - // except for the group head (already has a window). - if (w->m_sGroupData.head) { - Debug::log(LOG, "Ungroupping member head window"); - w->m_sGroupData.head = false; - - // Update the window decorations (removing group bar) - // w->updateWindowDecos(); - } - else { - Debug::log(LOG, "Ungroupping member non-head window"); - g_pLayoutManager->getCurrentLayout()->onWindowCreatedTiling(w); - // Focus the window that we just spawned, so that on the next iteration - // the window created will be it's dwindle child node. - // This allows the original group head to remain a parent window to all - // of the other (groupped) nodes - g_pCompositor->focusWindow(w); - } - } - - g_pKeybindManager->m_bGroupsLocked = GROUPSLOCKEDPREV; - - g_pCompositor->updateAllWindowsAnimatedDecorationValues(); - - // Leave with the focus the original group (head) window - g_pCompositor->focusWindow(PWINDOW); -} - -void groupCreate(const SDwindleNodeData* PNODE, CHyprDwindleLayout* layout) -{ - const auto PWINDOW = PNODE->pWindow; - - const auto PWORKSPACE = g_pCompositor->getWorkspaceByID(PNODE->workspaceID); - if (PWORKSPACE->m_bHasFullscreenWindow) { - Debug::log(LOG, "Ignoring autogroup on a fullscreen window"); - originalToggleGroup(""); - return; - } - - if (!PNODE->pParent) { - Debug::log(LOG, "Ignoring autogroup for a solitary window"); - originalToggleGroup(""); - return; - } - - Debug::log(LOG, "Creating group"); - // Call the original toggleGroup function, and only do extra things afterwards - originalToggleGroup(""); - - std::deque newGroupMembers; - collectChildNodes(&newGroupMembers, PNODE->pParent->children[0] == PNODE ? PNODE->pParent->children[1] : PNODE->pParent->children[0]); - - // Make sure one of the child nodes isn't itself a group - for (auto& n : newGroupMembers) { - if (n->pWindow->m_sGroupData.pNextWindow) { - Debug::log(LOG, "Ignoring autogroup for nested groups"); - return; - } - } - - // Add all of the children nodes into the group - for (auto& n : newGroupMembers) { - auto window = n->pWindow; - moveIntoGroup(window, PWINDOW); - } -} - -void toggleGroup(std::string args) -{ - // We only care about group creations, not group disolves - const auto PWINDOW = g_pCompositor->m_pLastWindow; - if (!PWINDOW || !g_pCompositor->windowValidMapped(PWINDOW)) - return; - - // Don't do anything if we're not on "dwindle" layout - IHyprLayout* layout = g_pLayoutManager->getCurrentLayout(); - if (layout->getLayoutName() != "dwindle") { - Debug::log(LOG, "Ignoring autogroup for non-dinwle layout"); - originalToggleGroup(args); - return; - } - - CHyprDwindleLayout* cur_dwindle = (CHyprDwindleLayout*)layout; - - const auto PNODE = g_pNodeFromWindow(cur_dwindle, PWINDOW); - if (!PNODE) { - Debug::log(LOG, "Ignoring autogroup for floating window"); - originalToggleGroup(args); - return; - } - - if (PWINDOW->m_sGroupData.pNextWindow) { - groupDissolve(PNODE, cur_dwindle); - } - else { - groupCreate(PNODE, cur_dwindle); - } -} +#include "plugin.hpp" // Do NOT change this function. APICALL EXPORT std::string PLUGIN_API_VERSION() @@ -193,44 +8,53 @@ APICALL EXPORT std::string PLUGIN_API_VERSION() return HYPRLAND_API_VERSION; } +APICALL EXPORT void PLUGIN_EXIT() +{ + // Unhook the overridden functions and remove the hooks + if (g_pCreateGroupHook) { + g_pCreateGroupHook->unhook(); + HyprlandAPI::removeFunctionHook(PHANDLE, g_pCreateGroupHook); + g_pCreateGroupHook = nullptr; + } + if (g_pDestroyGroupHook) { + g_pDestroyGroupHook->unhook(); + HyprlandAPI::removeFunctionHook(PHANDLE, g_pDestroyGroupHook); + g_pCreateGroupHook = nullptr; + } + + // Plugin unloading was successful + HyprlandAPI::addNotification(PHANDLE, "[dwindle-autogroup] Unloaded successfully!", s_notifyColor, 5000); +} + APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { PHANDLE = handle; - // Get address of the private CHyprDwindleLayout::getNodeFromWindow member function, we'll need it in toggleGroup - static const auto METHODS = HyprlandAPI::findFunctionsByName(PHANDLE, "getNodeFromWindow"); - g_pNodeFromWindow = (nodeFromWindowT)METHODS[1].address; + Debug::log(LOG, "[dwindle-autogroup] Loading Hyprland functions"); - for (auto& method : METHODS) { - if (method.demangled == "CHyprDwindleLayout::getNodeFromWindow(CWindow*)") { - g_pNodeFromWindow = (nodeFromWindowT)method.address; - break; - } + // Find pointers to functions by name (from the Hyprland binary) + g_pNodeFromWindow = (nodeFromWindowFuncT)findHyprlandFunction("getNodeFromWindow", "\nCHyprDwindleLayout::getNodeFromWindow(CWindow*)"); + auto pCreateGroup = findHyprlandFunction("createGroup", "\nCWindow::createGroup()"); + auto pDestroyGroup = findHyprlandFunction("destroyGroup", "\nCWindow::destroyGroup()"); + + // Return immediately if one of the functions wasn't found + if (!g_pNodeFromWindow || !pCreateGroup || !pDestroyGroup) { + // Set all of the global function pointers to NULL, to avoid any potential issues + g_pNodeFromWindow = nullptr; + + return s_pluginDescription; } - if (g_pNodeFromWindow == nullptr) { - Debug::log(ERR, "getNodeFromWindow funnction for dwindle layout wasn't found! This function's signature probably changed, report this"); - HyprlandAPI::addNotification( - PHANDLE, "[dwindle-autogroup] Initialization failed!! getNodeFromWindow functio not found", s_pluginColor, 10000); - } - else { - originalToggleGroup = g_pKeybindManager->m_mDispatchers["togglegroup"]; - HyprlandAPI::addDispatcher(PHANDLE, "togglegroup", toggleGroup); + Debug::log(LOG, "[dwindle-autogroup] Registering function hooks"); - HyprlandAPI::reloadConfig(); + // Register function hooks, for overriding the original group methods + g_pCreateGroupHook = HyprlandAPI::createFunctionHook(PHANDLE, pCreateGroup, (void*)&newCreateGroup); + g_pDestroyGroupHook = HyprlandAPI::createFunctionHook(PHANDLE, pDestroyGroup, (void*)&newDestroyGroup); - HyprlandAPI::addNotification(PHANDLE, "[dwindle-autogroup] Initialized successfully!", s_pluginColor, 5000); - } + // Initialize the hooks, from now on, the original functions will be overridden + g_pCreateGroupHook->hook(); + g_pDestroyGroupHook->hook(); - return {"dwindle-autogroup", "Dwindle Autogroup", "ItsDrike", "1.0"}; -} - -APICALL EXPORT void PLUGIN_EXIT() -{ - // Since we added the "togglegroup" dispatcher ourselves, by default, the cleanup would just remove it - // but we want to restore it back to the original function instead - HyprlandAPI::removeDispatcher(PHANDLE, "togglegroup"); - g_pKeybindManager->m_mDispatchers["togglegroup"] = originalToggleGroup; - - HyprlandAPI::addNotification(PHANDLE, "[dwindle-autogroup] Unloaded successfully!", s_pluginColor, 5000); + HyprlandAPI::addNotification(PHANDLE, "[dwindle-autogroup] Initialized successfully!", s_notifyColor, 5000); + return s_pluginDescription; } diff --git a/src/plugin.cpp b/src/plugin.cpp new file mode 100644 index 0000000..d2be3db --- /dev/null +++ b/src/plugin.cpp @@ -0,0 +1,226 @@ +#include "plugin.hpp" +#include "globals.hpp" +#include +#include +#include +#include +#include + +/*! Recursively collect all dwindle child nodes for given root node + * + * \param[out] pDeque deque to store the found child nodes into + * \param[in] pNode Dwindle node which children should be collected + */ +void collectDwindleChildNodes(std::deque* pDeque, SDwindleNodeData* pNode) +{ + if (pNode->isNode) { + collectDwindleChildNodes(pDeque, pNode->children[0]); + collectDwindleChildNodes(pDeque, pNode->children[1]); + } + else { + pDeque->emplace_back(pNode); + } +} + +/*! Collect all windows that belong to the same group + * + * \param[out] pDeque deque to store the found group windows into + * \param[in] pWindow any window that belongs to a group (doesn't have to be the group head window) + */ +void collectGroupWindows(std::deque* pDeque, CWindow* pWindow) +{ + CWindow* curr = pWindow; + do { + const auto PLAST = curr; + pDeque->emplace_back(curr); + curr = curr->m_sGroupData.pNextWindow; + } while (curr != pWindow); +} + +/*! Move given window into a group + * + * This is almost the same as CKeybindManager::moveWindowIntoGroup (dispatcher) function, + * but without making the new window a group head and focused. + * + * \param[in] pWindow Window to be inserted into a group + * \param[in] pGroupWindow Window that's a part of a group to insert the pWindow into + */ +void moveIntoGroup(CWindow* pWindow, CWindow* pGroupHeadWindow) +{ + const auto P_LAYOUT = g_pLayoutManager->getCurrentLayout(); + + const auto* USE_CURR_POS = &g_pConfigManager->getConfigValuePtr("misc:group_insert_after_current")->intValue; + CWindow* pGroupWindow = *USE_CURR_POS ? pGroupHeadWindow : pGroupHeadWindow->getGroupTail(); + + // Remove the window from layout (will become a part of a group) + P_LAYOUT->onWindowRemoved(pWindow); + + // Create a group bar decoration for the window we'll be inserting + // (if it's not already a group, in which case it should already have it) + if (!pWindow->m_sGroupData.pNextWindow) + pWindow->m_dWindowDecorations.emplace_back(std::make_unique(pWindow)); + + pGroupWindow->insertWindowToGroup(pWindow); + pGroupHeadWindow->updateWindowDecos(); + pWindow->setHidden(true); + + g_pLayoutManager->getCurrentLayout()->recalculateWindow(pGroupHeadWindow); +} + +/*! Check common pre-conditions for group creation/deletion and perform needed initializations + * + * \param[out] pDwindleLayout Pointer to dwindle layout instance + * \return Necessary pre-conditions succeded? + */ +bool handleGroupOperation(CHyprDwindleLayout** pDwindleLayout) +{ + const auto P_LAYOUT = g_pLayoutManager->getCurrentLayout(); + if (P_LAYOUT->getLayoutName() != "dwindle") { + Debug::log(LOG, "[dwindle-autogroup] Ignoring non-dwindle layout"); + return false; + } + + *pDwindleLayout = dynamic_cast(P_LAYOUT); + return true; +} + +void newCreateGroup(CWindow* self) +{ + // Run the original function first, creating a "classical" group + // with just the currently selected window in it + ((createGroupFuncT)g_pCreateGroupHook->m_pOriginal)(self); + + // Only continue if the group really was created, as there are some pre-conditions to that. + if (!self->m_sGroupData.pNextWindow) { + Debug::log(LOG, "[dwindle-autogroup] Ignoring autogroup - invalid / non-group widnow"); + return; + } + + Debug::log(LOG, "[dwindle-autogroup] Triggered createGroup for {:x}", self); + + // Obtain an instance of the dwindle layout, also run some general pre-conditions + // for the plugin, quit now if they're not met. + CHyprDwindleLayout* pDwindleLayout = nullptr; + if (!handleGroupOperation(&pDwindleLayout)) + return; + + Debug::log(LOG, "[dwindle-autogroup] Autogroupping dwindle child nodes"); + + // Collect all child dwindle nodes, we'll want to add all of those into a group + const auto P_DWINDLE_NODE = g_pNodeFromWindow(pDwindleLayout, self); + std::deque p_dDwindleNodes; + const auto P_SIBLING_NODE = + P_DWINDLE_NODE->pParent->children[0] == P_DWINDLE_NODE ? P_DWINDLE_NODE->pParent->children[1] : P_DWINDLE_NODE->pParent->children[0]; + collectDwindleChildNodes(&p_dDwindleNodes, P_SIBLING_NODE); + + // Stop if one of the dwindle child nodes is already in a group + for (auto& node : p_dDwindleNodes) { + auto curWindow = node->pWindow; + if (curWindow->m_sGroupData.pNextWindow) { + Debug::log(LOG, "[dwindle-autogroup] Ignoring autogroup for nested groups: window {:x} is group", curWindow); + return; + } + } + + Debug::log(LOG, "[dwindle-autogroup] Found {} dwindle nodes to autogroup", p_dDwindleNodes.size()); + + // Add all of the dwindle child node widnows into the group + for (auto& node : p_dDwindleNodes) { + auto curWindow = node->pWindow; + Debug::log(LOG, "[dwindle-autogroup] Moving window {:x} into group", curWindow); + moveIntoGroup(curWindow, self); + } + + Debug::log(LOG, "[dwindle-autogroup] Autogroup done, {} child nodes moved", p_dDwindleNodes.size()); +} + +void newDestroyGroup(CWindow* self) +{ + // We can't use the original function here (other than for falling back) + // as it removes the group head and then creates the new windows on the + // layout. This often messes up the user layout of the windows. + // + // The goal of this function is to ungroup the windows such that they + // only continue as children from the dwindle binary tree node the group + // head was on. + + // Only continue if the window isn't in a group + if (!self->m_sGroupData.pNextWindow) { + Debug::log(LOG, "[dwindle-autogroup] Ignoring ungroup - invalid / non-group widnow"); + return; + } + + Debug::log(LOG, "[dwindle-autogroup] Triggered destroyGroup for {:x}", self); + + // Obtain an instance of the dwindle layout, also run some general pre-conditions + // for the plugin, fall back now if they're not met. + CHyprDwindleLayout* pDwindleLayout = nullptr; + if (!handleGroupOperation(&pDwindleLayout)) { + ((createGroupFuncT)g_pDestroyGroupHook->m_pOriginal)(self); + return; + } + + std::deque dGroupWindows; + collectGroupWindows(&dGroupWindows, self); + + // If the group head window is in fullscreen, unfullscreen it. + // We need to have the window placed in the layout, to figure out where + // to ungroup the rest of the windows. + g_pCompositor->setWindowFullscreen(self, false, FULLSCREEN_FULL); + + Debug::log(LOG, "[dwindle-autogroup] Ungroupping {} windows", dGroupWindows.size()); + + const bool GROUPS_LOCKED_PREV = g_pKeybindManager->m_bGroupsLocked; + g_pKeybindManager->m_bGroupsLocked = true; + + for (auto& pWindow : dGroupWindows) { + Debug::log(LOG, "[dwindle-autogroup] Ungroupping window {:x}", pWindow); + pWindow->m_sGroupData.pNextWindow = nullptr; + pWindow->m_sGroupData.head = false; + + // Current / Visible window (this isn't always the head) + if (!pWindow->isHidden()) { + Debug::log(LOG, "[dwindle-autogroup] -> Visible window ungroup"); + + // This window is already visible in the layout, we don't need to create + // a new layout window for it. + // + // The original destroyGroup removes the window from the layout here, + // which is what causes the weird ungroupping behavior as this window + // is then recreated, which spawns it in a potentially unexpected place + // (often determined by the cursor position). + + // Update the window decorations (removing group bar) + pWindow->updateWindowDecos(); + } + else { + pWindow->setHidden(false); + + g_pLayoutManager->getCurrentLayout()->onWindowCreatedTiling(pWindow); + pWindow->updateWindowDecos(); + + // Focus the window that we just spawned, so that on the next iteration + // the window created will be it's dwindle child node. + // This allows the original group head to remain a parent window to all + // of the other (groupped) nodes. + // + // Note that this won't preserve the exact original layout of the group + // but it will make sure all of the groupped windows will extend from + // the dwindle node of the group head window. Preserving the original + // layout isn't really possible, since new windows can be added into + // groups after they were created. + g_pCompositor->focusWindow(pWindow); + } + } + + g_pKeybindManager->m_bGroupsLocked = GROUPS_LOCKED_PREV; + + Debug::log(LOG, "[dwindle-autogroup] All windows ungroupped"); + + g_pCompositor->updateAllWindowsAnimatedDecorationValues(); + + // Leave with the focus the original (main) window + g_pCompositor->focusWindow(self); + + Debug::log(LOG, "[dwindle-autogroup] Ungroupping done"); +}