6 import time |
6 import time |
7 import datetime |
7 import datetime |
8 import logging |
8 import logging |
9 import borgend |
9 import borgend |
10 import utils |
10 import utils |
|
11 import backup |
11 from threading import Lock, Timer |
12 from threading import Lock, Timer |
12 from config import settings |
13 from config import settings |
13 import objc |
14 import objc |
14 |
15 |
15 logger=borgend.logger.getChild(__name__) |
16 logger=borgend.logger.getChild(__name__) |
16 |
17 |
17 INACTIVE=0 |
|
18 SCHEDULED_OK=1 |
|
19 ACTIVE=2 |
|
20 OFFLINE=3 |
|
21 ERRORS=4 |
|
22 |
|
23 traynames={ |
18 traynames={ |
24 INACTIVE: 'B.', |
19 backup.INACTIVE: 'B.', |
25 SCHEDULED_OK: 'B.', |
20 backup.SCHEDULED: 'B.', |
26 ACTIVE: 'B!', |
21 backup.ACTIVE: 'B!', |
27 OFFLINE: 'B⦙', |
22 backup.BUSY: 'B⦙', |
28 ERRORS: 'B?' |
23 backup.OFFLINE: 'B⦙', |
|
24 backup.ERRORS: 'B?' |
|
25 } |
|
26 |
|
27 statestring={ |
|
28 backup.INACTIVE: 'inactive', |
|
29 backup.SCHEDULED: 'scheduled', |
|
30 backup.ACTIVE: 'active', |
|
31 backup.BUSY: 'busy', |
|
32 backup.OFFLINE: 'offline', |
|
33 backup.ERRORS: 'errors' |
29 } |
34 } |
30 |
35 |
31 # Refresh the menu at most once a second to reduce flicker |
36 # Refresh the menu at most once a second to reduce flicker |
32 refresh_interval=1.0 |
37 refresh_interval=1.0 |
33 |
|
34 def combine_state(state1, state2): |
|
35 return max(state1, state2) |
|
36 |
38 |
37 # Workaround to rumps brokenness; |
39 # Workaround to rumps brokenness; |
38 # see https://github.com/jaredks/rumps/issues/59 |
40 # see https://github.com/jaredks/rumps/issues/59 |
39 def notification_workaround(title, subtitle, message): |
41 def notification_workaround(title, subtitle, message): |
40 try: |
42 try: |
86 if twhen.tm_sec>30: |
87 if twhen.tm_sec>30: |
87 # Round up minute display to avoid user confusion |
88 # Round up minute display to avoid user confusion |
88 twhen=time.localtime(when+30) |
89 twhen=time.localtime(when+30) |
89 whenstr='at %02d:%02d' % (twhen.tm_hour, twhen.tm_min) |
90 whenstr='at %02d:%02d' % (twhen.tm_hour, twhen.tm_min) |
90 |
91 |
91 if status['detail']=='normal': |
92 detail='' |
92 state=SCHEDULED_OK |
93 if state>=backup.BUSY and state in statestring: |
93 detail='' |
94 detail=statestring[state] + '; ' |
94 else: |
95 if status['detail']!='normal': |
95 if status['detail']=='retry': |
96 detail=detail+status['detail']+' ' |
96 state=ERRORS |
|
97 detail=status['detail']+' ' |
|
98 title="%s (%s%s %s)" % (status['name'], detail, status['operation'], whenstr) |
97 title="%s (%s%s %s)" % (status['name'], detail, status['operation'], whenstr) |
99 elif status['type']=='current': |
98 elif status['type']=='current': |
100 # Operation running |
99 # Operation running |
101 progress='' |
100 progress='' |
102 if 'progress_current' in status and 'progress_total' in status: |
101 if 'progress_current' in status and 'progress_total' in status: |
103 progress=' %d%%' % (status['progress_current']/status['progress_total']) |
102 progress=' %d%%' % (status['progress_current']/status['progress_total']) |
104 elif 'original_size' in status and 'deduplicated_size' in status: |
103 elif 'original_size' in status and 'deduplicated_size' in status: |
105 progress=' %s→%s' % (humanbytes(status['original_size']), |
104 progress=' %s→%s' % (humanbytes(status['original_size']), |
106 humanbytes(status['deduplicated_size'])) |
105 humanbytes(status['deduplicated_size'])) |
107 title="%s (running: %s%s)" % (status['name'], status['operation'], progress) |
106 title="%s (running: %s%s)" % (status['name'], status['operation'], progress) |
108 state=ACTIVE |
|
109 else: # status['type']=='nothing': |
107 else: # status['type']=='nothing': |
110 # Should be unscheduled, nothing running |
108 # Should be unscheduled, nothing running |
111 title=status['name'] |
109 detail='' |
112 if status['errors']: |
110 if state>=backup.BUSY and state in statestring: |
113 title=title + " (errors)" |
111 detail=' (' + statestring[state] + ')' |
114 |
112 title=status['name'] + detail |
115 if status['errors']: |
|
116 state=ERRORS |
|
117 |
113 |
118 return title, state |
114 return title, state |
119 |
115 |
120 class BorgendTray(rumps.App): |
116 class BorgendTray(rumps.App): |
121 def __init__(self, backups): |
117 def __init__(self, backups): |
145 |
141 |
146 super().__init__(traynames[state], menu=menu, quit_button=None) |
142 super().__init__(traynames[state], menu=menu, quit_button=None) |
147 |
143 |
148 def __rebuild_menu(self): |
144 def __rebuild_menu(self): |
149 menu=[] |
145 menu=[] |
150 state=INACTIVE |
146 state=backup.INACTIVE |
151 for index in range(len(self.backups)): |
147 for index in range(len(self.backups)): |
152 b=self.backups[index] |
148 b=self.backups[index] |
153 title, this_state=make_title(self.statuses[index]) |
149 title, this_state=make_title(self.statuses[index]) |
154 # Python closures suck dog's balls... |
150 # Python closures suck dog's balls... |
155 # first and the last program I write in Python until somebody |
151 # first and the last program I write in Python until somebody |
156 # fixes this brain damage |
152 # fixes this brain damage |
157 cbm=lambda sender, _b=b: self.__menu_select_backup(sender, _b) |
153 cbm=lambda sender, _b=b: self.__menu_select_backup(sender, _b) |
158 item=rumps.MenuItem(title, callback=cbm) |
154 item=rumps.MenuItem(title, callback=cbm) |
159 if this_state==SCHEDULED_OK: |
155 if this_state==backup.SCHEDULED: |
160 item.state=1 |
156 item.state=1 |
161 elif this_state>=OFFLINE: |
157 elif this_state>=backup.BUSY: |
162 item.state=-1 |
158 item.state=-1 |
163 menu.append(item) |
159 menu.append(item) |
164 state=combine_state(state, this_state) |
160 state=backup.combine_state(state, this_state) |
165 |
161 |
166 menu_log=rumps.MenuItem("Show log", callback=lambda _: showlog()) |
162 menu_log=rumps.MenuItem("Show log", callback=lambda _: showlog()) |
167 menu.append(menu_log) |
163 menu.append(menu_log) |
168 |
164 |
169 if not settings['no_quit_menu_entry']: |
165 if not settings['no_quit_menu_entry']: |