Compare commits

...

18 Commits

Author SHA1 Message Date
d1e13093c5 Rearrange files 2025-10-30 10:37:05 +00:00
e22fbbd504 Focus new windows 2025-10-29 19:34:49 +00:00
2c253da55d Formatting fixes 2025-10-29 19:32:44 +00:00
de595a0802 Final touches 2025-10-29 18:17:59 +00:00
a21e716475 Add remote commands support 2025-10-29 17:27:23 +00:00
11806119df Make it work 2025-10-26 14:16:56 +00:00
f4af09eb34 Grammar fixes 2025-10-05 20:08:42 +01:00
da0af70b1e Rearrange files. 2025-10-05 20:06:47 +01:00
c6eb56ef29 Minor fixes. 2025-10-05 19:42:13 +01:00
131351c258 Add explaination to .keep file 2025-10-05 19:40:07 +01:00
4d97d1d759 Refractor main ruby script.
Signed-off-by: Syed Daanish <me@syedm.dev>
2025-10-05 19:38:24 +01:00
6da2429c54 Modularize 2025-10-05 19:34:06 +01:00
5ac77254a4 Fix incorrect claim
(just found RubyWM by github.com/vidarh)
2025-10-05 14:45:52 +01:00
7ba16d1452 Remove unnecessary <hr> tag 2025-10-03 12:50:22 +01:00
c4efa9a8cb Update README 2025-10-03 12:49:55 +01:00
d29736265e Update README 2025-10-03 12:49:09 +01:00
f7451f6ee1 Modularize 2025-10-03 12:45:54 +01:00
049d762cbb Add a README 2025-10-03 12:45:43 +01:00
22 changed files with 1236 additions and 552 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
*.so *.so
*.vim *.vim
*p.yml *p.yml
.num.json

26
README.md Normal file
View File

@@ -0,0 +1,26 @@
# KUTU WM
> It is still under development, even though it is techically working.
KUTU is a window manager for X11.
### To know what a window manager is, please check out [this](https://en.wikipedia.org/wiki/Window_manager).
## Philosophy
KUTU means box in turkish, which is my idea behind implementing this WM.
- It is to be used as a bunch of boxes inside which you can throw your windows.
- It is non-reparenting. (So maybe add that flag to your Java Applications)
- It is primarily a tiled WM, but it also supports floating windows.
- This WM is written mostly in `Ruby` and is meant for my personal use only.
- It does use `C` to expose the underlying X11 API.
## Installation & Usage
You need to run `./compile.sh` to build the c library.
<br>
Then add `kutu.rb` to your `~/.xinitrc` file.
## TODO
Add `ruby` like configuration files support.

17
bin/kutu-run.rb Executable file
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

96
bin/kutu.rb Executable file
View File

@@ -0,0 +1,96 @@
#!/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"
# 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
run File.join(__dir__, "../src/shell/startup.sh")
# Add keybinds
load File.join(__dir__, "../src/ruby/bindings.rb")
# 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

1
build/.keep Normal file
View File

@@ -0,0 +1 @@
Keep this folder for generating the shared library file.

View File

@@ -1,23 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
if ! XCB=$(pkg-config --cflags --libs xcb xcb-icccm 2>/dev/null); then
echo "Error: lib-xcb not found. Please install lib-xcb." >&2
exit 1
fi
if ! xrandr --version >/dev/null 2>&1; then
echo "Error: xrandr not found. Please install xrandr." >&2
exit 1
fi
gcc -shared -fPIC -Wall -Wextra -o "$DIR/X-kutu.so" "$DIR/X-kutu.c" $XCB
if [ ! -f "$DIR/X-kutu.so" ]; then
echo "Error: compilation failed." >&2
exit 1
else
echo "Success: $DIR/X-kutu.so is compiled."
fi

447
kutu.rb
View File

