Package pywingo
[frames] | no frames]

Source Code for Package pywingo

  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   
21 -class WingoError(Exception):
22 - def __init__(self, message):
23 self.message = message
24
25 - def __str__(self):
26 return self.message
27 28
29 -class Disconnected(Exception):
30 - def __init__(self):
31 pass
32
33 - def __str__(self):
34 return 'socket disconnected'
35 36
37 -class WingoUtil(WingoCommands):
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 '''
42 - def __init__(self):
43 assert False, 'cannot create WingoCommands directly'
44
45 - def GetAllNormalClients(self):
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
57 - def IsEmpty(self, Workspace):
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
67 - def GetVisibleWorkspaceList(self):
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
77 - def GetHiddenWorkspaceList(self):
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
88 - def LoadConfig(self):
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
110 -class Wingo(WingoUtil):
111 - def __init__(self, display=None):
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 # Not opened until the first command is issued. 136 self.__sock = None 137 138 # Not opened until the event loop is started. 139 self.__evsock = None 140 141 self.__path = _socket_filepath(display)
142
143 - def __del__(self):
144 if self.__sock is not None: 145 self.__sock.close()
146
147 - def __reconnect(self):
148 self.__sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 149 self.__sock.connect(self.__path)
150
151 - def __recv(self):
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
163 - def __recv_event(self):
164 assert self.__evsock is not None 165 166 # So far this is the same as `__recv`, but they use different 167 # buffers and could potentially use different protocols. 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
179 - def gribble(self, cmd):
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 # Try again, just once. 190 self.__send_cmd(cmd) 191 return self.__recv()
192
193 - def __send_cmd(self, cmd):
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
201 - def _assert_arg_type(self, name, val, types):
202 for t in types: 203 if isinstance(val, t): 204 return 205 assert False, '%s has invalid type %s' % (name, type(val))
206
207 - def _gribble_arg_str(self, vals):
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
218 - def _escape_str(self, s):
219 return s.replace('"', '\\"')
220
221 - def _from_str(self, cmd_name, s):
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
248 - def _primitive_from_str(self, cmd_name, s):
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
265 - def __wingo_restarting(self, ev):
266 if self.__sock is not None: 267 self.__sock.close() 268 self.__sock = None
269
270 - def loop(self, restart=True):
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 # Invalidate the Gribble socket once Wingo starts restarting. 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
329 -def _socket_filepath(display=None):
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