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