Initial commit

This commit is contained in:
WayfinderAK 2026-03-16 15:25:26 -08:00
commit 5a6fe00c9b
No known key found for this signature in database
5 changed files with 314 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
libhyprcolumns.so

21
LICENSE Normal file
View 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
View 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
View 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
View 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");
}