1#!/usr/bin/env python 2# encoding: utf-8 3 4""" 5Bmpanel2 classes for python. 6 7 1. Theme/config parser/deparser (read/write). 8 2. Remote control, currently only one signal (reload config). 9 3. Themes operations (list, install, create bundle). 10 4. Config operations (all variables as a python interface, get/set, 11 etc). 12""" 13 14import sys, os 15 16#---------------------------------------------------------------------- 17# Config parser. 18# 19# This version may be slow, but it should do some extra stuff, like 20# keeping comments and formatting. It should be really friendly in that 21# kind of area. 22#---------------------------------------------------------------------- 23 24def _parse_indent(line): 25 """ 26 This function is used for parsing indents, it returns the 27 contents of an indent area and the count of indent symbols. 28 """ 29 offset = 0 30 contents = "" 31 for c in line: 32 if c.isspace(): 33 offset += 1 34 contents = contents + c 35 else: 36 break 37 38 return (contents, offset) 39 40# ConfigNode types 41CONFIG_NODE_NAME_VALUE = 1 42CONFIG_NODE_COMMENT = 2 43CONFIG_NODE_EMPTY = 3 44 45class ConfigNode: 46 def __init__(self, **kw): 47 """ 48 Init a node with initial values. Possible are: 49 - 'name': The name of the node. Or it also serves as 50 contents of the comment line, you should include '#' 51 symbol in that case too. 52 - 'value': The value of the node. 53 - 'type': The type - CONFIG_NODE_*, the default is 54 CONFIG_NODE_NAME_VALUE. 55 - 'parent': The parent of this node. 56 """ 57 self.name = None 58 if 'name' in kw: 59 self.name = kw['name'] 60 self.value = None 61 if 'value' in kw: 62 self.value = kw['value'] 63 self.type = CONFIG_NODE_NAME_VALUE 64 if 'type' in kw: 65 self.type = kw['type'] 66 self.parent = None 67 if 'parent' in kw: 68 self.parent = kw['parent'] 69 self.children = [] 70 self.indent_contents = "" 71 self.indent_offset = 0 72 self.children_offset = -1 73 74 def is_child(self, indent_offset): 75 """ 76 Using 'indent_offset' figures out is the node with that 77 offset is a child of this node. Also updates 78 'children_offset' implicitly. 79 """ 80 if self.children_offset != -1: 81 return self.children_offset == indent_offset 82 elif self.indent_offset < indent_offset: 83 self.children_offset = indent_offset 84 return True 85 return False 86 87 def parse(self, line): 88 """ 89 Parse a line of text to a node. 90 """ 91 # check empty line 92 sline = line.strip() 93 if sline == "": 94 self.type = CONFIG_NODE_EMPTY 95 return 96 97 # non-empties have indent, parse it 98 (indent_contents, indent_offset) = _parse_indent(line) 99 self.indent_contents = indent_contents 100 self.indent_offset = indent_offset 101 102 # check comment (first non-indent symbol: #) 103 if line[indent_offset] == "#": 104 self.type = CONFIG_NODE_COMMENT 105 self.name = line[indent_offset:] 106 return 107 108 # and finally try to parse name/value 109 self.type = CONFIG_NODE_NAME_VALUE 110 111 cur = indent_offset 112 113 # name 114 name_beg = cur 115 while cur < len(line) and not line[cur].isspace(): 116 cur += 1 117 name_end = cur 118 self.name = line[name_beg:name_end] 119 120 # value 121 value = line[name_end:].lstrip() 122 if value != "": 123 self.value = value 124 125 def make_child_of(self, parent): 126 """ 127 Make this node a child of the 'parent'. This method 128 doesn't add 'self' to the children list of the 'parent'. 129 You should do it manually. Updates 'children_offset' 130 implicitly. 131 """ 132 if parent.children_offset == -1: 133 parent.children_offset = parent.indent_offset + 1 134 co = parent.children_offset 135 self.indent_contents = co * "\t" 136 self.indent_offset = co 137 self.parent = parent 138 139 def __getitem__(self, item): 140 for c in self.children: 141 if c.name == item: 142 return c 143 raise KeyError, item 144 145#---------------------------------------------------------------------- 146# ConfigFormat 147#---------------------------------------------------------------------- 148class ConfigFormat(ConfigNode): 149 def __init__(self, filename): 150 """ 151 Parse a config format file from the 'filename'. 152 """ 153 ConfigNode.__init__(self) 154 self.indent_offset = -1 155 self.filename = filename 156 157 data = "" 158 try: 159 with file(filename, "r") as f: 160 data = f.read() 161 except IOError: 162 pass 163 lines = data.splitlines() 164 nodes = [] 165 parent = self 166 for line in lines: 167 node = ConfigNode() 168 node.parse(line) 169 nodes.append(node) 170 171 # add tree info 172 if node.type != CONFIG_NODE_NAME_VALUE: 173 continue 174 175 if parent.indent_offset < node.indent_offset: 176 if parent.is_child(node.indent_offset): 177 node.parent = parent 178 parent.children.append(node) 179 parent = node 180 else: 181 while parent.indent_offset >= node.indent_offset: 182 parent = parent.parent 183 184 if parent.is_child(node.indent_offset): 185 node.parent = parent 186 parent.children.append(node) 187 parent = node 188 189 self.nodes = nodes 190 191 def save(self, filename): 192 """ 193 Save config format file in the 'filename'. Function makes sure 194 that the dir for 'filename' exists, if not - tries to create it. 195 """ 196 d = os.path.dirname(filename) 197 if not os.path.exists(d): 198 os.makedirs(d) 199 with file(filename, "w") as f: 200 for node in self.nodes: 201 f.write(node.indent_contents) 202 if node.name: 203 f.write(node.name) 204 if node.value: 205 f.write(" ") 206 f.write(node.value) 207 f.write("\n") 208 209 def _find_last_node(self, root): 210 """ 211 Find an index in the nodes list of the last node in the 212 tree chain:: 213 214 ------------------------------------------ 215 one <---- ('root') 216 two 217 three 218 four 219 five 220 six 221 seven <----(this one) 222 ------------------------------------------ 223 """ 224 if not len(root.children): 225 try: 226 return self.nodes.index(root) 227 except ValueError: 228 return 0 229 230 return self._find_last_node(root.children[-1]) 231 232 def append_node_after(self, node, after): 233 """ 234 Append 'node' after another node. Handles both tree and 235 nodes list. 236 """ 237 parent = after.parent 238 nodei = parent.children.index(after) 239 240 node.make_child_of(parent) 241 242 i = self._find_last_node(after) 243 parent.children.insert(nodei+1, node) 244 self.nodes.insert(i+1, node) 245 246 def append_node_as_child(self, node, parent): 247 """ 248 Append 'node' as a child of the 'parent'. Handles both 249 tree and nodes list. 250 """ 251 node.make_child_of(parent) 252 253 i = self._find_last_node(parent) 254 parent.children.append(node) 255 self.nodes.insert(i+1, node) 256 257 def remove_node(self, node): 258 """ 259 Remove 'node' and all its children. Handles both tree 260 and nodes list. 261 """ 262 for c in node.children: 263 self.remove_node(c) 264 265 parent = node.parent 266 if not parent: 267 return 268 parent.children.remove(node) 269 self.nodes.remove(node) 270 271#---------------------------------------------------------------------- 272# XDG functions 273#---------------------------------------------------------------------- 274def XDG_get_config_home(): 275 """ 276 Return XDG_CONFIG_HOME directory according to XDG spec. 277 """ 278 xdghome = os.getenv("XDG_CONFIG_HOME") 279 if not xdghome: 280 xdghome = os.path.join(os.getenv("HOME"), ".config") 281 return xdghome 282 283def XDG_get_data_dirs(): 284 """ 285 Return XDG_DATA_HOME + XDG_DATA_DIRS array according to XDG spec. 286 """ 287 ret = [] 288 xdgdata = os.getenv("XDG_DATA_HOME") 289 if not xdgdata: 290 xdgdata = os.path.join(os.getenv("HOME"), ".local/share") 291 ret.append(xdgdata) 292 293 xdgdirs = os.getenv("XDG_DATA_DIRS") 294 if xdgdirs: 295 ret += xdgdirs.split(":") 296 return ret 297 298#---------------------------------------------------------------------- 299# Bmpanel2Config 300#---------------------------------------------------------------------- 301class Bmpanel2Config: 302 def _get_int_value(self, name, default): 303 try: 304 ret = int(self.tree[name].value) 305 except: 306 ret = default 307 return ret 308 309 def _set_int_value(self, name, value): 310 s = "{0}".format(value) 311 try: 312 node = self.tree[name] 313 node.value = s 314 except: 315 node = ConfigNode(name=name, value=s) 316 self.tree.append_node_as_child(node, self.tree) 317 self.fire_unsaved_notifiers(True) 318 319 def _get_str_value(self, name, default): 320 try: 321 ret = self.tree[name].value 322 except: 323 ret = default 324 return ret 325 326 def _set_str_value(self, name, value): 327 try: 328 node = self.tree[name] 329 node.value = value 330 except: 331 node = ConfigNode(name=name, value=value) 332 self.tree.append_node_as_child(node, self.tree) 333 self.fire_unsaved_notifiers(True) 334 335 def _get_bool_value(self, name): 336 try: 337 node = self.tree[name] 338 return True 339 except: 340 return False 341 342 def _set_bool_value(self, name, value): 343 try: 344 node = self.tree[name] 345 if not value: 346 self.tree.remove_node(node) 347 except: 348 if value: 349 node = ConfigNode(name=name) 350 self.tree.append_node_as_child(node, self.tree) 351 self.fire_unsaved_notifiers(True) 352 #-------------------------------------------------------------- 353 def __init__(self): 354 self.path = os.path.join(XDG_get_config_home(), "bmpanel2/bmpanel2rc") 355 self.tree = ConfigFormat(self.path) 356 357 # an array of function pointers 358 # function(state) 359 # where 'state' is a boolean 360 # functions are being called when there are: 361 # True - unsaved changes are here 362 # False - config was just saved, no unsaved changes 363 self.unsaved_notifiers = [] 364 365 def add_unsaved_notifier(self, notifier): 366 self.unsaved_notifiers.append(notifier) 367 368 def fire_unsaved_notifiers(self, status): 369 for n in self.unsaved_notifiers: 370 n(status) 371 372 def save(self): 373 self.tree.save(self.path) 374 self.fire_unsaved_notifiers(False) 375 #self.tree.save("testrc") 376 377 def get_theme(self): 378 return self._get_str_value('theme', 'native') 379 380 def set_theme(self, value): 381 self._set_str_value('theme', value) 382 383 def get_task_death_threshold(self): 384 return self._get_int_value('task_death_threshold', 50) 385 386 def set_task_death_threshold(self, value): 387 self._set_int_value('task_death_threshold', value) 388 389 def get_drag_threshold(self): 390 return self._get_int_value('drag_threshold', 30) 391 392 def set_drag_threshold(self, value): 393 self._set_int_value('drag_threshold', value) 394 395 def get_task_urgency_hint(self): 396 return self._get_bool_value('task_urgency_hint') 397 398 def set_task_urgency_hint(self, value): 399 self._set_bool_value('task_urgency_hint', value) 400 401 def get_clock_prog(self): 402 return self._get_str_value('clock_prog', None) 403 404 def set_clock_prog(self, value): 405 self._set_str_value('clock_prog', value) 406 407 # TODO: launchbar 408 409#---------------------------------------------------------------------- 410# Bmpanel2Remote 411#---------------------------------------------------------------------- 412class Bmpanel2Remote: 413 def __init__(self): 414 self.started_with_theme = False 415 self.pid = None 416 self.update_pid() 417 418 def update_pid(self): 419 # find pid 420 try: 421 self.pid = int(os.popen("pidof bmpanel2").read().splitlines()[0]) 422 except: 423 return 424 # check if bmpanel2 was started with "--theme" parameter 425 try: 426 args = os.popen("ps --no-heading o %a -p {0}".format(self.pid)).read().splitlines()[0] 427 self.started_with_theme = args.find("--theme") != -1 428 except: 429 pass 430 431 def reconfigure(self): 432 if self.pid: 433 os.kill(self.pid, 10) 434 435#---------------------------------------------------------------------- 436# Bmpanel2Themes 437#---------------------------------------------------------------------- 438class Theme: 439 def __init__(self, dirname, name=None, author=None, path=None): 440 self.dirname = dirname 441 self.name = name 442 self.author = author 443 self.path = path 444 445class Bmpanel2Themes: 446 def _try_load_theme(self, dirname, themefile): 447 c = ConfigFormat(themefile) 448 path = os.path.dirname(themefile) 449 name = None 450 author = None 451 try: 452 t = c['theme'] 453 name = t['name'].value 454 author = t['author'].value 455 except: 456 pass 457 458 if not dirname in self.themes: 459 self.themes[dirname] = Theme(dirname, name, author, path) 460 461 def _lookup_for_themes(self, d): 462 try: 463 files = os.listdir(d) 464 except OSError: 465 return 466 467 for f in files: 468 path = os.path.join(d, f) 469 path = os.path.join(path, "theme") 470 if os.path.exists(path): 471 self._try_load_theme(f, path) 472 473 def __init__(self): 474 self.themes = {} 475 dirs = XDG_get_data_dirs() 476 for d in dirs: 477 path = os.path.join(d, "bmpanel2/themes") 478 self._lookup_for_themes(path) 479 480 def get_dirname(theme): 481 if theme.name: 482 return theme.name 483 else: 484 return theme.dirname 485 tmp = self.themes.values() 486 tmp.sort(key=get_dirname) 487 self.themes = tmp 488#---------------------------------------------------------------------- 489# Bmpanel2Launchbar 490#---------------------------------------------------------------------- 491 492class LaunchbarItem: 493 def __init__(self, prog=None, icon=None): 494 self.prog = prog 495 self.icon = icon 496 497class Bmpanel2Launchbar: 498 def __init__(self, config): 499 try: 500 launchbar = config.tree['launchbar'] 501 except: 502 launchbar = ConfigNode(name="launchbar") 503 config.tree.append_node_as_child(launchbar, config.tree) 504 505 self.launchbar = launchbar 506 507 def __iter__(self): 508 for c in self.launchbar.children: 509 yield LaunchbarItem(c.value, c['icon'].value) 510 511 def __getitem__(self, n): 512 c = self.launchbar.children[n] 513 return LaunchbarItem(c.value, c['icon'].value) 514 515