1 import json
2 import os
3 import os.path
4 import socket
5 import subprocess
6 import sys
7 import tempfile
8 import time
9
10 from pywingo.commands import WingoCommands
11 import pywingo.events as events
12
13 _bool_cmds = ['True', 'False', 'Not', 'And', 'Or']
14 _string_cmds = ['GetWorkspace', 'GetWorkspaceList',
15 'GetWorkspaceNext', 'GetWorkspacePrev', 'GetWorkspacePrefix',
16 'GetHeadWorkspace', 'GetClientWorkspace']
17 _list_cmds = ['GetAllClients']
18
19
22 self.message = message
23
26
27
31
33 return 'socket disconnected'
34
35
37 '''
38 Provides a set of utility functions on top of the base commands
39 defined by Wingo. These are special to the Python Wingo bindings.
40 '''
42 assert False, 'cannot create WingoCommands directly'
43
45 '''
46 Exactly the same as GetAllClients, except only clients with
47 type "normal" are returned. (i.e., excludes "desktop" and
48 "dock" clients.)
49 '''
50 cids = []
51 for cid in self.GetAllClients():
52 if self.GetClientType(cid) == 'normal':
53 cids.append(cid)
54 return cids
55
57 '''
58 Returns true if the given Workspace has no clients. (Including
59 iconified clients.)
60
61 Workspace may be a workspace index (integer) starting at 0, or a
62 workspace name.
63 '''
64 return len(self.GetClientList(Workspace)) == 0
65
67 '''
68 Returns a list of all visible workspaces in order of their
69 physical position: left to right and then top to bottom.
70 '''
71 spaces = []
72 for i in xrange(self.GetNumHeads()):
73 spaces.append(self.GetHeadWorkspace(i))
74 return spaces
75
77 '''
78 Returns a list of all hidden workspaces.
79 '''
80 spaces = []
81 visibles = set(self.GetVisibleWorkspaceList())
82 for space in self.GetWorkspaceList():
83 if space not in visibles:
84 spaces.append(space)
85 return spaces
86
87
90 '''
91 Initializes a connection with an instance of Wingo.
92 Once a connection has been established, commands can be
93 executed.
94
95 If `display` is not set, then pywingo will try to find the
96 current instance of Wingo and connect to that. This is almost
97 always what you want, unless you know you need to connect to
98 an instance of Wingo from within a different X session (or no
99 X session at all).
100
101 If `display` is set, then it *must* be in the following format:
102
103 :{X Server}.{X Screen}
104
105 e.g., `:0.0` or `:11.1`.
106
107 Any other format is invalid.
108 '''
109 self.__buf = ''
110 self.__evbuf = ''
111 self.__callbacks = {}
112
113
114 self.__sock = None
115
116
117 self.__evsock = None
118
119 self.__path = _socket_filepath(display)
120
122 if self.__sock is not None:
123 self.__sock.close()
124
126 self.__sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
127 self.__sock.connect(self.__path)
128
130 while chr(0) not in self.__buf:
131 data = self.__sock.recv(4096)
132 if not data:
133 raise Disconnected
134 self.__buf += data
135
136 sentinel = self.__buf.index(chr(0))
137 payload = self.__buf[0:sentinel][:]
138 self.__buf = self.__buf[sentinel+1:]
139 return payload
140
142 assert self.__evsock is not None
143
144
145
146 while chr(0) not in self.__evbuf:
147 data = self.__evsock.recv(4096)
148 if not data:
149 raise Disconnected
150 self.__evbuf += data
151
152 sentinel = self.__evbuf.index(chr(0))
153 payload = self.__evbuf[0:sentinel][:]
154 self.__evbuf = self.__evbuf[sentinel+1:]
155 return payload
156
158 '''
159 Executes a raw Gribble commands and always returns the result
160 as a string. This should only be used if you know it's necessary.
161 Otherwise, use the API to run specific commands.
162 '''
163 try:
164 self.__send_cmd(cmd)
165 return self.__recv()
166 except Disconnected:
167
168 self.__send_cmd(cmd)
169 return self.__recv()
170
172 if self.__sock is None:
173 self.__reconnect()
174 try:
175 self.__sock.send('%s%s' % (cmd, chr(0)))
176 except:
177 raise Disconnected
178
180 for t in types:
181 if isinstance(val, t):
182 return
183 assert False, '%s has invalid type %s' % (name, type(val))
184
186 args = []
187 for v in vals:
188 if isinstance(v, int) or isinstance(v, float):
189 args.append(repr(v))
190 elif isinstance(v, basestring):
191 args.append('"%s"' % self._escape_str(v))
192 else:
193 assert False, 'bug'
194 return ' '.join(args)
195
197 return s.replace('"', '\\"')
198
200 if cmd_name in _bool_cmds or cmd_name.startswith('Match'):
201 return bool(int(s))
202
203 if 'List' in cmd_name or '\n' in s or cmd_name in _list_cmds:
204 trimmed = s.strip()
205 if len(trimmed) == 0:
206 return []
207 return map(lambda item: self._primitive_from_str(cmd_name, item),
208 trimmed.split('\n'))
209
210 if cmd_name in _string_cmds:
211 return s
212
213 try:
214 return int(s)
215 except ValueError:
216 try:
217 return float(s)
218 except ValueError:
219 if s.startswith('ERROR:'):
220 raise WingoError(s)
221 else:
222 return s
223
224 assert False, 'bug'
225
227 if cmd_name in _bool_cmds or cmd_name.startswith('Match'):
228 return bool(int(s))
229
230 if cmd_name in _string_cmds:
231 return s
232
233 try:
234 return int(s)
235 except ValueError:
236 try:
237 return float(s)
238 except ValueError:
239 return s
240
241 assert False, 'bug'
242
244 if self.__sock is not None:
245 self.__sock.close()
246 self.__sock = None
247
249 '''
250 Listens for event notifications and executes callbacks when
251 corresponding events are received.
252
253 When `restart` is enabled, the event loop will be restarted if there
254 was an error reading from the socket. This is intended to keep the
255 program alive if Wingo restarts. (If Wingo really does die, the
256 reconnection will fail and a regular socket error will be raised.)
257 '''
258
259
260 self.bind('Restarting', self.__wingo_restarting)
261
262 try:
263 self.__evsock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
264 f = os.path.join(self.__path + '-notify')
265 self.__evsock.connect(f)
266
267 while True:
268 evJson = self.__recv_event()
269 j = json.loads(evJson)
270 ev_name = str(j['EventName'])
271 key = '_new_%s' % ev_name
272 if key not in events.__dict__:
273 print >> sys.stderr, \
274 'Event "%s" is not defined in pywingo. ' \
275 'Time to update!' % ev_name
276 continue
277 ev = events.__dict__[key](j)
278
279 for cb in self.__callbacks.get(ev_name, []):
280 cb(ev)
281 except Disconnected:
282 if not restart:
283 raise Disconnected
284 time.sleep(1)
285 self.loop(restart)
286
287 - def bind(self, event_name, f=None):
288 '''
289 Binds an event named `event_name` to a callback function `f`.
290 `f` should be a function that takes a single argument `event`,
291 which will correspond to a namedtuple of the event with any
292 relevant data as properties.
293
294 If `f` is None, then a partially applied function is returned.
295 (For decorator support.)
296 '''
297 def doit(fun):
298 if event_name not in self.__callbacks:
299 self.__callbacks[event_name] = []
300 self.__callbacks[event_name].append(fun)
301 return fun
302
303 if f is None:
304 return doit
305 return doit(f)
306
308 if display is not None:
309 rundir = os.getenv('XDG_RUNTIME_DIR').strip()
310 if len(rundir) == 0:
311 rundir = tempfile.gettempdir()
312 return os.path.join(rundir, 'wingo', display)
313
314 try:
315 fp = subprocess.check_output(['wingo', '--show-socket'],
316 stderr=subprocess.STDOUT)
317 return fp.strip()
318 except subprocess.CalledProcessError, e:
319 raise WingoError(e.output)
320