@@ -1,447 +0,0 @@
#!/usr/bin/env ruby
require "ffi"
module X
extend FFI::Library
ffi_lib File.join(__dir__, "X-kutu.so")
typedef :uint32, :window_id
typedef :uint32, :xcb_window_t
typedef :uint32, :xcb_pixmap_t
class Geometry < FFI::Struct
layout :x, :int16,
:y, :int16,
:width, :uint16,
:height, :uint16
end
class Event < FFI::Struct
layout :type, :int32,
:window, :window_id,
:override_redirect, :int8,
:btn, :uint32,
:x, :int32,
:y, :int32,
:height, :uint32,
:width, :uint32,
:state, :uint32,
:is_root, :int8
end
class SizeHints < FFI::Struct
layout :flags, :uint32,
:x, :int32,
:y, :int32,
:width, :int32,
:height, :int32,
:min_width, :int32,
:min_height, :int32,
:max_width, :int32,
:max_height, :int32,
:width_inc, :int32,
:height_inc, :int32,
:min_aspect_num, :int32,
:min_aspect_den, :int32,
:max_aspect_num, :int32,
:max_aspect_den, :int32,
:base_width, :int32,
:base_height, :int32,
:win_gravity, :uint32
end
class WMHints < FFI::Struct
layout :flags, :int32,
:input, :uint32,
:initial_state,:int32,
:icon_pixmap, :xcb_pixmap_t,
:icon_window, :xcb_window_t,
:icon_x, :int32,
:icon_y, :int32,
:icon_mask, :xcb_pixmap_t,
:window_group, :xcb_window_t
end
attach_function :deploy, [], :int
attach_function :add_keybind, [:int], :void
attach_function :add_mousebind, [:int], :void
attach_function :cleanup, [], :void
attach_function :focus, [:window_id], :void
attach_function :get_focus, [], :window_id
attach_function :subscribe, [:window_id], :void
attach_function :kill, [:window_id], :void
attach_function :show, [:window_id], :void
attach_function :hide, [:window_id], :void
attach_function :send_to_top, [:window_id], :void
attach_function :free_geometry, [:pointer], :void
attach_function :get_geometry, [:window_id], Geometry.by_value
attach_function :get_pointer, [], Geometry.by_value
attach_function :get_screen, [], Geometry.by_value
attach_function :warp_pointer, [:window_id, :int, :int], :void
attach_function :move_window, [:window_id, :int, :int], :void
attach_function :resize_window, [:window_id, :int, :int], :void
attach_function :wait_for_event, [], Event.by_value
attach_function :get_wm_name, [:window_id], :string
attach_function :get_wm_n_hints, [:window_id], SizeHints.by_value
attach_function :get_wm_hints, [:window_id], WMHints.by_value
attach_function :get_wm_transient_for, [:window_id], :uint8
attach_function :set_wm_state, [:window_id, :int], :void
end
if X.deploy < 0
raise "Failed to deploy X"
end
at_exit { X.cleanup }
$monitors = {}
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
refresh_monitors
$workspaces = {}
$windows = {}
$keybind_actions = {}
$mousebind_actions = {}
def keybind(key, &block)
X.add_keybind key
$keybind_actions[key] = block
end
def mousebind(btn, &block)
X.add_mousebind btn
$mousebind_actions[btn] = block if block
end
class Node
attr_accessor :size
def initialize
@size = 0
end
end
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
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_fixed = @children.map { |c| c.size || 0 }.sum
flex_count = @children.count { |c| c.size.to_i <= 0 }
total_space = horizontal ? @width : @height
remaining_space = total_space - total_fixed
remaining_space = 0 if remaining_space < 0
flex_size = flex_count > 0 ? remaining_space / flex_count : 0
pos = horizontal ? @x : @y
@children.each do |child|
child_size = child.size.to_i > 0 ? child.size.to_i : flex_size
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
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.window_id
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
EVENT_TYPES = {
0 => :unused,
1 => :create,
2 => :closed,
3 => :enter,
4 => :showed,
5 => :show_request,
6 => :mouse_press,
7 => :mouse_drag,
8 => :mouse_release,
9 => :key_press,
10 => :key_release,
11 => :closed,
12 => :configure_request,
13 => :resize_request
}.freeze
$workspaces[:main] = Workspace.new(:main)
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
keybind(27) do |_event|
pp $workspaces[:main].windows
end
mousebind 1
mousebind 3
$mouse_state = -1
$mouse_window = -1
loop do
event = X.wait_for_event
case EVENT_TYPES[event[:type]]
when :create
next unless event[:override_redirect].zero?
X.subscribe event[:window]
X.focus event[:window]
when :closed
pp "Deleting window #{event[:window]}"
$windows[event[:window]]&.delete
$workspaces[:main].tiled_root_block.compute_geometry!
when :enter
X.focus event[:window]
X.send_to_top event[:window]
when :showed
next unless event[:override_redirect].zero?
X.focus event[:window]
X.send_to_top event[:window]
when :show_request
X.show event[:window]
$workspaces[:main].add event[:window] if $windows[event[:window]].nil?
$workspaces[:main].tiled_root_block.compute_geometry!
when :mouse_press
next if event[:is_root] != 0
X.send_to_top event[:window]
X.focus event[:window]
$mouse_state = event[:btn]
$mouse_window = $windows[event[:window]]
if $mouse_window.nil?
$mouse_state = -1
$mouse_window = -1
next
end
$mouse_pos_start = X.get_pointer
$geom_start = X.get_geometry event[:window]
$mousebind_actions[event[:btn]]&.call(event)
when :mouse_drag
screen_bounds = $monitors[:primary]
mouse_pos = X.get_pointer
dx = mouse_pos[:x] - $mouse_pos_start[:x]
dy = mouse_pos[:y] - $mouse_pos_start[:y]
if $mouse_state == 1
new_x = [[$geom_start[:x] + dx, screen_bounds[:x]].max,
screen_bounds[:x] + screen_bounds[:width] - $geom_start[:width]].min
new_y = [[$geom_start[:y] + dy, screen_bounds[:y]].max,
screen_bounds[:y] + screen_bounds[:height] - $geom_start[:height]].min
$mouse_window.move new_x, new_y
elsif $mouse_state == 3
$mouse_window.resize [$geom_start[:width] + dx, 50].max,
[$geom_start[:height] + dy, 50].max
end
when :mouse_release
if [1, 3].include?($mouse_state)
X.focus $mouse_window.window_id
X.send_to_top $mouse_window.window_id
$mouse_state = -1
$mouse_window = -1
$mouse_pos_start = { x: -1, y: -1 }
$geom_start = nil
end
when :key_press
$keybind_actions[event[:btn]]&.call(event)
when :key_release
# TODO
when :configure_request
$windows[event[:window]]&.resize event[:width], event[:height]
X.send_to_top event[:window]
X.focus event[:window]
when :resize_request
$windows[event[:window]]&.resize event[:width], event[:height]
X.send_to_top event[:window]
X.focus event[:window]
end
end

