|     12 from config import settings | 
    12 from config import settings | 
|     13 import objc | 
    13 import objc | 
|     14  | 
    14  | 
|     15 logger=borgend.logger.getChild(__name__) | 
    15 logger=borgend.logger.getChild(__name__) | 
|     16  | 
    16  | 
|     17 traynames={ | 
    17 traynames_ok={ | 
|     18     backup.INACTIVE: 'B.', | 
    18     backup.State.INACTIVE: 'B.', | 
|     19     backup.SCHEDULED: 'B.', | 
    19     backup.State.SCHEDULED: 'B.', | 
|     20     backup.QUEUED: 'B:', | 
    20     backup.State.QUEUED: 'B:', | 
|     21     backup.ACTIVE: 'B!', | 
    21     backup.State.ACTIVE: 'B!', | 
|     22     backup.BUSY: 'B⦙', | 
        | 
|     23     backup.OFFLINE: 'B⦙', | 
        | 
|     24     backup.ERRORS: 'B?' | 
        | 
|     25 } | 
    22 } | 
|     26  | 
    23  | 
|     27 statestring={ | 
    24 traynames_errors={ | 
|     28     backup.INACTIVE: 'inactive', | 
    25     # The first one should never be used | 
|     29     backup.SCHEDULED: 'scheduled', | 
    26     backup.Errors.OK: traynames_ok[backup.State.INACTIVE], | 
|     30     backup.QUEUED: 'queued', | 
    27     backup.Errors.BUSY: 'B⦙', | 
|     31     backup.ACTIVE: 'active', | 
    28     backup.Errors.OFFLINE: 'B⦙', | 
|     32     backup.BUSY: 'busy', | 
    29     backup.Errors.ERRORS: 'B?' | 
|     33     backup.OFFLINE: 'offline', | 
        | 
|     34     backup.ERRORS: 'errors' | 
        | 
|     35 } | 
    30 } | 
|         | 
    31  | 
|         | 
    32 def trayname(ste): | 
|         | 
    33     state=ste[0] | 
|         | 
    34     errors=ste[1] | 
|         | 
    35     if not errors.ok(): | 
|         | 
    36         return traynames_errors[errors] | 
|         | 
    37     else: | 
|         | 
    38         return traynames_ok[state] | 
|         | 
    39  | 
|         | 
    40 def combine_state(a, b): | 
|         | 
    41     return (max(a[0], b[0]), max(a[1], b[1])) | 
|     36  | 
    42  | 
|     37 # Refresh the menu at most once a second to reduce flicker | 
    43 # Refresh the menu at most once a second to reduce flicker | 
|     38 refresh_interval=1.0 | 
    44 refresh_interval=1.0 | 
|     39  | 
    45  | 
|     40 # Workaround to rumps brokenness; | 
    46 # Workaround to rumps brokenness; | 
|     68       return '{0:.2f}GB'.format(B/GB) | 
    74       return '{0:.2f}GB'.format(B/GB) | 
|     69    elif TB <= B: | 
    75    elif TB <= B: | 
|     70       return '{0:.2f}TB'.format(B/TB) | 
    76       return '{0:.2f}TB'.format(B/TB) | 
|     71  | 
    77  | 
|     72 def make_title(status): | 
    78 def make_title(status): | 
|     73     state=status['state'] | 
    79     def add_detail(detail, new): | 
|     74     detail='' | 
    80         if detail: | 
|     75     if status['type']=='scheduled': | 
    81             return "%s; %s" % (detail, new) | 
|         | 
    82         else: | 
|         | 
    83             return new | 
|         | 
    84  | 
|         | 
    85     errors=status['errors'] | 
|         | 
    86     detail=None | 
|         | 
    87  | 
|         | 
    88     if not errors.ok(): | 
|         | 
    89         detail=add_detail(detail, str(errors)) | 
|         | 
    90  | 
|         | 
    91     if status['state']==backup.State.SCHEDULED: | 
|     76         # Operation scheduled | 
    92         # Operation scheduled | 
