/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk

« back to all changes in this revision

Viewing changes to mandos-monitor

merge

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
# -*- mode: python; coding: utf-8 -*-
 
3
 
 
4
from __future__ import division, absolute_import, with_statement
 
5
 
 
6
import sys
 
7
import os
 
8
import signal
 
9
 
 
10
import datetime
 
11
 
 
12
import urwid.curses_display
 
13
import urwid
 
14
 
 
15
from dbus.mainloop.glib import DBusGMainLoop
 
16
import gobject
 
17
 
 
18
import dbus
 
19
 
 
20
import UserList
 
21
 
 
22
import locale
 
23
 
 
24
locale.setlocale(locale.LC_ALL, u'')
 
25
 
 
26
import logging
 
27
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
 
28
 
 
29
# Some useful constants
 
30
domain = 'se.bsnet.fukt'
 
31
server_interface = domain + '.Mandos'
 
32
client_interface = domain + '.Mandos.Client'
 
33
version = "1.0.15"
 
34
 
 
35
# Always run in monochrome mode
 
36
urwid.curses_display.curses.has_colors = lambda : False
 
37
 
 
38
# Urwid doesn't support blinking, but we want it.  Since we have no
 
39
# use for underline on its own, we make underline also always blink.
 
40
urwid.curses_display.curses.A_UNDERLINE |= (
 
41
    urwid.curses_display.curses.A_BLINK)
 
42
 
 
43
def isoformat_to_datetime(iso):
 
44
    "Parse an ISO 8601 date string to a datetime.datetime()"
 
45
    if not iso:
 
46
        return None
 
47
    d, t = iso.split(u"T", 1)
 
48
    year, month, day = d.split(u"-", 2)
 
49
    hour, minute, second = t.split(u":", 2)
 
50
    second, fraction = divmod(float(second), 1)
 
51
    return datetime.datetime(int(year),
 
52
                             int(month),
 
53
                             int(day),
 
54
                             int(hour),
 
55
                             int(minute),
 
56
                             int(second),           # Whole seconds
 
57
                             int(fraction*1000000)) # Microseconds
 
58
 
 
59
class MandosClientPropertyCache(object):
 
60
    """This wraps a Mandos Client D-Bus proxy object, caches the
 
61
    properties and calls a hook function when any of them are
 
62
    changed.
 
63
    """
 
64
    def __init__(self, proxy_object=None, *args, **kwargs):
 
65
        self.proxy = proxy_object # Mandos Client proxy object
 
66
        
 
67
        self.properties = dict()
 
68
        self.proxy.connect_to_signal(u"PropertyChanged",
 
69
                                     self.property_changed,
 
70
                                     client_interface,
 
71
                                     byte_arrays=True)
 
72
        
 
73
        self.properties.update(
 
74
            self.proxy.GetAll(client_interface,
 
75
                              dbus_interface = dbus.PROPERTIES_IFACE))
 
76
 
 
77
        #XXX This break good super behaviour!
 
78
#        super(MandosClientPropertyCache, self).__init__(
 
79
#            *args, **kwargs)
 
80
    
 
81
    def property_changed(self, property=None, value=None):
 
82
        """This is called whenever we get a PropertyChanged signal
 
83
        It updates the changed property in the "properties" dict.
 
84
        """
 
85
        # Update properties dict with new value
 
86
        self.properties[property] = value
 
87
 
 
88
 
 
89
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
 
90
    """A Mandos Client which is visible on the screen.
 
91
    """
 
92
    
 
93
    def __init__(self, server_proxy_object=None, update_hook=None,
 
94
                 delete_hook=None, logger=None, *args, **kwargs):
 
95
        # Called on update
 
96
        self.update_hook = update_hook
 
97
        # Called on delete
 
98
        self.delete_hook = delete_hook
 
99
        # Mandos Server proxy object
 
100
        self.server_proxy_object = server_proxy_object
 
101
        # Logger
 
102
        self.logger = logger
 
103
        
 
104
        self._update_timer_callback_tag = None
 
105
        self.last_checker_failed = False
 
