From 6da2429c5453e0694e3251dfb52d1467acdf83b2 Mon Sep 17 00:00:00 2001 From: Syed Daanish Date: Sun, 5 Oct 2025 19:34:06 +0100 Subject: [PATCH] Modularize --- X-kutu.c | 17 +- build/.keep | 0 compile.sh | 8 +- kutu.rb | 395 +++-------------------------------------- {lib => src}/X-kutu.rb | 4 +- src/bindings.rb | 18 ++ src/events.rb | 127 +++++++++++++ src/node.rb | 13 ++ src/utils.rb | 81 +++++++++ src/window.rb | 61 +++++++ src/window_block.rb | 64 +++++++ src/workspace.rb | 52 ++++++ 12 files changed, 466 insertions(+), 374 deletions(-) create mode 100644 build/.keep rename {lib => src}/X-kutu.rb (95%) create mode 100644 src/bindings.rb create mode 100644 src/events.rb create mode 100644 src/node.rb create mode 100644 src/utils.rb create mode 100644 src/window.rb create mode 100644 src/window_block.rb create mode 100644 src/workspace.rb diff --git a/X-kutu.c b/X-kutu.c index b518e73..80d1a5f 100644 --- a/X-kutu.c +++ b/X-kutu.c @@ -1,4 +1,3 @@ -#include #define CLEANMASK(m) ((m & ~0x80)) // Definitions for modifier keys @@ -127,6 +126,12 @@ void kill(xcb_window_t window) { xcb_flush(conn); } +// Destroy a window +void destroy(xcb_window_t win) { + xcb_destroy_window(conn, win); + xcb_flush(conn); +} + // Show a window void show(xcb_window_t window) { xcb_map_window(conn, window); @@ -283,16 +288,22 @@ xcb_window_t draw_rectangle(int x, int y, int width, int height, return win; } +xcb_window_t get_root(void) { return scr->root; } + void grab_pointer(xcb_window_t win) { xcb_grab_pointer(conn, 0, win, XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_BUTTON_MOTION | XCB_EVENT_MASK_POINTER_MOTION_HINT, - XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, win, XCB_NONE, + XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, XCB_NONE, XCB_NONE, XCB_CURRENT_TIME); + xcb_flush(conn); } -void ungrab_pointer(void) { xcb_ungrab_pointer(conn, XCB_CURRENT_TIME); } +void ungrab_pointer(void) { + xcb_ungrab_pointer(conn, XCB_CURRENT_TIME); + xcb_flush(conn); +} // Wait for an event and return it as an Event structure // This function is blocking diff --git a/build/.keep b/build/.keep new file mode 100644 index 0000000..e69de29 diff --git a/compile.sh b/compile.sh index 41db450..1a06566 100755 --- a/compile.sh +++ b/compile.sh @@ -13,11 +13,13 @@ if ! xrandr --version >/dev/null 2>&1; then exit 1 fi -gcc -shared -fPIC -Wall -Wextra -o "$DIR/lib/X-kutu.so" "$DIR/X-kutu.c" $XCB +mkdir -p "$DIR/build" -if [ ! -f "$DIR/lib/X-kutu.so" ]; then +gcc -shared -fPIC -Wall -Wextra -o "$DIR/build/X-kutu.so" "$DIR/X-kutu.c" $XCB + +if [ ! -f "$DIR/build/X-kutu.so" ]; then echo "Error: compilation failed." >&2 exit 1 else - echo "Success: $DIR/lib/X-kutu.so is compiled." + echo "Success: $DIR/build/X-kutu.so is compiled." fi diff --git a/kutu.rb b/kutu.rb index 5e17c16..ba97f15 100755 --- a/kutu.rb +++ b/kutu.rb @@ -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 diff --git a/lib/X-kutu.rb b/src/X-kutu.rb similarity index 95% rename from lib/X-kutu.rb rename to src/X-kutu.rb index 3418456..297c93e 100644 --- a/lib/X-kutu.rb +++ b/src/X-kutu.rb @@ -2,7 +2,7 @@ require "ffi" module X extend FFI::Library - ffi_lib File.join(__dir__, "X-kutu.so") + ffi_lib File.join(__dir__, "../build/X-kutu.so") typedef :uint32, :xcb_window_t typedef :uint32, :xcb_pixmap_t @@ -68,6 +68,7 @@ module X attach_function :get_focus, [], :xcb_window_t attach_function :subscribe, [:xcb_window_t], :void attach_function :kill, [:xcb_window_t], :void + attach_function :destroy, [:xcb_window_t], :void attach_function :show, [:xcb_window_t], :void attach_function :hide, [:xcb_window_t], :void attach_function :send_to_top, [:xcb_window_t], :void @@ -87,4 +88,5 @@ module X attach_function :draw_rectangle, [:int, :int, :int, :int, :uint32], :xcb_window_t attach_function :grab_pointer, [:xcb_window_t], :void attach_function :ungrab_pointer, [], :void + attach_function :get_root, [], :xcb_window_t end diff --git a/src/bindings.rb b/src/bindings.rb new file mode 100644 index 0000000..376c50b --- /dev/null +++ b/src/bindings.rb @@ -0,0 +1,18 @@ +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 + + +mousebind 1 + +mousebind 3 diff --git a/src/events.rb b/src/events.rb new file mode 100644 index 0000000..65ec399 --- /dev/null +++ b/src/events.rb @@ -0,0 +1,127 @@ +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 + + +def handle_event(event) + case EVENT_TYPES[event[:type]] + + + when :create + return unless event[:override_redirect].zero? + X.subscribe event[:window] + + + when :closed + $windows[event[:window]]&.delete + $workspaces[:main].tiled_root_block.compute_geometry! + + + when :enter + 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 } + X.focus event[:window] + + + when :showed + return unless event[:override_redirect].zero? + 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 } + X.focus event[:window] + + + when :show_request + $workspaces[:main].add event[:window] if $windows[event[:window]].nil? + $workspaces[:main].tiled_root_block.compute_geometry! + X.show event[:window] + + + when :mouse_press + return if event[:is_root] != 0 + return if $windows[event[:window]].nil? + $mouse_data[:btn] = event[:btn] + $mouse_data[:window] = $windows[event[:window]] + X.grab_pointer $root + $mouse_data[:pointer] = X.get_pointer + if $mouse_data[:window].floating + $mouse_data[:mode] = :floating + $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! $mouse_data[:window] + target = drop_target(find_targets, mouse_pos[:x], mouse_pos[:y]) + if target + if $rect[:target] != target + X.destroy $rect[:id] if $rect[:id] + $rect[:id] = X.draw_rectangle target[:x], target[:y], target[:width], target[:height], 0x500070 + $rect[:target] = target + end + else + X.destroy $rect[:id] if $rect[:id] + $rect = {} + end + elsif $mouse_data[:btn] == 3 + # TODO: tile resize . dynamic + end + end + + when :mouse_release + if $mouse_data[:mode] == :tiled + X.destroy $rect[:id] if $rect[:id] + $rect = {} + end + X.ungrab_pointer + X.send_to_top $mouse_data[:window].window_id if $mouse_data[:window] + 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 diff --git a/src/node.rb b/src/node.rb new file mode 100644 index 0000000..7c4b47c --- /dev/null +++ b/src/node.rb @@ -0,0 +1,13 @@ +class Node + attr_accessor :size + + def initialize + @size = 0 + end +end + + +# Require submodules + +require_relative "./window" +require_relative "./window_block" diff --git a/src/utils.rb b/src/utils.rb new file mode 100644 index 0000000..5bfa641 --- /dev/null +++ b/src/utils.rb @@ -0,0 +1,81 @@ +def keybind(key, &block) + X.add_keybind key + $keybind_actions[key] = block if block +end + +def mousebind(btn, &block) + X.add_mousebind btn + $mousebind_actions[btn] = block if block +end + + + +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 + + + +def compute_drop_targets!(window) + targets = [] + + $monitors.each_value do |monitor| + # if monitor[:windows].nil? || monitor[:windows].empty? + # targets << { type: :monitor_full, x: monitor[:x], y: monitor[:y], width: monitor[:width], height: monitor[:height], monitor: monitor } + # else + margin = 60 + 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 + end + + $windows.each_value do |w| + next if w.floating + next if w == window + + margin_x = (w.width / 4).round + margin_y = (w.height / 4).round + + targets << { type: :window_top, x: w.x, y: w.y, width: w.width, height: margin_y, window: w } + targets << { type: :window_bottom, x: w.x, y: w.y + w.height - margin_y, width: w.width, height: margin_y, window: w } + targets << { type: :window_left, x: w.x, y: w.y, width: margin_x, height: w.height, window: w } + targets << { type: :window_right, x: w.x + w.width - margin_x, y: w.y, width: margin_x, 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 diff --git a/src/window.rb b/src/window.rb new file mode 100644 index 0000000..9a8537e --- /dev/null +++ b/src/window.rb @@ -0,0 +1,61 @@ +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 diff --git a/src/window_block.rb b/src/window_block.rb new file mode 100644 index 0000000..a78aa33 --- /dev/null +++ b/src/window_block.rb @@ -0,0 +1,64 @@ +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_percent = @children.map { |c| c.size.to_i > 0 ? c.size.to_i : 0 }.sum + flex_count = @children.count { |c| c.size.to_i <= 0 } + total_space = horizontal ? @width : @height + total_percent = [total_percent, 100].min + remaining_percent = 100 - total_percent + flex_percent = flex_count > 0 ? remaining_percent / flex_count : 0 + pos = horizontal ? @x : @y + + @children.each do |child| + percent = child.size.to_i > 0 ? child.size.to_i : flex_percent + child_size = (total_space * percent) / 100.0 + 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 diff --git a/src/workspace.rb b/src/workspace.rb new file mode 100644 index 0000000..4d1a110 --- /dev/null +++ b/src/workspace.rb @@ -0,0 +1,52 @@ +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 + 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