Compare commits

...

22 Commits

Author SHA1 Message Date
syedm 4b8599132f Fix Import 2026-07-07 23:03:34 +01:00
syedm 1cae22ad2c Fix incorrect path 2026-07-07 22:25:19 +01:00
syedm a859bab33d Fix imports 2026-07-07 22:21:04 +01:00
syedm 220f8cd810 Fix typos 2026-07-07 22:13:03 +01:00
syedm a0bec00988 Update bindings api 2026-07-07 22:00:30 +01:00
syedm 7fb1ea8cb3 Make persistence path a proper global 2026-07-07 14:18:33 +01:00
syedm 0d2f52347d Fix persistence loading issue 2026-07-07 14:12:48 +01:00
syedm 3eb15736f3 Fix persistence and update deprecated dependencies 2026-07-07 13:58:25 +01:00
syedm 7460d38653 T 2026-07-07 13:25:42 +01:00
syedm 248e5a2c05 Update gitignore and minor fixes 2026-07-07 12:24:44 +01:00
syedm 10e0de5b05 Add package.nix and cleanup build process 2026-07-07 12:09:06 +01:00
syedm 2930d71889 Add flake.nix 2026-07-07 11:15:27 +01:00
syedm 49ba7ddb41 Improvements 2026-07-02 12:43:02 +01:00
syedm d1e13093c5 Rearrange files 2025-10-30 10:37:05 +00:00
syedm e22fbbd504 Focus new windows 2025-10-29 19:34:49 +00:00
syedm 2c253da55d Formatting fixes 2025-10-29 19:32:44 +00:00
syedm de595a0802 Final touches 2025-10-29 18:17:59 +00:00
syedm a21e716475 Add remote commands support 2025-10-29 17:27:23 +00:00
syedm 11806119df Make it work 2025-10-26 14:16:56 +00:00
syedm f4af09eb34 Grammar fixes 2025-10-05 20:08:42 +01:00
syedm da0af70b1e Rearrange files. 2025-10-05 20:06:47 +01:00
syedm c6eb56ef29 Minor fixes. 2025-10-05 19:42:13 +01:00
27 changed files with 1117 additions and 482 deletions
+2
View File
@@ -1,3 +1,5 @@
*.so *.so
*.vim *.vim
*p.yml *p.yml
.num.json
result
+2 -2
View File
@@ -17,10 +17,10 @@ KUTU means box in turkish, which is my idea behind implementing this WM.
## Installation & Usage ## Installation & Usage
You need to run `./compile.sh` to built the c library. You need to run `./compile.sh` to build the c library.
<br> <br>
Then add `kutu.rb` to your `~/.xinitrc` file. Then add `kutu.rb` to your `~/.xinitrc` file.
## TODO ## TODO
Add `ruby` like configuration files support. Add better `ruby` configuration files support.
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env ruby
require "socket"
SOCK_PATH = "/tmp/kutu.sock"
cli_addr_path = "/tmp/kutu_client#{Process.pid}.sock"
File.delete cli_addr_path if File.exist? cli_addr_path
client = Socket.new :UNIX, :DGRAM
client.bind Socket.pack_sockaddr_un(cli_addr_path)
client.send ARGV.join(" "), 0, Socket.pack_sockaddr_un(SOCK_PATH)
data, = client.recvfrom 1024
puts data
Executable
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env ruby
# Require X-kutu library for all x11 functions
require_relative "../src/ruby/X-kutu"
# Require dependencies
require 'json'
require 'socket'
# Initialize X
if X.deploy >= 0
puts "Started kutu WM for X11 successfully! :)"
else
raise "Failed to deploy X, try running using xinit or startx."
end
# Require submodules
require_relative "../src/ruby/utils"
require_relative "../src/ruby/controller"
require_relative "../src/ruby/window"
require_relative "../src/ruby/workspace"
require_relative "../src/ruby/events"
require_relative "../src/ruby/commands"
require_relative "../src/ruby/factory"
require_relative "../src/ruby/config"
# Cleanup on exit
at_exit do
X.cleanup
puts "Exited kutu WM, sadly :("
end
# Globals
$monitors = {}
$windows = {}
$all_windows = []
$keybind_actions = {}
$mousebind_actions = {}
$mouse_data = {}
$root = X.get_root
$rect = {}
# Initialize monitors
load_monitors!
# Run startup script
load_config!
# Setup unix socket
SOCK_PATH = "/tmp/kutu.sock"
File.delete SOCK_PATH if File.exist? SOCK_PATH
$socket = Socket.new :UNIX, :DGRAM
$socket.bind Socket.pack_sockaddr_un(SOCK_PATH)
# Main loop
loop do
sleep 0.001
if IO.select([$socket], nil, nil, 0)
command, sender = $socket.recvfrom 1024
reply = handle_command command
$socket.send JSON.generate(reply), 0, sender
end
event_pointer = X.next_event
handle_event X.translate_event(event_pointer) if !event_pointer.null?
X.flush
end
Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1783148766,
"narHash": "sha256-uslt2pqShTIXDdAHRHv2QkYLsVdY8Oqwz0EA48/RSM8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a50de1b7d8a586adc18d2395c19de7d6058e6030",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-26.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+33
View File
@@ -0,0 +1,33 @@
{
description = "kutu wm environment";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
};
in {
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
(ruby.withPackages (ps: [
ps.ffi
]))
gcc
pkg-config
libxcb
xcbutilwm
xrandr
xprop
];
shellHook = ''
echo "kutu dev shell"
'';
};
packages.${system}.default = pkgs.callPackage ./package.nix {};
};
}
-67
View File
@@ -1,67 +0,0 @@
#!/usr/bin/env ruby
# Require X-kutu library for all x11 functions
require_relative "./src/X-kutu"
# Initialize X
if X.deploy >= 0
puts "Started kutu WM for X11 successfully!"
else
raise "Failed to deploy X"
end
# Require modules
require_relative "./src/utils"
require_relative "./src/node"
require_relative "./src/workspace"
require_relative "./src/events"
# Cleanup on exit
at_exit do
X.cleanup
puts "Exited kutu WM, sadly :("
end
# Globals
$monitors = {}
$workspaces = {}
$windows = {}
$keybind_actions = {}
$mousebind_actions = {}
$mouse_data = {}
$root = X.get_root
$rect = {}
# Initialize monitors
refresh_monitors!
# Add keybinds
load "./src/bindings.rb"
# Initialize workspaces
$workspaces[:main] = Workspace.new(:main)
# Main loop
loop do
event = X.wait_for_event
handle_event event
end
+63
View File
@@ -0,0 +1,63 @@
{ stdenv
, ruby
, makeWrapper
, pkg-config
, libxcb
, xcbutilwm
, xrandr
, xprop
, rubyPackages
, gcc
, lib
}:
stdenv.mkDerivation {
pname = "kutu";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [
pkg-config
makeWrapper
gcc
];
buildInputs = [
(ruby.withPackages (ps: [
ps.ffi
]))
libxcb
xcbutilwm
xrandr
xprop
];
buildPhase = ''
mkdir -p build
gcc -shared -fPIC \
-Wall -Wextra \
-o build/X-kutu.so \
src/c/X-kutu.c \
$(pkg-config --libs --cflags xcb xcb-icccm);
'';
installPhase = ''
mkdir -p $out/bin
mkdir -p $out/build
mkdir -p $out/src
cp bin/kutu.rb $out/bin/kutu.rb
cp bin/kutu-run.rb $out/bin/kutu-run.rb
cp build/X-kutu.so $out/build/
cp -r src/. $out/src/
wrapProgram $out/bin/kutu.rb \
--prefix PATH : ${lib.makeBinPath [
xrandr
xprop
]}
'';
}
+12 -6
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
DIR="$(cd -- "$(dirname -- "$0")" && pwd)" DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
@@ -8,14 +9,10 @@ if ! XCB=$(pkg-config --cflags --libs xcb xcb-icccm 2>/dev/null); then
exit 1 exit 1
fi fi
if ! xrandr --version >/dev/null 2>&1; then
echo "Error: xrandr not found. Please install xrandr." >&2
exit 1
fi
mkdir -p "$DIR/build" mkdir -p "$DIR/build"
gcc -shared -fPIC -Wall -Wextra -o "$DIR/build/X-kutu.so" "$DIR/X-kutu.c" $XCB # shellcheck disable=SC2086
gcc -shared -fPIC -Wall -Wextra -o "$DIR/build/X-kutu.so" "$DIR/src/c/X-kutu.c" $XCB
if [ ! -f "$DIR/build/X-kutu.so" ]; then if [ ! -f "$DIR/build/X-kutu.so" ]; then
echo "Error: compilation failed." >&2 echo "Error: compilation failed." >&2
@@ -23,3 +20,12 @@ if [ ! -f "$DIR/build/X-kutu.so" ]; then
else else
echo "Success: $DIR/build/X-kutu.so is compiled." echo "Success: $DIR/build/X-kutu.so is compiled."
fi fi
for f in "$DIR/bin/"* "$DIR/src/shell/"*; do
chmod +x "$f" || {
echo "Error: Failed to chmod $f" >&2
exit 1
}
done
echo -e "\e[32mAll done! Build successful.\e[0m"
-18
View File
@@ -1,18 +0,0 @@
keybind(24) do |event|
X.kill event[:window]
end
keybind(25) do |_event|
pid = spawn("kitty")
Process.detach pid
end
keybind(26) do |_event|
$workspaces.each_value(&:close_all)
exit 1
end
mousebind 1
mousebind 3
+48 -82
View File
@@ -1,23 +1,5 @@
#define CLEANMASK(m) ((m & ~0x80)) // Self-header
#include "X-kutu.h"
// Definitions for modifier keys
#define SUPER XCB_MOD_MASK_4
#define ALT XCB_MOD_MASK_1
#define CTRL XCB_MOD_MASK_CONTROL
#define SHIFT XCB_MOD_MASK_SHIFT
// Change this to change the modifier key you want to use
#define MOD SUPER
// Standard headers
#include <err.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// XCB header
#include <xcb/xcb.h>
#include <xcb/xcb_icccm.h>
// Global variables // Global variables
// Connection to X server // Connection to X server
@@ -27,27 +9,8 @@ xcb_screen_t *scr;
// Currently focused window // Currently focused window
xcb_window_t focuswin; xcb_window_t focuswin;
// Geometry structure for rectangles and positions // Flush
typedef struct Geometry { void flush(void) { xcb_flush(conn); }
int16_t x;
int16_t y;
uint16_t width;
uint16_t height;
} Geometry;
// Event structure to represent various X events
typedef struct Event {
int32_t type;
uint32_t window;
int8_t override_redirect;
uint32_t btn;
int32_t x;
int32_t y;
uint32_t height;
uint32_t width;
uint32_t state;
int8_t is_root;
} Event;
// Cleanup function to close the X connection on exit // Cleanup function to close the X connection on exit
void cleanup(void) { void cleanup(void) {
@@ -57,18 +20,18 @@ void cleanup(void) {
// Keybind function to setup a key grab if the keycode is clicked along with the // Keybind function to setup a key grab if the keycode is clicked along with the
// MOD key // MOD key
void add_keybind(int key) { void add_keybind(int key, int mod) {
xcb_grab_key(conn, 0, scr->root, MOD, key, XCB_GRAB_MODE_ASYNC, xcb_grab_key(conn, 0, scr->root, mod, key, XCB_GRAB_MODE_ASYNC,
XCB_GRAB_MODE_ASYNC); XCB_GRAB_MODE_ASYNC);
} }
// Mousebind function to setup a mouse button grab if the button is clicked // Mousebind function to setup a mouse button grab if the button is clicked
// along with the MOD key // along with the MOD key
void add_mousebind(int button) { void add_mousebind(int button, int mod) {
xcb_grab_button(conn, 0, scr->root, xcb_grab_button(conn, 0, scr->root,
XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE, XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE,
XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, scr->root, XCB_NONE, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, scr->root, XCB_NONE,
button, MOD); button, mod);
} }
// Deploy function to initialize the X connection, set up event masks, and // Deploy function to initialize the X connection, set up event masks, and
@@ -122,33 +85,51 @@ void subscribe(xcb_window_t win) {
// Kill a window // Kill a window
void kill(xcb_window_t window) { void kill(xcb_window_t window) {
xcb_kill_client(conn, window); xcb_intern_atom_cookie_t wm_protocols_cookie =
xcb_intern_atom(conn, 1, 12, "WM_PROTOCOLS");
xcb_intern_atom_cookie_t wm_delete_window_cookie =
xcb_intern_atom(conn, 0, 16, "WM_DELETE_WINDOW");
xcb_intern_atom_reply_t *wm_protocols =
xcb_intern_atom_reply(conn, wm_protocols_cookie, NULL);
xcb_intern_atom_reply_t *wm_delete_window =
xcb_intern_atom_reply(conn, wm_delete_window_cookie, NULL);
if (!wm_protocols || !wm_delete_window) {
// fallback to force kill
xcb_kill_client(conn, window);
xcb_flush(conn);
return;
}
xcb_client_message_event_t ev = {0};
ev.response_type = XCB_CLIENT_MESSAGE;
ev.window = window;
ev.type = wm_protocols->atom;
ev.format = 32;
ev.data.data32[0] = wm_delete_window->atom;
ev.data.data32[1] = XCB_CURRENT_TIME;
xcb_send_event(conn, 0, window, XCB_EVENT_MASK_NO_EVENT, (char *)&ev);
xcb_flush(conn); xcb_flush(conn);
free(wm_protocols);
free(wm_delete_window);
} }
// Destroy a window // Destroy a window
void destroy(xcb_window_t win) { void destroy(xcb_window_t win) { xcb_destroy_window(conn, win); }
xcb_destroy_window(conn, win);
xcb_flush(conn);
}
// Show a window // Show a window
void show(xcb_window_t window) { void show(xcb_window_t window) { xcb_map_window(conn, window); }
xcb_map_window(conn, window);
xcb_flush(conn);
}
// Hide a window // Hide a window
void hide(xcb_window_t window) { void hide(xcb_window_t window) { xcb_unmap_window(conn, window); }
xcb_unmap_window(conn, window);
xcb_flush(conn);
}
// Bring a window to the top of the stack // Bring a window to the top of the stack
void send_to_top(xcb_window_t win) { void send_to_top(xcb_window_t win) {
uint32_t values[1] = {XCB_STACK_MODE_ABOVE}; uint32_t values[1] = {XCB_STACK_MODE_ABOVE};
xcb_configure_window(conn, win, XCB_CONFIG_WINDOW_STACK_MODE, values); xcb_configure_window(conn, win, XCB_CONFIG_WINDOW_STACK_MODE, values);
xcb_flush(conn);
} }
// Get the geometry of a window // Get the geometry of a window
@@ -161,12 +142,12 @@ Geometry get_geometry(xcb_window_t win) {
} }
// Get the current pointer position // Get the current pointer position
Geometry get_pointer(void) { PointerInfo get_pointer(void) {
xcb_query_pointer_reply_t *geom; xcb_query_pointer_reply_t *geom;
geom = xcb_query_pointer_reply(conn, xcb_query_pointer(conn, scr->root), 0); geom = xcb_query_pointer_reply(conn, xcb_query_pointer(conn, scr->root), 0);
Geometry g = {geom->root_x, geom->root_y, 0, 0}; PointerInfo p = {geom->root_x, geom->root_y, geom->child};
free(geom); free(geom);
return g; return p;
} }
// Get the screen geometry // Get the screen geometry
@@ -179,7 +160,6 @@ Geometry get_screen(void) {
// the center) // the center)
void warp_pointer(xcb_window_t win, int x, int y) { void warp_pointer(xcb_window_t win, int x, int y) {
xcb_warp_pointer(conn, XCB_NONE, win, 0, 0, 0, 0, x, y); xcb_warp_pointer(conn, XCB_NONE, win, 0, 0, 0, 0, x, y);
xcb_flush(conn);
} }
// Move a window to a specific position // Move a window to a specific position
@@ -187,7 +167,6 @@ void move_window(xcb_window_t win, int x, int y) {
uint32_t values[2] = {x, y}; uint32_t values[2] = {x, y};
xcb_configure_window(conn, win, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, xcb_configure_window(conn, win, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y,
values); values);
xcb_flush(conn);
} }
// Resize a window to specific dimensions // Resize a window to specific dimensions
@@ -195,7 +174,6 @@ void resize_window(xcb_window_t win, int width, int height) {
uint32_t values[2] = {width, height}; uint32_t values[2] = {width, height};
xcb_configure_window( xcb_configure_window(
conn, win, XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, values); conn, win, XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, values);
xcb_flush(conn);
} }
xcb_size_hints_t get_wm_n_hints(xcb_window_t win) { xcb_size_hints_t get_wm_n_hints(xcb_window_t win) {
@@ -240,8 +218,6 @@ void set_wm_state(xcb_window_t win, int state) {
xcb_change_property(conn, XCB_PROP_MODE_REPLACE, win, WM_STATE, WM_STATE, 32, xcb_change_property(conn, XCB_PROP_MODE_REPLACE, win, WM_STATE, WM_STATE, 32,
2, data); 2, data);
xcb_flush(conn);
} }
xcb_window_t draw_rectangle(int x, int y, int width, int height, xcb_window_t draw_rectangle(int x, int y, int width, int height,
@@ -282,9 +258,6 @@ xcb_window_t draw_rectangle(int x, int y, int width, int height,
xcb_rectangle_t rect = {0, 0, width, height}; // relative to window xcb_rectangle_t rect = {0, 0, width, height}; // relative to window
xcb_poly_fill_rectangle(conn, win, gc, 1, &rect); xcb_poly_fill_rectangle(conn, win, gc, 1, &rect);
// Flush the connection to send commands to the server
xcb_flush(conn);
return win; return win;
} }
@@ -297,23 +270,18 @@ void grab_pointer(xcb_window_t win) {
XCB_EVENT_MASK_POINTER_MOTION_HINT, XCB_EVENT_MASK_POINTER_MOTION_HINT,
XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, XCB_NONE, XCB_NONE, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, XCB_NONE, XCB_NONE,
XCB_CURRENT_TIME); XCB_CURRENT_TIME);
xcb_flush(conn);
} }
void ungrab_pointer(void) { void ungrab_pointer(void) { xcb_ungrab_pointer(conn, XCB_CURRENT_TIME); }
xcb_ungrab_pointer(conn, XCB_CURRENT_TIME);
xcb_flush(conn); xcb_generic_event_t *next_event(void) { return xcb_poll_for_event(conn); }
}
// Wait for an event and return it as an Event structure // Wait for an event and return it as an Event structure
// This function is blocking // This function is blocking
// The event is sent by value, so no need to free anything // The event is sent by value, so no need to free anything
Event wait_for_event(void) { Event translate_event(xcb_generic_event_t *ev) {
Event ret = {0}; Event ret = {0};
xcb_generic_event_t *ev;
ev = xcb_wait_for_event(conn);
if (!ev) if (!ev)
errx(1, "xcb connection broken"); errx(1, "xcb connection broken");
@@ -358,14 +326,13 @@ Event wait_for_event(void) {
case XCB_BUTTON_PRESS: { case XCB_BUTTON_PRESS: {
xcb_button_press_event_t *e = (xcb_button_press_event_t *)ev; xcb_button_press_event_t *e = (xcb_button_press_event_t *)ev;
if (!e->child)
break;
ret.type = 6; ret.type = 6;
ret.window = e->child; ret.window = e->child;
ret.is_root = e->child == scr->root; ret.is_root = e->child == scr->root;
ret.x = e->event_x; ret.x = e->event_x;
ret.y = e->event_y; ret.y = e->event_y;
ret.btn = e->detail; ret.btn = e->detail;
ret.state = e->state;
} break; } break;
case XCB_MOTION_NOTIFY: { case XCB_MOTION_NOTIFY: {
@@ -428,7 +395,6 @@ Event wait_for_event(void) {
} break; } break;
} }
xcb_flush(conn);
free(ev); free(ev);
return ret; return ret;
} }
+107
View File
@@ -0,0 +1,107 @@
#ifndef X_KUTU_H
#define X_KUTU_H
// Standard headers
#include <err.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// XCB headers
#include <xcb/xcb.h>
#include <xcb/xcb_icccm.h>
// Macro to clean modifier masks
#define CLEANMASK(m) ((m & ~0x80))
// Definitions for modifier keys
#define SUPER XCB_MOD_MASK_4
#define ALT XCB_MOD_MASK_1
#define CTRL XCB_MOD_MASK_CONTROL
#define SHIFT XCB_MOD_MASK_SHIFT
// Change this to change the modifier key you want to use
#define MOD SUPER
// Forward declarations of global variables
extern xcb_connection_t *conn;
extern xcb_screen_t *scr;
extern xcb_window_t focuswin;
// Geometry structure for rectangles and positions
typedef struct Geometry {
int16_t x;
int16_t y;
uint16_t width;
uint16_t height;
} Geometry;
typedef struct PointerInfo {
int16_t x;
int16_t y;
uint32_t window;
} PointerInfo;
// Event structure to represent various X events
typedef struct Event {
int32_t type;
uint32_t window;
int8_t override_redirect;
uint32_t btn;
int32_t x;
int32_t y;
uint32_t height;
uint32_t width;
uint32_t state;
int8_t is_root;
} Event;
// Function prototypes
void flush(void);
int deploy(void);
void cleanup(void);
void add_keybind(int key, int mod);
void add_mousebind(int button, int mod);
xcb_window_t get_focus(void);
xcb_window_t get_root(void);
void focus(xcb_window_t win);
void subscribe(xcb_window_t win);
void kill(xcb_window_t window);
void destroy(xcb_window_t win);
void show(xcb_window_t window);
void hide(xcb_window_t window);
void send_to_top(xcb_window_t win);
Geometry get_geometry(xcb_window_t win);
PointerInfo get_pointer(void);
Geometry get_screen(void);
void free_geometry(Geometry *g);
void warp_pointer(xcb_window_t win, int x, int y);
void move_window(xcb_window_t win, int x, int y);
void resize_window(xcb_window_t win, int width, int height);
xcb_size_hints_t get_wm_n_hints(xcb_window_t win);
xcb_icccm_wm_hints_t get_wm_hints(xcb_window_t win);
char *get_wm_name(xcb_window_t win);
uint8_t get_wm_transient_for(xcb_window_t win);
void set_wm_state(xcb_window_t win, int state);
xcb_window_t draw_rectangle(int x, int y, int width, int height,
uint32_t color);
void grab_pointer(xcb_window_t win);
void ungrab_pointer(void);
xcb_generic_event_t *next_event(void);
Event translate_event(xcb_generic_event_t *ev);
extern inline int mod() { return MOD; }
extern inline int clk() { return XCB_MOD_MASK_LOCK; }
extern inline int num() { return XCB_MOD_MASK_2; }
#endif // X_KUTU_H
-13
View File
@@ -1,13 +0,0 @@
class Node
attr_accessor :size
def initialize
@size = 0
end
end
# Require submodules
require_relative "./window"
require_relative "./window_block"
+16 -5
View File
@@ -2,7 +2,7 @@ require "ffi"
module X module X
extend FFI::Library extend FFI::Library
ffi_lib File.join(__dir__, "../build/X-kutu.so") ffi_lib File.join(__dir__, "../../build/X-kutu.so")
typedef :uint32, :xcb_window_t typedef :uint32, :xcb_window_t
typedef :uint32, :xcb_pixmap_t typedef :uint32, :xcb_pixmap_t
@@ -14,6 +14,12 @@ module X
:height, :uint16 :height, :uint16
end end
class PointerInfo < FFI::Struct
layout :x, :int16,
:y, :int16,
:window, :xcb_window_t
end
class Event < FFI::Struct class Event < FFI::Struct
layout :type, :int32, layout :type, :int32,
:window, :xcb_window_t, :window, :xcb_window_t,
@@ -60,9 +66,10 @@ module X
:window_group, :xcb_window_t :window_group, :xcb_window_t
end end
attach_function :flush, [], :void
attach_function :deploy, [], :int attach_function :deploy, [], :int
attach_function :add_keybind, [:int], :void attach_function :add_keybind, [:int, :int], :void
attach_function :add_mousebind, [:int], :void attach_function :add_mousebind, [:int, :int], :void
attach_function :cleanup, [], :void attach_function :cleanup, [], :void
attach_function :focus, [:xcb_window_t], :void attach_function :focus, [:xcb_window_t], :void
attach_function :get_focus, [], :xcb_window_t attach_function :get_focus, [], :xcb_window_t
@@ -74,12 +81,13 @@ module X
attach_function :send_to_top, [:xcb_window_t], :void attach_function :send_to_top, [:xcb_window_t], :void
attach_function :free_geometry, [:pointer], :void attach_function :free_geometry, [:pointer], :void
attach_function :get_geometry, [:xcb_window_t], Geometry.by_value attach_function :get_geometry, [:xcb_window_t], Geometry.by_value
attach_function :get_pointer, [], Geometry.by_value attach_function :get_pointer, [], PointerInfo.by_value
attach_function :get_screen, [], Geometry.by_value attach_function :get_screen, [], Geometry.by_value
attach_function :warp_pointer, [:xcb_window_t, :int, :int], :void attach_function :warp_pointer, [:xcb_window_t, :int, :int], :void
attach_function :move_window, [:xcb_window_t, :int, :int], :void attach_function :move_window, [:xcb_window_t, :int, :int], :void
attach_function :resize_window, [:xcb_window_t, :int, :int], :void attach_function :resize_window, [:xcb_window_t, :int, :int], :void
attach_function :wait_for_event, [], Event.by_value attach_function :translate_event, [:pointer], Event.by_value
attach_function :next_event, [], :pointer
attach_function :get_wm_name, [:xcb_window_t], :string attach_function :get_wm_name, [:xcb_window_t], :string
attach_function :get_wm_n_hints, [:xcb_window_t], SizeHints.by_value attach_function :get_wm_n_hints, [:xcb_window_t], SizeHints.by_value
attach_function :get_wm_hints, [:xcb_window_t], WMHints.by_value attach_function :get_wm_hints, [:xcb_window_t], WMHints.by_value
@@ -89,4 +97,7 @@ module X
attach_function :grab_pointer, [:xcb_window_t], :void attach_function :grab_pointer, [:xcb_window_t], :void
attach_function :ungrab_pointer, [], :void attach_function :ungrab_pointer, [], :void
attach_function :get_root, [], :xcb_window_t attach_function :get_root, [], :xcb_window_t
attach_function :mod, [], :int
attach_function :clk, [], :int
attach_function :num, [], :int
end end
+27
View File
@@ -0,0 +1,27 @@
def handle_command(command)
reply = {}
command_parts = command.split(" ")
command = command_parts[0]
args = command_parts[1..-1]
case command
when "get-ws"
monitor = args[0] ? $monitors[args[0].to_sym] : current_monitor
return reply if monitor.nil?
reply[:workspace] = monitor[:selected_workspace]
reply[:monitor] = $monitors.key(monitor)
reply[:count] = monitor[:workspaces].length
when "set-ws"
monitor = args[1] ? $monitors[args[1].to_sym] : current_monitor
return reply if monitor.nil?
select_workspace args[0].to_i, monitor
reply[:workspace] = monitor[:selected_workspace]
reply[:monitor] = $monitors.key(monitor)
reply[:count] = monitor[:workspaces].length
when "stop"
exit 1
end
reply
end
+24
View File
@@ -0,0 +1,24 @@
def config_path
File.join(
ENV["XDG_CONFIG_HOME"] || File.expand_path("~/.config"),
"kutu",
"config.rb"
)
end
def load_config!
path = config_path
unless File.exist?(path)
load "../src/ruby/defaults.rb"
return
end
puts "loading kutu config: #{path}"
load path
rescue Exception => e
warn "failed to load kutu config"
warn "#{e.class}: #{e.message}"
warn e.backtrace.join("\n")
end
+25
View File
@@ -0,0 +1,25 @@
def create_workspace(monitor)
monitor[:workspaces] << Workspace.new($monitors.key(monitor))
monitor[:selected_workspace] = monitor[:workspaces].length - 1
end
def delete_workspace(n, monitor)
return if monitor[:workspaces].length <= 2
monitor[:workspaces][n].windows.each { |w| monitor[:workspaces][(n - 1) % monitor[:workspaces].length].drop 0, w }
monitor[:workspaces].delete_at n
if monitor[:selected_workspace] >= n
monitor[:selected_workspace] -= 1
end
end
def select_workspace(n, monitor)
if n >= monitor[:workspaces].length
select_workspace monitor[:workspaces].length - 1, monitor
elsif n < 0
select_workspace 1, monitor
end
monitor[:workspaces].each { |w| w.hide if w != monitor[:workspaces][n] }
monitor[:selected_workspace] = n
monitor[:workspaces][n].show
monitor[:workspaces][n].compute_tiled!
end
+89
View File
@@ -0,0 +1,89 @@
bind_key 23, true, "firefox"
bind_key 24, true, :kill_window
bind_key 25, true, "kitty"
bind_key 26, true, "bash /home/me/dotfiles/scripts/power.sh"
bind_key 123, false, %q(
pactl set-sink-volume @DEFAULT_SINK@ +5%;
vol=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]\+%' | head -n1);
dunstify "Volume Adjusted" "" -h int:value:$vol -r 997
)
bind_key 122, false, %q(
pactl set-sink-volume @DEFAULT_SINK@ -5%;
vol=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]\+%' | head -n1);
dunstify "Volume Adjusted" "" -h int:value:$vol -r 997
)
bind_key 121, false, %q(
pactl set-sink-mute @DEFAULT_SINK@ toggle;
vol=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]\+%' | head -n1);
dunstify "Volume Adjusted" "" -h int:value:$vol -r 997
)
bind_key 38, true, %Q(
maim -c 0.3,0.5,1.0,0.8 -s | tee /tmp/screenshot_temp.png | xclip -selection clipboard -t image/png;
if [ -s '/tmp/screenshot_temp.png' ]; then
mv /tmp/screenshot_temp.png ~/screenshots/$(date +%Y-%m-%d_%H:%M:%S).png;
fi
)
bind_key 232, false, %q(
brightnessctl set 5%-;
pct=$(brightnessctl -m | cut -d, -f4 | tr -d ' %');
dunstify "Brightness" "" -h int:value:$pct -r 998
)
bind_key 233, false, %q(
brightnessctl set 5%+;
pct=$(brightnessctl -m | cut -d, -f4 | tr -d ' %');
dunstify "Brightness" "" -h int:value:$pct -r 998
)
bind_key 54, true, 'kitty -e fish -c "y"'
bind_key 53, true, 'kitty -e fish -c "editor"'
bind_key 41, true, 'kitty -e fish -c "btop"'
bind_key 40, true, "bash /home/me/dotfiles/scripts/run.sh"
bind_key 56, true, :add_workspace
bind_key 57, true, :delete_workspace
bind_key 110, false, %q(
dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify \
/org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Pause;
dbus-send --print-reply --dest=$(busctl --user list | grep -oP 'org.mpris.MediaPlayer2.firefox.instance_1_\d+') \
/org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.PlayPause
)
bind_key 173, true,
"dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Previous"
bind_key 171, true,
"dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Next"
bind_key 89, true, :toggle_scratchpad
bind_key 81, true, :toggle_move_scratchpad
bind_key 118, false,
"dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.PlayPause"
bind_key 86, false, :workspace_next
bind_key 86, true, :move_window_next_workspace
bind_key 82, false, :workspace_previous
bind_key 82, true, :move_window_previous_workspace
bind_key 10, true, "setxkbmap us"
bind_key 11, true, "setxkbmap ara"
bind_key 55, true, "CM_LAUNCHER=rofi clipmenu"
bind_key 39, true, :toggle_floating
bind_mouse 9, false, :workspace_next
bind_mouse 9, true, :move_window_next_workspace
bind_mouse 8, false, :workspace_previous
bind_mouse 8, true, :move_window_previous_workspace
+39 -31
View File
@@ -9,50 +9,58 @@ EVENT_TYPES = {
8 => :mouse_release, 8 => :mouse_release,
9 => :key_press, 9 => :key_press,
10 => :key_release, 10 => :key_release,
11 => :closed, 11 => :closed_temp,
12 => :configure_request, 12 => :configure_request,
13 => :resize_request 13 => :resize_request
}.freeze }.freeze
def handle_event(event) def handle_event(event)
case EVENT_TYPES[event[:type]] case EVENT_TYPES[event[:type]]
when :create when :create
$all_windows << event[:window]
return unless event[:override_redirect].zero? return unless event[:override_redirect].zero?
X.subscribe event[:window] X.subscribe event[:window]
when :closed when :closed
$all_windows.delete event[:window]
$windows[event[:window]]&.delete $windows[event[:window]]&.delete
$workspaces[:main].tiled_root_block.compute_geometry! X.focus $root
X.flush
mouse_pos = X.get_pointer
X.focus mouse_pos[:window] if mouse_pos[:window] != 0
when :closed_temp
X.focus $root
X.flush
mouse_pos = X.get_pointer
X.focus mouse_pos[:window] if mouse_pos[:window] != 0
when :enter when :enter
X.send_to_top event[:window]
floating_windows = $windows.select { |_i, w| w.floating }
floating_windows.each { |_i, w| X.send_to_top w.window_id }
X.focus event[:window]
when :showed
return unless event[:override_redirect].zero?
X.send_to_top event[:window]
floating_windows = $windows.select { |_i, w| w.floating }
floating_windows.each { |_i, w| X.send_to_top w.window_id }
X.focus event[:window] X.focus event[:window]
when :show_request when :show_request
$workspaces[:main].add event[:window] if $windows[event[:window]].nil? monitor = current_monitor
$workspaces[:main].tiled_root_block.compute_geometry! if $windows[event[:window]].nil?
X.show event[:window] monitor[:workspaces][monitor[:selected_workspace]].create event[:window]
X.show event[:window]
$all_windows.each do |window|
above = `xprop -id #{window} _NET_WM_STATE`
X.send_to_top window if above.include?("ABOVE")
end
end
X.focus event[:window]
when :mouse_press when :mouse_press
$mousebind_actions[[event[:btn], event[:state]]]&.call(event)
return if event[:is_root] != 0 return if event[:is_root] != 0
return if $windows[event[:window]].nil? return if $windows[event[:window]].nil?
return if event[:state] != 1
$mouse_data[:btn] = event[:btn] $mouse_data[:btn] = event[:btn]
$mouse_data[:window] = $windows[event[:window]] $mouse_data[:window] = $windows[event[:window]]
X.grab_pointer $root X.grab_pointer $root
@@ -63,7 +71,6 @@ def handle_event(event)
else else
$mouse_data[:mode] = :tiled $mouse_data[:mode] = :tiled
end end
$mousebind_actions[event[:btn]]&.call(event)
when :mouse_drag when :mouse_drag
mouse_pos = X.get_pointer mouse_pos = X.get_pointer
@@ -73,13 +80,14 @@ def handle_event(event)
if $mouse_data[:btn] == 1 if $mouse_data[:btn] == 1
$mouse_data[:window].move $mouse_data[:geometry][:x] + dx, $mouse_data[:geometry][:y] + dy $mouse_data[:window].move $mouse_data[:geometry][:x] + dx, $mouse_data[:geometry][:y] + dy
elsif $mouse_data[:btn] == 3 elsif $mouse_data[:btn] == 3
X.resize_window $mouse_data[:window].window_id, $mouse_data[:window]&.resize [$mouse_data[:geometry][:width] + dx, 50].max,
[$mouse_data[:geometry][:width] + dx, 50].max, [$mouse_data[:geometry][:height] + dy, 50].max,
[$mouse_data[:geometry][:height] + dy, 50].max true
end end
elsif $mouse_data[:mode] == :tiled elsif $mouse_data[:mode] == :tiled
if $mouse_data[:btn] == 1 if $mouse_data[:btn] == 1
find_targets = compute_drop_targets! $mouse_data[:window] monitor = current_monitor
find_targets = monitor[:workspaces][monitor[:selected_workspace]].insertion_rects
target = drop_target(find_targets, mouse_pos[:x], mouse_pos[:y]) target = drop_target(find_targets, mouse_pos[:x], mouse_pos[:y])
if target if target
if $rect[:target] != target if $rect[:target] != target
@@ -98,17 +106,21 @@ def handle_event(event)
when :mouse_release when :mouse_release
if $mouse_data[:mode] == :tiled if $mouse_data[:mode] == :tiled
X.destroy $rect[:id] if $rect[:id] if $mouse_data[:btn] == 1
$rect = {} if $rect[:id]
X.destroy $rect[:id]
monitor = current_monitor
monitor[:workspaces][monitor[:selected_workspace]].drop $rect[:target][:drop], $mouse_data[:window]
end
$rect = {}
end
end end
X.ungrab_pointer X.ungrab_pointer
X.send_to_top $mouse_data[:window].window_id if $mouse_data[:window]
X.focus $mouse_data[:window].window_id if $mouse_data[:window]
$mouse_data = {} $mouse_data = {}
when :key_press when :key_press
$keybind_actions[event[:btn]]&.call(event) $keybind_actions[[event[:btn], event[:state]]]&.call(event)
when :key_release when :key_release
# TODO # TODO
@@ -116,12 +128,8 @@ def handle_event(event)
when :configure_request when :configure_request
$windows[event[:window]]&.resize event[:width], event[:height] $windows[event[:window]]&.resize event[:width], event[:height]
X.send_to_top event[:window]
X.focus event[:window]
when :resize_request when :resize_request
$windows[event[:window]]&.resize event[:width], event[:height] $windows[event[:window]]&.resize event[:width], event[:height]
X.send_to_top event[:window]
X.focus event[:window]
end end
end end
+179
View File
@@ -0,0 +1,179 @@
PERSISTENCE_PATH = File.join(
ENV["XDG_STATE_HOME"] || File.expand_path("~/.local/state"),
"kutu",
"num.json"
)
ACTIONS = {}
def action(name, &block)
ACTIONS[name] = block
end
action :kill_window do |event|
X.kill event[:window]
end
action :toggle_floating do |event|
window = $windows[event[:window]]
window.toggle_floating if window
end
action :workspace_next do |_event|
monitor = current_monitor
next if monitor[:selected_workspace].zero?
next_ws =
(monitor[:selected_workspace] %
(monitor[:workspaces].length - 1)) + 1
select_workspace next_ws, monitor
end
action :workspace_previous do |_event|
monitor = current_monitor
next if monitor[:selected_workspace].zero?
next_ws =
((monitor[:selected_workspace] - 2) %
(monitor[:workspaces].length - 1)) + 1
select_workspace next_ws, monitor
end
action :move_window_next_workspace do |event|
monitor = current_monitor
next if monitor[:selected_workspace].zero?
ws =
monitor[:workspaces][
(monitor[:selected_workspace] %
(monitor[:workspaces].length - 1)) + 1
]
ws.drop ws.tiled_windows.length,
$windows[event[:window]] if $windows[event[:window]]
end
action :move_window_previous_workspace do |event|
monitor = current_monitor
next if monitor[:selected_workspace].zero?
ws =
monitor[:workspaces][
((monitor[:selected_workspace] - 2) % (monitor[:workspaces].length - 1)) + 1
]
ws.drop ws.tiled_windows.length,
$windows[event[:window]] if $windows[event[:window]]
end
action :toggle_scratchpad do |_event|
monitor = current_monitor
persistence = File.exist?(PERSISTENCE_PATH) ?
JSON.parse(File.read(PERSISTENCE_PATH), symbolize_names: true) :
{}
if monitor[:selected_workspace] == 0
select_workspace persistence[$monitors.key(monitor)]&.[](:saved) || 1, monitor
else
persistence[$monitors.key(monitor)] ||= {}
persistence[$monitors.key(monitor)][:saved] = monitor[:selected_workspace]
File.write(PERSISTENCE_PATH, JSON.pretty_generate(persistence))
select_workspace 0, monitor
end
end
action :toggle_move_scratchpad do |event|
monitor = current_monitor
persistence = File.exist?(PERSISTENCE_PATH) ?
JSON.parse(File.read(PERSISTENCE_PATH), symbolize_names: true) :
{}
ws = monitor[:workspaces][
monitor[:selected_workspace] == 0 ?
monitor[:workspaces][persistence[$monitors.key(monitor)]&.[](:saved) || 1] : 0
]
ws.drop ws.tiled_windows.length, $windows[event[:window]] if $windows[event[:window]]
end
action :add_workspace do |_event|
monitor = current_monitor
create_workspace monitor
persistence = File.exist?(PERSISTENCE_PATH) ?
JSON.parse(File.read(PERSISTENCE_PATH), symbolize_names: true) :
{}
persistence[$monitors.key(monitor)] ||= {}
persistence[$monitors.key(monitor)][:length] = monitor[:workspaces].length
File.write(PERSISTENCE_PATH, JSON.pretty_generate(persistence))
end
action :delete_workspace do |_event|
monitor = current_monitor
delete_workspace monitor[:workspaces].length - 1, monitor
persistence = File.exist?(PERSISTENCE_PATH) ?
JSON.parse(File.read(PERSISTENCE_PATH), symbolize_names: true) :
{}
persistence[$monitors.key(monitor)] ||= {}
persistence[$monitors.key(monitor)][:length] = monitor[:workspaces].length
File.write(PERSISTENCE_PATH, JSON.pretty_generate(persistence))
end
PRESETS = {
normal: [0, X.clk, X.num | X.clk, X.num],
mod: [
X.mod,
X.mod | X.clk,
X.num | X.clk | X.mod,
X.num | X.mod
]
}
def resolve_action(action, &block)
action ||= block
case action
when Symbol
ACTIONS.fetch(action) do
raise "Unknown kutu action: #{action}"
end
when String
proc do |_event|
run action
end
when Proc
action
else
raise ArgumentError,
"Expected action Symbol, String, or Block, got #{action.class}"
end
end
PRESETS[:mod].each do |modifier|
X.add_mousebind 1, modifier
end
PRESETS[:mod].each do |modifier|
X.add_mousebind 3, modifier
end
def bind_key(code, mod, action = nil, &block)
callback = resolve_action(action, &block)
PRESETS[mod ? :mod : :normal].each do |modifier|
X.add_keybind code, modifier
$keybind_actions[[code, modifier]] = callback
end
end
def bind_mouse(button, mod, action = nil, &block)
callback = resolve_action(action, &block)
PRESETS[mod ? :mod : :normal].each do |modifier|
X.add_mousebind button, modifier
$mousebind_actions[[button, modifier]] = callback
end
end
+65
View File
@@ -0,0 +1,65 @@
PERSISTENCE_PATH = File.join(
ENV["XDG_STATE_HOME"] || File.expand_path("~/.local/state"),
"kutu",
"num.json"
)
def run(cmd)
pid = spawn "bash", "-c", cmd
Process.detach pid
end
def load_monitors!
$monitors.clear
xrandr_output = `xrandr --query`
connected = xrandr_output.each_line.select { |line| line.include?(" connected") }
connected.sort_by! { |line| line.include?(" primary") ? 0 : 1 }
persistence = File.exist?(PERSISTENCE_PATH) ?
JSON.parse(File.read(PERSISTENCE_PATH), symbolize_names: true) :
{}
connected.first(2).each_with_index do |line, index|
next unless line =~ /(\d+)x(\d+)\+(\d+)\+(\d+)/
w, h, x, y = $1.to_i, $2.to_i, $3.to_i, $4.to_i
key = index.zero? ? :primary : :secondary
$monitors[key] = {
x: x, y: y, width: w, height: h,
workspaces: Array.new(persistence[key]&.[](:length) || 2) { Workspace.new(key) },
selected_workspace: 1
}
end
end
def current_monitor(pointer = X.get_pointer)
$monitors.find do |_, r|
pointer[:x] >= r[:x] &&
pointer[:x] < r[:x] + r[:width] &&
pointer[:y] >= r[:y] &&
pointer[:y] < r[:y] + r[:height]
end&.last
end
def fixed_size_or_aspect?(hints)
return false unless hints
flags = hints[:flags] || 0
same_size = (flags & (16 | 256) != 0) && (flags & 32 != 0) &&
hints[:max_width] == (hints[:min_width] || hints[:base_width]) &&
hints[:max_height] == (hints[:min_height] || hints[:base_height])
fixed_aspect = (flags & 128 != 0) &&
hints[:min_aspect_num] == hints[:max_aspect_num] &&
hints[:min_aspect_den] == hints[:max_aspect_den] &&
hints[:min_aspect_num] != 0
same_size || fixed_aspect
end
def drop_target(targets, cx, cy)
targets.find do |t|
cx >= t[:x] &&
cy >= t[:y] &&
cx < t[:x] + t[:width] &&
cy < t[:y] + t[:height]
end
end
+90
View File
@@ -0,0 +1,90 @@
def godot_window?(window)
`xprop -id #{window} WM_CLASS`.downcase.include?("godot")
end
class Window
attr_accessor :window_id, :floating, :workspace, :def_floating, :size, :x, :y, :width, :height
def initialize(window_id, workspace)
@size = 0
@workspace = workspace
@window_id = window_id
geometry = X.get_geometry(window_id)
@x = geometry[:x]
@y = geometry[:y]
@width = geometry[:width]
@height = geometry[:height]
wm_n_hints = X.get_wm_n_hints(window_id)
@floating = godot_window?(window_id)
@def_floating = @floating
if wm_n_hints
if fixed_size_or_aspect?(wm_n_hints)
@floating = true
@def_floating = true
@width = wm_n_hints[:max_width]
if wm_n_hints[:flags] & 128 != 0
@height = wm_n_hints[:max_width] * (wm_n_hints[:min_aspect_num] / wm_n_hints[:min_aspect_den])
else
@height = wm_n_hints[:max_height]
end
@x = $monitors[@workspace.monitor_id][:width] / 2 - @width / 2
@y = $monitors[@workspace.monitor_id][:height] / 2 - @height / 2
apply_geometry!
end
end
X.set_wm_state window_id, 1
transient_for = X.get_wm_transient_for(window_id)
if !transient_for.zero? && !godot_window?(window_id)
@floating = true
@def_floating = true
@width = wm_n_hints[:max_width]
if wm_n_hints[:flags] & 128 != 0
@height = wm_n_hints[:max_width] * (wm_n_hints[:min_aspect_num] / wm_n_hints[:min_aspect_den])
else
@height = wm_n_hints[:max_height]
end
@x = $monitors[@workspace.monitor_id][:width] / 2 - @width / 2
@y = $monitors[@workspace.monitor_id][:height] / 2 - @height / 2
apply_geometry!
end
super()
end
def delete
@workspace.remove self
end
def each_leaf(&block)
block.call(self)
end
def toggle_floating
if !@def_floating
@floating = !@floating
@workspace.check_floating self
apply_geometry!
end
end
def move(x, y)
return unless @floating
@x, @y = x, y
apply_geometry!
end
def resize(width, height, force = false)
return unless @floating
@width, @height = width, height
if !force
@x = $monitors[@workspace.monitor_id][:width] / 2 - @width / 2
@y = $monitors[@workspace.monitor_id][:height] / 2 - @height / 2
end
apply_geometry!
end
def apply_geometry!
X.send_to_top @window_id if @floating
X.move_window @window_id, @x, @y
X.resize_window @window_id, @width, @height
end
end
+161
View File
@@ -0,0 +1,161 @@
class Workspace
attr_reader :monitor_id
attr_accessor :tiled_windows, :untiled_windows
def initialize(monitor_id = :primary, direction = :horizontal)
@monitor_id = monitor_id
@direction = direction
@tiled_windows = []
@untiled_windows = []
end
def drop(idx_dst, window)
idx_src = window.workspace.tiled_windows.index(window)
window.workspace.tiled_windows.delete_at idx_src
self.tiled_windows.insert idx_dst, window
window.workspace.compute_tiled! if window.workspace != self
window.workspace = self
compute_tiled!
select_workspace $monitors[@monitor_id][:selected_workspace], $monitors[@monitor_id]
end
def insertion_rects
rects = []
return rects if @tiled_windows.empty?
compute_tiled!
horizontal = (@direction == :horizontal)
z = 100
half = z / 2
wins = @tiled_windows
first = wins.first
if horizontal
rects << { x: first.x, y: first.y, width: z, height: first.height, drop: 0 }
else
rects << { x: first.x, y: first.y, width: first.width, height: z, drop: 0 }
end
wins.each_cons(2).with_index do |(a, b), i|
if horizontal
mid = (a.x + a.width + b.x) / 2.0
rects << {
x: mid - half,
y: a.y,
width: z,
height: a.height,
drop: i
}
else
mid = (a.y + a.height + b.y) / 2.0
rects << {
x: a.x,
y: mid - half,
width: a.width,
height: z,
drop: i
}
end
end
last = wins.last
if horizontal
rects << { x: last.x + last.width - z, y: last.y, width: z, height: last.height, drop: wins.count - 1 }
else
rects << { x: last.x, y: last.y + last.height - z, width: z, height: last.width, drop: wins.count - 1 }
end
rects
end
def windows
@tiled_windows + @untiled_windows
end
def check_floating(window)
if window.floating
@untiled_windows << window
@tiled_windows.delete window
else
@tiled_windows << window
@untiled_windows.delete window
end
compute_tiled!
end
def switch_direction
@direction = @direction == :horizontal ? :vertical : :horizontal
windows.each { |window| window.size = 0 }
compute_tiled!
end
def create(window_id)
window = Window.new(window_id, self)
$windows[window_id] = window
if window.floating
@untiled_windows << window
else
@tiled_windows << window
compute_tiled!
end
end
def remove(window)
@untiled_windows.delete window
@tiled_windows.delete window
$windows.delete window.window_id
compute_tiled!
end
def compute_tiled!
return if @tiled_windows.empty?
monitor = $monitors[@monitor_id]
horizontal = @direction == :horizontal
abs_total = @tiled_windows.sum { |c| c.size.to_i }
flex_count = @tiled_windows.count { |c| c.size.to_i <= 0 }
total_space = horizontal ? monitor[:width] : monitor[:height]
remaining = total_space - abs_total
remaining = 0 if remaining < 0
flex_each = flex_count > 0 ? (remaining.to_f / flex_count) : 0
pos = horizontal ? monitor[:x] : monitor[:y]
@tiled_windows.each do |window|
px = window.size.to_i > 0 ? window.size.to_i : flex_each
if horizontal
window.x = pos
window.y = monitor[:y]
window.width = px
window.height = monitor[:height]
else
window.x = monitor[:x]
window.y = pos
window.width = monitor[:width]
window.height = px
end
window.apply_geometry!
pos += px
end
end
def close_all
self.windows.each { |window| remove window }
end
def hide
self.windows.each do |window|
X.hide window.window_id
X.set_wm_state window.window_id, 3
end
X.focus $root
end
def show
self.windows.each do |window|
X.show window.window_id
X.set_wm_state window.window_id, 1
end
X.focus $root
end
end
-81
View File
@@ -1,81 +0,0 @@
def keybind(key, &block)
X.add_keybind key
$keybind_actions[key] = block if block
end
def mousebind(btn, &block)
X.add_mousebind btn
$mousebind_actions[btn] = block if block
end
def refresh_monitors!
$monitors.clear
xrandr_output = `xrandr --query`
connected = xrandr_output.each_line.select { |line| line.include?(" connected") }
connected.sort_by! { |line| line.include?(" primary") ? 0 : 1 }
connected.first(2).each_with_index do |line, index|
next unless line =~ /(\d+)x(\d+)\+(\d+)\+(\d+)/
w, h, x, y = $1.to_i, $2.to_i, $3.to_i, $4.to_i
key = index.zero? ? :primary : :secondary
$monitors[key] = { x: x, y: y, width: w, height: h }
end
end
def fixed_size_or_aspect?(hints)
return false unless hints
flags = hints[:flags] || 0
same_size = (flags & (16 | 256) != 0) && (flags & 32 != 0) &&
hints[:max_width] == (hints[:min_width] || hints[:base_width]) &&
hints[:max_height] == (hints[:min_height] || hints[:base_height])
fixed_aspect = (flags & 128 != 0) &&
hints[:min_aspect_num] == hints[:max_aspect_num] &&
hints[:min_aspect_den] == hints[:max_aspect_den] &&
hints[:min_aspect_num] != 0
same_size || fixed_aspect
end
def compute_drop_targets!(window)
targets = []
$monitors.each_value do |monitor|
# if monitor[:windows].nil? || monitor[:windows].empty?
# targets << { type: :monitor_full, x: monitor[:x], y: monitor[:y], width: monitor[:width], height: monitor[:height], monitor: monitor }
# else
margin = 60
targets << { type: :monitor_top, x: monitor[:x], y: monitor[:y], width: monitor[:width], height: margin, monitor: monitor }
targets << { type: :monitor_bottom, x: monitor[:x], y: monitor[:y] + monitor[:height] - margin, width: monitor[:width], height: margin, monitor: monitor }
targets << { type: :monitor_left, x: monitor[:x], y: monitor[:y], width: margin, height: monitor[:height], monitor: monitor }
targets << { type: :monitor_right, x: monitor[:x] + monitor[:width] - margin, y: monitor[:y], width: margin, height: monitor[:height], monitor: monitor }
# end
end
$windows.each_value do |w|
next if w.floating
next if w == window
margin_x = (w.width / 4).round
margin_y = (w.height / 4).round
targets << { type: :window_top, x: w.x, y: w.y, width: w.width, height: margin_y, window: w }
targets << { type: :window_bottom, x: w.x, y: w.y + w.height - margin_y, width: w.width, height: margin_y, window: w }
targets << { type: :window_left, x: w.x, y: w.y, width: margin_x, height: w.height, window: w }
targets << { type: :window_right, x: w.x + w.width - margin_x, y: w.y, width: margin_x, height: w.height, window: w }
end
targets
end
def drop_target(targets, cx, cy)
targets.find do |t|
cx >= t[:x] &&
cy >= t[:y] &&
cx < t[:x] + t[:width] &&
cy < t[:y] + t[:height]
end
end
-61
View File
@@ -1,61 +0,0 @@
class Window < Node
attr_accessor :window_id, :x, :y, :width, :height, :state, :floating, :workspace
def initialize(window_id, workspace)
@workspace = workspace
@window_id = window_id
@name = X.get_wm_name(window_id)
@wm_n_hints = X.get_wm_n_hints(window_id)
@floating = false
if @wm_n_hints
if fixed_size_or_aspect?(@wm_n_hints)
@floating = true
@width = @wm_n_hints[:max_width]
if @wm_n_hints[:flags] & 128 != 0
@height = @wm_n_hints[:max_width] * (@wm_n_hints[:min_aspect_num] / @wm_n_hints[:min_aspect_den])
else
@height = @wm_n_hints[:max_height]
end
@x = @workspace.width / 2 - @width / 2
@y = @workspace.height / 2 - @height / 2
apply_geometry!
end
end
@wm_hints = X.get_wm_hints(window_id)
if @wm_hints
if @wm_hints[:flags] & 1 != 0 && @wm_hints[:initial_state] == 3
X.set_wm_state window_id, 1
end
end
@transient_for = X.get_wm_transient_for(window_id)
@floating = true unless @transient_for.zero?
super()
end
def delete
@workspace.remove self
end
def each_leaf(&block)
block.call(self)
end
def move(x, y)
return unless @floating
@x, @y = x, y
apply_geometry!
end
def resize(width, height)
return unless @floating
@x = @workspace.width / 2 - width / 2
@y = @workspace.height / 2 - height / 2
@width, @height = width, height
apply_geometry!
end
def apply_geometry!
X.move_window @window_id, @x, @y
X.resize_window @window_id, @width, @height
end
end
-64
View File
@@ -1,64 +0,0 @@
class WindowBlock < Node
attr_accessor :children, :direction, :size, :x, :y, :width, :height
def initialize(direction = :horizontal)
@children = []
@direction = direction
super()
end
def add_node(node)
@children << node
end
def each_leaf(&block)
@children.each { |child| child.each_leaf(&block) }
end
def each_node(&block)
block.call(self)
@children.each { |child| child.each_node(&block) if child.is_a? WindowBlock }
end
def compute_geometry!
return if @children.empty?
horizontal = @direction == :horizontal
total_percent = @children.map { |c| c.size.to_i > 0 ? c.size.to_i : 0 }.sum
flex_count = @children.count { |c| c.size.to_i <= 0 }
total_space = horizontal ? @width : @height
total_percent = [total_percent, 100].min
remaining_percent = 100 - total_percent
flex_percent = flex_count > 0 ? remaining_percent / flex_count : 0
pos = horizontal ? @x : @y
@children.each do |child|
percent = child.size.to_i > 0 ? child.size.to_i : flex_percent
child_size = (total_space * percent) / 100.0
if horizontal
child.x = pos
child.y = @y
child.width = child_size
child.height = @height
else
child.x = @x
child.y = pos
child.width = @width
child.height = child_size
end
child.compute_geometry! if child.is_a?(WindowBlock)
child.apply_geometry! if child.is_a?(Window)
pos += child_size
end
end
end
class RootWindowBlock < WindowBlock
def initialize(workspace)
super(:horizontal)
@x = workspace.x
@y = workspace.y
@width = workspace.width
@height = workspace.height
end
end
-52
View File
@@ -1,52 +0,0 @@
class Workspace
attr_reader :name, :monitor, :x, :y, :width, :height, :tiled_root_block
def initialize(name, monitor = :primary)
@monitor = monitor
@x = $monitors[monitor][:x]
@y = $monitors[monitor][:y]
@width = $monitors[monitor][:width]
@height = $monitors[monitor][:height]
@tiled_root_block = RootWindowBlock.new(self)
@untiled_windows = []
@name = name
end
def windows
windows = []
@untiled_windows.each { |w| windows << w }
@tiled_root_block.each_leaf { |w| windows << w }
windows
end
def add(window_id)
window = Window.new(window_id, self)
if window.floating
@untiled_windows << window
else
@tiled_root_block.add_node window
end
$windows[window_id] = window
end
def remove(window)
@untiled_windows.delete window
@tiled_root_block.each_node { |node| node.children.delete window }
$windows.delete window.window_id
end
def close_all
self.windows.each do |window|
X.kill window.window_id
remove window
end
end
def hide
self.windows.each { |window| X.hide window.window_id }
end
def show
self.windows.each { |window| X.show window.window_id }
end
end