106
        
 
107
        # The widget shown normally
 
108
        self._text_widget = urwid.Text(u"")
 
109
        # The widget shown when we have focus
 
110
        self._focus_text_widget = urwid.Text(u"")
 
111
        super(MandosClientWidget, self).__init__(
 
112
            update_hook=update_hook, delete_hook=delete_hook,
 
113
            *args, **kwargs)
 
114
        self.update()
 
115
        self.opened = False
 
116
        self.proxy.connect_to_signal(u"CheckerCompleted",
 
117
                                     self.checker_completed,
 
118
                                     client_interface,
 
119
                                     byte_arrays=True)
 
120
        self.proxy.connect_to_signal(u"CheckerStarted",
 
121
                                     self.checker_started,
 
122
                                     client_interface,
 
123
                                     byte_arrays=True)
 
124
        self.proxy.connect_to_signal(u"GotSecret",
 
125
                                     self.got_secret,
 
126
                                     client_interface,
 
127
                                     byte_arrays=True)
 
128
        self.proxy.connect_to_signal(u"NeedApproval",
 
129
                                     self.need_approval,
 
130
                                     client_interface,
 
131
                                     byte_arrays=True)
 
132
        self.proxy.connect_to_signal(u"Rejected",
 
133
                                     self.rejected,
 
134
                                     client_interface,
 
135
                                     byte_arrays=True)
 
136
        last_checked_ok = isoformat_to_datetime(self.properties
 
137
                                                ["last_checked_ok"])
 
138
        if last_checked_ok is None:
 
139
            self.last_checker_failed = True
 
140
        else:
 
141
            self.last_checker_failed = ((datetime.datetime.utcnow()
 
142
                                         - last_checked_ok)
 
143
                                        > datetime.timedelta
 
144
                                        (milliseconds=
 
145
                                         self.properties["interval"]))
 
146
        if self.last_checker_failed:
 
147
            self._update_timer_callback_tag = (gobject.timeout_add
 
148
                                               (1000,
 
149
                                                self.update_timer))
 
150
    
 
151
    def checker_completed(self, exitstatus, condition, command):
 
152
        if exitstatus == 0:
 
153
            if self.last_checker_failed:
 
154
                self.last_checker_failed = False
 
155
                gobject.source_remove(self._update_timer_callback_tag)
 
156
                self._update_timer_callback_tag = None
 
157
            self.logger(u'Checker for client %s (command "%s")'
 
158
                        u' was successful'
 
159
                        % (self.properties[u"name"], command))
 
160
            self.update()
 
161
            return
 
162
        # Checker failed
 
163
        if not self.last_checker_failed:
 
164
            self.last_checker_failed = True
 
165
            self._update_timer_callback_tag = (gobject.timeout_add
 
166
                                               (1000,
 
167
                                                self.update_timer))
 
168
        if os.WIFEXITED(condition):
 
169
            self.logger(u'Checker for client %s (command "%s")'
 
170
                        u' failed with exit code %s'
 
171
                        % (self.properties[u"name"], command,
 
172
                           os.WEXITSTATUS(condition)))
 
173
        elif os.WIFSIGNALED(condition):
 
174
            self.logger(u'Checker for client %s (command "%s")'
 
175
                        u' was killed by signal %s'
 
176
                        % (self.properties[u"name"], command,
 
177
                           os.WTERMSIG(condition)))
 
178
        elif os.WCOREDUMP(condition):
 
179
            self.logger(u'Checker for client %s (command "%s")'
 
180
                        u' dumped core'
 
181
                        % (self.properties[u"name"], command))
 
182
        else:
 
183
            self.logger(u'Checker for client %s completed mysteriously')
 
184
        self.update()
 
185
    
 
186
    def checker_started(self, command):
 
187
        #self.logger(u'Client %s started checker "%s"'
 
188
        #            % (self.properties[u"name"], unicode(command)))
 
189
        pass
 
190
    
 
191
    def got_secret(self):
 
192
        self.last_checker_failed = False
 
193
        self.logger(u'Client %s received its secret'
 
194
                    % self.properties[u"name"])
 