40
setup.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
if ! XCB=$(pkg-config --cflags --libs xcb xcb-icccm 2>/dev/null); then
echo "Error: lib-xcb not found. Please install lib-xcb." >&2
exit 1
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"
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
echo "Error: compilation failed." >&2
exit 1
else
echo "Success: $DIR/build/X-kutu.so is compiled."
fi
for f in "$DIR/bin/"* "$DIR/src/shell/"*; do
chmod +x "$f" || {
echo "Error: Failed to chmod $f" >&2
exit 1
}
done
if ! command -v kutu.rb >/dev/null 2>&1 || ! command -v kutu-run.rb >/dev/null 2>&1; then
echo "Tip: Add $DIR/bin to your PATH to run 'kutu.rb' and 'kutu-run.rb' from anywhere:"
echo "export PATH=\"\$PATH:$DIR/bin\""
fi
echo -e "\e[32mAll done! Build successful.\e[0m"

View File

@@ -1,24 +1,5 @@
#include <xcb/xproto.h> // Self-header
#define CLEANMASK(m) ((m & ~0x80)) #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
@@ -28,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) {
@@ -58,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 ? MOD : 0, 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 ? MOD : 0);
} }
// 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,28 +84,21 @@ 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_kill_client(conn, window);
xcb_flush(conn); // Destroy a window
} void destroy(xcb_window_t win) { xcb_destroy_window(conn, win); }
// 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
@@ -174,7 +129,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
@@ -182,7 +136,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
@@ -190,7 +143,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) {
@@ -235,19 +187,70 @@ 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,
uint32_t color) {
// Get the screen
const xcb_setup_t *setup = xcb_get_setup(conn);
xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup);
xcb_screen_t *screen = iter.data;
// Create an override-redirect window
xcb_window_t win = xcb_generate_id(conn);
uint32_t mask =
XCB_CW_BACK_PIXEL | XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK;
uint32_t values[3];
values[0] = screen->black_pixel; // initial background pixel
values[1] = 1; // override_redirect = true
values[2] = XCB_EVENT_MASK_EXPOSURE; // we want exposure events
xcb_create_window(conn,
XCB_COPY_FROM_PARENT, // depth
win, // window ID
screen->root, // parent
x, y, // x, y
width, height, // width, height
0, // border width
XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, mask,
values);
// Map the window (make it visible)
xcb_map_window(conn, win);
// Fill it with the solid color using a Graphics Context
xcb_gcontext_t gc = xcb_generate_id(conn);
uint32_t gc_values[] = {color, XCB_LINE_STYLE_SOLID};
xcb_create_gc(conn, gc, win, XCB_GC_FOREGROUND | XCB_GC_LINE_STYLE,
gc_values);
xcb_rectangle_t rect = {0, 0, width, height}; // relative to window
xcb_poly_fill_rectangle(conn, win, gc, 1, &rect);
return win;
}
xcb_window_t get_root(void) { return scr->root; }
void grab_pointer(xcb_window_t win) {
xcb_grab_pointer(conn, 0, win,
XCB_EVENT_MASK_BUTTON_RELEASE |
XCB_EVENT_MASK_BUTTON_MOTION |
XCB_EVENT_MASK_POINTER_MOTION_HINT,
XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, XCB_NONE, XCB_NONE,
XCB_CURRENT_TIME);
}
void ungrab_pointer(void) { xcb_ungrab_pointer(conn, XCB_CURRENT_TIME); }
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");
@@ -292,27 +295,18 @@ 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;
// Grab pointer for dragging
xcb_grab_pointer(conn, 0, e->child,
XCB_EVENT_MASK_BUTTON_RELEASE |
XCB_EVENT_MASK_BUTTON_MOTION |
XCB_EVENT_MASK_POINTER_MOTION_HINT,
XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, e->child,
XCB_NONE, XCB_CURRENT_TIME);
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 & MOD) != 0;
} break; } break;
case XCB_MOTION_NOTIFY: { case XCB_MOTION_NOTIFY: {
xcb_motion_notify_event_t *e = (xcb_motion_notify_event_t *)ev; xcb_motion_notify_event_t *e = (xcb_motion_notify_event_t *)ev;
ret.type = 7; ret.type = 7;
ret.window = e->child;
ret.x = e->event_x; ret.x = e->event_x;
ret.y = e->event_y; ret.y = e->event_y;
ret.state = e->state; ret.state = e->state;
@@ -320,8 +314,6 @@ Event wait_for_event(void) {
case XCB_BUTTON_RELEASE: { case XCB_BUTTON_RELEASE: {
xcb_button_release_event_t *e = (xcb_button_release_event_t *)ev; xcb_button_release_event_t *e = (xcb_button_release_event_t *)ev;
// Ungrab pointer after dragging
xcb_ungrab_pointer(conn, XCB_CURRENT_TIME);
ret.type = 8; ret.type = 8;
ret.x = e->event_x; ret.x = e->event_x;
ret.y = e->event_y; ret.y = e->event_y;
@@ -334,7 +326,7 @@ Event wait_for_event(void) {
ret.type = 9; ret.type = 9;
ret.window = e->child; ret.window = e->child;
ret.btn = e->detail; ret.btn = e->detail;
ret.state = e->state; ret.state = (e->state & MOD) != 0;
} break; } break;
case XCB_KEY_RELEASE: { case XCB_KEY_RELEASE: {
@@ -372,7 +364,6 @@ Event wait_for_event(void) {
} break; } break;
} }
xcb_flush(conn);
free(ev); free(ev);
return ret; return ret;
} }

96
src/c/X-kutu.h Normal file
View File

@@ -0,0 +1,96 @@
#ifndef X_KUTU_H
#define X_KUTU_H
// Standard headers
#include <err.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;
// 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);
Geometry 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);
#endif // X_KUTU_H

