Files
kutu/kutu.rb
2025-09-29 21:20:48 +01:00

324 lines
7.9 KiB
Ruby
Executable File

#!/usr/bin/env ruby
require "ffi"
module X
extend FFI::Library
ffi_lib File.join(__dir__, "X-kutu.so")
typedef :uint32, :window_id
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 :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
end
if X.deploy < 0
raise "Failed to deploy X"
end
at_exit { X.cleanup }
$monitors = {}
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
$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
def initialize(window_id)
@window_id = window_id
super()
end
def each_leaf(&block)
block.call(self)
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)
@tiled_root_block.add_node Window.new(window_id)
$windows[window_id] = self
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
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 => :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)
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
if event[:override_redirect].zero?
X.subscribe event[:window]
X.focus 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]
$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]
$mouse_state = event[:btn]
$mouse_window = event[:window]
$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
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
$keybind_actions[event[:btn]]&.call(event)
when :key_release
# TODO
when :configured
# TODO
end
end