Modularize

This commit is contained in:
2025-10-05 19:34:06 +01:00
parent 5ac77254a4
commit 6da2429c54
12 changed files with 466 additions and 374 deletions

395
kutu.rb
View File

@@ -1,13 +1,31 @@
#!/usr/bin/env ruby
require_relative "./lib/X-kutu"
require_relative "./src/X-kutu"
# Initialize X
if X.deploy < 0
raise "Failed to deploy X"
else
puts "Started kutu WM for X11"
end
# Require modules
require_relative "./src/utils"
require_relative "./src/node"
require_relative "./src/workspace"
require_relative "./src/events"
# Cleanup on exit
at_exit { X.cleanup }
# Globals
$monitors = {}
$workspaces = {}
$windows = {}
@@ -17,385 +35,28 @@ $mousebind_actions = {}
$mouse_data = {}
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
$root = X.get_root
$rect = {}
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
# Initialize monitors
refresh_monitors!
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
# Add keybinds
class Node
attr_accessor :size
load "./src/bindings.rb"
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 = {
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
# Initialize workspaces
$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
def compute_drop_targets!
targets = []
$monitors.each_value do |monitor|
margin = 40
targets << { type: :monitor_top, x: monitor[:x], y: monitor[:y], width: monitor[:width], height: margin, monitor: monitor }
targets << { type: :monitor_bottom, x: monitor[:x], y: monitor[:y] + monitor[:height] - margin, width: monitor[:width], height: margin, monitor: monitor }
targets << { type: :monitor_left, x: monitor[:x], y: monitor[:y], width: margin, height: monitor[:height], monitor: monitor }
targets << { type: :monitor_right, x: monitor[:x] + monitor[:width] - margin, y: monitor[:y], width: margin, height: monitor[:height], monitor: monitor }
end
$windows.each do |w|
# next if w.hidden?
margin = 60
targets << { type: :window_top, x: w.x, y: w.y, width: w.width, height: margin, window: w }
targets << { type: :window_bottom, x: w.x, y: w.y + w.height - margin, width: w.width, height: margin, window: w }
targets << { type: :window_left, x: w.x, y: w.y, width: margin, height: w.height, window: w }
targets << { type: :window_right, x: w.x + w.width - margin, y: w.y, width: margin, height: w.height, window: w }
end
targets
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
# Main loop
loop do
event = X.wait_for_event
case EVENT_TYPES[event[:type]]
when :create
next unless event[:override_redirect].zero?
X.subscribe 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]
floating_windows = $windows.select { |_i, w| w.floating }
floating_windows.each { |_i, w| X.send_to_top w.window_id }
when :showed
next unless event[:override_redirect].zero?
X.focus event[:window]
X.send_to_top event[:window]
floating_windows = $windows.select { |_i, w| w.floating }
floating_windows.each { |_i, w| X.send_to_top w.window_id }
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.focus event[:window]
$mouse_data[:btn] = event[:btn]
$mouse_data[:window] = $windows[event[:window]]
X.grab_pointer event[:window]
if $mouse_data[:window].floating
$mouse_data[:mode] = :floating
$mouse_data[:pointer] = X.get_pointer
$mouse_data[:geometry] = X.get_geometry event[:window]
else
$mouse_data[:mode] = :tiled
end
$mousebind_actions[event[:btn]]&.call(event)
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
X.resize_window $mouse_data[:window].window_id,
[$mouse_data[:geometry][:width] + dx, 50].max,
[$mouse_data[:geometry][:height] + dy, 50].max
end
elsif $mouse_data[:mode] == :tiled
if $mouse_data[:btn] == 1
find_targets = compute_drop_targets!
target = drop_target(find_targets, mouse_pos[:x], mouse_pos[:y])
X.draw_rectangle target[:x], target[:y], target[:width], target[:height], 0x00ff00
elsif $mouse_data[:btn] == 3
# TODO: tile resize . dynamic
end
end
when :mouse_release
if $mouse_data[:mode] == :floating
if [1, 3].include?($mouse_data[:btn])
X.ungrab_pointer
X.focus $mouse_data[:window].window_id
X.send_to_top $mouse_data[:window].window_id
end
elsif $mouse_data[:mode] == :tiled
# TODO
end
X.focus $mouse_data[:window].window_id if $mouse_data[:window]
$mouse_data = {}
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
handle_event event
end