From 09eb87e1b73b4badb54b98feb86718bdc2af19e7 Mon Sep 17 00:00:00 2001 From: Syed Daanish Date: Thu, 2 Oct 2025 15:22:43 +0100 Subject: [PATCH] Add better icccm support --- X-kutu.c | 111 +++++++++++++++++++++++++----- compile.sh | 2 +- kutu.rb | 195 +++++++++++++++++++++++++++++++++++++++++------------ list.tmp | 26 +++---- 4 files changed, 260 insertions(+), 74 deletions(-) diff --git a/X-kutu.c b/X-kutu.c index 34c40ec..c2db714 100644 --- a/X-kutu.c +++ b/X-kutu.c @@ -1,3 +1,4 @@ +#include #define CLEANMASK(m) ((m & ~0x80)) // Definitions for modifier keys @@ -12,10 +13,12 @@ // Standard headers #include #include +#include #include // XCB header #include +#include // Global variables // 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 // prepare the window manager int deploy(void) { - uint32_t values[2]; int mask; // Start X connection and return -1 if it fails @@ -85,7 +87,12 @@ int deploy(void) { // Setuup event masks 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_flush(conn); @@ -186,6 +193,52 @@ void resize_window(xcb_window_t win, int width, int height) { 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 // This function is blocking // 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: { xcb_map_notify_event_t *e; e = (xcb_map_notify_event_t *)ev; - ret.type = -1; + ret.type = 4; ret.window = e->window; ret.override_redirect = e->override_redirect; } break; @@ -233,9 +286,8 @@ Event wait_for_event(void) { case XCB_MAP_REQUEST: { xcb_map_request_event_t *e; e = (xcb_map_request_event_t *)ev; - ret.type = 4; + ret.type = 5; ret.window = e->window; - ret.override_redirect = 1; } break; case XCB_BUTTON_PRESS: { @@ -249,7 +301,7 @@ Event wait_for_event(void) { XCB_EVENT_MASK_POINTER_MOTION_HINT, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, e->child, XCB_NONE, XCB_CURRENT_TIME); - ret.type = 5; + ret.type = 6; ret.window = e->child; ret.is_root = e->child == scr->root; ret.x = e->event_x; @@ -259,7 +311,7 @@ Event wait_for_event(void) { case XCB_MOTION_NOTIFY: { xcb_motion_notify_event_t *e = (xcb_motion_notify_event_t *)ev; - ret.type = 6; + ret.type = 7; ret.window = e->child; ret.x = e->event_x; 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; // Ungrab pointer after dragging xcb_ungrab_pointer(conn, XCB_CURRENT_TIME); - ret.type = 7; + ret.type = 8; ret.x = e->event_x; ret.y = e->event_y; ret.btn = e->detail; @@ -279,7 +331,7 @@ Event wait_for_event(void) { case XCB_KEY_PRESS: { xcb_key_press_event_t *e; e = (xcb_key_press_event_t *)ev; - ret.type = 8; + ret.type = 9; ret.window = e->child; ret.btn = e->detail; ret.state = e->state; @@ -288,22 +340,49 @@ Event wait_for_event(void) { case XCB_KEY_RELEASE: { xcb_key_release_event_t *e; e = (xcb_key_release_event_t *)ev; - ret.type = 9; + ret.type = 10; ret.window = e->child; ret.btn = e->detail; ret.state = e->state; } break; - case XCB_CONFIGURE_NOTIFY: { - xcb_configure_notify_event_t *e; - e = (xcb_configure_notify_event_t *)ev; - ret.type = 10; + case XCB_UNMAP_NOTIFY: { + xcb_unmap_notify_event_t *e; + e = (xcb_unmap_notify_event_t *)ev; + 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.width = e->width; ret.height = e->height; - ret.x = e->x; - ret.y = e->y; } 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); diff --git a/compile.sh b/compile.sh index 3228c4b..8264be8 100755 --- a/compile.sh +++ b/compile.sh @@ -3,7 +3,7 @@ set -euo pipefail 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 exit 1 fi diff --git a/kutu.rb b/kutu.rb index 693062f..80e753b 100755 --- a/kutu.rb +++ b/kutu.rb @@ -7,6 +7,8 @@ module X 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, @@ -28,6 +30,39 @@ module X :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 @@ -47,6 +82,11 @@ module X 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 @@ -70,15 +110,20 @@ def refresh_monitors 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) -# 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 +refresh_monitors $workspaces = {} $windows = {} @@ -105,20 +150,62 @@ class Node end 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 - # @state = :widthrawn # :iconic, :withdrawn, :normal - # ADD: properties for https://tronche.com/gui/x/icccm/sec-4.html#s-4 sec 4.1.2.(3, 4) + @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 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.resize_window @window_id, @width, @height end @@ -172,7 +259,7 @@ class WindowBlock < Node child.height = child_size end 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 end end @@ -210,14 +297,19 @@ class Workspace end def add(window_id) - @tiled_root_block.add_node Window.new(window_id) - $windows[window_id] = self + 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_id) - @untiled_windows.delete window_id - @tiled_root_block.each_node { |node| node.children.delete window_id } - $windows.delete window_id + 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 @@ -239,16 +331,18 @@ end EVENT_TYPES = { 0 => :unused, 1 => :create, - 2 => :close, + 2 => :closed, 3 => :enter, - -1 => :show_after, - 4 => :show, - 5 => :mouse_press, - 6 => :mouse_drag, - 7 => :mouse_release, - 8 => :key_press, - 9 => :key_release, - 10 => :configured + 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) @@ -284,24 +378,32 @@ loop do next unless event[:override_redirect].zero? X.subscribe event[:window] X.focus event[:window] - when :close - X.kill event[:window] - $windows[event[:window]]&.remove 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 :show_after + 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 :show - X.show event[:window] 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 = 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 $geom_start = X.get_geometry event[:window] $mousebind_actions[event[:btn]]&.call(event) @@ -315,16 +417,15 @@ loop do 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 - X.move_window $mouse_window, new_x, new_y + $mouse_window.move new_x, new_y elsif $mouse_state == 3 - X.resize_window $mouse_window, - [$geom_start[:width] + dx, 50].max, - [$geom_start[:height] + dy, 50].max + $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 - X.send_to_top $mouse_window + 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 } @@ -334,7 +435,13 @@ loop do $keybind_actions[event[:btn]]&.call(event) when :key_release # TODO - when :configured - # 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 diff --git a/list.tmp b/list.tmp index 9d99ddb..416000f 100644 --- a/list.tmp +++ b/list.tmp @@ -1,29 +1,29 @@ -#define XCB_KEY_PRESS 2 -#define XCB_KEY_RELEASE 3 +define XCB_KEY_PRESS 2 +define XCB_KEY_RELEASE 3 -#define XCB_BUTTON_PRESS 4 -#define XCB_BUTTON_RELEASE 5 -#define XCB_MOTION_NOTIFY 6 +define XCB_BUTTON_PRESS 4 +define XCB_BUTTON_RELEASE 5 +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 -#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_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_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_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_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_CLIENT_MESSAGE 33 // only if netwm requires