Merge pull request #19 from levnikmyskin/feat/vdesk_pinned_clients

Implement new stickyrule feature
This commit is contained in:
Alessio Molinari 2024-03-01 16:50:34 +01:00 committed by GitHub
commit 3dca522d78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 262 additions and 37 deletions

View File

@ -9,6 +9,9 @@
- [It's just workspaces, really](#its-just-workspaces-really)
- [Hyprctl dispatchers](#hyprctl-dispatchers)
- [Mix with Hyprland native workspaces](#mix-with-hyprland-native-workspaces)
- [Hyprland keywords](#hyprland-keywords)
- [Syntax](#syntax)
- [Examples](#examples)
- [Configuration values](#configuration-values)
- [Example config](#example-config)
- [Layouts](#layouts)
@ -103,6 +106,29 @@ to the same vdesk given the same number of monitors, unless you focus (e.g. with
The vdesk a workspace will end up to is easily computed by doing `ceil(workspace_id / n_monitors)`. You know where I'm going with this one...you can easily script it.
### Hyprland keywords
Since version 2.2, this plugin exposes one keyword: `stickyrule`.
A sticky rule is composed of a window identifier and a vdesk identifier.
A window matched by a sticky rule will be moved to the matched vdesk:
1. When the window is created (similar to [Hyprland's `workspace` windowrule](https://wiki.hyprland.org/Configuring/Window-Rules/#window-rules-v2), but with virtual desks);
2. Every time a monitor is connected/disconnected.
**BE CAREFUL**:
1. **NOT** to mix this with Hyprland's `workspace` windowrule (it wouldn't make sense right?);
2. This is not a plugin config, but an Hyprland keyword. Place it in the top level of Hyprland's config (i.e., where you'd put windowrules too).
#### Syntax
```bash
stickyrule = window,vdesk
```
- `window` identifier has the same syntax as [Hyprland's windowrule window](https://wiki.hyprland.org/Configuring/Window-Rules/#window-rules-v2) identifier;
- `vdesk` identifier has the same syntax specified above.
#### Examples
`stickyrule = class:^(kittysticky)$,3`
`stickyrule = title:thunderbird,mail`
### Configuration values
@ -122,6 +148,9 @@ This plugin exposes a few configuration options, under the `plugin:virtual-deskt
#### Example config
```ini
stickyrule = class:^(kittysticky)$,3
stickyrule = title:thunderbird,mail
plugin {
virtual-desktops {
names = 1:coding, 2:internet, 3:mail and chats

View File

@ -39,6 +39,7 @@ class VirtualDesk {
void deleteInvalidMonitorsOnActiveLayout();
void deleteInvalidMonitorOnAllLayouts(const CMonitor*);
static std::shared_ptr<CMonitor> firstAvailableMonitor(const std::vector<std::shared_ptr<CMonitor>>&);
bool isWorkspaceOnActiveLayout(int workspaceId);
private:
int m_activeLayout_idx;

View File

@ -29,12 +29,13 @@ class VirtualDeskManager {
void deleteInvalidMonitorsOnAllVdesks();
int prevDeskId(bool backwardCycle);
int nextDeskId(bool cycle);
int getDeskIdFromName(const std::string& name, bool createIfNotFound = true);
private:
int m_activeDeskKey = 1;
bool confLoaded = false;
void cycleWorkspaces();
int getDeskIdFromName(const std::string& name, bool createIfNotFound = true);
CMonitor* getCurrentMonitor();
int m_activeDeskKey = 1;
bool confLoaded = false;
void cycleWorkspaces();
CMonitor* getCurrentMonitor();
std::shared_ptr<VirtualDesk> getOrCreateVdesk(int vdeskId);
};
#endif

33
include/sticky_apps.hpp Normal file
View File

@ -0,0 +1,33 @@
#pragma once
#ifndef STICKYAPPS_H
#define STICKYAPPS_H
#include <string>
#include <VirtualDeskManager.hpp>
const std::string TITLE = "title";
const std::string INITIAL_TITLE = "initialTitle";
const std::string CLASS = "class";
const std::string INITIAL_CLASS = "initialClass";
namespace StickyApps {
struct SStickyRule {
int vdesk;
std::string property;
std::string value;
};
bool parseRule(const std::string&, SStickyRule&, std::unique_ptr<VirtualDeskManager>&);
bool parseWindowRule(const std::string&, SStickyRule&);
void matchRules(const std::vector<SStickyRule>&, std::unique_ptr<VirtualDeskManager>&);
int matchRuleOnWindow(const std::vector<SStickyRule>&, std::unique_ptr<VirtualDeskManager>&, CWindow*);
const std::string extractProperty(const SStickyRule&, std::unique_ptr<CWindow>&);
const std::string extractProperty(const SStickyRule&, CWindow*);
bool ruleMatch(const std::string&, const std::string&);
}
#endif

View File

@ -13,7 +13,10 @@ const std::string CYCLEWORKSPACES_CONF = "plugin:virtual-desktops:cycleworkspa
const std::string REMEMBER_LAYOUT_CONF = "plugin:virtual-desktops:rememberlayout";
const std::string NOTIFY_INIT = "plugin:virtual-desktops:notifyinit";
const std::string VERBOSE_LOGS = "plugin:virtual-desktops:verbose_logging";
const std::string VDESK_DISPATCH_STR = "vdesk";
const std::string STICKY_RULES_KEYW = "stickyrule";
const std::string VDESK_DISPATCH_STR = "vdesk";
const std::string MOVETODESK_DISPATCH_STR = "movetodesk";
const std::string MOVETOLASTDESK_DISPATCH_STR = "movetolastdesk";
@ -52,5 +55,9 @@ std::string parseMoveDispatch(std::string& arg);
bool extractBool(std::string& arg);
std::vector<std::shared_ptr<CMonitor>> currentlyEnabledMonitors(const CMonitor* exclude = nullptr);
std::string ltrim(const std::string& s);
std::string rtrim(const std::string& s);
std::string trim(const std::string& s);
bool isVerbose();
#endif

View File

@ -130,6 +130,14 @@ std::shared_ptr<CMonitor> VirtualDesk::firstAvailableMonitor(const std::vector<s
return newMonitor;
}
bool VirtualDesk::isWorkspaceOnActiveLayout(int workspaceId) {
for (auto [_, wid] : layouts[m_activeLayout_idx]) {
if (workspaceId == wid)
return true;
}
return false;
}
void VirtualDesk::checkAndAdaptLayout(Layout* layout, const CMonitor* exclude) {
auto enabledMons = currentlyEnabledMonitors(exclude);
if (enabledMons.size() == 0)

View File

@ -4,9 +4,8 @@
#include <ranges>
VirtualDeskManager::VirtualDeskManager() {
this->conf = RememberLayoutConf::size;
vdeskNamesMap[1] = "1";
vdesksMap[1] = std::make_shared<VirtualDesk>();
this->conf = RememberLayoutConf::size;
getOrCreateVdesk(1);
}
const std::shared_ptr<VirtualDesk>& VirtualDeskManager::activeVdesk() {
@ -28,13 +27,7 @@ void VirtualDeskManager::changeActiveDesk(int vdeskId, bool apply) {
return;
}
if (!vdesksMap.contains(vdeskId)) {
if (isVerbose())
printLog("creating new vdesk with id " + std::to_string(vdeskId));
if (!vdeskNamesMap.contains(vdeskId))
vdeskNamesMap[vdeskId] = std::to_string(vdeskId);
vdesksMap[vdeskId] = std::make_shared<VirtualDesk>(vdeskId, vdeskNamesMap[vdeskId]);
}
getOrCreateVdesk(vdeskId);
lastDesk = activeVdesk()->id;
m_activeDeskKey = vdeskId;
if (apply)
@ -64,9 +57,9 @@ void VirtualDeskManager::applyCurrentVDesk() {
}
if (isVerbose())
printLog("applying vdesk" + activeVdesk()->name);
auto currentMonitor = getCurrentMonitor();
auto layout = activeVdesk()->activeLayout(conf);
CWorkspace* focusedWorkspace;
auto currentMonitor = getCurrentMonitor();
auto layout = activeVdesk()->activeLayout(conf);
CWorkspace* focusedWorkspace = nullptr;
for (auto [lmon, workspaceId] : layout) {
CMonitor* mon = g_pCompositor->getMonitorFromID(lmon->ID);
if (!lmon || !lmon->m_bEnabled) {
@ -95,7 +88,7 @@ void VirtualDeskManager::applyCurrentVDesk() {
}
mon->changeWorkspace(workspace, false);
}
if (currentMonitor)
if (currentMonitor && focusedWorkspace)
currentMonitor->changeWorkspace(focusedWorkspace, false);
}
@ -118,10 +111,8 @@ int VirtualDeskManager::moveToDesk(std::string& arg, int vdeskId) {
if (isVerbose())
printLog("creating new vdesk with id " + std::to_string(vdeskId));
if (!vdeskNamesMap.contains(vdeskId))
vdeskNamesMap[vdeskId] = std::to_string(vdeskId);
auto vdesk = vdesksMap[vdeskId] = std::make_shared<VirtualDesk>(vdeskId, vdeskNamesMap[vdeskId]);
auto vdesk = getOrCreateVdesk(vdeskId);
auto* window = g_pCompositor->getWindowByRegex(arg);
if (!window) {
@ -164,8 +155,8 @@ int VirtualDeskManager::getDeskIdFromName(const std::string& name, bool createIf
max_key = key;
}
if (!found && createIfNotFound) {
vdesk = max_key + 1;
vdeskNamesMap[vdesk] = name;
vdesk = max_key + 1;
getOrCreateVdesk(vdesk);
}
return vdesk;
}
@ -271,6 +262,18 @@ int VirtualDeskManager::nextDeskId(bool cycle) {
return nextId;
}
std::shared_ptr<VirtualDesk> VirtualDeskManager::getOrCreateVdesk(int vdeskId) {
if (!vdeskNamesMap.contains(vdeskId))
vdeskNamesMap[vdeskId] = std::to_string(vdeskId);
if (!vdesksMap.contains(vdeskId)) {
if (isVerbose())
printLog("creating new vdesk with id " + std::to_string(vdeskId));
auto vdesk = vdesksMap[vdeskId] = std::make_shared<VirtualDesk>(vdeskId, vdeskNamesMap[vdeskId]);
return vdesk;
}
return vdesksMap[vdeskId];
}
void VirtualDeskManager::invalidateAllLayouts() {
for (const auto& [_, vdesk] : vdesksMap) {
vdesk->invalidateActiveLayout();
@ -281,9 +284,9 @@ CMonitor* VirtualDeskManager::getCurrentMonitor() {
CMonitor* currentMonitor = g_pCompositor->m_pLastMonitor;
// This can happen when we receive the "on disconnect" signal
// let's just take first monitor we can find
if (currentMonitor && !currentMonitor->m_bEnabled) {
if (currentMonitor && (!currentMonitor->m_bEnabled || !currentMonitor->output)) {
for (std::shared_ptr<CMonitor> mon : g_pCompositor->m_vMonitors) {
if (mon->m_bEnabled)
if (mon->m_bEnabled && mon->output)
return mon.get();
}
return nullptr;

View File

@ -9,23 +9,25 @@
#include "globals.hpp"
#include "VirtualDeskManager.hpp"
#include "utils.hpp"
#include "sticky_apps.hpp"
#include <any>
#include <iostream>
#include <sstream>
#include <vector>
static HOOK_CALLBACK_FN* onWorkspaceChangeHook = nullptr;
static HOOK_CALLBACK_FN* onConfigReloadedHook = nullptr;
static HOOK_CALLBACK_FN* onMonitorDisconnectHook = nullptr;
static HOOK_CALLBACK_FN* onMonitorAddedHook = nullptr;
static HOOK_CALLBACK_FN* onRenderHook = nullptr;
std::unique_ptr<VirtualDeskManager> manager = std::make_unique<VirtualDeskManager>();
bool notifiedInit = false;
bool needsReloading = false;
bool monitorLayoutChanging = false;
static HOOK_CALLBACK_FN* onWorkspaceChangeHook = nullptr;
static HOOK_CALLBACK_FN* onWindowOpenHook = nullptr;
static HOOK_CALLBACK_FN* onConfigReloadedHook = nullptr;
static HOOK_CALLBACK_FN* onMonitorDisconnectHook = nullptr;
static HOOK_CALLBACK_FN* onMonitorAddedHook = nullptr;
static HOOK_CALLBACK_FN* onRenderHook = nullptr;
std::unique_ptr<VirtualDeskManager> manager = std::make_unique<VirtualDeskManager>();
std::vector<StickyApps::SStickyRule> stickyRules;
bool notifiedInit = false;
bool needsReloading = false;
bool monitorLayoutChanging = false;
inline CFunctionHook* g_pMonitorDestroy = nullptr;
typedef void (*origMonitorDestroy)(void*, void*);
inline CFunctionHook* g_pMonitorAdded = nullptr;
@ -58,6 +60,19 @@ void parseNamesConf(std::string& conf) {
}
}
Hyprlang::CParseResult parseStickyRule(const char* command, const char* value) {
Hyprlang::CParseResult result;
StickyApps::SStickyRule rule;
std::string value_str = value;
if (!StickyApps::parseRule(value_str, rule, manager)) {
std::string err = std::format("Error in your sticky rule: {}", value);
result.setError(err.c_str());
} else {
stickyRules.push_back(rule);
}
return result;
}
void virtualDeskDispatch(std::string arg) {
manager->changeActiveDesk(arg, true);
}
@ -169,6 +184,7 @@ void resetVDeskDispatch(std::string arg) {
manager->resetVdesk(arg);
}
manager->applyCurrentVDesk();
StickyApps::matchRules(stickyRules, manager);
}
void onWorkspaceChange(void*, SCallbackInfo&, std::any val) {
@ -186,6 +202,13 @@ void onWorkspaceChange(void*, SCallbackInfo&, std::any val) {
printLog("workspace changed: workspace id " + std::to_string(workspaceID) + "; on monitor " + std::to_string(workspace->m_iMonitorID));
}
void onWindowOpen(void*, SCallbackInfo&, std::any val) {
CWindow* window = std::any_cast<CWindow*>(val);
int vdesk = StickyApps::matchRuleOnWindow(stickyRules, manager, window);
if (vdesk > 0)
manager->changeActiveDesk(vdesk, true);
}
void onMonitorDisconnect(void*, SCallbackInfo&, std::any val) {
CMonitor* monitor = std::any_cast<CMonitor*>(val);
if (isVerbose())
@ -213,6 +236,7 @@ void onRender(void*, SCallbackInfo&, std::any val) {
if (needsReloading) {
printLog("on render called and needs reloading");
manager->applyCurrentVDesk();
StickyApps::matchRules(stickyRules, manager);
needsReloading = false;
}
}
@ -267,7 +291,11 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
HyprlandAPI::addConfigValue(PHANDLE, NOTIFY_INIT, Hyprlang::INT{1});
HyprlandAPI::addConfigValue(PHANDLE, VERBOSE_LOGS, Hyprlang::INT{0});
// Keywords
HyprlandAPI::addConfigKeyword(PHANDLE, STICKY_RULES_KEYW, parseStickyRule, Hyprlang::SHandlerOptions{});
onWorkspaceChangeHook = HyprlandAPI::registerCallbackDynamic(PHANDLE, "workspace", onWorkspaceChange);
onWindowOpenHook = HyprlandAPI::registerCallbackDynamic(PHANDLE, "openWindow", onWindowOpen);
onConfigReloadedHook = HyprlandAPI::registerCallbackDynamic(PHANDLE, "configReloaded", onConfigReloaded);
onMonitorDisconnectHook = HyprlandAPI::registerCallbackDynamic(PHANDLE, "monitorRemoved", onMonitorDisconnect);
onMonitorAddedHook = HyprlandAPI::registerCallbackDynamic(PHANDLE, "monitorAdded", onMonitorAdded);

98
src/sticky_apps.cpp Normal file
View File

@ -0,0 +1,98 @@
#include "sticky_apps.hpp"
#include "utils.hpp"
bool StickyApps::parseRule(const std::string& rule, SStickyRule& sticky, std::unique_ptr<VirtualDeskManager>& vdeskManager) {
auto comma_pos = rule.find(",");
if (comma_pos == std::string::npos)
return false;
auto window = rule.substr(0, comma_pos);
auto vdesk = rule.substr(comma_pos + 1, rule.length());
try {
sticky.vdesk = std::stoi(vdesk);
} catch (std::exception const& ex) { sticky.vdesk = vdeskManager->getDeskIdFromName(vdesk); }
return parseWindowRule(window, sticky);
}
bool StickyApps::parseWindowRule(const std::string& rule, SStickyRule& sticky) {
auto colon_pos = rule.find(":");
if (colon_pos == std::string::npos)
return false;
auto property = trim(rule.substr(0, colon_pos));
auto value = trim(rule.substr(colon_pos + 1, rule.length()));
sticky.property = property;
sticky.value = value;
return true;
}
void StickyApps::matchRules(const std::vector<SStickyRule>& rules, std::unique_ptr<VirtualDeskManager>& vdeskManager) {
for (auto& r : rules) {
for (auto& w : g_pCompositor->m_vWindows) {
auto windowProp = extractProperty(r, w);
if (windowProp == "")
continue;
if (ruleMatch(r.value, windowProp)) {
printLog(std::format("rule matched {}: {}", r.value, windowProp));
auto windowPidFmt = std::format("pid:{}", w->getPID());
auto map = vdeskManager->vdesksMap;
if (map.contains(r.vdesk) && map[r.vdesk]->isWorkspaceOnActiveLayout(w->m_iWorkspaceID))
continue;
vdeskManager->moveToDesk(windowPidFmt, r.vdesk);
}
}
}
}
int StickyApps::matchRuleOnWindow(const std::vector<SStickyRule>& rules, std::unique_ptr<VirtualDeskManager>& vdeskManager, CWindow* window) {
for (auto& r : rules) {
auto windowProp = extractProperty(r, window);
if (windowProp == "")
continue;
if (ruleMatch(r.value, windowProp)) {
printLog(std::format("rule matched {}: {}", r.value, windowProp));
auto windowPidFmt = std::format("pid:{}", window->getPID());
auto map = vdeskManager->vdesksMap;
if (map.find(r.vdesk) != map.end() && map[r.vdesk]->isWorkspaceOnActiveLayout(window->m_iWorkspaceID))
continue;
vdeskManager->moveToDesk(windowPidFmt, r.vdesk);
return r.vdesk;
}
}
return -1;
}
const std::string StickyApps::extractProperty(const SStickyRule& rule, std::unique_ptr<CWindow>& window) {
if (rule.property == TITLE) {
return g_pXWaylandManager->getTitle(window.get());
} else if (rule.property == INITIAL_TITLE) {
return window->m_szInitialTitle;
} else if (rule.property == CLASS) {
return g_pXWaylandManager->getAppIDClass(window.get());
} else if (rule.property == INITIAL_CLASS) {
return window->m_szInitialClass;
}
return "";
}
const std::string StickyApps::extractProperty(const SStickyRule& rule, CWindow* window) {
if (rule.property == TITLE) {
return g_pXWaylandManager->getTitle(window);
} else if (rule.property == INITIAL_TITLE) {
return window->m_szInitialTitle;
} else if (rule.property == CLASS) {
return g_pXWaylandManager->getAppIDClass(window);
} else if (rule.property == INITIAL_CLASS) {
return window->m_szInitialClass;
}
return "";
}
bool StickyApps::ruleMatch(const std::string& rule, const std::string& str) {
try {
std::regex ruleRegex(rule);
return std::regex_search(str, ruleRegex);
} catch (...) { printLog(std::format("Regex error for sticky app rule: {}", rule)); }
return false;
}

View File

@ -51,6 +51,9 @@ RememberLayoutConf layoutConfFromString(const std::string& conf) {
}
bool isVerbose() {
// this might happen if called before plugin is initalized
if (!PHANDLE)
return true;
static auto* const PVERBOSELOGS = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, VERBOSE_LOGS)->getDataStaticPtr();
return **PVERBOSELOGS;
}
@ -74,3 +77,17 @@ std::vector<std::shared_ptr<CMonitor>> currentlyEnabledMonitors(const CMonitor*
});
return monitors;
}
std::string ltrim(const std::string& s) {
size_t start = s.find_first_not_of(" ");
return (start == std::string::npos) ? "" : s.substr(start);
}
std::string rtrim(const std::string& s) {
size_t end = s.find_last_not_of(" ");
return (end == std::string::npos) ? "" : s.substr(0, end + 1);
}
std::string trim(const std::string& s) {
return ltrim(rtrim(s));
}