Initial commit
This commit is contained in:
commit
5a6fe00c9b
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
libhyprcolumns.so
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
23
Makefile
Normal file
23
Makefile
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
PLUGIN_NAME := libhyprcolumns.so
|
||||||
|
PREFIX ?= $(HOME)/.local
|
||||||
|
LIBDIR := $(PREFIX)/lib/hyprland
|
||||||
|
CXX ?= c++
|
||||||
|
CXXFLAGS ?= -std=gnu++23 -O2 -fPIC -Wall -Wextra -Wpedantic
|
||||||
|
PKG_CFLAGS := $(shell pkg-config --cflags hyprland)
|
||||||
|
PKG_LIBS := $(shell pkg-config --libs hyprland)
|
||||||
|
|
||||||
|
SRC := src/main.cpp
|
||||||
|
|
||||||
|
all: $(PLUGIN_NAME)
|
||||||
|
|
||||||
|
$(PLUGIN_NAME): $(SRC)
|
||||||
|
$(CXX) $(CXXFLAGS) $(PKG_CFLAGS) -shared -o $@ $(SRC) $(PKG_LIBS)
|
||||||
|
|
||||||
|
install: $(PLUGIN_NAME)
|
||||||
|
mkdir -p $(LIBDIR)
|
||||||
|
install -m755 $(PLUGIN_NAME) $(LIBDIR)/$(PLUGIN_NAME)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(PLUGIN_NAME)
|
||||||
|
|
||||||
|
.PHONY: all install clean
|
||||||
49
README.md
Normal file
49
README.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# hyprland-columns-plugin
|
||||||
|
|
||||||
|
A custom Hyprland tiled layout plugin that arranges all tiled windows as equal-width vertical columns so every window remains visible on screen.
|
||||||
|
|
||||||
|
## Layout name
|
||||||
|
|
||||||
|
`columns`
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/src/hyprland-columns-plugin
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs the plugin to:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/lib/hyprland/libhyprcolumns.so
|
||||||
|
```
|
||||||
|
|
||||||
|
## Load now
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hyprctl plugin load ~/.local/lib/hyprland/libhyprcolumns.so
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use it on the current workspace
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hyprctl keyword workspace "$(hyprctl activeworkspace -j | jq -r '.id'), layout:columns"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Persist across logins
|
||||||
|
|
||||||
|
Load it during Hyprland startup, e.g. from `~/.config/hypr/autostart.conf`:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
exec-once = hyprctl plugin load /home/wayfinderak/.local/lib/hyprland/libhyprcolumns.so
|
||||||
|
```
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
- All tiled windows are shown at once.
|
||||||
|
- Each tiled window gets an equal-width column.
|
||||||
|
- Columns fill the workspace height.
|
||||||
|
- Left/right move and swap operations work naturally for the column order.
|
||||||
|
- Interactive resize is intentionally ignored to preserve equal columns.
|
||||||
220
src/main.cpp
Normal file
220
src/main.cpp
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
#include <algorithm>
|
||||||
|
#include <expected>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <hyprland/src/helpers/Color.hpp>
|
||||||
|
#include <hyprland/src/layout/algorithm/TiledAlgorithm.hpp>
|
||||||
|
#include <hyprland/src/layout/algorithm/Algorithm.hpp>
|
||||||
|
#include <hyprland/src/layout/space/Space.hpp>
|
||||||
|
#include <hyprland/src/layout/target/Target.hpp>
|
||||||
|
#include <hyprland/src/plugins/PluginAPI.hpp>
|
||||||
|
|
||||||
|
static HANDLE g_pluginHandle = nullptr;
|
||||||
|
|
||||||
|
namespace Layout::Tiled {
|
||||||
|
class CColumnsAlgorithm final : public ITiledAlgorithm {
|
||||||
|
public:
|
||||||
|
void newTarget(SP<ITarget> target) override {
|
||||||
|
addTarget(std::move(target));
|
||||||
|
recalculate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void movedTarget(SP<ITarget> target, std::optional<Vector2D> /*focalPoint*/ = std::nullopt) override {
|
||||||
|
addTarget(std::move(target));
|
||||||
|
recalculate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeTarget(SP<ITarget> target) override {
|
||||||
|
cleanup();
|
||||||
|
std::erase_if(m_targets, [&](const WP<ITarget>& weak) {
|
||||||
|
const auto locked = weak.lock();
|
||||||
|
return !locked || locked == target;
|
||||||
|
});
|
||||||
|
recalculate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resizeTarget(const Vector2D& /*delta*/, SP<ITarget> /*target*/, eRectCorner /*corner*/ = CORNER_NONE) override {
|
||||||
|
// Intentionally ignored: this layout always keeps equal-width columns.
|
||||||
|
}
|
||||||
|
|
||||||
|
void recalculate() override {
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
const auto parent = m_parent.lock();
|
||||||
|
if (!parent)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto space = parent->space();
|
||||||
|
if (!space)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::vector<SP<ITarget>> tiledTargets;
|
||||||
|
tiledTargets.reserve(m_targets.size());
|
||||||
|
|
||||||
|
for (auto const& weak : m_targets) {
|
||||||
|
const auto target = weak.lock();
|
||||||
|
if (target)
|
||||||
|
tiledTargets.push_back(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tiledTargets.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto area = space->workArea(false);
|
||||||
|
const auto count = tiledTargets.size();
|
||||||
|
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
const double left = area.x + (area.w * static_cast<double>(i) / static_cast<double>(count));
|
||||||
|
const double right = area.x + (area.w * static_cast<double>(i + 1) / static_cast<double>(count));
|
||||||
|
const CBox box(left, area.y, right - left, area.h);
|
||||||
|
|
||||||
|
tiledTargets[i]->setPositionGlobal(box);
|
||||||
|
tiledTargets[i]->recalc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SP<ITarget> getNextCandidate(SP<ITarget> old) override {
|
||||||
|
cleanup();
|
||||||
|
auto targets = liveTargets();
|
||||||
|
|
||||||
|
if (targets.empty())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
if (!old)
|
||||||
|
return targets.front();
|
||||||
|
|
||||||
|
const auto it = std::ranges::find(targets, old);
|
||||||
|
if (it == targets.end())
|
||||||
|
return targets.front();
|
||||||
|
|
||||||
|
const auto idx = static_cast<size_t>(std::distance(targets.begin(), it));
|
||||||
|
return targets[(idx + 1) % targets.size()];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<void, std::string> layoutMsg(const std::string_view& sv) override {
|
||||||
|
if (sv == "togglesplit" || sv == "swapsplit")
|
||||||
|
return {};
|
||||||
|
|
||||||
|
if (sv == "recalculate") {
|
||||||
|
recalculate();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::unexpected(std::string{"columns: unsupported layout message: "} + std::string{sv});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Vector2D> predictSizeForNewTarget() override {
|
||||||
|
const auto parent = m_parent.lock();
|
||||||
|
if (!parent)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
const auto space = parent->space();
|
||||||
|
if (!space)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
const auto area = space->workArea(false);
|
||||||
|
const auto n = std::max<size_t>(1, liveTargets().size() + 1);
|
||||||
|
return Vector2D(area.w / static_cast<double>(n), area.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
void swapTargets(SP<ITarget> a, SP<ITarget> b) override {
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
auto ia = indexOf(a);
|
||||||
|
auto ib = indexOf(b);
|
||||||
|
if (!ia.has_value() || !ib.has_value() || ia == ib)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::swap(m_targets[*ia], m_targets[*ib]);
|
||||||
|
recalculate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool /*silent*/) override {
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
const auto idx = indexOf(t);
|
||||||
|
if (!idx.has_value())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (dir == Math::DIRECTION_LEFT && *idx > 0) {
|
||||||
|
std::swap(m_targets[*idx], m_targets[*idx - 1]);
|
||||||
|
recalculate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dir == Math::DIRECTION_RIGHT && *idx + 1 < m_targets.size()) {
|
||||||
|
std::swap(m_targets[*idx], m_targets[*idx + 1]);
|
||||||
|
recalculate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<WP<ITarget>> m_targets;
|
||||||
|
|
||||||
|
void addTarget(SP<ITarget> target) {
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
const auto exists = std::ranges::any_of(m_targets, [&](const WP<ITarget>& weak) {
|
||||||
|
const auto locked = weak.lock();
|
||||||
|
return locked && locked == target;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!exists)
|
||||||
|
m_targets.emplace_back(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cleanup() {
|
||||||
|
std::erase_if(m_targets, [](const WP<ITarget>& weak) { return weak.expired(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<SP<ITarget>> liveTargets() const {
|
||||||
|
std::vector<SP<ITarget>> out;
|
||||||
|
out.reserve(m_targets.size());
|
||||||
|
|
||||||
|
for (auto const& weak : m_targets) {
|
||||||
|
const auto locked = weak.lock();
|
||||||
|
if (locked)
|
||||||
|
out.push_back(locked);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<size_t> indexOf(SP<ITarget> target) const {
|
||||||
|
for (size_t i = 0; i < m_targets.size(); ++i) {
|
||||||
|
const auto locked = m_targets[i].lock();
|
||||||
|
if (locked && locked == target)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
APICALL EXPORT std::string PLUGIN_API_VERSION() {
|
||||||
|
return HYPRLAND_API_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
|
||||||
|
g_pluginHandle = handle;
|
||||||
|
|
||||||
|
const bool ok = HyprlandAPI::addTiledAlgo(handle, "columns", &typeid(Layout::Tiled::CColumnsAlgorithm), []() -> UP<Layout::ITiledAlgorithm> {
|
||||||
|
return makeUnique<Layout::Tiled::CColumnsAlgorithm>();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ok)
|
||||||
|
HyprlandAPI::addNotification(handle, "hyprcolumns: failed to register columns layout", CHyprColor{1.0, 0.2, 0.2, 1.0}, 5000);
|
||||||
|
else
|
||||||
|
HyprlandAPI::addNotification(handle, "hyprcolumns: registered columns layout", CHyprColor{0.2, 1.0, 0.6, 1.0}, 3000);
|
||||||
|
|
||||||
|
return {.name = "hyprcolumns", .description = "Equal-width tiled columns layout", .author = "OpenAI", .version = "0.1.0"};
|
||||||
|
}
|
||||||
|
|
||||||
|
APICALL EXPORT void PLUGIN_EXIT() {
|
||||||
|
if (g_pluginHandle)
|
||||||
|
HyprlandAPI::removeAlgo(g_pluginHandle, "columns");
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user