xinerama 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b933357913f6fed409fde25826b23f96b615be6d22856960c796a6edb2e8655e
4
+ data.tar.gz: 5019b91d24b032ae78b5e2fe6ee1c5ec60855bb82a36a7a5a30658e6ef30ac54
5
+ SHA512:
6
+ metadata.gz: 10d929bfe5dda72bca4d05b796c08db422c5f29951b6a4cd863e34000fe8c2ce67c0c53f942499b0eee9045c78b8f036b882309c03414f58c5551d73f5b74b0c
7
+ data.tar.gz: 12d2672635c3c30987bee19d26185a7aa207e1f2d58903d9e95b486d488040f88e3e08de8f85b975a40697df523b3eabd52edba0084e66c49884f46a661e4395
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # Xinerama
2
+
3
+ Ruby FFI bindings for the Xinerama X11 extension. Query multi-monitor screen configurations with a clean, idiomatic Ruby API.
4
+
5
+ Official libXinerama repo: https://gitlab.freedesktop.org/xorg/lib/libxinerama
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem 'xinerama'
13
+ ```
14
+
15
+ Or install directly:
16
+
17
+ ```bash
18
+ gem install xinerama
19
+ ```
20
+
21
+ **Dependencies:** Requires `libX11` and `libXinerama` to be installed on your system.
22
+
23
+ ```bash
24
+ # Debian/Ubuntu
25
+ apt-get install libx11-6 libxinerama1
26
+
27
+ # Fedora/RHEL
28
+ dnf install libX11 libXinerama
29
+
30
+ # Arch
31
+ pacman -S libx11 libxinerama
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Quick Access
37
+
38
+ ```ruby
39
+ require 'xinerama'
40
+
41
+ # Get all screens (lazy enumerator)
42
+ Xinerama.screens.each do |screen|
43
+ puts "Screen #{screen.screen_number}: #{screen.width}x#{screen.height}+#{screen.x}+#{screen.y}"
44
+ end
45
+
46
+ # Check if Xinerama is active
47
+ Xinerama.active? # => true
48
+
49
+ # Get version
50
+ Xinerama.version # => #<Xinerama::Version 1.1>
51
+ ```
52
+
53
+ ### Display Connection
54
+
55
+ For multiple queries, reuse the display connection:
56
+
57
+ ```ruby
58
+ Xinerama.open do |display|
59
+ puts "Xinerama active: #{display.active?}"
60
+ puts "Version: #{display.version}"
61
+ puts "Extension event base: #{display.extension.event_base}"
62
+
63
+ display.screens.each do |screen|
64
+ puts screen.inspect
65
+ end
66
+ end
67
+ ```
68
+
69
+ ### Screen Information
70
+
71
+ ```ruby
72
+ Xinerama.open do |display|
73
+ display.screens.each do |screen|
74
+ screen.screen_number # => 0
75
+ screen.x # => 0
76
+ screen.y # => 0
77
+ screen.width # => 1920
78
+ screen.height # => 1080
79
+ screen.origin # => [0, 0]
80
+ screen.dimensions # => [1920, 1080]
81
+ screen.area # => 2073600
82
+ screen.aspect_ratio # => (16/9)
83
+ screen.to_h # => { screen_number: 0, x: 0, y: 0, width: 1920, height: 1080 }
84
+ end
85
+ end
86
+ ```
87
+
88
+ ### Finding Screens
89
+
90
+ ```ruby
91
+ Xinerama.open do |display|
92
+ # Primary screen (first in list)
93
+ display.primary_screen
94
+
95
+ # Find screen containing a point
96
+ display.screen_at(100, 200)
97
+
98
+ # Total virtual desktop dimensions
99
+ display.total_dimensions # => [3840, 1080]
100
+ end
101
+ ```
102
+
103
+ ### Lazy Evaluation
104
+
105
+ Screen queries return lazy enumerators, efficient for large multi-monitor setups:
106
+
107
+ ```ruby
108
+ # Only iterates until first matching screen is found
109
+ large_screen = Xinerama.screens.find { |s| s.area > 2_000_000 }
110
+
111
+ # Chain operations without intermediate arrays
112
+ Xinerama.screens
113
+ .select { |s| s.width > 1920 }
114
+ .map(&:to_h)
115
+ .first(2)
116
+ ```
117
+
118
+ ### Connecting to Remote Displays
119
+
120
+ ```ruby
121
+ Xinerama.open("remote:0.0") do |display|
122
+ display.screens.each { |s| puts s }
123
+ end
124
+ ```
125
+
126
+ ### Error Handling
127
+
128
+ ```ruby
129
+ begin
130
+ Xinerama.open do |display|
131
+ display.extension # Raises if Xinerama unavailable
132
+ end
133
+ rescue Xinerama::DisplayError => e
134
+ puts "Cannot connect to X server"
135
+ rescue Xinerama::NotAvailableError => e
136
+ puts "Xinerama extension not available"
137
+ end
138
+ ```
139
+
140
+ ## API Reference
141
+
142
+ ### Module Methods
143
+
144
+ | Method | Description |
145
+ |--------|-------------|
146
+ | `Xinerama.open(display_name = nil, &block)` | Open display connection |
147
+ | `Xinerama.screens(display_name = nil)` | Get lazy enumerator of screens |
148
+ | `Xinerama.active?(display_name = nil)` | Check if Xinerama is active |
149
+ | `Xinerama.version(display_name = nil)` | Get Xinerama version |
150
+
151
+ ### Display
152
+
153
+ | Method | Description |
154
+ |--------|-------------|
155
+ | `#active?` | Boolean indicating Xinerama activation |
156
+ | `#available?` | Boolean indicating extension availability |
157
+ | `#screens` | Lazy enumerator of ScreenInfo objects |
158
+ | `#primary_screen` | First screen |
159
+ | `#screen_at(x, y)` | Screen containing the given point |
160
+ | `#screen_count` | Number of screens |
161
+ | `#total_dimensions` | `[width, height]` of virtual desktop |
162
+ | `#version` | Version object |
163
+ | `#extension` | Extension object with event/error bases |
164
+ | `#close` | Close display connection |
165
+ | `#closed?` | Check if connection is closed |
166
+
167
+ ### ScreenInfo
168
+
169
+ | Method | Description |
170
+ |--------|-------------|
171
+ | `#screen_number` | Screen index |
172
+ | `#x`, `#y` | Origin coordinates |
173
+ | `#width`, `#height` | Dimensions |
174
+ | `#origin` | `[x, y]` |
175
+ | `#dimensions` | `[width, height]` |
176
+ | `#area` | `width * height` |
177
+ | `#aspect_ratio` | Rational width/height |
178
+ | `#contains?(x, y)` | Point containment check |
179
+ | `#to_h`, `#to_a` | Conversion methods |
180
+
181
+ ## License
182
+
183
+ MIT License. See LICENSE.txt.
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xinerama
4
+ class Display
5
+ attr_reader :display_name
6
+
7
+ def initialize(display_pointer, display_name = nil)
8
+ @pointer = display_pointer
9
+ @display_name = display_name
10
+ @closed = false
11
+ end
12
+
13
+ def close
14
+ return if @closed
15
+
16
+ FFI.XCloseDisplay(@pointer)
17
+ @closed = true
18
+ nil
19
+ end
20
+
21
+ def closed?
22
+ @closed
23
+ end
24
+
25
+ def extension
26
+ ensure_open!
27
+ @extension ||= query_extension
28
+ end
29
+
30
+ def available?
31
+ ensure_open!
32
+ @available ||= check_availability
33
+ end
34
+
35
+ def version
36
+ ensure_open!
37
+ @version ||= query_version
38
+ end
39
+
40
+ def active?
41
+ ensure_open!
42
+ FFI.XineramaIsActive(@pointer)
43
+ end
44
+
45
+ def screens
46
+ ensure_open!
47
+ query_screens
48
+ end
49
+
50
+ def primary_screen
51
+ screens.first
52
+ end
53
+
54
+ def screen_count
55
+ screens.count
56
+ end
57
+
58
+ def screen_at(x, y)
59
+ screens.find { |screen| screen.contains?(x, y) }
60
+ end
61
+
62
+ def total_dimensions
63
+ all_screens = screens.to_a
64
+ return [0, 0] if all_screens.empty?
65
+
66
+ max_x = all_screens.map { |s| s.x + s.width }.max
67
+ max_y = all_screens.map { |s| s.y + s.height }.max
68
+ [max_x, max_y]
69
+ end
70
+
71
+ class << self
72
+ def open(display_name = nil)
73
+ display = connect(display_name)
74
+ return display unless block_given?
75
+
76
+ begin
77
+ yield display
78
+ ensure
79
+ display.close
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def connect(display_name)
86
+ pointer = FFI.XOpenDisplay(display_name)
87
+ raise DisplayError, "Cannot open display: #{display_name || '$DISPLAY'}" if pointer.null?
88
+
89
+ new(pointer, display_name)
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def ensure_open!
96
+ raise DisplayError, "Display connection closed" if @closed
97
+ end
98
+
99
+ def check_availability
100
+ event_base = ::FFI::MemoryPointer.new(:int)
101
+ error_base = ::FFI::MemoryPointer.new(:int)
102
+ FFI.XineramaQueryExtension(@pointer, event_base, error_base)
103
+ end
104
+
105
+ def query_extension
106
+ event_base = ::FFI::MemoryPointer.new(:int)
107
+ error_base = ::FFI::MemoryPointer.new(:int)
108
+
109
+ available = FFI.XineramaQueryExtension(@pointer, event_base, error_base)
110
+ raise NotAvailableError, "Xinerama extension not available" unless available
111
+
112
+ Extension.new(
113
+ event_base: event_base.read_int,
114
+ error_base: error_base.read_int
115
+ )
116
+ end
117
+
118
+ def query_version
119
+ major = ::FFI::MemoryPointer.new(:int)
120
+ minor = ::FFI::MemoryPointer.new(:int)
121
+
122
+ status = FFI.XineramaQueryVersion(@pointer, major, minor)
123
+ raise NotAvailableError, "Cannot query Xinerama version" if status.zero?
124
+
125
+ Version.new(
126
+ major: major.read_int,
127
+ minor: minor.read_int
128
+ )
129
+ end
130
+
131
+ def query_screens
132
+ count_ptr = ::FFI::MemoryPointer.new(:int)
133
+ screens_ptr = FFI.XineramaQueryScreens(@pointer, count_ptr)
134
+ count = count_ptr.read_int
135
+
136
+ return Enumerator.new { }.lazy if screens_ptr.null? || count.zero?
137
+
138
+ build_screens_enumerator(screens_ptr, count)
139
+ end
140
+
141
+ def build_screens_enumerator(screens_ptr, count)
142
+ Enumerator.new do |yielder|
143
+ begin
144
+ ScreenInfo.from_pointer(screens_ptr, count).each do |screen|
145
+ yielder << screen
146
+ end
147
+ ensure
148
+ FFI.XFree(screens_ptr) unless screens_ptr.null?
149
+ end
150
+ end.lazy
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xinerama
4
+ class Extension
5
+ attr_reader :event_base, :error_base
6
+
7
+ def initialize(event_base:, error_base:)
8
+ @event_base = event_base
9
+ @error_base = error_base
10
+ freeze
11
+ end
12
+
13
+ def to_h
14
+ { event_base: event_base, error_base: error_base }
15
+ end
16
+
17
+ def inspect
18
+ "#<#{self.class} event_base=#{event_base} error_base=#{error_base}>"
19
+ end
20
+
21
+ alias_method :to_s, :inspect
22
+ end
23
+
24
+ class Version
25
+ attr_reader :major, :minor
26
+
27
+ def initialize(major:, minor:)
28
+ @major = major
29
+ @minor = minor
30
+ freeze
31
+ end
32
+
33
+ def to_s
34
+ "#{major}.#{minor}"
35
+ end
36
+
37
+ def to_a
38
+ [major, minor]
39
+ end
40
+
41
+ def to_h
42
+ { major: major, minor: minor }
43
+ end
44
+
45
+ def inspect
46
+ "#<#{self.class} #{self}>"
47
+ end
48
+
49
+ def <=>(other)
50
+ [major, minor] <=> [other.major, other.minor]
51
+ end
52
+
53
+ include Comparable
54
+ end
55
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module Xinerama
6
+ module FFI
7
+ extend ::FFI::Library
8
+
9
+ ffi_lib "X11"
10
+ ffi_lib "Xinerama"
11
+
12
+ # XineramaScreenInfo struct
13
+ # typedef struct {
14
+ # int screen_number;
15
+ # short x_org;
16
+ # short y_org;
17
+ # short width;
18
+ # short height;
19
+ # } XineramaScreenInfo;
20
+ class XineramaScreenInfoStruct < ::FFI::Struct
21
+ layout :screen_number, :int,
22
+ :x_org, :short,
23
+ :y_org, :short,
24
+ :width, :short,
25
+ :height, :short
26
+ end
27
+
28
+ # Display* XOpenDisplay(const char *display_name)
29
+ attach_function :XOpenDisplay, [:string], :pointer
30
+
31
+ # int XCloseDisplay(Display *display)
32
+ attach_function :XCloseDisplay, [:pointer], :int
33
+
34
+ # int XFree(void *data)
35
+ attach_function :XFree, [:pointer], :int
36
+
37
+ # Bool XineramaQueryExtension(Display *dpy, int *event_base_return, int *error_base_return)
38
+ attach_function :XineramaQueryExtension, [:pointer, :pointer, :pointer], :bool
39
+
40
+ # Status XineramaQueryVersion(Display *dpy, int *major_version_return, int *minor_version_return)
41
+ attach_function :XineramaQueryVersion, [:pointer, :pointer, :pointer], :int
42
+
43
+ # Bool XineramaIsActive(Display *dpy)
44
+ attach_function :XineramaIsActive, [:pointer], :bool
45
+
46
+ # XineramaScreenInfo* XineramaQueryScreens(Display *dpy, int *number)
47
+ attach_function :XineramaQueryScreens, [:pointer, :pointer], :pointer
48
+ end
49
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xinerama
4
+ class ScreenInfo
5
+ attr_reader :screen_number, :x, :y, :width, :height
6
+
7
+ def initialize(screen_number:, x:, y:, width:, height:)
8
+ @screen_number = screen_number
9
+ @x = x
10
+ @y = y
11
+ @width = width
12
+ @height = height
13
+ freeze
14
+ end
15
+
16
+ def origin
17
+ [x, y]
18
+ end
19
+
20
+ def dimensions
21
+ [width, height]
22
+ end
23
+
24
+ def area
25
+ width * height
26
+ end
27
+
28
+ def aspect_ratio
29
+ Rational(width, height)
30
+ end
31
+
32
+ def contains?(point_x, point_y)
33
+ point_x >= x && point_x < (x + width) &&
34
+ point_y >= y && point_y < (y + height)
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ screen_number: screen_number,
40
+ x: x,
41
+ y: y,
42
+ width: width,
43
+ height: height
44
+ }
45
+ end
46
+
47
+ def to_a
48
+ [screen_number, x, y, width, height]
49
+ end
50
+
51
+ def inspect
52
+ "#<#{self.class} screen=#{screen_number} geometry=#{width}x#{height}+#{x}+#{y}>"
53
+ end
54
+
55
+ alias_method :to_s, :inspect
56
+
57
+ def ==(other)
58
+ other.is_a?(ScreenInfo) &&
59
+ screen_number == other.screen_number &&
60
+ x == other.x &&
61
+ y == other.y &&
62
+ width == other.width &&
63
+ height == other.height
64
+ end
65
+
66
+ alias_method :eql?, :==
67
+
68
+ def hash
69
+ to_a.hash
70
+ end
71
+
72
+ class << self
73
+ def from_struct(struct)
74
+ new(
75
+ screen_number: struct[:screen_number],
76
+ x: struct[:x_org],
77
+ y: struct[:y_org],
78
+ width: struct[:width],
79
+ height: struct[:height]
80
+ )
81
+ end
82
+
83
+ def from_pointer(pointer, count)
84
+ Enumerator.new do |yielder|
85
+ count.times do |index|
86
+ offset = index * FFI::XineramaScreenInfoStruct.size
87
+ struct = FFI::XineramaScreenInfoStruct.new(pointer + offset)
88
+ yielder << from_struct(struct)
89
+ end
90
+ end.lazy
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xinerama
4
+ VERSION = "0.1.0"
5
+ end
data/lib/xinerama.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "xinerama/version"
4
+ require_relative "xinerama/ffi"
5
+ require_relative "xinerama/screen_info"
6
+ require_relative "xinerama/display"
7
+ require_relative "xinerama/extension"
8
+
9
+ module Xinerama
10
+ class Error < StandardError; end
11
+ class NotAvailableError < Error; end
12
+ class DisplayError < Error; end
13
+
14
+ class << self
15
+ def open(display_name = nil, &block)
16
+ Display.open(display_name, &block)
17
+ end
18
+
19
+ def screens(display_name = nil)
20
+ Display.open(display_name, &:screens)
21
+ end
22
+
23
+ def active?(display_name = nil)
24
+ Display.open(display_name, &:active?)
25
+ end
26
+
27
+ def version(display_name = nil)
28
+ Display.open(display_name, &:version)
29
+ end
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xinerama
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nathan
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ffi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.15'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.15'
26
+ description: Provides a clean Ruby interface to query Xinerama screen information
27
+ for multi-monitor X11 setups
28
+ email:
29
+ - nathankidd@hey.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE.txt
35
+ - README.md
36
+ - lib/xinerama.rb
37
+ - lib/xinerama/display.rb
38
+ - lib/xinerama/extension.rb
39
+ - lib/xinerama/ffi.rb
40
+ - lib/xinerama/screen_info.rb
41
+ - lib/xinerama/version.rb
42
+ homepage: https://github.com/n-at-han-k/ruby-xinerama
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ homepage_uri: https://github.com/n-at-han-k/ruby-xinerama
47
+ source_code_uri: https://github.com/n-at-han-k/ruby-xinerama
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 3.0.0
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.6.7
63
+ specification_version: 4
64
+ summary: Ruby FFI bindings for the Xinerama X11 extension
65
+ test_files: []