94
src/ruby/X-kutu.rb Normal file
View File

@@ -0,0 +1,94 @@
require "ffi"
module X
extend FFI::Library
ffi_lib File.join(__dir__, "../../build/X-kutu.so")
typedef :uint32, :xcb_window_t
typedef :uint32, :xcb_pixmap_t
class Geometry < FFI::Struct
layout :x, :int16,
:y, :int16,
:width, :uint16,
:height, :uint16
end
class Event < FFI::Struct
layout :type, :int32,
:window, :xcb_window_t,
:override_redirect, :int8,
:btn, :uint32,
:x, :int32,
:y, :int32,
:height, :uint32,
:width, :uint32,
:state, :uint32,
:is_root, :int8
end
class SizeHints < FFI::Struct
layout :flags, :uint32,
:x, :int32,
:y, :int32,
:width, :int32,
:height, :int32,
:min_width, :int32,
:min_height, :int32,
:max_width, :int32,
:max_height, :int32,
:width_inc, :int32,
:height_inc, :int32,
:min_aspect_num, :int32,
:min_aspect_den, :int32,
:max_aspect_num, :int32,
:max_aspect_den, :int32,
:base_width, :int32,
:base_height, :int32,
:win_gravity, :uint32
end
class WMHints < FFI::Struct
layout :flags, :int32,
:input, :uint32,
:initial_state,:int32,
:icon_pixmap, :xcb_pixmap_t,
:icon_window, :xcb_window_t,
:icon_x, :int32,
:icon_y, :int32,
:icon_mask, :xcb_pixmap_t,
:window_group, :xcb_window_t
end
attach_function :flush, [], :void
attach_function :deploy, [], :int
attach_function :add_keybind, [:int, :int], :void
attach_function :add_mousebind, [:int, :int], :void
attach_function :cleanup, [], :void
attach_function :focus, [:xcb_window_t], :void
attach_function :get_focus, [], :xcb_window_t
attach_function :subscribe, [:xcb_window_t], :void
attach_function :kill, [:xcb_window_t], :void
attach_function :destroy, [:xcb_window_t], :void
attach_function :show, [:xcb_window_t], :void
attach_function :hide, [:xcb_window_t], :void
attach_function :send_to_top, [:xcb_window_t], :void
attach_function :free_geometry, [:pointer], :void
attach_function :get_geometry, [:xcb_window_t], Geometry.by_value
attach_function :get_pointer, [], Geometry.by_value
attach_function :get_screen, [], Geometry.by_value
attach_function :warp_pointer, [: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 :translate_event, [:pointer], Event.by_value
attach_function :next_event, [], :pointer
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_hints, [:xcb_window_t], WMHints.by_value
attach_function :get_wm_transient_for, [:xcb_window_t], :uint8
attach_function :set_wm_state, [:xcb_window_t, :int], :void
attach_function :draw_rectangle, [:int, :int, :int, :int, :uint32], :xcb_window_t
attach_function :grab_pointer, [:xcb_window_t], :void
attach_function :ungrab_pointer, [], :void
attach_function :get_root, [], :xcb_window_t
end

225
src/ruby/bindings.rb Normal file
View File

@@ -0,0 +1,225 @@
keybind 23 do |_event|
run "firefox"
end
keybind 24 do |event|
X.kill event[:window]
end
keybind 25 do |_event|
run "kitty"
end
keybind 26 do |_event|
run File.join(__dir__, "../shell/power.sh")
end
keybind 123, 0 do |_event|
run %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
)
end
keybind 122, 0 do |_event|
run %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
)
end
keybind 121, 0 do |_event|
run %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
)
end
keybind 38 do |_event|
run %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
)
end
keybind 232, 0 do |_event|
run %q(
brightnessctl set 5%-;
pct=$(brightnessctl -m | cut -d, -f4 | tr -d ' %');
dunstify "Brightness" "" -h int:value:$pct -r 998
)
end
keybind 233, 0 do |_event|
run %q(
brightnessctl set 5%+;
pct=$(brightnessctl -m | cut -d, -f4 | tr -d ' %');
dunstify "Brightness" "" -h int:value:$pct -r 998
)
end
keybind 54 do |_event|
run "kitty -e fish -c \"y\""
end
keybind 53 do |_event|
run "kitty -e fish -c \"editor\""
end
keybind 40 do |_event|
run File.join(__dir__, "../shell/run.sh")
end
keybind 33 do |_event|
run "~/.config/polybar/launch.sh"
end
keybind 45 do |_event|
run File.join(__dir__, "../shell/caffiene.sh")
end
keybind 56 do |_event|
monitor = current_monitor
create_workspace monitor
persistence_path = File.join(__dir__, ".num.json")
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
keybind 57 do |_event|
monitor = current_monitor
delete_workspace monitor[:workspaces].length - 1, monitor
persistence_path = File.join(__dir__, ".num.json")
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
keybind 110, 0 do |_event|
run %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
)
end
keybind 110 do |_event|
monitor = current_monitor
persistence_path = File.join(__dir__, ".num.json")
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
keybind 115 do |event|
monitor = current_monitor
persistence_path = File.join(__dir__, ".num.json")
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
keybind 118, 0 do |_event|
run "dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.PlayPause"
end
keybind 117, 0 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
keybind 117 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
keybind 112, 0 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
keybind 112 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
keybind 27 do |_event|
load File.join(__dir__, "./bindings.rb")
monitor = current_monitor
next unless monitor
monitor[:workspaces][monitor[:selected_workspace]].switch_direction
end
keybind 55 do |_event|
run "rofi -modi 'clipboard:greenclip print' -show clipboard -run-command '{cmd}'"
end
keybind 39 do |event|
window = $windows[event[:window]]
window.toggle_floating if window
end
mousebind 1
mousebind 3
mousebind 9, 0 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
mousebind 9 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
mousebind 8, 0 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
mousebind 8 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

