#!/usr/bin/env ruby require_relative "./lib/X-kutu" if X.deploy < 0 raise "Failed to deploy X" end at_exit { X.cleanup } $monitors = {} $workspaces = {} $windows = {} $keybind_actions = {} $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 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! 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 = { 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 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 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 end