|     77         when=status['when'] | 
    93         when=status['when'] | 
|     78         now=time.time() | 
    94         now=time.time() | 
|     79         if when<now: | 
    95         if when<now: | 
|     80             whenstr='overdue' | 
    96             whenstr='overdue' | 
|     89                 if twhen.tm_sec>30: | 
   105                 if twhen.tm_sec>30: | 
|     90                     # Round up minute display to avoid user confusion | 
   106                     # Round up minute display to avoid user confusion | 
|     91                     twhen=time.localtime(when+30) | 
   107                     twhen=time.localtime(when+30) | 
|     92                 whenstr='at %02d:%02d' % (twhen.tm_hour, twhen.tm_min) | 
   108                 whenstr='at %02d:%02d' % (twhen.tm_hour, twhen.tm_min) | 
|     93  | 
   109  | 
|     94             detail='' | 
   110             this_detail='' | 
|     95             if state>=backup.BUSY and state in statestring: | 
        | 
|     96                 detail=statestring[state] + '; ' | 
        | 
|     97             if status['detail']!='normal': | 
   111             if status['detail']!='normal': | 
|     98                 detail=detail+status['detail']+' ' | 
   112                 this_detail=status['detail'] + ' ' | 
|     99         title="%s (%s%s %s)" % (status['name'], detail, status['operation'], whenstr) | 
   113  | 
|    100     elif status['type']=='queued': | 
   114             when_how_sched= "%s%s %s" % (this_detail, status['operation'], whenstr) | 
|    101         title="%s (queued)" % status['name'] | 
   115  | 
|    102     elif status['type']=='current': | 
   116             detail=add_detail(detail, when_how_sched) | 
|         | 
   117  | 
|         | 
   118     elif status['state']==backup.State.QUEUED: | 
|         | 
   119         detail=add_detail(detail, "queued") | 
|         | 
   120     elif status['state']==backup.State.ACTIVE: | 
|    103         # Operation running | 
   121         # Operation running | 
|    104         progress='' | 
   122         progress='' | 
|    105         if 'progress_current' in status and 'progress_total' in status: | 
   123         if 'progress_current' in status and 'progress_total' in status: | 
|    106             progress=' %d%%' % (status['progress_current']/status['progress_total']) | 
   124             progress=' %d%%' % (status['progress_current']/status['progress_total']) | 
|    107         elif 'original_size' in status and 'deduplicated_size' in status: | 
   125         elif 'original_size' in status and 'deduplicated_size' in status: | 
|    108             progress=' %s→%s' % (humanbytes(status['original_size']), | 
   126             progress=' %s→%s' % (humanbytes(status['original_size']), | 
|    109                                  humanbytes(status['deduplicated_size'])) | 
   127                                  humanbytes(status['deduplicated_size'])) | 
|    110         title="%s (running: %s%s)" % (status['name'], status['operation'], progress) | 
   128  | 
|    111     else: # status['type']=='nothing': | 
   129         howrunning = "running %s%s" % (status['operation'], progress) | 
|    112         # Should be unscheduled, nothing running | 
   130  | 
|    113         detail='' | 
   131         detail=add_detail(detail, howrunning) | 
|    114         if state>=backup.BUSY and state in statestring: | 
   132     else: | 
|    115             detail=' (' + statestring[state] + ')' | 
   133         pass | 
|    116         title=status['name'] + detail | 
   134  | 
|    117  | 
   135     if detail: | 
|    118     return title, state | 
   136         title=status['name'] + ' (' + detail + ')' | 
|         | 
   137     else: | 
|         | 
   138         title=status['name'] | 
|         | 
   139  | 
|         | 
   140     return title, (status['state'], status['errors']) | 
|    119  | 
   141  | 
|    120 class BorgendTray(rumps.App): | 
   142 class BorgendTray(rumps.App): | 
|    121     def __init__(self, backups): | 
   143     def __init__(self, backups): | 
|    122         self.lock=Lock() | 
   144         self.lock=Lock() | 