29
src/ruby/commands.rb Normal file
View File

@@ -0,0 +1,29 @@
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 "topped"
reply[:topped] = $topped_windows
when "stop"
exit 1
end
reply
end

25
src/ruby/controller.rb Normal file
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

127
src/ruby/events.rb Normal file
View File

@@ -0,0 +1,127 @@
EVENT_TYPES = {
1 => :create,
2 => :closed,
3 => :enter,
4 => :showed,
5 => :show_request,
6 => :mouse_press,
7 => :mouse_drag,
8 => :mouse_release,
9 => :key_press,
10 => :key_release,
11 => :closed_temp,
12 => :configure_request,
13 => :resize_request
}.freeze
def handle_event(event)
case EVENT_TYPES[event[:type]]
when :create
$all_windows << event[:window]
return unless event[:override_redirect].zero?
X.subscribe event[:window]
when :closed
$all_windows.delete event[:window]
$windows[event[:window]]&.delete
X.focus $root
when :enter
X.focus event[:window]
when :show_request
monitor = current_monitor
if $windows[event[:window]].nil?
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
$mousebind_actions[[event[:btn], event[:state]]]&.call(event)
return if event[:is_root] != 0
return if $windows[event[:window]].nil?
return if event[:state] != 1
$mouse_data[:btn] = event[:btn]
$mouse_data[:window] = $windows[event[:window]]
X.grab_pointer $root
$mouse_data[:pointer] = X.get_pointer
if $mouse_data[:window].floating
$mouse_data[:mode] = :floating
$mouse_data[:geometry] = X.get_geometry event[:window]
else
$mouse_data[:mode] = :tiled
end
when :mouse_drag
mouse_pos = X.get_pointer
if $mouse_data[:mode] == :floating
dx = mouse_pos[:x] - $mouse_data[:pointer][:x]
dy = mouse_pos[:y] - $mouse_data[:pointer][:y]
if $mouse_data[:btn] == 1
$mouse_data[:window].move $mouse_data[:geometry][:x] + dx, $mouse_data[:geometry][:y] + dy
elsif $mouse_data[:btn] == 3
$mouse_data[:window]&.resize [$mouse_data[:geometry][:width] + dx, 50].max,
[$mouse_data[:geometry][:height] + dy, 50].max,
true
end
elsif $mouse_data[:mode] == :tiled
if $mouse_data[:btn] == 1
monitor = current_monitor
find_targets = monitor[:workspaces][monitor[:selected_workspace]].insertion_rects
target = drop_target(find_targets, mouse_pos[:x], mouse_pos[:y])
if target
if $rect[:target] != target
X.destroy $rect[:id] if $rect[:id]
$rect[:id] = X.draw_rectangle target[:x], target[:y], target[:width], target[:height], 0x500070
$rect[:target] = target
end
else
X.destroy $rect[:id] if $rect[:id]
$rect = {}
end
elsif $mouse_data[:btn] == 3
# TODO: tile resize . dynamic
end
end
when :mouse_release
if $mouse_data[:mode] == :tiled
if $mouse_data[:btn] == 1
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
X.ungrab_pointer
$mouse_data = {}
when :key_press
$keybind_actions[[event[:btn], event[:state]]]&.call(event)
when :key_release
# TODO
when :configure_request
$windows[event[:window]]&.resize event[:width], event[:height]
when :resize_request
$windows[event[:window]]&.resize event[:width], event[:height]
end
end

