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"_" or key == u"ctrl k":
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"bold",
254
u"Mandos Monitor version " + version))
255
self.log_message((u"bold",
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]
316
self.logbox.set_focus(len(self.logbox.body.contents),
317
coming_from=u"above")
319
def toggle_log_display(self):
320
"""Toggle visibility of the log buffer."""
321
self.log_visible = not self.log_visible
323
self.log_message(u"Log visibility changed to: "
324
+ unicode(self.log_visible))
326
def change_log_display(self):
327
"""Change type of log display.
328
Currently, this toggles wrapping of text lines."""
329
if self.log_wrap == u"clip":
330
self.log_wrap = u"any"
332
self.log_wrap = u"clip"
333
for textwidget in self.log:
334
textwidget.set_wrap_mode(self.log_wrap)
335
self.log_message(u"Wrap mode: " + self.log_wrap)
337
def find_and_remove_client(self, path, name):
338
"""Find an client from its object path and remove it.
340
This is connected to the ClientRemoved signal from the
341
Mandos server object."""
343
client = self.clients_dict[path]
347
self.remove_client(client, path)
349
def add_new_client(self, path, properties):
350
client_proxy_object = self.bus.get_object(self.busname, path)
351
self.add_client(MandosClientWidget(server_proxy_object
354
=client_proxy_object,
355
properties=properties,
359
=self.remove_client),
362
def add_client(self, client, path=None):
363
self.clients.append(client)
365
path = client.proxy.object_path
366
self.clients_dict[path] = client
367
self.clients.sort(None, lambda c: c.properties[u"name"])
370
def remove_client(self, client, path=None):
371
self.clients.remove(client)
373
path = client.proxy.object_path
374
del self.clients_dict[path]
375
if not self.clients_dict:
376
# Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
377
# is completely emptied, we need to recreate it.
378
self.clients = urwid.SimpleListWalker([])
383
"""Redraw the screen"""
384
canvas = self.topwidget.render(self.size, focus=True)
385
self.screen.draw_screen(self.size, canvas)
388
"""Start the main loop and exit when it's done."""
390
self._input_callback_tag = (gobject.io_add_watch
395
# Main loop has finished, we should close everything now
396
gobject.source_remove(self._input_callback_tag)
400
self.main_loop.quit()
402
def process_input(self, source, condition):
403
keys = self.screen.get_input()
404
translations = { u"ctrl n": u"down", # Emacs
405
u"ctrl p": u"up", # Emacs
406
u"ctrl v": u"page down", # Emacs
407
u"meta v": u"page up", # Emacs
408
u" ": u"page down", # less
409
u"f": u"page down", # less
410
u"b": u"page up", # less
416
key = translations[key]
417
except KeyError: # :-)
420
if key == u"q" or key == u"Q":
423
elif key == u"window resize":
424
self.size = self.screen.get_cols_rows()
426
elif key == u"\f": # Ctrl-L
428
elif key == u"l" or key == u"D":
429
self.toggle_log_display()
431
elif key == u"w" or key == u"i":
432
self.change_log_display()
434
elif key == u"?" or key == u"f1" or key == u"esc":
435
if not self.log_visible:
436
self.log_visible = True
438
self.log_message((u"bold",
439
u" ".join((u"q: Quit",
441
u"l: Log window toggle",
442
u"TAB: Switch window",
444
self.log_message((u"bold",
445
u" ".join((u"Clients:",
449
u"s: Start new checker",
454
if self.topwidget.get_focus() is self.logbox:
455
self.topwidget.set_focus(0)
457
self.topwidget.set_focus(self.logbox)
459
#elif (key == u"end" or key == u"meta >" or key == u"G"
461
# pass # xxx end-of-buffer
462
#elif (key == u"home" or key == u"meta <" or key == u"g"
464
# pass # xxx beginning-of-buffer
465
#elif key == u"ctrl e" or key == u"$":
466
# pass # xxx move-end-of-line
467
#elif key == u"ctrl a" or key == u"^":
468
# pass # xxx move-beginning-of-line
469
#elif key == u"ctrl b" or key == u"meta (" or key == u"h":
471
#elif key == u"ctrl f" or key == u"meta )" or key == u"l":
474
# pass # scroll up log
476
# pass # scroll down log
477
elif self.topwidget.selectable():
478
self.topwidget.keypress(self.size, key)