2
# -*- mode: python; coding: utf-8 -*-
4
from __future__ import division, absolute_import, with_statement
9
import urwid.curses_display
12
from dbus.mainloop.glib import DBusGMainLoop
19
# Some useful constants
20
domain = 'se.bsnet.fukt'
21
server_interface = domain + '.Mandos'
22
client_interface = domain + '.Mandos.Client'
25
# Always run in monochrome mode
26
urwid.curses_display.curses.has_colors = lambda : False
28
# Urwid doesn't support blinking, but we want it. Since we have no
29
# use for underline on its own, we make underline also always blink.
30
urwid.curses_display.curses.A_UNDERLINE |= (
31
urwid.curses_display.curses.A_BLINK)
33
class MandosClientPropertyCache(object):
34
"""This wraps a Mandos Client D-Bus proxy object, caches the
35
properties and calls a hook function when any of them are
38
def __init__(self, proxy_object=None, properties=None, *args,
40
self.proxy = proxy_object # Mandos Client proxy object
42
if properties is None:
43
self.properties = dict()
45
self.properties = properties
46
self.proxy.connect_to_signal("PropertyChanged",
47
self.property_changed,
51
if properties is None:
52
self.properties.update(self.proxy.GetAll(client_interface,
54
dbus.PROPERTIES_IFACE))
55
super(MandosClientPropertyCache, self).__init__(
56
proxy_object=proxy_object,
57
properties=properties, *args, **kwargs)
59
def property_changed(self, property=None, value=None):
60
"""This is called whenever we get a PropertyChanged signal
61
It updates the changed property in the "properties" dict.
63
# Update properties dict with new value
64
self.properties[property] = value
67
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
68
"""A Mandos Client which is visible on the screen.
71
def __init__(self, server_proxy_object=None, update_hook=None,
72
delete_hook=None, *args, **kwargs):
74
self.update_hook = update_hook
76
self.delete_hook = delete_hook
77
# Mandos Server proxy object
78
self.server_proxy_object = server_proxy_object
80
# The widget shown normally
81
self._text_widget = urwid.Text("")
82
# The widget shown when we have focus
83
self._focus_text_widget = urwid.Text("")
84
super(MandosClientWidget, self).__init__(
85
update_hook=update_hook, delete_hook=delete_hook,
91
"""Make this a "selectable" widget.
92
This overrides the method from urwid.FlowWidget."""
95
def rows(self, (maxcol,), focus=False):
96
"""How many rows this widget will occupy might depend on
97
whether we have focus or not.
98
This overrides the method from urwid.FlowWidget"""
99
return self.current_widget(focus).rows((maxcol,), focus=focus)
101
def current_widget(self, focus=False):
102
if focus or self.opened:
103
return self._focus_widget
107
"Called when what is visible on the screen should be updated."
108
# How to add standout mode to a style
109
with_standout = { u"normal": u"standout",
110
u"bold": u"bold-standout",
112
u"underline-blink-standout",
113
u"bold-underline-blink":
114
u"bold-underline-blink-standout",
117
# Rebuild focus and non-focus widgets using current properties
118
self._text = (u'name="%(name)s", enabled=%(enabled)s'
120
if not urwid.supports_unicode():
121
self._text = self._text.encode("ascii", "replace")
122
textlist = [(u"normal", u"BLARGH: "), (u"bold", self._text)]
123
self._text_widget.set_text(textlist)
124
self._focus_text_widget.set_text([(with_standout[text[0]],
126
if isinstance(text, tuple)
128
for text in textlist])
129
self._widget = self._text_widget
130
self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
132
# Run update hook, if any
133
if self.update_hook is not None:
137
if self.delete_hook is not None:
138
self.delete_hook(self)
140
def render(self, (maxcol,), focus=False):
141
"""Render differently if we have focus.
142
This overrides the method from urwid.FlowWidget"""
143
return self.current_widget(focus).render((maxcol,),
146
def keypress(self, (maxcol,), key):
148
This overrides the method from urwid.FlowWidget"""
149
if key == u"e" or key == u"+":
151
elif key == u"d" or key == u"-":
153
elif key == u"r" or key == u"_":
154
self.server_proxy_object.RemoveClient(self.proxy
157
self.proxy.StartChecker()
159
self.proxy.StopChecker()
161
self.proxy.CheckedOK()
163
# elif key == u"p" or key == "=":
165
# elif key == u"u" or key == ":":
166
# self.proxy.unpause()
167
# elif key == u"RET":
172
def property_changed(self, property=None, value=None,
174
"""Call self.update() if old value is not new value.
175
This overrides the method from MandosClientPropertyCache"""
176
property_name = unicode(property)
177
old_value = self.properties.get(property_name)
178
super(MandosClientWidget, self).property_changed(
179
property=property, value=value, *args, **kwargs)
180
if self.properties.get(property_name) != old_value:
184
class ConstrainedListBox(urwid.ListBox):
185
"""Like a normal urwid.ListBox, but will consume all "up" or
186
"down" key presses, thus not allowing any containing widgets to
187
use them as an excuse to shift focus away from this widget.
189
def keypress(self, (maxcol, maxrow), key):
190
ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
191
if ret in (u"up", u"down"):
196
class UserInterface(object):
197
"""This is the entire user interface - the whole screen
198
with boxes, lists of client widgets, etc.
200
def __init__(self, max_log_length=1000):
201
DBusGMainLoop(set_as_default=True)
203
self.screen = urwid.curses_display.Screen()
205
self.screen.register_palette((
207
u"default", u"default", None),
209
u"default", u"default", u"bold"),
211
u"default", u"default", u"underline"),
213
u"default", u"default", u"standout"),
214
(u"bold-underline-blink",
215
u"default", u"default", (u"bold", u"underline")),
217
u"default", u"default", (u"bold", u"standout")),
218
(u"underline-blink-standout",
219
u"default", u"default", (u"underline", u"standout")),
220
(u"bold-underline-blink-standout",
221
u"default", u"default", (u"bold", u"underline",
225
if urwid.supports_unicode():
226
#self.divider = u"─" # \u2500
227
self.divider = u"━" # \u2501
229
#self.divider = u"-" # \u002d
230
self.divider = u"_" # \u005f
234
self.size = self.screen.get_cols_rows()
236
self.clients = urwid.SimpleListWalker([])
237
self.clients_dict = {}
239
# We will add Text widgets to this list
241
self.max_log_length = max_log_length
243
# We keep a reference to the log widget so we can remove it
244
# from the ListWalker without it getting destroyed
245
self.logbox = ConstrainedListBox(self.log)
247
# This keeps track of whether self.uilist currently has
248
# self.logbox in it or not
249
self.log_visible = True
250
self.log_wrap = u"any"
253
self.log_message(u"Message")
254
self.log_message(u"Message0 Message1 Message2 Message3 Message4 Message5 Message6 Message7 Message8 Message9")
255
self.log_message(u"Message10 Message11 Message12 Message13 Message14 Message15 Message16 Message17 Message18 Message19")
256
self.log_message(u"Message20 Message21 Message22 Message23 Message24 Message25 Message26 Message27 Message28 Message29")
258
self.busname = domain + '.Mandos'
259
self.main_loop = gobject.MainLoop()
260
self.bus = dbus.SystemBus()
261
mandos_dbus_objc = self.bus.get_object(
262
self.busname, u"/", follow_name_owner_changes=True)
263
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
267
mandos_clients = (self.mandos_serv
268
.GetAllClientsWithProperties())
269
except dbus.exceptions.DBusException:
270
mandos_clients = dbus.Dictionary()
273
.connect_to_signal("ClientRemoved",
274
self.find_and_remove_client,
275
dbus_interface=server_interface,
278
.connect_to_signal("ClientAdded",
280
dbus_interface=server_interface,
282
for path, client in mandos_clients.iteritems():
283
client_proxy_object = self.bus.get_object(self.busname,
285
self.add_client(MandosClientWidget(server_proxy_object
288
=client_proxy_object,
293
=self.remove_client),
297
"""This rebuilds the User Interface.
298
Call this when the widget layout needs to change"""
300
#self.uilist.append(urwid.ListBox(self.clients))
301
self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
302
#header=urwid.Divider(),
304
footer=urwid.Divider(div_char=self.divider)))
306
self.uilist.append(self.logbox)
308
self.topwidget = urwid.Pile(self.uilist)
310
def log_message(self, markup):
311
"""Add a log message to the log buffer."""
312
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
313
if (self.max_log_length
314
and len(self.log) > self.max_log_length):
315
del self.log[0:len(self.log)-self.max_log_length-1]
317
def toggle_log_display(self):
318
"""Toggle visibility of the log buffer."""
319
self.log_visible = not self.log_visible
321
self.log_message(u"Log visibility changed to: "
322
+ unicode(self.log_visible))
324
def change_log_display(self):
325
"""Change type of log display.
326
Currently, this toggles wrapping of text lines."""
327
if self.log_wrap == u"clip":
328
self.log_wrap = u"any"
330
self.log_wrap = u"clip"
331
for textwidget in self.log:
332
textwidget.set_wrap_mode(self.log_wrap)
333
self.log_message(u"Wrap mode: " + self.log_wrap)
335
def find_and_remove_client(self, path, name):
336
"""Find an client from its object path and remove it.
338
This is connected to the ClientRemoved signal from the
339
Mandos server object."""
341
client = self.clients_dict[path]
345
self.remove_client(client, path)
347
def add_new_client(self, path, properties):
348
client_proxy_object = self.bus.get_object(self.busname, path)
349
self.add_client(MandosClientWidget(server_proxy_object
352
=client_proxy_object,
353
properties=properties,
357
=self.remove_client),
360
def add_client(self, client, path=None):
361
self.clients.append(client)
363
path = client.proxy.object_path
364
self.clients_dict[path] = client
365
self.clients.sort(None, lambda c: c.properties[u"name"])
368
def remove_client(self, client, path=None):
369
self.clients.remove(client)
371
path = client.proxy.object_path
372
del self.clients_dict[path]
373
if not self.clients_dict:
374
# Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
375
# is completely emptied, we need to recreate it.
376
self.clients = urwid.SimpleListWalker([])
381
"""Redraw the screen"""
382
canvas = self.topwidget.render(self.size, focus=True)
383
self.screen.draw_screen(self.size, canvas)
386
"""Start the main loop and exit when it's done."""
388
self._input_callback_tag = (gobject.io_add_watch
393
# Main loop has finished, we should close everything now
394
gobject.source_remove(self._input_callback_tag)
398
self.main_loop.quit()
400
def process_input(self, source, condition):
401
keys = self.screen.get_input()
402
translations = { u"ctrl n": u"down", # Emacs
403
u"ctrl p": u"up", # Emacs
404
u"ctrl v": u"page down", # Emacs
405
u"meta v": u"page up", # Emacs
406
u" ": u"page down", # less
407
u"f": u"page down", # less
408
u"b": u"page up", # less
414
key = translations[key]
415
except KeyError: # :-)
418
if key == u"q" or key == u"Q":
421
elif key == u"window resize":
422
self.size = self.screen.get_cols_rows()
424
elif key == u"\f": # Ctrl-L
426
elif key == u"l" or key == u"D":
427
self.toggle_log_display()
429
elif key == u"w" or key == u"i":
430
self.change_log_display()
432
elif key == u"?" or key == u"f1":
433
self.log_message(u"Help!")
436
if self.topwidget.get_focus() is self.logbox:
437
self.topwidget.set_focus(0)
439
self.topwidget.set_focus(self.logbox)
441
elif (key == u"end" or key == u"meta >" or key == u"G"
443
pass # xxx end-of-buffer
444
elif (key == u"home" or key == u"meta <" or key == u"g"
446
pass # xxx beginning-of-buffer
447
elif key == u"ctrl e" or key == u"$":
448
pass # xxx move-end-of-line
449
elif key == u"ctrl a" or key == u"^":
450
pass # xxx move-beginning-of-line
451
elif key == u"ctrl b" or key == u"meta (" or key == u"h":
453
elif key == u"ctrl f" or key == u"meta )" or key == u"l":
458
pass # scroll down log
459
elif self.topwidget.selectable():
460
self.topwidget.keypress(self.size, key)