71
src/ruby/utils.rb Normal file
View File

@@ -0,0 +1,71 @@
def keybind(key, mod = 1, &block)
X.add_keybind key, mod
$keybind_actions[[key, mod]] = block if block
end
def mousebind(btn, mod = 1, &block)
X.add_mousebind btn, mod
$mousebind_actions[[btn, mod]] = block if block
end
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_path = File.join(__dir__, ".num.json")
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

80
src/ruby/window.rb Normal file
View File

@@ -0,0 +1,80 @@
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
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
@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)
unless transient_for.zero?
@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

160
src/ruby/workspace.rb Normal file
View File

@@ -0,0 +1,160 @@
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
end
end

9
src/shell/caffiene.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
if [[ "$(xset q | awk '/timeout:/ {print $2}')" == "0" ]]; then
xset s on
xset +dpms
else
xset s off
xset -dpms
fi

34
src/shell/power.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
opts="
󰒲 Suspend
 Stop KutuWM
 Shutdown
󰜉 Reboot"
sel=$(printf "%s\n" "$opts" | dmenu -i -p "Select Power Option:" \
-nf '#e0af68' -nb '#1f2335' -sb '#f7768e' -sf '#1a1b26' -fn 'HurmitNerdFont-16')
# user pressed Esc
[ -z "$sel" ] && exit 0
case "$sel" in
*Shutdown*)
confirm=$(printf "No\nYes" | dmenu -i -p "Are you sure you want to shutdown? :" \
-nf '#e0af68' -nb '#1f2335' -sb '#f7768e' -sf '#1a1b26' -fn 'HurmitNerdFont-16')
[ "$confirm" = "Yes" ] && exec shutdown -h now
;;
*Reboot*)
confirm=$(printf "No\nYes" | dmenu -i -p "Are you sure you want to reboot? :" \
-nf '#e0af68' -nb '#1f2335' -sb '#f7768e' -sf '#1a1b26' -fn 'HurmitNerdFont-16')
[ "$confirm" = "Yes" ] && exec reboot
;;
*Stop\ KutuWM*)
exec kutu-run.rb stop
;;
*Suspend*)
~/dotfiles/scripts/lock.sh &
exec systemctl suspend
;;
esac

17
src/shell/run.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
choice=$(
(
printf "%s\n" godot mvox mox aseprite terraria
compgen -c
) |
grep -v -E '^(if|fi|case|esac|for|done|while|until|select|function|return|continue|break|time|exec|source|alias|builtin|read|export|unset|local|set|declare|typeset|:|\.|\[|coproc|l|ll|ls|then|else|elif|do|in|\{|\}|!|\[\[|\]\]|_.*|compgen)$' |
sort |
dmenu -i -p "Enter command  " \
-nf '#4abaaf' -nb '#1f2335' -sb '#7aa2f7' -sf '#102030' -fn 'HurmitNerdFont-16'
)
[ -z "$choice" ] && exit 0
fish -c "$choice" >/dev/null 2>&1 &
disown

15
src/shell/startup.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
picom --config ~/.config/i3/picom.conf &
greenclip daemon &
# xss-lock --transfer-sleep-lock -- ~/dotfiles/scripts/lock.sh &
dunst -config ~/.config/dunst/dunstrc &
bluetoothctl power off
magick -size 1920x1080 xc:#000000 /tmp/f_bg.png
feh --bg-fill /tmp/f_bg.png
xsetroot -cursor_name left_ptr
xrdb ~/.Xresources
setxkbmap us