Add hyprsome fork

This commit is contained in:
Thomas Avé 2023-12-02 13:49:32 +01:00
parent 933322768f
commit dcd09dee2a
13 changed files with 1989 additions and 0 deletions

2
hypr/hyprsome/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/result

1207
hypr/hyprsome/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
hypr/hyprsome/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "hyprsome"
description = "A small CLI apps that allows to make Hyprland's workspaces work like Awesome in multi-monitor setup"
license = "GPL-3.0"
version = "0.1.11"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.0.15", features = ["derive"] }
ipc-rpc = "1.2.2"
schemars = "0.8.11"
serde = "1.0.145"
serde_json = "1.0.86"
tokio = "1.21.2"
hyprland = "0.3.1"

16
hypr/hyprsome/LICENSE.txt Normal file
View File

@ -0,0 +1,16 @@
The GPLv3 License (GPLv3)
Copyright (c) 2022 Author
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

76
hypr/hyprsome/README.md Normal file
View File

@ -0,0 +1,76 @@
# Hyprsome
Hyprsome is a binary that interacts with Hyprland's Unix socket to make workspaces behave similarly to AwesomeWM in a multi-monitor setup.
If you're focused on a monitor and press SUPER+[1-9], you'll only switch to the workspaces that are bound to that monitor.
It is inspired by Swaysome, which does a similar thing for Sway.
# Installation
`
cargo install hyprsome
`
# Usage
Once the binary is installed, you can modify your ~/.config/hypr/hyprland.conf to accomodate it.
Here is an example of a dual monitor setup:
```
monitor=DP-1,1920x1080@60,0x0,1.33
monitor=DP-1,transform,1
workspace=DP-1,1
monitor=HDMI-A-1,3440x1440@100,813x0,1
workspace=HDMI-A-1,11
```
Most noteworthy thing here is the 'workspace' keyword that I use to bind a default workspace for each monitor.
Then you can bind workspaces to your different monitors.
It is very important that you bind your workspaces in order.
Check the results of `hyprctl monitors`. Bind workspaces from 1 to 9 on your monitor that has 0 as an id.
Then just bind workspaces by prefixing numbers by the id of the monitor they're bound to.
Here, HDMI-A-1's id is 1, so I bind workspaces from 11 to 19 to it.
```
workspace=1,monitor:DP-1
workspace=2,monitor:DP-1
workspace=3,monitor:DP-1
workspace=4,monitor:DP-1
workspace=5,monitor:DP-1
workspace=11,monitor:HDMI-A-1
workspace=12,monitor:HDMI-A-1
workspace=13,monitor:HDMI-A-1
workspace=14,monitor:HDMI-A-1
workspace=15,monitor:HDMI-A-1
```
Then it's just a matter of making sure your regular workspace keybinds call hyprsome.
```
bind=SUPER,1,exec,hyprsome workspace 1
bind=SUPER,2,exec,hyprsome workspace 2
bind=SUPER,3,exec,hyprsome workspace 3
bind=SUPER,4,exec,hyprsome workspace 4
bind=SUPER,5,exec,hyprsome workspace 5
bind=SUPERSHIFT,1,exec,hyprsome move 1
bind=SUPERSHIFT,2,exec,hyprsome move 2
bind=SUPERSHIFT,3,exec,hyprsome move 3
bind=SUPERSHIFT,4,exec,hyprsome move 4
bind=SUPERSHIFT,5,exec,hyprsome move 5
```
# Limitations
This is alpha software and my first program in Rust, bugs are bound to happen but nothing that will break your system.
Some features are most likely missing.
You can only have 9 workspaces per monitor as of now.
I haven't worked on supporting monitor hot-plug at all. It may work but it's unlikely.

159
hypr/hyprsome/flake.lock Normal file
View File