|    123         self.backups=backups | 
   145         self.backups=backups | 
|    141  | 
   163  | 
|    142             menu, state=self.__rebuild_menu() | 
   164             menu, state=self.__rebuild_menu() | 
|    143  | 
   165  | 
|    144             self.refresh_timer=None | 
   166             self.refresh_timer=None | 
|    145  | 
   167  | 
|    146             super().__init__(traynames[state], menu=menu, quit_button=None) | 
   168             super().__init__(trayname(state), menu=menu, quit_button=None) | 
|    147  | 
   169  | 
|    148     def __rebuild_menu(self): | 
   170     def __rebuild_menu(self): | 
|    149         menu=[] | 
   171         menu=[] | 
|    150         state=backup.INACTIVE | 
   172         state=(backup.State.INACTIVE, backup.Errors.OK) | 
|    151         for index in range(len(self.backups)): | 
   173         for index in range(len(self.backups)): | 
|    152             b=self.backups[index] | 
   174             b=self.backups[index] | 
|    153             title, this_state=make_title(self.statuses[index]) | 
   175             title, this_state=make_title(self.statuses[index]) | 
|    154             # Python closures suck dog's balls... | 
   176             # Python closures suck dog's balls... | 
|    155             # first and the last program I write in Python until somebody | 
   177             # first and the last program I write in Python until somebody | 
|    156             # fixes this brain damage | 
   178             # fixes this brain damage | 
|    157             cbm=lambda sender, _b=b: self.__menu_select_backup(sender, _b) | 
   179             cbm=lambda sender, _b=b: self.__menu_select_backup(sender, _b) | 
|    158             item=rumps.MenuItem(title, callback=cbm) | 
   180             item=rumps.MenuItem(title, callback=cbm) | 
|    159             if this_state==backup.SCHEDULED: | 
   181             if not this_state[1].ok(): | 
|         | 
   182                 item.state=-1 | 
|         | 
   183             elif this_state[0]==backup.State.SCHEDULED or this_state[0]==backup.State.QUEUED: | 
|    160                 item.state=1 | 
   184                 item.state=1 | 
|    161             elif this_state>=backup.BUSY: | 
        | 
|    162                 item.state=-1 | 
        | 
|    163             menu.append(item) | 
   185             menu.append(item) | 
|    164             state=backup.combine_state(state, this_state) | 
   186             state=combine_state(state, this_state) | 
|    165  | 
   187  | 
|    166         menu_log=rumps.MenuItem("Show log", callback=lambda _: showlog()) | 
   188         menu_log=rumps.MenuItem("Show log", callback=lambda _: showlog()) | 
|    167         menu.append(menu_log) | 
   189         menu.append(menu_log) | 
|    168  | 
   190  | 
|    169         if not settings['no_quit_menu_entry']: | 
   191         if not settings['no_quit_menu_entry']: | 
|    174  | 
   196  | 
|    175     def refresh_ui(self): | 
   197     def refresh_ui(self): | 
|    176         with self.lock: | 
   198         with self.lock: | 
|    177             self.refresh_timer=None | 
   199             self.refresh_timer=None | 
|    178             logger.debug('Rebuilding menu') | 
   200             logger.debug('Rebuilding menu') | 
|    179             menu, active=self.__rebuild_menu() | 
   201             menu, state=self.__rebuild_menu() | 
|    180             self.menu.clear() | 
   202             self.menu.clear() | 
|    181             self.menu.update(menu) | 
   203             self.menu.update(menu) | 
|    182             self.title=traynames[active] | 
   204             self.title=trayname(state) | 
|    183  | 
   205  | 
|    184     def __status_callback(self, index, status, errorlog=None): | 
   206     def __status_callback(self, index, status, errorlog=None): | 
|    185         logger.debug('Status callback: %s' % str(status)) | 
   207         logger.debug('Status callback: %s' % str(status)) | 
|    186  | 
   208  | 
|    187         with self.lock: | 
   209         with self.lock: |