This commit is contained in:
2025-09-29 07:17:37 +01:00
commit f08113b532
3 changed files with 529 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.so
*.vim

303
X-kutu.c Normal file
View File

@@ -0,0 +1,303 @@
#define CLEANMASK(m) ((m & ~0x80))
#define SUPER XCB_MOD_MASK_4
#define ALT XCB_MOD_MASK_1
#define CTRL XCB_MOD_MASK_CONTROL
#define SHIFT XCB_MOD_MASK_SHIFT
#define MOD SUPER
#include <err.h>
#include <stdlib.h>
#include <unistd.h>
#include <xcb/randr.h>
#include <xcb/xcb.h>
xcb_connection_t *conn;
xcb_screen_t *scr;
xcb_window_t focuswin;
typedef struct Geometry {
int16_t x;
int16_t y;
uint16_t width;
uint16_t height;
} Geometry;
void cleanup(void) {
if (conn != NULL)
xcb_disconnect(conn);
}
void add_keybind(int key) {
xcb_grab_key(conn, 0, scr->root, MOD, key, XCB_GRAB_MODE_ASYNC,
XCB_GRAB_MODE_ASYNC);
}
void add_mousebind(int button) {
xcb_grab_button(conn, 0, scr->root,
XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE,
XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, scr->root, XCB_NONE,
button, MOD);
}
int deploy(void) {
uint32_t values[2];
int mask;
if (xcb_connection_has_error(conn = xcb_connect(NULL, NULL)))
return -1;
scr = xcb_setup_roots_iterator(xcb_get_setup(conn)).data;
focuswin = scr->root;
mask = XCB_CW_EVENT_MASK;
values[0] = XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY;
xcb_change_window_attributes_checked(conn, scr->root, mask, values);
xcb_flush(conn);
return 0;
}
void free_geometry(Geometry *g) { free(g); }
Geometry *xrandr_get_monitors(void) {
xcb_randr_get_screen_resources_current_cookie_t res_cookie =
xcb_randr_get_screen_resources_current(conn, scr->root);
xcb_randr_get_screen_resources_current_reply_t *res =
xcb_randr_get_screen_resources_current_reply(conn, res_cookie, NULL);
if (!res)
return NULL;
int num_outputs = xcb_randr_get_screen_resources_current_outputs_length(res);
xcb_randr_output_t *outputs =
xcb_randr_get_screen_resources_current_outputs(res);
Geometry *all = calloc(2, sizeof(Geometry));
int count = 0;
for (int i = 0; i < num_outputs && count < 2; i++) {
xcb_randr_get_output_info_cookie_t ocookie =
xcb_randr_get_output_info(conn, outputs[i], XCB_CURRENT_TIME);
xcb_randr_get_output_info_reply_t *oinfo =
xcb_randr_get_output_info_reply(conn, ocookie, NULL);
if (!oinfo)
continue;
if (oinfo->connection == XCB_RANDR_CONNECTION_CONNECTED &&
oinfo->crtc != XCB_NONE) {
xcb_randr_get_crtc_info_cookie_t ccookie =
xcb_randr_get_crtc_info(conn, oinfo->crtc, XCB_CURRENT_TIME);
xcb_randr_get_crtc_info_reply_t *cinfo =
xcb_randr_get_crtc_info_reply(conn, ccookie, NULL);
if (cinfo) {
all[count].x = cinfo->x;
all[count].y = cinfo->y;
all[count].width = cinfo->width;
all[count].height = cinfo->height;
free(cinfo);
count++;
}
}
free(oinfo);
}
free(res);
return all;
}
void focus(xcb_window_t win) {
xcb_set_input_focus(conn, XCB_INPUT_FOCUS_POINTER_ROOT, win,
XCB_CURRENT_TIME);
if (win != focuswin)
focuswin = win;
}
xcb_window_t get_focus(void) { return focuswin; }
void subscribe(xcb_window_t win) {
uint32_t values[2];
values[0] = XCB_EVENT_MASK_ENTER_WINDOW;
values[1] = XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY;
xcb_change_window_attributes(conn, win, XCB_CW_EVENT_MASK, values);
}
void kill(xcb_window_t window) {
xcb_kill_client(conn, window);
xcb_flush(conn);
}
void show(xcb_window_t window) {
xcb_map_window(conn, window);
xcb_flush(conn);
}
void hide(xcb_window_t window) {
xcb_unmap_window(conn, window);
xcb_flush(conn);
}
void send_to_top(xcb_window_t win) {
uint32_t values[1] = {XCB_STACK_MODE_ABOVE};
xcb_configure_window(conn, win, XCB_CONFIG_WINDOW_STACK_MODE, values);
xcb_flush(conn);
}
Geometry get_geometry(xcb_window_t win) {
xcb_get_geometry_reply_t *geom =
xcb_get_geometry_reply(conn, xcb_get_geometry(conn, win), NULL);
Geometry g = {geom->x, geom->y, geom->width, geom->height};
free(geom);
return g;
}
Geometry get_pointer(void) {
xcb_query_pointer_reply_t *geom;
geom = xcb_query_pointer_reply(conn, xcb_query_pointer(conn, scr->root), 0);
Geometry g = {geom->root_x, geom->root_y, 0, 0};
free(geom);
return g;
}
Geometry get_screen(void) {
return (Geometry){0, 0, scr->width_in_pixels, scr->height_in_pixels};
}
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_flush(conn);
}
void move_window(xcb_window_t win, int x, int y) {
uint32_t values[2] = {x, y};
xcb_configure_window(conn, win, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y,
values);
xcb_flush(conn);
}
void resize_window(xcb_window_t win, int width, int height) {
uint32_t values[2] = {width, height};
xcb_configure_window(
conn, win, XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, values);
xcb_flush(conn);
}
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;
Event wait_for_event(void) {
Event ret = {0};
xcb_generic_event_t *ev;
ev = xcb_wait_for_event(conn);
if (!ev)
errx(1, "xcb connection broken");
switch (CLEANMASK(ev->response_type)) {
case XCB_CREATE_NOTIFY: {
xcb_create_notify_event_t *e;
e = (xcb_create_notify_event_t *)ev;
ret.type = 1;
ret.window = e->window;
ret.override_redirect = e->override_redirect;
} break;
case XCB_DESTROY_NOTIFY: {
xcb_destroy_notify_event_t *e;
e = (xcb_destroy_notify_event_t *)ev;
ret.type = 2;
ret.window = e->window;
} break;
case XCB_ENTER_NOTIFY: {
xcb_enter_notify_event_t *e;
e = (xcb_enter_notify_event_t *)ev;
ret.type = 3;
ret.window = e->event;
} break;
case XCB_MAP_NOTIFY: {
xcb_map_notify_event_t *e;
e = (xcb_map_notify_event_t *)ev;
ret.type = 4;
ret.window = e->window;
} break;
case XCB_BUTTON_PRESS: {
xcb_button_press_event_t *e = (xcb_button_press_event_t *)ev;
if (!e->child)
break;
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 = 5;
ret.window = e->child;
ret.is_root = e->child == scr->root;
ret.x = e->event_x;
ret.y = e->event_y;
ret.btn = e->detail;
} break;
case XCB_MOTION_NOTIFY: {
xcb_motion_notify_event_t *e = (xcb_motion_notify_event_t *)ev;
ret.type = 6;
ret.window = e->child;
ret.x = e->event_x;
ret.y = e->event_y;
ret.state = e->state;
} break;
case XCB_BUTTON_RELEASE: {
xcb_button_release_event_t *e = (xcb_button_release_event_t *)ev;
xcb_ungrab_pointer(conn, XCB_CURRENT_TIME);
ret.type = 7;
ret.x = e->event_x;
ret.y = e->event_y;
ret.btn = e->detail;
} break;
case XCB_KEY_PRESS: {
xcb_key_press_event_t *e;
e = (xcb_key_press_event_t *)ev;
ret.type = 8;
ret.window = e->child;
ret.btn = e->detail;
ret.state = e->state;
} break;
case XCB_KEY_RELEASE: {
xcb_key_release_event_t *e;
e = (xcb_key_release_event_t *)ev;
ret.type = 9;
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;
ret.window = e->window;
ret.width = e->width;
ret.height = e->height;
ret.x = e->x;
ret.y = e->y;
} break;
}
xcb_flush(conn);
free(ev);
return ret;
}

