8 Commits
v1.4 ... v1.4.2

21 changed files with 294 additions and 99 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.cache/
build/
.vscode/
result

View File

@@ -1,9 +1,45 @@
cmake_minimum_required(VERSION 3.10.0)
project(hyprland-toggle-tiling VERSION 1.4.0 LANGUAGES C CXX)
cmake_minimum_required(VERSION 3.16)
project(hyprland-toggle-tiling
VERSION 1.4.2
LANGUAGES CXX
)
include(GNUInstallDirs)
include(FetchContent)
FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz)
FetchContent_MakeAvailable(json)
# --------------------------------------------------
# Dependency handling
# --------------------------------------------------
option(USE_SYSTEM_JSON
"Use system-installed nlohmann_json instead of fetching"
OFF
)
if(USE_SYSTEM_JSON)
find_package(nlohmann_json CONFIG REQUIRED)
else()
# Try system package first silently
find_package(nlohmann_json CONFIG QUIET)
if(NOT nlohmann_json_FOUND)
message(STATUS "nlohmann_json not found, fetching from GitHub")
FetchContent_Declare(
nlohmann_json
URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz
)
FetchContent_MakeAvailable(nlohmann_json)
else()
message(STATUS "Using system nlohmann_json")
endif()
endif()
# --------------------------------------------------
# Executable
# --------------------------------------------------
add_executable(htt
src/main.cpp
@@ -12,6 +48,22 @@ add_executable(htt
src/ShellService.cpp
src/FileService.cpp
src/WindowRule.cpp
src/Client.cpp)
target_link_libraries(htt PRIVATE nlohmann_json::nlohmann_json)
target_include_directories(htt PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
src/Client.cpp
)
target_compile_features(htt PRIVATE cxx_std_17)
target_link_libraries(htt
PRIVATE
nlohmann_json::nlohmann_json
)
target_include_directories(htt PRIVATE ${PROJECT_SOURCE_DIR}/src)
# --------------------------------------------------
# Install
# --------------------------------------------------
install(TARGETS htt
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

View File

@@ -30,6 +30,7 @@ Check out [the demo](https://typofelho.ddns.net/TypoMustakes/hyprland-toggle-til
Switch to floating mode again and newly opened windows will be in floating mode.
- Floating/tiling window rules are isolated between workspaces. You can set one workspace to be floating, and all the rest to tiling for example.
- Returns applied rule for the current workspace. Useful for scripting.
- Moving a window from a tiling workspace to a floating one changes the float state of the window to respect the new workspace.
- Useful for workflows that require both tiling and floating window management.
- Lightweight and easy to integrate with your Hyprland setup.
@@ -41,6 +42,34 @@ You have a few options of obtainting the program.
You may install this program [from the AUR.](https://aur.archlinux.org/packages/hyprland-toggle-tiling-git)
### Nix Flake (for Nix users)
This project provides a Nix flake for reproducible builds and easy installation.
#### Build the package
```shell
nix build git+ssh://gitea@typofelho.ddns.net/TypoMustakes/hyprland-toggle-tiling.git
```
The binary will be at `./result/bin/htt`.
#### Add to Home Manager
1. Add the flake as an input:
```shell
inputs.htt.url = "git+ssh://gitea@typofelho.ddns.net/TypoMustakes/hyprland-toggle-tiling.git";
```
2. Then include it in your configuration:
```shell
home.packages = [
inputs.htt.packages.${pkgs.system}.default
];
```
### Download the release
You can also [download the release binary.](https://typofelho.ddns.net/TypoMustakes/hyprland-toggle-tiling/releases)
@@ -76,7 +105,7 @@ or go nuts and...
# Usage
```shell
./htt <config-file-path> (-q)
./htt <config-file-path> (flags)
```
- `-q`: Print the rule applied on the current workspace to STDOUT, but don't change anything. If there isn't an active rule for the current workspace, it returns a tiling rule. The returned string is a valid Hyprland window rule configuration line, like so:
@@ -89,6 +118,8 @@ windowrule = tile on, match:workspace 2
Potential applications for this are mainly scripts, like my waybar module here:
![wayland module showcase](https://typofelho.ddns.net/TypoMustakes/hyprland-toggle-tiling/raw/branch/master/assets/waybar_module.gif)
- `-m [integer]`: Move currently active window to specified workspace. Upon moving, the window will adapt to the windowing mode of the new workspace. Doesn't work with `-q`. See ![the 1.4 changelog](https://typofelho.ddns.net/TypoMustakes/hyprland-toggle-tiling/releases/tag/v1.4) for details.
- If the specified configuration file does not exist, it will be created.
- If the configuration contains existing rules, this should still work, but your existing configuration will probably get a bit messy, syntax-wise. I advise against it.
@@ -104,5 +135,37 @@ Potential applications for this are mainly scripts, like my waybar module here:
bind = $mod + t, exec, /path/to/htt <config-file-path>
```
Or not. Do whatever you want.
3. If you want windows to respect workspaces' rules after moving them to another workspace (see ![the 1.4 changelog](https://typofelho.ddns.net/TypoMustakes/hyprland-toggle-tiling/releases/tag/v1.4) for details), you might want to tell HTT to move your windows instead of telling Hyprland.
On the default configuration, this would mean changing this:
```sh
bind = $mainMod SHIFT, 1, movetoworkspace, 1
bind = $mainMod SHIFT, 2, movetoworkspace, 2
bind = $mainMod SHIFT, 3, movetoworkspace, 3
bind = $mainMod SHIFT, 4, movetoworkspace, 4
bind = $mainMod SHIFT, 5, movetoworkspace, 5
bind = $mainMod SHIFT, 6, movetoworkspace, 6
bind = $mainMod SHIFT, 7, movetoworkspace, 7
bind = $mainMod SHIFT, 8, movetoworkspace, 8
bind = $mainMod SHIFT, 9, movetoworkspace, 9
bind = $mainMod SHIFT, 0, movetoworkspace, 10
```
to this:
```sh
bind = $mainMod SHIFT, 1, exec, htt [your htt config file] -m 1
bind = $mainMod SHIFT, 2, exec, htt [your htt config file] -m 2
bind = $mainMod SHIFT, 3, exec, htt [your htt config file] -m 3
bind = $mainMod SHIFT, 4, exec, htt [your htt config file] -m 4
bind = $mainMod SHIFT, 5, exec, htt [your htt config file] -m 5
bind = $mainMod SHIFT, 6, exec, htt [your htt config file] -m 6
bind = $mainMod SHIFT, 7, exec, htt [your htt config file] -m 7
bind = $mainMod SHIFT, 8, exec, htt [your htt config file] -m 8
bind = $mainMod SHIFT, 9, exec, htt [your htt config file] -m 9
bind = $mainMod SHIFT, 0, exec, htt [your htt config file] -m 10
```
... in your configuration.
Otherwise, just use `exec, htt [your htt config file] -m [workspace ID]` wherever you used `movetoworkspace [workspace ID]` before.

37
default.nix Normal file
View File

@@ -0,0 +1,37 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation rec {
pname = "hyprland-toggle-tiling";
version = "1.4.2";
src = pkgs.fetchFromGitHub {
owner = "TypoMustakes";
repo = "hyprland-toggle-tiling";
rev = "v${version}";
sha256 = "sha256-NxNT2pZ4zWkF5JPPUcqJl+VFOIHyxq4ZsACg9GsWnwM=";
};
nativeBuildInputs = [
pkgs.cmake
pkgs.pkg-config
];
buildInputs = [
pkgs.hyprland
pkgs.nlohmann_json
];
cmakeFlags = [
"-DUSE_SYSTEM_JSON=ON"
];
cmakeBuildType = "Release";
meta = with pkgs.lib; {
description = "Toggle tiling and floating modes in Hyprland globally.";
homepage = "https://github.com/TypoMustakes/hyprland-toggle-tiling";
license = licenses.gpl3Only;
platforms = platforms.linux;
mainProgram = "htt";
};
}

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1775710090,
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

20
flake.nix Normal file
View File

@@ -0,0 +1,20 @@
{
description = "hyprland-toggle-tiling";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages.${system}.default = pkgs.callPackage ./default.nix {};
apps.${system}.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/htt";
};
};
}

View File

@@ -23,7 +23,6 @@ class Client {
std::array<int, 2> size;
WorkspaceSignature workspace;
bool floating;
bool pseudo;
int monitor;
std::string className;
std::string title;
@@ -34,6 +33,7 @@ class Client {
bool pinned;
int fullscreen;
int fullscreenClient;
bool overFullscreen;
std::string swallowing;
int focusHistory;
bool inhibitingIdle;

View File

@@ -2,9 +2,9 @@
#define HYPRLAND_SERVICE_H
#include <list>
#include "Workspace.h"
#include "Client.h"
#include "WindowRule.h"
#include "Workspace.hpp"
#include "Client.hpp"
#include "WindowRule.hpp"
class HyprlandService {
private:

View File

@@ -1,9 +0,0 @@
#ifndef MACROS_H
#define MACROS_H
#define HYPRCTL_BINARY "/usr/bin/hyprctl"
#define NULL_PATH "/dev/null"
#define ECHO_PATH "/usr/bin/echo"
#define CAT_PATH "/usr/bin/cat"
#endif

9
include/Macros.hpp Normal file
View File

@@ -0,0 +1,9 @@
#ifndef MACROS_H
#define MACROS_H
#define HYPRCTL_BINARY "hyprctl"
#define NULL_PATH "/dev/null"
#define ECHO_PATH "echo"
#define CAT_PATH "cat"
#endif

View File

@@ -1,4 +1,4 @@
#include "../include/Client.h"
#include "../include/Client.hpp"
void from_json(const json& j, WorkspaceSignature& ws) {
j.at("id").get_to(ws.id);
@@ -12,7 +12,6 @@ void from_json(const json& j, Client& client) {
j.at("size").get_to(client.size);
j.at("workspace").get_to(client.workspace);
j.at("floating").get_to(client.floating);
j.at("pseudo").get_to(client.pseudo);
j.at("monitor").get_to(client.monitor);
j.at("class").get_to(client.className); // Maps "class" JSON field to className
j.at("title").get_to(client.title);
@@ -23,6 +22,7 @@ void from_json(const json& j, Client& client) {
j.at("pinned").get_to(client.pinned);
j.at("fullscreen").get_to(client.fullscreen);
j.at("fullscreenClient").get_to(client.fullscreenClient);
j.at("overFullscreen").get_to(client.overFullscreen);
j.at("swallowing").get_to(client.swallowing);
j.at("focusHistoryID").get_to(client.focusHistory);
j.at("inhibitingIdle").get_to(client.inhibitingIdle);

View File

@@ -1,8 +1,8 @@
#include <sstream>
#include <fstream>
#include "../include/FileService.h"
#include "../include/Macros.h"
#include "../include/ShellService.h"
#include "../include/FileService.hpp"
#include "../include/Macros.hpp"
#include "../include/ShellService.hpp"
bool FileService::doesNonEmptyFileExist(std::string path) {
std::ifstream file(path);

View File

@@ -1,10 +1,10 @@
#include "../include/HyprlandService.hpp"
#include "../include/FileService.hpp"
#include "../include/Macros.hpp"
#include "../include/ShellService.hpp"
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
#include "../include/HyprlandService.h"
#include "../include/ShellService.h"
#include "../include/Macros.h"
#include "../include/FileService.h"
using json = nlohmann::json;
@@ -20,9 +20,7 @@ void HyprlandService::setConfigFilePath(std::string path) {
configFilePath = path;
};
std::string HyprlandService::getConfigFilePath() {
return configFilePath;
};
std::string HyprlandService::getConfigFilePath() { return configFilePath; };
std::list<Workspace> HyprlandService::getWorkspaces() {
json j = json::parse(ShellService::exec(HYPRCTL_BINARY " workspaces -j"));
@@ -44,7 +42,8 @@ std::optional<Workspace> HyprlandService::getWorkspace(int id) {
}
Workspace HyprlandService::getCurrentWorkspace() {
json j = json::parse(ShellService::exec(HYPRCTL_BINARY " activeworkspace -j"));
json j =
json::parse(ShellService::exec(HYPRCTL_BINARY " activeworkspace -j"));
return j.get<Workspace>();
};
@@ -84,15 +83,17 @@ std::list<WindowRule> HyprlandService::getWindowRules() {
};
void HyprlandService::setClientFloating(Client c) {
ShellService::exec(HYPRCTL_BINARY " dispatch setfloating address:" + c.address);
ShellService::exec(HYPRCTL_BINARY " dispatch setfloating address:" +
c.address);
};
void HyprlandService::setClientTiled(Client c) {
ShellService::exec(HYPRCTL_BINARY " dispatch settiled address:" + c.address);
}
//on = true -> creates a window rule to ENABLE floating mode for currently active workspace
//on = false -> creates a window rule to DISABLE floating mode for currently active workspace
// on = true -> creates a window rule to ENABLE floating mode for currently
// active workspace on = false -> creates a window rule to DISABLE floating mode
// for currently active workspace
void HyprlandService::setFloatingRule(bool on) {
WindowRule rule{.tile = !on, .workspaceID = getCurrentWorkspace().id};
auto conflictingRule = findConflictingRule(rule);
@@ -100,19 +101,16 @@ void HyprlandService::setFloatingRule(bool on) {
removeRule(conflictingRule.value());
}
FileService::appendToFile(
getConfigFilePath(),
rule.toString()
);
FileService::appendToFile(getConfigFilePath(), rule.toString());
};
std::optional<WindowRule> HyprlandService::findConflictingRule(WindowRule subject) {
std::optional<WindowRule>
HyprlandService::findConflictingRule(WindowRule subject) {
std::list<WindowRule> rules = getWindowRules();
int id = getCurrentWorkspace().id;
for (auto &rule : rules) {
if (rule.tile == !subject.tile && rule.workspaceID == subject.workspaceID)
{
if (rule.tile == !subject.tile && rule.workspaceID == subject.workspaceID) {
return rule;
}
}
@@ -126,8 +124,7 @@ void HyprlandService::removeRule(WindowRule rule) {
int foundIndex = -1;
for (auto &it : rules) {
if (it.toString() == rule.toString())
{
if (it.toString() == rule.toString()) {
foundIndex = index;
break;
}
@@ -135,10 +132,7 @@ void HyprlandService::removeRule(WindowRule rule) {
}
if (foundIndex != -1) {
FileService::deleteNthLine(
getConfigFilePath(),
foundIndex
);
FileService::deleteNthLine(getConfigFilePath(), foundIndex);
}
// else: rule not found, do nothing
@@ -181,7 +175,8 @@ void HyprlandService::moveToWorkspace(int workspaceId) {
setClientTiled(getActiveClient());
}
ShellService::exec(HYPRCTL_BINARY " dispatch movetoworkspace " + std::to_string(workspaceId));
ShellService::exec(HYPRCTL_BINARY " dispatch movetoworkspace " +
std::to_string(workspaceId));
}
void HyprlandService::toggleFloating() {

View File

@@ -1,4 +1,4 @@
#include "../include/ShellService.h"
#include "../include/ShellService.hpp"
#include <pwd.h>
#include <unistd.h>
@@ -23,15 +23,16 @@ std::string ShellService::exec(const std::string& command) {
return result;
};
std::string ShellService::getHomePath()
{
std::string ShellService::getHomePath() {
char *home = getenv("HOME");
if (home && *home) {
return std::string(home);
return std::string(home) + std::string("/");
}
// Fallback: try to get home directory from passwd if HOME is not set
// This assumes that the 'htt' process is ran with the user as the process owner. Should it be run with systemd or some other wonky method, it will behave unexpectedly
// This assumes that the 'htt' process is ran with the user as the process
// owner. Should it be run with systemd or some other wonky method, it will
// behave unexpectedly
struct passwd *pw = getpwuid(getuid());
if (pw && pw->pw_dir) {
return std::string(pw->pw_dir);

View File

@@ -1,5 +1,5 @@
#include <algorithm>
#include "../include/WindowRule.h"
#include "../include/WindowRule.hpp"
std::string WindowRule::toString() {
std::string mode = this->tile ? "tile on" : "float on";

View File

@@ -1,4 +1,4 @@
#include "../include/Workspace.h"
#include "../include/Workspace.hpp"
void from_json(const nlohmann::json &j, Workspace &w) {
j.at("id").get_to(w.id);

View File

@@ -1,6 +1,6 @@
#include <iostream>
#include <stdexcept>
#include "../include/HyprlandService.h"
#include "../include/HyprlandService.hpp"
void help(char* execPath) {
std::cerr << "Usage: " << execPath << " <config_file_path> (flags)\n\n";
@@ -20,7 +20,6 @@ int main(int argc, char** argv) {
else if (argc == 3 && argv[2] == std::string("-q")) {
std::cout << HyprlandService::getActiveWorkspaceRule().toString() << std::endl;
} else if (argc == 4 && argv[2] == std::string("-m")) {
int workspace = 0;
try {
HyprlandService::moveToWorkspace(std::stoi(argv[3]));
} catch (std::invalid_argument) {