195
    
 
196
    def need_approval(self, timeout, default):
 
197
        if not default:
 
198
            message = u'Client %s needs approval within %s seconds'
 
199
        else:
 
200
            message = u'Client %s will get its secret in %s seconds'
 
201
        self.logger(message
 
202
                    % (self.properties[u"name"], timeout/1000))
 
203
    
 
204
    def rejected(self, reason):
 
205
        self.logger(u'Client %s was rejected; reason: %s'
 
206
                    % (self.properties[u"name"], reason))
 
207
    
 
208
    def selectable(self):
 
209
        """Make this a "selectable" widget.
 
210
        This overrides the method from urwid.FlowWidget."""
 
211
        return True
 
212
    
 
213
    def rows(self, (maxcol,), focus=False):
 
214
        """How many rows this widget will occupy might depend on
 
215
        whether we have focus or not.
 
216
        This overrides the method from urwid.FlowWidget"""
 
217
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
218
    
 
219
    def current_widget(self, focus=False):
 
220
        if focus or self.opened:
 
221
            return self._focus_widget
 
222
        return self._widget
 
223
    
 
224
    def update(self):
 
225
        "Called when what is visible on the screen should be updated."
 
226
        # How to add standout mode to a style
 
227
        with_standout = { u"normal": u"standout",
 
228
                          u"bold": u"bold-standout",
 
229
                          u"underline-blink":
 
230
                              u"underline-blink-standout",
 
231
                          u"bold-underline-blink":
 
232
                              u"bold-underline-blink-standout",
 
233
                          }
 
234
 
 
235
        # Rebuild focus and non-focus widgets using current properties
 
236
 
 
237
        # Base part of a client. Name!
 
238
        base = (u'%(name)s: '
 
239
                      % {u"name": self.properties[u"name"]})
 
240
        if not self.properties[u"enabled"]:
 
241
            message = u"DISABLED"
 
242
        elif self.properties[u"approved_pending"]:
 
243
            if self.properties[u"approved_by_default"]:
 
244
                message = u"Connection established to client. (d)eny?"
 
245
            else:
 
246
                message = u"Seeks approval to send secret. (a)pprove?"
 
247
        elif self.last_checker_failed:
 
248
            timeout = datetime.timedelta(milliseconds
 
249
                                         = self.properties[u"timeout"])
 
250
            last_ok = isoformat_to_datetime(
 
251
                max((self.properties["last_checked_ok"]
 
252
                     or self.properties["created"]),
 
253
                    self.properties[u"last_enabled"]))
 
254
            timer = timeout - (datetime.datetime.utcnow() - last_ok)
 
255
            message = (u'A checker has failed! Time until client gets diabled: %s'
 
256
                           % unicode(timer).rsplit(".", 1)[0])
 
257
        else:
 
258
            message = u"enabled"
 
259
        self._text = "%s%s" % (base, message)
 
260
            
 
261
        if not urwid.supports_unicode():
 
262
            self._text = self._text.encode("ascii", "replace")
 
263
        textlist = [(u"normal", self._text)]
 
264
        self._text_widget.set_text(textlist)
 
265
        self._focus_text_widget.set_text([(with_standout[text[0]],
 
266
                                           text[1])
 
267
                                          if isinstance(text, tuple)
 
268
                                          else text
 
269
                                          for text in textlist])
 
270
        self._widget = self._text_widget
 
271
        self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
 
272
                                            "standout")
 
273
        # Run update hook, if any
 
274
        if self.update_hook is not None:
 
275
            self.update_hook()
 
276
    
 
277
    def update_timer(self):
 
278
        "called by gobject"
 
279
        self.update()
 
280
        return True             # Keep calling this
 
281
    
 
282
    def delete(self):
 
283
        if self._update_timer_callback_tag is not None:
 
284
            gobject.source_remove(self._update_timer_callback_tag)
 
285
            self._update_timer_callback_tag = None
 
286
        if self.delete_hook is not None:
 
287
            self.delete_hook(self)
 
