From ccbc0e4a2e4b1bcd9966b59edfbb0aaf6c27a52d Mon Sep 17 00:00:00 2001
From: vaxerski <43317083+vaxerski@users.noreply.github.com>
Date: Thu, 24 Mar 2022 18:22:01 +0100
Subject: [PATCH] Added window rules

---
 README.md                        |  2 +-
 example/hyprland.conf            | 16 ++++++++
 src/config/ConfigManager.cpp     | 57 +++++++++++++++++++++++++++
 src/config/ConfigManager.hpp     | 12 ++++++
 src/events/Windows.cpp           | 66 ++++++++++++++++++++++++++++++--
 src/layout/DwindleLayout.cpp     |  9 ++++-
 src/managers/XWaylandManager.cpp | 26 ++++++++++---
 src/managers/XWaylandManager.hpp |  1 +
 8 files changed, 178 insertions(+), 11 deletions(-)

diff --git a/README.md b/README.md
index 288015ab5..7b4c9007f 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ Nevertheless, REPORT any you find! Make an issue!
  - Config reloaded instantly upon saving
  - Easily expandable and readable codebase
  - Support for docks/whatever
+ - Window rules
  - Monitor rules
  - Socket-based IPC
  - Tiling/floating/fullscreen windows
@@ -30,7 +31,6 @@ Nevertheless, REPORT any you find! Make an issue!
  - Rounded corners
  - Blur
  - Fadein/out
- - Window rules
  - Fix electron rendering issues
  - Optimization
  - Fix weird scroll on XWayland
diff --git a/example/hyprland.conf b/example/hyprland.conf
index f2479eb80..524ac94b1 100644
--- a/example/hyprland.conf
+++ b/example/hyprland.conf
@@ -18,6 +18,22 @@ general {
     col.inactive_border=0x66333333
 }
 
+animations {
+    enabled=1
+    windows=1
+    borders=1 # not yet implemented
+    fadein=1 # not yet implemented
+}
+
+# example window rules
+# for windows named/classed as abc and xyz
+windowrule=move 69 420,abc
+windowrule=size 420 69,abc
+windowrule=tile,xyz
+windowrule=float,abc
+windowrule=monitor 0,xyz
+
+# example binds
 bind=SUPER,Q,exec,kitty
 bind=SUPER,C,killactive,
 bind=SUPER,M,exec,pkill Hyprland
diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp
index 2a481ed21..b29edacce 100644
--- a/src/config/ConfigManager.cpp
+++ b/src/config/ConfigManager.cpp
@@ -163,6 +163,30 @@ void CConfigManager::handleBind(const std::string& command, const std::string& v
         g_pKeybindManager->addKeybind(SKeybind{KEY, MOD, HANDLER, COMMAND});
 }
 
