Below are some utilities I edit to make my desktop comfortable.
The main Gentoo-specific advantageous change I make is the integration of an internal hyprland gesture patch I use.
I use gui-wm/hyprland (uses the Wayland secure display protocol), since animations make tiling WMs more accessible (i.e. visual responsivity). One important such feature is the progress-of-the-gesture-is-distance-in-the-animation. But I like to have both swiping up and swiping left/right, in a row-major basically-mod-20 workspace setup (a diy grid).
/etc/portage/patches/gui-wm/hyprland/grid-swipe.patch
diff --git a/src/config/lua/bindings/LuaBindingsConfigRules.cpp b/src/config/lua/bindings/LuaBindingsConfigRules.cpp
index cc6108ff..d99c44c6 100644
--- a/src/config/lua/bindings/LuaBindingsConfigRules.cpp
+++ b/src/config/lua/bindings/LuaBindingsConfigRules.cpp
@@ -878,6 +878,20 @@ static int hlGesture(lua_State* L) {
#undef GET_ACTION_STRING
+ // "workspace" action: how many workspace IDs a full swipe jumps (grid row/column size). Default 1 = adjacent.
+ int step = 1;
+ lua_getfield(L, 1, "step");
+ if (!lua_isnil(L, -1)) {
+ CLuaConfigInt stepParser(1, 1, 1000);
+ auto stepErr = stepParser.parse(L);
+ if (stepErr.errorCode != PARSE_ERROR_OK) {
+ lua_pop(L, 1);
+ return Internal::configError(L, std::format("hl.gesture: field \"step\": {}", stepErr.message));
+ }
+ step = stepParser.parsed();
+ }
+ lua_pop(L, 1);
+
Input::ModifierMask modMask = Input::HL_MODIFIER_NONE;
lua_getfield(L, 1, "mods");
if (!lua_isnil(L, -1)) {
@@ -942,7 +956,7 @@ static int hlGesture(lua_State* L) {
const auto& action = actionParser.parsed();
if (action == "workspace")
- result = g_pTrackpadGestures->addGesture(makeUnique<CWorkspaceSwipeGesture>(), fingerCount, direction, modMask, deltaScale, disableInhibit);
+ result = g_pTrackpadGestures->addGesture(makeUnique<CWorkspaceSwipeGesture>(step), fingerCount, direction, modMask, deltaScale, disableInhibit);
else if (action == "resize")
result = g_pTrackpadGestures->addGesture(makeUnique<CResizeTrackpadGesture>(), fingerCount, direction, modMask, deltaScale, disableInhibit);
else if (action == "move")
diff --git a/src/managers/input/UnifiedWorkspaceSwipeGesture.cpp b/src/managers/input/UnifiedWorkspaceSwipeGesture.cpp
index 2f407790..60d05e93 100644
--- a/src/managers/input/UnifiedWorkspaceSwipeGesture.cpp
+++ b/src/managers/input/UnifiedWorkspaceSwipeGesture.cpp
@@ -1,5 +1,7 @@
#include "UnifiedWorkspaceSwipeGesture.hpp"
+#include <format>
+
#include "../../Compositor.hpp"
#include "../../state/WorkspaceState.hpp"
#include "../../state/workspace/Resolver.hpp"
@@ -22,7 +24,7 @@ bool CUnifiedWorkspaceSwipeGesture::isGestureInProgress() {
return !!m_workspaceBegin;
}
-void CUnifiedWorkspaceSwipeGesture::begin() {
+void CUnifiedWorkspaceSwipeGesture::begin(int step, std::optional<bool> vertical) {
if (isGestureInProgress())
return;
@@ -34,6 +36,8 @@ void CUnifiedWorkspaceSwipeGesture::begin() {
LOG(Log::DEBUG, "CUnifiedWorkspaceSwipeGesture::begin: Starting a swipe from {}", PWORKSPACE->displayName());
+ m_step = std::max(1, step);
+ m_vertical = vertical;
m_workspaceBegin = PWORKSPACE;
m_delta = 0;
m_monitor = MONITOR;
@@ -66,15 +70,15 @@ void CUnifiedWorkspaceSwipeGesture::update(double delta) {
const auto XDISTANCE = m_monitor->m_size.x + *PWORKSPACEGAP;
const auto YDISTANCE = m_monitor->m_size.y + *PWORKSPACEGAP;
const auto ANIMSTYLE = m_workspaceBegin->m_renderOffset->getStyle();
- const bool VERTANIMS = ANIMSTYLE == "slidevert" || ANIMSTYLE.starts_with("slidefadevert");
+ const bool VERTANIMS = m_vertical.value_or(ANIMSTYLE == "slidevert" || ANIMSTYLE.starts_with("slidefadevert"));
const double d = m_delta - delta;
m_delta = delta;
m_avgSpeed = (m_avgSpeed * m_speedPoints + abs(d)) / (m_speedPoints + 1);
m_speedPoints++;
- auto workspaceLeft = State::Workspace::resolver()->getWorkspaceTargetFromString((*PSWIPEUSER ? "r-1" : "m-1"));
- auto workspaceRight = State::Workspace::resolver()->getWorkspaceTargetFromString((*PSWIPEUSER ? "r+1" : "m+1"));
+ auto workspaceLeft = State::Workspace::resolver()->getWorkspaceTargetFromString(std::format("{}-{}", *PSWIPEUSER || m_step != 1 ? "r" : "m", m_step));
+ auto workspaceRight = State::Workspace::resolver()->getWorkspaceTargetFromString(std::format("{}+{}", *PSWIPEUSER || m_step != 1 ? "r" : "m", m_step));
if (!workspaceLeft.valid() || !workspaceRight.valid() || (sameWorkspaceIdentity(workspaceLeft, m_workspaceBegin) && !*PSWIPENEW)) {
m_workspaceBegin = nullptr; // invalidate the swipe
@@ -90,7 +94,7 @@ void CUnifiedWorkspaceSwipeGesture::update(double delta) {
const auto RIGHT_ID = std::get_if<Workspace::SWorkspaceNumberedID>(&*workspaceRight.id);
if ((sameWorkspaceIdentity(workspaceLeft, m_workspaceBegin) && *PSWIPENEW && (m_delta < 0)) ||
(m_delta > 0 && m_workspaceBegin->getWindowCount() == 0 && BEGIN_ID && RIGHT_ID && RIGHT_ID->value <= *BEGIN_ID) ||
- (m_delta < 0 && BEGIN_ID && LEFT_ID && *BEGIN_ID <= LEFT_ID->value)) {
+ (m_delta < 0 && BEGIN_ID && LEFT_ID && *BEGIN_ID <= LEFT_ID->value) || (m_delta < 0 && m_step > 1 && BEGIN_ID && m_step >= *BEGIN_ID)) {
m_delta = 0;
g_pHyprRenderer->damageMonitor(m_monitor.lock());
@@ -194,7 +198,7 @@ void CUnifiedWorkspaceSwipeGesture::update(double delta) {
if (*PSWIPEFOREVER) {
if (abs(m_delta) >= SWIPEDISTANCE) {
end();
- begin();
+ begin(m_step, m_vertical);
}
}
}
@@ -210,11 +214,11 @@ void CUnifiedWorkspaceSwipeGesture::end() {
static auto PSWIPEUSER = CConfigValue<Config::INTEGER>("gestures:workspace_swipe_use_r");
static auto PWORKSPACEGAP = CConfigValue<Config::INTEGER>("general:gaps_workspaces");
const auto ANIMSTYLE = m_workspaceBegin->m_renderOffset->getStyle();
- const bool VERTANIMS = ANIMSTYLE == "slidevert" || ANIMSTYLE.starts_with("slidefadevert");
+ const bool VERTANIMS = m_vertical.value_or(ANIMSTYLE == "slidevert" || ANIMSTYLE.starts_with("slidefadevert"));
// commit
- auto workspaceLeft = State::Workspace::resolver()->getWorkspaceTargetFromString((*PSWIPEUSER ? "r-1" : "m-1"));
- auto workspaceRight = State::Workspace::resolver()->getWorkspaceTargetFromString((*PSWIPEUSER ? "r+1" : "m+1"));
+ auto workspaceLeft = State::Workspace::resolver()->getWorkspaceTargetFromString(std::format("{}-{}", *PSWIPEUSER || m_step != 1 ? "r" : "m", m_step));
+ auto workspaceRight = State::Workspace::resolver()->getWorkspaceTargetFromString(std::format("{}+{}", *PSWIPEUSER || m_step != 1 ? "r" : "m", m_step));
const auto SWIPEDISTANCE = std::clamp(*PSWIPEDIST, sc<int64_t>(1LL), sc<int64_t>(UINT32_MAX));
// If we've been swiping off the right end with PSWIPENEW enabled, there is
@@ -222,7 +226,7 @@ void CUnifiedWorkspaceSwipeGesture::end() {
const auto BEGIN_ID = m_workspaceBegin->numberedID();
const auto RIGHT_ID = workspaceRight.id ? std::get_if<Workspace::SWorkspaceNumberedID>(&*workspaceRight.id) : nullptr;
if (BEGIN_ID && RIGHT_ID && RIGHT_ID->value <= *BEGIN_ID && *PSWIPENEW)
- workspaceRight = State::Workspace::resolver()->getWorkspaceTargetFromString("r+1");
+ workspaceRight = State::Workspace::resolver()->getWorkspaceTargetFromString(std::format("r+{}", m_step));
if (!workspaceLeft.valid() || !workspaceRight.valid()) {
m_workspaceBegin->m_renderOffset->setValueAndWarp({});
@@ -241,7 +245,9 @@ void CUnifiedWorkspaceSwipeGesture::end() {
PHLWORKSPACE pSwitchedTo = nullptr;
- if ((abs(m_delta) < SWIPEDISTANCE * *PSWIPEPERC && (*PSWIPEFORC == 0 || (*PSWIPEFORC != 0 && m_avgSpeed < *PSWIPEFORC))) || abs(m_delta) < 2) {
+ // Reject grid jumps below workspace 1 instead of accepting the resolver's clamped target.
+ if ((m_delta < 0 && m_step > 1 && BEGIN_ID && m_step >= *BEGIN_ID) ||
+ (abs(m_delta) < SWIPEDISTANCE * *PSWIPEPERC && (*PSWIPEFORC == 0 || (*PSWIPEFORC != 0 && m_avgSpeed < *PSWIPEFORC))) || abs(m_delta) < 2) {
// revert
if (abs(m_delta) < 2) {
if (PWORKSPACEL)
diff --git a/src/managers/input/UnifiedWorkspaceSwipeGesture.hpp b/src/managers/input/UnifiedWorkspaceSwipeGesture.hpp
index 4dbb6c5d..73609572 100644
--- a/src/managers/input/UnifiedWorkspaceSwipeGesture.hpp
+++ b/src/managers/input/UnifiedWorkspaceSwipeGesture.hpp
@@ -1,25 +1,33 @@
#pragma once
+#include <optional>
+
#include "../../helpers/memory/Memory.hpp"
#include "../../desktop/DesktopTypes.hpp"
class CUnifiedWorkspaceSwipeGesture {
public:
- void begin();
+ // step: how many workspace IDs a full swipe jumps (1 = classic adjacent swipe, >1 = grid row/column jump).
+ // vertical: force the swipe axis (true = vertical, false = horizontal). If unset, falls back to the global
+ // `workspaces` animation style (slidevert -> vertical), preserving legacy behavior.
+ void begin(int step = 1, std::optional<bool> vertical = std::nullopt);
void update(double delta);
void end();
bool isGestureInProgress();
private:
- PHLWORKSPACE m_workspaceBegin = nullptr;
- PHLMONITORREF m_monitor;
-
- double m_delta = 0;
- int m_initialDirection = 0;
- float m_avgSpeed = 0;
- int m_speedPoints = 0;
- int m_touchID = 0;
+ PHLWORKSPACE m_workspaceBegin = nullptr;
+ PHLMONITORREF m_monitor;
+
+ int m_step = 1;
+ std::optional<bool> m_vertical = std::nullopt;
+
+ double m_delta = 0;
+ int m_initialDirection = 0;
+ float m_avgSpeed = 0;
+ int m_speedPoints = 0;
+ int m_touchID = 0;
friend class CWorkspaceSwipeGesture;
friend class CInputManager;
diff --git a/src/managers/input/trackpad/gestures/WorkspaceSwipeGesture.cpp b/src/managers/input/trackpad/gestures/WorkspaceSwipeGesture.cpp
index ead116a7..23bcd1f3 100644
--- a/src/managers/input/trackpad/gestures/WorkspaceSwipeGesture.cpp
+++ b/src/managers/input/trackpad/gestures/WorkspaceSwipeGesture.cpp
@@ -1,5 +1,7 @@
#include "WorkspaceSwipeGesture.hpp"
+#include <optional>
+
#include "../../../../Compositor.hpp"
#include "../../../../state/WorkspaceState.hpp"
#include "../../../../desktop/state/FocusState.hpp"
@@ -7,6 +9,10 @@
#include "../../UnifiedWorkspaceSwipeGesture.hpp"
+CWorkspaceSwipeGesture::CWorkspaceSwipeGesture(int step) : m_step(step) {
+ ;
+}
+
void CWorkspaceSwipeGesture::begin(const ITrackpadGesture::STrackpadGestureBegin& e) {
ITrackpadGesture::begin(e);
@@ -24,7 +30,15 @@ void CWorkspaceSwipeGesture::begin(const ITrackpadGesture::STrackpadGestureBegin
if (onMonitor < 2 && !*PSWIPENEW)
return; // disallow swiping when there's 1 workspace on a monitor
- g_pUnifiedWorkspaceSwipe->begin();
+ // Force the animation axis to match the actual swipe direction, so a vertical grid swipe slides
+ // vertically regardless of the global `workspaces` animation style (and horizontal stays horizontal).
+ std::optional<bool> vertical;
+ if (e.direction == TRACKPAD_GESTURE_DIR_UP || e.direction == TRACKPAD_GESTURE_DIR_DOWN)
+ vertical = true;
+ else if (e.direction == TRACKPAD_GESTURE_DIR_LEFT || e.direction == TRACKPAD_GESTURE_DIR_RIGHT)
+ vertical = false;
+
+ g_pUnifiedWorkspaceSwipe->begin(m_step, vertical);
}
void CWorkspaceSwipeGesture::update(const ITrackpadGesture::STrackpadGestureUpdate& e) {
diff --git a/src/managers/input/trackpad/gestures/WorkspaceSwipeGesture.hpp b/src/managers/input/trackpad/gestures/WorkspaceSwipeGesture.hpp
index 203fc329..8561367b 100644
--- a/src/managers/input/trackpad/gestures/WorkspaceSwipeGesture.hpp
+++ b/src/managers/input/trackpad/gestures/WorkspaceSwipeGesture.hpp
@@ -5,7 +5,8 @@
class CWorkspaceSwipeGesture : public ITrackpadGesture {
public:
- CWorkspaceSwipeGesture() = default;
+ // step: how many workspace IDs a full swipe jumps. 1 = classic adjacent swipe, >1 = grid row/column jump.
+ CWorkspaceSwipeGesture(int step = 1);
virtual ~CWorkspaceSwipeGesture() = default;
virtual void begin(const ITrackpadGesture::STrackpadGestureBegin& e);
@@ -13,4 +14,7 @@ class CWorkspaceSwipeGesture : public ITrackpadGesture {
virtual void end(const ITrackpadGesture::STrackpadGestureEnd& e);
virtual bool isDirectionSensitive();
+
+ private:
+ int m_step = 1;
};
I use the latest hyprland git cuz I'd rather risk having new bugs than having old ones. So I added this patch (stuff broke when i updated to 92b82c0c1e4168d93903ec42a2276843bbd84821).
/etc/portage/patches/gui-wm/hyprland/gcc15-string-subview.patch
diff --git a/src/ipc/s1/S1.cpp b/src/ipc/s1/S1.cpp
--- a/src/ipc/s1/S1.cpp
+++ b/src/ipc/s1/S1.cpp
@@ -8,6 +8,7 @@
#include <optional>
#include <ranges>
#include <sstream>
+#include <string_view>
#include <hyprutils/string/String.hpp>
using namespace IPC::Socket1;
@@ -145,7 +146,7 @@
if (i < request.size() && (request[i + 1] == '\\' || request[i + 1] == ';'))
++i;
else
- LOG(Log::ERR, "Malformed socket1 request: invalid escape sequence {} at position {}, using it verbatim", request.subview(i, 2), i);
+ LOG(Log::ERR, "Malformed socket1 request: invalid escape sequence {} at position {}, using it verbatim", std::string_view{request}.substr(i, 2), i);
}
parsedCommand << request[i];
}
/etc/portage/package.accept_keywords/hyprland
gui-wm/hyprland **
dev-libs/wayland-protocols ~amd64
# required by xdg-desktop-portal-hyprland
dev-cpp/sdbus-c++ ~amd64
Btw make sure you're on gcc 15 to avoid class std::vector<unsigned char> has no member named append_range
The only Gentoo-specific switch I need to hit is
/etc/portage/package.accept_keywords/kvantum
x11-themes/kvantum ~amd64
since it makes QT6 apps look more standardized to my theme. Other than that, everything in Blackbriar-theme ⇗ is distro-agnostic.
TODO: mention that i patched ags at https://github.com/swomf/overlay-funroll/blob/main/gui-libs/astal-hyprland/files/hyprwm-v0.55-lua.patch
For a while I'd used ags v2.3.0 way past the due date. Just to use ags v2.3.0 after the Hyprland v0.55 breaking Lua changes, I'd even written a basic ags backport ⇗. I want to make my bar and leave it there forever. But I finally got around to updating and fixing the package management of ags in the ::funroll overlay. Harhar.
Also I don't use quickshell; here's the boring comparison ⇗ that made me use ags instead.
Aylur's GTK Shell covers my bar and notification daemon, and will probably eventually subsume my app launchers and dmenu-like apps (i.e. anyrun). It's expressive, minimal, doesn't need a domain-specific language (ewww), and has pleasant TypeScript GTK support.
/etc/portage/package.use/ags
# I use ::funroll libcava since guru doesn't have
# libcava 1.0 at the time of writing (2026-07-25)
gui-libs/astal-meta apps auth battery bluetooth brightness
gui-libs/astal-meta cava gjs greetd gtk3 gtk4 hyprland mpris
gui-libs/astal-meta network notifd powerprofiles river tray wireplumber
gui-libs/astal-gjs gtk4
dev-lang/vala valadoc
# network USE flag deps
net-libs/libnma vala
net-misc/networkmanager vala
# gtk USE flag deps
gui-libs/gtk4-layer-shell vala
gui-libs/gtk-layer-shell vala
/etc/portage/package.accept_keywords/ags
gui-libs/gtk4-layer-shell ~amd64
For a file manager I use app-misc/nnn (terminal-based) and gnome-extra/nemo (gui-based) over competitors:
Setting up app-misc/nnn thumbnailing is done via a single-file plugin ⇗ and requires the Kitty terminal (or any terminal supporting the Kitty image protocol).
Setting up good Nemo thumbnailing is a bit manual.
/etc/portage/package.use/filemanager
# nnn
app-misc/nnn nerdfonts
# nemo and dependencies
gnome-extra/nemo exif
dev-libs/libdbusmenu gtk3
# nemo thumbnailing
media-video/ffmpegthumbnailer gnome gtk jpeg png
x11-libs/gdk-pixbuf jpeg gif tiff