1# This file is part of ReText
2# Copyright: 2015-2017 Dmitry Shachnev
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, either version 2 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17# This is implementation of XSettings specification, described at
18# <http://standards.freedesktop.org/xsettings-spec/xsettings-spec-0.5.html>
19
20import ctypes
21import ctypes.util
22import struct
23
24class _xcb_reply_t(ctypes.Structure):
25	# this can be used instead of xcb_intern_atom_reply_t,
26	# xcb_get_selection_owner_reply_t, etc
27	_fields_ = [('response_type', ctypes.c_uint8),
28	            ('pad0',          ctypes.c_uint8),
29	            ('sequence',      ctypes.c_uint16),
30	            ('length',        ctypes.c_uint32),
31	            ('payload',       ctypes.c_uint32)]
32
33class _xcb_cookie_t(ctypes.Structure):
34	# this can be used instead of xcb_intern_atom_cookie_t,
35	# xcb_get_selection_owner_cookie_t, etc
36	_fields_ = [('sequence',      ctypes.c_uint)]
37
38_xcb_error_messages = [
39	None,
40	'XCB error: socket, pipe and other stream error',
41	'XCB connection closed: extension unsupported',
42	'XCB connection closed: insufficient memory',
43	'XCB connection closed: request length exceeded',
44	'XCB connection closed: DISPLAY parse error',
45	'XCB connection closed: invalid screen'
46]
47
48class XSettingsError(RuntimeError):
49	pass
50
51class XSettingsParseError(XSettingsError):
52	pass
53
54def get_raw_xsettings(display=0):
55	# initialize the libraries
56	xcb_library_name = ctypes.util.find_library('xcb')
57	if xcb_library_name is None:
58		raise XSettingsError('Xcb library not found')
59	xcb = ctypes.CDLL(xcb_library_name)
60
61	c_library_name = ctypes.util.find_library('c')
62	if c_library_name is None:
63		raise XSettingsError('C library not found')
64	c = ctypes.CDLL(c_library_name)
65
66	# set some args and return types
67	c.free.argtypes = [ctypes.c_void_p]
68	c.free.restype = None
69	xcb.xcb_connect.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_int)]
70	xcb.xcb_connect.restype = ctypes.c_void_p
71	xcb.xcb_connection_has_error.argtypes = [ctypes.c_void_p]
72	xcb.xcb_connection_has_error.restype = ctypes.c_int
73	xcb.xcb_disconnect.argtypes = [ctypes.c_void_p]
74	xcb.xcb_disconnect.restype = None
75	xcb.xcb_intern_atom.argtypes = [ctypes.c_void_p, ctypes.c_uint8, ctypes.c_uint16, ctypes.c_char_p]
76	xcb.xcb_intern_atom.restype = _xcb_cookie_t
77	xcb.xcb_intern_atom_reply.argtypes = [ctypes.c_void_p, _xcb_cookie_t, ctypes.c_void_p]
78	xcb.xcb_intern_atom_reply.restype = ctypes.POINTER(_xcb_reply_t)
79	xcb.xcb_get_selection_owner.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
80	xcb.xcb_get_selection_owner.restype = _xcb_cookie_t
81	xcb.xcb_get_selection_owner_reply.argtypes = [ctypes.c_void_p, _xcb_cookie_t, ctypes.c_void_p]
82	xcb.xcb_get_selection_owner_reply.restype = ctypes.POINTER(_xcb_reply_t)
83	xcb.xcb_get_property.argtypes = [ctypes.c_void_p, ctypes.c_uint8, ctypes.c_uint32, ctypes.c_uint32,
84	                                 ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32]
85	xcb.xcb_get_property.restype = _xcb_cookie_t
86	xcb.xcb_get_property_reply.argtypes = [ctypes.c_void_p, _xcb_cookie_t, ctypes.c_void_p]
87	xcb.xcb_get_property_reply.restype = ctypes.c_void_p
88	xcb.xcb_get_property_value_length.argtypes = [ctypes.c_void_p]
89	xcb.xcb_get_property_value_length.restype = ctypes.c_int
90	xcb.xcb_get_property_value.argtypes = [ctypes.c_void_p]
91	xcb.xcb_get_property_value.restype = ctypes.c_void_p
92
93	# open the connection
94	connection = xcb.xcb_connect(None, None)
95	error = xcb.xcb_connection_has_error(connection)
96	if error:
97		raise XSettingsError(_xcb_error_messages[error])
98
99	# get selection atom cookie
100	buffer = ('_XSETTINGS_S%d' % display).encode()
101	cookie = xcb.xcb_intern_atom(connection, 0, len(buffer), buffer)
102
103	# get selection atom reply
104	reply = xcb.xcb_intern_atom_reply(connection, cookie, None)
105	selection_atom = reply.contents.payload
106	c.free(reply)
107
108	# get selection owner cookie
109	cookie = xcb.xcb_get_selection_owner(connection, selection_atom)
110
111	# get selection owner reply
112	reply = xcb.xcb_get_selection_owner_reply(connection, cookie, None)
113	window = reply.contents.payload
114	c.free(reply)
115
116	# get settings atom cookie
117	buffer = b'_XSETTINGS_SETTINGS'
118	cookie = xcb.xcb_intern_atom(connection, 0, len(buffer), buffer)
119
120	# get settings atom reply
121	reply = xcb.xcb_intern_atom_reply(connection, cookie, None)
122	settings_atom = reply.contents.payload
123	c.free(reply)
124
125	# get property cookie
126	cookie = xcb.xcb_get_property(connection, 0, window, settings_atom, 0, 0, 0x2000)
127
128	# get property reply
129	reply = xcb.xcb_get_property_reply(connection, cookie, None)
130	if reply is not None:
131		length = xcb.xcb_get_property_value_length(reply)
132		pointer = xcb.xcb_get_property_value(reply) if length else None
133		result = ctypes.string_at(pointer, length)
134		c.free(reply)
135
136	# close the connection
137	xcb.xcb_disconnect(connection)
138
139	# handle possible errors
140	if reply is None or not length:
141		raise XSettingsError('XSettings not available')
142
143	return result
144
145def parse_xsettings(raw_xsettings):
146	if len(raw_xsettings) < 12:
147		raise XSettingsParseError('length < 12')
148
149	if raw_xsettings[0] not in (0, 1):
150		raise XSettingsParseError('wrong order byte: %d' % raw_xsettings[0])
151	byte_order = '<>'[raw_xsettings[0]]
152	settings_count = struct.unpack(byte_order + 'I', raw_xsettings[8:12])[0]
153
154	TypeInteger, TypeString, TypeColor = range(3)
155	result = {}
156
157	raw_xsettings = raw_xsettings[12:]
158	offset = 0
159	for i in range(settings_count):
160		setting_type = raw_xsettings[offset]
161		offset += 2
162		name_length = struct.unpack(byte_order + 'H', raw_xsettings[offset:offset + 2])[0]
163		offset += 2
164		name = raw_xsettings[offset:offset + name_length]
165		offset += name_length
166		if offset & 3:
167			offset += 4 - (offset & 3)
168		offset += 4     # skip last-change-serial
169
170		if setting_type == TypeInteger:
171			value = struct.unpack(byte_order + 'I', raw_xsettings[offset:offset + 4])[0]
172			offset += 4
173		elif setting_type == TypeString:
174			value_length = struct.unpack(byte_order + 'I', raw_xsettings[offset:offset + 4])[0]
175			offset += 4
176			value = raw_xsettings[offset:offset + value_length]
177			offset += value_length
178			if offset & 3:
179				offset += 4 - (offset & 3)
180		elif setting_type == TypeColor:
181			value = struct.unpack(byte_order + 'HHHH', raw_xsettings[offset:offset + 8])
182			offset += 8
183		else:
184			raise XSettingsParseError('Wrong setting type: %d' % setting_type)
185		result[name] = value
186	return result
187
188def get_xsettings(display=0):
189	raw_xsettings = get_raw_xsettings(display)
190	return parse_xsettings(raw_xsettings)
191