+void CConfigManager::handleWindowRule(const std::string& command, const std::string& value) {
+    const auto RULE = value.substr(0, value.find_first_of(","));
+    const auto VALUE = value.substr(value.find_first_of(",") + 1);
+
+    // check rule and value
+    if (RULE == "" || VALUE == "") {
+        return;
+    }
+
+    // verify we support a rule
+    if (RULE != "float" 
+        && RULE != "tile"
+        && RULE.find("move") != 0
+        && RULE.find("size") != 0
+        && RULE.find("monitor") != 0) {
+            Debug::log(ERR, "Invalid rule found: %s", RULE.c_str());
+            parseError = "Invalid rule found: " + RULE;
+            return;
+        }
+
+    m_dWindowRules.push_back({RULE, VALUE});
+
+}
+
 void CConfigManager::handleDefaultWorkspace(const std::string& command, const std::string& value) {
 
     const auto DISPLAY = value.substr(0, value.find_first_of(','));
@@ -230,6 +254,9 @@ void CConfigManager::parseLine(std::string& line) {
     } else if (COMMAND == "workspace") {
         handleDefaultWorkspace(COMMAND, VALUE);
         return;
+    } else if (COMMAND == "windowrule") {
+        handleWindowRule(COMMAND, VALUE);
+        return;
     }
 
     configSetValueSafe(currentCategory + (currentCategory == "" ? "" : ":") + COMMAND, VALUE);
@@ -241,6 +268,7 @@ void CConfigManager::loadConfigLoadVars() {
     currentCategory = "";  // reset the category
 
     m_dMonitorRules.clear();
+    m_dWindowRules.clear();
     g_pKeybindManager->clearKeybinds();
 
     const char* const ENVHOME = getenv("HOME");
@@ -357,4 +385,33 @@ SMonitorRule CConfigManager::getMonitorRuleFor(std::string name) {
     Debug::log(WARN, "No rules configured. Using the default hardcoded one.");
 
     return SMonitorRule{.name = "", .resolution = Vector2D(1280, 720), .offset = Vector2D(0, 0), .mfact = 0.5f, .scale = 1};
+}
+
+std::vector<SWindowRule> CConfigManager::getMatchingRules(CWindow* pWindow) {
+    if (!g_pCompositor->windowValidMapped(pWindow))
+        return std::vector<SWindowRule>();
+
+    std::vector<SWindowRule> returns;
+
+    std::string title = g_pXWaylandManager->getTitle(pWindow);
+    std::string appidclass = g_pXWaylandManager->getAppIDClass(pWindow);
+
+    for (auto& rule : m_dWindowRules) {
+        // check if we have a matching rule
+        try {
+            std::regex classCheck(rule.szValue);
+
+            if (!std::regex_search(title, classCheck) && !std::regex_search(appidclass, classCheck))
+                continue;
+        } catch (...) {
+            Debug::log(ERR, "Regex error at %s", rule.szValue.c_str());
+        }
+
+        // applies. Read the rule and behave accordingly
+        Debug::log(LOG, "Window rule %s -> %s matched %x [%s]", rule.szRule.c_str(), rule.szValue.c_str(), pWindow, pWindow->m_szTitle.c_str());
+
+        returns.push_back(rule);
+    }
+
+    return returns;
 }
\ No newline at end of file
diff --git a/src/config/ConfigManager.hpp b/src/config/ConfigManager.hpp
index c34aadb14..8c38efa54 100644
--- a/src/config/ConfigManager.hpp
+++ b/src/config/ConfigManager.hpp
@@ -6,6 +6,9 @@
 #include "../defines.hpp"
 #include <vector>
 #include <deque>
+#include <algorithm>
+#include <regex>
+#include "../Window.hpp"
 
 struct SConfigValue {
     int64_t intValue = -1;
@@ -23,6 +26,11 @@ struct SMonitorRule {
     int         defaultWorkspaceID = -1;
 };
 
+struct SWindowRule {
+    std::string szRule;
+    std::string szValue;
+};
+
 class CConfigManager {
 public:
     CConfigManager();
@@ -36,6 +44,8 @@ public:
 
     SMonitorRule        getMonitorRuleFor(std::string);
 
+    std::vector<SWindowRule> getMatchingRules(CWindow*);
+
 private:
     std::unordered_map<std::string, SConfigValue> configValues;
     time_t lastModifyTime = 0;  // for reloading the config if changed
@@ -47,6 +57,7 @@ private:
     bool isFirstLaunch = true;  // For exec-once
 
     std::deque<SMonitorRule> m_dMonitorRules;
+    std::deque<SWindowRule> m_dWindowRules;
 
     // internal methods
     void                loadConfigLoadVars();
@@ -56,6 +67,7 @@ private:
     void                handleRawExec(const std::string&, const std::string&);
     void                handleMonitor(const std::string&, const std::string&);
     void                handleBind(const std::string&, const std::string&);
+    void                handleWindowRule(const std::string&, const std::string&);
     void                handleDefaultWorkspace(const std::string&, const std::string&);
 };
 
diff --git a/src/events/Windows.cpp b/src/events/Windows.cpp
index c6f4e3524..0ee5e1581 100644
--- a/src/events/Windows.cpp
+++ b/src/events/Windows.cpp
@@ -36,13 +36,73 @@ void Events::listener_mapWindow(wl_listener* listener, void* data) {
 
     wl_signal_add(&PWINDOWSURFACE->events.new_subsurface, &PWINDOW->listen_newSubsurfaceWindow);
 
-    if (g_pXWaylandManager->shouldBeFloated(PWINDOW)) {
-        g_pLayoutManager->getCurrentLayout()->onWindowCreatedFloating(PWINDOW);
+    if (g_pXWaylandManager->shouldBeFloated(PWINDOW))
         PWINDOW->m_bIsFloating = true;
+
+    // window rules
+    const auto WINDOWRULES = g_pConfigManager->getMatchingRules(PWINDOW);
+
+    for (auto& r : WINDOWRULES) {
+        if (r.szRule.find("monitor") == 0) {
+            try {
+                const long int MONITOR = std::stoi(r.szRule.substr(r.szRule.find(" ")));
+
+                Debug::log(LOG, "Rule monitor, applying to window %x", PWINDOW);
+
+                if (MONITOR >= (long int)g_pCompositor->m_lMonitors.size() || MONITOR < (long int)0)
+                    PWINDOW->m_iMonitorID = 0;
+                else
+                    PWINDOW->m_iMonitorID = MONITOR;
+
+                PWINDOW->m_iWorkspaceID = g_pCompositor->getMonitorFromID(PWINDOW->m_iMonitorID)->activeWorkspace;
+            } catch (...) {
+                Debug::log(LOG, "Rule monitor failed, rule: %s -> %s", r.szRule.c_str(), r.szValue.c_str());
+            }
+        } else if (r.szRule.find("float") == 0) {
+            PWINDOW->m_bIsFloating = true;
+        } else if (r.szRule.find("tile") == 0) {
+            PWINDOW->m_bIsFloating = false;
+        }
+    }
+
+    if (PWINDOW->m_bIsFloating) {
+        g_pLayoutManager->getCurrentLayout()->onWindowCreatedFloating(PWINDOW);
+
+        // size and move rules
+        for (auto& r : WINDOWRULES) {
+            if (r.szRule.find("size") == 0) {
+                try {
+                    const auto VALUE = r.szRule.substr(r.szRule.find(" ") + 1);
+                    const auto SIZEX = stoi(VALUE.substr(0, VALUE.find(" ")));
+                    const auto SIZEY = stoi(VALUE.substr(VALUE.find(" ") + 1));
+
+                    Debug::log(LOG, "Rule size, applying to window %x", PWINDOW);
+
+                    PWINDOW->m_vEffectiveSize = Vector2D(SIZEX, SIZEY);
+                    g_pXWaylandManager->setWindowSize(PWINDOW, PWINDOW->m_vEffectiveSize);
+                } catch (...) {
+                    Debug::log(LOG, "Rule size failed, rule: %s -> %s", r.szRule.c_str(), r.szValue.c_str());
+                }
+            } else if (r.szRule.find("move") == 0) {
+                try {
+                    const auto VALUE = r.szRule.substr(r.szRule.find(" ") + 1);
+                    const auto POSX = stoi(VALUE.substr(0, VALUE.find(" ")));
+                    const auto POSY = stoi(VALUE.substr(VALUE.find(" ") + 1));
+
+                    Debug::log(LOG, "Rule move, applying to window %x", PWINDOW);
+
+                    PWINDOW->m_vEffectivePosition = Vector2D(POSX, POSY) + PMONITOR->vecPosition;
+                } catch (...) {
+                    Debug::log(LOG, "Rule move failed, rule: %s -> %s", r.szRule.c_str(), r.szValue.c_str());
+                }
+            }
+        }
     }
     else
         g_pLayoutManager->getCurrentLayout()->onWindowCreated(PWINDOW);
-    
+
+    PWINDOW->m_szTitle = g_pXWaylandManager->getTitle(PWINDOW);
+
     if (!PWINDOW->m_bIsModal)
         g_pCompositor->focusWindow(PWINDOW);
 
diff --git a/src/layout/DwindleLayout.cpp b/src/layout/DwindleLayout.cpp
index 00309330e..adcc884c4 100644
--- a/src/layout/DwindleLayout.cpp
+++ b/src/layout/DwindleLayout.cpp
@@ -357,8 +357,13 @@ void CHyprDwindleLayout::onWindowCreatedFloating(CWindow* pWindow) {
         }
     }
 
-    pWindow->m_vRealPosition = pWindow->m_vEffectivePosition + pWindow->m_vEffectiveSize / 2.f;
-    pWindow->m_vRealSize = Vector2D(5,5);
+    if (!pWindow->m_bX11DoesntWantBorders) {
+        pWindow->m_vRealPosition = pWindow->m_vEffectivePosition + pWindow->m_vEffectiveSize / 2.f;
+        pWindow->m_vRealSize = Vector2D(5, 5);
+    } else {
+        pWindow->m_vRealPosition = pWindow->m_vEffectivePosition;
+        pWindow->m_vRealSize = pWindow->m_vEffectiveSize;
+    }
 
     g_pXWaylandManager->setWindowSize(pWindow, pWindow->m_vRealSize);
     g_pCompositor->fixXWaylandWindowsOnWorkspace(PMONITOR->activeWorkspace);
diff --git a/src/managers/XWaylandManager.cpp b/src/managers/XWaylandManager.cpp
index d17c1e5c8..bbd8f9f0d 100644
--- a/src/managers/XWaylandManager.cpp
+++ b/src/managers/XWaylandManager.cpp
@@ -54,11 +54,7 @@ std::string CHyprXWaylandManager::getTitle(CWindow* pWindow) {
             if (pWindow->m_uSurface.xwayland) {
                 return std::string(pWindow->m_uSurface.xwayland->title);
             }
-        } else {
-            return "";
-        }
-
-        if (pWindow->m_uSurface.xdg) {
+        } else if (pWindow->m_uSurface.xdg) {
             if (pWindow->m_uSurface.xdg->toplevel) {
                 return std::string(pWindow->m_uSurface.xdg->toplevel->title);
             }
@@ -72,6 +68,26 @@ std::string CHyprXWaylandManager::getTitle(CWindow* pWindow) {
     return "";
 }
 
+std::string CHyprXWaylandManager::getAppIDClass(CWindow* pWindow) {
+    try {
+        if (pWindow->m_bIsX11) {
+            if (pWindow->m_uSurface.xwayland) {
+                return std::string(pWindow->m_uSurface.xwayland->_class);
+            }
+        } else if (pWindow->m_uSurface.xdg) {
+            if (pWindow->m_uSurface.xdg->toplevel) {
+                return std::string(pWindow->m_uSurface.xdg->toplevel->app_id);
+            }
+        } else {
+            return "";
+        }
+    } catch (std::logic_error& e) {
+        Debug::log(ERR, "Error in getAppIDClass: %s", e.what());
+    }
+
+    return "";
+}
+
 void CHyprXWaylandManager::sendCloseWindow(CWindow* pWindow) {
     if (pWindow->m_bIsX11) {
         wlr_xwayland_surface_close(pWindow->m_uSurface.xwayland);
diff --git a/src/managers/XWaylandManager.hpp b/src/managers/XWaylandManager.hpp
index be7221d9d..00de81395 100644
--- a/src/managers/XWaylandManager.hpp
+++ b/src/managers/XWaylandManager.hpp
@@ -14,6 +14,7 @@ public:
     void                activateSurface(wlr_surface*, bool);
     void                getGeometryForWindow(CWindow*, wlr_box*);
     std::string         getTitle(CWindow*);
+    std::string         getAppIDClass(CWindow*);
     void                sendCloseWindow(CWindow*);
     void                setWindowSize(CWindow*, const Vector2D&);
     void                setWindowStyleTiled(CWindow*, uint32_t);