ui.py

changeset 80
a409242121d5
parent 79
b075b3db3044
child 81
7bcd715f19e3
--- a/ui.py	Sun Jan 28 11:38:01 2018 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,311 +0,0 @@
-#
-# Borgend MacOS UI
-#
-
-import rumps
-import time
-import datetime
-import objc
-from threading import Lock, Timer
-
-import loggers
-import backup
-import dreamtime
-import branding
-from config import settings
-
-logger=loggers.get(__name__)
-
-traynames_ok={
-    backup.State.INACTIVE: 'B.',
-    backup.State.SCHEDULED: 'B.',
-    backup.State.QUEUED: 'B:',
-    backup.State.ACTIVE: 'B!',
-}
-
-traynames_errors={
-    # The first one should never be used
-    backup.Errors.OK: traynames_ok[backup.State.INACTIVE],
-    backup.Errors.BUSY: 'B⦙',
-    backup.Errors.OFFLINE: 'B⦙',
-    backup.Errors.ERRORS: 'B?'
-}
-
-def trayname(ste):
-    state=ste[0]
-    errors=ste[1]
-    if not errors.ok():
-        return traynames_errors[errors]
-    else:
-        return traynames_ok[state]
-
-def combine_state(a, b):
-    return (max(a[0], b[0]), max(a[1], b[1]))
-
-# Refresh the menu at most once a second to reduce flicker
-refresh_interval=1.0
-
-# Workaround to rumps brokenness;
-# see https://github.com/jaredks/rumps/issues/59
-def notification_workaround(title, subtitle, message):
-    try:
-        NSDictionary = objc.lookUpClass("NSDictionary")
-        d=NSDictionary()
-
-        rumps.notification(title, subtitle, message, data=d)
-    except Exception as err:
-        logger.exception("Failed to display notification")
-
-# Based on code snatched from
-# https://stackoverflow.com/questions/12523586/python-format-size-application-converting-b-to-kb-mb-gb-tb/37423778
-def humanbytes(B):
-   'Return the given bytes as a human friendly KB, MB, GB, or TB string'
-   B = float(B)
-   KB = float(1024)
-   MB = float(KB ** 2) # 1,048,576
-   GB = float(KB ** 3) # 1,073,741,824
-   TB = float(KB ** 4) # 1,099,511,627,776
-
-   if B < KB:
-      return '{0}B'.format(B)
-   elif KB <= B < MB:
-      return '{0:.2f}KB'.format(B/KB)
-   elif MB <= B < GB:
-      return '{0:.2f}MB'.format(B/MB)
-   elif GB <= B < TB:
-      return '{0:.2f}GB'.format(B/GB)
-   elif TB <= B:
-      return '{0:.2f}TB'.format(B/TB)
-
-def make_title(status):
-    def add_info(info, new):
-        if info:
-            return "%s; %s" % (info, new)
-        else:
-            return new
-
-    info=None
-    this_need_reconstruct=None
-
-    if not status.errors.ok():
-        info=add_info(info, str(status.errors))
-
-    if status.state==backup.State.SCHEDULED:
-        # Operation scheduled
-        when=status.when()
-        now=time.time()
-
-        if when<now:
-            whenstr='overdue'
-            info=''
-        else:
-            tnow=datetime.datetime.fromtimestamp(now)
-            twhen=datetime.datetime.fromtimestamp(when)
-            tendtoday=twhen.replace(hour=23,minute=59,second=59)
-            tendtomorrow=tendtoday+datetime.timedelta(days=1)
-            diff=datetime.timedelta(seconds=when-now)
-
-            if twhen>tendtomorrow:
-                whenday=datetime.date.fromtimestamp(when)
-                whenstr='on %s' % twhen.date().isoformat()
-                this_need_reconstruct=tendtoday+datetime.timedelta(seconds=1)
-            elif diff.seconds>=12*60*60: # 12 hours
-                whenstr='tomorrow'
-                this_need_reconstruct=twhen-datetime.timedelta(hours=12)
-            else:
-                twhen=time.localtime(when)
-                if twhen.tm_sec>30:
-                    # Round up minute display to avoid user confusion
-                    twhen=time.localtime(when+30)
-                whenstr='at %02d:%02d' % (twhen.tm_hour, twhen.tm_min)
-
-            this_info=''
-            if 'reason' in status.detail:
-                this_info=status.detail['reason'] + ' '
-
-            when_how_sched= "%s%s %s" % (this_info, status.operation, whenstr)
-
-            info=add_info(info, when_how_sched)
-
-    elif status.state==backup.State.QUEUED:
-        info=add_info(info, "queued")
-    elif status.state==backup.State.ACTIVE:
-        # Operation running
-        progress=''
-        d=status.detail
-        if 'progress_current' in d and 'progress_total' in d:
-            progress=' %d%%' % (d['progress_current']/d['progress_total'])
-        elif 'original_size' in d and 'deduplicated_size' in d:
-            progress=' %s→%s' % (humanbytes(d['original_size']),
-                                 humanbytes(d['deduplicated_size']))
-
-        howrunning = "running %s%s" % (status.operation, progress)
-
-        info=add_info(info, howrunning)
-    else:
-        pass
-
-    if info:
-        title=status.name + ' (' + info + ')'
-    else:
-        title=status.name
-
-    return title, (status.state, status.errors), this_need_reconstruct
-
-class BorgendTray(rumps.App):
-    def __init__(self, backups):
-        self.lock=Lock()
-        self.backups=backups
-        self.refresh_timer=None
-        self.refresh_timer_time=None
-        self.statuses=[None]*len(backups)
-
-        for index in range(len(backups)):
-            self.statuses[index]=backups[index].status()
-
-        menu, title=self.build_menu_and_timer()
-
-        super().__init__(title, menu=menu, quit_button=None)
-
-        for index in range(len(backups)):
-            # Python closures suck dog's balls; hence the _index=index hack
-            # See also http://math.andrej.com/2009/04/09/pythons-lambda-is-broken/
-            cb=(lambda status, errors=None, _index=index:
-                self.__status_callback(_index, status, errorlog=errors))
-            backups[index].set_status_update_callback(cb)
-
-        dreamtime.add_callback(self, self.refresh_ui)
-
-    def __rebuild_menu(self):
-        menu=[]
-        state=(backup.State.INACTIVE, backup.Errors.OK)
-        need_reconstruct=None
-        for index in range(len(self.backups)):
-            b=self.backups[index]
-            title, this_state, this_need_reconstruct=make_title(self.statuses[index])
-            # Python closures suck dog's balls...
-            # first and the last program I write in Python until somebody
-            # fixes this brain damage
-            cbm=lambda sender, _b=b: self.__menu_select_backup(sender, _b)
-            item=rumps.MenuItem(title, callback=cbm)
-            if not this_state[1].ok():
-                item.state=-1
-            elif this_state[0]==backup.State.SCHEDULED or this_state[0]==backup.State.QUEUED:
-                item.state=1
-            menu.append(item)
-            state=combine_state(state, this_state)
-
-            # Do we have to automatically update menu display?
-            if not need_reconstruct:
-                need_reconstruct=this_need_reconstruct
-            elif this_need_reconstruct:
-                need_reconstruct=min(need_reconstruct, this_need_reconstruct)
-
-        menu_log=rumps.MenuItem("Show log", callback=lambda _: showlog())
-        menu.append(menu_log)
-
-        if not settings['no_quit_menu_entry']:
-            menu_quit=rumps.MenuItem("Quit...", callback=lambda _: self.quit())
-            menu.append(menu_quit)
-
-        return menu, state, need_reconstruct
-
-    def build_menu_and_timer(self):
-        if self.refresh_timer:
-            self.refresh_timer.cancel()
-            self.refresh_timer=None
-            self.refresh_timer_time=None
-        logger.debug('Rebuilding menu')
-        menu, state, need_reconstruct=self.__rebuild_menu()
-        title=trayname(state)
-
-        if need_reconstruct:
-            when=time.mktime(need_reconstruct.timetuple())
-            delay=when-time.time()
-            self.refresh_timer=Timer(delay, self.refresh_ui)
-            self.refresh_timer_time=need_reconstruct
-            self.refresh_timer.start()
-
-        return menu, title
-
-    def refresh_ui(self):
-        with self.lock:
-            menu, title=self.build_menu_and_timer()
-            self.menu.clear()
-            self.menu.update(menu)
-            self.title=title
-
-    def __status_callback(self, index, status, errorlog=None):
-        logger.debug("Tray status callback")
-        with self.lock:
-            self.statuses[index]=status
-            # Time the refresh if it has not been timed, or if the timer
-            # is timing for the "long-term" (refresh_timer_time set)
-            if not self.refresh_timer or self.refresh_timer_time:
-                logger.debug("Timing refresh")
-                self.refresh_timer=Timer(refresh_interval, self.refresh_ui)
-                # refresh_timer_time is only set for "long-term timers"
-                self.refresh_timer_time=None
-                self.refresh_timer.start()
-
-        if errorlog:
-            if 'msgid' not in errorlog or not isinstance(errorlog['msgid'], str):
-                msgid='UnknownError'
-            else:
-                msgid=errorlog['msgid']
-
-            logger.debug("Opening notification for error %s '%s'",
-                         msgid, errorlog['message'])
-
-            notification_workaround(branding.appname_stylised,
-                                    msgid, errorlog['message'])
-
-    def quit(self):
-        rumps.quit_application()
-
-    def __menu_select_backup(self, sender, b):
-        #sender.state=not sender.state
-        logger.debug("Manually backup '%s'", b.name)
-        try:
-            b.create()
-        except Exception as err:
-            logger.exception("Failure to initialise backup")
-            notification_workaround(branding.appname_stylised,
-                                    err.__class__.__name__, str(err))
-
-#
-# Log window
-#
-
-logwindow=[None]
-logwindow_lock=Lock()
-
-def showlog():
-    try:
-        w=None
-        with logwindow_lock:
-            if not logwindow[0]:
-                lines=borgend.fifolog.formatAll()
-                msg="\n".join(lines[0:])
-                w=rumps.Window(title=borgend.appname_stylised+' log',
-                               default_text=msg,
-                               ok='Close',
-                               dimensions=(640,320))
-                logwindow[0]=w
-        if w:
-            try:
-                w.run()
-            finally:
-                with logwindow_lock:
-                    logwindow[0]=None
-    except Exception as err:
-        logger.exception("Failed to display log")
-
-#
-# Notification click response => show log window
-#
-
-@rumps.notifications
-def notification_center(_):
-    showlog()
-

mercurial