#!/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 = {} 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 refresh_monitors # 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 $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#, :state def initialize(window_id) @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) 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, -1 => :show_after, 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 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 :enter X.focus event[:window] X.send_to_top event[:window] when :show_after next unless event[:override_redirect].zero? X.focus 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] $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