288
    
 
289
    def render(self, (maxcol,), focus=False):
 
290
        """Render differently if we have focus.
 
291
        This overrides the method from urwid.FlowWidget"""
 
292
        return self.current_widget(focus).render((maxcol,),
 
293
                                                 focus=focus)
 
294
    
 
295
    def keypress(self, (maxcol,), key):
 
296
        """Handle keys.
 
297
        This overrides the method from urwid.FlowWidget"""
 
298
        if key == u"+":
 
299
            self.proxy.Enable(dbus_interface = client_interface)
 
300
        elif key == u"-":
 
301
            self.proxy.Disable(dbus_interface = client_interface)
 
302
        elif key == u"a":
 
303
            self.proxy.Approve(dbus.Boolean(True, variant_level=1),
 
304
                               dbus_interface = client_interface)
 
305
        elif key == u"d":
 
306
            self.proxy.Approve(dbus.Boolean(False, variant_level=1),
 
307
                                  dbus_interface = client_interface)
 
308
        elif key == u"r" or key == u"_" or key == u"ctrl k":
 
309
            self.server_proxy_object.RemoveClient(self.proxy
 
310
                                                  .object_path)
 
311
        elif key == u"s":
 
312
            self.proxy.StartChecker(dbus_interface = client_interface)
 
313
        elif key == u"S":
 
314
            self.proxy.StopChecker(dbus_interface = client_interface)
 
315
        elif key == u"C":
 
316
            self.proxy.CheckedOK(dbus_interface = client_interface)
 
317
        # xxx
 
318
#         elif key == u"p" or key == "=":
 
319
#             self.proxy.pause()
 
320
#         elif key == u"u" or key == ":":
 
321
#             self.proxy.unpause()
 
322
#         elif key == u"RET":
 
323
#             self.open()
 
324
#        elif key == u"+":
 
325
#            self.proxy.Approve(True)
 
326
#        elif key == u"-":
 
327
#            self.proxy.Approve(False)
 
328
        else:
 
329
            return key
 
330
    
 
331
    def property_changed(self, property=None, value=None,
 
332
                         *args, **kwargs):
 
333
        """Call self.update() if old value is not new value.
 
334
        This overrides the method from MandosClientPropertyCache"""
 
335
        property_name = unicode(property)
 
336
        old_value = self.properties.get(property_name)
 
337
        super(MandosClientWidget, self).property_changed(
 
338
            property=property, value=value, *args, **kwargs)
 
339
        if self.properties.get(property_name) != old_value:
 
340
            self.update()
 
341
 
 
342
 
 
343
class ConstrainedListBox(urwid.ListBox):
 
344
    """Like a normal urwid.ListBox, but will consume all "up" or
 
345
    "down" key presses, thus not allowing any containing widgets to
 
346
    use them as an excuse to shift focus away from this widget.
 
347
    """
 
348
    def keypress(self, (maxcol, maxrow), key):
 
349
        ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
 
350
        if ret in (u"up", u"down"):
 
351
            return
 
352
        return ret
 
353
 
 
354
 
 
355
class UserInterface(object):
 
356
    """This is the entire user interface - the whole screen
 
357
    with boxes, lists of client widgets, etc.
 
358
    """
 
359
    def __init__(self, max_log_length=1000):
 
360
        DBusGMainLoop(set_as_default=True)
 
361
        
 
362
        self.screen = urwid.curses_display.Screen()
 
363
        
 
364
        self.screen.register_palette((
 
365
                (u"normal",
 
366
                 u"default", u"default", None),
 
367
                (u"bold",
 
368
                 u"default", u"default", u"bold"),
 
369
                (u"underline-blink",
 
370
                 u"default", u"default", u"underline"),
 
371
                (u"standout",
 
372
                 u"default", u"default", u"standout"),
 
373
                (u"bold-underline-blink",
 
374
                 u"default", u"default", (u"bold", u"underline")),
 
375
                (u"bold-standout",
 
376
                 u"default", u"default", (u"bold", u"standout")),
 
377
                (u"underline-blink-standout",
 
378
                 u"default", u"default", (u"underline", u"standout")),
 
379
                (u"bold-underline-blink-standout",
 
380
                 u"default", u"default", (u"bold", u"underline",
 
381
                                          u"standout")),
 
382
                ))
 