224
kutu.rb Executable file
View File

@@ -0,0 +1,224 @@
#!/usr/bin/env ruby
require "ffi"
module X
extend FFI::Library
ffi_lib "/home/syed/main/kutu/X-kutu.so"
typedef :uint32, :window_id
typedef :pointer, :goemetry_array
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
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 :xrandr_get_monitors, [], :goemetry_array
attach_function :free_geometry, [:goemetry_array], :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
def self.get_monitors
ptr = xrandr_get_monitors
return [] if ptr.null?
monitors = []
2.times do |i|
geom_ptr = ptr + i * Geometry.size
geom = Geometry.new(geom_ptr)
break if geom[:width] == 0 || geom[:height] == 0
monitors << geom
end
free_geometry(ptr)
monitors
end
end
if X.deploy < 0
raise "Failed to deploy X"
exit 1
end
at_exit { X.cleanup }
$monitors = X.get_monitors
$monitors.each do |monitor|
puts "Monitor: #{monitor[:x]} #{monitor[:y]} #{monitor[:width]} #{monitor[:height]}"
end
$workspaces = {}
$windows = {}
class Window
attr_reader :window_id
def initialize(window_id)
@window_id = window_id
end
end
class Workspace
attr_reader :windows, :name, :screen
def initialize(name)
@windows = {}
@name = name
@screen = :main
end
def add(window_id)
@windows[window_id] = Window.new(window_id)
$windows[window_id] = self
end
def remove(window_id)
@windows.delete window_id
$windows.delete window_id
end
def close_all
@windows.each_key do |window|
X.kill window
self.remove window
end
end
def hide
@windows.each_key do |window|
X.hide window
end
end
def show
@windows.each_key do |window|
X.show window
end
end
end
EVENT_TYPES = {
0 => :unused,
1 => :create,
2 => :close,
3 => :enter,
4 => :show,
5 => :mouse_press,
6 => :mouse_drag,
7 => :mouse_release,
8 => :key_press,
9 => :key_release,
10 => :configured
}.freeze
$workspaces[:main] = Workspace.new(:main)
X.add_keybind 24
X.add_keybind 25
X.add_keybind 26
X.add_mousebind 1
X.add_mousebind 3
$mouse_state = -1
$mouse_window = -1
loop do
event = X.wait_for_event
case EVENT_TYPES[event[:type]]
when :create
if event[:override_redirect] == 0
X.subscribe event[:window]
X.focus event[:window]
$workspaces[:main].add event[:window]
end
when :close
X.kill event[:window]
$windows[event[:window]].remove event[:window]
when :enter
X.focus event[:window]
X.send_to_top event[:window]
when :show
X.show event[:window]
X.focus event[:window]
when :mouse_press
next if event[:is_root] != 0
X.send_to_top event[:window]
$mouse_state = event[:btn]
$mouse_window = event[:window]
$mouse_pos_start = X.get_pointer
$geom_start = X.get_geometry event[:window]
when :mouse_drag
screen_bounds = $monitors[0]
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
X.move_window $mouse_window, 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
end
when :mouse_release
if [1, 3].include?($mouse_state)
X.focus $mouse_window
X.send_to_top $mouse_window
$mouse_state = -1
$mouse_window = -1
$mouse_pos_start = { x: -1, y: -1 }
$geom_start = nil
end
when :key_press
case event[:btn]
when 24
X.kill event[:window]
when 25
pid = spawn("kitty")
Process.detach pid
when 26
$workspaces.each_value do |workspace|
workspace.close_all
end
exit 1
end
when :key_release
# TODO
when :configured
# TODO
end
end