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