383
        
 
384
        if urwid.supports_unicode():
 
385
            self.divider = u"─" # \u2500
 
386
            #self.divider = u"━" # \u2501
 
387
        else:
 
388
            #self.divider = u"-" # \u002d
 
389
            self.divider = u"_" # \u005f
 
390
        
 
391
        self.screen.start()
 
392
        
 
393
        self.size = self.screen.get_cols_rows()
 
394
        
 
395
        self.clients = urwid.SimpleListWalker([])
 
396
        self.clients_dict = {}
 
397
        
 
398
        # We will add Text widgets to this list
 
399
        self.log = []
 
400
        self.max_log_length = max_log_length
 
401
        
 
402
        # We keep a reference to the log widget so we can remove it
 
403
        # from the ListWalker without it getting destroyed
 
404
        self.logbox = ConstrainedListBox(self.log)
 
405
        
 
406
        # This keeps track of whether self.uilist currently has
 
407
        # self.logbox in it or not
 
408
        self.log_visible = True
 
409
        self.log_wrap = u"any"
 
410
        
 
411
        self.rebuild()
 
412
        self.log_message_raw((u"bold",
 
413
                              u"Mandos Monitor version " + version))
 
414
        self.log_message_raw((u"bold",
 
415
                              u"q: Quit  ?: Help"))
 
416
        
 
417
        self.busname = domain + '.Mandos'
 
418
        self.main_loop = gobject.MainLoop()
 
419
        self.bus = dbus.SystemBus()
 
420
        mandos_dbus_objc = self.bus.get_object(
 
421
            self.busname, u"/", follow_name_owner_changes=True)
 
422
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
423
                                          dbus_interface
 
424
                                          = server_interface)
 
425
        try:
 
426
            mandos_clients = (self.mandos_serv
 
427
                              .GetAllClientsWithProperties())
 
428
        except dbus.exceptions.DBusException:
 
429
            mandos_clients = dbus.Dictionary()
 
430
        
 
431
        (self.mandos_serv
 
432
         .connect_to_signal(u"ClientRemoved",
 
433
                            self.find_and_remove_client,
 
434
                            dbus_interface=server_interface,
 
435
                            byte_arrays=True))
 
436
        (self.mandos_serv
 
437
         .connect_to_signal(u"ClientAdded",
 
438
                            self.add_new_client,
 
439
                            dbus_interface=server_interface,
 
440
                            byte_arrays=True))
 
441
        (self.mandos_serv
 
442
         .connect_to_signal(u"ClientNotFound",
 
443
                            self.client_not_found,
 
444
                            dbus_interface=server_interface,
 
445
                            byte_arrays=True))
 
446
        for path, client in mandos_clients.iteritems():
 
447
            client_proxy_object = self.bus.get_object(self.busname,
 
448
                                                      path)
 
449
            self.add_client(MandosClientWidget(server_proxy_object
 
450
                                               =self.mandos_serv,
 
451
                                               proxy_object
 
452
                                               =client_proxy_object,
 
453
                                               properties=client,
 
454
                                               update_hook
 
455
                                               =self.refresh,
 
456
                                               delete_hook
 
457
                                               =self.remove_client,
 
458
                                               logger
 
459
                                               =self.log_message),
 
460
                            path=path)
 
461
    
 
462
    def client_not_found(self, fingerprint, address):
 
463
        self.log_message((u"Client with address %s and fingerprint %s"
 
464
                          u" could not be found" % (address,
 
465
                                                    fingerprint)))
 
466
    
 
467
    def rebuild(self):
 
468
        """This rebuilds the User Interface.
 
469
        Call this when the widget layout needs to change"""
 
470
        self.uilist = []
 
471
        #self.uilist.append(urwid.ListBox(self.clients))
 
472
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
 
473
                                       #header=urwid.Divider(),
 
474
                                       header=None,
 