@ -0,0 +1,159 @@
{
"nodes": {
"crane": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1684981077,
"narHash": "sha256-68X9cFm0RTZm8u0rXPbeBzOVUH5OoUGAfeHHVoxGd9o=",
"owner": "ipetkov",
"repo": "crane",
"rev": "35110cccf28823320f4fd697fcafcb5038683982",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1685518550,
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1685498995,
"narHash": "sha256-rdyjnkq87tJp+T2Bm1OD/9NXKSsh/vLlPeqCc/mm7qs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9cfaa8a1a00830d17487cb60a19bb86f96f09b27",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": [
"crane",
"flake-utils"
],
"nixpkgs": [
"crane",
"nixpkgs"
]
},
"locked": {
"lastModified": 1683080331,
"narHash": "sha256-nGDvJ1DAxZIwdn6ww8IFwzoHb2rqBP4wv/65Wt5vflk=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "d59c3fa0cba8336e115b376c2d9e91053aa59e56",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

23
hypr/hyprsome/flake.nix Normal file
View File

@ -0,0 +1,23 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
crane.url = "github:ipetkov/crane";
crane.inputs.nixpkgs.follows = "nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, crane, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
craneLib = crane.lib.${system};
in
{
packages.default = craneLib.buildPackage {
src = craneLib.cleanCargoSource (craneLib.path ./.);
# Add extra inputs here or any other derivation settings
# doCheck = true;
# buildInputs = [];
# nativeBuildInputs = [];
};
});
}

View File

@ -0,0 +1,17 @@
use hyprland::{
data::{Client, Clients},
dispatch::{Direction, Dispatch, DispatchType},
shared::{HyprData, HyprDataActiveOptional},
};
pub fn get_active() -> Option<Client> {
Client::get_active().unwrap()
}
pub fn get() -> Clients {
Clients::get().unwrap()
}
pub fn focus_by_direction(direction: Direction) {
let _ = Dispatch::call(DispatchType::MoveFocus(direction));
}

View File

@ -0,0 +1,37 @@
pub mod client;
pub mod monitor;
pub mod option;
pub mod workspace;
use std::env;
use std::io::prelude::*;
use std::os::unix::net::UnixStream;
extern crate serde_json;
fn send_message(action: &str, args: Vec<&str>) -> String {
let env_var_name = "HYPRLAND_INSTANCE_SIGNATURE";
let hyprland_instance_sig = match env::var(env_var_name) {
Ok(v) => v,
Err(e) => panic!("${} is not set ({})", env_var_name, e),
};
let socket_path = format!("/tmp/hypr/{}/.socket.sock", hyprland_instance_sig);
let mut stream = match UnixStream::connect(socket_path) {
Err(_) => panic!("server is not running"),
Ok(stream) => stream,
};
let mut message = format!("j/{}", action);
args.into_iter()
.for_each(|a| message.push_str(&format!(" {}", a)));
// TODO: stop being stinky and manage errors
let _ = stream.write_all(message.as_bytes());
let mut response = String::new();
// TODO: stop being stinky and manage errors
let _ = stream.read_to_string(&mut response);
response
}

View File

@ -0,0 +1,43 @@
use hyprland::data::{Monitor, Monitors};
use hyprland::dispatch::*;
use hyprland::shared::HyprData;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct ActiveWorkspace {
pub id: u64,
pub name: String,
}
pub fn get_by_id(id: i16) -> Monitor {
let mut monitors = get();
monitors.find(|m| m.id == id).unwrap()
}
pub fn get() -> Monitors {
Monitors::get().unwrap()
}
pub fn focus_left() {
let _ = Dispatch::call(DispatchType::FocusMonitor(MonitorIdentifier::Direction(
Direction::Left,
)));
}
pub fn focus_right() {
let _ = Dispatch::call(DispatchType::FocusMonitor(MonitorIdentifier::Direction(
Direction::Right,
)));
}
pub fn focus_up() {
let _ = Dispatch::call(DispatchType::FocusMonitor(MonitorIdentifier::Direction(
Direction::Up,
)));
}
pub fn focus_down() {
let _ = Dispatch::call(DispatchType::FocusMonitor(MonitorIdentifier::Direction(
Direction::Down,
)));
}

View File

@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
const GETOPTIONS: &str = "getoptions";
const GENERAL_GAPS_OUT: &str = "general:gaps_out";
#[derive(Serialize, Deserialize, Debug)]
pub struct HyprlandOption {
pub option: String,
pub int: i32,
pub float: f64,
pub str: String,
}
pub fn get_gaps() -> i16 {
let response = super::send_message(GETOPTIONS, vec![GENERAL_GAPS_OUT]);
let gap_option: HyprlandOption = serde_json::from_str(&response).unwrap();
gap_option.int as i16
}

View File

