first commit

This commit is contained in:
Alessio Molinari 2023-05-07 15:51:31 +02:00
commit 61a17b6592
9 changed files with 458 additions and 0 deletions

65
.clang-format Normal file
View File

@ -0,0 +1,65 @@
---
Language: Cpp
BasedOnStyle: LLVM
AccessModifierOffset: -2
AlignAfterOpenBracket: Align
AlignConsecutiveMacros: true
AlignConsecutiveAssignments: true
AlignEscapedNewlines: Right
AlignOperands: false
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: true
AllowAllConstructorInitializersOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: true
AllowShortCaseLabelsOnASingleLine: true
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: Yes
BreakBeforeBraces: Attach
BreakBeforeTernaryOperators: false
BreakConstructorInitializers: AfterColon
ColumnLimit: 180
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: false
IncludeBlocks: Preserve
IndentCaseLabels: true
IndentWidth: 4
PointerAlignment: Left
ReflowComments: false
SortIncludes: false
SortUsingDeclarations: false
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInContainerLiterals: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Auto
TabWidth: 4
UseTab: Never
AllowShortEnumsOnASingleLine: false
BraceWrapping:
AfterEnum: false
AlignConsecutiveDeclarations: AcrossEmptyLines
NamespaceIndentation: All

BIN
.github/hyprland.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.so
compile_flags.txt
compile_commands.json
.cache

29
LICENSE Normal file
View File

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2022-2023, LevMyskin
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

49
Makefile Normal file
View File