475
                                       footer=urwid.Divider(div_char=self.divider)))
 
476
        if self.log_visible:
 
477
            self.uilist.append(self.logbox)
 
478
            pass
 
479
        self.topwidget = urwid.Pile(self.uilist)
 
480
    
 
481
    def log_message(self, message):
 
482
        timestamp = datetime.datetime.now().isoformat()
 
483
        self.log_message_raw(timestamp + u": " + message)
 
484
    
 
485
    def log_message_raw(self, markup):
 
486
        """Add a log message to the log buffer."""
 
487
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
488
        if (self.max_log_length
 
489
            and len(self.log) > self.max_log_length):
 
490
            del self.log[0:len(self.log)-self.max_log_length-1]
 
491
        self.logbox.set_focus(len(self.logbox.body.contents),
 
492
                              coming_from=u"above")
 
493
        self.refresh()
 
494
    
 
495
    def toggle_log_display(self):
 
496
        """Toggle visibility of the log buffer."""
 
497
        self.log_visible = not self.log_visible
 
498
        self.rebuild()
 
499
        self.log_message(u"Log visibility changed to: "
 
500
                         + unicode(self.log_visible))
 
501
    
 
502
    def change_log_display(self):
 
503
        """Change type of log display.
 
504
        Currently, this toggles wrapping of text lines."""
 
505
        if self.log_wrap == u"clip":
 
506
            self.log_wrap = u"any"
 
507
        else:
 
508
            self.log_wrap = u"clip"
 
509
        for textwidget in self.log:
 
510
            textwidget.set_wrap_mode(self.log_wrap)
 
511
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
512
    
 
513
    def find_and_remove_client(self, path, name):
 
514
        """Find an client from its object path and remove it.
 
515
        
 
516
        This is connected to the ClientRemoved signal from the
 
517
        Mandos server object."""
 
518
        try:
 
519
            client = self.clients_dict[path]
 
520
        except KeyError:
 
521
            # not found?
 
522
            return
 
523
        self.remove_client(client, path)
 
524
    
 
525
    def add_new_client(self, path):
 
526
        client_proxy_object = self.bus.get_object(self.busname, path)
 
527
        self.add_client(MandosClientWidget(server_proxy_object
 
528
                                           =self.mandos_serv,
 
529
                                           proxy_object
 
530
                                           =client_proxy_object,
 
531
                                           update_hook
 
532
                                           =self.refresh,
 
533
                                           delete_hook
 
534
                                           =self.remove_client,
 
535
                                           logger
 
536
                                           =self.log_message),
 
537
                        path=path)
 
538
    
 
539
    def add_client(self, client, path=None):
 
540
        self.clients.append(client)
 
541
        if path is None:
 
542
            path = client.proxy.object_path
 
543
        self.clients_dict[path] = client
 
544
        self.clients.sort(None, lambda c: c.properties[u"name"])
 
545
        self.refresh()
 
546
    
 
547
    def remove_client(self, client, path=None):
 
548
        self.clients.remove(client)
 
549
        if path is None:
 
550
            path = client.proxy.object_path
 
551
        del self.clients_dict[path]
 
552
        if not self.clients_dict:
 
553
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
 
554
            # is completely emptied, we need to recreate it.
 
555
            self.clients = urwid.SimpleListWalker([])
 
556
            self.rebuild()
 
557
        self.refresh()
 
558
    
 
559
    def refresh(self):
 
560
        """Redraw the screen"""
 
561
        canvas = self.topwidget.render(self.size, focus=True)
 
562
        self.screen.draw_screen(self.size, canvas)
 
563
    
 
564
    def run(self):
 
565
        """Start the main loop and exit when it's done."""
 
566
        self.refresh()
 
567
        self._input_callback_tag = (gobject.io_add_watch
 
568
                                    (sys.stdin.fileno(),
 
569
                                     gobject.IO_IN,
 
570
                                     self.process_input))
 
571
        self.main_loop.run()
 
572
        # Main loop has finished, we should close everything now
 
573
        gobject.source_remove(self._input_callback_tag)
 
