#!/usr/bin/env ruby require "ffi" module X extend FFI::Library 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, :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 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 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 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 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 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 refresh_monitors $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, :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 = { 0 => :unused, 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 $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 :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 :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 :mouse_press next if event[:is_root] != 0 X.send_to_top event[:window] X.focus event[:window] $mouse_state = event[:btn] $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) 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 $mouse_window.move new_x, new_y elsif $mouse_state == 3 $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.window_id X.send_to_top $mouse_window.window_id $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 :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