Add better icccm support

This commit is contained in:
2025-10-02 15:22:43 +01:00
parent 2eed7c1dad
commit 09eb87e1b7
4 changed files with 260 additions and 74 deletions

111
X-kutu.c
View File

@@ -1,3 +1,4 @@
#include <xcb/xproto.h>
#define CLEANMASK(m) ((m & ~0x80)) #define CLEANMASK(m) ((m & ~0x80))
// Definitions for modifier keys // Definitions for modifier keys
@@ -12,10 +13,12 @@
// Standard headers // Standard headers
#include <err.h> #include <err.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include <unistd.h> #include <unistd.h>
// XCB header // XCB header
#include <xcb/xcb.h> #include <xcb/xcb.h>
#include <xcb/xcb_icccm.h>
// Global variables // Global variables
// Connection to X server // Connection to X server
@@ -72,7 +75,6 @@ void add_mousebind(int button) {
// Deploy function to initialize the X connection, set up event masks, and // Deploy function to initialize the X connection, set up event masks, and
// prepare the window manager // prepare the window manager
int deploy(void) { int deploy(void) {
uint32_t values[2];
int mask; int mask;
// Start X connection and return -1 if it fails // Start X connection and return -1 if it fails
@@ -85,7 +87,12 @@ int deploy(void) {
// Setuup event masks // Setuup event masks
mask = XCB_CW_EVENT_MASK; mask = XCB_CW_EVENT_MASK;
values[0] = XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY;
uint32_t values[] = {XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT |
XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY |
XCB_EVENT_MASK_STRUCTURE_NOTIFY |
XCB_EVENT_MASK_PROPERTY_CHANGE};
xcb_change_window_attributes_checked(conn, scr->root, mask, values); xcb_change_window_attributes_checked(conn, scr->root, mask, values);
xcb_flush(conn); xcb_flush(conn);
@@ -186,6 +193,52 @@ void resize_window(xcb_window_t win, int width, int height) {
xcb_flush(conn); xcb_flush(conn);
} }
xcb_size_hints_t get_wm_n_hints(xcb_window_t win) {
xcb_size_hints_t hints;
xcb_get_property_cookie_t cookie = xcb_icccm_get_wm_normal_hints(conn, win);
xcb_icccm_get_wm_normal_hints_reply(conn, cookie, &hints, NULL);
return hints;
}
xcb_icccm_wm_hints_t get_wm_hints(xcb_window_t win) {
xcb_get_property_cookie_t cookie = xcb_icccm_get_wm_hints(conn, win);
xcb_icccm_wm_hints_t hints;
xcb_icccm_get_wm_hints_reply(conn, cookie, &hints, NULL);
return hints;
}
char *get_wm_name(xcb_window_t win) {
xcb_get_property_cookie_t cookie = xcb_icccm_get_wm_name(conn, win);
xcb_icccm_get_text_property_reply_t reply;
xcb_icccm_get_wm_name_reply(conn, cookie, &reply, NULL);
return reply.name;
}
uint8_t get_wm_transient_for(xcb_window_t win) {
xcb_get_property_cookie_t cookie = xcb_icccm_get_wm_transient_for(conn, win);
return xcb_icccm_get_wm_transient_for_reply(conn, cookie, &win, NULL);
}
void set_wm_state(xcb_window_t win, int state) {
xcb_intern_atom_cookie_t cookie =
xcb_intern_atom(conn, 0, strlen("WM_STATE"), "WM_STATE");
xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(conn, cookie, NULL);
if (!reply)
return;
xcb_atom_t WM_STATE = reply->atom;
free(reply);
uint32_t data[2];
data[0] = state;
data[1] = XCB_WINDOW_NONE;
xcb_change_property(conn, XCB_PROP_MODE_REPLACE, win, WM_STATE, WM_STATE, 32,
2, data);
xcb_flush(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
@@ -225,7 +278,7 @@ Event wait_for_event(void) {
case XCB_MAP_NOTIFY: { case XCB_MAP_NOTIFY: {
xcb_map_notify_event_t *e; xcb_map_notify_event_t *e;
e = (xcb_map_notify_event_t *)ev; e = (xcb_map_notify_event_t *)ev;
ret.type = -1; ret.type = 4;
ret.window = e->window; ret.window = e->window;
ret.override_redirect = e->override_redirect; ret.override_redirect = e->override_redirect;
} break; } break;
@@ -233,9 +286,8 @@ Event wait_for_event(void) {
case XCB_MAP_REQUEST: { case XCB_MAP_REQUEST: {
xcb_map_request_event_t *e; xcb_map_request_event_t *e;
e = (xcb_map_request_event_t *)ev; e = (xcb_map_request_event_t *)ev;
ret.type = 4; ret.type = 5;
ret.window = e->window; ret.window = e->window;
ret.override_redirect = 1;
} break; } break;
case XCB_BUTTON_PRESS: { case XCB_BUTTON_PRESS: {
@@ -249,7 +301,7 @@ Event wait_for_event(void) {
XCB_EVENT_MASK_POINTER_MOTION_HINT, XCB_EVENT_MASK_POINTER_MOTION_HINT,
XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, e->child, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, e->child,
XCB_NONE, XCB_CURRENT_TIME); XCB_NONE, XCB_CURRENT_TIME);
ret.type = 5; 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;
@@ -259,7 +311,7 @@ Event wait_for_event(void) {
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 = 6; ret.type = 7;
ret.window = e->child; ret.window = e->child;
ret.x = e->event_x; ret.x = e->event_x;
ret.y = e->event_y; ret.y = e->event_y;
@@ -270,7 +322,7 @@ Event wait_for_event(void) {
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 // Ungrab pointer after dragging
xcb_ungrab_pointer(conn, XCB_CURRENT_TIME); xcb_ungrab_pointer(conn, XCB_CURRENT_TIME);
ret.type = 7; ret.type = 8;
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;
@@ -279,7 +331,7 @@ Event wait_for_event(void) {
case XCB_KEY_PRESS: { case XCB_KEY_PRESS: {
xcb_key_press_event_t *e; xcb_key_press_event_t *e;
e = (xcb_key_press_event_t *)ev; e = (xcb_key_press_event_t *)ev;
ret.type = 8; 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;
@@ -288,22 +340,49 @@ Event wait_for_event(void) {
case XCB_KEY_RELEASE: { case XCB_KEY_RELEASE: {
xcb_key_release_event_t *e; xcb_key_release_event_t *e;
e = (xcb_key_release_event_t *)ev; e = (xcb_key_release_event_t *)ev;
ret.type = 9; ret.type = 10;
ret.window = e->child; ret.window = e->child;
ret.btn = e->detail; ret.btn = e->detail;
ret.state = e->state; ret.state = e->state;
} break; } break;
case XCB_CONFIGURE_NOTIFY: { case XCB_UNMAP_NOTIFY: {
xcb_configure_notify_event_t *e; xcb_unmap_notify_event_t *e;
e = (xcb_configure_notify_event_t *)ev; e = (xcb_unmap_notify_event_t *)ev;
ret.type = 10; ret.type = 11;
ret.window = e->window;
} break;
case XCB_CONFIGURE_REQUEST: {
xcb_configure_request_event_t *e = (xcb_configure_request_event_t *)ev;
ret.type = 12;
ret.window = e->window;
ret.x = e->x;
ret.y = e->y;
ret.width = e->width;
ret.height = e->height;
// ret.stack_mode =
// e->value_mask & XCB_CONFIG_WINDOW_STACK_MODE ? e->stack_mode : 0;
} break;
case XCB_RESIZE_REQUEST: {
xcb_resize_request_event_t *e = (xcb_resize_request_event_t *)ev;
ret.type = 13;
ret.window = e->window; ret.window = e->window;
ret.width = e->width; ret.width = e->width;
ret.height = e->height; ret.height = e->height;
ret.x = e->x;
ret.y = e->y;
} break; } break;
// case XCB_CONFIGURE_NOTIFY: {
// xcb_configure_notify_event_t *e;
// e = (xcb_configure_notify_event_t *)ev;
// ret.type = 12;
// ret.window = e->window;
// ret.width = e->width;
// ret.height = e->height;
// ret.x = e->x;
// ret.y = e->y;
// } break;
} }
xcb_flush(conn); xcb_flush(conn);

View File

@@ -3,7 +3,7 @@ set -euo pipefail
DIR="$(cd -- "$(dirname -- "$0")" && pwd)" DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
if ! XCB=$(pkg-config --cflags --libs xcb 2>/dev/null); then if ! XCB=$(pkg-config --cflags --libs xcb xcb-icccm 2>/dev/null); then
echo "Error: lib-xcb not found. Please install lib-xcb." >&2 echo "Error: lib-xcb not found. Please install lib-xcb." >&2
exit 1 exit 1
fi fi

195
kutu.rb
View File

@@ -7,6 +7,8 @@ module X
ffi_lib File.join(__dir__, "X-kutu.so") ffi_lib File.join(__dir__, "X-kutu.so")
typedef :uint32, :window_id typedef :uint32, :window_id
typedef :uint32, :xcb_window_t
typedef :uint32, :xcb_pixmap_t
class Geometry < FFI::Struct class Geometry < FFI::Struct
layout :x, :int16, layout :x, :int16,
@@ -28,6 +30,39 @@ module X
:is_root, :int8 :is_root, :int8
end 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 :deploy, [], :int
attach_function :add_keybind, [:int], :void attach_function :add_keybind, [:int], :void
attach_function :add_mousebind, [:int], :void attach_function :add_mousebind, [:int], :void
@@ -47,6 +82,11 @@ module X
attach_function :move_window, [: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 :resize_window, [:window_id, :int, :int], :void
attach_function :wait_for_event, [], Event.by_value 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 end
if X.deploy < 0 if X.deploy < 0
@@ -70,15 +110,20 @@ def refresh_monitors
end end
end end
refresh_monitors 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 get_wm_normal_hints(window_id) refresh_monitors
# xprop_output = `xprop -id 0x#{window_id.to_s(16)} WM_NORMAL_HINTS`
# return {} unless xprop_output =~ /WM_NORMAL_HINTS\(([^)]+)\):\s*(.*)/
# hints = {}
# # TODO: parse the output properly
# hints
# end
$workspaces = {} $workspaces = {}
$windows = {} $windows = {}
@@ -105,20 +150,62 @@ class Node
end end
class Window < Node class Window < Node
attr_accessor :window_id, :x, :y, :width, :height#, :state attr_accessor :window_id, :x, :y, :width, :height, :state, :floating, :workspace
def initialize(window_id) def initialize(window_id, workspace)
@workspace = workspace
@window_id = window_id @window_id = window_id
# @state = :widthrawn # :iconic, :withdrawn, :normal @name = X.get_wm_name(window_id)
# ADD: properties for https://tronche.com/gui/x/icccm/sec-4.html#s-4 sec 4.1.2.(3, 4) @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() super()
end end
def delete
@workspace.remove self
end
def each_leaf(&block) def each_leaf(&block)
block.call(self) block.call(self)
end end
def apply_geometry 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.move_window @window_id, @x, @y
X.resize_window @window_id, @width, @height X.resize_window @window_id, @width, @height
end end
@@ -172,7 +259,7 @@ class WindowBlock < Node
child.height = child_size child.height = child_size
end end
child.compute_geometry! if child.is_a?(WindowBlock) child.compute_geometry! if child.is_a?(WindowBlock)
child.apply_geometry if child.is_a?(Window) child.apply_geometry! if child.is_a?(Window)
pos += child_size pos += child_size
end end
end end
@@ -210,14 +297,19 @@ class Workspace
end end
def add(window_id) def add(window_id)
@tiled_root_block.add_node Window.new(window_id) window = Window.new(window_id, self)
$windows[window_id] = self if window.floating
@untiled_windows << window
else
@tiled_root_block.add_node window
end
$windows[window_id] = window
end end
def remove(window_id) def remove(window)
@untiled_windows.delete window_id @untiled_windows.delete window
@tiled_root_block.each_node { |node| node.children.delete window_id } @tiled_root_block.each_node { |node| node.children.delete window }
$windows.delete window_id $windows.delete window.window_id
end end
def close_all def close_all
@@ -239,16 +331,18 @@ end
EVENT_TYPES = { EVENT_TYPES = {
0 => :unused, 0 => :unused,
1 => :create, 1 => :create,
2 => :close, 2 => :closed,
3 => :enter, 3 => :enter,
-1 => :show_after, 4 => :showed,
4 => :show, 5 => :show_request,
5 => :mouse_press, 6 => :mouse_press,
6 => :mouse_drag, 7 => :mouse_drag,
7 => :mouse_release, 8 => :mouse_release,
8 => :key_press, 9 => :key_press,
9 => :key_release, 10 => :key_release,
10 => :configured 11 => :closed,
12 => :configure_request,
13 => :resize_request
}.freeze }.freeze
$workspaces[:main] = Workspace.new(:main) $workspaces[:main] = Workspace.new(:main)
@@ -284,24 +378,32 @@ loop do
next unless event[:override_redirect].zero? next unless event[:override_redirect].zero?
X.subscribe event[:window] X.subscribe event[:window]
X.focus event[:window] X.focus event[:window]
when :close when :closed
X.kill event[:window] pp "Deleting window #{event[:window]}"
$windows[event[:window]]&.remove event[:window] $windows[event[:window]]&.delete
$workspaces[:main].tiled_root_block.compute_geometry!
when :enter when :enter
X.focus event[:window] X.focus event[:window]
X.send_to_top event[:window] X.send_to_top event[:window]
when :show_after when :showed
next unless event[:override_redirect].zero? next unless event[:override_redirect].zero?
X.focus event[:window] 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].add event[:window] if $windows[event[:window]].nil?
$workspaces[:main].tiled_root_block.compute_geometry! $workspaces[:main].tiled_root_block.compute_geometry!
when :show
X.show event[:window]
when :mouse_press when :mouse_press
next if event[:is_root] != 0 next if event[:is_root] != 0
X.send_to_top event[:window] X.send_to_top event[:window]
X.focus event[:window]
$mouse_state = event[:btn] $mouse_state = event[:btn]
$mouse_window = event[:window] $mouse_window = $windows[event[:window]]
if $mouse_window.nil?
$mouse_state = -1
$mouse_window = -1
next
end
$mouse_pos_start = X.get_pointer $mouse_pos_start = X.get_pointer
$geom_start = X.get_geometry event[:window] $geom_start = X.get_geometry event[:window]
$mousebind_actions[event[:btn]]&.call(event) $mousebind_actions[event[:btn]]&.call(event)
@@ -315,16 +417,15 @@ loop do
screen_bounds[:x] + screen_bounds[:width] - $geom_start[:width]].min screen_bounds[:x] + screen_bounds[:width] - $geom_start[:width]].min
new_y = [[$geom_start[:y] + dy, screen_bounds[:y]].max, new_y = [[$geom_start[:y] + dy, screen_bounds[:y]].max,
screen_bounds[:y] + screen_bounds[:height] - $geom_start[:height]].min screen_bounds[:y] + screen_bounds[:height] - $geom_start[:height]].min
X.move_window $mouse_window, new_x, new_y $mouse_window.move new_x, new_y
elsif $mouse_state == 3 elsif $mouse_state == 3
X.resize_window $mouse_window, $mouse_window.resize [$geom_start[:width] + dx, 50].max,
[$geom_start[:width] + dx, 50].max, [$geom_start[:height] + dy, 50].max
[$geom_start[:height] + dy, 50].max
end end
when :mouse_release when :mouse_release
if [1, 3].include?($mouse_state) if [1, 3].include?($mouse_state)
X.focus $mouse_window X.focus $mouse_window.window_id
X.send_to_top $mouse_window X.send_to_top $mouse_window.window_id
$mouse_state = -1 $mouse_state = -1
$mouse_window = -1 $mouse_window = -1
$mouse_pos_start = { x: -1, y: -1 } $mouse_pos_start = { x: -1, y: -1 }
@@ -334,7 +435,13 @@ loop do
$keybind_actions[event[:btn]]&.call(event) $keybind_actions[event[:btn]]&.call(event)
when :key_release when :key_release
# TODO # TODO
when :configured when :configure_request
# TODO $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
end end

View File

@@ -1,29 +1,29 @@
#define XCB_KEY_PRESS 2 define XCB_KEY_PRESS 2
#define XCB_KEY_RELEASE 3 define XCB_KEY_RELEASE 3
#define XCB_BUTTON_PRESS 4 define XCB_BUTTON_PRESS 4
#define XCB_BUTTON_RELEASE 5 define XCB_BUTTON_RELEASE 5
#define XCB_MOTION_NOTIFY 6 define XCB_MOTION_NOTIFY 6
#define XCB_ENTER_NOTIFY 7 define XCB_ENTER_NOTIFY 7
#define XCB_LEAVE_NOTIFY 8 - maybe # define XCB_LEAVE_NOTIFY 8 - maybe
#define XCB_CREATE_NOTIFY 16 define XCB_CREATE_NOTIFY 16
// treat these similarly // treat these similarly
#define XCB_DESTROY_NOTIFY 17 // to remove from struct define XCB_DESTROY_NOTIFY 17 // to remove from struct
#define XCB_UNMAP_NOTIFY 18 // i guess this is minimize but idk if it is icccm IconicState? define XCB_UNMAP_NOTIFY 18 // i guess this is minimize but idk if it is icccm IconicState?
#define XCB_MAP_NOTIFY 19 // whatever im doin (dont map this is after mapping) define XCB_MAP_NOTIFY 19 // whatever im doin (dont map this is after mapping)
#define XCB_MAP_REQUEST 20 // Actual request to map // so map the window if possible define XCB_MAP_REQUEST 20 // Actual request to map // so map the window if possible
#define XCB_CONFIGURE_REQUEST 23 // for floats maybe #define XCB_CONFIGURE_REQUEST 23 // for floats maybe
#define XCB_GRAVITY_NOTIFY 24 // similar to XCB_CONFIGURE_NOTIFY but for pos #define XCB_GRAVITY_NOTIFY 24 // similar to XCB_CONFIGURE_NOTIFY but for pos
#define XCB_RESIZE_REQUEST 25 // similar to XCB_CONFIGURE_REQUEST but for resize #define XCB_RESIZE_REQUEST 25 // similar to XCB_CONFIGURE_REQUEST but for resize
#define XCB_CONFIGURE_NOTIFY 22 // prolly remove it # define XCB_CONFIGURE_NOTIFY 22 // prolly remove it
#define XCB_PROPERTY_NOTIFY 28 // only if netwm requires so #define XCB_PROPERTY_NOTIFY 28 // only if netwm requires so
#define XCB_CLIENT_MESSAGE 33 // only if netwm requires #define XCB_CLIENT_MESSAGE 33 // only if netwm requires