@ -0,0 +1,23 @@
// TODO: change this file to hyprland-rs
const WORKSPACE: &str = "workspace";
const DISPATCH: &str = "dispatch";
const MOVETOWORKSPACESILENT: &str = "movetoworkspacesilent";
const MOVETOWORKSPACE: &str = "movetoworkspace";
pub fn focus(workspace_number: &u64) {
let _ = super::send_message(DISPATCH, vec![WORKSPACE, &workspace_number.to_string()]);
}
pub fn move_to(workspace_number: &u64) {
super::send_message(
DISPATCH,
vec![MOVETOWORKSPACESILENT, &workspace_number.to_string()],
);
}
pub fn move_focus(workspace_number: &u64) {
super::send_message(
DISPATCH,
vec![MOVETOWORKSPACE, &workspace_number.to_string()],
);
}

350
hypr/hyprsome/src/main.rs Normal file
View File

@ -0,0 +1,350 @@
use clap::{Parser, Subcommand, ValueEnum};
mod hyprland_ipc;
use hyprland::{
data::{Client, Monitor, Transforms, Workspaces},
dispatch::Direction, shared::HyprData,
};
use hyprland_ipc::{client, monitor, option, workspace};
#[derive(Parser)]
#[command(name = "hyprsome")]
#[command(author = "sopa0")]
#[command(version = "0.1.11")]
#[command(about = "Makes hyprland workspaces behave like awesome", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Focus { direction: Directions },
Workspace { workspace_number: u64 },
Move { workspace_number: u64 },
Movefocus { workspace_number: u64 },
MoveEmpty,
FocusEmpty,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
enum Directions {
L,
R,
U,
D,
}
pub trait MonitorDimensions {
fn real_width(&self) -> f32;
fn real_height(&self) -> f32;
}
impl MonitorDimensions for Monitor {
fn real_width(&self) -> f32 {
match self.transform {
Transforms::Normal
| Transforms::Normal180
| Transforms::Flipped
| Transforms::Flipped180 => self.width as f32 / self.scale,
Transforms::Normal90 | Transforms::Normal270 | Transforms::Flipped90 => {
self.height as f32 / self.scale
}
_ => self.width as f32,
}
}
fn real_height(&self) -> f32 {
match self.transform {
Transforms::Normal
| Transforms::Flipped
| Transforms::Normal180
| Transforms::Flipped180 => self.height as f32 / self.scale,
Transforms::Normal90 | Transforms::Normal270 | Transforms::Flipped90 => {
self.width as f32 / self.scale
}
_ => self.height as f32,
}
}
}
pub fn get_current_monitor() -> Monitor {
monitor::get().find(|m| m.focused).unwrap()
}
//TODO: refactor this nonsense
pub fn select_workspace(workspace_number: &u64) {
let mon = get_current_monitor();
match mon.id {
0 => workspace::focus(workspace_number),
_ => {
workspace::focus(
&format!("{}{}", mon.id, workspace_number)
.parse::<u64>()
.unwrap(),
);
}
}
}
pub fn get_empty_workspace() -> u64 {
let mon = get_current_monitor();
let mut found = Vec::new();
for workspaces in Workspaces::get().iter() {
for workspace in workspaces.iter() {
if workspace.monitor == mon.name {
let mut id = workspace.name.clone();
if id.len() > 1 {
id = id.chars().nth(1).unwrap().to_string();
}
found.push(id);
}
}
}
for i in 1..9 {
if !found.contains(&i.to_string()) {
return i;
}
}
return 1; // Send to the first workspace if no others are available
}
//TODO: refactor this nonsense
pub fn send_to_workspace(workspace_number: &u64) {
let mon = get_current_monitor();
match mon.id {
0 => workspace::move_to(workspace_number),
_ => {
workspace::move_to(
&format!("{}{}", mon.id, workspace_number)
.parse::<u64>()
.unwrap(),
);
}
}
}
//TODO: refactor this nonsense
pub fn movefocus(workspace_number: &u64) {
let mon = get_current_monitor();
match mon.id {
0 => workspace::move_focus(workspace_number),
_ => {
workspace::move_focus(
&format!("{}{}", mon.id, workspace_number)
.parse::<u64>()
.unwrap(),
);
}
}
}
pub fn get_leftmost_client_for_monitor(mon_id: i16) -> Client {
let clients = client::get();
clients
.into_iter()
.filter(|c| c.monitor == mon_id)
.min_by_key(|c| c.at.0)
.unwrap()
}
pub fn focus_left(aw: Client) {
let mon = monitor::get_by_id(aw.monitor);
let is_leftmost_client = is_leftmost_client(&aw, &mon);
if is_leftmost_monitor(&mon) && is_leftmost_client {
return;
}
if is_leftmost_client {
monitor::focus_left();
return;
}
client::focus_by_direction(Direction::Left);
}
pub fn focus_right(aw: Client) {
let mon = monitor::get_by_id(aw.monitor);
if is_rightmost_monitor(&mon) && is_rightmost_client(&aw, &mon) {
return;
}
if is_rightmost_client(&aw, &mon) {
monitor::focus_right();
return;
}
client::focus_by_direction(Direction::Right);
}
pub fn focus_up(aw: Client) {
let mon = monitor::get_by_id(aw.monitor);
let is_top_client = is_top_client(&aw, &mon);
if is_top_monitor(&mon) && is_top_client {
return;
}
if is_top_client {
monitor::focus_up();
return;
}
client::focus_by_direction(Direction::Up);
}
pub fn focus_down(aw: Client) {
let mon = monitor::get_by_id(aw.monitor);
let is_bottom_client = is_bottom_client(&aw, &mon);
if is_bottom_monitor(&mon) && is_bottom_client {
return;
}
if is_bottom_client {
monitor::focus_down();
return;
}
client::focus_by_direction(Direction::Down);
}
pub fn is_leftmost_client(aw: &Client, mon: &Monitor) -> bool {
let gaps = option::get_gaps();
if (aw.at.0 - gaps) as i32 == mon.x {
return true;
}
false
}
pub fn is_rightmost_client(aw: &Client, mon: &Monitor) -> bool {
let gaps = option::get_gaps();
if mon.real_width() + mon.x as f32 - gaps as f32 == aw.size.0 as f32 + aw.at.0 as f32 {
return true;
}
false
}
pub fn is_top_client(aw: &Client, mon: &Monitor) -> bool {
let gaps = option::get_gaps();
if mon.y + (gaps as i32) + (mon.reserved.1 as i32) == (aw.at.1 as i32) {
return true;
}
false
}
pub fn is_bottom_client(aw: &Client, mon: &Monitor) -> bool {
let gaps = option::get_gaps();
if mon.real_height() + mon.y as f32 - gaps as f32 - mon.reserved.1 as f32
== aw.size.1 as f32 + gaps as f32
{
return true;
}
false
}
pub fn is_rightmost_monitor(mon: &Monitor) -> bool {
let monitors = monitor::get();
let max = monitors.into_iter().max_by_key(|m| m.x).unwrap();
if max.x == mon.x {
return true;
}
false
}
pub fn is_leftmost_monitor(mon: &Monitor) -> bool {
let monitors = monitor::get();
let min = monitors.into_iter().min_by_key(|m| m.x).unwrap();
if min.x == mon.x {
return true;
}
false
}
pub fn is_top_monitor(mon: &Monitor) -> bool {
let monitors = monitor::get();
let min = monitors.into_iter().min_by_key(|m| m.y).unwrap();
if min.y == mon.y {
return true;
}
false
}
pub fn is_bottom_monitor(mon: &Monitor) -> bool {
let monitors = monitor::get();
let max = monitors.into_iter().max_by_key(|m| m.y).unwrap();
if max.y == mon.y {
return true;
}
false
}
fn main() {
let cli = Cli::parse();
match &cli.command {
Commands::Focus { direction } => match direction {
Directions::L => {
let aw = client::get_active();
match aw {
Some(aw) => focus_left(aw),
None => monitor::focus_left(),
};
}
Directions::R => {
let aw = client::get_active();
match aw {
Some(aw) => focus_right(aw),
None => monitor::focus_right(),
};
}
Directions::U => {
let aw = client::get_active();
match aw {
Some(aw) => focus_up(aw),
None => monitor::focus_up(),
};
}
Directions::D => {
let aw = client::get_active();
match aw {
Some(aw) => focus_down(aw),
None => monitor::focus_down(),
};
}
},
Commands::Workspace { workspace_number } => {
select_workspace(workspace_number);
}
Commands::Move { workspace_number } => {
send_to_workspace(workspace_number);
}
Commands::Movefocus { workspace_number } => {
movefocus(workspace_number);
}
Commands::MoveEmpty => {
movefocus(&get_empty_workspace());
},
Commands::FocusEmpty => {
send_to_workspace(&get_empty_workspace());
},
}
}