574
        self.screen.stop()
 
575
    
 
576
    def stop(self):
 
577
        self.main_loop.quit()
 
578
    
 
579
    def process_input(self, source, condition):
 
580
        keys = self.screen.get_input()
 
581
        translations = { u"ctrl n": u"down",      # Emacs
 
582
                         u"ctrl p": u"up",        # Emacs
 
583
                         u"ctrl v": u"page down", # Emacs
 
584
                         u"meta v": u"page up",   # Emacs
 
585
                         u" ": u"page down",      # less
 
586
                         u"f": u"page down",      # less
 
587
                         u"b": u"page up",        # less
 
588
                         u"j": u"down",           # vi
 
589
                         u"k": u"up",             # vi
 
590
                         }
 
591
        for key in keys:
 
592
            try:
 
593
                key = translations[key]
 
594
            except KeyError:    # :-)
 
595
                pass
 
596
            
 
597
            if key == u"q" or key == u"Q":
 
598
                self.stop()
 
599
                break
 
600
            elif key == u"window resize":
 
601
                self.size = self.screen.get_cols_rows()
 
602
                self.refresh()
 
603
            elif key == u"\f":  # Ctrl-L
 
604
                self.refresh()
 
605
            elif key == u"l" or key == u"D":
 
606
                self.toggle_log_display()
 
607
                self.refresh()
 
608
            elif key == u"w" or key == u"i":
 
609
                self.change_log_display()
 
610
                self.refresh()
 
611
            elif key == u"?" or key == u"f1" or key == u"esc":
 
612
                if not self.log_visible:
 
613
                    self.log_visible = True
 
614
                    self.rebuild()
 
615
                self.log_message_raw((u"bold",
 
616
                                      u"  ".
 
617
                                      join((u"q: Quit",
 
618
                                            u"?: Help",
 
619
                                            u"l: Log window toggle",
 
620
                                            u"TAB: Switch window",
 
621
                                            u"w: Wrap (log)"))))
 
622
                self.log_message_raw((u"bold",
 
623
                                      u"  "
 
624
                                      .join((u"Clients:",
 
625
                                             u"+: Enable",
 
626
                                             u"-: Disable",
 
627
                                             u"r: Remove",
 
628
                                             u"s: Start new checker",
 
629
                                             u"S: Stop checker",
 
630
                                             u"C: Checker OK",
 
631
                                             u"a: Approve",
 
632
                                             u"d: Deny"))))
 
633
                self.refresh()
 
634
            elif key == u"tab":
 
635
                if self.topwidget.get_focus() is self.logbox:
 
636
                    self.topwidget.set_focus(0)
 
637
                else:
 
638
                    self.topwidget.set_focus(self.logbox)
 
639
                self.refresh()
 
640
            #elif (key == u"end" or key == u"meta >" or key == u"G"
 
641
            #      or key == u">"):
 
642
            #    pass            # xxx end-of-buffer
 
643
            #elif (key == u"home" or key == u"meta <" or key == u"g"
 
644
            #      or key == u"<"):
 
645
            #    pass            # xxx beginning-of-buffer
 
646
            #elif key == u"ctrl e" or key == u"$":
 
647
            #    pass            # xxx move-end-of-line
 
648
            #elif key == u"ctrl a" or key == u"^":
 
649
            #    pass            # xxx move-beginning-of-line
 
650
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
651
            #    pass            # xxx left
 
652
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
653
            #    pass            # xxx right
 
654
            #elif key == u"a":
 
655
            #    pass            # scroll up log
 
656
            #elif key == u"z":
 
657
            #    pass            # scroll down log
 
658
            elif self.topwidget.selectable():
 
659
                self.topwidget.keypress(self.size, key)
 
660
                self.refresh()
 
661
        return True
 
662
 
 
663
ui = UserInterface()
 
664
try:
 
665
    ui.run()
 
666
except KeyboardInterrupt:
 
667
    ui.screen.stop()
 
668
except Exception, e:
 
669
    ui.log_message(unicode(e))
 
670
    ui.screen.stop()
 
671
    raise