diff -r b075b3db3044 -r a409242121d5 ui.py --- 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 whentendtomorrow: - 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() -