mirror of
https://github.com/vidarh/ruby-x11
synced 2025-01-13 20:01:22 +01:00
3fa4211f06
This is still "rough", but it works.
450 lines
12 KiB
Ruby
450 lines
12 KiB
Ruby
# FIXME: Temp workaround
|
|
require 'stringio'
|
|
|
|
module X11
|
|
|
|
class DisplayError < X11Error; end
|
|
class ConnectionError < X11Error; end
|
|
class AuthorizationError < X11Error; end
|
|
class ProtocolError < X11Error; end
|
|
|
|
class Display
|
|
attr_accessor :socket
|
|
|
|
# Open a connection to the specified display (numbered from 0) on the specified host
|
|
def initialize(target = ENV['DISPLAY'])
|
|
target =~ /^([\w.-]*):(\d+)(?:.(\d+))?$/
|
|
host, display_id, screen_id = $1, $2, $3
|
|
family = nil
|
|
|
|
if host.empty?
|
|
@socket = UNIXSocket.new("/tmp/.X11-unix/X#{display_id}")
|
|
family = :Local
|
|
host = nil
|
|
else
|
|
@socket = TCPSocket.new(host,6000+display_id)
|
|
family = :Internet
|
|
end
|
|
|
|
authorize(host, family, display_id)
|
|
|
|
@requestseq = 1
|
|
@queue = []
|
|
|
|
@extensions = {}
|
|
|
|
# Interned atoms
|
|
@atoms = {}
|
|
end
|
|
|
|
def event_handler= block
|
|
@event_handler= block
|
|
end
|
|
|
|
def display_info
|
|
@internal
|
|
end
|
|
|
|
def screens
|
|
@internal.screens.map do |s|
|
|
Screen.new(self, s)
|
|
end
|
|
end
|
|
|
|
##
|
|
# The resource-id-mask contains a single contiguous set of bits (at least 18).
|
|
# The client allocates resource IDs for types WINDOW, PIXMAP, CURSOR, FONT,
|
|
# GCONTEXT, and COLORMAP by choosing a value with only some subset of these
|
|
# bits set and ORing it with resource-id-base.
|
|
|
|
def new_id
|
|
id = (@xid_next ||= 0)
|
|
@xid_next += 1
|
|
|
|
(id & @internal.resource_id_mask) | @internal.resource_id_base
|
|
end
|
|
|
|
def read_error data
|
|
error = Form::Error.from_packet(StringIO.new(data))
|
|
STDERR.puts "ERROR: #{error.inspect}"
|
|
error
|
|
end
|
|
|
|
def read_reply data
|
|
len = data.unpack("@4L")[0]
|
|
extra = len > 0 ? @socket.read(len*4) : ""
|
|
#STDERR.puts "REPLY: #{data.inspect}"
|
|
#STDERR.puts "EXTRA: #{extra.inspect}"
|
|
data + extra
|
|
end
|
|
|
|
def read_event type, data, event_class
|
|
case type
|
|
when 2
|
|
return Form::KeyPress.from_packet(StringIO.new(data))
|
|
when 3
|
|
return Form::KeyRelease.from_packet(StringIO.new(data))
|
|
when 4
|
|
return Form::ButtonPress.from_packet(StringIO.new(data))
|
|
when 6
|
|
return Form::MotionNotify.from_packet(StringIO.new(data))
|
|
when 12
|
|
return Form::Expose.from_packet(StringIO.new(data))
|
|
when 14
|
|
return Form::NoExposure.from_packet(StringIO.new(data))
|
|
when 19
|
|
return Form::MapNotify.from_packet(StringIO.new(data))
|
|
when 22
|
|
return Form::ConfigureNotify.from_packet(StringIO.new(data))
|
|
else
|
|
STDERR.puts "FIXME: Event: #{type}"
|
|
STDERR.puts "EVENT: #{data.inspect}"
|
|
end
|
|
end
|
|
|
|
def read_full_packet(len = 32)
|
|
data = @socket.read_nonblock(32)
|
|
return nil if data.nil?
|
|
while data.length < 32
|
|
IO.select([@socket],nil,nil,0.001)
|
|
data.concat(@socket.read_nonblock(32 - data.length))
|
|
end
|
|
return data
|
|
rescue IO::WaitReadable
|
|
return nil
|
|
end
|
|
|
|
def read_packet timeout=5.0
|
|
IO.select([@socket],nil,nil, timeout)
|
|
data = read_full_packet(32)
|
|
return nil if data.nil?
|
|
|
|
type = data.unpack("C").first
|
|
case type
|
|
when 0
|
|
read_error(data)
|
|
when 1
|
|
read_reply(data)
|
|
when 2..34
|
|
read_event(type, data, nil)
|
|
else
|
|
raise ProtocolError, "Unsupported reply type: #{type}"
|
|
end
|
|
end
|
|
|
|
def write_request ob
|
|
#p data
|
|
#p [:write_request, @requestseq, ob.class]
|
|
data = ob.to_packet if ob.respond_to?(:to_packet)
|
|
#p [:AddGlyph,data] if ob.is_a?(X11::Form::XRenderAddGlyphs)
|
|
#p [ob.request_length.to_i*4, data.size]
|
|
raise "BAD LENGTH for #{ob.inspect} (#{ob.request_length.to_i*4} ! #{data.size} " if ob.request_length && ob.request_length.to_i*4 != data.size
|
|
@requestseq += 1
|
|
@socket.write(data)
|
|
end
|
|
|
|
def write_sync(data, reply=nil)
|
|
write_request(data)
|
|
pkt = next_reply
|
|
return nil if !pkt
|
|
reply ? reply.from_packet(StringIO.new(pkt)) : pkt
|
|
end
|
|
|
|
def peek_packet
|
|
!@queue.empty?
|
|
end
|
|
|
|
def next_packet
|
|
@queue.shift || read_packet
|
|
end
|
|
|
|
def next_reply
|
|
# FIXME: This is totally broken
|
|
while pkt = read_packet
|
|
if pkt.is_a?(String)
|
|
return pkt
|
|
else
|
|
@queue.push(pkt)
|
|
end
|
|
end
|
|
end
|
|
|
|
def run
|
|
loop do
|
|
pkt = read_packet
|
|
return if !pkt
|
|
yield(pkt)
|
|
end
|
|
end
|
|
|
|
# Requests
|
|
def create_window(*args)
|
|
write_request(X11::Form::CreateWindow.new(*args))
|
|
end
|
|
|
|
def atom(name)
|
|
intern_atom(false, name) if !@atoms[name]
|
|
@atoms[name]
|
|
end
|
|
|
|
def query_extension(name)
|
|
r = write_sync(X11::Form::QueryExtension.new(name), X11::Form::QueryExtensionReply)
|
|
@extensions[name] = {
|
|
major: r.major_opcode
|
|
}
|
|
r
|
|
end
|
|
|
|
def major_opcode(name)
|
|
if !@extensions[name]
|
|
query_extension(name)
|
|
end
|
|
raise "No such extension '#{name}'" if !@extensions[name]
|
|
@extensions[name][:major]
|
|
end
|
|
|
|
def intern_atom(flag, name)
|
|
reply = write_sync(X11::Form::InternAtom.new(flag, name.to_s),
|
|
X11::Form::InternAtomReply)
|
|
if reply
|
|
@atoms[name.to_sym] = reply.atom
|
|
end
|
|
end
|
|
|
|
def get_keyboard_mapping(min_keycode=display_info.min_keycode, count= display_info.max_keycode - min_keycode)
|
|
write_sync(X11::Form::GetKeyboardMapping.new(min_keycode, count), X11::Form::GetKeyboardMappingReply)
|
|
end
|
|
|
|
def create_colormap(alloc, window, visual)
|
|
mid = new_id
|
|
write_request(X11::Form::CreateColormap.new(alloc, mid, window, visual))
|
|
mid
|
|
end
|
|
|
|
def change_property(*args)
|
|
write_request(X11::Form::ChangeProperty.new(*args))
|
|
end
|
|
|
|
def list_fonts(*args)
|
|
write_sync(X11::Form::ListFonts.new(*args),
|
|
X11::Form::ListFontsReply)
|
|
end
|
|
|
|
def open_font(*args)
|
|
write_request(X11::Form::OpenFont.new(*args))
|
|
end
|
|
|
|
def change_gc(*args)
|
|
write_request(X11::Form::ChangeGC.new(*args))
|
|
end
|
|
|
|
def map_window(*args)
|
|
write_request(X11::Form::MapWindow.new(*args))
|
|
end
|
|
|
|
|
|
def create_gc(window, foreground: nil, background: nil)
|
|
mask = 0
|
|
args = []
|
|
|
|
# FIXME:
|
|
# The rest can be found here:
|
|
# https://tronche.com/gui/x/xlib/GC/manipulating.html#XGCValues
|
|
if foreground
|
|
mask |= 0x04
|
|
args << foreground
|
|
end
|
|
if background
|
|
mask |= 0x08
|
|
args << background
|
|
end
|
|
|
|
|
|
gc = new_id
|
|
write_request(X11::Form::CreateGC.new(gc, window, mask, args))
|
|
gc
|
|
end
|
|
|
|
def put_image(*args)
|
|
write_request(X11::Form::PutImage.new(*args))
|
|
end
|
|
|
|
def clear_area(*args)
|
|
write_request(X11::Form::ClearArea.new(*args))
|
|
end
|
|
|
|
def copy_area(*args)
|
|
write_request(X11::Form::CopyArea.new(*args))
|
|
end
|
|
|
|
def image_text8(*args)
|
|
write_request(X11::Form::ImageText8.new(*args))
|
|
end
|
|
|
|
def image_text16(*args)
|
|
write_request(X11::Form::ImageText16.new(*args))
|
|
end
|
|
|
|
def poly_fill_rectangle(*args)
|
|
write_request(X11::Form::PolyFillRectangle.new(*args))
|
|
end
|
|
|
|
def create_pixmap(depth, drawable, w,h)
|
|
pid = new_id
|
|
write_request(X11::Form::CreatePixmap.new(depth, pid, drawable, w,h))
|
|
pid
|
|
end
|
|
|
|
# XRender
|
|
|
|
def render_opcode
|
|
return @render_opcode if @render_opcode
|
|
@render_opcode = major_opcode("RENDER")
|
|
if @render_opcode
|
|
@render_version = write_sync(X11::Form::XRenderQueryVersion.new(
|
|
@render_opcode,0,11),
|
|
X11::Form::XRenderQueryVersionReply
|
|
)
|
|
end
|
|
@render_opcode
|
|
end
|
|
|
|
def render_create_picture(drawable, format, vmask=0, vlist=[])
|
|
pid = new_id
|
|
write_request(X11::Form::XRenderCreatePicture.new(
|
|
render_opcode, pid, drawable, format, vmask, vlist))
|
|
pid
|
|
end
|
|
|
|
def render_query_pict_formats
|
|
@render_formats ||= write_sync(
|
|
X11::Form::XRenderQueryPictFormats.new(render_opcode),
|
|
X11::Form::XRenderQueryPictFormatsReply
|
|
)
|
|
end
|
|
|
|
def render_find_visual_format(visual)
|
|
# FIXME.
|
|
render_query_pict_formats.screens.map do |s|
|
|
s.depths.map do |d|
|
|
d.visuals.map {|v| v.visual == visual ? v : nil }
|
|
end
|
|
end.flatten.compact.first.format
|
|
end
|
|
|
|
def render_find_standard_format(sym)
|
|
# A pox be on the people who made this necessary
|
|
|
|
formats = render_query_pict_formats
|
|
|
|
case sym
|
|
when :a8
|
|
@a8 ||= formats.formats.find do |f|
|
|
f.type == 1 &&
|
|
f.depth == 8 &&
|
|
f.direct.alpha_mask == 255
|
|
end
|
|
when :rgb24
|
|
@rgb24 ||= formats.formats.find do |f|
|
|
f.type == 1 &&
|
|
f.depth == 24 &&
|
|
f.direct.red == 16 &&
|
|
f.direct.green == 8 &&
|
|
f.direct.blue == 0
|
|
end
|
|
when :argb24
|
|
@argb24 ||= formats.formats.find do |f|
|
|
f.type == 1 &&
|
|
f.depth == 32 &&
|
|
f.direct.alpha == 24 &&
|
|
f.direct.red == 16 &&
|
|
f.direct.green == 8 &&
|
|
f.direct.blue == 0
|
|
end
|
|
else
|
|
raise "Unsupported format (a4/a1 by omission)"
|
|
end
|
|
end
|
|
|
|
def render_create_glyph_set(format)
|
|
glyphset = new_id
|
|
write_request(X11::Form::XRenderCreateGlyphSet.new(
|
|
major_opcode("RENDER"),glyphset, format))
|
|
glyphset
|
|
end
|
|
|
|
def render_add_glyphs(glyphset, glyphids, glyphinfos, data)
|
|
write_request(X11::Form::XRenderAddGlyphs.new(render_opcode,
|
|
glyphset, Array(glyphids), Array(glyphinfos), data))
|
|
end
|
|
|
|
def render_fill_rectangles(op, dst, color, rects)
|
|
color = Form::XRenderColor.new(*color) if color.is_a?(Array)
|
|
rects = rects.map{|r| r.is_a?(Array) ? Form::Rectangle.new(*r) : r}
|
|
write_request(Form::XRenderFillRectangles.new(render_opcode, op, dst, color, rects))
|
|
end
|
|
|
|
def render_composite_glyphs32(op, src, dst, fmt, glyphset, srcx,srcy, *elts)
|
|
write_request(X11::Form::XRenderCompositeGlyphs32.new(
|
|
render_opcode,
|
|
op, src, dst, fmt,
|
|
glyphset,
|
|
srcx, srcy,
|
|
elts.map {|e| e.is_a?(Array) ? Form::GlyphElt32.new(*e) : e }
|
|
))
|
|
end
|
|
|
|
def render_create_solid_fill(*color)
|
|
if color.length == 1 && color.is_a?(Form::XRenderColor)
|
|
color = color[0]
|
|
else
|
|
color = Form::XRenderColor.new(*color)
|
|
end
|
|
fill = new_id
|
|
write_request(Form::XRenderCreateSolidFill.new(render_opcode,fill,color))
|
|
fill
|
|
end
|
|
|
|
private
|
|
|
|
def authorize(host, family, display_id)
|
|
auth = Auth.new
|
|
auth_info = auth.get_by_hostname(host||"localhost", family, display_id)
|
|
|
|
auth_name, auth_data = auth_info.auth_name, auth_info.auth_data
|
|
p [auth_name, auth_data]
|
|
|
|
handshake = Form::ClientHandshake.new(
|
|
Protocol::BYTE_ORDER,
|
|
Protocol::MAJOR,
|
|
Protocol::MINOR,
|
|
auth_name,
|
|
auth_data
|
|
)
|
|
|
|
@socket.write(handshake.to_packet)
|
|
|
|
data = @socket.read(1)
|
|
raise AuthorizationError, "Failed to read response from server" if !data
|
|
|
|
case data.unpack("w").first
|
|
when X11::Auth::FAILED
|
|
len, major, minor, xlen = @socket.read(7).unpack("CSSS")
|
|
reason = @socket.read(xlen * 4)
|
|
reason = reason[0..len]
|
|
raise AuthorizationError, "Connection to server failed -- (version #{major}.#{minor}) #{reason}"
|
|
when X11::Auth::AUTHENTICATE
|
|
raise AuthorizationError, "Connection requires authentication"
|
|
when X11::Auth::SUCCESS
|
|
@socket.read(7) # skip unused bytes
|
|
@internal = Form::DisplayInfo.from_packet(@socket)
|
|
else
|
|
raise AuthorizationError, "Received unknown opcode #{type}"
|
|
end
|
|
end
|
|
|
|
def to_s
|
|
"#<X11::Display:0x#{object_id.to_s(16)} screens=#{@internal.screens.size}>"
|
|
end
|
|
end
|
|
end
|