@ -0,0 +1,49 @@
# compile with HYPRLAND_HEADERS=<path_to_hl> 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.
PLUGIN_NAME=virtual-desktops
SOURCE_FILES=$(wildcard src/*.cpp)
COMPILE_FLAGS=-g -fPIC --no-gnu-unique -std=c++23
COMPILE_FLAGS+=-I "/usr/include/pixman-1"
COMPILE_FLAGS+=-I "/usr/include/libdrm"
COMPILE_FLAGS+=-I "${HYPRLAND_HEADERS}"
COMPILE_FLAGS+=-I "${HYPRLAND_HEADERS}/protocols"
COMPILE_FLAGS+=-I "${HYPRLAND_HEADERS}/subprojects/wlroots/include"
COMPILE_FLAGS+=-I "${HYPRLAND_HEADERS}/subprojects/wlroots/build/include"
COMPILE_FLAGS+=-Iinclude
COMPILE_DEFINES=-DWLR_USE_UNSTABLE
ifeq ($(shell whereis -b jq), "jq:")
$(error "jq not found. Please install jq.")
else
BUILT_WITH_NOXWAYLAND=$(shell hyprctl version -j | jq -r '.flags | .[]' | grep 'no xwayland')
ifneq ($(BUILT_WITH_NOXWAYLAND),)
COMPILE_DEFINES+=-DNO_XWAYLAND
endif
endif
.PHONY: clean clangd
all: check_env $(PLUGIN_NAME).so
install: all
cp $(PLUGIN_NAME).so ${HOME}/.local/share/hyprload/plugins/bin
check_env:
@if [ -z "${HYPRLAND_HEADERS}" ]; then \
echo "HYPRLAND_HEADERS not set. Please set it to the root of the hl repo directory."; \
exit 1; \
fi
$(PLUGIN_NAME).so: $(SOURCE_FILES) $(INCLUDE_FILES)
g++ -shared $(COMPILE_FLAGS) $(COMPILE_DEFINES) $(SOURCE_FILES) -o $(PLUGIN_NAME).so
clean:
rm -f ./$(PLUGIN_NAME).so
clangd:
printf "%b" "-I/usr/include/pixman-1\n-I/usr/include/libdrm\n-I${HYPRLAND_HEADERS}\n-Iinclude\n-std=c++2b" > compile_flags.txt

104
README.md Normal file
View File

@ -0,0 +1,104 @@
# Virtual desktops for Hyprland ![hyprico](.github/hyprland.ico)
`virtual-desktops` is a plugin for the [Hyprland](https://github.com/hyprwm/Hyprland) compositor. `virtual-desktops` manages multiple screens workspaces as if they were a single virtual desktop.
## What is this exactly?
In Hyprland, each screen has its own set of workspaces. For instance, say you have two monitors, with workspace 1 on screen 1
and workspace 2 on screen 2:
- When you switch from workspace 1 to 2, Hyprland will simply focus your second screen;
- If you switch to workspace 3, your active screen will go to workspace 3, whereas the other screen will stay on whichever workspace it is currently on.
You may think of a virtual desktop, instead, as a "single"
workspace which extends across your screens (even though, internally, you will still have _n_ different workspaces on your _n_ monitors). If you've ever used KDE Plasma or Gnome with
multiple screens, this plugin basically replicates that
functionality.
Taking the previous example:
- You will be on virtual desktop 1. Let's say you open your web browser on your first screen and an IDE on your second screen;
- When you switch to virtual desktop 2, both screens will switch to empty workspaces. Let's say here you open your email client and your favourite chat applicaiton;
- If you switch back to virtual desktop 1, you will get back your web browser and the IDE on screen 1 and 2; and viceversa when you go back to virtual desktop 2.
## How does this work?
### It's just workspaces, really
Internally, this simply ties _n_ workspaces to your _n_ screens, for each virtual desktop. That is, on virtual desktop 1 you will always have workspace 1 on screen 1 and workspace 2 on screen 2;
on virtual desktop 2, you will always have workspace 3 on screen 1 and workspace 4 on screen 2, and so on.
**Notice**: screen 1 and screen 2 are not necessarily what you expect your first and second screen to be, i.e., screen 1 is not necessarily your left screen, and screen 2 is not necessarily your right screen.
### Hyprctl dispatchers
This plugin exposes a few hyprctl dispatchers:
| Dispatcher | descritpion | type |
|------------|-------------|------|
| vdesk [vdesk] | Changes to virtual desktop `vdesk` | see below |
| prevdesk | Changes to previous virtual desktop | `none` |
| printdesk | Prints to Hyprland log the currently active vdesk* | `none`|
\*`printdesk` currently prints to the active Hyprland session log, thus probably not really useful.
For `vdesk` names, you can use:
- ID: e.g., `1`, `2` etc;
- Name: e.g., `coding`, `internet`, `mail and chats`
If a `vdesk` with a given ID or name does not exist, it'll be created on the fly. If you give a (non configured, see [below](#configuration-values))
name, it will be assigned to the next available vdesk id: the virtual-desktops
plugin will remember this association even if Hyprland kills the related workspaces.
#### Don't mix with Hyprland native workspaces
I wouldn't mix this with any of Hyprland native workspaces functionality. For instance, instead of using `hyprctl dispatch workspace n` use
`hyprctl dispatch vdesk n`, even if you have no secondary screen connected at the moment (the behaviour would be identical to native workspaces). Also, I would REMOVE
any workspace related configuration, such as `wsbind`. If you want to leverage [workspace-specific rules](https://wiki.hyprland.org/Configuring/Workspace-Rules/), you can: workspaces are always assigned
to the same vdesk given the same number of monitors. For instance:
- Given two monitors:
- vdesk 1 has workspaces 1 and 2;
- vdesk 2 has workspaces 3 and 4, and so on;
- Given three monitors:
- vdesk 1 has workspaces 1, 2 and 3;
- vdesk 2 has workspaces 4, 5 and 6, and so on.
- Given four monitors...
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.
### Configuration values
This plugin exposes a few configuration options, under the `plugin:virtual-desktops:` category, namely:
| Name | type | example|
|------|------|--------|
| names | string, see below| `1:coding, 2:internet, 3:mail and chats`|
| cycleworkspaces | `0` or `1`| `1`|
* The `names` config option maps virtual desktop IDs to a name (you can then use this with the hyprctl [dispatcher](#hyprctl-dispatchers));
* If `cycleworkspaces` is set to `1`, and you switch to the currently active virtual desktop, this swaps the workspaces of your two monitors (see hyprctl [swapactiveworkspaces](https://wiki.hyprland.org/Configuring/Dispatchers/#list-of-dispatchers)).
THIS CURRENTLY DOES NOT WORK WITH MORE THAN 2 MONITORS. If you need this feature, please feel welcome to submit a PR ^^.
#### Example config
```ini
plugin {
virtual-desktops {
names = 1:coding, 2:internet, 3:mail and chats
cycleworkspaces = 1
}
}
```
## Install
In order to use plugins, you should compile Hyprland yourself. See [Hyprland Wiki#Using Plugins](https://wiki.hyprland.org/Plugins/Using-Plugins/).
You can use:
```bash
HYPRLAND_HEADERS=path/to/hyprlandrepo make all
```
this will compile and copy the compiled `.so` plugin in the `$HOME/.local/share/hyprload/plugins/bin` path.
You can also use `make virtual-desktops.so` to compile the plugin in the repo directory.
Once compiled, you can tell Hyprland to load the plugin as described in the Hyprland wiki.
### Thanks to
[split-workspaces](https://github.com/Duckonaut/split-monitor-workspaces/), from which I borrowed the Makefile,
and the general idea of how to write Hyprland plugins.

10
hyprload.toml Normal file
View File

@ -0,0 +1,10 @@
[virtual-desktops]
description = "Virtual desktops"
version = "1.0.0"
author = "LevMyskin"
[virtual-desktops.build]
output = "virtual-desktops.so"
steps = [
"make all",
]

5
include/globals.hpp Normal file
View File

@ -0,0 +1,5 @@
#pragma once
#include <src/plugins/PluginAPI.hpp>
inline HANDLE PHANDLE = nullptr;

192
src/main.cpp Normal file
View File

@ -0,0 +1,192 @@
#include <src/Compositor.hpp>
#include <src/config/ConfigManager.hpp>
#include <src/helpers/Color.hpp>
#include <src/helpers/MiscFunctions.hpp>
#include <src/helpers/Workspace.hpp>
#include <src/debug/Log.hpp>
#include "globals.hpp"
#include <any>
#include <iostream>
#include <map>
#include <math.h>
#include <vector>
static HOOK_CALLBACK_FN* onWorkspaceChangeHook = nullptr;
const std::string VIRTUALDESK_NAMES_CONF = "plugin:virtual-desktops:names";
const std::string CYCLEWORKSPACES_CONF = "plugin:virtual-desktops:cycleworkspaces";
const std::string VDESK_DISPATCH_STR = "vdesk";
const std::string PREVDESK_DISPATCH_STR = "prevdesk";
const std::string PRINTDESK_DISPATCH_STR = "printdesk";
std::map<int, std::string> virtualDeskNames = {{1, "1"}};
int prevVDesk = -1;
int currentVDesk = 1; // when plugin is launched, we assume we start at vdesk 1
void printLog(std::string s) {
Debug::log(INFO, ("[virtual-desktops] " + s).c_str());
// std::cout << "[virtual-desktops] " + s << std::endl;
}
void parseConf(std::string conf) {
size_t pos;
size_t delim;
std::string rule;
try {
while ((pos = conf.find(',')) != std::string::npos) {
rule = conf.substr(0, pos);
if ((delim = rule.find(':')) != std::string::npos) {
int vdeskId = std::stoi(rule.substr(0, delim));
virtualDeskNames[vdeskId] = rule.substr(delim + 1);
}
conf.erase(0, pos + 1);
}
if ((delim = conf.find(':')) != std::string::npos) {
int vdeskId = std::stoi(conf.substr(0, delim));
virtualDeskNames[vdeskId] = conf.substr(delim + 1);
}
} catch (std::exception const& ex) {
// #aa1245
HyprlandAPI::addNotification(PHANDLE, "Syntax error in your virtual-desktops names config", CColor{4289335877}, 8000);
}
}
void printVdesk(int vdeskId) {
printLog("VDesk " + std::to_string(vdeskId) + ": " + virtualDeskNames[vdeskId]);
}
void printVdesk(std::string name) {
for (auto const& [key, val] : virtualDeskNames) {
if (val == name) {
printLog("Vdesk " + std::to_string(key) + ": " + val);
return;
}
}
}
void changeVDesk(int vdesk) {
auto n_monitors = g_pCompositor->m_vMonitors.size();
CMonitor* currentMonitor = g_pCompositor->m_pLastMonitor;
if (!currentMonitor) {
printLog("No active monitor!! Not changing vdesk.");
return;
}
printLog("Changing to virtual desktop " + std::to_string(vdesk));
static auto* const PCYCLEWORKSPACES = &HyprlandAPI::getConfigValue(PHANDLE, CYCLEWORKSPACES_CONF)->intValue;
if (vdesk == currentVDesk) {
if (!*PCYCLEWORKSPACES)
return;
// TODO implement for more than two monitors as well.
// This probably requires to compute monitors position
// in order to consistently move left/right or up/down.
if (n_monitors == 2) {
int other = g_pCompositor->m_vMonitors[0]->ID == currentMonitor->ID;
g_pCompositor->swapActiveWorkspaces(currentMonitor, g_pCompositor->m_vMonitors[other].get());
} else if (n_monitors > 2) {
printLog("Cycling workspaces is not yet implemented for more than 2 monitors."
"\nIf you would like to have this feature, open an issue on virtual-desktops github repo, or even "
"better, open a PR :)");
}
return;
}
auto vdeskFirstWorkspace = (vdesk - 1) * n_monitors + 1;
int j = 0;
for (int i = vdeskFirstWorkspace; i < vdeskFirstWorkspace + n_monitors; i++) {
CWorkspace* workspace = g_pCompositor->getWorkspaceByID(i);
auto mon = g_pCompositor->m_vMonitors[j];
if (!workspace) {
printLog("Creating workspace " + std::to_string(i));
workspace = g_pCompositor->createNewWorkspace(i, mon->ID);
}
g_pCompositor->m_vMonitors[j]->changeWorkspace(workspace, false);
j++;
}
g_pCompositor->setActiveMonitor(currentMonitor);
}
void virtualDeskDispatch(std::string arg) {
static auto* const PVDESKNAMES = &HyprlandAPI::getConfigValue(PHANDLE, VIRTUALDESK_NAMES_CONF)->strValue;
parseConf(*PVDESKNAMES);
int vdesk;
try {
vdesk = std::stoi(arg);
} catch (std::exception const& ex) {
// user input a vdesk name. If we have it already,
// we switch to it, otherwise we simply assign the name
// to the next available vdesk
int max_key = -1;
bool found = false;
for (auto const& [key, val] : virtualDeskNames) {
if (val == arg) {
vdesk = key;
found = true;
break;
}
if (key > max_key)
max_key = key;
}
if (!found) {
vdesk = max_key + 1;
virtualDeskNames[vdesk] = arg;
}
}
changeVDesk(vdesk);
}
void goPreviousVDeskDispatch(std::string _) {
if (prevVDesk == -1) {
printLog("There's no previous desk");
return;
}
changeVDesk(prevVDesk);
}
void printVDeskDispatch(std::string arg) {
static auto* const PVDESKNAMES = &HyprlandAPI::getConfigValue(PHANDLE, VIRTUALDESK_NAMES_CONF)->strValue;
parseConf(*PVDESKNAMES);
if (arg == PRINTDESK_DISPATCH_STR) {
printVdesk(currentVDesk);
} else
try {
// maybe id
printVdesk(std::stoi(arg));
} catch (std::exception const& ex) {
// by name then
printVdesk(arg);
}
}
void onWorkspaceChange(void*, std::any val) {
int workspaceID = std::any_cast<CWorkspace*>(val)->m_iID;
auto n_monitors = g_pCompositor->m_vMonitors.size();
auto newDesk = ceil((float)workspaceID / (float)n_monitors);
if (currentVDesk != newDesk)
prevVDesk = currentVDesk;
currentVDesk = newDesk;
}
// Do NOT change this function.
APICALL EXPORT std::string PLUGIN_API_VERSION() {
return HYPRLAND_API_VERSION;
}
APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
PHANDLE = handle;
HyprlandAPI::addDispatcher(PHANDLE, VDESK_DISPATCH_STR, virtualDeskDispatch);
HyprlandAPI::addDispatcher(PHANDLE, PREVDESK_DISPATCH_STR, goPreviousVDeskDispatch);
HyprlandAPI::addDispatcher(PHANDLE, PRINTDESK_DISPATCH_STR, printVDeskDispatch);
HyprlandAPI::addConfigValue(PHANDLE, VIRTUALDESK_NAMES_CONF, SConfigValue{.strValue = "unset"});
HyprlandAPI::addConfigValue(PHANDLE, CYCLEWORKSPACES_CONF, SConfigValue{.intValue = 1});
onWorkspaceChangeHook = HyprlandAPI::registerCallbackDynamic(PHANDLE, "workspace", onWorkspaceChange);
HyprlandAPI::reloadConfig();
HyprlandAPI::addNotification(PHANDLE, "Virtual desk Initialized successfully!", CColor{0.f, 1.f, 1.f, 1.f}, 5000);
return {"virtual-desktops", "Virtual desktop like workspaces", "LevMyskin", "1.0"};
}
APICALL EXPORT void PLUGIN_EXIT() {
virtualDeskNames.clear();
}